fix(io): refuse truncated files in the VOL, async and MPI readers

The truncated-file check and the end-of-file clamp reached File,
LazyFile and MmapFile but not clawhdf5-io's readers, which still opened
truncated files and read past the recorded end of file. NativeVol
(open, and read_dataset for from_bytes), AsyncHDF5File::from_bytes and
MpiVol's collective read now view the file through the new
vol::hdf5_view: from the superblock to Superblock::data_end, refusing a
file shorter than that.

MpiVol's read is compiled only with the mpi-io feature, which needs an
MPI installation; it was not built here. The edit there only swaps its
two-line superblock setup for hdf5_view.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 01:30:56 -05:00
co-authored by Claude Opus 5.5
parent dd40bea467
commit 3938f7f8a2
4 changed files with 88 additions and 12 deletions
+52 -5
View File
@@ -210,6 +210,24 @@ pub struct NativeVol {
location: Option<String>,
}
/// The HDF5 bytes of a whole file and its superblock: from the superblock
/// (addresses are relative to it, so any user block is skipped) to the end
/// of file the superblock records. A file shorter than that is truncated
/// and refused, and nothing past it is read, as in libhdf5.
pub(crate) fn hdf5_view(
whole: &[u8],
) -> Result<(&[u8], clawhdf5_format::superblock::Superblock), VolError> {
use clawhdf5_format::{signature::split_user_block, superblock::Superblock};
let err = |e: clawhdf5_format::error::FormatError| VolError::DataError(e.to_string());
let (user_block, data) = split_user_block(whole).map_err(err)?;
let sb = Superblock::parse(data, 0).map_err(err)?;
let end = sb
.data_end(user_block.len() as u64, whole.len() as u64)
.map_err(err)?;
// data_end is at most the file length less the user block.
Ok((&data[..end as usize], sb))
}
impl NativeVol {
/// Create a new native VOL connector.
pub fn new() -> Self {
@@ -264,6 +282,8 @@ impl VirtualObjectLayer for NativeVol {
fn open(&mut self, location: &str) -> Result<(), VolError> {
let data = std::fs::read(location)?;
// Refuse a truncated file at open, as libhdf5 does.
hdf5_view(&data)?;
self.data = Some(data);
self.location = Some(location.to_string());
Ok(())
@@ -283,13 +303,10 @@ impl VirtualObjectLayer for NativeVol {
use clawhdf5_format::{
data_layout::DataLayout, data_read::read_raw_data_full, dataspace::Dataspace,
datatype::Datatype, filter_pipeline::FilterPipeline, group_v2::resolve_path_any,
message_type::MessageType, object_header::ObjectHeader, signature::split_user_block,
superblock::Superblock,
message_type::MessageType, object_header::ObjectHeader,
};
// Addresses are relative to the superblock: skip any user block.
let (_, data) = split_user_block(data).map_err(|e| VolError::DataError(e.to_string()))?;
let sb = Superblock::parse(data, 0).map_err(|e| VolError::DataError(e.to_string()))?;
let (data, sb) = hdf5_view(data)?;
let addr = resolve_path_any(data, &sb, path)
.map_err(|e| VolError::NotFound(format!("{path}: {e}")))?;
@@ -387,6 +404,36 @@ mod tests {
assert!(vol.as_bytes().is_none());
}
/// As libhdf5 does: a file shorter than the end of file its superblock
/// records is truncated and refused (at open, and when read from
/// memory), and bytes appended past that end are not part of the file.
#[test]
fn native_vol_refuses_truncated_files_and_ignores_trailing_bytes() {
use clawhdf5_format::file_writer::FileWriter as FmtWriter;
let mut fw = FmtWriter::new();
fw.create_dataset("x").with_f64_data(&[1.0, 2.0, 3.0]);
let bytes = fw.finish().unwrap();
let truncated = bytes[..bytes.len() - 8].to_vec();
let err = NativeVol::from_bytes(truncated.clone())
.read_dataset("x")
.unwrap_err();
assert!(err.to_string().contains("truncated"), "{err}");
let dir = std::env::temp_dir().join(format!("clawhdf5_vol_trunc_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("truncated.h5");
std::fs::write(&path, &truncated).unwrap();
let err = NativeVol::open_path(path.to_str().unwrap()).err().unwrap();
assert!(err.to_string().contains("truncated"), "{err}");
std::fs::remove_dir_all(&dir).ok();
let mut appended = bytes.clone();
appended.extend_from_slice(&[0xAB; 64]);
let raw = NativeVol::from_bytes(appended).read_dataset("x").unwrap();
assert_eq!(raw.len(), 24);
}
#[test]
fn vol_error_display() {
let err = VolError::Unsupported("read_dataset".into());