feat(format): read the other attributes when one cannot be read

attrs() read every attribute of an object through extract_attributes_full,
so one attribute it could not read (a corrupt or unsupported attribute
message, or a heap object it could not locate) failed all of them — the
same shape as the huge-object bug, where one 8 KiB attribute hid every
attribute on a NetCDF file's root group.

- clawhdf5-format: new attribute::extract_attributes_tolerant returns the
  attributes it could read plus one error per attribute it could not.
  Errors in the attribute index itself (Attribute Info message, dense
  heap header, B-tree) still fail, since then it is unknown which
  attributes exist. extract_attributes_full is unchanged (strict); both
  share one implementation.
- clawhdf5: attrs() on Group/Dataset, MmapGroup/MmapDataset and
  LazyGroup/LazyDataset leaves an unreadable attribute out (documented),
  and the new attrs_with_errors() returns the map with the per-attribute
  errors. A value is either returned complete or not at all.

Regression test: one_unreadable_attribute_does_not_hide_the_others (h5py
writes 11 dense attributes; one message's version byte is corrupted;
before: attrs() failed with InvalidAttributeVersion(127), after: the 10
others come back with their values and one error is reported).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-25 22:03:09 -05:00
co-authored by Claude Opus 5.5
parent aadfd18d4c
commit d54a0f4737
8 changed files with 290 additions and 97 deletions
+8
View File
@@ -337,6 +337,14 @@
old-style (symbol table) groups a soft link made the listing fail. New old-style (symbol table) groups a soft link made the listing fail. New
`group_v2::resolve_group_children` / `resolve_path_from` and `group_v2::resolve_group_children` / `resolve_path_from` and
`group_v1::v1_soft_links` in `clawhdf5-format`. `group_v1::v1_soft_links` in `clawhdf5-format`.
- `clawhdf5` — one unreadable attribute no longer fails `attrs()` for every
attribute on its object: it is left out of the map, and the new
`attrs_with_errors()` (on every group and dataset handle) returns the map
plus one error per attribute left out. Returned values are always complete.
An error in the attribute index itself (attribute info message, dense heap
header or B-tree) still fails the call. `clawhdf5-format` gains
`attribute::extract_attributes_tolerant`; `extract_attributes_full` stays
strict.
- `clawhdf5-format` writer — **files libhdf5 rejects or reads wrong:** - `clawhdf5-format` writer — **files libhdf5 rejects or reads wrong:**
- Extensible Array (one unlimited dimension): chunks from index 244 on were - Extensible Array (one unlimited dimension): chunks from index 244 on were
written but never indexed and read as 0, by libhdf5 and by us. written but never indexed and read as 0, by libhdf5 and by us.
+82 -32
View File
@@ -394,42 +394,80 @@ pub fn find_attribute<'a>(
/// ///
/// Use this instead of `extract_attributes` when reading files that may use dense storage /// Use this instead of `extract_attributes` when reading files that may use dense storage
/// (e.g., objects with many attributes, typically >8). /// (e.g., objects with many attributes, typically >8).
///
/// Fails if any attribute cannot be read; see [`extract_attributes_tolerant`]
/// to read the others.
pub fn extract_attributes_full( pub fn extract_attributes_full(
file_data: &[u8], file_data: &[u8],
header: &ObjectHeader, header: &ObjectHeader,
offset_size: u8, offset_size: u8,
length_size: u8, length_size: u8,
) -> Result<Vec<AttributeMessage>, FormatError> {
extract_attributes_with(file_data, header, offset_size, length_size, &mut Err)
}
/// Like [`extract_attributes_full`], but an attribute that cannot be read
/// (a corrupt or unsupported attribute message, or a heap object that cannot
/// be located) is left out and its error returned alongside the attributes
/// that could be read, instead of failing them all.
///
/// Errors in the structures that index the attributes (the Attribute Info
/// message, the dense-storage heap header or B-tree) still fail the call:
/// then it is unknown which attributes exist at all.
pub fn extract_attributes_tolerant(
file_data: &[u8],
header: &ObjectHeader,
offset_size: u8,
length_size: u8,
) -> Result<(Vec<AttributeMessage>, Vec<FormatError>), FormatError> {
let mut errors = Vec::new();
let attrs = extract_attributes_with(file_data, header, offset_size, length_size, &mut |e| {
errors.push(e);
Ok(())
})?;
Ok((attrs, errors))
}
/// Read every attribute; each one that fails goes to `on_error`, which
/// either stops the read (returns the error) or skips that attribute.
fn extract_attributes_with(
file_data: &[u8],
header: &ObjectHeader,
offset_size: u8,
length_size: u8,
on_error: &mut dyn FnMut(FormatError) -> Result<(), FormatError>,
) -> Result<Vec<AttributeMessage>, FormatError> { ) -> Result<Vec<AttributeMessage>, FormatError> {
let mut attrs = Vec::new(); let mut attrs = Vec::new();
// Collect compact attributes (inline in OH) // Collect compact attributes (inline in OH)
for msg in &header.messages { for msg in &header.messages {
if msg.msg_type == MessageType::Attribute { if msg.msg_type == MessageType::Attribute {
if shared_message::is_shared(msg.flags) { let attr = if shared_message::is_shared(msg.flags) {
// Shared attribute: resolve the reference to get actual attribute data // Shared attribute: resolve the reference to get actual attribute data
let shared_ref = shared_message::parse_shared_ref(&msg.data, offset_size)?; shared_message::parse_shared_ref(&msg.data, offset_size)
let resolved_data = shared_message::resolve_shared_message( .and_then(|shared_ref| {
shared_message::resolve_shared_message(
file_data, file_data,
&shared_ref, &shared_ref,
MessageType::Attribute, MessageType::Attribute,
offset_size, offset_size,
length_size, length_size,
)?; )
let attr = AttributeMessage::parse_in_file( })
&resolved_data, .and_then(|resolved| {
AttributeMessage::parse_in_file(
&resolved,
file_data, file_data,
offset_size, offset_size,
length_size, length_size,
)?; )
attrs.push(attr); })
} else { } else {
let attr = AttributeMessage::parse_in_file( AttributeMessage::parse_in_file(&msg.data, file_data, offset_size, length_size)
&msg.data, };
file_data, match attr {
offset_size, Ok(attr) => attrs.push(attr),
length_size, Err(e) => on_error(e)?,
)?;
attrs.push(attr);
} }
} }
} }
@@ -439,9 +477,15 @@ pub fn extract_attributes_full(
if let Some(info) = attr_info if let Some(info) = attr_info
&& let Some(fh_addr) = info.fractal_heap_address && let Some(fh_addr) = info.fractal_heap_address
{ {
let dense_attrs = extract_dense_attributes(
extract_dense_attributes(file_data, &info, fh_addr, offset_size, length_size)?; file_data,
attrs.extend(dense_attrs); &info,
fh_addr,
offset_size,
length_size,
&mut attrs,
on_error,
)?;
} }
Ok(attrs) Ok(attrs)
@@ -468,7 +512,9 @@ fn extract_dense_attributes(
fh_addr: u64, fh_addr: u64,
offset_size: u8, offset_size: u8,
length_size: u8, length_size: u8,
) -> Result<Vec<AttributeMessage>, FormatError> { attrs: &mut Vec<AttributeMessage>,
on_error: &mut dyn FnMut(FormatError) -> Result<(), FormatError>,
) -> Result<(), FormatError> {
// Parse fractal heap // Parse fractal heap
let fh = FractalHeapHeader::parse(file_data, fh_addr as usize, offset_size, length_size)?; let fh = FractalHeapHeader::parse(file_data, fh_addr as usize, offset_size, length_size)?;
@@ -482,28 +528,32 @@ fn extract_dense_attributes(
let btree_hdr = BTreeV2Header::parse(file_data, btree_addr as usize, offset_size, length_size)?; let btree_hdr = BTreeV2Header::parse(file_data, btree_addr as usize, offset_size, length_size)?;
let records = collect_btree_v2_records(file_data, &btree_hdr, offset_size, length_size)?; let records = collect_btree_v2_records(file_data, &btree_hdr, offset_size, length_size)?;
let mut attrs = Vec::new();
for record in &records { for record in &records {
// Per HDF5 spec, both type 8 and type 9 records start with heap_id: // Per HDF5 spec, both type 8 and type 9 records start with heap_id:
// Type 8: heap_id(8) + msg_flags(1) + creation_order(4) + hash(4) // Type 8: heap_id(8) + msg_flags(1) + creation_order(4) + hash(4)
// Type 9: heap_id(8) + msg_flags(1) + creation_order(4) // Type 9: heap_id(8) + msg_flags(1) + creation_order(4)
let id_offset = 0; let id_len = fh.heap_id_length as usize;
let Some(id_bytes) = record.data.get(..id_len) else {
if record.data.len() < id_offset + fh.heap_id_length as usize { on_error(FormatError::UnexpectedEof {
expected: id_len,
available: record.data.len(),
})?;
continue; continue;
} };
let id_bytes = &record.data[id_offset..id_offset + fh.heap_id_length as usize];
// Read attribute message from fractal heap
let attr_data = fh.read_managed_object(file_data, id_bytes, offset_size)?;
// The data in the heap is a complete attribute message // The data in the heap is a complete attribute message
let attr = let attr = fh
AttributeMessage::parse_in_file(&attr_data, file_data, offset_size, length_size)?; .read_managed_object(file_data, id_bytes, offset_size)
attrs.push(attr); .and_then(|attr_data| {
AttributeMessage::parse_in_file(&attr_data, file_data, offset_size, length_size)
});
match attr {
Ok(attr) => attrs.push(attr),
Err(e) => on_error(e)?,
}
} }
Ok(attrs) Ok(())
} }
#[cfg(test)] #[cfg(test)]
+36 -18
View File
@@ -12,7 +12,6 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use clawhdf5_format::attribute::extract_attributes_full;
use clawhdf5_format::data_layout::DataLayout; use clawhdf5_format::data_layout::DataLayout;
use clawhdf5_format::data_read; use clawhdf5_format::data_read;
use clawhdf5_format::dataspace::Dataspace; use clawhdf5_format::dataspace::Dataspace;
@@ -29,7 +28,7 @@ use clawhdf5_format::superblock::Superblock;
use clawhdf5_io::HDF5Read; use clawhdf5_io::HDF5Read;
use crate::error::Error; use crate::error::Error;
use crate::types::{AttrValue, DType, attrs_to_map, classify_datatype}; use crate::types::{AttrValue, DType, classify_datatype, read_attrs};
/// A lazy HDF5 file handle that parses metadata on demand. /// A lazy HDF5 file handle that parses metadata on demand.
/// ///
@@ -231,17 +230,26 @@ impl<'f, R: HDF5Read> LazyGroup<'f, R> {
} }
/// Read all attributes of this group. /// Read all attributes of this group.
///
/// An attribute that cannot be read — a corrupt or unsupported attribute
/// message, or a dense-storage heap object that cannot be located — is
/// left out of the map instead of failing every attribute on the object;
/// [`attrs_with_errors`](Self::attrs_with_errors) reports which failed.
/// Values that are returned are complete (never partially decoded). An
/// error in the index of the attributes itself (the attribute info
/// message, the dense heap header or B-tree) still fails the call.
pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> { pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
self.attrs_with_errors().map(|(attrs, _)| attrs)
}
/// Like [`attrs`](Self::attrs), also returning one error for each
/// attribute that could not be read and was left out.
pub fn attrs_with_errors(
&self,
) -> Result<(HashMap<String, AttrValue>, Vec<FormatError>), Error> {
let hdr = self.file.get_or_parse_header(self.address)?; let hdr = self.file.get_or_parse_header(self.address)?;
let data = self.file.reader.as_bytes(); let data = self.file.reader.as_bytes();
let attr_msgs = read_attrs(data, &hdr, self.file.offset_size(), self.file.length_size())
extract_attributes_full(data, &hdr, self.file.offset_size(), self.file.length_size())?;
Ok(attrs_to_map(
&attr_msgs,
data,
self.file.offset_size(),
self.file.length_size(),
))
} }
/// Get a dataset within this group by name. /// Get a dataset within this group by name.
@@ -401,20 +409,30 @@ impl<'f, R: HDF5Read> LazyDataset<'f, R> {
} }
/// Read all attributes of this dataset. /// Read all attributes of this dataset.
///
/// An attribute that cannot be read — a corrupt or unsupported attribute
/// message, or a dense-storage heap object that cannot be located — is
/// left out of the map instead of failing every attribute on the object;
/// [`attrs_with_errors`](Self::attrs_with_errors) reports which failed.
/// Values that are returned are complete (never partially decoded). An
/// error in the index of the attributes itself (the attribute info
/// message, the dense heap header or B-tree) still fails the call.
pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> { pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
self.attrs_with_errors().map(|(attrs, _)| attrs)
}
/// Like [`attrs`](Self::attrs), also returning one error for each
/// attribute that could not be read and was left out.
pub fn attrs_with_errors(
&self,
) -> Result<(HashMap<String, AttrValue>, Vec<FormatError>), Error> {
let data = self.file.reader.as_bytes(); let data = self.file.reader.as_bytes();
let attr_msgs = extract_attributes_full( read_attrs(
data, data,
&self.header, &self.header,
self.file.offset_size(), self.file.offset_size(),
self.file.length_size(), self.file.length_size(),
)?; )
Ok(attrs_to_map(
&attr_msgs,
data,
self.file.offset_size(),
self.file.length_size(),
))
} }
/// A header message's payload, resolved through the shared-message /// A header message's payload, resolved through the shared-message
+37 -19
View File
@@ -7,7 +7,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use clawhdf5_format::attribute::extract_attributes_full;
use clawhdf5_format::data_layout::DataLayout; use clawhdf5_format::data_layout::DataLayout;
use clawhdf5_format::data_read; use clawhdf5_format::data_read;
use clawhdf5_format::dataspace::Dataspace; use clawhdf5_format::dataspace::Dataspace;
@@ -24,7 +23,7 @@ use clawhdf5_format::superblock::Superblock;
use clawhdf5_io::MmapReader; use clawhdf5_io::MmapReader;
use crate::error::Error; use crate::error::Error;
use crate::types::{AttrValue, DType, attrs_to_map, classify_datatype}; use crate::types::{AttrValue, DType, classify_datatype, read_attrs};
/// An HDF5 file opened via memory mapping. /// An HDF5 file opened via memory mapping.
/// ///
@@ -153,17 +152,26 @@ impl<'f> MmapGroup<'f> {
} }
/// Read all attributes of this group. /// Read all attributes of this group.
///
/// An attribute that cannot be read — a corrupt or unsupported attribute
/// message, or a dense-storage heap object that cannot be located — is
/// left out of the map instead of failing every attribute on the object;
/// [`attrs_with_errors`](Self::attrs_with_errors) reports which failed.
/// Values that are returned are complete (never partially decoded). An
/// error in the index of the attributes itself (the attribute info
/// message, the dense heap header or B-tree) still fails the call.
pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> { pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
let data = self.file.reader.as_bytes(); self.attrs_with_errors().map(|(attrs, _)| attrs)
}
/// Like [`attrs`](Self::attrs), also returning one error for each
/// attribute that could not be read and was left out.
pub fn attrs_with_errors(
&self,
) -> Result<(HashMap<String, AttrValue>, Vec<FormatError>), Error> {
let hdr = self.file.parse_header(self.address)?; let hdr = self.file.parse_header(self.address)?;
let attr_msgs = let data = self.file.reader.as_bytes();
extract_attributes_full(data, &hdr, self.file.offset_size(), self.file.length_size())?; read_attrs(data, &hdr, self.file.offset_size(), self.file.length_size())
Ok(attrs_to_map(
&attr_msgs,
data,
self.file.offset_size(),
self.file.length_size(),
))
} }
/// Get a dataset within this group by name. /// Get a dataset within this group by name.
@@ -342,20 +350,30 @@ impl<'f> MmapDataset<'f> {
} }
/// Read all attributes of this dataset. /// Read all attributes of this dataset.
///
/// An attribute that cannot be read — a corrupt or unsupported attribute
/// message, or a dense-storage heap object that cannot be located — is
/// left out of the map instead of failing every attribute on the object;
/// [`attrs_with_errors`](Self::attrs_with_errors) reports which failed.
/// Values that are returned are complete (never partially decoded). An
/// error in the index of the attributes itself (the attribute info
/// message, the dense heap header or B-tree) still fails the call.
pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> { pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
self.attrs_with_errors().map(|(attrs, _)| attrs)
}
/// Like [`attrs`](Self::attrs), also returning one error for each
/// attribute that could not be read and was left out.
pub fn attrs_with_errors(
&self,
) -> Result<(HashMap<String, AttrValue>, Vec<FormatError>), Error> {
let data = self.file.reader.as_bytes(); let data = self.file.reader.as_bytes();
let attr_msgs = extract_attributes_full( read_attrs(
data, data,
&self.header, &self.header,
self.file.offset_size(), self.file.offset_size(),
self.file.length_size(), self.file.length_size(),
)?; )
Ok(attrs_to_map(
&attr_msgs,
data,
self.file.offset_size(),
self.file.length_size(),
))
} }
/// A header message's payload, resolved through the shared-message /// A header message's payload, resolved through the shared-message
+37 -19
View File
@@ -7,7 +7,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use clawhdf5_format::attribute::extract_attributes_full;
use clawhdf5_format::chunk_cache::ChunkCache; use clawhdf5_format::chunk_cache::ChunkCache;
use clawhdf5_format::data_layout::DataLayout; use clawhdf5_format::data_layout::DataLayout;
use clawhdf5_format::data_read; use clawhdf5_format::data_read;
@@ -23,7 +22,7 @@ use clawhdf5_format::signature;
use clawhdf5_format::superblock::Superblock; use clawhdf5_format::superblock::Superblock;
use crate::error::Error; use crate::error::Error;
use crate::types::{AttrValue, DType, attrs_to_map, classify_datatype}; use crate::types::{AttrValue, DType, classify_datatype, read_attrs};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// FileData — internal storage for either owned bytes or an mmap // FileData — internal storage for either owned bytes or an mmap
@@ -293,17 +292,26 @@ impl<'f> Group<'f> {
} }
/// Read all attributes of this group. /// Read all attributes of this group.
///
/// An attribute that cannot be read — a corrupt or unsupported attribute
/// message, or a dense-storage heap object that cannot be located — is
/// left out of the map instead of failing every attribute on the object;
/// [`attrs_with_errors`](Self::attrs_with_errors) reports which failed.
/// Values that are returned are complete (never partially decoded). An
/// error in the index of the attributes itself (the attribute info
/// message, the dense heap header or B-tree) still fails the call.
pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> { pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
let data = self.file.data.as_bytes(); self.attrs_with_errors().map(|(attrs, _)| attrs)
}
/// Like [`attrs`](Self::attrs), also returning one error for each
/// attribute that could not be read and was left out.
pub fn attrs_with_errors(
&self,
) -> Result<(HashMap<String, AttrValue>, Vec<FormatError>), Error> {
let hdr = self.file.parse_header(self.address)?; let hdr = self.file.parse_header(self.address)?;
let attr_msgs = let data = self.file.data.as_bytes();
extract_attributes_full(data, &hdr, self.file.offset_size(), self.file.length_size())?; read_attrs(data, &hdr, self.file.offset_size(), self.file.length_size())
Ok(attrs_to_map(
&attr_msgs,
data,
self.file.offset_size(),
self.file.length_size(),
))
} }
/// Get a dataset within this group by name. /// Get a dataset within this group by name.
@@ -732,20 +740,30 @@ impl<'f> Dataset<'f> {
} }
/// Read all attributes of this dataset. /// Read all attributes of this dataset.
///
/// An attribute that cannot be read — a corrupt or unsupported attribute
/// message, or a dense-storage heap object that cannot be located — is
/// left out of the map instead of failing every attribute on the object;
/// [`attrs_with_errors`](Self::attrs_with_errors) reports which failed.
/// Values that are returned are complete (never partially decoded). An
/// error in the index of the attributes itself (the attribute info
/// message, the dense heap header or B-tree) still fails the call.
pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> { pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
self.attrs_with_errors().map(|(attrs, _)| attrs)
}
/// Like [`attrs`](Self::attrs), also returning one error for each
/// attribute that could not be read and was left out.
pub fn attrs_with_errors(
&self,
) -> Result<(HashMap<String, AttrValue>, Vec<FormatError>), Error> {
let data = self.file.data.as_bytes(); let data = self.file.data.as_bytes();
let attr_msgs = extract_attributes_full( read_attrs(
data, data,
&self.header, &self.header,
self.file.offset_size(), self.file.offset_size(),
self.file.length_size(), self.file.length_size(),
)?; )
Ok(attrs_to_map(
&attr_msgs,
data,
self.file.offset_size(),
self.file.length_size(),
))
} }
/// Verify this dataset's content against its stored provenance hash /// Verify this dataset's content against its stored provenance hash
+27
View File
@@ -155,6 +155,33 @@ pub(crate) fn classify_datatype(dt: &clawhdf5_format::datatype::Datatype) -> DTy
/// Read attribute messages into a `HashMap<String, AttrValue>`. /// Read attribute messages into a `HashMap<String, AttrValue>`.
/// ///
/// Best-effort: attributes that can't be decoded are silently skipped. /// Best-effort: attributes that can't be decoded are silently skipped.
/// The attributes of the object with header `header` that could be read,
/// and one error for each that could not (see
/// [`extract_attributes_tolerant`](clawhdf5_format::attribute::extract_attributes_tolerant)).
pub(crate) fn read_attrs(
file_data: &[u8],
header: &clawhdf5_format::object_header::ObjectHeader,
offset_size: u8,
length_size: u8,
) -> Result<
(
HashMap<String, AttrValue>,
Vec<clawhdf5_format::error::FormatError>,
),
crate::Error,
> {
let (msgs, errors) = clawhdf5_format::attribute::extract_attributes_tolerant(
file_data,
header,
offset_size,
length_size,
)?;
Ok((
attrs_to_map(&msgs, file_data, offset_size, length_size),
errors,
))
}
pub(crate) fn attrs_to_map( pub(crate) fn attrs_to_map(
attrs: &[clawhdf5_format::attribute::AttributeMessage], attrs: &[clawhdf5_format::attribute::AttributeMessage],
file_data: &[u8], file_data: &[u8],
@@ -364,3 +364,55 @@ fn soft_links_are_listed_as_their_targets() {
check_soft_links("latest"); check_soft_links("latest");
check_soft_links("earliest"); check_soft_links("earliest");
} }
/// One unreadable attribute used to fail `attrs()` for every attribute on
/// the object. Now it is left out (and reported by `attrs_with_errors`),
/// and the others are returned with their full values.
#[test]
fn one_unreadable_attribute_does_not_hide_the_others() {
skip_if_no_python!();
let (dir, path) = h5py_file(
"d = f.create_dataset('d', data=[1.0])\n\
for i in range(10):\n\
\x20 d.attrs['a%d' % i] = float(i)\n\
d.attrs['zz_broken_attribute'] = 42.0\n",
);
// Dense attributes live in a fractal heap whose blocks carry no checksum
// by default: give the one named `zz_broken_attribute` a nonexistent
// attribute message version (the byte 9 before its name in a v3 message).
let mut bytes = std::fs::read(&path).unwrap();
let needle = b"zz_broken_attribute";
let at = bytes
.windows(needle.len())
.position(|w| w == needle)
.expect("attribute name in the file");
assert_eq!(bytes[at - 9], 3, "expected a version-3 attribute message");
bytes[at - 9] = 0x7f;
let broken = dir.path().join("broken.h5");
std::fs::write(&broken, &bytes).unwrap();
for file in [
File::open(&broken).unwrap(),
File::from_bytes(bytes.clone()).unwrap(),
] {
let ds = file.dataset("d").unwrap();
let (attrs, errors) = ds.attrs_with_errors().unwrap();
assert_eq!(errors.len(), 1, "{errors:?}");
assert_eq!(attrs.len(), 10);
for i in 0..10 {
assert!(
matches!(attrs[&format!("a{i}")], AttrValue::F64(v) if v == f64::from(i)),
"a{i}"
);
}
assert!(!attrs.contains_key("zz_broken_attribute"));
assert_eq!(ds.attrs().unwrap().len(), 10);
}
let m = clawhdf5::MmapFile::open(&broken).unwrap();
assert_eq!(
m.dataset("d").unwrap().attrs_with_errors().unwrap().1.len(),
1
);
let l = clawhdf5::LazyFile::from_bytes(bytes).unwrap();
assert_eq!(l.dataset("d").unwrap().attrs().unwrap().len(), 10);
}
+3 -1
View File
@@ -87,7 +87,9 @@ the VDS item, which is marked.
- **Dense attributes:** a large attribute stored as a fractal-heap "huge" - **Dense attributes:** a large attribute stored as a fractal-heap "huge"
object makes every attribute on the object fail. This affects real NetCDF object makes every attribute on the object fail. This affects real NetCDF
files (`issue671.nc`). **Fixed 2026-09-25:** huge and tiny heap objects, files (`issue671.nc`). **Fixed 2026-09-25:** huge and tiny heap objects,
and filtered heaps, are read. and filtered heaps, are read; and an attribute that still cannot be read
is left out of `attrs()` (reported by `attrs_with_errors()`) instead of
failing the others.
- **Other readers:** - **Other readers:**
- VL-string datasets are not readable through `File`. - VL-string datasets are not readable through `File`.
- Metadata cache images are not supported. - Metadata cache images are not supported.