use std::collections::HashMap;
use std::io::{Read, Seek, SeekFrom};
use crate::attribute::{extract_attributes_full, extract_attributes_full_from_source};
use crate::chunk_cache::{ChunkCache, ChunkCacheConfig, ChunkCacheStats};
use crate::compound::CompoundType;
use crate::convert::TryToUsize;
use crate::data_layout::DataLayout;
use crate::data_read;
use crate::dataspace::Dataspace;
use crate::datatype::{Datatype, ReferenceType};
use crate::error::{Error, FormatError};
use crate::file_space_info::{FileSpaceInfo, FileSpaceStrategy};
use crate::filter_pipeline::FilterPipeline;
use crate::free_space_manager;
use crate::group_v1::GroupEntry;
use crate::group_v2;
use crate::libver::LibVer;
use crate::message_type::MessageType;
use crate::object_header::ObjectHeader;
use crate::signature;
use crate::source::{
BytesSource, FileSource, MetadataCacheConfig, MetadataCachingSource, ReadSeekSource,
};
use crate::superblock::Superblock;
use crate::vl_data::{self, VlenStringReadOptions};
use crate::types::{AttrValue, DType, attrs_to_map, classify_datatype};
enum Backend {
InMemory(Vec<u8>),
Streaming(Box<dyn FileSource + Send + Sync>),
}
pub(crate) enum SourceView<'a> {
Mem(&'a [u8]),
Stream(&'a (dyn FileSource + Send + Sync)),
}
impl FileSource for SourceView<'_> {
fn len(&self) -> u64 {
match self {
SourceView::Mem(b) => b.len() as u64,
SourceView::Stream(s) => s.len(),
}
}
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
match self {
SourceView::Mem(b) => BytesSource::new(*b).read_at(offset, buf),
SourceView::Stream(s) => s.read_at(offset, buf),
}
}
fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
match self {
SourceView::Mem(b) => BytesSource::new(*b).read_metadata_at(offset, len),
SourceView::Stream(s) => s.read_metadata_at(offset, len),
}
}
}
struct BaseOffsetSource<'a, S: FileSource + ?Sized> {
inner: &'a S,
base: u64,
}
impl<S: FileSource + ?Sized> FileSource for BaseOffsetSource<'_, S> {
fn len(&self) -> u64 {
self.inner.len().saturating_sub(self.base)
}
fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
let abs = offset
.checked_add(self.base)
.ok_or(FormatError::OffsetOverflow {
offset,
length: buf.len() as u64,
})?;
self.inner.read_at(abs, buf)
}
fn read_metadata_at(&self, offset: u64, len: usize) -> Result<Vec<u8>, FormatError> {
let abs = offset
.checked_add(self.base)
.ok_or(FormatError::OffsetOverflow {
offset,
length: len as u64,
})?;
self.inner.read_metadata_at(abs, len)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct FileAccessOptions {
metadata_cache: MetadataCacheConfig,
chunk_cache: ChunkCacheConfig,
}
impl FileAccessOptions {
pub const fn new() -> Self {
Self {
metadata_cache: MetadataCacheConfig::disabled(),
chunk_cache: ChunkCacheConfig::new(),
}
}
pub const fn with_metadata_cache(mut self, metadata_cache: MetadataCacheConfig) -> Self {
self.metadata_cache = metadata_cache;
self
}
pub const fn with_chunk_cache(mut self, chunk_cache: ChunkCacheConfig) -> Self {
self.chunk_cache = chunk_cache;
self
}
pub const fn metadata_cache(&self) -> MetadataCacheConfig {
self.metadata_cache
}
pub const fn chunk_cache(&self) -> ChunkCacheConfig {
self.chunk_cache
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DatasetAccessOptions {
chunk_cache: Option<ChunkCacheConfig>,
}
impl DatasetAccessOptions {
pub const fn new() -> Self {
Self { chunk_cache: None }
}
pub const fn with_chunk_cache(mut self, chunk_cache: ChunkCacheConfig) -> Self {
self.chunk_cache = Some(chunk_cache);
self
}
pub const fn chunk_cache(&self) -> Option<ChunkCacheConfig> {
self.chunk_cache
}
const fn resolved_chunk_cache(&self, default: ChunkCacheConfig) -> ChunkCacheConfig {
match self.chunk_cache {
Some(config) => config,
None => default,
}
}
}
pub fn is_hdf5<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<bool> {
let handle = std::fs::File::open(path)?;
let source = ReadSeekSource::new(handle).map_err(std::io::Error::other)?;
match signature::find_signature_in(&source) {
Ok(_) => Ok(true),
Err(FormatError::SignatureNotFound) => Ok(false),
Err(e) => Err(std::io::Error::other(e)),
}
}
pub fn is_hdf5_bytes(data: &[u8]) -> bool {
signature::find_signature(data).is_ok()
}
pub struct File {
backend: Backend,
superblock: Superblock,
addr_offset: u64,
handle: Option<std::fs::File>,
file_space_info: Option<FileSpaceInfo>,
access_options: FileAccessOptions,
}
impl File {
pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
Self::open_with_options(path, FileAccessOptions::new())
}
pub fn open_with_options<P: AsRef<std::path::Path>>(
path: P,
options: FileAccessOptions,
) -> Result<Self, Error> {
let bytes = std::fs::read(path.as_ref()).map_err(Error::Io)?;
Self::from_bytes_with_options(bytes, options)
}
pub fn open_streaming<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
Self::open_streaming_with_options(path, FileAccessOptions::new())
}
pub fn open_streaming_with_options<P: AsRef<std::path::Path>>(
path: P,
options: FileAccessOptions,
) -> Result<Self, Error> {
let handle = std::fs::File::open(path.as_ref()).map_err(Error::Io)?;
let source = ReadSeekSource::new(handle).map_err(Error::Format)?;
let source: Box<dyn FileSource + Send + Sync> = if options.metadata_cache.is_enabled() {
Box::new(MetadataCachingSource::new(source, options.metadata_cache))
} else {
Box::new(source)
};
let (superblock, addr_offset) = Self::parse_superblock_source(source.as_ref())?;
Ok(Self::from_parts(
Backend::Streaming(source),
superblock,
addr_offset,
None,
options,
))
}
pub fn open_swmr<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
Self::open_swmr_with_options(path, FileAccessOptions::new())
}
pub fn open_swmr_with_options<P: AsRef<std::path::Path>>(
path: P,
options: FileAccessOptions,
) -> Result<Self, Error> {
let mut handle = std::fs::File::open(path.as_ref()).map_err(Error::Io)?;
let mut data = Vec::new();
handle.read_to_end(&mut data).map_err(Error::Io)?;
let (superblock, addr_offset) = Self::parse_superblock(&data)?;
Ok(Self::from_parts(
Backend::InMemory(data),
superblock,
addr_offset,
Some(handle),
options,
))
}
pub fn from_bytes(data: Vec<u8>) -> Result<Self, Error> {
Self::from_bytes_with_options(data, FileAccessOptions::new())
}
pub fn from_bytes_with_options(
data: Vec<u8>,
options: FileAccessOptions,
) -> Result<Self, Error> {
let (superblock, addr_offset) = Self::parse_superblock(&data)?;
Ok(Self::from_parts(
Backend::InMemory(data),
superblock,
addr_offset,
None,
options,
))
}
pub(crate) fn source(&self) -> SourceView<'_> {
match &self.backend {
Backend::InMemory(v) => SourceView::Mem(v),
Backend::Streaming(s) => SourceView::Stream(s.as_ref()),
}
}
fn parse_superblock(data: &[u8]) -> Result<(Superblock, u64), Error> {
let sig_offset = signature::find_signature(data)?;
let mut superblock = Superblock::parse(data, sig_offset)?;
let addr_offset = superblock.base_address;
superblock.root_group_address += addr_offset;
Ok((superblock, addr_offset))
}
fn parse_superblock_source<S: FileSource + ?Sized>(
source: &S,
) -> Result<(Superblock, u64), Error> {
let sig_offset = signature::find_signature_in(source)?;
let mut superblock = Superblock::parse_from_source(source, sig_offset)?;
let addr_offset = superblock.base_address;
superblock.root_group_address += addr_offset;
Ok((superblock, addr_offset))
}
fn from_parts(
backend: Backend,
superblock: Superblock,
addr_offset: u64,
handle: Option<std::fs::File>,
access_options: FileAccessOptions,
) -> Self {
let mut file = File {
backend,
superblock,
addr_offset,
handle,
file_space_info: None,
access_options,
};
file.file_space_info = file.read_file_space_info();
file
}
fn read_file_space_info(&self) -> Option<FileSpaceInfo> {
let rel = self.superblock.superblock_extension_address?;
if rel == u64::MAX {
return None;
}
let abs = self.addr_offset.checked_add(rel)?;
let header = self.parse_header(abs).ok()?;
let msg = header
.messages
.iter()
.find(|m| m.msg_type == MessageType::FileSpaceInfo)?;
FileSpaceInfo::parse(
&msg.data,
self.superblock.offset_size,
self.superblock.length_size,
)
.ok()
}
pub fn refresh(&mut self) -> Result<(), Error> {
let handle = self.handle.as_mut().ok_or(Error::SwmrUnsupported)?;
const MAX_ATTEMPTS: u32 = 100;
let mut last_err = None;
for attempt in 0..MAX_ATTEMPTS {
let mut data = Vec::new();
handle.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
handle.read_to_end(&mut data).map_err(Error::Io)?;
match Self::parse_superblock(&data) {
Ok((superblock, addr_offset)) => {
self.backend = Backend::InMemory(data);
self.superblock = superblock;
self.addr_offset = addr_offset;
self.file_space_info = self.read_file_space_info();
return Ok(());
}
Err(e) => {
last_err = Some(e);
if attempt + 1 < MAX_ATTEMPTS {
std::thread::sleep(std::time::Duration::from_micros(
50 * (attempt + 1) as u64,
));
}
}
}
}
Err(last_err.expect("refresh retried at least once before failing"))
}
pub fn root(&self) -> Group<'_> {
Group {
file: self,
address: self.superblock.root_group_address,
}
}
fn resolve_path(&self, path: &str) -> Result<u64, Error> {
Ok(match &self.backend {
Backend::InMemory(v) => group_v2::resolve_path_any(v, &self.superblock, path)?,
Backend::Streaming(s) => {
group_v2::resolve_path_any_from_source(s.as_ref(), &self.superblock, path)?
}
})
}
pub fn dataset(&self, path: &str) -> Result<Dataset<'_>, Error> {
self.dataset_with_options(path, DatasetAccessOptions::new())
}
pub fn dataset_with_options(
&self,
path: &str,
options: DatasetAccessOptions,
) -> Result<Dataset<'_>, Error> {
let addr = self.resolve_path(path)?;
let hdr = self.parse_header(addr)?;
if !has_message(&hdr, MessageType::DataLayout) {
return Err(Error::NotADataset(path.to_string()));
}
let chunk_cache = options.resolved_chunk_cache(self.access_options.chunk_cache);
Ok(Dataset {
file: self,
address: addr,
header: hdr,
chunk_cache: ChunkCache::with_config(chunk_cache),
chunk_cache_config: chunk_cache,
})
}
pub fn group(&self, path: &str) -> Result<Group<'_>, Error> {
let addr = self.resolve_path(path)?;
Ok(Group {
file: self,
address: addr,
})
}
pub fn as_bytes(&self) -> &[u8] {
match &self.backend {
Backend::InMemory(v) => v,
Backend::Streaming(_) => &[],
}
}
pub const fn access_options(&self) -> FileAccessOptions {
self.access_options
}
pub fn superblock(&self) -> &Superblock {
&self.superblock
}
pub(crate) fn in_memory_image(&self) -> Option<&[u8]> {
match &self.backend {
Backend::InMemory(data) => Some(data),
Backend::Streaming(_) => None,
}
}
pub(crate) fn base_address(&self) -> u64 {
self.addr_offset
}
pub fn file_space_strategy(&self) -> Option<FileSpaceStrategy> {
self.file_space_info.as_ref().map(|info| info.strategy)
}
pub fn file_space_info(&self) -> Option<&FileSpaceInfo> {
self.file_space_info.as_ref()
}
pub fn persisted_free_space(&self) -> Vec<(u64, u64)> {
let Some(info) = &self.file_space_info else {
return Vec::new();
};
if !info.persist {
return Vec::new();
}
let Backend::InMemory(data) = &self.backend else {
return Vec::new();
};
let mut sections = free_space_manager::read_persisted_sections(
data,
&info.manager_addrs,
self.addr_offset,
self.superblock.offset_size,
)
.unwrap_or_default();
sections.sort_by_key(|s| s.addr);
sections.into_iter().map(|s| (s.addr, s.size)).collect()
}
pub fn file_size(&self) -> u64 {
self.source().len()
}
pub fn libver_bound(&self) -> LibVer {
LibVer::from_superblock_version(self.superblock.version)
}
fn parse_header(&self, address: u64) -> Result<ObjectHeader, FormatError> {
let os = self.superblock.offset_size;
let ls = self.superblock.length_size;
match &self.backend {
Backend::InMemory(v) => {
ObjectHeader::parse_with_base(v, address.to_usize()?, os, ls, self.addr_offset)
}
Backend::Streaming(s) => {
ObjectHeader::parse_from_source(s.as_ref(), address, os, ls, self.addr_offset)
}
}
}
fn object_at_relative(&self, rel_addr: u64) -> Result<Object<'_>, Error> {
if rel_addr == u64::MAX || rel_addr == 0 {
return Err(FormatError::InvalidObjectReference(rel_addr).into());
}
let abs = rel_addr
.checked_add(self.addr_offset)
.ok_or(FormatError::InvalidObjectReference(rel_addr))?;
let hdr = self.parse_header(abs)?;
if has_message(&hdr, MessageType::DataLayout) {
let chunk_cache =
DatasetAccessOptions::new().resolved_chunk_cache(self.access_options.chunk_cache);
Ok(Object::Dataset(Box::new(Dataset {
file: self,
address: abs,
header: hdr,
chunk_cache: ChunkCache::with_config(chunk_cache),
chunk_cache_config: chunk_cache,
})))
} else if is_group(&hdr) {
Ok(Object::Group(Group {
file: self,
address: abs,
}))
} else {
Err(FormatError::InvalidObjectReference(rel_addr).into())
}
}
fn offset_size(&self) -> u8 {
self.superblock.offset_size
}
fn length_size(&self) -> u8 {
self.superblock.length_size
}
fn group_children(&self, hdr: &ObjectHeader) -> Result<Vec<GroupEntry>, Error> {
let (os, ls, base) = (self.offset_size(), self.length_size(), self.addr_offset);
let mut entries = match &self.backend {
Backend::InMemory(v) => group_v2::resolve_group_entries(v, hdr, os, ls, base),
Backend::Streaming(s) => {
group_v2::resolve_group_entries_from_source(s.as_ref(), hdr, os, ls, base)
}
}
.map_err(Error::Format)?;
for entry in &mut entries {
entry.object_header_address += base;
}
Ok(entries)
}
fn attrs_of(&self, hdr: &ObjectHeader) -> Result<HashMap<String, AttrValue>, Error> {
let (os, ls, base) = (self.offset_size(), self.length_size(), self.addr_offset);
let attr_msgs = self.attr_messages_of(hdr)?;
Ok(attrs_to_map(&attr_msgs, &self.source(), os, ls, base))
}
pub(crate) fn attr_message_names_of(&self, hdr: &ObjectHeader) -> Result<Vec<String>, Error> {
Ok(self
.attr_messages_of(hdr)?
.into_iter()
.map(|a| a.name)
.collect())
}
fn attr_messages_of(
&self,
hdr: &ObjectHeader,
) -> Result<Vec<crate::attribute::AttributeMessage>, Error> {
let (os, ls) = (self.offset_size(), self.length_size());
match &self.backend {
Backend::InMemory(v) => Ok(extract_attributes_full(v, hdr, os, ls)?),
Backend::Streaming(s) => Ok(extract_attributes_full_from_source(
s.as_ref(),
hdr,
os,
ls,
)?),
}
}
fn read_dataset_raw(
&self,
dl: &DataLayout,
ds: &Dataspace,
dt: &Datatype,
pipeline: Option<&FilterPipeline>,
cache: &ChunkCache,
) -> Result<Vec<u8>, FormatError> {
let (os, ls) = (self.offset_size(), self.length_size());
let base = self.addr_offset;
match &self.backend {
Backend::InMemory(v) => {
let frame = if base == 0 {
v.as_slice()
} else {
let start = base.to_usize()?;
v.get(start..).ok_or(FormatError::UnexpectedEof {
expected: start,
available: v.len(),
})?
};
data_read::read_raw_data_cached(frame, dl, ds, dt, pipeline, os, ls, cache)
}
Backend::Streaming(s) if base == 0 => data_read::read_raw_data_cached_from_source(
s.as_ref(),
dl,
ds,
dt,
pipeline,
os,
ls,
cache,
),
Backend::Streaming(s) => {
let framed = BaseOffsetSource {
inner: s.as_ref(),
base,
};
data_read::read_raw_data_cached_from_source(
&framed, dl, ds, dt, pipeline, os, ls, cache,
)
}
}
}
}
impl std::fmt::Debug for File {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("File")
.field("size", &self.source().len())
.field("superblock_version", &self.superblock.version)
.finish()
}
}
pub enum Object<'f> {
Group(Group<'f>),
Dataset(Box<Dataset<'f>>),
}
impl std::fmt::Debug for Object<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Object::Group(_) => f.write_str("Object::Group"),
Object::Dataset(_) => f.write_str("Object::Dataset"),
}
}
}
pub struct Group<'f> {
file: &'f File,
address: u64,
}
impl<'f> Group<'f> {
pub(crate) fn header_address(&self) -> u64 {
self.address
}
pub fn datasets(&self) -> Result<Vec<String>, Error> {
let entries = self.children()?;
let mut names = Vec::new();
for entry in &entries {
let hdr = self.file.parse_header(entry.object_header_address)?;
if has_message(&hdr, MessageType::DataLayout) {
names.push(entry.name.clone());
}
}
Ok(names)
}
pub fn groups(&self) -> Result<Vec<String>, Error> {
let entries = self.children()?;
let mut names = Vec::new();
for entry in &entries {
let hdr = self.file.parse_header(entry.object_header_address)?;
if is_group(&hdr) {
names.push(entry.name.clone());
}
}
Ok(names)
}
pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
let hdr = self.file.parse_header(self.address)?;
self.file.attrs_of(&hdr)
}
pub(crate) fn attr_names(&self) -> Result<Vec<String>, Error> {
let hdr = self.file.parse_header(self.address)?;
self.file.attr_message_names_of(&hdr)
}
pub fn dataset(&self, name: &str) -> Result<Dataset<'f>, Error> {
self.dataset_with_options(name, DatasetAccessOptions::new())
}
pub fn dataset_with_options(
&self,
name: &str,
options: DatasetAccessOptions,
) -> Result<Dataset<'f>, Error> {
let entries = self.children()?;
let entry = entries
.iter()
.find(|e| e.name == name)
.ok_or_else(|| Error::Format(FormatError::PathNotFound(name.to_string())))?;
let hdr = self.file.parse_header(entry.object_header_address)?;
if !has_message(&hdr, MessageType::DataLayout) {
return Err(Error::NotADataset(name.to_string()));
}
let chunk_cache = options.resolved_chunk_cache(self.file.access_options.chunk_cache);
Ok(Dataset {
file: self.file,
address: entry.object_header_address,
header: hdr,
chunk_cache: ChunkCache::with_config(chunk_cache),
chunk_cache_config: chunk_cache,
})
}
pub fn group(&self, name: &str) -> Result<Group<'f>, Error> {
let entries = self.children()?;
let entry = entries
.iter()
.find(|e| e.name == name)
.ok_or_else(|| Error::Format(FormatError::PathNotFound(name.to_string())))?;
Ok(Group {
file: self.file,
address: entry.object_header_address,
})
}
fn children(&self) -> Result<Vec<GroupEntry>, Error> {
let hdr = self.file.parse_header(self.address)?;
self.file.group_children(&hdr)
}
}
pub struct Dataset<'f> {
file: &'f File,
address: u64,
header: ObjectHeader,
chunk_cache: ChunkCache,
chunk_cache_config: ChunkCacheConfig,
}
impl<'f> std::fmt::Debug for Dataset<'f> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Dataset")
.field("messages", &self.header.messages.len())
.finish()
}
}
impl<'f> Dataset<'f> {
pub(crate) fn header_address(&self) -> u64 {
self.address
}
pub const fn chunk_cache_config(&self) -> ChunkCacheConfig {
self.chunk_cache_config
}
pub fn chunk_cache_stats(&self) -> ChunkCacheStats {
self.chunk_cache.stats()
}
pub fn shape(&self) -> Result<Vec<u64>, Error> {
let ds = self.dataspace()?;
Ok(ds.dimensions.clone())
}
pub fn dtype(&self) -> Result<DType, Error> {
let dt = self.datatype()?;
Ok(classify_datatype(&dt))
}
pub fn read_f64(&self) -> Result<Vec<f64>, Error> {
let raw = self.read_raw()?;
let dt = self.datatype()?;
Ok(data_read::read_as_f64(&raw, &dt)?)
}
pub fn read_f32(&self) -> Result<Vec<f32>, Error> {
let raw = self.read_raw()?;
let dt = self.datatype()?;
Ok(data_read::read_as_f32(&raw, &dt)?)
}
pub fn read_i32(&self) -> Result<Vec<i32>, Error> {
let raw = self.read_raw()?;
let dt = self.datatype()?;
Ok(data_read::read_as_i32(&raw, &dt)?)
}
pub fn read_i64(&self) -> Result<Vec<i64>, Error> {
let raw = self.read_raw()?;
let dt = self.datatype()?;
Ok(data_read::read_as_i64(&raw, &dt)?)
}
pub fn read_u64(&self) -> Result<Vec<u64>, Error> {
let raw = self.read_raw()?;
let dt = self.datatype()?;
Ok(data_read::read_as_u64(&raw, &dt)?)
}
pub fn read_u8(&self) -> Result<Vec<u8>, Error> {
self.read_raw()
}
#[expect(
clippy::cast_possible_wrap,
reason = "read_i8 reinterprets each stored byte as the signed i8 the caller requested"
)]
pub fn read_i8(&self) -> Result<Vec<i8>, Error> {
let raw = self.read_raw()?;
Ok(raw.iter().map(|&b| b as i8).collect())
}
pub fn read_i16(&self) -> Result<Vec<i16>, Error> {
let raw = self.read_raw()?;
let dt = self.datatype()?;
Ok(data_read::read_as_i16(&raw, &dt)?)
}
pub fn read_u16(&self) -> Result<Vec<u16>, Error> {
let raw = self.read_raw()?;
let dt = self.datatype()?;
Ok(data_read::read_as_u16(&raw, &dt)?)
}
pub fn read_u32(&self) -> Result<Vec<u32>, Error> {
let raw = self.read_raw()?;
let dt = self.datatype()?;
Ok(data_read::read_as_u32(&raw, &dt)?)
}
pub fn read_string(&self) -> Result<Vec<String>, Error> {
let dt = self.datatype()?;
if vl_data::is_vlen_string_datatype(&dt) {
self.read_vlen_strings(VlenStringReadOptions::default())
} else {
let raw = self.read_raw()?;
Ok(data_read::read_as_strings(&raw, &dt)?)
}
}
pub fn vlen_string_payload_size(&self) -> Result<u64, Error> {
let datatype = self.datatype()?;
if !vl_data::is_vlen_string_datatype(&datatype) {
return Err(FormatError::TypeMismatch {
expected: "VariableLength string",
actual: "non-VariableLength string",
}
.into());
}
let dataspace = self.dataspace()?;
let raw = self.read_raw()?;
Ok(vl_data::vlen_string_payload_size(
&raw,
dataspace.num_elements(),
self.file.offset_size(),
)?)
}
pub fn read_vlen_strings(&self, options: VlenStringReadOptions) -> Result<Vec<String>, Error> {
let mut strings = Vec::new();
self.visit_vlen_strings(options, |string| strings.push(string.to_owned()))?;
Ok(strings)
}
pub fn visit_vlen_strings<F>(
&self,
options: VlenStringReadOptions,
visitor: F,
) -> Result<(), Error>
where
F: FnMut(&str),
{
let datatype = self.datatype()?;
if !vl_data::is_vlen_string_datatype(&datatype) {
return Err(FormatError::TypeMismatch {
expected: "VariableLength string",
actual: "non-VariableLength string",
}
.into());
}
let dataspace = self.dataspace()?;
if let Some(limit) = options.max_elements()
&& dataspace.num_elements() > limit as u64
{
return Err(FormatError::VariableLengthElementLimitExceeded {
limit,
actual: dataspace.num_elements(),
}
.into());
}
let raw = self.read_raw()?;
let source = self.file.source();
Ok(vl_data::visit_vl_strings_from_source(
&source,
&raw,
dataspace.num_elements(),
self.file.offset_size(),
self.file.length_size(),
self.file.addr_offset,
options,
visitor,
)?)
}
pub(crate) fn read_vlen_string_bytes(
&self,
options: VlenStringReadOptions,
) -> Result<Vec<vl_data::VlByteObject>, Error> {
let datatype = self.datatype()?;
if !vl_data::is_vlen_string_datatype(&datatype) {
return Err(FormatError::TypeMismatch {
expected: "VariableLength string",
actual: "non-VariableLength string",
}
.into());
}
let dataspace = self.dataspace()?;
if let Some(limit) = options.max_elements()
&& dataspace.num_elements() > limit as u64
{
return Err(FormatError::VariableLengthElementLimitExceeded {
limit,
actual: dataspace.num_elements(),
}
.into());
}
let raw = self.read_raw()?;
let source = self.file.source();
Ok(vl_data::read_vl_byte_objects_from_source(
&source,
&raw,
dataspace.num_elements(),
self.file.offset_size(),
self.file.length_size(),
self.file.addr_offset,
1, options,
)?)
}
pub(crate) fn read_vlen_sequence_bytes(
&self,
options: VlenStringReadOptions,
) -> Result<(Vec<vl_data::VlByteObject>, usize), Error> {
let datatype = self.datatype()?;
let Datatype::VariableLength { base_type, .. } = &datatype else {
return Err(FormatError::TypeMismatch {
expected: "non-string VariableLength",
actual: "non-VariableLength",
}
.into());
};
if vl_data::is_vlen_string_datatype(&datatype) {
return Err(FormatError::TypeMismatch {
expected: "non-string VariableLength",
actual: "VariableLength string",
}
.into());
}
let element_size = base_type.type_size() as usize;
if element_size == 0 {
return Err(
FormatError::VlDataError("non-string VL base type has zero size".into()).into(),
);
}
let dataspace = self.dataspace()?;
if let Some(limit) = options.max_elements()
&& dataspace.num_elements() > limit as u64
{
return Err(FormatError::VariableLengthElementLimitExceeded {
limit,
actual: dataspace.num_elements(),
}
.into());
}
let raw = self.read_raw()?;
let source = self.file.source();
let objects = vl_data::read_vl_byte_objects_from_source(
&source,
&raw,
dataspace.num_elements(),
self.file.offset_size(),
self.file.length_size(),
self.file.addr_offset,
element_size,
options,
)?;
Ok((objects, element_size))
}
pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
self.file.attrs_of(&self.header)
}
pub(crate) fn attr_names(&self) -> Result<Vec<String>, Error> {
self.file.attr_message_names_of(&self.header)
}
pub fn datatype(&self) -> Result<Datatype, Error> {
let msg = find_message(&self.header, MessageType::Datatype)?;
let (dt, _) = Datatype::parse(&msg.data)?;
Ok(dt)
}
pub(crate) fn dataspace(&self) -> Result<Dataspace, Error> {
let msg = find_message(&self.header, MessageType::Dataspace)?;
Ok(Dataspace::parse(&msg.data, self.file.length_size())?)
}
pub(crate) fn data_layout(&self) -> Result<DataLayout, Error> {
let msg = find_message(&self.header, MessageType::DataLayout)?;
Ok(DataLayout::parse(
&msg.data,
self.file.offset_size(),
self.file.length_size(),
)?)
}
pub(crate) fn filter_pipeline(&self) -> Option<FilterPipeline> {
self.header
.messages
.iter()
.find(|m| m.msg_type == MessageType::FilterPipeline)
.and_then(|msg| FilterPipeline::parse(&msg.data).ok())
}
pub(crate) fn raw_chunks(&self) -> Result<Vec<crate::chunked_read::ChunkInfo>, Error> {
let DataLayout::Chunked {
chunk_dimensions,
btree_address,
version,
chunk_index_type,
single_chunk_filtered_size,
single_chunk_filter_mask,
} = self.data_layout()?
else {
return Err(Error::Format(crate::error::FormatError::ChunkedReadError(
"raw_chunks called on a non-chunked dataset".into(),
)));
};
let Some(addr) = btree_address else {
return Ok(Vec::new());
};
let dataspace = self.dataspace()?;
let elem_size = self.datatype()?.type_size() as usize;
let base = self.file.addr_offset;
let source = self.file.source();
if base == 0 {
return Ok(crate::chunked_read::collect_chunks_for_layout_from_source(
&source,
version,
chunk_index_type,
addr,
single_chunk_filtered_size,
single_chunk_filter_mask,
&chunk_dimensions,
&dataspace,
elem_size,
self.file.offset_size(),
self.file.length_size(),
)?);
}
let framed = BaseOffsetSource {
inner: &source,
base,
};
let mut chunks = crate::chunked_read::collect_chunks_for_layout_from_source(
&framed,
version,
chunk_index_type,
addr,
single_chunk_filtered_size,
single_chunk_filter_mask,
&chunk_dimensions,
&dataspace,
elem_size,
self.file.offset_size(),
self.file.length_size(),
)?;
for c in &mut chunks {
c.address =
c.address
.checked_add(base)
.ok_or(crate::error::FormatError::OffsetOverflow {
offset: c.address,
length: 0,
})?;
}
Ok(chunks)
}
pub(crate) fn filter_pipeline_message_bytes(&self) -> Option<Vec<u8>> {
self.header
.messages
.iter()
.find(|m| m.msg_type == MessageType::FilterPipeline)
.map(|msg| msg.data.clone())
}
pub fn read_raw(&self) -> Result<Vec<u8>, Error> {
let dt = self.datatype()?;
let ds = self.dataspace()?;
let dl = self.data_layout()?;
let pipeline = self.filter_pipeline();
Ok(self
.file
.read_dataset_raw(&dl, &ds, &dt, pipeline.as_ref(), &self.chunk_cache)?)
}
pub fn dereference(&self) -> Result<Vec<Object<'f>>, Error> {
let dt = self.datatype()?;
if !matches!(
dt,
Datatype::Reference {
ref_type: ReferenceType::Object,
..
}
) {
return Err(FormatError::TypeMismatch {
expected: "object reference",
actual: "non-reference datatype",
}
.into());
}
let elem_size = dt.type_size().to_usize()?;
if elem_size < 8 {
return Err(FormatError::TypeMismatch {
expected: "8-byte object reference",
actual: "object reference narrower than 8 bytes",
}
.into());
}
let raw = self.read_raw()?;
if raw.is_empty() {
return Ok(Vec::new());
}
if !raw.len().is_multiple_of(elem_size) {
return Err(FormatError::DataSizeMismatch {
expected: elem_size,
actual: raw.len(),
}
.into());
}
let mut out = Vec::with_capacity(raw.len() / elem_size);
for chunk in raw.chunks_exact(elem_size) {
let addr = u64::from_le_bytes(chunk[..8].try_into().expect("chunk has >= 8 bytes"));
out.push(self.file.object_at_relative(addr)?);
}
Ok(out)
}
pub fn read_compound<T: CompoundType>(&self) -> Result<Vec<T>, Error> {
let datatype = self.datatype()?;
let element_size = datatype.type_size().to_usize()?;
if !matches!(datatype, Datatype::Compound { .. }) {
return Err(FormatError::TypeMismatch {
expected: "Compound",
actual: "non-Compound",
}
.into());
}
let raw = self.read_raw()?;
if element_size == 0 || !raw.len().is_multiple_of(element_size) {
return Err(FormatError::DataSizeMismatch {
expected: element_size,
actual: raw.len(),
}
.into());
}
raw.chunks_exact(element_size)
.map(|bytes| T::decode(&datatype, bytes).map_err(Error::from))
.collect()
}
#[cfg(feature = "provenance")]
pub fn verify_provenance(&self) -> Result<crate::provenance::VerifyResult, Error> {
use crate::provenance::{ATTR_SHA256, VerifyResult, sha256_hex};
let attrs = self.attrs()?;
let stored = match attrs.get(ATTR_SHA256) {
Some(AttrValue::String(s) | AttrValue::AsciiString(s)) => {
s.trim_end_matches('\0').to_string()
}
_ => return Ok(VerifyResult::NoHash),
};
let computed = sha256_hex(&self.read_raw()?);
if computed == stored {
Ok(VerifyResult::Ok)
} else {
Ok(VerifyResult::Mismatch { stored, computed })
}
}
}
fn find_message(
header: &ObjectHeader,
msg_type: MessageType,
) -> Result<&crate::object_header::HeaderMessage, Error> {
header
.messages
.iter()
.find(|m| m.msg_type == msg_type)
.ok_or(Error::MissingMessage(msg_type))
}
fn has_message(header: &ObjectHeader, msg_type: MessageType) -> bool {
header.messages.iter().any(|m| m.msg_type == msg_type)
}
fn is_group(header: &ObjectHeader) -> bool {
header.messages.iter().any(|m| {
m.msg_type == MessageType::LinkInfo
|| m.msg_type == MessageType::Link
|| m.msg_type == MessageType::SymbolTable
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FileBuilder;
fn chunked_file_bytes() -> Vec<u8> {
let data: Vec<i32> = (0..256).collect();
let mut b = FileBuilder::new();
b.create_dataset("chunked")
.with_i32_data(&data)
.with_shape(&[256])
.with_chunks(&[32]);
b.finish().unwrap()
}
#[test]
fn enabled_override_populates_live_cache_over_disabled_file_default() {
let file = File::from_bytes_with_options(
chunked_file_bytes(),
FileAccessOptions::new().with_chunk_cache(ChunkCacheConfig::disabled()),
)
.unwrap();
let ds = file
.dataset_with_options(
"chunked",
DatasetAccessOptions::new().with_chunk_cache(ChunkCacheConfig::new()),
)
.unwrap();
assert_eq!(ds.read_i32().unwrap(), (0..256).collect::<Vec<i32>>());
assert!(ds.chunk_cache_stats().index_loaded());
assert!(ds.chunk_cache_stats().cached_chunks() > 0);
}
#[test]
fn disabled_override_suppresses_live_cache_over_enabled_file_default() {
let file = File::from_bytes_with_options(
chunked_file_bytes(),
FileAccessOptions::new().with_chunk_cache(ChunkCacheConfig::new()),
)
.unwrap();
let ds = file
.dataset_with_options(
"chunked",
DatasetAccessOptions::new().with_chunk_cache(ChunkCacheConfig::disabled()),
)
.unwrap();
assert_eq!(ds.read_i32().unwrap(), (0..256).collect::<Vec<i32>>());
assert!(!ds.chunk_cache_stats().index_loaded());
assert_eq!(ds.chunk_cache_stats().cached_chunks(), 0);
}
}