use std::fmt;
use std::marker::PhantomData;
use std::ops::Deref;
use std::sync::Arc;
use crate::io::file_handle::LockedMap;
use crate::io::reader::{DatasetViewSource, ViewStorage};
use crate::types::H5Type;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ViewRefusal {
NotMapped,
Layout(&'static str),
Unallocated,
ElementSize {
view: usize,
stored: usize,
},
ElementImage(&'static str),
Alignment {
offset: u64,
align: usize,
},
PastMappedEnd {
end: u64,
mapped: u64,
},
Range(String),
}
impl fmt::Display for ViewRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotMapped => write!(f, "the file is not memory-mapped"),
Self::Layout(what) => write!(f, "{what}"),
Self::Unallocated => write!(
f,
"it has no storage allocated; every element reads as the fill value"
),
Self::ElementSize { view, stored } => write!(
f,
"the view type is {view} bytes wide but the stored element is {stored}"
),
Self::ElementImage(what) => write!(
f,
"the stored elements are not the host image of the view type: {what}"
),
Self::Alignment { offset, align } => write!(
f,
"its data starts at file offset {offset}, which is not a multiple of the \
view type's {align}-byte alignment"
),
Self::PastMappedEnd { end, mapped } => write!(
f,
"its image ends at {end} but the map holds {mapped} bytes"
),
Self::Range(what) => write!(f, "{what}"),
}
}
}
pub(crate) enum ViewRange<'a> {
Whole,
Slab {
starts: &'a [usize],
counts: &'a [usize],
},
}
pub(crate) fn view<T: H5Type>(
src: &DatasetViewSource,
range: ViewRange<'_>,
) -> Result<MappedView<T>, ViewRefusal> {
let Some(map) = src.map.as_ref() else {
return Err(ViewRefusal::NotMapped);
};
let (offset, bytes) = match src.storage {
ViewStorage::Contiguous { offset, len } => (offset, len),
ViewStorage::Unallocated => return Err(ViewRefusal::Unallocated),
ViewStorage::Elsewhere(what) => return Err(ViewRefusal::Layout(what)),
};
let width = T::element_size();
let stored = src.datatype.element_size() as usize;
if width != stored {
return Err(ViewRefusal::ElementSize {
view: width,
stored,
});
}
if let Some(what) = crate::dataset::stored_image_mismatch(&src.datatype, width) {
return Err(ViewRefusal::ElementImage(what));
}
let (offset, count) = match range {
ViewRange::Whole => (offset, bytes / width as u64),
ViewRange::Slab { starts, counts } => {
let (first, count) = slab_run(&src.dims, starts, counts)?;
let skip = first.checked_mul(width as u64).ok_or_else(|| {
ViewRefusal::Range(format!(
"the range starts at element {first}, whose byte offset does not fit \
in the address space"
))
})?;
let offset = offset.checked_add(skip).ok_or_else(|| {
ViewRefusal::Range(format!(
"the range starts {skip} bytes into a dataset at file offset {offset}, \
which does not fit in the address space"
))
})?;
(offset, count)
}
};
MappedView::new(Arc::clone(map), offset, count)
}
fn slab_run(dims: &[u64], starts: &[usize], counts: &[usize]) -> Result<(u64, u64), ViewRefusal> {
if starts.len() != dims.len() || counts.len() != dims.len() {
return Err(ViewRefusal::Range(format!(
"the dataset has {} dimension(s) but the range names {} start(s) and {} count(s)",
dims.len(),
starts.len(),
counts.len()
)));
}
for (d, dim) in dims.iter().enumerate() {
let end = (starts[d] as u64).checked_add(counts[d] as u64);
if end.is_none_or(|e| e > *dim) {
return Err(ViewRefusal::Range(format!(
"dimension {d} holds {dim} element(s) but the range asks for {} from {}",
counts[d], starts[d]
)));
}
}
let mut count = 1u64;
for &c in counts {
count = count.checked_mul(c as u64).ok_or_else(|| {
ViewRefusal::Range("the range holds more elements than fit in a count".into())
})?;
}
if count != 0 {
let mut d = dims.len();
while d > 0 && counts[d - 1] as u64 == dims[d - 1] {
d -= 1;
}
if d > 0 {
for (i, &c) in counts.iter().enumerate().take(d - 1) {
if c != 1 {
return Err(ViewRefusal::Range(format!(
"the range is not one contiguous run of the stored image: it takes \
{c} indices along dimension {i} while dimension {} is only \
partially selected",
d - 1
)));
}
}
}
}
let mut first = 0u64;
let mut stride = 1u64;
for d in (0..dims.len()).rev() {
first = first.saturating_add((starts[d] as u64).saturating_mul(stride));
stride = stride.saturating_mul(dims[d]);
}
Ok((first, count))
}
pub struct MappedView<T> {
map: Arc<LockedMap>,
start: usize,
count: usize,
_elem: PhantomData<T>,
}
impl<T: H5Type> MappedView<T> {
fn new(map: Arc<LockedMap>, offset: u64, count: u64) -> Result<Self, ViewRefusal> {
let mapped = map.len();
let past = |end: u64| ViewRefusal::PastMappedEnd {
end,
mapped: mapped as u64,
};
let bytes_u64 = count.saturating_mul(std::mem::size_of::<T>() as u64);
let claimed_end = offset.saturating_add(bytes_u64);
let fits = usize::try_from(offset)
.ok()
.zip(usize::try_from(bytes_u64).ok())
.zip(usize::try_from(count).ok())
.filter(|((start, bytes), _)| start.checked_add(*bytes).is_some_and(|e| e <= mapped));
let Some(((start, _), count)) = fits else {
return Err(past(claimed_end));
};
if !map[start..].as_ptr().cast::<T>().is_aligned() {
return Err(ViewRefusal::Alignment {
offset,
align: std::mem::align_of::<T>(),
});
}
Ok(Self {
map,
start,
count,
_elem: PhantomData,
})
}
}
impl<T> Deref for MappedView<T> {
type Target = [T];
fn deref(&self) -> &[T] {
unsafe {
std::slice::from_raw_parts(self.map[self.start..].as_ptr().cast::<T>(), self.count)
}
}
}
impl<T> AsRef<[T]> for MappedView<T> {
fn as_ref(&self) -> &[T] {
self
}
}
impl<T: fmt::Debug> fmt::Debug for MappedView<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MappedView")
.field("offset", &self.start)
.field("elements", &self.count)
.field("data", &&**self)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::file::{borrow_inner_mut, H5FileInner};
use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
use crate::{FileLocking, FileSpaceStrategy, H5Dataset, H5File, Hdf5Error};
use std::path::PathBuf;
fn temp_path(name: &str) -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"hdf5_mapped_view_{}_{}_{}.h5",
name,
std::process::id(),
n
))
}
fn refusal<T: H5Type>(ds: &H5Dataset) -> ViewRefusal {
match ds.read_mapped::<T>() {
Err(Hdf5Error::NotViewable(reason)) => reason,
Err(other) => panic!("expected a view refusal, got: {other}"),
Ok(view) => panic!("expected a view refusal, got {} elements", view.len()),
}
}
#[test]
fn a_view_matches_read_raw_bit_for_bit() {
let path = temp_path("differential");
let f64s: Vec<f64> = (0..24).map(|i| i as f64 * -0.5).collect();
let i32s: Vec<i32> = (0..12).map(|i| i * 7 - 30).collect();
let u8s: Vec<u8> = (0..24).map(|i| (i * 11) as u8).collect();
let u16s: Vec<u16> = (0..6).map(|i| (i * 4097) as u16).collect();
{
let file = H5File::create(&path).unwrap();
file.new_dataset::<f64>()
.shape([24usize])
.create("flat")
.unwrap()
.write_raw(&f64s)
.unwrap();
file.new_dataset::<i32>()
.shape([3usize, 4])
.create("grid")
.unwrap()
.write_raw(&i32s)
.unwrap();
file.new_dataset::<u8>()
.shape([2usize, 3, 4])
.create("cube")
.unwrap()
.write_raw(&u8s)
.unwrap();
file.new_dataset::<u16>()
.shape([1usize, 6])
.create("row")
.unwrap()
.write_raw(&u16s)
.unwrap();
file.close().unwrap();
}
let file = H5File::open(&path).unwrap();
for name in ["flat", "grid", "cube", "row"] {
let ds = file.dataset(name).unwrap();
match name {
"flat" => {
let view = ds.read_mapped::<f64>().unwrap();
assert_eq!(&*view, &ds.read_raw::<f64>().unwrap()[..], "{name}");
assert_eq!(&*view, &f64s[..], "{name}");
}
"grid" => {
let view = ds.read_mapped::<i32>().unwrap();
assert_eq!(&*view, &ds.read_raw::<i32>().unwrap()[..], "{name}");
assert_eq!(&*view, &i32s[..], "{name}");
}
"cube" => {
let view = ds.read_mapped::<u8>().unwrap();
assert_eq!(&*view, &ds.read_raw::<u8>().unwrap()[..], "{name}");
assert_eq!(&*view, &u8s[..], "{name}");
}
_ => {
let view = ds.read_mapped::<u16>().unwrap();
assert_eq!(&*view, &ds.read_raw::<u16>().unwrap()[..], "{name}");
assert_eq!(&*view, &u16s[..], "{name}");
}
}
}
drop(file);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_range_view_matches_read_slice_where_the_run_is_one_piece() {
let path = temp_path("range");
let vals: Vec<i32> = (0..20).collect();
{
let file = H5File::create(&path).unwrap();
file.new_dataset::<i32>()
.shape([4usize, 5])
.create("grid")
.unwrap()
.write_raw(&vals)
.unwrap();
file.close().unwrap();
}
let file = H5File::open(&path).unwrap();
let ds = file.dataset("grid").unwrap();
for (starts, counts) in [
(vec![0usize, 0], vec![4usize, 5]),
(vec![1, 0], vec![2, 5]),
(vec![2, 1], vec![1, 3]),
(vec![3, 4], vec![1, 1]),
(vec![1, 2], vec![0, 0]),
] {
let view = ds.read_mapped_slice::<i32>(&starts, &counts).unwrap();
let copied = ds.read_slice::<i32>(&starts, &counts).unwrap();
assert_eq!(&*view, &copied[..], "{starts:?} {counts:?}");
}
let reason = match ds.read_mapped_slice::<i32>(&[1, 1], &[2, 3]) {
Err(Hdf5Error::NotViewable(r)) => r,
other => panic!("expected a refusal, got {:?}", other.map(|v| v.len())),
};
assert!(
matches!(&reason, ViewRefusal::Range(m) if m.contains("not one contiguous run")),
"unexpected refusal: {reason}"
);
assert!(matches!(
ds.read_mapped_slice::<i32>(&[0], &[4]),
Err(Hdf5Error::NotViewable(ViewRefusal::Range(_)))
));
assert!(matches!(
ds.read_mapped_slice::<i32>(&[0, 0], &[5, 5]),
Err(Hdf5Error::NotViewable(ViewRefusal::Range(_)))
));
drop(file);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_layout_that_is_not_one_stretch_of_the_file_is_refused() {
let path = temp_path("layout");
let vals: Vec<f64> = (0..64).map(|i| i as f64).collect();
{
let file = H5File::create(&path).unwrap();
file.new_dataset::<f64>()
.shape([64usize])
.chunk(&[16])
.create("chunked")
.unwrap()
.write_raw(&vals)
.unwrap();
file.new_dataset::<f64>()
.shape([8usize])
.compact()
.create("compact")
.unwrap()
.write_raw(&vals[..8])
.unwrap();
file.close().unwrap();
}
let file = H5File::open(&path).unwrap();
assert_eq!(
refusal::<f64>(&file.dataset("chunked").unwrap()),
ViewRefusal::Layout("it is chunked")
);
assert_eq!(
refusal::<f64>(&file.dataset("compact").unwrap()),
ViewRefusal::Layout("its raw data is compact, stored inside the object header")
);
drop(file);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_dataset_with_no_storage_is_refused() {
let path = temp_path("unallocated");
{
let file = H5File::create(&path).unwrap();
file.new_dataset::<f64>().null().create("nothing").unwrap();
file.close().unwrap();
}
let file = H5File::open(&path).unwrap();
let ds = file.dataset("nothing").unwrap();
assert_eq!(refusal::<f64>(&ds), ViewRefusal::Unallocated);
assert_eq!(ds.read_raw::<f64>().unwrap(), Vec::<f64>::new());
drop(file);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_view_type_of_the_wrong_width_is_refused() {
let path = temp_path("width");
{
let file = H5File::create(&path).unwrap();
file.new_dataset::<f64>()
.shape([8usize])
.create("doubles")
.unwrap()
.write_raw(&[1.0f64; 8])
.unwrap();
file.close().unwrap();
}
let file = H5File::open(&path).unwrap();
let ds = file.dataset("doubles").unwrap();
assert_eq!(
refusal::<f32>(&ds),
ViewRefusal::ElementSize { view: 4, stored: 8 }
);
drop(file);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_foreign_byte_order_dataset_is_refused() {
let path = temp_path("byte_order");
let vals: Vec<f64> = (0..8).map(|i| i as f64 + 0.25).collect();
let mut be = DatatypeMessage::f64_type();
if let DatatypeMessage::FloatingPoint { byte_order, .. } = &mut be {
*byte_order = ByteOrder::BigEndian;
}
{
let file = H5File::create(&path).unwrap();
file.new_dataset::<f64>()
.shape([8usize])
.datatype(be)
.create("big")
.unwrap()
.write_raw(&vals)
.unwrap();
file.close().unwrap();
}
let file = H5File::open(&path).unwrap();
let ds = file.dataset("big").unwrap();
assert_eq!(
refusal::<f64>(&ds),
ViewRefusal::ElementImage("they are stored in the foreign byte order")
);
assert_eq!(ds.read_raw::<f64>().unwrap(), vals);
drop(file);
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_misaligned_data_offset_is_refused() {
let path = temp_path("alignment");
{
let file = H5File::options()
.file_space(FileSpaceStrategy::Page, true, 1)
.file_space_page_size(512)
.create(&path)
.unwrap();
file.new_dataset::<u8>()
.shape([7usize])
.create("shim")
.unwrap()
.write_raw(&[1u8; 7])
.unwrap();
file.new_dataset::<f64>()
.shape([8usize])
.create("doubles")
.unwrap()
.write_raw(&[2.0f64; 8])
.unwrap();
file.close().unwrap();
}
let file = H5File::open(&path).unwrap();
let ds = file.dataset("doubles").unwrap();
let reason = refusal::<f64>(&ds);
assert!(
matches!(reason, ViewRefusal::Alignment { align: 8, offset } if offset % 8 != 0),
"unexpected refusal: {reason}"
);
assert_eq!(ds.read_raw::<f64>().unwrap(), vec![2.0f64; 8]);
drop(file);
let _ = std::fs::remove_file(&path);
}
#[test]
fn an_image_past_the_end_of_the_map_is_refused() {
let path = temp_path("truncated");
{
let file = H5File::options()
.file_space(FileSpaceStrategy::Page, true, 1)
.file_space_page_size(512)
.create(&path)
.unwrap();
file.new_dataset::<f64>()
.shape([64usize])
.create("doubles")
.unwrap()
.write_raw(&[3.0f64; 64])
.unwrap();
file.close().unwrap();
}
let full = std::fs::metadata(&path).unwrap().len();
std::fs::OpenOptions::new()
.write(true)
.open(&path)
.unwrap()
.set_len(full - 64)
.unwrap();
let file = H5File::open(&path).unwrap();
let ds = file.dataset("doubles").unwrap();
assert_eq!(
refusal::<f64>(&ds),
ViewRefusal::PastMappedEnd {
end: full,
mapped: full - 64
}
);
drop(file);
let _ = std::fs::remove_file(&path);
}
#[test]
fn an_unmapped_file_is_refused() {
let src = DatasetViewSource {
map: None,
storage: ViewStorage::Contiguous {
offset: 2048,
len: 64,
},
datatype: DatatypeMessage::f64_type(),
dims: vec![8],
};
assert_eq!(
view::<f64>(&src, ViewRange::Whole).err(),
Some(ViewRefusal::NotMapped)
);
}
#[test]
fn a_view_keeps_the_shared_lock_its_map_was_taken_under() {
let path = temp_path("view_lock");
let data: Vec<f64> = (0..8).map(|i| i as f64).collect();
{
let file = H5File::create(&path).unwrap();
file.new_dataset::<f64>()
.shape([8usize])
.create("d")
.unwrap()
.write_raw(&data)
.unwrap();
file.close().unwrap();
}
let file = H5File::options()
.locking(FileLocking::Enabled)
.open(&path)
.unwrap();
let view = file.dataset("d").unwrap().read_mapped::<f64>().unwrap();
drop(file);
assert!(
H5File::options()
.locking(FileLocking::Enabled)
.open_rw(&path)
.is_err(),
"a writer opened the file under a live view"
);
assert_eq!(&*view, &data[..]);
drop(view);
H5File::options()
.locking(FileLocking::Enabled)
.open_rw(&path)
.unwrap()
.close()
.unwrap();
let _ = std::fs::remove_file(&path);
}
#[cfg(not(windows))]
#[test]
fn a_view_outlives_the_file_and_a_refresh_that_retakes_the_map() {
let path = temp_path("snapshot");
let first: Vec<f64> = (0..8).map(|i| i as f64).collect();
let second: Vec<f64> = (0..8).map(|i| 100.0 + i as f64).collect();
{
let file = H5File::create(&path).unwrap();
file.new_dataset::<f64>()
.shape([8usize])
.create("first")
.unwrap()
.write_raw(&first)
.unwrap();
file.close().unwrap();
}
let mapped_len = std::fs::metadata(&path).unwrap().len();
let file = H5File::open(&path).unwrap();
let ds = file.dataset("first").unwrap();
let view = ds.read_mapped::<f64>().unwrap();
{
let w = H5File::options().no_locking().open_rw(&path).unwrap();
w.new_dataset::<f64>()
.shape([8usize])
.create("second")
.unwrap()
.write_raw(&second)
.unwrap();
w.close().unwrap();
}
assert!(std::fs::metadata(&path).unwrap().len() > mapped_len);
assert!(
file.dataset("second").is_err(),
"reader saw the write early"
);
{
let mut inner = borrow_inner_mut(&file.inner);
let H5FileInner::Reader(reader) = &mut *inner else {
panic!("not a reader");
};
reader.refresh().unwrap();
}
assert_eq!(&*view, &first[..]);
let fresh = file.dataset("second").unwrap();
assert_eq!(&*fresh.read_mapped::<f64>().unwrap(), &second[..]);
drop(fresh);
drop(ds);
drop(file);
assert_eq!(&*view, &first[..]);
let _ = std::fs::remove_file(&path);
}
}