Skip to main content

rust_hdf5/
dataset.rs

1//! Dataset creation and I/O.
2//!
3//! Datasets are created via the fluent [`DatasetBuilder`] API obtained from
4//! [`H5File::new_dataset`](crate::file::H5File::new_dataset). Once created,
5//! the [`H5Dataset`] handle can read or write raw typed data.
6
7use std::borrow::Cow;
8
9use crate::attribute::AttrBuilder;
10use crate::error::{Hdf5Error, Result};
11use crate::file::{
12    borrow_inner, borrow_inner_mut, clone_inner, same_inner, H5FileInner, SharedInner,
13};
14use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
15use crate::format::messages::filter::Filter;
16use crate::format::messages::virtual_mapping::VirtualMapping;
17use crate::format::reference::{Reference, ReferenceTarget};
18use crate::format::selection::check_hyperslab;
19use crate::format::selection::Selection;
20use crate::format::storage_kind::AttributeStorage;
21use crate::io::file_handle::ReadDst;
22use crate::io::reader::{read_image_into_new, ExternalFileSegment};
23use crate::io::writer::{ChunkBytes, ChunkIndexKind};
24use crate::types::H5Type;
25
26// ---------------------------------------------------------------------------
27// DatasetBuilder
28// ---------------------------------------------------------------------------
29
30/// A fluent builder for creating datasets.
31///
32/// Obtained from [`H5File::new_dataset::<T>()`](crate::file::H5File::new_dataset).
33///
34/// ```no_run
35/// # use rust_hdf5::H5File;
36/// let file = H5File::create("builder.h5").unwrap();
37/// let ds = file.new_dataset::<f32>()
38///     .shape(&[10, 20])
39///     .create("temperatures")
40///     .unwrap();
41/// ```
42pub struct DatasetBuilder<T: H5Type> {
43    file_inner: SharedInner,
44    shape: Option<Vec<usize>>,
45    is_null: bool,
46    chunk_dims: Option<Vec<usize>>,
47    max_shape: Option<Vec<Option<usize>>>,
48    is_compact: bool,
49    early_allocation: bool,
50    deflate_level: Option<u32>,
51    shuffle: bool,
52    custom_pipeline: Option<crate::format::messages::filter::FilterPipeline>,
53    group_path: Option<String>,
54    fill_value: Option<Vec<u8>>,
55    fill_time: Option<FillTime>,
56    datatype_override: Option<crate::format::messages::datatype::DatatypeMessage>,
57    committed_type: Option<String>,
58    references: Option<ReferenceElement>,
59    external: Option<Vec<(String, u64, u64)>>,
60    efile_prefix: Option<String>,
61    virtual_mappings: Vec<VirtualMapping>,
62    _marker: std::marker::PhantomData<T>,
63}
64
65/// Which reference a `*_references()` builder call asked the elements to be.
66///
67/// One field rather than a flag per kind: an element is a whole-object
68/// reference or a region reference, never both, and the width of each is only
69/// known once the file's address size is (see
70/// [`DatatypeMessage::object_reference`] and
71/// [`DatatypeMessage::region_reference`]).
72///
73/// [`DatatypeMessage::object_reference`]: crate::format::messages::datatype::DatatypeMessage::object_reference
74/// [`DatatypeMessage::region_reference`]: crate::format::messages::datatype::DatatypeMessage::region_reference
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76enum ReferenceElement {
77    /// `H5T_STD_REF_OBJ`.
78    Object,
79    /// `H5T_STD_REF_DSETREG`.
80    Region,
81    /// `H5T_STD_REF`, the 1.12 form. One datatype for all three 1.12 kinds:
82    /// the element leads with the kind it holds, so `H5T__ref_disk_getsize`
83    /// sizes every element for the widest of them and a dataset of this type
84    /// may hold objects, regions and attributes alike.
85    Revised,
86}
87
88impl ReferenceElement {
89    /// The stored datatype for this kind in a file with `ctx`'s address size.
90    fn datatype(
91        self,
92        ctx: &crate::format::FormatContext,
93    ) -> crate::format::messages::datatype::DatatypeMessage {
94        use crate::format::messages::datatype::DatatypeMessage;
95        match self {
96            Self::Object => DatatypeMessage::object_reference(ctx),
97            Self::Region => DatatypeMessage::region_reference(ctx),
98            Self::Revised => DatatypeMessage::std_object_reference(ctx),
99        }
100    }
101}
102
103impl<T: H5Type> DatasetBuilder<T> {
104    pub(crate) fn new(file_inner: SharedInner) -> Self {
105        Self {
106            file_inner,
107            shape: None,
108            is_null: false,
109            chunk_dims: None,
110            max_shape: None,
111            is_compact: false,
112            early_allocation: false,
113            deflate_level: None,
114            shuffle: false,
115            custom_pipeline: None,
116            group_path: None,
117            fill_value: None,
118            fill_time: None,
119            datatype_override: None,
120            committed_type: None,
121            references: None,
122            external: None,
123            efile_prefix: None,
124            virtual_mappings: Vec::new(),
125            _marker: std::marker::PhantomData,
126        }
127    }
128
129    pub(crate) fn new_in_group(file_inner: SharedInner, group_path: String) -> Self {
130        Self {
131            file_inner,
132            shape: None,
133            is_null: false,
134            chunk_dims: None,
135            max_shape: None,
136            is_compact: false,
137            early_allocation: false,
138            deflate_level: None,
139            shuffle: false,
140            custom_pipeline: None,
141            group_path: Some(group_path),
142            fill_value: None,
143            fill_time: None,
144            datatype_override: None,
145            committed_type: None,
146            references: None,
147            external: None,
148            efile_prefix: None,
149            virtual_mappings: Vec::new(),
150            _marker: std::marker::PhantomData,
151        }
152    }
153
154    /// Set the dataset dimensions.
155    ///
156    /// This is required before calling [`create`](Self::create), unless
157    /// [`null`](Self::null) was called instead.
158    /// Use an empty slice `&[]` for a scalar (0-dimensional) dataset.
159    #[must_use]
160    pub fn shape<S: AsRef<[usize]>>(mut self, dims: S) -> Self {
161        self.shape = Some(dims.as_ref().to_vec());
162        self
163    }
164
165    /// Create a scalar (0-dimensional) dataset holding a single value.
166    #[must_use]
167    pub fn scalar(mut self) -> Self {
168        self.shape = Some(vec![]);
169        self
170    }
171
172    /// Create a dataset with the NULL dataspace: no elements at all.
173    ///
174    /// Distinct from [`scalar`](Self::scalar), which holds exactly one
175    /// element. A NULL dataset holds zero bytes of data and cannot be
176    /// written to — [`write_raw`](H5Dataset::write_raw) and
177    /// [`write_raw_bytes`](H5Dataset::write_raw_bytes) return an error, and
178    /// it cannot be chunked or filtered, matching h5py's `h5py.Empty`.
179    #[must_use]
180    pub fn null(mut self) -> Self {
181        self.is_null = true;
182        self
183    }
184
185    /// Set chunk dimensions for chunked storage.
186    ///
187    /// When set, the dataset uses chunked storage with the extensible array
188    /// index. You should also call [`max_shape`](Self::max_shape) or
189    /// [`resizable`](Self::resizable) to allow extending.
190    #[must_use]
191    pub fn chunk(mut self, chunk_dims: &[usize]) -> Self {
192        self.chunk_dims = Some(chunk_dims.to_vec());
193        self
194    }
195
196    /// Make all dimensions unlimited (resizable).
197    ///
198    /// This sets max_dims to u64::MAX for all dimensions.
199    #[must_use]
200    pub fn resizable(mut self) -> Self {
201        self.max_shape = Some(vec![None; self.shape.as_ref().map_or(0, |s| s.len())]);
202        self
203    }
204
205    /// Set maximum dimensions. `None` means unlimited for that dimension.
206    #[must_use]
207    pub fn max_shape(mut self, max: &[Option<usize>]) -> Self {
208        self.max_shape = Some(max.to_vec());
209        self
210    }
211
212    /// Store the raw data inside the dataset's object header —
213    /// `H5Pset_layout(dcpl, H5D_COMPACT)`.
214    ///
215    /// A compact dataset costs no data block and no second seek to read, which
216    /// suits the small per-run constants an analysis file is full of. It is
217    /// bounded by what one object header message can hold
218    /// ([`MAX_COMPACT_DATA`](crate::MAX_COMPACT_DATA) bytes) and it
219    /// is fixed in size: [`chunk`](Self::chunk), a filter, and an unlimited
220    /// [`max_shape`](Self::max_shape) are all rejected at
221    /// [`create`](Self::create), as libhdf5 rejects them.
222    ///
223    /// ```no_run
224    /// # use rust_hdf5::H5File;
225    /// let file = H5File::create("compact.h5").unwrap();
226    /// let ds = file.new_dataset::<i32>()
227    ///     .shape([16])
228    ///     .compact()
229    ///     .create("data")
230    ///     .unwrap();
231    /// ds.write_raw(&(0..16i32).collect::<Vec<_>>()).unwrap();
232    /// ```
233    #[must_use]
234    pub fn compact(mut self) -> Self {
235        self.is_compact = true;
236        self
237    }
238
239    /// Allocate the whole of a chunked dataset's storage at create —
240    /// `H5Pset_alloc_time(dcpl, H5D_ALLOC_TIME_EARLY)`, h5py's
241    /// `alloc_time=h5d.ALLOC_TIME_EARLY`.
242    ///
243    /// Every chunk exists, holding the fill value, before anything is
244    /// written, so an unwritten chunk costs a read of fill bytes rather than
245    /// a miss. On a fixed-shape unfiltered dataset that is also what lets
246    /// libhdf5 pick its cheapest chunk index — the *implicit* index, which
247    /// is no index at all: the chunks are one contiguous run in grid order
248    /// and a chunk's address is arithmetic. This builder makes the same
249    /// choice under the same conditions, so such a dataset is written with
250    /// no index structure in the file.
251    ///
252    /// Ignored by storage that has no chunk grid to allocate: contiguous,
253    /// compact and NULL-dataspace datasets.
254    ///
255    /// ```no_run
256    /// # use rust_hdf5::H5File;
257    /// let file = H5File::create("implicit.h5").unwrap();
258    /// let ds = file.new_dataset::<i32>()
259    ///     .shape([16])
260    ///     .chunk(&[4])
261    ///     .early_allocation()
262    ///     .create("data")
263    ///     .unwrap();
264    /// ds.write_raw(&(0..16i32).collect::<Vec<_>>()).unwrap();
265    /// ```
266    #[must_use]
267    pub fn early_allocation(mut self) -> Self {
268        self.early_allocation = true;
269        self
270    }
271
272    /// Enable deflate (gzip) compression with the given level (0-9).
273    ///
274    /// Requires chunked storage (call `.chunk()` before `.create()`).
275    /// Level 0 = no compression, 9 = maximum compression. Default is 6.
276    #[must_use]
277    pub fn deflate(mut self, level: u32) -> Self {
278        self.deflate_level = Some(level);
279        self
280    }
281
282    /// Enable the shuffle filter — `H5Pset_shuffle(dcpl)`, h5py's
283    /// `shuffle=True`.
284    ///
285    /// Shuffle reorders a chunk's bytes by their position within an element,
286    /// which typically improves how well a compressor behind it does on
287    /// numeric data. It is a permutation, not a compressor: on its own it
288    /// leaves the chunk exactly as large as it was, which is what
289    /// `H5Pset_shuffle` without a compressor writes. Combine it with
290    /// [`deflate`](Self::deflate) to compress the shuffled stream. Requires
291    /// chunked storage.
292    ///
293    /// The element width the filter records is the dataset's, so a
294    /// [`datatype`](Self::datatype) override is what it follows when the
295    /// stored element is not `T` itself.
296    #[must_use]
297    pub fn shuffle(mut self) -> Self {
298        self.shuffle = true;
299        self
300    }
301
302    /// Enable shuffle + deflate compression — the same pipeline as
303    /// `.shuffle().deflate(level)`.
304    ///
305    /// Shuffle reorders bytes by position within elements before compression,
306    /// which typically improves compression ratios for numeric data.
307    /// Requires chunked storage.
308    #[must_use]
309    pub fn shuffle_deflate(mut self, level: u32) -> Self {
310        self.shuffle = true;
311        self.deflate_level = Some(level);
312        self
313    }
314
315    /// Enable Zstandard compression with the given level (1-22, default 3).
316    ///
317    /// Requires chunked storage (call `.chunk()` before `.create()`).
318    #[must_use]
319    pub fn zstd(mut self, level: u32) -> Self {
320        self.custom_pipeline = Some(crate::format::messages::filter::FilterPipeline::zstd(level));
321        self
322    }
323
324    /// Set a custom filter pipeline for compression.
325    ///
326    /// This takes precedence over [`deflate`](Self::deflate) and
327    /// [`shuffle_deflate`](Self::shuffle_deflate). Requires chunked storage.
328    #[must_use]
329    pub fn filter_pipeline(
330        mut self,
331        pipeline: crate::format::messages::filter::FilterPipeline,
332    ) -> Self {
333        self.custom_pipeline = Some(pipeline);
334        self
335    }
336
337    /// Override the stored element datatype.
338    ///
339    /// By default the dataset is created with the datatype derived from the
340    /// Rust type parameter `T` ([`H5Type::hdf5_type`]). Use this to store a
341    /// different on-disk datatype than the in-memory element type — for
342    /// example a reduced-precision fixed-point type that matches an N-bit
343    /// filter (see [`FilterPipeline::nbit`]). The element *byte* size of the
344    /// override must equal `T::element_size()`; the N-bit filter packs the
345    /// significant bits within that fixed footprint.
346    ///
347    /// [`H5Type::hdf5_type`]: crate::H5Type::hdf5_type
348    /// [`FilterPipeline::nbit`]: crate::FilterPipeline::nbit
349    #[must_use]
350    pub fn datatype(mut self, dt: crate::format::messages::datatype::DatatypeMessage) -> Self {
351        self.datatype_override = Some(dt);
352        self
353    }
354
355    /// Build the dataset on the committed (named) datatype at `path` —
356    /// h5py's `dtype=f["name"]`, `H5Dcreate2` with a committed type id.
357    ///
358    /// The dataset does not describe its type: its header stores a pointer to
359    /// that object, so the type is defined once and every dataset sharing it
360    /// is guaranteed to agree. The type comes from the committed object, so
361    /// this supersedes both `T` and [`datatype`](Self::datatype).
362    ///
363    /// The path is resolved at [`create`](Self::create), which fails when no
364    /// committed datatype is there — commit it with
365    /// [`H5File::commit_datatype`](crate::file::H5File::commit_datatype)
366    /// first.
367    ///
368    /// ```no_run
369    /// # use rust_hdf5::H5File;
370    /// # use rust_hdf5::format::messages::datatype::DatatypeMessage;
371    /// let file = H5File::create("committed.h5").unwrap();
372    /// file.commit_datatype("temperature", DatatypeMessage::f64_type()).unwrap();
373    /// file.new_dataset::<f64>()
374    ///     .committed_type("temperature")
375    ///     .shape([4])
376    ///     .create("readings")
377    ///     .unwrap();
378    /// ```
379    #[must_use]
380    pub fn committed_type(mut self, path: &str) -> Self {
381        self.committed_type = Some(path.to_string());
382        self
383    }
384
385    /// Store object references — h5py's `h5py.ref_dtype`.
386    ///
387    /// The elements are written with
388    /// [`write_object_references`](H5Dataset::write_object_references) and
389    /// name objects by path. The element width is the file's address size, so
390    /// the datatype is resolved at [`create`](Self::create) rather than here;
391    /// it overrides both `T` and any [`datatype`](Self::datatype) call.
392    ///
393    /// ```no_run
394    /// # use rust_hdf5::H5File;
395    /// let file = H5File::create("refs.h5").unwrap();
396    /// file.new_dataset::<i32>().shape([4]).create("target").unwrap();
397    /// let refs = file.new_dataset::<u64>()
398    ///     .object_references()
399    ///     .shape([1])
400    ///     .create("refs")
401    ///     .unwrap();
402    /// refs.write_object_references(&["/target"]).unwrap();
403    /// file.close().unwrap();
404    /// ```
405    #[must_use]
406    pub fn object_references(mut self) -> Self {
407        self.references = Some(ReferenceElement::Object);
408        self
409    }
410
411    /// Store revised object references — the 1.12 `H5T_STD_REF`.
412    ///
413    /// Same paths and same [`write_object_references`](H5Dataset::write_object_references)
414    /// call as [`object_references`](Self::object_references); only the stored
415    /// element differs, carrying the reference's kind alongside the address so
416    /// one datatype can hold every reference kind. h5py cannot read it, so
417    /// prefer the pre-1.12 form for files h5py will open.
418    ///
419    /// ```no_run
420    /// # use rust_hdf5::H5File;
421    /// let file = H5File::create("stdrefs.h5").unwrap();
422    /// file.new_dataset::<i32>().shape([4]).create("target").unwrap();
423    /// let refs = file.new_dataset::<u64>()
424    ///     .std_object_references()
425    ///     .shape([1])
426    ///     .create("refs")
427    ///     .unwrap();
428    /// refs.write_object_references(&["/target"]).unwrap();
429    /// file.close().unwrap();
430    /// ```
431    #[must_use]
432    pub fn std_object_references(mut self) -> Self {
433        self.references = Some(ReferenceElement::Revised);
434        self
435    }
436
437    /// Store revised region references — `H5R_DATASET_REGION2`, written into
438    /// the same `H5T_STD_REF` datatype
439    /// [`std_object_references`](Self::std_object_references) makes.
440    ///
441    /// The elements are written with
442    /// [`write_std_region_references`](H5Dataset::write_std_region_references).
443    /// What distinguishes them from the pre-1.12
444    /// [`region_references`](Self::region_references) is the element, not the
445    /// datatype: a 1.12 element names its own kind, so one dataset of this type
446    /// may hold object, region and attribute references together. h5py 3.15
447    /// cannot read any of them, so prefer the pre-1.12 form for files h5py will
448    /// open.
449    ///
450    /// ```no_run
451    /// # use rust_hdf5::{H5File, Hyperslab, HyperslabBlock, LibverBound, Selection};
452    /// let file = H5File::options().libver(LibverBound::V112).create("stdregions.h5").unwrap();
453    /// file.new_dataset::<i32>().shape([8]).create("target").unwrap();
454    /// let refs = file.new_dataset::<u64>()
455    ///     .std_region_references()
456    ///     .shape([1])
457    ///     .create("refs")
458    ///     .unwrap();
459    /// let rows = Selection::Hyperslab {
460    ///     rank: 1,
461    ///     form: Hyperslab::Blocks(vec![HyperslabBlock { start: vec![0], end: vec![2] }]),
462    /// };
463    /// refs.write_std_region_references(&[("/target", rows)]).unwrap();
464    /// file.close().unwrap();
465    /// ```
466    #[must_use]
467    pub fn std_region_references(self) -> Self {
468        self.std_object_references()
469    }
470
471    /// Store attribute references — `H5R_ATTR`, the one reference kind with no
472    /// pre-1.12 form, in the same `H5T_STD_REF` datatype
473    /// [`std_object_references`](Self::std_object_references) makes.
474    ///
475    /// The elements are written with
476    /// [`write_attribute_references`](H5Dataset::write_attribute_references) and
477    /// name an object and one of its attributes. h5py 3.15 cannot read them.
478    ///
479    /// ```no_run
480    /// # use rust_hdf5::{H5File, LibverBound};
481    /// let file = H5File::options().libver(LibverBound::V112).create("attrrefs.h5").unwrap();
482    /// let target = file.new_dataset::<i32>().shape([4]).create("target").unwrap();
483    /// target.new_attr::<i32>().shape([3]).create("note").unwrap()
484    ///     .write_array(&[7i32, 8, 9]).unwrap();
485    /// let refs = file.new_dataset::<u64>()
486    ///     .attribute_references()
487    ///     .shape([1])
488    ///     .create("refs")
489    ///     .unwrap();
490    /// refs.write_attribute_references(&[("/target", "note")]).unwrap();
491    /// file.close().unwrap();
492    /// ```
493    #[must_use]
494    pub fn attribute_references(self) -> Self {
495        self.std_object_references()
496    }
497
498    /// Store dataset region references — h5py's `h5py.regionref_dtype`.
499    ///
500    /// The elements are written with
501    /// [`write_region_references`](H5Dataset::write_region_references) and name
502    /// a dataset plus a selection over it. The element is a global-heap id, so
503    /// its width follows the file's address size and the datatype is resolved
504    /// at [`create`](Self::create) rather than here; it overrides both `T` and
505    /// any [`datatype`](Self::datatype) call.
506    ///
507    /// ```no_run
508    /// # use rust_hdf5::{H5File, Hyperslab, HyperslabBlock, Selection};
509    /// let file = H5File::create("regions.h5").unwrap();
510    /// file.new_dataset::<i32>().shape([8]).create("target").unwrap();
511    /// let refs = file.new_dataset::<u64>()
512    ///     .region_references()
513    ///     .shape([1])
514    ///     .create("refs")
515    ///     .unwrap();
516    /// let rows = Selection::Hyperslab {
517    ///     rank: 1,
518    ///     form: Hyperslab::Blocks(vec![HyperslabBlock { start: vec![0], end: vec![2] }]),
519    /// };
520    /// refs.write_region_references(&[("/target", rows)]).unwrap();
521    /// file.close().unwrap();
522    /// ```
523    #[must_use]
524    pub fn region_references(mut self) -> Self {
525        self.references = Some(ReferenceElement::Region);
526        self
527    }
528
529    /// Keep the raw data in files outside this one — `H5Pset_external`,
530    /// h5py's `external=[(name, offset, size)]`.
531    ///
532    /// Each entry is a file name, the byte offset in it where that entry's
533    /// region starts, and how many bytes of the dataset it holds; the entries
534    /// concatenate, in order, into the dataset's bytes and together must cover
535    /// them. A relative name is resolved against `HDF5_EXTFILE_PREFIX` the way
536    /// libhdf5 resolves it, so the same name reads back through this crate and
537    /// through h5py. The storage is contiguous by definition, which rules out
538    /// [`chunk`](Self::chunk), a filter, [`compact`](Self::compact),
539    /// [`null`](Self::null) and either reference kind.
540    ///
541    /// The named files are created on first write and never truncated, so
542    /// several datasets may own disjoint ranges of one file.
543    ///
544    /// The last entry may take the unlimited size
545    /// [`external_file_list::UNLIMITED`](crate::format::messages::external_file_list::UNLIMITED)
546    /// (`H5O_EFL_UNLIMITED`), which makes it absorb the whole rest of the
547    /// dataset however far it grows. A dataset whose
548    /// [`max_shape`](Self::max_shape) is unlimited must have one, since no
549    /// finite reservation could cover it, and only the first dimension may be
550    /// extendible — both `H5D__efl_construct`'s rules.
551    ///
552    /// ```no_run
553    /// # use rust_hdf5::H5File;
554    /// let file = H5File::create("ext.h5").unwrap();
555    /// let ds = file.new_dataset::<i32>()
556    ///     .shape([16])
557    ///     .external(&[("ext.raw", 0, 64)])
558    ///     .create("data")
559    ///     .unwrap();
560    /// ds.write_raw(&(0..16i32).collect::<Vec<_>>()).unwrap();
561    /// ```
562    #[must_use]
563    pub fn external(mut self, files: &[(&str, u64, u64)]) -> Self {
564        self.external = Some(
565            files
566                .iter()
567                .map(|&(name, offset, size)| (name.to_string(), offset, size))
568                .collect(),
569        );
570        self
571    }
572
573    /// `H5Pset_efile_prefix` on the dapl `H5Dcreate2` takes — the directory
574    /// the raw data files named by [`external`](Self::external) are created
575    /// under, and looked for under on every later write through this handle.
576    ///
577    /// `H5D__create` builds `dset->shared->extfile_prefix` from the dapl
578    /// (H5Dint.c:1318) and `H5D__efl_write` joins each slot name against it
579    /// with the same single-path `H5_combine_path` the read side uses
580    /// (H5Defl.c:429-431) — so this decides where the bytes land, and the
581    /// prefix a later reader names must agree for it to find them.
582    ///
583    /// Measured under libhdf5 1.14.6 and 2.0.0: writing through a dapl that
584    /// names a directory creates the raw data file there and nowhere else,
585    /// and a directory that does not exist fails the write outright rather
586    /// than being created.
587    ///
588    /// It shares [`DatasetAccess::efile_prefix`]'s rules, both being
589    /// `H5D__build_file_prefix`: `HDF5_EXTFILE_PREFIX` shadows this outright
590    /// (H5Dint.c:1084-1090), a leading `${ORIGIN}` stands for the directory
591    /// holding the HDF5 file (:1105-1113), and `"."` or `""` means no prefix
592    /// (:1098-1102), which leaves a stored name to resolve against the
593    /// process's current directory.
594    ///
595    /// Ignored by a dataset that names no external files, which has no slot
596    /// name to join.
597    #[must_use]
598    pub fn efile_prefix(mut self, prefix: impl Into<String>) -> Self {
599        self.efile_prefix = Some(prefix.into());
600        self
601    }
602
603    /// Map part of this dataset onto part of a dataset in another file, making
604    /// it virtual — `H5Pset_virtual`, one `VirtualLayout[...] =
605    /// VirtualSource(...)` assignment in h5py.
606    ///
607    /// The arguments are `H5Pset_virtual`'s, in its order: which elements of
608    /// *this* dataset the mapping fills, the file and dataset the data comes
609    /// from, and which elements of that source dataset it comes from. Call it
610    /// once per mapping; they apply in the order given, which is the order
611    /// libhdf5 resolves overlapping ones in.
612    ///
613    /// The source file is named exactly as stored — resolved against
614    /// `HDF5_VDS_PREFIX`, or the virtual dataset's own directory, when the
615    /// file is read — and `"."` means this file. Nothing is opened or checked
616    /// here: a source that does not exist yet is legal, and reads of the
617    /// unmapped or unresolvable parts return the [`fill_value`](Self::fill_value).
618    ///
619    /// A virtual dataset stores nothing of its own, which rules out
620    /// [`chunk`](Self::chunk), a filter, [`compact`](Self::compact),
621    /// [`null`](Self::null), [`external`](Self::external) and either reference
622    /// kind — and makes writing to it an error, since its elements belong to
623    /// the source datasets.
624    ///
625    /// An unlimited (`H5S_UNLIMITED`) selection is written as one: such a
626    /// mapping grows with its source, and the dataset's extent in that
627    /// dimension is whatever the sources reachable when it is opened supply
628    /// (`H5D__virtual_set_extent_unlim`). Give it a
629    /// [`max_shape`](Self::max_shape) unlimited in the same dimension, as
630    /// libhdf5 requires of the dataspace behind one.
631    ///
632    /// A source name may carry libhdf5's `printf`-style substitutions: `%b`
633    /// is the block index and `%%` an escaped literal `%`. One such mapping
634    /// stands for the family of source datasets that fill the successive
635    /// blocks of an unlimited virtual selection, so it is legal only with an
636    /// unlimited virtual selection over a limited source selection, and the
637    /// dataset's extent stops at the first block whose source is missing.
638    ///
639    /// ```no_run
640    /// # use rust_hdf5::{H5File, Selection};
641    /// let file = H5File::create("vds.h5").unwrap();
642    /// let ds = file.new_dataset::<i32>()
643    ///     .shape([16])
644    ///     .virtual_mapping(Selection::All, "src.h5", "src", Selection::All)
645    ///     .create("vds")
646    ///     .unwrap();
647    /// ```
648    #[must_use]
649    pub fn virtual_mapping(
650        mut self,
651        virtual_selection: Selection,
652        source_file: &str,
653        source_dataset: &str,
654        source_selection: Selection,
655    ) -> Self {
656        self.virtual_mappings.push(VirtualMapping {
657            source_file_name: source_file.to_string(),
658            source_dset_name: source_dataset.to_string(),
659            source_selection,
660            virtual_selection,
661        });
662        self
663    }
664
665    /// Set a user-defined fill value for unwritten elements.
666    ///
667    /// Without this, datasets use the HDF5 default zero-fill. When set,
668    /// the value is written into the dataset's fill-value message
669    /// (`fill_defined = 2`), so HDF5 readers treat unallocated chunks and
670    /// unwritten regions as this value rather than zero.
671    ///
672    /// ```no_run
673    /// # use rust_hdf5::H5File;
674    /// let file = H5File::create("fv.h5").unwrap();
675    /// let ds = file.new_dataset::<f32>()
676    ///     .shape(&[100])
677    ///     .fill_value(f32::NAN)
678    ///     .create("data")
679    ///     .unwrap();
680    /// ```
681    #[must_use]
682    pub fn fill_value(mut self, value: T) -> Self {
683        let es = T::element_size();
684        // Safety: `T: H5Type` is a `Copy` numeric primitive with a
685        // well-defined byte representation; `element_size()` matches
686        // `size_of::<T>()`. The slice borrows `value` only for this call.
687        let raw = unsafe { std::slice::from_raw_parts(&value as *const T as *const u8, es) };
688        self.fill_value = Some(raw.to_vec());
689        self
690    }
691
692    /// Set the fill value from one element already encoded in the dataset's
693    /// stored datatype — for a stored type no `T` carries, such as an N-bit
694    /// [`datatype`](Self::datatype) override or a runtime compound.
695    ///
696    /// `bytes` is the element's host-order image; [`create`](Self::create)
697    /// checks its length against the stored datatype's element size and puts
698    /// it in the declared byte order, as [`fill_value`](Self::fill_value)
699    /// does.
700    #[must_use]
701    pub fn fill_value_bytes(mut self, bytes: &[u8]) -> Self {
702        self.fill_value = Some(bytes.to_vec());
703        self
704    }
705
706    /// Set when the fill value is written into allocated storage —
707    /// `H5Pset_fill_time`.
708    ///
709    /// Without this, a dataset gets [`FillTime::IfSet`]
710    /// (`H5D_CRT_FILL_TIME_DEF`), the default every dataset creation
711    /// property list carries. [`FillTime::Never`] applies to a dataset with
712    /// no fill value too: it only stops this writer's own eager tiling of
713    /// the value into newly allocated storage, not the default zero-fill
714    /// that storage already has, so its only observable effect is on a
715    /// dataset that also calls [`fill_value`](Self::fill_value).
716    ///
717    /// ```no_run
718    /// # use rust_hdf5::{FillTime, H5File};
719    /// let file = H5File::create("fv.h5").unwrap();
720    /// let ds = file.new_dataset::<f32>()
721    ///     .shape(&[100])
722    ///     .fill_value(f32::NAN)
723    ///     .fill_time(FillTime::Never)
724    ///     .create("data")
725    ///     .unwrap();
726    /// ```
727    #[must_use]
728    pub fn fill_time(mut self, time: FillTime) -> Self {
729        self.fill_time = Some(time);
730        self
731    }
732
733    /// Finalize and create the dataset with the given `name`.
734    ///
735    /// The name is the link name within the root group (e.g. `"data"` or
736    /// `"group1/data"` once nested groups are supported).
737    pub fn create(self, name: &str) -> Result<H5Dataset> {
738        // A committed type is resolved before the dataset exists and recorded
739        // after, here rather than in each storage path: every path reaches
740        // this one return, so a dataset can never be built on a committed
741        // type and then fail to say so — which would silently write the type
742        // out in full instead of pointing at the object.
743        let committed = self.resolve_committed_type()?;
744        let file_inner = clone_inner(&self.file_inner);
745        let ds = self.create_object(name, committed.as_ref().map(|(_, dt)| dt.clone()))?;
746        if let (Some((share, _)), DatasetInfo::Writer { index, .. }) = (committed, &ds.info) {
747            let inner = borrow_inner(&file_inner);
748            if let H5FileInner::Writer(writer) = &*inner {
749                writer.share_committed_type(*index, share);
750            }
751        }
752        Ok(ds)
753    }
754
755    /// The committed datatype this dataset is built on, with the type it
756    /// holds; `None` when [`committed_type`](Self::committed_type) was not
757    /// called.
758    fn resolve_committed_type(&self) -> Result<Option<(usize, DatatypeMessage)>> {
759        let Some(path) = self.committed_type.as_deref() else {
760            return Ok(None);
761        };
762        if self.references.is_some() {
763            // Both name the stored type and they cannot both be it: the
764            // pointer would say the elements are the committed type while the
765            // reference writers write addresses. True of either reference
766            // kind — an object reference is an address, a region reference is
767            // a global-heap address plus a serialized selection.
768            return Err(Hdf5Error::InvalidState(
769                "a dataset cannot be built on a committed datatype and hold references".into(),
770            ));
771        }
772        let inner = borrow_inner(&self.file_inner);
773        match &*inner {
774            H5FileInner::Writer(writer) => Ok(Some(writer.committed_datatype_for_share(path)?)),
775            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
776                "cannot create a dataset in read mode".into(),
777            )),
778            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
779        }
780    }
781
782    /// Everything [`create`](Self::create) does apart from recording the
783    /// committed-type share; `committed` is the type that object holds.
784    fn create_object(self, name: &str, committed: Option<DatatypeMessage>) -> Result<H5Dataset> {
785        // Build the full name: if created within a group, prefix with group path
786        let full_name = if let Some(ref gp) = self.group_path {
787            if gp == "/" {
788                name.to_string()
789            } else {
790                let trimmed = gp.trim_start_matches('/');
791                format!("{}/{}", trimmed, name)
792            }
793        } else {
794            name.to_string()
795        };
796
797        let datatype = if let Some(kind) = self.references {
798            // The element is measured in file addresses, and only the writer
799            // knows how wide one is for this file.
800            let inner = borrow_inner(&self.file_inner);
801            match &*inner {
802                H5FileInner::Writer(writer) => kind.datatype(writer.ctx()),
803                H5FileInner::Reader(_) => {
804                    return Err(Hdf5Error::InvalidState(
805                        "cannot create a dataset in read mode".into(),
806                    ))
807                }
808                H5FileInner::Closed => {
809                    return Err(Hdf5Error::InvalidState("file is closed".into()))
810                }
811            }
812        } else if let Some(dt) = committed {
813            // The object header holds the type; the dataset stores a pointer
814            // to it, but every size and payload check still needs the type
815            // itself.
816            dt
817        } else {
818            self.datatype_override.clone().unwrap_or_else(T::hdf5_type)
819        };
820        // Size one element from the on-disk datatype, not the carrier `T`. For
821        // the default path this equals `T::element_size()`; when a `datatype()`
822        // override is set (N-bit, or a runtime `CompoundType`), the stored type
823        // — not `T` — defines the element width, so the dataspace, the raw
824        // allocation, and the `write_raw` length check all agree with the bytes
825        // libhdf5/h5py will read.
826        let element_size = datatype.element_size() as usize;
827        // `fill_value` took the host image of a `T` (or `fill_value_bytes` one
828        // element of the stored type); the fill-value message
829        // holds one element in the dataset's own datatype, so it is converted
830        // here — the order is only known once the override is resolved, and
831        // the builder's calls can arrive in either order.
832        let fill_value = match self.fill_value.as_deref() {
833            Some(bytes) if bytes.len() != element_size => {
834                return Err(Hdf5Error::TypeMismatch(format!(
835                    "fill value is {} bytes but datatype {datatype} stores {element_size}",
836                    bytes.len()
837                )))
838            }
839            Some(bytes) => Some(to_stored_byte_order(bytes, &datatype, element_size)?.into_owned()),
840            None => None,
841        };
842
843        let wants_filter =
844            self.custom_pipeline.is_some() || self.shuffle || self.deflate_level.is_some();
845
846        // External storage *is* contiguous storage: the layout message says
847        // contiguous with an undefined address, and the External File List
848        // beside it says where the bytes really are. Every other storage class
849        // names bytes of its own, so none of them can also name these.
850        if self.external.is_some() {
851            if self.chunk_dims.is_some() || wants_filter || self.is_compact || self.is_null {
852                return Err(Hdf5Error::InvalidState(
853                    "a dataset whose raw data lives in external files is contiguous, so it \
854                     cannot also be chunked, filtered, compact or NULL"
855                        .into(),
856                ));
857            }
858            if self.references.is_some() {
859                return Err(Hdf5Error::InvalidState(
860                    "object and region references are stamped into the dataset's own \
861                     contiguous block, which a dataset stored in external files has none of"
862                        .into(),
863                ));
864            }
865        }
866
867        // A virtual dataset stores nothing of its own — its elements are read
868        // out of the datasets its mappings name — so it can be none of the
869        // storage classes that do, and there is no block for a reference
870        // writer to stamp into either.
871        if !self.virtual_mappings.is_empty() {
872            if self.chunk_dims.is_some()
873                || wants_filter
874                || self.is_compact
875                || self.is_null
876                || self.external.is_some()
877            {
878                return Err(Hdf5Error::InvalidState(
879                    "a virtual dataset's elements live in the datasets its mappings name, \
880                     so it cannot also be chunked, filtered, compact, NULL or stored in \
881                     external files"
882                        .into(),
883                ));
884            }
885            if self.references.is_some() {
886                return Err(Hdf5Error::InvalidState(
887                    "object and region references are stamped into the dataset's own \
888                     contiguous block, which a virtual dataset has none of"
889                        .into(),
890                ));
891            }
892        }
893
894        if self.is_null {
895            // A NULL dataspace holds no elements at all: no chunk grid to
896            // scatter into, no raw image to put in an object header, no fill
897            // value to apply to unwritten elements (there are none), matching
898            // upstream's rejection of these combinations (`H5Dchunk.c`'s
899            // chunked-layout dataspace check).
900            if self.chunk_dims.is_some() || wants_filter || self.is_compact {
901                return Err(Hdf5Error::InvalidState(
902                    "a NULL dataspace dataset cannot be chunked, filtered or compact".into(),
903                ));
904            }
905            if fill_value.is_some() {
906                return Err(Hdf5Error::InvalidState(
907                    "a NULL dataspace dataset cannot have a fill value".into(),
908                ));
909            }
910            if self.fill_time.is_some() {
911                return Err(Hdf5Error::InvalidState(
912                    "a NULL dataspace dataset cannot have a fill time".into(),
913                ));
914            }
915
916            let index = {
917                let inner = borrow_inner(&self.file_inner);
918                match &*inner {
919                    H5FileInner::Writer(writer) => {
920                        let idx = writer.create_null_dataset(&full_name, datatype)?;
921                        if let Some(ref gp) = self.group_path {
922                            if gp != "/" {
923                                writer.assign_dataset_to_group(gp, idx)?;
924                            }
925                        }
926                        idx
927                    }
928                    H5FileInner::Reader(_) => {
929                        return Err(Hdf5Error::InvalidState(
930                            "cannot create a dataset in read mode".into(),
931                        ));
932                    }
933                    H5FileInner::Closed => {
934                        return Err(Hdf5Error::InvalidState("file is closed".into()));
935                    }
936                }
937            };
938
939            return Ok(H5Dataset {
940                file_inner: clone_inner(&self.file_inner),
941                info: DatasetInfo::Writer {
942                    index,
943                    shape: Vec::new(),
944                    element_size,
945                    chunk_index: None,
946                    is_null: true,
947                },
948                _open: None,
949            });
950        }
951
952        let shape = self.shape.ok_or_else(|| {
953            Hdf5Error::InvalidState("shape must be set before calling create()".into())
954        })?;
955        let dims_u64: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
956
957        if self.is_compact {
958            // The raw data is the layout message, so there is no chunk grid to
959            // filter and no room to grow into: `H5D__compact_construct` refuses
960            // a max dimension above the current one, and `H5Pset_layout` and
961            // `H5Pset_chunk` overwrite each other rather than combining.
962            if self.chunk_dims.is_some() || wants_filter {
963                return Err(Hdf5Error::InvalidState(
964                    "a compact dataset stores its data in the object header, so it \
965                     cannot be chunked or filtered"
966                        .into(),
967                ));
968            }
969            if self
970                .max_shape
971                .as_ref()
972                .is_some_and(|max| max.iter().zip(&shape).any(|(m, &d)| *m != Some(d)))
973            {
974                return Err(Hdf5Error::InvalidState(
975                    "a compact dataset cannot be extendible: its maximum shape must \
976                     equal its shape"
977                        .into(),
978                ));
979            }
980
981            let index = {
982                let inner = borrow_inner(&self.file_inner);
983                match &*inner {
984                    H5FileInner::Writer(writer) => {
985                        let idx = writer.create_compact_dataset(&full_name, datatype, &dims_u64)?;
986                        // Set before the fill value: NEVER must be in place
987                        // before that call decides whether to eager-tile it.
988                        if let Some(time) = self.fill_time {
989                            writer.set_dataset_fill_time(idx, time.wire_byte())?;
990                        }
991                        if let Some(ref fv) = fill_value {
992                            writer.set_dataset_fill_value(idx, fv.clone())?;
993                        }
994                        idx
995                    }
996                    H5FileInner::Reader(_) => {
997                        return Err(Hdf5Error::InvalidState(
998                            "cannot create a dataset in read mode".into(),
999                        ));
1000                    }
1001                    H5FileInner::Closed => {
1002                        return Err(Hdf5Error::InvalidState("file is closed".into()));
1003                    }
1004                }
1005            };
1006
1007            return Ok(H5Dataset {
1008                file_inner: clone_inner(&self.file_inner),
1009                info: DatasetInfo::Writer {
1010                    index,
1011                    shape,
1012                    element_size,
1013                    chunk_index: None,
1014                    is_null: false,
1015                },
1016                _open: None,
1017            });
1018        }
1019
1020        if !self.virtual_mappings.is_empty() {
1021            let index = {
1022                let inner = borrow_inner(&self.file_inner);
1023                match &*inner {
1024                    H5FileInner::Writer(writer) => {
1025                        let idx = writer.create_virtual_dataset(
1026                            &full_name,
1027                            datatype,
1028                            &dims_u64,
1029                            self.max_shape
1030                                .as_ref()
1031                                .map(|max| {
1032                                    max.iter()
1033                                        .map(|m| m.map_or(u64::MAX, |v| v as u64))
1034                                        .collect::<Vec<u64>>()
1035                                })
1036                                .as_deref(),
1037                            &self.virtual_mappings,
1038                        )?;
1039                        // The fill value is what a read of an unmapped — or
1040                        // unresolvable — element returns, so it is the one
1041                        // dataset property a virtual dataset carries about its
1042                        // own elements. Nothing is tiled into storage: it has
1043                        // none.
1044                        // Set before the fill value: NEVER must be in place
1045                        // before that call decides whether to eager-tile it.
1046                        if let Some(time) = self.fill_time {
1047                            writer.set_dataset_fill_time(idx, time.wire_byte())?;
1048                        }
1049                        if let Some(ref fv) = fill_value {
1050                            writer.set_dataset_fill_value(idx, fv.clone())?;
1051                        }
1052                        idx
1053                    }
1054                    H5FileInner::Reader(_) => {
1055                        return Err(Hdf5Error::InvalidState(
1056                            "cannot create a dataset in read mode".into(),
1057                        ));
1058                    }
1059                    H5FileInner::Closed => {
1060                        return Err(Hdf5Error::InvalidState("file is closed".into()));
1061                    }
1062                }
1063            };
1064
1065            return Ok(H5Dataset {
1066                file_inner: clone_inner(&self.file_inner),
1067                info: DatasetInfo::Writer {
1068                    index,
1069                    shape,
1070                    element_size,
1071                    chunk_index: None,
1072                    is_null: false,
1073                },
1074                _open: None,
1075            });
1076        }
1077
1078        // A filter pipeline requires chunked storage. When a filter is
1079        // requested without explicit chunk dimensions, store the whole
1080        // dataset as a single chunk instead of silently dropping the filter
1081        // on the contiguous path. (This is one whole-dataset chunk, not
1082        // h5py's ~1 MiB chunk-size heuristic; pass explicit chunk dimensions
1083        // for large datasets.)
1084        let auto_chunk: Option<Vec<usize>> =
1085            if self.chunk_dims.is_none() && wants_filter && !shape.is_empty() {
1086                Some(shape.iter().map(|&d| d.max(1)).collect())
1087            } else {
1088                None
1089            };
1090
1091        if let Some(chunk_dims) = self.chunk_dims.as_ref().or(auto_chunk.as_ref()) {
1092            // Chunked dataset
1093            let chunk_u64: Vec<u64> = chunk_dims.iter().map(|&d| d as u64).collect();
1094            let max_u64: Vec<u64> = if let Some(ref max) = self.max_shape {
1095                max.iter()
1096                    .map(|m| m.map_or(u64::MAX, |v| v as u64))
1097                    .collect()
1098            } else {
1099                // Default: max = current
1100                dims_u64.clone()
1101            };
1102
1103            // The file's format settles the question before the shape gets a
1104            // say. `H5D__chunk_set_info` reaches the index-selection block
1105            // only once the data layout message is at version 4 (H5Dchunk.c:936)
1106            // — which the file's library-version bound decides, not the
1107            // dataspace — and below it the version-3 message carries a
1108            // version-1 B-tree and nothing else. The writer owns that reading
1109            // of `H5O_layout_ver_bounds`; the chunk's byte count is the one
1110            // input from here, a chunk over 4 GiB being the one thing that
1111            // forces the newer message whatever the bound says.
1112            let chunk_bytes = chunk_u64.iter().product::<u64>() * element_size as u64;
1113            let v110_indexing = match &*borrow_inner(&self.file_inner) {
1114                H5FileInner::Writer(writer) => writer.uses_v110_chunk_indexing(chunk_bytes),
1115                // Neither can create a dataset at all; the creator below
1116                // reports which of the two it is.
1117                _ => true,
1118            };
1119
1120            // Inside the block libhdf5 selects the chunk index from the
1121            // dataspace and the creation properties, in this order
1122            // (`H5D__chunk_set_info`, H5Dchunk.c:955): a v2 B-tree for two or
1123            // more unlimited dimensions, an extensible array for exactly one;
1124            // for a fixed shape, the single-chunk index takes priority —
1125            // unconditional of filter or allocation time — whenever the shape
1126            // is exactly one whole chunk, ahead of the implicit index (no
1127            // filter, and early allocation, which is what puts every chunk at
1128            // a computable address) and the fixed array (everything else).
1129            let n_unlimited = max_u64.iter().filter(|&&m| m == u64::MAX).count();
1130            let one_chunk = chunk_u64 == dims_u64 && max_u64 == dims_u64;
1131            let kind = if !v110_indexing {
1132                ChunkIndexKind::BtreeV1
1133            } else if n_unlimited >= 2 {
1134                ChunkIndexKind::BtreeV2
1135            } else if n_unlimited == 1 {
1136                ChunkIndexKind::ExtensibleArray
1137            } else if one_chunk {
1138                ChunkIndexKind::SingleChunk
1139            } else if self.early_allocation && !wants_filter {
1140                ChunkIndexKind::Implicit
1141            } else {
1142                ChunkIndexKind::FixedArray
1143            };
1144
1145            let index = {
1146                let inner = borrow_inner(&self.file_inner);
1147                match &*inner {
1148                    H5FileInner::Writer(writer) => {
1149                        // The requested filter pipeline, if any. Every index
1150                        // builds it from the same options, so one owner
1151                        // resolves it: a second construction site is what let
1152                        // a request naming no compressor — shuffle on its own
1153                        // — fall through to unfiltered storage.
1154                        let explicit_pipeline = || {
1155                            use crate::format::messages::filter::FilterPipeline;
1156                            if let Some(p) = self.custom_pipeline.clone() {
1157                                return p;
1158                            }
1159                            // Shuffle records the width of the element it
1160                            // permutes, which is the stored one — a `datatype`
1161                            // override moves that away from `T`.
1162                            let es = element_size as u32;
1163                            match (self.shuffle, self.deflate_level) {
1164                                (true, Some(level)) => FilterPipeline::shuffle_deflate(es, level),
1165                                (true, None) => FilterPipeline::shuffle(es),
1166                                // deflate_level (checked by wants_filter).
1167                                (false, level) => FilterPipeline::deflate(level.unwrap()),
1168                            }
1169                        };
1170                        let idx = if kind == ChunkIndexKind::BtreeV1 {
1171                            // The classic index, which takes the pipeline the
1172                            // same way the others do — and is refused with it
1173                            // in a classic file, whose filter pipeline
1174                            // message is a version this crate does not write.
1175                            let pipeline = wants_filter.then(explicit_pipeline);
1176                            writer.create_btree_v1_dataset(
1177                                &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, pipeline,
1178                            )?
1179                        } else if kind == ChunkIndexKind::BtreeV2 {
1180                            // Two or more unlimited dimensions: a v2 B-tree,
1181                            // whose records carry the stored size and filter
1182                            // mask when the dataset is compressed (libhdf5
1183                            // H5D_BT2_FILT).
1184                            if wants_filter {
1185                                writer.create_btree_v2_dataset_with_pipeline(
1186                                    &full_name,
1187                                    datatype,
1188                                    &dims_u64,
1189                                    &max_u64,
1190                                    &chunk_u64,
1191                                    explicit_pipeline(),
1192                                )?
1193                            } else {
1194                                writer.create_btree_v2_dataset(
1195                                    &full_name, datatype, &dims_u64, &max_u64, &chunk_u64,
1196                                )?
1197                            }
1198                        } else if kind == ChunkIndexKind::Implicit {
1199                            // No index structure at all: every chunk of the
1200                            // grid is allocated at create in one run, so
1201                            // there is no pipeline arm — a filter is what
1202                            // makes chunks different sizes, and this index
1203                            // has no room to say so.
1204                            writer.create_implicit_dataset(
1205                                &full_name, datatype, &dims_u64, &chunk_u64,
1206                            )?
1207                        } else if kind == ChunkIndexKind::SingleChunk {
1208                            // A fixed shape covered by exactly one chunk:
1209                            // libhdf5 picks this index ahead of Implicit and
1210                            // Fixed Array regardless of filter or allocation
1211                            // time. Filtered or not, it takes the same
1212                            // explicit pipeline the other indexes do; a
1213                            // filtered chunk's stored size isn't known ahead
1214                            // of its first write, so early allocation only
1215                            // ever applies to the unfiltered form.
1216                            if wants_filter {
1217                                writer.create_single_chunk_dataset_with_pipeline(
1218                                    &full_name,
1219                                    datatype,
1220                                    &dims_u64,
1221                                    &chunk_u64,
1222                                    explicit_pipeline(),
1223                                )?
1224                            } else {
1225                                writer.create_single_chunk_dataset(
1226                                    &full_name,
1227                                    datatype,
1228                                    &dims_u64,
1229                                    &chunk_u64,
1230                                    self.early_allocation,
1231                                )?
1232                            }
1233                        } else if kind == ChunkIndexKind::FixedArray {
1234                            // A chunked dataset with no unlimited dimension
1235                            // must use the fixed-array index — libhdf5
1236                            // rejects an extensible-array index here. A
1237                            // compressed fixed-shape dataset uses a *filtered*
1238                            // fixed array (FA client id 1). The maximum shape
1239                            // sizes the array, so a finite max above the
1240                            // current shape stays growable.
1241                            let pipeline = wants_filter.then(explicit_pipeline);
1242                            writer.create_fixed_array_dataset_with_max(
1243                                &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, pipeline,
1244                            )?
1245                        } else if wants_filter {
1246                            // The extensible-array index takes the pipeline
1247                            // the same way, so it goes through the one owner
1248                            // too.
1249                            writer.create_chunked_dataset_with_pipeline(
1250                                &full_name,
1251                                datatype,
1252                                &dims_u64,
1253                                &max_u64,
1254                                &chunk_u64,
1255                                explicit_pipeline(),
1256                            )?
1257                        } else {
1258                            writer.create_chunked_dataset(
1259                                &full_name, datatype, &dims_u64, &max_u64, &chunk_u64,
1260                            )?
1261                        };
1262                        // Set before the fill value: NEVER must be in place
1263                        // before that call decides whether to eager-tile it.
1264                        if let Some(time) = self.fill_time {
1265                            writer.set_dataset_fill_time(idx, time.wire_byte())?;
1266                        }
1267                        if let Some(ref fv) = fill_value {
1268                            writer.set_dataset_fill_value(idx, fv.clone())?;
1269                        }
1270                        idx
1271                    }
1272                    H5FileInner::Reader(_) => {
1273                        return Err(Hdf5Error::InvalidState(
1274                            "cannot create a dataset in read mode".into(),
1275                        ));
1276                    }
1277                    H5FileInner::Closed => {
1278                        return Err(Hdf5Error::InvalidState("file is closed".into()));
1279                    }
1280                }
1281            };
1282
1283            Ok(H5Dataset {
1284                file_inner: clone_inner(&self.file_inner),
1285                info: DatasetInfo::Writer {
1286                    index,
1287                    shape,
1288                    element_size,
1289                    chunk_index: Some(kind),
1290                    is_null: false,
1291                },
1292                _open: None,
1293            })
1294        } else {
1295            // Contiguous dataset (original path)
1296            let efile_access = match self.efile_prefix.as_deref() {
1297                Some(p) => DatasetAccess::new().efile_prefix(p),
1298                None => DatasetAccess::new(),
1299            };
1300            let (index, open) = {
1301                let inner = borrow_inner(&self.file_inner);
1302                match &*inner {
1303                    H5FileInner::Writer(writer) => {
1304                        let idx = match self.external.as_deref() {
1305                            Some(files) => {
1306                                let slots: Vec<(&str, u64, u64)> = files
1307                                    .iter()
1308                                    .map(|(name, offset, size)| (name.as_str(), *offset, *size))
1309                                    .collect();
1310                                writer.create_external_dataset(
1311                                    &full_name,
1312                                    datatype,
1313                                    &dims_u64,
1314                                    self.max_shape
1315                                        .as_ref()
1316                                        .map(|max| {
1317                                            max.iter()
1318                                                .map(|m| m.map_or(u64::MAX, |v| v as u64))
1319                                                .collect::<Vec<u64>>()
1320                                        })
1321                                        .as_deref(),
1322                                    &slots,
1323                                )?
1324                            }
1325                            None => writer.create_dataset(&full_name, datatype, &dims_u64)?,
1326                        };
1327                        // Before anything that can write raw bytes: the
1328                        // prefix an external dataset's slot names are joined
1329                        // against is settled by the create, as
1330                        // `H5D__build_file_prefix` settles it for
1331                        // `H5D__create` (H5Dint.c:1318).
1332                        let open = writer.bind_efile_prefix(idx, &efile_access)?;
1333                        // Set before the fill value: NEVER must be in place
1334                        // before that call decides whether to eager-tile it.
1335                        if let Some(time) = self.fill_time {
1336                            writer.set_dataset_fill_time(idx, time.wire_byte())?;
1337                        }
1338                        if let Some(ref fv) = fill_value {
1339                            writer.set_dataset_fill_value(idx, fv.clone())?;
1340                        }
1341                        (idx, open)
1342                    }
1343                    H5FileInner::Reader(_) => {
1344                        return Err(Hdf5Error::InvalidState(
1345                            "cannot create a dataset in read mode".into(),
1346                        ));
1347                    }
1348                    H5FileInner::Closed => {
1349                        return Err(Hdf5Error::InvalidState("file is closed".into()));
1350                    }
1351                }
1352            };
1353
1354            Ok(H5Dataset {
1355                file_inner: clone_inner(&self.file_inner),
1356                info: DatasetInfo::Writer {
1357                    index,
1358                    shape,
1359                    element_size,
1360                    chunk_index: None,
1361                    is_null: false,
1362                },
1363                _open: open,
1364            })
1365        }
1366    }
1367}
1368
1369// ---------------------------------------------------------------------------
1370// DatasetInfo
1371// ---------------------------------------------------------------------------
1372
1373/// Internal metadata about a dataset handle.
1374enum DatasetInfo {
1375    /// A dataset created via `new_dataset().create()` in write mode.
1376    Writer {
1377        /// Index into the writer's dataset list.
1378        index: usize,
1379        /// Shape (current dimensions).
1380        shape: Vec<usize>,
1381        /// Size of one element in bytes.
1382        element_size: usize,
1383        /// Which chunk index this dataset uses, `None` when its storage is
1384        /// not chunked. One field rather than a flag per index: a dataset
1385        /// has exactly one chunk index, and the flags could spell
1386        /// combinations ("not chunked, but indexed by a v2 B-tree") that no
1387        /// dataset has — which is what a write path reading only some of
1388        /// them turns into a write to the wrong index.
1389        chunk_index: Option<ChunkIndexKind>,
1390        /// Whether this is a NULL dataspace (no elements at all — distinct
1391        /// from a scalar, which holds exactly one). Always `false` when
1392        /// `chunk_index` is `Some`: a NULL dataspace can never be chunked.
1393        is_null: bool,
1394    },
1395    /// A dataset opened by name in read mode.
1396    Reader {
1397        /// The link name of the dataset.
1398        name: String,
1399        /// Shape (current dimensions).
1400        shape: Vec<usize>,
1401        /// Size of one element in bytes.
1402        element_size: usize,
1403    },
1404}
1405
1406// ---------------------------------------------------------------------------
1407// H5Dataset
1408// ---------------------------------------------------------------------------
1409
1410/// A handle to an HDF5 dataset, supporting typed read and write operations.
1411///
1412/// The dataset holds a shared reference to the file's I/O backend, so it
1413/// remains valid even if the originating [`H5File`](crate::file::H5File) is
1414/// moved or dropped (they share ownership via `Rc`).
1415pub struct H5Dataset {
1416    file_inner: SharedInner,
1417    info: DatasetInfo,
1418    /// Keeps this dataset's *open* alive for as long as the handle is, so
1419    /// the reader or the writer can tell whether a later open of the same
1420    /// name joins this one or starts fresh — libhdf5's `H5FO_opened`
1421    /// shared-info count (H5Dint.c:1496-1500). `None` for a dataset with no
1422    /// per-open answer to hold: in write mode, one whose raw data is in this
1423    /// file rather than in the files an external file list names.
1424    ///
1425    /// Held, never read: its whole job is to keep the reader's `Weak` on it
1426    /// upgradable until this handle goes away.
1427    _open: Option<crate::io::reader::DatasetOpenToken>,
1428}
1429
1430impl Drop for H5Dataset {
1431    /// Closing a virtual dataset's last handle closes the source files that
1432    /// open was holding, which is what `H5D__virtual_reset_layout` does at
1433    /// the last `H5Dclose` (H5Dvirtual.c:709-710, closing each
1434    /// `source_dset->dset` at :955 and with it the file that dataset kept
1435    /// open). Nothing else in this crate can end a virtual open, so this is
1436    /// where the reader is told.
1437    ///
1438    /// The token is dropped *before* the reader is asked, so the reader's
1439    /// `Weak` already reads dead for the handle going away here. A write-mode
1440    /// handle's token belongs to the writer's own external file prefix, which
1441    /// has no source files to close, so it takes no lock either.
1442    fn drop(&mut self) {
1443        let Some(open) = self._open.take() else {
1444            return;
1445        };
1446        drop(open);
1447        if matches!(self.info, DatasetInfo::Writer { .. }) {
1448            return;
1449        }
1450        let Some(mut inner) = crate::file::try_borrow_inner_mut(&self.file_inner) else {
1451            return;
1452        };
1453        if let crate::file::H5FileInner::Reader(reader) = &mut *inner {
1454            reader.release_closed_virtual_sources();
1455        }
1456    }
1457}
1458
1459/// The byte order this build reads and writes natively.
1460pub(crate) const HOST_BYTE_ORDER: ByteOrder = if cfg!(target_endian = "big") {
1461    ByteOrder::BigEndian
1462} else {
1463    ByteOrder::LittleEndian
1464};
1465
1466/// The byte order this build does not read or write natively.
1467pub(crate) const FOREIGN_BYTE_ORDER: ByteOrder = match HOST_BYTE_ORDER {
1468    ByteOrder::LittleEndian => ByteOrder::BigEndian,
1469    ByteOrder::BigEndian => ByteOrder::LittleEndian,
1470};
1471
1472/// What a typed access has to do with an element image of a given datatype.
1473#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1474enum ByteOrderAction {
1475    /// Stored order is the host's: the image is already the typed value.
1476    Keep,
1477    /// The whole element is one scalar in the foreign order: reverse it.
1478    SwapElements,
1479    /// A composite storing something in the foreign order.
1480    Refuse,
1481}
1482
1483/// Classify a datatype for a typed access of element width `width`.
1484///
1485/// The single owner of the rule; both directions ask it, so a type a read
1486/// converts is exactly a type a write converts.
1487///
1488/// A composite element cannot be swapped as a unit — its members have their
1489/// own orders and offsets — so one that touches the foreign order is refused
1490/// rather than silently passed through in the wrong order.
1491fn byte_order_action(datatype: &DatatypeMessage, width: usize) -> ByteOrderAction {
1492    match datatype.scalar_byte_order() {
1493        Some(order) if order == FOREIGN_BYTE_ORDER && width > 1 => ByteOrderAction::SwapElements,
1494        Some(_) => ByteOrderAction::Keep,
1495        None if datatype.contains_byte_order(FOREIGN_BYTE_ORDER) => ByteOrderAction::Refuse,
1496        None => ByteOrderAction::Keep,
1497    }
1498}
1499
1500/// Why the stored image of an element is not already the host image of a
1501/// value of width `width` — `None` when it is, and a copying read would only
1502/// be memcpy-ing bytes it does not touch.
1503///
1504/// The two ways a stored element can need work before it is a value are the
1505/// two conversions a copying read performs in place: a byte-order swap
1506/// ([`to_host_byte_order`]) and the n-bit/scale-offset unpacking
1507/// (`Hdf5Reader::apply_post_filter_conversion`). Asking one question of both
1508/// is what lets a zero-copy view refuse exactly the datatypes a copying read
1509/// would have had to rewrite.
1510#[cfg(feature = "mmap")]
1511pub(crate) fn stored_image_mismatch(
1512    datatype: &DatatypeMessage,
1513    width: usize,
1514) -> Option<&'static str> {
1515    match byte_order_action(datatype, width) {
1516        ByteOrderAction::SwapElements => return Some("they are stored in the foreign byte order"),
1517        ByteOrderAction::Refuse => {
1518            return Some("it is a composite storing members in the foreign byte order")
1519        }
1520        ByteOrderAction::Keep => {}
1521    }
1522    if crate::format::nbit_scaleoffset::datatype_needs_bit_conversion(datatype) {
1523        return Some("the significant bits do not fill the stored element");
1524    }
1525    None
1526}
1527
1528/// Put a raw element image into host byte order, in place, for a typed read.
1529///
1530/// Every path that reinterprets the on-disk image as `T` — `read_raw`,
1531/// `read_slice`, `read_raw_into`, `read_slice_into` and their SWMR
1532/// counterparts — passes through here. Reinterpretation only yields the
1533/// stored value when the stored order is the host's.
1534///
1535/// A refused datatype is one no reinterpretation can decode;
1536/// [`H5Dataset::read_raw_bytes`] hands over the image for the caller to
1537/// decode member by member.
1538///
1539/// `width` is the element size, already checked equal to `T::element_size()`.
1540pub(crate) fn to_host_byte_order(
1541    bytes: &mut [u8],
1542    datatype: &DatatypeMessage,
1543    width: usize,
1544) -> Result<()> {
1545    match byte_order_action(datatype, width) {
1546        ByteOrderAction::Keep => {}
1547        ByteOrderAction::SwapElements => {
1548            for elem in bytes.chunks_exact_mut(width) {
1549                elem.reverse();
1550            }
1551        }
1552        ByteOrderAction::Refuse => {
1553            return Err(Hdf5Error::TypeMismatch(format!(
1554                "dataset datatype {datatype} stores {FOREIGN_BYTE_ORDER:?} values, which a \
1555                 typed read cannot reinterpret element by element; read_raw_bytes() returns \
1556                 the image to decode member by member"
1557            )))
1558        }
1559    }
1560    Ok(())
1561}
1562
1563/// The stored byte image of each variable-length sequence in a batch.
1564///
1565/// The vlen writers take `&[&[T]]` and store one global-heap object per
1566/// sequence, so each sequence needs the same host-image-to-stored-image step
1567/// [`to_stored_byte_order`] performs for a fixed-shape write — a `T` is
1568/// written from its host bytes, and `T::hdf5_type()` declares little-endian.
1569/// Borrows on a little-endian host, which is every machine that does not have
1570/// to swap.
1571pub(crate) fn vlen_sequence_images<'a, T: H5Type>(
1572    items: &'a [&'a [T]],
1573) -> Result<Vec<std::borrow::Cow<'a, [u8]>>> {
1574    let base = T::hdf5_type();
1575    items
1576        .iter()
1577        .map(|item| {
1578            // Safety: the same contract `write_raw` relies on — `T: Copy +
1579            // 'static` is a numeric primitive whose byte image is its value —
1580            // and the extent comes from the slice itself, so it cannot name
1581            // memory past it. The result borrows `items` and outlives nothing.
1582            let host = unsafe {
1583                std::slice::from_raw_parts(item.as_ptr() as *const u8, std::mem::size_of_val(*item))
1584            };
1585            to_stored_byte_order(host, &base, T::element_size())
1586        })
1587        .collect()
1588}
1589
1590/// Put a typed value's host-order image into the order the datatype declares.
1591///
1592/// The write-side counterpart of [`to_host_byte_order`], and the one place
1593/// every path that hands a `&[T]` to the file — `write_raw`, `write_slice`,
1594/// `append`, and the builder's fill value — turns those bytes into stored
1595/// bytes. A `T` is written from its host image, so a dataset declaring the
1596/// foreign order would otherwise hold host bytes under that declaration: a
1597/// file that is wrong by its own header.
1598///
1599/// Borrows when the declared order is the host's, which is every write that
1600/// does not set a [`datatype`](DatasetBuilder::datatype) override.
1601///
1602/// `width` is the element size, already checked equal to `T::element_size()`.
1603pub(crate) fn to_stored_byte_order<'a>(
1604    bytes: &'a [u8],
1605    datatype: &DatatypeMessage,
1606    width: usize,
1607) -> Result<std::borrow::Cow<'a, [u8]>> {
1608    match byte_order_action(datatype, width) {
1609        ByteOrderAction::Keep => Ok(std::borrow::Cow::Borrowed(bytes)),
1610        ByteOrderAction::SwapElements => {
1611            let mut owned = bytes.to_vec();
1612            for elem in owned.chunks_exact_mut(width) {
1613                elem.reverse();
1614            }
1615            Ok(std::borrow::Cow::Owned(owned))
1616        }
1617        ByteOrderAction::Refuse => Err(Hdf5Error::TypeMismatch(format!(
1618            "dataset datatype {datatype} stores {FOREIGN_BYTE_ORDER:?} values, which a typed \
1619             write cannot lay out element by element; write_raw_bytes() takes the image the \
1620             caller encodes member by member"
1621        ))),
1622    }
1623}
1624
1625/// Strip a fixed-string element's padding, leaving the bytes that carry the
1626/// value.
1627///
1628/// The rule itself lives with the datatype message
1629/// ([`fixed_string_content`]); this adds the element index a reserved padding
1630/// rule needs to be reported against.
1631fn trim_fixed_string(elem: &[u8], padding: u8, index: usize) -> Result<&[u8]> {
1632    crate::format::messages::datatype::fixed_string_content(elem, padding).ok_or_else(|| {
1633        Hdf5Error::InvalidState(format!(
1634            "string {index} uses padding rule {padding}, which the format reserves"
1635        ))
1636    })
1637}
1638
1639/// Decode one string element's bytes under the datatype's character set.
1640///
1641/// `lossy` replaces what it cannot decode with U+FFFD instead of failing;
1642/// `index` names the element in the error otherwise.
1643fn decode_string(bytes: &[u8], charset: u8, lossy: bool, index: usize) -> Result<String> {
1644    if lossy {
1645        return Ok(String::from_utf8_lossy(bytes).into_owned());
1646    }
1647    match charset {
1648        // ASCII. Bytes are 7-bit, which makes them UTF-8 as well.
1649        0 => match bytes.iter().position(|&b| b >= 0x80) {
1650            None => Ok(String::from_utf8_lossy(bytes).into_owned()),
1651            Some(at) => Err(Hdf5Error::InvalidState(format!(
1652                "string {index} declares the ASCII character set but byte {at} is {:#04x}",
1653                bytes[at]
1654            ))),
1655        },
1656        1 => String::from_utf8(bytes.to_vec()).map_err(|e| {
1657            Hdf5Error::InvalidState(format!(
1658                "string {index} declares UTF-8 but is not valid UTF-8: {e}"
1659            ))
1660        }),
1661        other => Err(Hdf5Error::InvalidState(format!(
1662            "string {index} uses character set {other}, which the format reserves"
1663        ))),
1664    }
1665}
1666
1667/// A dataset's storage layout class (read mode only) — `H5Pget_layout`'s
1668/// four values.
1669///
1670/// Distinct from [`ChunkIndex`], which names the structure a `Chunked`
1671/// layout's index uses; this only says which of the four storage classes
1672/// the dataset was created with.
1673#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1674pub enum StorageLayout {
1675    /// Raw data stored inline in the object header.
1676    Compact,
1677    /// Raw data in a single contiguous block — or, when the dataset also
1678    /// carries an external file list, in one or more blocks of an outside
1679    /// file instead ([`H5Dataset::external_files`]); the layout itself
1680    /// still reports `Contiguous` either way.
1681    Contiguous,
1682    /// Raw data split into fixed-size chunks, each independently
1683    /// allocated. [`H5Dataset::chunk_dims`] gives the chunk shape,
1684    /// [`H5Dataset::chunk_index`] the index structure.
1685    Chunked,
1686    /// No raw data of its own: every element comes from another dataset,
1687    /// possibly in another file ([`H5Dataset::virtual_mappings`]).
1688    Virtual,
1689}
1690
1691/// The chunk index structure a chunked dataset uses on disk (read mode
1692/// only) — which of libhdf5's chunk-lookup structures the layout message
1693/// names.
1694///
1695/// `BtreeV1` belongs to the version-3 chunked layout message (the only
1696/// index a file whose superblock predates version 2 can carry); the other
1697/// five are what a version-4 message's index-type byte selects, per
1698/// `H5D__layout_set_latest_indexing` (H5Dlayout.c).
1699#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1700pub enum ChunkIndex {
1701    /// Version-1 B-tree — the classic chunk index, and the only one a
1702    /// version-3 chunked layout message can carry.
1703    BtreeV1,
1704    /// Version-2 B-tree — two or more unlimited dimensions.
1705    BtreeV2,
1706    /// A single index entry for a dataset whose one chunk covers the whole
1707    /// dataspace (`dims == max_dims == chunk_dims`).
1708    SingleChunk,
1709    /// No index structure at all: chunk addresses are computed
1710    /// arithmetically over a contiguous run (no filter, early allocation).
1711    Implicit,
1712    /// Fixed-size array — a fixed shape needing per-chunk bookkeeping.
1713    FixedArray,
1714    /// Extensible array — exactly one unlimited dimension.
1715    ExtensibleArray,
1716}
1717
1718/// A dataset's fill-value state (read mode only) — `H5Pfill_value_defined`'s
1719/// tri-state (`H5D_fill_value_t`).
1720#[derive(Debug, Clone, PartialEq, Eq)]
1721pub enum FillValue {
1722    /// No fill value has ever been set: unwritten elements read back
1723    /// zero-filled, and no fill-value message named an explicit value.
1724    Default,
1725    /// The fill value was explicitly disabled: unallocated storage is never
1726    /// fill-initialized.
1727    Undefined,
1728    /// An explicit fill value, one element wide.
1729    UserDefined(Vec<u8>),
1730}
1731
1732/// When a dataset's fill value is written into allocated storage —
1733/// `H5Pset_fill_time`/`H5Pget_fill_time`'s `H5D_fill_time_t`.
1734///
1735/// Distinct from [`FillValue`], which says *what* the fill value is; this
1736/// says *when* it is written. The two agree everywhere except a dataset with
1737/// no fill value of its own: there, `Alloc` writes the default fill (zeros)
1738/// at allocation and `IfSet` writes nothing into space that already reads as
1739/// zeros — indistinguishable on disk in the value itself, only in this byte.
1740#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1741pub enum FillTime {
1742    /// Fill at allocation regardless of whether a fill value was ever set.
1743    Alloc,
1744    /// Never write the fill value into allocated storage.
1745    Never,
1746    /// Fill at allocation only when a fill value was set — the default
1747    /// every dataset gets unless [`fill_time`](DatasetBuilder::fill_time)
1748    /// says otherwise.
1749    IfSet,
1750}
1751
1752impl FillTime {
1753    /// The on-disk `H5D_fill_time_t` byte this variant is — what the writer
1754    /// stores and the fill-value message's write-time field carries.
1755    fn wire_byte(self) -> u8 {
1756        match self {
1757            Self::Alloc => 0,
1758            Self::Never => 1,
1759            Self::IfSet => 2,
1760        }
1761    }
1762}
1763
1764/// When a dataset's raw-data storage is allocated —
1765/// `H5Pset_alloc_time`/`H5Pget_alloc_time`'s `H5D_alloc_time_t`, read back
1766/// from the same fill-value message [`FillTime`] is.
1767///
1768/// `H5P__set_layout` (H5Pdcpl.c) picks this from the dataset's storage
1769/// class (`H5D_ALLOC_TIME_DEFAULT` per layout — compact is `Early`,
1770/// chunked and virtual are `Incr`, contiguous is `Late`). The one override
1771/// this crate's builder offers is [`DatasetBuilder::early_allocation`],
1772/// which a chunked dataset reads back as `Early` where the writer took it
1773/// up: an implicit index, or a single unfiltered chunk.
1774/// [`H5Dataset::alloc_time`] reads back what the writer declared.
1775#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1776pub enum AllocTime {
1777    /// Space is allocated as soon as the dataset is created.
1778    Early,
1779    /// Space is allocated when data is first written.
1780    Late,
1781    /// Space is allocated incrementally, as chunks (or virtual source
1782    /// datasets) are written.
1783    Incr,
1784}
1785
1786/// Which mapped data an unlimited virtual dataset's extent covers —
1787/// libhdf5's `H5D_vds_view_t`, set with `H5Pset_virtual_view` and read back
1788/// with `H5Pget_virtual_view` (H5Pdapl.c:1067, :1102).
1789///
1790/// A *dataset access* property: it is never stored in the file, so it says
1791/// how *this* open reads a virtual dataset, not what its writer intended.
1792#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1793pub enum VirtualView {
1794    /// `H5D_VDS_LAST_AVAILABLE` — the extent reaches the end of the last
1795    /// mapped block that has a source, so a gap before it reads as the fill
1796    /// value. libhdf5's default (`H5D_ACS_VDS_VIEW_DEF`, H5Pdapl.c:62).
1797    #[default]
1798    LastAvailable,
1799    /// `H5D_VDS_FIRST_MISSING` — the extent stops where the first missing
1800    /// mapped block begins, so no unmapped block is inside it.
1801    ///
1802    /// Under this view libhdf5 ignores
1803    /// [`virtual_printf_gap`](DatasetAccess::virtual_printf_gap) entirely:
1804    /// `H5D__virtual_init` reads the gap property only for
1805    /// [`LastAvailable`](Self::LastAvailable) and forces it to 0 otherwise
1806    /// (H5Dvirtual.c:2182-2188).
1807    FirstMissing,
1808}
1809
1810/// The dataset *access* properties this crate models — libhdf5's
1811/// `H5P_DATASET_ACCESS` property list, as much of it as affects reading.
1812///
1813/// [`virtual_view`](Self::virtual_view) and
1814/// [`virtual_printf_gap`](Self::virtual_printf_gap) govern how a virtual
1815/// dataset's extent is resolved when it is opened
1816/// (`H5D__virtual_set_extent_unlim`, H5Dvirtual.c:1386);
1817/// [`virtual_prefix`](Self::virtual_prefix) and
1818/// [`efile_prefix`](Self::efile_prefix) say where the *other files* a
1819/// dataset's data lives in are looked for. None of them is stored in the
1820/// file: opening a dataset without naming them reads it exactly as
1821/// libhdf5's default dapl does.
1822///
1823/// Pass one to [`H5File::dataset_with`](crate::H5File::dataset_with).
1824///
1825/// ```no_run
1826/// use rust_hdf5::{DatasetAccess, H5File, VirtualView};
1827///
1828/// let file = H5File::open("vds.h5").unwrap();
1829/// let access = DatasetAccess::new()
1830///     .virtual_view(VirtualView::LastAvailable)
1831///     .virtual_printf_gap(2);
1832/// let ds = file.dataset_with("vds", access).unwrap();
1833/// ```
1834#[derive(Debug, Clone, PartialEq, Eq, Default)]
1835pub struct DatasetAccess {
1836    view: VirtualView,
1837    printf_gap: u64,
1838    virtual_prefix: Option<String>,
1839    efile_prefix: Option<String>,
1840}
1841
1842impl DatasetAccess {
1843    /// A property list holding libhdf5's defaults —
1844    /// [`VirtualView::LastAvailable`] and a printf gap of 0, the values
1845    /// `H5D_ACS_VDS_VIEW_DEF` and `H5D_ACS_VDS_PRINTF_GAP_DEF` register
1846    /// (H5Pdapl.c:62, :67).
1847    pub fn new() -> Self {
1848        Self::default()
1849    }
1850
1851    /// `H5Pset_virtual_view` (H5Pdapl.c:1067). The two legal values are the
1852    /// two [`VirtualView`] variants, so the "not a valid bounds option"
1853    /// argument check that call makes has nothing to reject here.
1854    pub fn virtual_view(mut self, view: VirtualView) -> Self {
1855        self.view = view;
1856        self
1857    }
1858
1859    /// `H5Pset_virtual_printf_gap` (H5Pdapl.c:1207): how many consecutive
1860    /// missing printf-named source datasets the extent resolution looks past
1861    /// before it stops. 0 — the default — stops at the first one missing.
1862    ///
1863    /// `u64::MAX` is libhdf5's `HSIZE_UNDEF`, which that call rejects as "not
1864    /// a valid printf gap size"; here the rejection surfaces from the open
1865    /// that uses the property, since a builder method has no way to report
1866    /// it.
1867    pub fn virtual_printf_gap(mut self, gap: u64) -> Self {
1868        self.printf_gap = gap;
1869        self
1870    }
1871
1872    /// `H5Pget_virtual_view` (H5Pdapl.c:1102).
1873    pub fn view(&self) -> VirtualView {
1874        self.view
1875    }
1876
1877    /// `H5Pset_virtual_prefix` (H5Pdapl.c:1478): a directory a virtual
1878    /// dataset's *source file names* are looked for under, before the
1879    /// virtual file's own directory and after `HDF5_VDS_PREFIX`.
1880    ///
1881    /// It is the third step of `H5F_prefix_open_file`'s search order
1882    /// (H5Fint.c:938-950), and it is reached only when `HDF5_VDS_PREFIX` is
1883    /// unset or empty: `H5D__build_file_prefix` reads the environment first
1884    /// and falls back to this property (H5Dint.c:1077-1082), so an
1885    /// environment prefix shadows this one outright rather than being tried
1886    /// alongside it.
1887    ///
1888    /// A leading `${ORIGIN}` stands for the directory holding the virtual
1889    /// dataset's own file (H5Dint.c:1105-1113), and `"."` or `""` means "no
1890    /// prefix" (:1096-1100), both exactly as for the environment variable.
1891    ///
1892    /// Like the other two, this is a *dataset access* property that is never
1893    /// stored in the file, and the first open of a virtual dataset fixes it
1894    /// for every open that overlaps it.
1895    pub fn virtual_prefix(mut self, prefix: impl Into<String>) -> Self {
1896        self.virtual_prefix = Some(prefix.into());
1897        self
1898    }
1899
1900    /// `H5Pset_efile_prefix` (H5Pdapl.c:1392): a directory the *raw data
1901    /// files* of a dataset stored through an external file list are looked
1902    /// for under.
1903    ///
1904    /// This one takes no search at all, unlike the other two prefixes:
1905    /// `H5D__efl_read` joins the prefix to the stored name with
1906    /// `H5_combine_path` and opens exactly that one path (H5Defl.c:315-317).
1907    /// With no prefix in force the stored name is used as written, so a
1908    /// relative one resolves against the *process's current directory* and
1909    /// not against the directory holding the HDF5 file — measured under
1910    /// libhdf5 1.14.6 and 2.0.0: a raw data file next to the HDF5 file is
1911    /// not found, while the same name under the current directory is.
1912    ///
1913    /// It shares [`virtual_prefix`](Self::virtual_prefix)'s expansion rules,
1914    /// because both are built by `H5D__build_file_prefix`: `HDF5_EXTFILE_PREFIX`
1915    /// shadows this property outright rather than merely preceding it
1916    /// (H5Dint.c:1084-1090), a leading `${ORIGIN}` stands for the directory
1917    /// holding the HDF5 file (:1105-1113), and `"."` or `""` means no prefix
1918    /// (:1098-1102).
1919    ///
1920    /// # A second open must name the same one
1921    ///
1922    /// Where a mismatched [`virtual_prefix`](Self::virtual_prefix) is
1923    /// silently ignored by the second open, a mismatched external file prefix
1924    /// is an *error*: `H5D_open` compares the expanded prefix against the one
1925    /// the already-open dataset resolved under and refuses the open when they
1926    /// differ (H5Dint.c:1533-1545). Expanded, so two opens that differ only
1927    /// in a property the environment shadows still agree. Closing every
1928    /// handle releases the answer, and the next open sets its own.
1929    pub fn efile_prefix(mut self, prefix: impl Into<String>) -> Self {
1930        self.efile_prefix = Some(prefix.into());
1931        self
1932    }
1933
1934    /// `H5Pget_virtual_printf_gap` (H5Pdapl.c:1243) — the value set, not the
1935    /// one the extent resolution ends up using; see
1936    /// [`VirtualView::FirstMissing`].
1937    pub fn printf_gap(&self) -> u64 {
1938        self.printf_gap
1939    }
1940
1941    /// `H5Pget_virtual_prefix` (H5Pdapl.c:1510) — the property as set, before
1942    /// `HDF5_VDS_PREFIX` gets to shadow it and before `${ORIGIN}` is
1943    /// expanded. `None` is `H5D_ACS_VDS_PREFIX_DEF`, a null prefix
1944    /// (H5Pdapl.c:72).
1945    pub fn virtual_prefix_value(&self) -> Option<&str> {
1946        self.virtual_prefix.as_deref()
1947    }
1948
1949    /// `H5Pget_efile_prefix` (H5Pdapl.c:1422) — the property as set, before
1950    /// `HDF5_EXTFILE_PREFIX` gets to shadow it and before `${ORIGIN}` is
1951    /// expanded. `None` is `H5D_ACS_EFILE_PREFIX_DEF`, a null prefix
1952    /// (H5Pdapl.c:90).
1953    pub fn efile_prefix_value(&self) -> Option<&str> {
1954        self.efile_prefix.as_deref()
1955    }
1956
1957    /// The printf gap `H5D__virtual_set_extent_unlim` actually scans with:
1958    /// the property under [`VirtualView::LastAvailable`], and 0 under
1959    /// [`VirtualView::FirstMissing`], because `H5D__virtual_init` only reads
1960    /// the property in the first case (H5Dvirtual.c:2182-2188).
1961    ///
1962    /// The single owner of that rule — the resolution never reads
1963    /// [`printf_gap`](Self::printf_gap) directly.
1964    pub(crate) fn effective_printf_gap(&self) -> u64 {
1965        match self.view {
1966            VirtualView::LastAvailable => self.printf_gap,
1967            VirtualView::FirstMissing => 0,
1968        }
1969    }
1970
1971    /// Reject what `H5Pset_virtual_printf_gap` rejects, at the open that uses
1972    /// the property.
1973    pub(crate) fn validate(&self) -> Result<()> {
1974        if self.printf_gap == u64::MAX {
1975            return Err(Hdf5Error::InvalidState(
1976                "virtual_printf_gap(u64::MAX) is libhdf5's HSIZE_UNDEF, which \
1977                 H5Pset_virtual_printf_gap refuses as \"not a valid printf gap size\""
1978                    .into(),
1979            ));
1980        }
1981        Ok(())
1982    }
1983}
1984
1985/// Elements a selection of `counts` holds, refusing a product that overflows
1986/// `usize` rather than wrapping it into a small allocation.
1987fn element_count(counts: &[u64]) -> Result<usize> {
1988    counts
1989        .iter()
1990        .try_fold(1usize, |acc, &c| {
1991            usize::try_from(c).ok().and_then(|c| acc.checked_mul(c))
1992        })
1993        .ok_or_else(|| {
1994            Hdf5Error::InvalidState(format!("selection {counts:?} has more elements than usize"))
1995        })
1996}
1997
1998impl H5Dataset {
1999    /// Create a reader-mode dataset handle (called internally by `H5File::dataset`).
2000    pub(crate) fn new_reader(
2001        file_inner: SharedInner,
2002        name: String,
2003        shape: Vec<usize>,
2004        element_size: usize,
2005        open: Option<crate::io::reader::DatasetOpenToken>,
2006    ) -> Self {
2007        Self {
2008            file_inner,
2009            info: DatasetInfo::Reader {
2010                name,
2011                shape,
2012                element_size,
2013            },
2014            _open: open,
2015        }
2016    }
2017
2018    /// Create a writer-mode dataset handle for an already-created dataset
2019    /// (called internally by [`H5File::dataset_writer`](crate::file::H5File::dataset_writer)).
2020    ///
2021    /// Reconstructs the same handle `new_dataset().create()` returns, so the
2022    /// reopened dataset supports attribute writes and chunk appends.
2023    ///
2024    /// `is_null` is always `false` here: reopening an existing NULL-dataspace
2025    /// dataset for further writes is not a case this constructor's caller
2026    /// distinguishes (a NULL dataset has nothing to append or chunk-write in
2027    /// the first place).
2028    pub(crate) fn new_writer(
2029        file_inner: SharedInner,
2030        index: usize,
2031        parts: crate::io::writer::DatasetHandleParts,
2032    ) -> Self {
2033        Self {
2034            file_inner,
2035            info: DatasetInfo::Writer {
2036                index,
2037                shape: parts.shape,
2038                element_size: parts.element_size,
2039                chunk_index: parts.chunk_index,
2040                is_null: false,
2041            },
2042            _open: parts.open,
2043        }
2044    }
2045
2046    /// Return the dataset dimensions.
2047    pub fn shape(&self) -> Vec<usize> {
2048        match &self.info {
2049            DatasetInfo::Writer { shape, .. } => shape.clone(),
2050            DatasetInfo::Reader { shape, .. } => shape.clone(),
2051        }
2052    }
2053
2054    /// Return the number of dimensions (rank) of the dataset.
2055    pub fn ndims(&self) -> usize {
2056        match &self.info {
2057            DatasetInfo::Writer { shape, .. } => shape.len(),
2058            DatasetInfo::Reader { shape, .. } => shape.len(),
2059        }
2060    }
2061
2062    /// Return the total number of elements in the dataset.
2063    ///
2064    /// 0 for a NULL dataspace ([`is_null`](Self::is_null)) — unlike a scalar,
2065    /// whose `shape()` is the same empty `Vec` but which holds exactly one
2066    /// element, so `shape().iter().product()` cannot be used here.
2067    pub fn total_elements(&self) -> usize {
2068        if self.is_null() {
2069            return 0;
2070        }
2071        match &self.info {
2072            DatasetInfo::Writer { shape, .. } => shape.iter().product(),
2073            DatasetInfo::Reader { shape, .. } => shape.iter().product(),
2074        }
2075    }
2076
2077    /// Return the size of one element in bytes.
2078    pub fn element_size(&self) -> usize {
2079        match &self.info {
2080            DatasetInfo::Writer { element_size, .. } => *element_size,
2081            DatasetInfo::Reader { element_size, .. } => *element_size,
2082        }
2083    }
2084
2085    /// Return whether this dataset has the NULL dataspace: no elements at
2086    /// all, distinct from a scalar dataset (rank 0, exactly one element) —
2087    /// both report the same empty [`shape`](Self::shape). See
2088    /// [`DatasetBuilder::null`].
2089    pub fn is_null(&self) -> bool {
2090        match &self.info {
2091            DatasetInfo::Writer { is_null, .. } => *is_null,
2092            DatasetInfo::Reader { name, .. } => {
2093                let mut inner = borrow_inner_mut(&self.file_inner);
2094                match &mut *inner {
2095                    H5FileInner::Reader(reader) => reader
2096                        .dataset_info(name)
2097                        .map(|info| info.dataspace.is_null())
2098                        .unwrap_or(false),
2099                    _ => false,
2100                }
2101            }
2102        }
2103    }
2104
2105    /// Return the element datatype as parsed from the file (read mode only).
2106    ///
2107    /// Unlike [`element_size`](Self::element_size), which reports only the
2108    /// byte width, this exposes the full datatype: its class (integer vs
2109    /// floating-point vs string vs compound …), signedness, byte order and
2110    /// bit precision. Callers that must reconstruct the exact stored type —
2111    /// for example to map it to a NumPy / Arrow dtype — should use this
2112    /// instead of inferring a type from the byte width, which cannot
2113    /// distinguish `u8` from `i8` (both 1 byte) or `i32` from `f32` (both 4
2114    /// bytes).
2115    ///
2116    /// # Errors
2117    ///
2118    /// Returns an error if the file is in write mode, or if the dataset can
2119    /// no longer be found in the reader's metadata.
2120    ///
2121    /// ```no_run
2122    /// # use rust_hdf5::{H5File, DatatypeMessage};
2123    /// let file = H5File::open("data.h5").unwrap();
2124    /// let ds = file.dataset("image").unwrap();
2125    /// match ds.datatype().unwrap() {
2126    ///     DatatypeMessage::FixedPoint { size, signed, .. } => {
2127    ///         println!("integer: {} bytes, signed={}", size, signed);
2128    ///     }
2129    ///     DatatypeMessage::FloatingPoint { size, .. } => {
2130    ///         println!("float: {} bytes", size);
2131    ///     }
2132    ///     other => println!("other type: {other}"),
2133    /// }
2134    /// ```
2135    pub fn datatype(&self) -> Result<DatatypeMessage> {
2136        match &self.info {
2137            DatasetInfo::Reader { name, .. } => {
2138                let mut inner = borrow_inner_mut(&self.file_inner);
2139                match &mut *inner {
2140                    H5FileInner::Reader(reader) => reader
2141                        .dataset_info(name)
2142                        .map(|info| info.datatype.clone())
2143                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2144                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2145                }
2146            }
2147            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2148                "datatype() is only available in read mode".into(),
2149            )),
2150        }
2151    }
2152
2153    /// Return the chunk dimensions, if this is a chunked dataset.
2154    pub fn chunk_dims(&self) -> Option<Vec<usize>> {
2155        match &self.info {
2156            DatasetInfo::Reader { name, .. } => {
2157                let mut inner = borrow_inner_mut(&self.file_inner);
2158                if let H5FileInner::Reader(reader) = &mut *inner {
2159                    if let Some(info) = reader.dataset_info(name) {
2160                        use crate::format::messages::data_layout::DataLayoutMessage;
2161                        let chunk_dims = match &info.layout {
2162                            DataLayoutMessage::ChunkedV4 { chunk_dims, .. }
2163                            | DataLayoutMessage::ChunkedV3 { chunk_dims, .. } => Some(chunk_dims),
2164                            _ => None,
2165                        };
2166                        if let Some(chunk_dims) = chunk_dims {
2167                            // Strip trailing element-size dimension
2168                            return Some(
2169                                chunk_dims[..chunk_dims.len() - 1]
2170                                    .iter()
2171                                    .map(|&d| d as usize)
2172                                    .collect(),
2173                            );
2174                        }
2175                    }
2176                }
2177                None
2178            }
2179            DatasetInfo::Writer { .. } => None,
2180        }
2181    }
2182
2183    /// Return whether this is a chunked dataset.
2184    pub fn is_chunked(&self) -> bool {
2185        match &self.info {
2186            DatasetInfo::Writer { chunk_index, .. } => chunk_index.is_some(),
2187            DatasetInfo::Reader { name, .. } => {
2188                let mut inner = borrow_inner_mut(&self.file_inner);
2189                match &mut *inner {
2190                    H5FileInner::Reader(reader) => {
2191                        if let Some(info) = reader.dataset_info(name) {
2192                            use crate::format::messages::data_layout::DataLayoutMessage;
2193                            matches!(
2194                                info.layout,
2195                                DataLayoutMessage::ChunkedV4 { .. }
2196                                    | DataLayoutMessage::ChunkedV3 { .. }
2197                            )
2198                        } else {
2199                            false
2200                        }
2201                    }
2202                    _ => false,
2203                }
2204            }
2205        }
2206    }
2207
2208    /// Return the dataset's storage layout class (read mode only).
2209    ///
2210    /// # Errors
2211    ///
2212    /// Returns an error if the file is in write mode, or if the dataset can
2213    /// no longer be found in the reader's metadata.
2214    pub fn storage_layout(&self) -> Result<StorageLayout> {
2215        match &self.info {
2216            DatasetInfo::Reader { name, .. } => {
2217                let mut inner = borrow_inner_mut(&self.file_inner);
2218                match &mut *inner {
2219                    H5FileInner::Reader(reader) => {
2220                        use crate::format::messages::data_layout::DataLayoutMessage;
2221                        reader
2222                            .dataset_info(name)
2223                            .map(|info| match &info.layout {
2224                                DataLayoutMessage::Compact { .. } => StorageLayout::Compact,
2225                                DataLayoutMessage::Contiguous { .. } => StorageLayout::Contiguous,
2226                                DataLayoutMessage::ChunkedV3 { .. }
2227                                | DataLayoutMessage::ChunkedV4 { .. } => StorageLayout::Chunked,
2228                                DataLayoutMessage::Virtual { .. } => StorageLayout::Virtual,
2229                            })
2230                            .ok_or_else(|| Hdf5Error::NotFound(name.clone()))
2231                    }
2232                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2233                }
2234            }
2235            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2236                "storage_layout() is only available in read mode".into(),
2237            )),
2238        }
2239    }
2240
2241    /// Return the chunk index structure this dataset's layout uses, or
2242    /// `None` for a dataset that is not chunked (read mode only).
2243    ///
2244    /// # Errors
2245    ///
2246    /// Returns an error if the file is in write mode, or if the dataset can
2247    /// no longer be found in the reader's metadata.
2248    pub fn chunk_index(&self) -> Result<Option<ChunkIndex>> {
2249        match &self.info {
2250            DatasetInfo::Reader { name, .. } => {
2251                let mut inner = borrow_inner_mut(&self.file_inner);
2252                match &mut *inner {
2253                    H5FileInner::Reader(reader) => {
2254                        use crate::format::messages::data_layout::{
2255                            ChunkIndexType, DataLayoutMessage,
2256                        };
2257                        reader
2258                            .dataset_info(name)
2259                            .map(|info| match &info.layout {
2260                                DataLayoutMessage::ChunkedV3 { .. } => Some(ChunkIndex::BtreeV1),
2261                                DataLayoutMessage::ChunkedV4 { index_type, .. } => {
2262                                    Some(match index_type {
2263                                        ChunkIndexType::SingleChunk => ChunkIndex::SingleChunk,
2264                                        ChunkIndexType::Implicit => ChunkIndex::Implicit,
2265                                        ChunkIndexType::FixedArray => ChunkIndex::FixedArray,
2266                                        ChunkIndexType::ExtensibleArray => {
2267                                            ChunkIndex::ExtensibleArray
2268                                        }
2269                                        ChunkIndexType::BTreeV2 => ChunkIndex::BtreeV2,
2270                                    })
2271                                }
2272                                _ => None,
2273                            })
2274                            .ok_or_else(|| Hdf5Error::NotFound(name.clone()))
2275                    }
2276                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2277                }
2278            }
2279            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2280                "chunk_index() is only available in read mode".into(),
2281            )),
2282        }
2283    }
2284
2285    /// Return this dataset's filter pipeline (read mode only), in
2286    /// application order. Empty when the dataset has no filter pipeline
2287    /// message at all — an unfiltered dataset, not an error.
2288    ///
2289    /// # Errors
2290    ///
2291    /// Returns an error if the file is in write mode, or if the dataset can
2292    /// no longer be found in the reader's metadata.
2293    pub fn filters(&self) -> Result<Vec<Filter>> {
2294        match &self.info {
2295            DatasetInfo::Reader { name, .. } => {
2296                let mut inner = borrow_inner_mut(&self.file_inner);
2297                match &mut *inner {
2298                    H5FileInner::Reader(reader) => reader
2299                        .dataset_info(name)
2300                        .map(|info| {
2301                            info.filter_pipeline
2302                                .as_ref()
2303                                .map(|fp| fp.filters.clone())
2304                                .unwrap_or_default()
2305                        })
2306                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2307                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2308                }
2309            }
2310            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2311                "filters() is only available in read mode".into(),
2312            )),
2313        }
2314    }
2315
2316    /// Return this dataset's fill-value state (read mode only).
2317    ///
2318    /// # Errors
2319    ///
2320    /// Returns an error if the file is in write mode, or if the dataset can
2321    /// no longer be found in the reader's metadata.
2322    pub fn fill_value(&self) -> Result<FillValue> {
2323        match &self.info {
2324            DatasetInfo::Reader { name, .. } => {
2325                let mut inner = borrow_inner_mut(&self.file_inner);
2326                match &mut *inner {
2327                    H5FileInner::Reader(reader) => reader
2328                        .dataset_info(name)
2329                        .map(|info| match info.fill_defined {
2330                            0 => FillValue::Undefined,
2331                            2 => {
2332                                FillValue::UserDefined(info.fill_value.clone().unwrap_or_default())
2333                            }
2334                            _ => FillValue::Default,
2335                        })
2336                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2337                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2338                }
2339            }
2340            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2341                "fill_value() is only available in read mode".into(),
2342            )),
2343        }
2344    }
2345
2346    /// Return when this dataset's fill value is written into allocated
2347    /// storage (read mode only) — `H5Pget_fill_time`.
2348    ///
2349    /// # Errors
2350    ///
2351    /// Returns an error if the file is in write mode, or if the dataset can
2352    /// no longer be found in the reader's metadata.
2353    pub fn fill_time(&self) -> Result<FillTime> {
2354        match &self.info {
2355            DatasetInfo::Reader { name, .. } => {
2356                let mut inner = borrow_inner_mut(&self.file_inner);
2357                match &mut *inner {
2358                    H5FileInner::Reader(reader) => reader
2359                        .dataset_info(name)
2360                        .map(|info| match info.fill_write_time {
2361                            0 => FillTime::Alloc,
2362                            1 => FillTime::Never,
2363                            _ => FillTime::IfSet,
2364                        })
2365                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2366                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2367                }
2368            }
2369            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2370                "fill_time() is only available in read mode".into(),
2371            )),
2372        }
2373    }
2374
2375    /// Return when this dataset's raw-data storage is allocated (read mode
2376    /// only) — `H5Pget_alloc_time`.
2377    ///
2378    /// # Errors
2379    ///
2380    /// Returns an error if the file is in write mode, or if the dataset can
2381    /// no longer be found in the reader's metadata.
2382    pub fn alloc_time(&self) -> Result<AllocTime> {
2383        match &self.info {
2384            DatasetInfo::Reader { name, .. } => {
2385                let mut inner = borrow_inner_mut(&self.file_inner);
2386                match &mut *inner {
2387                    H5FileInner::Reader(reader) => reader
2388                        .dataset_info(name)
2389                        .map(|info| match info.alloc_time {
2390                            1 => AllocTime::Early,
2391                            3 => AllocTime::Incr,
2392                            _ => AllocTime::Late,
2393                        })
2394                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2395                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2396                }
2397            }
2398            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2399                "alloc_time() is only available in read mode".into(),
2400            )),
2401        }
2402    }
2403
2404    /// Return this dataset's external raw-data file segments (read mode
2405    /// only), in the order the dataset's logical byte range concatenates
2406    /// them. Empty for a dataset whose data lives in this file.
2407    ///
2408    /// # Errors
2409    ///
2410    /// Returns an error if the file is in write mode, or if the dataset can
2411    /// no longer be found in the reader's metadata.
2412    pub fn external_files(&self) -> Result<Vec<ExternalFileSegment>> {
2413        match &self.info {
2414            DatasetInfo::Reader { name, .. } => {
2415                let mut inner = borrow_inner_mut(&self.file_inner);
2416                match &mut *inner {
2417                    H5FileInner::Reader(reader) => reader
2418                        .dataset_info(name)
2419                        .map(|info| info.external_files.clone())
2420                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2421                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2422                }
2423            }
2424            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2425                "external_files() is only available in read mode".into(),
2426            )),
2427        }
2428    }
2429
2430    /// Return this dataset's maximum dimension sizes (read mode only):
2431    /// `None` in a dimension marks that axis unlimited. A dataset with no
2432    /// maximum-dimensions message reports its current shape (max == current
2433    /// — the upstream convention for a fixed-extent dataset).
2434    ///
2435    /// # Errors
2436    ///
2437    /// Returns an error if the file is in write mode, or if the dataset can
2438    /// no longer be found in the reader's metadata.
2439    pub fn max_shape(&self) -> Result<Vec<Option<usize>>> {
2440        match &self.info {
2441            DatasetInfo::Reader { name, .. } => {
2442                let mut inner = borrow_inner_mut(&self.file_inner);
2443                match &mut *inner {
2444                    H5FileInner::Reader(reader) => reader
2445                        .dataset_info(name)
2446                        .map(|info| match &info.dataspace.max_dims {
2447                            Some(max_dims) => max_dims
2448                                .iter()
2449                                .map(|&d| (d != u64::MAX).then_some(d as usize))
2450                                .collect(),
2451                            None => info
2452                                .dataspace
2453                                .dims
2454                                .iter()
2455                                .map(|&d| Some(d as usize))
2456                                .collect(),
2457                        })
2458                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2459                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2460                }
2461            }
2462            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2463                "max_shape() is only available in read mode".into(),
2464            )),
2465        }
2466    }
2467
2468    /// Return this dataset's virtual-dataset source/virtual mappings (read
2469    /// mode only), in on-disk order. Empty for any dataset whose layout is
2470    /// not virtual, and for a virtual dataset that has no mappings yet.
2471    ///
2472    /// # Errors
2473    ///
2474    /// Returns an error if the file is in write mode, or if the dataset can
2475    /// no longer be found in the reader's metadata.
2476    pub fn virtual_mappings(&self) -> Result<Vec<VirtualMapping>> {
2477        match &self.info {
2478            DatasetInfo::Reader { name, .. } => {
2479                let mut inner = borrow_inner_mut(&self.file_inner);
2480                match &mut *inner {
2481                    H5FileInner::Reader(reader) => reader
2482                        .dataset_info(name)
2483                        .map(|info| {
2484                            info.virtual_mappings
2485                                .as_ref()
2486                                .map(|vml| vml.mappings.clone())
2487                                .unwrap_or_default()
2488                        })
2489                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2490                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2491                }
2492            }
2493            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2494                "virtual_mappings() is only available in read mode".into(),
2495            )),
2496        }
2497    }
2498
2499    /// Return the names of all attributes on this dataset (read mode only).
2500    pub fn attr_names(&self) -> Result<Vec<String>> {
2501        match &self.info {
2502            DatasetInfo::Reader { name, .. } => {
2503                let mut inner = borrow_inner_mut(&self.file_inner);
2504                match &mut *inner {
2505                    H5FileInner::Reader(reader) => Ok(reader.dataset_attr_names(name)?),
2506                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2507                }
2508            }
2509            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2510                "attr_names not available in write mode".into(),
2511            )),
2512        }
2513    }
2514
2515    /// Why the attribute `attr_name` on this dataset cannot be read, or `None`
2516    /// when it can be.
2517    ///
2518    /// An attribute whose message this crate cannot decode is still listed by
2519    /// [`attr_names`](Self::attr_names) — the object header carries it — and
2520    /// this says what stands in the way. Opening it through
2521    /// [`attr`](Self::attr) fails with the same text.
2522    pub fn attr_unreadable_reason(&self, attr_name: &str) -> Result<Option<String>> {
2523        match &self.info {
2524            DatasetInfo::Reader { name, .. } => {
2525                let mut inner = borrow_inner_mut(&self.file_inner);
2526                match &mut *inner {
2527                    H5FileInner::Reader(reader) => Ok(reader
2528                        .dataset_attr_unreadable_reason(name, attr_name)
2529                        .map(str::to_string)),
2530                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2531                }
2532            }
2533            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2534                "attr_unreadable_reason not available in write mode".into(),
2535            )),
2536        }
2537    }
2538
2539    /// Why this dataset's attribute *set* cannot be listed, or `None` when it
2540    /// can be.
2541    ///
2542    /// The object-scope counterpart of
2543    /// [`attr_unreadable_reason`](Self::attr_unreadable_reason). A dense
2544    /// attribute set is indexed by name hash, so a heap or index that will not
2545    /// read yields no names to hang a per-attribute reason on;
2546    /// [`attr_names`](Self::attr_names) then returns the failure rather than a
2547    /// short list, and this reports it without an attribute name.
2548    pub fn attrs_unreadable_reason(&self) -> Result<Option<String>> {
2549        match &self.info {
2550            DatasetInfo::Reader { name, .. } => {
2551                let mut inner = borrow_inner_mut(&self.file_inner);
2552                match &mut *inner {
2553                    H5FileInner::Reader(reader) => Ok(reader
2554                        .dataset_attrs_unreadable_reason(name)
2555                        .map(str::to_string)),
2556                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2557                }
2558            }
2559            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2560                "attrs_unreadable_reason not available in write mode".into(),
2561            )),
2562        }
2563    }
2564
2565    /// This dataset's own compact-vs-dense attribute storage — the
2566    /// equivalent of `h5py.h5o.get_info(did.id).meta_size.attr.index_size`
2567    /// being nonzero (read mode only).
2568    pub fn attr_storage(&self) -> Result<AttributeStorage> {
2569        match &self.info {
2570            DatasetInfo::Reader { name, .. } => {
2571                let mut inner = borrow_inner_mut(&self.file_inner);
2572                match &mut *inner {
2573                    H5FileInner::Reader(reader) => Ok(reader.dataset_attr_storage(name)?),
2574                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2575                }
2576            }
2577            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2578                "attr_storage not available in write mode".into(),
2579            )),
2580        }
2581    }
2582
2583    /// This dataset's own object-header attribute count — the equivalent of
2584    /// `h5py.h5o.get_info(did.id).num_attrs` (read mode only).
2585    pub fn header_attr_count(&self) -> Result<u64> {
2586        match &self.info {
2587            DatasetInfo::Reader { name, .. } => {
2588                let mut inner = borrow_inner_mut(&self.file_inner);
2589                match &mut *inner {
2590                    H5FileInner::Reader(reader) => Ok(reader.dataset_header_attr_count(name)?),
2591                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2592                }
2593            }
2594            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2595                "header_attr_count not available in write mode".into(),
2596            )),
2597        }
2598    }
2599
2600    /// Open an attribute by name (read mode only).
2601    pub fn attr(&self, attr_name: &str) -> Result<crate::attribute::H5Attribute> {
2602        match &self.info {
2603            DatasetInfo::Reader { name, .. } => {
2604                let mut inner = borrow_inner_mut(&self.file_inner);
2605                match &mut *inner {
2606                    H5FileInner::Reader(reader) => {
2607                        let attr_msg = reader.dataset_attr(name, attr_name)?.clone();
2608                        Ok(crate::attribute::H5Attribute::new_reader(
2609                            clone_inner(&self.file_inner),
2610                            attr_msg,
2611                        ))
2612                    }
2613                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2614                }
2615            }
2616            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2617                "attr() not available in write mode".into(),
2618            )),
2619        }
2620    }
2621
2622    /// Start building a new attribute on this dataset.
2623    ///
2624    /// Returns a fluent builder. Call `.shape(())` for a scalar attribute
2625    /// and `.create("name")` to finalize.
2626    ///
2627    /// # Example
2628    ///
2629    /// ```no_run
2630    /// # use rust_hdf5::H5File;
2631    /// # use rust_hdf5::types::VarLenUnicode;
2632    /// let file = H5File::create("attr.h5").unwrap();
2633    /// let ds = file.new_dataset::<f32>().shape(&[10]).create("data").unwrap();
2634    /// let attr = ds.new_attr::<VarLenUnicode>().shape(()).create("units").unwrap();
2635    /// attr.write_scalar(&VarLenUnicode("meters".to_string())).unwrap();
2636    /// ```
2637    pub fn new_attr<T: 'static>(&self) -> AttrBuilder<'_, T> {
2638        let ds_index = match &self.info {
2639            DatasetInfo::Writer { index, .. } => *index,
2640            DatasetInfo::Reader { .. } => {
2641                // Reader mode: we'll return a builder that will error on create.
2642                // Using usize::MAX as sentinel.
2643                usize::MAX
2644            }
2645        };
2646        AttrBuilder::new(&self.file_inner, ds_index)
2647    }
2648
2649    /// Add a scalar attribute to this dataset whose datatype and raw value the
2650    /// caller supplies — the dataset form of
2651    /// [`H5File::set_attr_typed`](crate::H5File::set_attr_typed), for a type
2652    /// [`new_attr`](Self::new_attr) has no Rust carrier for, such as a
2653    /// fixed-length string sized by `H5Tset_size`.
2654    pub fn set_attr_typed(
2655        &self,
2656        name: &str,
2657        datatype: DatatypeMessage,
2658        value: Vec<u8>,
2659    ) -> Result<()> {
2660        let attr = crate::attribute::typed_scalar_attr(name, datatype, value)?;
2661        let DatasetInfo::Writer { index, .. } = &self.info else {
2662            return Err(Hdf5Error::InvalidState(
2663                "cannot write attributes in read mode".into(),
2664            ));
2665        };
2666        match &*borrow_inner(&self.file_inner) {
2667            H5FileInner::Writer(writer) => {
2668                writer.add_dataset_attribute(*index, attr)?;
2669                Ok(())
2670            }
2671            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
2672                "cannot write attributes in read mode".into(),
2673            )),
2674            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
2675        }
2676    }
2677
2678    /// Write a typed slice holding the dataset's whole image.
2679    ///
2680    /// The slice length must match the total number of elements declared by
2681    /// the dataset shape. The data is reinterpreted as raw bytes and written
2682    /// to the file: to the contiguous data block, or — for a chunked dataset —
2683    /// scattered across its chunk grid, through the filter pipeline if one is
2684    /// set. To write only part of a dataset, use
2685    /// [`write_slice`](Self::write_slice).
2686    ///
2687    /// # Errors
2688    ///
2689    /// Returns an error if:
2690    /// - The file is in read mode.
2691    /// - The data length does not match the declared shape.
2692    pub fn write_raw<T: H5Type>(&self, data: &[T]) -> Result<()> {
2693        match &self.info {
2694            DatasetInfo::Writer {
2695                index,
2696                shape,
2697                element_size,
2698                chunk_index,
2699                is_null,
2700            } => {
2701                if *is_null {
2702                    return Err(Hdf5Error::InvalidState(
2703                        "cannot write to a NULL dataspace dataset".into(),
2704                    ));
2705                }
2706                let total_elements: usize = shape.iter().product();
2707                if data.len() != total_elements {
2708                    return Err(Hdf5Error::InvalidState(format!(
2709                        "data length {} does not match dataset size {}",
2710                        data.len(),
2711                        total_elements,
2712                    )));
2713                }
2714
2715                // Verify element size matches
2716                if T::element_size() != *element_size {
2717                    return Err(Hdf5Error::TypeMismatch(format!(
2718                        "write type has element size {} but dataset expects {}",
2719                        T::element_size(),
2720                        element_size,
2721                    )));
2722                }
2723
2724                // Safety: T: Copy + 'static (numeric primitive) with well-defined
2725                // byte representation. The resulting slice borrows `data` and
2726                // lives only as long as this block.
2727                let byte_len = data.len() * T::element_size();
2728                let host =
2729                    unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
2730                let datatype = {
2731                    let inner = borrow_inner(&self.file_inner);
2732                    match &*inner {
2733                        H5FileInner::Writer(writer) => writer.dataset_datatype(*index),
2734                        _ => {
2735                            return Err(Hdf5Error::InvalidState(
2736                                "file is no longer in write mode".into(),
2737                            ))
2738                        }
2739                    }
2740                };
2741                let stored = to_stored_byte_order(host, &datatype, T::element_size())?;
2742
2743                if let Some(kind) = *chunk_index {
2744                    // A chunked dataset has no contiguous data block; scatter
2745                    // the full row-major image into its chunk grid and write
2746                    // each chunk through the dataset's filter pipeline.
2747                    return self.write_full_image_chunked(*index, kind, &stored, *element_size);
2748                }
2749
2750                let inner = borrow_inner(&self.file_inner);
2751                match &*inner {
2752                    H5FileInner::Writer(writer) => {
2753                        writer.write_dataset_raw(*index, &stored)?;
2754                        Ok(())
2755                    }
2756                    _ => Err(Hdf5Error::InvalidState(
2757                        "file is no longer in write mode".into(),
2758                    )),
2759                }
2760            }
2761            DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
2762                "cannot write to a dataset opened in read mode".into(),
2763            )),
2764        }
2765    }
2766
2767    /// Write the raw byte image of the whole dataset directly.
2768    ///
2769    /// Takes the same layouts as [`write_raw`](Self::write_raw): a contiguous
2770    /// data block, or a chunk grid the image is scattered across.
2771    ///
2772    /// Unlike [`write_raw`](Self::write_raw), this is not generic over an
2773    /// `H5Type` carrier, so it works for element types that have no matching
2774    /// Rust primitive — in particular a runtime
2775    /// [`CompoundType`](crate::types::CompoundType) of arbitrary size set via
2776    /// [`DatasetBuilder::datatype`]. `bytes.len()` must equal
2777    /// `product(shape) * element_size`, where `element_size` is taken from the
2778    /// dataset's on-disk datatype.
2779    ///
2780    /// ```no_run
2781    /// # use rust_hdf5::H5File;
2782    /// # use rust_hdf5::types::{CompoundType, H5Type};
2783    /// let file = H5File::create("c.h5").unwrap();
2784    /// let ct = CompoundType {
2785    ///     members: vec![
2786    ///         ("id".to_string(), i32::hdf5_type(), 0),
2787    ///         ("val".to_string(), f64::hdf5_type(), 4),
2788    ///     ],
2789    ///     total_size: 12,
2790    /// };
2791    /// let ds = file
2792    ///     .new_dataset::<u8>()
2793    ///     .datatype(ct.to_datatype())
2794    ///     .shape(&[2])
2795    ///     .create("records")
2796    ///     .unwrap();
2797    /// let mut bytes = Vec::new();
2798    /// bytes.extend_from_slice(&1i32.to_le_bytes());
2799    /// bytes.extend_from_slice(&2.5f64.to_le_bytes());
2800    /// bytes.extend_from_slice(&2i32.to_le_bytes());
2801    /// bytes.extend_from_slice(&3.5f64.to_le_bytes());
2802    /// ds.write_raw_bytes(&bytes).unwrap();
2803    /// ```
2804    pub fn write_raw_bytes(&self, bytes: &[u8]) -> Result<()> {
2805        match &self.info {
2806            DatasetInfo::Writer {
2807                index,
2808                shape,
2809                element_size,
2810                chunk_index,
2811                is_null,
2812            } => {
2813                if *is_null {
2814                    return Err(Hdf5Error::InvalidState(
2815                        "cannot write to a NULL dataspace dataset".into(),
2816                    ));
2817                }
2818                let expected: usize = shape.iter().product::<usize>() * *element_size;
2819                if bytes.len() != expected {
2820                    return Err(Hdf5Error::InvalidState(format!(
2821                        "raw byte length {} does not match dataset size {} \
2822                         (product(shape) * element_size {})",
2823                        bytes.len(),
2824                        expected,
2825                        element_size,
2826                    )));
2827                }
2828                if let Some(kind) = *chunk_index {
2829                    // Scatter the full row-major image into the chunk grid
2830                    // (same path as write_raw, carrier-agnostic bytes).
2831                    return self.write_full_image_chunked(*index, kind, bytes, *element_size);
2832                }
2833                let inner = borrow_inner(&self.file_inner);
2834                match &*inner {
2835                    H5FileInner::Writer(writer) => {
2836                        writer.write_dataset_raw(*index, bytes)?;
2837                        Ok(())
2838                    }
2839                    _ => Err(Hdf5Error::InvalidState(
2840                        "file is no longer in write mode".into(),
2841                    )),
2842                }
2843            }
2844            DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
2845                "cannot write to a dataset opened in read mode".into(),
2846            )),
2847        }
2848    }
2849
2850    /// Scatter a full row-major dataset image into its chunk grid, writing
2851    /// every chunk through the dataset's filter pipeline.
2852    ///
2853    /// This is the chunked counterpart of a single contiguous `write_dataset_raw`
2854    /// — it is how [`write_raw`](Self::write_raw) and
2855    /// [`write_raw_bytes`](Self::write_raw_bytes) populate a chunked dataset
2856    /// (including the single auto-chunk created when a filter is set without
2857    /// explicit chunk dimensions). Edge chunks are zero-padded to the full
2858    /// chunk footprint, exactly as libhdf5 stores them.
2859    fn write_full_image_chunked(
2860        &self,
2861        index: usize,
2862        kind: ChunkIndexKind,
2863        bytes: &[u8],
2864        element_size: usize,
2865    ) -> Result<()> {
2866        let inner = borrow_inner(&self.file_inner);
2867        let writer = match &*inner {
2868            H5FileInner::Writer(w) => w,
2869            _ => {
2870                return Err(Hdf5Error::InvalidState(
2871                    "file is no longer in write mode".into(),
2872                ))
2873            }
2874        };
2875        // Whole-operation guard: the flush, the grid snapshot and the chunk
2876        // writes below must not interleave with a concurrent same-dataset
2877        // operation.
2878        let cell = writer.ds(index);
2879        let _op = cell.op.lock();
2880        // A buffered append tail would flush over the image at close; hand
2881        // it to the chunks first, the image below overwrites everything.
2882        writer.flush_append_buffer(index)?;
2883        let chunk_dims = writer
2884            .dataset_chunk_dims(index)
2885            .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
2886            .to_vec();
2887        let dims = writer.dataset_dims(index).to_vec();
2888        let rank = dims.len();
2889
2890        // Chunk grid: number of chunks along each dimension (row-major).
2891        let mut grid = vec![0u64; rank];
2892        for d in 0..rank {
2893            grid[d] = if chunk_dims[d] > 0 {
2894                dims[d].div_ceil(chunk_dims[d])
2895            } else {
2896                0
2897            };
2898        }
2899        let total_chunks: u64 = grid.iter().product();
2900
2901        // Decode the iteration counter into row-major coordinates over the
2902        // *current* image's chunk grid. This is only an odometer over the
2903        // chunks the image spans — the slot a chunk is recorded under comes
2904        // from the index grid (`Hdf5Writer::chunk_slot`), which the maximum
2905        // extent decides.
2906        let coords_of = |linear: u64| -> Vec<u64> {
2907            let mut rem = linear;
2908            let mut coords = vec![0u64; rank];
2909            for d in (0..rank).rev() {
2910                coords[d] = rem % grid[d];
2911                rem /= grid[d];
2912            }
2913            coords
2914        };
2915
2916        // The batch entry points exist for one reason: to run the filter
2917        // pipeline over a window of chunks in parallel. An unfiltered dataset
2918        // has no pipeline to run, so it takes the plain per-chunk owner
2919        // whatever its index is, and only a filtered extensible or fixed array
2920        // — the two indexes with a batch entry point — takes the window below.
2921        let batched = writer.dataset_is_filtered(index)
2922            && matches!(
2923                kind,
2924                ChunkIndexKind::ExtensibleArray | ChunkIndexKind::FixedArray
2925            );
2926        if !batched {
2927            // One staging buffer for the whole image, reused chunk after
2928            // chunk: a chunk that already sits as one complete run of `bytes`
2929            // needs no staging at all and goes to the file straight out of the
2930            // caller's slice, so only an n-D interleave or a short edge pays
2931            // for a gather.
2932            let mut staging = Vec::new();
2933            for linear in 0..total_chunks {
2934                let coords = coords_of(linear);
2935                let chunk =
2936                    match Self::contiguous_chunk_span(&dims, &chunk_dims, &coords, element_size) {
2937                        Some(span) => &bytes[span],
2938                        None => {
2939                            Self::gather_chunk_into(
2940                                &mut staging,
2941                                bytes,
2942                                &dims,
2943                                &chunk_dims,
2944                                &coords,
2945                                element_size,
2946                            );
2947                            &staging[..]
2948                        }
2949                    };
2950                writer.write_chunk_at_coords(index, &coords, chunk)?;
2951            }
2952        } else {
2953            // Hand the pipeline a window of chunks so it compresses them in
2954            // parallel (with the `parallel` feature). A fixed-size window
2955            // bounds peak memory instead of materializing every chunk at once;
2956            // 256 keeps every rayon worker fed while capping the transient
2957            // buffers to window * chunk bytes. The compressors read the window
2958            // concurrently, so a gathered chunk here cannot share one reused
2959            // buffer the way the sequential path above does — but a chunk that
2960            // is already a complete run of `bytes` is borrowed, not copied.
2961            // The two indexes differ only in how a chunk is addressed: EA by
2962            // its linear grid index, FA by grid coordinates.
2963            const BATCH_WINDOW: u64 = 256;
2964            let mut start = 0u64;
2965            while start < total_chunks {
2966                let end = (start + BATCH_WINDOW).min(total_chunks);
2967                let items: Vec<(Vec<u64>, Cow<'_, [u8]>)> = (start..end)
2968                    .map(|counter| {
2969                        let coords = coords_of(counter);
2970                        let data = match Self::contiguous_chunk_span(
2971                            &dims,
2972                            &chunk_dims,
2973                            &coords,
2974                            element_size,
2975                        ) {
2976                            Some(span) => Cow::Borrowed(&bytes[span]),
2977                            None => {
2978                                let mut buf = Vec::new();
2979                                Self::gather_chunk_into(
2980                                    &mut buf,
2981                                    bytes,
2982                                    &dims,
2983                                    &chunk_dims,
2984                                    &coords,
2985                                    element_size,
2986                                );
2987                                Cow::Owned(buf)
2988                            }
2989                        };
2990                        (coords, data)
2991                    })
2992                    .collect();
2993                if kind == ChunkIndexKind::FixedArray {
2994                    let pairs: Vec<(&[u64], &[u8])> = items
2995                        .iter()
2996                        .map(|(c, d)| (c.as_slice(), d.as_ref()))
2997                        .collect();
2998                    writer.write_chunks_fixed_array_batch_inner(index, &pairs)?;
2999                } else {
3000                    let mut pairs: Vec<(u64, &[u8])> = Vec::with_capacity(items.len());
3001                    for (c, d) in &items {
3002                        pairs.push((writer.chunk_slot(index, c)?, d.as_ref()));
3003                    }
3004                    writer.write_chunks_batch_inner(index, &pairs)?;
3005                }
3006                start = end;
3007            }
3008        }
3009        Ok(())
3010    }
3011
3012    /// The byte range one chunk occupies in a row-major full-dataset image,
3013    /// for a chunk that needs no gather at all: its elements are one
3014    /// contiguous run of `source` *and* they fill the chunk shape exactly, so
3015    /// the bytes that go to the file are already sitting in the caller's
3016    /// buffer.
3017    ///
3018    /// Both halves hold when every dimension after the first spans the whole
3019    /// dataset (`chunk_dims[d] == dims[d]`, leaving nothing interleaved and no
3020    /// padding along those axes) and the chunk does not hang off the far edge
3021    /// of the first — which is every full chunk of a 1-D dataset. `None` means
3022    /// the chunk has to be gathered.
3023    fn contiguous_chunk_span(
3024        dims: &[u64],
3025        chunk_dims: &[u64],
3026        coords: &[u64],
3027        element_size: usize,
3028    ) -> Option<std::ops::Range<usize>> {
3029        let rank = dims.len();
3030        if rank == 0 || chunk_dims[1..] != dims[1..] {
3031            return None;
3032        }
3033        if (coords[0] + 1) * chunk_dims[0] > dims[0] {
3034            return None;
3035        }
3036        let plane: u64 = dims[1..].iter().product::<u64>() * element_size as u64;
3037        let start = usize::try_from(coords[0] * chunk_dims[0] * plane).ok()?;
3038        let len = usize::try_from(chunk_dims[0] * plane).ok()?;
3039        Some(start..start.checked_add(len)?)
3040    }
3041
3042    /// Gather one chunk's bytes from a row-major full-dataset image into
3043    /// `out`, replacing whatever it held.
3044    ///
3045    /// `coords` are the chunk's grid coordinates. `out` is left exactly
3046    /// `product(chunk_dims) * element_size` bytes long, holding the chunk's
3047    /// elements and zero where the chunk extends past the dataset edge — so a
3048    /// caller may hand the same buffer to one chunk after another.
3049    fn gather_chunk_into(
3050        out: &mut Vec<u8>,
3051        source: &[u8],
3052        dims: &[u64],
3053        chunk_dims: &[u64],
3054        coords: &[u64],
3055        element_size: usize,
3056    ) {
3057        let rank = dims.len();
3058        let chunk_elems: u64 = chunk_dims.iter().product();
3059        let chunk_bytes = chunk_elems as usize * element_size;
3060        if rank == 0 {
3061            // Scalar dataset: a single element, no chunking dimension.
3062            out.clear();
3063            out.resize(chunk_bytes, 0);
3064            if source.len() >= element_size {
3065                out[..element_size].copy_from_slice(&source[..element_size]);
3066            }
3067            return;
3068        }
3069
3070        // Actual extent of this chunk along each dimension (edge chunks are
3071        // smaller than the nominal chunk shape).
3072        let mut extent = vec![0u64; rank];
3073        for d in 0..rank {
3074            let start = coords[d] * chunk_dims[d];
3075            let end = ((coords[d] + 1) * chunk_dims[d]).min(dims[d]);
3076            extent[d] = end.saturating_sub(start);
3077        }
3078        // Size the buffer, then zero it only when this chunk leaves part of
3079        // its shape uncovered: a full chunk has every byte overwritten below,
3080        // while an edge chunk's padding must read as zero even though a
3081        // reused buffer still holds the previous chunk's bytes.
3082        if out.len() != chunk_bytes {
3083            out.clear();
3084            out.resize(chunk_bytes, 0);
3085        } else if extent != chunk_dims {
3086            out.fill(0);
3087        }
3088        if extent.contains(&0) {
3089            return; // nothing of the dataset falls in this chunk
3090        }
3091
3092        // Row-major strides (in elements) for the source (over `dims`) and the
3093        // destination chunk buffer (over `chunk_dims`).
3094        let mut src_stride = vec![1u64; rank];
3095        let mut dst_stride = vec![1u64; rank];
3096        for d in (0..rank - 1).rev() {
3097            src_stride[d] = src_stride[d + 1] * dims[d + 1];
3098            dst_stride[d] = dst_stride[d + 1] * chunk_dims[d + 1];
3099        }
3100
3101        // Copy one contiguous run along the last axis per outer multi-index.
3102        let last = rank - 1;
3103        let run = extent[last] as usize * element_size;
3104        let outer: u64 = extent[..last].iter().product::<u64>().max(1);
3105        let mut idx = vec![0u64; rank]; // local indices within the chunk extent
3106        for _ in 0..outer {
3107            let mut src_off = 0u64;
3108            let mut dst_off = 0u64;
3109            for d in 0..rank {
3110                let global = coords[d] * chunk_dims[d] + idx[d];
3111                src_off += global * src_stride[d];
3112                dst_off += idx[d] * dst_stride[d];
3113            }
3114            let s = src_off as usize * element_size;
3115            let dpos = dst_off as usize * element_size;
3116            out[dpos..dpos + run].copy_from_slice(&source[s..s + run]);
3117
3118            // Advance the multi-index over axes [0..last); the last axis is the
3119            // contiguous run handled above.
3120            let mut d = last;
3121            while d > 0 {
3122                d -= 1;
3123                idx[d] += 1;
3124                if idx[d] < extent[d] {
3125                    break;
3126                }
3127                idx[d] = 0;
3128            }
3129        }
3130    }
3131
3132    /// Write a single chunk to a chunked dataset.
3133    ///
3134    /// `chunk_idx` is the linear chunk index (typically the frame number for
3135    /// streaming datasets). `data` is the raw byte data for one chunk.
3136    ///
3137    /// For datasets with two or more unlimited dimensions (v2 B-tree index),
3138    /// use [`write_chunk_at`](Self::write_chunk_at) instead.
3139    pub fn write_chunk(&self, chunk_idx: usize, data: &[u8]) -> Result<()> {
3140        match &self.info {
3141            DatasetInfo::Writer {
3142                index, chunk_index, ..
3143            } => {
3144                let Some(kind) = *chunk_index else {
3145                    return Err(Hdf5Error::InvalidState(
3146                        "write_chunk is only for chunked datasets".into(),
3147                    ));
3148                };
3149                if kind == ChunkIndexKind::BtreeV2 {
3150                    return Err(Hdf5Error::InvalidState(
3151                        "this dataset uses a v2 B-tree chunk index; use write_chunk_at \
3152                         with the chunk's grid coordinates"
3153                            .into(),
3154                    ));
3155                }
3156
3157                let inner = borrow_inner(&self.file_inner);
3158                match &*inner {
3159                    H5FileInner::Writer(writer) => {
3160                        // One op: the slot decode and the write see the same
3161                        // extents.
3162                        let cell = writer.ds(*index);
3163                        let _op = cell.op.lock();
3164                        match kind {
3165                            // All four address a chunk by its grid
3166                            // coordinates, so the linear slot is decoded back
3167                            // into them.
3168                            ChunkIndexKind::FixedArray
3169                            | ChunkIndexKind::Implicit
3170                            | ChunkIndexKind::SingleChunk
3171                            | ChunkIndexKind::BtreeV1 => {
3172                                let coords =
3173                                    writer.chunk_coords_from_slot(*index, chunk_idx as u64)?;
3174                                writer.write_chunk_at_coords(*index, &coords, data)?;
3175                            }
3176                            _ => writer.write_chunk_inner(*index, chunk_idx as u64, data)?,
3177                        }
3178                        Ok(())
3179                    }
3180                    _ => Err(Hdf5Error::InvalidState(
3181                        "file is no longer in write mode".into(),
3182                    )),
3183                }
3184            }
3185            DatasetInfo::Reader { .. } => {
3186                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
3187            }
3188        }
3189    }
3190
3191    /// Write an already-filtered (pre-compressed) chunk **verbatim**, recording
3192    /// the caller-supplied `filter_mask`. The bytes are stored as-is without
3193    /// running the dataset's filter pipeline — the HDF5 "direct chunk write"
3194    /// (`H5Dwrite_chunk`, formerly `H5DOwrite_chunk`) operation.
3195    ///
3196    /// `chunk_idx` is the linear chunk index (the frame number for streaming
3197    /// datasets), exactly as for [`write_chunk`](Self::write_chunk). `data` is
3198    /// the already-filtered bytes of one chunk — its length is the *stored*
3199    /// (compressed) size, not the uncompressed chunk size.
3200    ///
3201    /// `filter_mask` is a bitfield: bit *i* set means filter *i* of the
3202    /// dataset's pipeline was **not** applied to this chunk and must be skipped
3203    /// on read. Pass 0 when the full pipeline was already applied upstream (the
3204    /// common case: a codec plugin handed you compressed frames).
3205    ///
3206    /// The dataset must be chunked **and** filtered; an unfiltered chunk index
3207    /// has no slot to record a stored size or mask. A v2-B-tree-indexed dataset
3208    /// (two or more unlimited dimensions) has no fixed chunk grid to linearize
3209    /// against, so address its chunks with
3210    /// [`write_chunk_raw_at`](Self::write_chunk_raw_at) instead.
3211    ///
3212    /// # Reading back
3213    ///
3214    /// Both this crate's reader and libhdf5/h5py honor the per-chunk
3215    /// `filter_mask`: a chunk written with any mask round-trips correctly, with
3216    /// the reader skipping exactly the filters the mask marks as not applied.
3217    pub fn write_chunk_raw(&self, chunk_idx: usize, data: &[u8], filter_mask: u32) -> Result<()> {
3218        match &self.info {
3219            DatasetInfo::Writer {
3220                index, chunk_index, ..
3221            } => {
3222                let Some(kind) = *chunk_index else {
3223                    return Err(Hdf5Error::InvalidState(
3224                        "write_chunk_raw is only for chunked datasets".into(),
3225                    ));
3226                };
3227                if kind == ChunkIndexKind::BtreeV2 {
3228                    return Err(Hdf5Error::InvalidState(
3229                        "this dataset uses a v2 B-tree chunk index; use \
3230                         write_chunk_raw_at with the chunk's grid coordinates"
3231                            .into(),
3232                    ));
3233                }
3234                if kind == ChunkIndexKind::Implicit {
3235                    return Err(Hdf5Error::InvalidState(
3236                        "this dataset uses the implicit chunk index, which stores \
3237                         every chunk at its full unfiltered size and has nowhere to \
3238                         record a stored size or a filter mask"
3239                            .into(),
3240                    ));
3241                }
3242
3243                let inner = borrow_inner(&self.file_inner);
3244                match &*inner {
3245                    H5FileInner::Writer(writer) => {
3246                        // One op: the slot decode and the write see the same
3247                        // extents.
3248                        let cell = writer.ds(*index);
3249                        let _op = cell.op.lock();
3250                        if kind == ChunkIndexKind::FixedArray {
3251                            // Fixed-array dataset: decode the index-grid slot
3252                            // into row-major grid coordinates.
3253                            let coords = writer.chunk_coords_from_slot(*index, chunk_idx as u64)?;
3254                            writer.write_compressed_chunk_fixed_array_inner(
3255                                *index,
3256                                &coords,
3257                                data,
3258                                filter_mask,
3259                            )?;
3260                        } else if kind == ChunkIndexKind::BtreeV1 {
3261                            // Same for the classic index, whose key carries a
3262                            // stored size and a filter mask of its own.
3263                            let coords = writer.chunk_coords_from_slot(*index, chunk_idx as u64)?;
3264                            writer.write_compressed_chunk_btree_v1_inner(
3265                                *index,
3266                                &coords,
3267                                data,
3268                                filter_mask,
3269                            )?;
3270                        } else if kind == ChunkIndexKind::SingleChunk {
3271                            // Same again for the single-chunk index, whose
3272                            // layout message carries the stored size and mask
3273                            // inline.
3274                            let coords = writer.chunk_coords_from_slot(*index, chunk_idx as u64)?;
3275                            writer.write_compressed_chunk_single_chunk_inner(
3276                                *index,
3277                                &coords,
3278                                data,
3279                                filter_mask,
3280                            )?;
3281                        } else {
3282                            writer.write_compressed_chunk_inner(
3283                                *index,
3284                                chunk_idx as u64,
3285                                data,
3286                                filter_mask,
3287                            )?;
3288                        }
3289                        Ok(())
3290                    }
3291                    _ => Err(Hdf5Error::InvalidState(
3292                        "file is no longer in write mode".into(),
3293                    )),
3294                }
3295            }
3296            DatasetInfo::Reader { .. } => {
3297                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
3298            }
3299        }
3300    }
3301
3302    /// Write a single chunk to a v2-B-tree-indexed dataset, addressed by its
3303    /// chunk-grid coordinates (one per dimension).
3304    ///
3305    /// This is the entry point for datasets with two or more unlimited
3306    /// dimensions. The dataset's logical dimensions are extended to cover
3307    /// the written chunk. `data` is the raw bytes of one full chunk.
3308    ///
3309    /// ```no_run
3310    /// # use rust_hdf5::H5File;
3311    /// let file = H5File::create("bt2.h5").unwrap();
3312    /// let ds = file.new_dataset::<i32>()
3313    ///     .shape(&[0, 0])
3314    ///     .chunk(&[2, 2])
3315    ///     .max_shape(&[None, None])
3316    ///     .create("grid")
3317    ///     .unwrap();
3318    /// let chunk = [0i32, 1, 2, 3];
3319    /// let bytes: Vec<u8> = chunk.iter().flat_map(|v| v.to_le_bytes()).collect();
3320    /// ds.write_chunk_at(&[0, 0], &bytes).unwrap();
3321    /// ```
3322    pub fn write_chunk_at(&self, chunk_coords: &[usize], data: &[u8]) -> Result<()> {
3323        self.write_chunk_at_inner(chunk_coords, ChunkBytes::Unfiltered(data), "write_chunk_at")
3324    }
3325
3326    /// Write an already-filtered chunk **verbatim** to a chunked dataset,
3327    /// addressed by its chunk-grid coordinates.
3328    ///
3329    /// The coordinate-addressed twin of
3330    /// [`write_chunk_raw`](Self::write_chunk_raw), and the form a
3331    /// v2-B-tree-indexed dataset needs: with two or more unlimited dimensions
3332    /// there is no fixed chunk grid for a linear index to mean anything against.
3333    /// As with `write_chunk_at`, the dataset's logical dimensions are extended
3334    /// to cover the written chunk.
3335    ///
3336    /// `data` is the already-filtered bytes of one chunk — its length is the
3337    /// *stored* size — and `filter_mask` bit *i* set means filter *i* of the
3338    /// pipeline was **not** applied and must be skipped on read. Pass 0 when the
3339    /// full pipeline already ran upstream.
3340    ///
3341    /// The dataset must be chunked **and** filtered; an unfiltered chunk index
3342    /// has no slot to record a stored size or mask.
3343    pub fn write_chunk_raw_at(
3344        &self,
3345        chunk_coords: &[usize],
3346        data: &[u8],
3347        filter_mask: u32,
3348    ) -> Result<()> {
3349        self.write_chunk_at_inner(
3350            chunk_coords,
3351            ChunkBytes::Prefiltered { data, filter_mask },
3352            "write_chunk_raw_at",
3353        )
3354    }
3355
3356    /// Coordinate-addressed chunk write: grows the dataspace to cover the
3357    /// chunk and hands the bytes to `Hdf5Writer::write_chunk_at_coords_inner`,
3358    /// the one dispatch over chunk indexes that the SWMR writer shares.
3359    /// Whether the filter pipeline runs there or already ran upstream is
3360    /// carried by `bytes`.
3361    fn write_chunk_at_inner(
3362        &self,
3363        chunk_coords: &[usize],
3364        bytes: ChunkBytes<'_>,
3365        what: &str,
3366    ) -> Result<()> {
3367        match &self.info {
3368            DatasetInfo::Writer {
3369                index, chunk_index, ..
3370            } => {
3371                let Some(kind) = *chunk_index else {
3372                    return Err(Hdf5Error::InvalidState(format!(
3373                        "{what} is only for chunked datasets"
3374                    )));
3375                };
3376                let coords: Vec<u64> = chunk_coords.iter().map(|&c| c as u64).collect();
3377                let inner = borrow_inner(&self.file_inner);
3378                let writer = match &*inner {
3379                    H5FileInner::Writer(w) => w,
3380                    _ => {
3381                        return Err(Hdf5Error::InvalidState(
3382                            "file is no longer in write mode".into(),
3383                        ))
3384                    }
3385                };
3386                // Whole-operation guard: the dims snapshot, the chunk write
3387                // and the extend below must not interleave with a concurrent
3388                // same-dataset operation.
3389                let cell = writer.ds(*index);
3390                let _op = cell.op.lock();
3391                let dims = writer.dataset_dims(*index).to_vec();
3392                // Only the indexes that can grow (v2 B-tree, v1 B-tree,
3393                // extensible array) move the extent with the chunk: compute
3394                // the grown dimensions up-front, before any chunk is written,
3395                // so an overflowing coordinate cannot leave an orphaned chunk
3396                // in the file. The writer refuses a coordinate list of the
3397                // wrong rank before it writes anything.
3398                //
3399                // The last chunk of a dimension usually hangs past the extent
3400                // — a length of 10 in chunks of 4 ends at 12 — so the growth
3401                // is capped at the declared maximum, which is what the chunk
3402                // still covers. Without the cap a legal edge chunk would be
3403                // written and then rejected by the extend below.
3404                let mut new_dims = dims.clone();
3405                if !matches!(
3406                    kind,
3407                    ChunkIndexKind::FixedArray
3408                        | ChunkIndexKind::Implicit
3409                        | ChunkIndexKind::SingleChunk
3410                ) {
3411                    let chunk_dims = writer.dataset_chunk_dims(*index).ok_or_else(|| {
3412                        Hdf5Error::InvalidState("dataset has no chunk info".into())
3413                    })?;
3414                    let max_dims = writer.dataset_max_dims(*index);
3415                    for (d, new) in new_dims.iter_mut().enumerate() {
3416                        let (Some(&c), Some(&cd), Some(&max)) =
3417                            (coords.get(d), chunk_dims.get(d), max_dims.get(d))
3418                        else {
3419                            break;
3420                        };
3421                        let needed = c
3422                            .checked_add(1)
3423                            .and_then(|c| c.checked_mul(cd))
3424                            .ok_or_else(|| {
3425                                Hdf5Error::InvalidState(format!(
3426                                    "chunk coordinate {c} in dimension {d} is too large"
3427                                ))
3428                            })?
3429                            .min(max);
3430                        *new = (*new).max(needed);
3431                    }
3432                }
3433                writer.write_chunk_at_coords_inner(*index, &coords, bytes)?;
3434                if new_dims != dims {
3435                    writer.extend_dataset_inner(*index, &new_dims)?;
3436                }
3437                Ok(())
3438            }
3439            DatasetInfo::Reader { .. } => {
3440                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
3441            }
3442        }
3443    }
3444
3445    /// Write multiple chunks in a batch, optionally compressing in parallel.
3446    ///
3447    /// `chunks` is a slice of `(chunk_index, raw_data)` pairs. When a filter
3448    /// pipeline is configured and the `parallel` feature is enabled, all
3449    /// chunks are compressed concurrently via rayon.
3450    pub fn write_chunks_batch(&self, chunks: &[(usize, &[u8])]) -> Result<()> {
3451        match &self.info {
3452            DatasetInfo::Writer {
3453                index, chunk_index, ..
3454            } => {
3455                if chunk_index.is_none() {
3456                    return Err(Hdf5Error::InvalidState(
3457                        "write_chunks_batch is only for chunked datasets".into(),
3458                    ));
3459                }
3460                let pairs: Vec<(u64, &[u8])> = chunks
3461                    .iter()
3462                    .map(|(idx, data)| (*idx as u64, *data))
3463                    .collect();
3464                let inner = borrow_inner(&self.file_inner);
3465                match &*inner {
3466                    H5FileInner::Writer(writer) => {
3467                        writer.write_chunks_batch(*index, &pairs)?;
3468                        Ok(())
3469                    }
3470                    _ => Err(Hdf5Error::InvalidState(
3471                        "file is no longer in write mode".into(),
3472                    )),
3473                }
3474            }
3475            DatasetInfo::Reader { .. } => {
3476                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
3477            }
3478        }
3479    }
3480
3481    /// Append data along the first dimension of a chunked dataset.
3482    ///
3483    /// `data` must contain a whole number of "frames" — slices along
3484    /// dimension 0. For example, if the dataset has shape `[N, H, W]`
3485    /// and `chunk_dims = [1, H, W]`, then `data.len()` must be a
3486    /// multiple of `H * W`.
3487    ///
3488    /// This method writes the necessary chunks and extends the dataset
3489    /// shape automatically.
3490    ///
3491    /// ```no_run
3492    /// # use rust_hdf5::H5File;
3493    /// let file = H5File::create("append.h5").unwrap();
3494    /// let ds = file.new_dataset::<f64>()
3495    ///     .shape(&[0, 3])
3496    ///     .chunk(&[1, 3])
3497    ///     .max_shape(&[None, Some(3)])
3498    ///     .create("data")
3499    ///     .unwrap();
3500    /// ds.append(&[1.0, 2.0, 3.0]).unwrap();       // shape becomes [1, 3]
3501    /// ds.append(&[4.0, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap(); // shape becomes [3, 3]
3502    /// ```
3503    pub fn append<T: H5Type>(&self, data: &[T]) -> Result<()> {
3504        match &self.info {
3505            DatasetInfo::Writer {
3506                index,
3507                element_size,
3508                chunk_index,
3509                ..
3510            } => {
3511                if chunk_index.is_none() {
3512                    return Err(Hdf5Error::InvalidState(
3513                        "append is only for chunked datasets".into(),
3514                    ));
3515                }
3516                if T::element_size() != *element_size {
3517                    return Err(Hdf5Error::TypeMismatch(format!(
3518                        "append type has element size {} but dataset expects {}",
3519                        T::element_size(),
3520                        element_size,
3521                    )));
3522                }
3523
3524                let ds_index = *index;
3525                let es = *element_size;
3526
3527                let inner = borrow_inner(&self.file_inner);
3528                let writer = match &*inner {
3529                    H5FileInner::Writer(w) => w,
3530                    _ => {
3531                        return Err(Hdf5Error::InvalidState(
3532                            "file is no longer in write mode".into(),
3533                        ))
3534                    }
3535                };
3536
3537                // Whole-operation guard: the buffer take, the frame writes,
3538                // the re-buffer and the extend below are separate slot
3539                // acquisitions that a concurrent same-dataset append must not
3540                // interleave with.
3541                let cell = writer.ds(ds_index);
3542                let _op = cell.op.lock();
3543
3544                let chunk_dims = writer
3545                    .dataset_chunk_dims(ds_index)
3546                    .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
3547                    .to_vec();
3548                let dims = writer.dataset_dims(ds_index).to_vec();
3549
3550                // Frame size = product of dims[1..]
3551                let frame_elems: usize = if dims.len() > 1 {
3552                    dims[1..].iter().map(|&d| d as usize).product()
3553                } else {
3554                    1
3555                };
3556
3557                if frame_elems == 0 {
3558                    return Err(Hdf5Error::InvalidState(
3559                        "cannot append to dataset with zero-size trailing dimensions".into(),
3560                    ));
3561                }
3562
3563                if !data.len().is_multiple_of(frame_elems) {
3564                    return Err(Hdf5Error::InvalidState(format!(
3565                        "data length {} is not a multiple of frame size {}",
3566                        data.len(),
3567                        frame_elems,
3568                    )));
3569                }
3570
3571                let n_new_frames = data.len() / frame_elems;
3572                let current_dim0 = dims[0] as usize;
3573
3574                // Chunk size along first dimension
3575                let chunk_dim0 = chunk_dims[0] as usize;
3576                let frame_bytes = frame_elems * es;
3577
3578                let host = unsafe {
3579                    std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * es)
3580                };
3581                let datatype = writer.dataset_datatype(ds_index);
3582                let raw = to_stored_byte_order(host, &datatype, es)?;
3583
3584                // Merge the buffer with the new frames when it is the
3585                // dataset's tail; a buffer left mid-extent (the extent moved
3586                // past it) keeps its recorded place — flush it and start
3587                // fresh at the current end.
3588                let taken = { writer.ds(ds_index).lock().append.take() };
3589                let (base_dim0, buffered_frames, mut combined) = match taken {
3590                    Some(b) if b.base + b.frames == current_dim0 as u64 => {
3591                        (b.base as usize, b.frames as usize, b.bytes)
3592                    }
3593                    Some(b) => {
3594                        writer.write_append_frames(ds_index, b.base, b.frames, &b.bytes)?;
3595                        (current_dim0, 0, Vec::new())
3596                    }
3597                    None => (current_dim0, 0, Vec::new()),
3598                };
3599                combined.extend_from_slice(&raw);
3600
3601                let total_frames = buffered_frames + n_new_frames;
3602
3603                // Rows up to the last chunk boundary are written now; the
3604                // tail that does not complete a chunk goes back in the
3605                // buffer for the next append (or the flush at close). The
3606                // boundary can precede `base_dim0` — a reopened file's
3607                // flushed partial chunk leaves the base mid-chunk — in
3608                // which case everything is tail.
3609                let last_boundary = ((base_dim0 + total_frames) / chunk_dim0) * chunk_dim0;
3610                let write_frames = last_boundary.saturating_sub(base_dim0);
3611                let tail_frames = total_frames - write_frames;
3612                if write_frames > 0 {
3613                    writer.write_append_frames(
3614                        ds_index,
3615                        base_dim0 as u64,
3616                        write_frames as u64,
3617                        &combined[..write_frames * frame_bytes],
3618                    )?;
3619                }
3620                if tail_frames > 0 {
3621                    let ds = writer.ds(ds_index);
3622                    let mut m = ds.lock();
3623                    m.append = Some(crate::io::writer::AppendBuffer {
3624                        base: (base_dim0 + write_frames) as u64,
3625                        frames: tail_frames as u64,
3626                        bytes: combined[write_frames * frame_bytes..].to_vec(),
3627                    });
3628                }
3629
3630                // Extend dims to include all frames (buffered + new)
3631                let logical_dim0 = base_dim0 + total_frames;
3632                let mut new_dims: Vec<u64> = dims;
3633                new_dims[0] = logical_dim0 as u64;
3634                writer.extend_dataset_inner(ds_index, &new_dims)?;
3635
3636                Ok(())
3637            }
3638            DatasetInfo::Reader { .. } => {
3639                Err(Hdf5Error::InvalidState("cannot append in read mode".into()))
3640            }
3641        }
3642    }
3643
3644    /// Extend the dimensions of a chunked dataset.
3645    pub fn extend(&self, new_dims: &[usize]) -> Result<()> {
3646        match &self.info {
3647            DatasetInfo::Writer {
3648                index, chunk_index, ..
3649            } => {
3650                if chunk_index.is_none() {
3651                    return Err(Hdf5Error::InvalidState(
3652                        "extend is only for chunked datasets".into(),
3653                    ));
3654                }
3655
3656                let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
3657                let inner = borrow_inner(&self.file_inner);
3658                match &*inner {
3659                    H5FileInner::Writer(writer) => {
3660                        writer.extend_dataset(*index, &dims_u64)?;
3661                        Ok(())
3662                    }
3663                    _ => Err(Hdf5Error::InvalidState(
3664                        "file is no longer in write mode".into(),
3665                    )),
3666                }
3667            }
3668            DatasetInfo::Reader { .. } => {
3669                Err(Hdf5Error::InvalidState("cannot extend in read mode".into()))
3670            }
3671        }
3672    }
3673
3674    /// Set the logical extent of a chunked dataset, growing **or
3675    /// shrinking** any dimension.
3676    ///
3677    /// Unlike [`extend`](Self::extend), which only grows, this can reduce a
3678    /// dimension — for example to correct an over-extended frame count
3679    /// after writing a partial multi-frame chunk. Shrinking prunes the
3680    /// stored chunks the way libhdf5's `H5Dset_extent` does: a chunk
3681    /// entirely beyond the new extent is removed from the chunk index and
3682    /// its storage freed for reuse, and a chunk the new extent cuts
3683    /// through has its out-of-extent region overwritten with the fill
3684    /// value — so growing the extent back exposes fill values, not the
3685    /// old data. The new extent must not exceed the dataset's maximum
3686    /// dimensions.
3687    pub fn set_extent(&self, new_dims: &[usize]) -> Result<()> {
3688        match &self.info {
3689            DatasetInfo::Writer { index, .. } => {
3690                let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
3691                let inner = borrow_inner(&self.file_inner);
3692                match &*inner {
3693                    H5FileInner::Writer(writer) => {
3694                        writer.set_dataset_extent(*index, &dims_u64)?;
3695                        Ok(())
3696                    }
3697                    _ => Err(Hdf5Error::InvalidState(
3698                        "file is no longer in write mode".into(),
3699                    )),
3700                }
3701            }
3702            DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
3703                "cannot set extent in read mode".into(),
3704            )),
3705        }
3706    }
3707
3708    /// Flush a chunked dataset's index structures to disk.
3709    pub fn flush(&self) -> Result<()> {
3710        match &self.info {
3711            DatasetInfo::Writer { index, .. } => {
3712                let inner = borrow_inner(&self.file_inner);
3713                match &*inner {
3714                    H5FileInner::Writer(writer) => {
3715                        writer.flush_dataset(*index)?;
3716                        Ok(())
3717                    }
3718                    _ => Ok(()),
3719                }
3720            }
3721            DatasetInfo::Reader { .. } => Ok(()),
3722        }
3723    }
3724
3725    /// Read a slice (hyperslab) of the dataset as a typed vector.
3726    ///
3727    /// `starts` and `counts` define the N-dimensional selection:
3728    /// `starts[d]` = first index along dim d, `counts[d]` = how many elements.
3729    pub fn read_slice<T: H5Type>(&self, starts: &[usize], counts: &[usize]) -> Result<Vec<T>> {
3730        match &self.info {
3731            DatasetInfo::Reader {
3732                name,
3733                shape,
3734                element_size,
3735            } => {
3736                if T::element_size() != *element_size {
3737                    return Err(Hdf5Error::TypeMismatch(format!(
3738                        "read type has element size {} but dataset has element size {}",
3739                        T::element_size(),
3740                        element_size,
3741                    )));
3742                }
3743                let datatype = self.datatype()?;
3744                let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
3745                let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
3746
3747                // Bounds before sizing: the destination is allocated here,
3748                // ahead of the reader's own check, so a selection the extent
3749                // does not admit must be refused before its size is computed.
3750                let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
3751                check_hyperslab(&dims, &starts_u64, &counts_u64)?;
3752                let count = element_count(&counts_u64)?;
3753                let mut inner = borrow_inner_mut(&self.file_inner);
3754                let H5FileInner::Reader(reader) = &mut *inner else {
3755                    return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
3756                };
3757                // The selection lands in the vector this returns, so its bytes
3758                // are touched once instead of being read into a byte buffer and
3759                // copied into a second one of the same size.
3760                read_image_into_new(count, |image| {
3761                    reader.read_slice_into_dst(
3762                        name,
3763                        &starts_u64,
3764                        &counts_u64,
3765                        image,
3766                        ReadDst::Fresh,
3767                    )?;
3768                    to_host_byte_order(image, &datatype, T::element_size())
3769                })
3770            }
3771            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
3772                "cannot read_slice from a dataset in write mode".into(),
3773            )),
3774        }
3775    }
3776
3777    /// Read a strided hyperslab as a typed vector — h5py's stepped slicing
3778    /// (`ds[a:b:s]`) or the general `start`/`stride`/`count`/`block` form of
3779    /// `H5Sselect_hyperslab`.
3780    ///
3781    /// One entry per dimension: `start[d]` is the first index, `stride[d]`
3782    /// the spacing between selected blocks (all-`1` is the same selection
3783    /// [`read_slice`](Self::read_slice) reads), `count[d]` how many blocks,
3784    /// and `block[d]` how many contiguous elements each block covers. The
3785    /// returned vector is row-major over `count[d] * block[d]` per
3786    /// dimension — exactly the shape h5py's stepped slicing produces.
3787    ///
3788    /// ```no_run
3789    /// # use rust_hdf5::H5File;
3790    /// let file = H5File::open("data.h5").unwrap();
3791    /// let ds = file.dataset("series").unwrap(); // shape [100]
3792    /// // Python: ds[0:100:2] — every other element.
3793    /// let evens: Vec<f64> = ds.read_hyperslab(&[0], &[2], &[50], &[1]).unwrap();
3794    /// ```
3795    pub fn read_hyperslab<T: H5Type>(
3796        &self,
3797        start: &[usize],
3798        stride: &[usize],
3799        count: &[usize],
3800        block: &[usize],
3801    ) -> Result<Vec<T>> {
3802        match &self.info {
3803            DatasetInfo::Reader {
3804                name, element_size, ..
3805            } => {
3806                if T::element_size() != *element_size {
3807                    return Err(Hdf5Error::TypeMismatch(format!(
3808                        "read type has element size {} but dataset has element size {}",
3809                        T::element_size(),
3810                        element_size,
3811                    )));
3812                }
3813                let datatype = self.datatype()?;
3814                let start_u64: Vec<u64> = start.iter().map(|&s| s as u64).collect();
3815                let stride_u64: Vec<u64> = stride.iter().map(|&s| s as u64).collect();
3816                let count_u64: Vec<u64> = count.iter().map(|&c| c as u64).collect();
3817                let block_u64: Vec<u64> = block.iter().map(|&b| b as u64).collect();
3818
3819                let selected: Vec<u64> = count_u64
3820                    .iter()
3821                    .zip(&block_u64)
3822                    .map(|(&c, &b)| c.saturating_mul(b))
3823                    .collect();
3824                let n = element_count(&selected)?;
3825                let mut inner = borrow_inner_mut(&self.file_inner);
3826                let H5FileInner::Reader(reader) = &mut *inner else {
3827                    return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
3828                };
3829                read_image_into_new(n, |image| {
3830                    reader.read_hyperslab_into(
3831                        name,
3832                        &start_u64,
3833                        &stride_u64,
3834                        &count_u64,
3835                        &block_u64,
3836                        image,
3837                    )?;
3838                    to_host_byte_order(image, &datatype, T::element_size())
3839                })
3840            }
3841            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
3842                "cannot read_hyperslab from a dataset in write mode".into(),
3843            )),
3844        }
3845    }
3846
3847    /// Read a list of coordinates in one call, as a typed vector — h5py
3848    /// fancy indexing with a coordinate list.
3849    ///
3850    /// `points[i]` is a coordinate with one entry per dimension. The
3851    /// returned vector holds one element per point, in the same order as
3852    /// `points`, regardless of the dataset's rank.
3853    ///
3854    /// ```no_run
3855    /// # use rust_hdf5::H5File;
3856    /// let file = H5File::open("data.h5").unwrap();
3857    /// let ds = file.dataset("grid").unwrap(); // shape [10, 10]
3858    /// // Python: ds[np.array([[0, 0], [3, 4], [9, 9]])]
3859    /// let picked: Vec<f64> = ds.read_points(&[vec![0, 0], vec![3, 4], vec![9, 9]]).unwrap();
3860    /// ```
3861    pub fn read_points<T: H5Type>(&self, points: &[Vec<usize>]) -> Result<Vec<T>> {
3862        match &self.info {
3863            DatasetInfo::Reader {
3864                name, element_size, ..
3865            } => {
3866                if T::element_size() != *element_size {
3867                    return Err(Hdf5Error::TypeMismatch(format!(
3868                        "read type has element size {} but dataset has element size {}",
3869                        T::element_size(),
3870                        element_size,
3871                    )));
3872                }
3873                let datatype = self.datatype()?;
3874                let points_u64: Vec<Vec<u64>> = points
3875                    .iter()
3876                    .map(|p| p.iter().map(|&c| c as u64).collect())
3877                    .collect();
3878
3879                let mut inner = borrow_inner_mut(&self.file_inner);
3880                let H5FileInner::Reader(reader) = &mut *inner else {
3881                    return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
3882                };
3883                read_image_into_new(points_u64.len(), |image| {
3884                    reader.read_points_into(name, &points_u64, image, ReadDst::Fresh)?;
3885                    to_host_byte_order(image, &datatype, T::element_size())
3886                })
3887            }
3888            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
3889                "cannot read_points from a dataset in write mode".into(),
3890            )),
3891        }
3892    }
3893
3894    /// Read one chunk's raw (still-filtered) bytes and its filter mask,
3895    /// addressed by chunk-grid coordinates — the read half of
3896    /// [`write_chunk_raw_at`](Self::write_chunk_raw_at) and the HDF5 "direct
3897    /// chunk read" (`H5Dread_chunk`, formerly `H5DOread_chunk`; h5py's
3898    /// `Dataset.id.read_direct_chunk`).
3899    ///
3900    /// The bytes are exactly what is stored on disk: filtered/compressed if
3901    /// the dataset has a filter pipeline, with no decompression applied. The
3902    /// returned `u32` is the chunk's filter mask: bit *i* set means filter
3903    /// *i* of the pipeline was **not** applied to this particular chunk and
3904    /// must be skipped when reversing it.
3905    ///
3906    /// `Err` if the dataset is not chunked, `chunk_coords` has the wrong
3907    /// rank, or the chunk at those coordinates has never been written.
3908    ///
3909    /// ```no_run
3910    /// # use rust_hdf5::H5File;
3911    /// let file = H5File::open("data.h5").unwrap();
3912    /// let ds = file.dataset("frames").unwrap();
3913    /// let (raw, filter_mask) = ds.read_chunk_raw_at(&[0, 0]).unwrap();
3914    /// ```
3915    pub fn read_chunk_raw_at(&self, chunk_coords: &[usize]) -> Result<(Vec<u8>, u32)> {
3916        match &self.info {
3917            DatasetInfo::Reader { name, .. } => {
3918                let coords_u64: Vec<u64> = chunk_coords.iter().map(|&c| c as u64).collect();
3919                let mut inner = borrow_inner_mut(&self.file_inner);
3920                match &mut *inner {
3921                    H5FileInner::Reader(reader) => Ok(reader.read_chunk_raw_at(name, &coords_u64)?),
3922                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
3923                }
3924            }
3925            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
3926                "cannot read_chunk_raw_at from a dataset in write mode".into(),
3927            )),
3928        }
3929    }
3930
3931    /// Write a typed slice to a sub-region of the dataset.
3932    ///
3933    /// `starts` and `counts` define the N-dimensional selection, which must lie
3934    /// inside the dataset's current extent.
3935    ///
3936    /// Works for both contiguous and chunked datasets. For a chunked dataset
3937    /// only the chunks the selection touches are rewritten — a partially
3938    /// covered chunk is read back, patched, and written again, so updating one
3939    /// row of an appendable dataset costs the chunks that row crosses rather
3940    /// than the whole dataset. Elements of a touched chunk that the selection
3941    /// does not cover keep their stored value, or the dataset's fill value if
3942    /// the chunk did not exist yet.
3943    pub fn write_slice<T: H5Type>(
3944        &self,
3945        starts: &[usize],
3946        counts: &[usize],
3947        data: &[T],
3948    ) -> Result<()> {
3949        match &self.info {
3950            DatasetInfo::Writer {
3951                index,
3952                element_size,
3953                ..
3954            } => {
3955                if T::element_size() != *element_size {
3956                    return Err(Hdf5Error::TypeMismatch(format!(
3957                        "write type has element size {} but dataset expects {}",
3958                        T::element_size(),
3959                        element_size,
3960                    )));
3961                }
3962
3963                let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
3964                let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
3965
3966                let inner = borrow_inner(&self.file_inner);
3967                let H5FileInner::Writer(writer) = &*inner else {
3968                    return Err(Hdf5Error::InvalidState(
3969                        "file is no longer in write mode".into(),
3970                    ));
3971                };
3972
3973                // Bounds before sizing, against the extent the writer holds
3974                // now (an extend since this handle was taken counts): a
3975                // selection the extent does not admit is refused for that
3976                // reason, not for the size it would have had.
3977                check_hyperslab(&writer.dataset_dims(*index), &starts_u64, &counts_u64)?;
3978                let expected = element_count(&counts_u64)?;
3979                if data.len() != expected {
3980                    return Err(Hdf5Error::InvalidState(format!(
3981                        "data length {} does not match slice size {}",
3982                        data.len(),
3983                        expected,
3984                    )));
3985                }
3986
3987                let byte_len = data.len() * T::element_size();
3988                let host =
3989                    unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
3990
3991                let datatype = writer.dataset_datatype(*index);
3992                let stored = to_stored_byte_order(host, &datatype, T::element_size())?;
3993                writer.write_slice(*index, &starts_u64, &counts_u64, &stored)?;
3994                Ok(())
3995            }
3996            DatasetInfo::Reader { .. } => {
3997                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
3998            }
3999        }
4000    }
4001
4002    /// Replace elements `start .. start + strings.len()` of a 1-D
4003    /// variable-length string dataset.
4004    ///
4005    /// The extent and every element outside the range are left alone, and the
4006    /// cost is the new strings plus the chunks holding their references — not
4007    /// the column. The dataset's character set is enforced: a non-ASCII
4008    /// replacement in a dataset that declares ASCII is rejected rather than
4009    /// stored under a datatype that misdescribes it.
4010    ///
4011    /// The global heap objects the replaced references pointed at are freed —
4012    /// the same reclaim libhdf5 performs on an overwrite — so updating one
4013    /// element repeatedly reuses space rather than growing the file. A
4014    /// collection emptied by the update returns its block to the allocator.
4015    /// Under SWMR nothing is freed, because a reader may still be following
4016    /// those references.
4017    ///
4018    /// ```no_run
4019    /// # use rust_hdf5::H5File;
4020    /// let file = H5File::open_rw("meta.h5").unwrap();
4021    /// let ds = file.dataset_writer("notes").unwrap();
4022    /// ds.write_vlen_strings_slice(42, &["replacement"]).unwrap();
4023    /// file.close().unwrap();
4024    /// ```
4025    pub fn write_vlen_strings_slice(&self, start: usize, strings: &[&str]) -> Result<()> {
4026        match &self.info {
4027            DatasetInfo::Writer { index, .. } => {
4028                let inner = borrow_inner(&self.file_inner);
4029                match &*inner {
4030                    H5FileInner::Writer(writer) => {
4031                        writer.write_vlen_strings_slice(*index, start as u64, strings)?;
4032                        Ok(())
4033                    }
4034                    _ => Err(Hdf5Error::InvalidState(
4035                        "file is no longer in write mode".into(),
4036                    )),
4037                }
4038            }
4039            DatasetInfo::Reader { .. } => {
4040                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
4041            }
4042        }
4043    }
4044
4045    /// Read variable-length strings from a dataset.
4046    ///
4047    /// This handles h5py-style vlen string datasets that store strings
4048    /// as global heap references. Returns one String per element.
4049    pub fn read_vlen_strings(&self) -> Result<Vec<String>> {
4050        match &self.info {
4051            DatasetInfo::Reader { name, .. } => {
4052                let mut inner = borrow_inner_mut(&self.file_inner);
4053                match &mut *inner {
4054                    H5FileInner::Reader(reader) => Ok(reader.read_vlen_strings(name)?),
4055                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
4056                }
4057            }
4058            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4059                "cannot read vlen strings from a dataset in write mode".into(),
4060            )),
4061        }
4062    }
4063
4064    /// Read variable-length byte arrays from a dataset.
4065    ///
4066    /// This handles vlen byte-array datasets (a vlen sequence of `u8`, e.g.
4067    /// those written by [`write_vlen_bytes`](crate::H5File::write_vlen_bytes))
4068    /// that store each element as a global heap reference. Returns one
4069    /// `Vec<u8>` per element.
4070    pub fn read_vlen_bytes(&self) -> Result<Vec<Vec<u8>>> {
4071        match &self.info {
4072            DatasetInfo::Reader { name, .. } => {
4073                let mut inner = borrow_inner_mut(&self.file_inner);
4074                match &mut *inner {
4075                    H5FileInner::Reader(reader) => Ok(reader.read_vlen_bytes(name)?),
4076                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
4077                }
4078            }
4079            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4080                "cannot read vlen bytes from a dataset in write mode".into(),
4081            )),
4082        }
4083    }
4084
4085    /// Write object references naming `paths` into elements `0..paths.len()`
4086    /// — h5py's `refs[i] = f['/target'].ref`.
4087    ///
4088    /// The dataset must have been created with
4089    /// [`object_references`](DatasetBuilder::object_references). A path names
4090    /// a dataset or a group (`/` is the root group) and must already exist;
4091    /// what reaches the file is the target's object header address, which is
4092    /// assigned when the file is finalized. Elements left unwritten read back
4093    /// as null references.
4094    pub fn write_object_references(&self, paths: &[&str]) -> Result<()> {
4095        match &self.info {
4096            DatasetInfo::Writer { index, .. } => {
4097                let inner = borrow_inner(&self.file_inner);
4098                match &*inner {
4099                    H5FileInner::Writer(writer) => {
4100                        writer.write_object_references(*index, 0, paths)?;
4101                        Ok(())
4102                    }
4103                    _ => Err(Hdf5Error::InvalidState(
4104                        "file is no longer in write mode".into(),
4105                    )),
4106                }
4107            }
4108            DatasetInfo::Reader { .. } => {
4109                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
4110            }
4111        }
4112    }
4113
4114    /// Mark this dataset as a dimension scale — `H5DSset_scale`, h5py's
4115    /// `ds.make_scale(name)`.
4116    ///
4117    /// Writes the `CLASS` attribute as the fixed-length null-terminated
4118    /// string `DIMENSION_SCALE` (16 bytes, the width `H5DSis_scale` checks
4119    /// for) and, when `name` is given, `NAME` the same way. A dataset that
4120    /// has scales attached to it cannot become one. Write mode only.
4121    ///
4122    /// ```no_run
4123    /// # use rust_hdf5::H5File;
4124    /// let file = H5File::create("scales.h5").unwrap();
4125    /// let x = file.new_dataset::<f64>().shape([4]).create("x").unwrap();
4126    /// x.write_raw(&[0.0, 0.5, 1.0, 1.5]).unwrap();
4127    /// x.set_scale(Some("x")).unwrap();
4128    /// ```
4129    pub fn set_scale(&self, name: Option<&str>) -> Result<()> {
4130        let index = self.writer_index("set_scale")?;
4131        let inner = borrow_inner(&self.file_inner);
4132        match &*inner {
4133            H5FileInner::Writer(writer) => Ok(writer.set_dimension_scale(index, name)?),
4134            _ => Err(Hdf5Error::InvalidState(
4135                "file is no longer in write mode".into(),
4136            )),
4137        }
4138    }
4139
4140    /// Attach `scale` as a dimension scale of this dataset's `axis` —
4141    /// `H5DSattach_scale`, h5py's `ds.dims[axis].attach_scale(scale)`.
4142    ///
4143    /// Records the attachment in this dataset's `DIMENSION_LIST` and the
4144    /// scale's `REFERENCE_LIST`, and marks `scale` as a dimension scale if it
4145    /// is not one yet. An axis may carry several scales: each attach appends.
4146    /// Attaching a scale already on that axis changes nothing. Both datasets
4147    /// must belong to this file, in write mode; a scale cannot have scales of
4148    /// its own, a dataset that is a scale cannot have scales attached, and
4149    /// `axis` must be below the rank (a scalar dataset counts as rank 1).
4150    ///
4151    /// ```no_run
4152    /// # use rust_hdf5::H5File;
4153    /// let file = H5File::create("scales.h5").unwrap();
4154    /// let data = file.new_dataset::<i32>().shape([2, 3]).create("data").unwrap();
4155    /// let x = file.new_dataset::<f64>().shape([3]).create("x").unwrap();
4156    /// x.set_scale(Some("x")).unwrap();
4157    /// data.attach_scale(1, &x).unwrap();
4158    /// ```
4159    pub fn attach_scale(&self, axis: usize, scale: &H5Dataset) -> Result<()> {
4160        let did = self.writer_index("attach_scale")?;
4161        let dsid = scale.writer_index("attach_scale")?;
4162        if !same_inner(&self.file_inner, &scale.file_inner) {
4163            return Err(Hdf5Error::InvalidState(
4164                "the dimension scale belongs to another file".into(),
4165            ));
4166        }
4167        let inner = borrow_inner(&self.file_inner);
4168        match &*inner {
4169            H5FileInner::Writer(writer) => Ok(writer.attach_dimension_scale(did, dsid, axis)?),
4170            _ => Err(Hdf5Error::InvalidState(
4171                "file is no longer in write mode".into(),
4172            )),
4173        }
4174    }
4175
4176    /// This dataset's index in the writer's registry, or the error a
4177    /// write-only operation `what` reports on a read-mode handle.
4178    fn writer_index(&self, what: &str) -> Result<usize> {
4179        match &self.info {
4180            DatasetInfo::Writer { index, .. } => Ok(*index),
4181            DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(format!(
4182                "{what} is only available in write mode"
4183            ))),
4184        }
4185    }
4186
4187    /// Write region references over `targets` into elements
4188    /// `0..targets.len()` — h5py's `refs[i] = f['/target'].regionref[0:3]`.
4189    ///
4190    /// The dataset must have been created with
4191    /// [`region_references`](DatasetBuilder::region_references). Each target is
4192    /// the path of an existing *dataset* and a [`Selection`] over it, which
4193    /// must fit that dataset's extent — the rule `H5Rcreate` applies. What
4194    /// reaches the file is a global-heap object holding the target's object
4195    /// header address (assigned when the file is finalized) and the serialized
4196    /// selection. Elements left unwritten read back as null references.
4197    ///
4198    /// ```no_run
4199    /// # use rust_hdf5::{H5File, PointSelection, Selection};
4200    /// let file = H5File::create("regions.h5").unwrap();
4201    /// file.new_dataset::<i32>().shape([4, 6]).create("m").unwrap();
4202    /// let refs = file.new_dataset::<u64>()
4203    ///     .region_references()
4204    ///     .shape([1])
4205    ///     .create("refs")
4206    ///     .unwrap();
4207    /// let points = Selection::Points(PointSelection {
4208    ///     rank: 2,
4209    ///     points: vec![vec![0, 1], vec![3, 5]],
4210    /// });
4211    /// refs.write_region_references(&[("/m", points)]).unwrap();
4212    /// file.close().unwrap();
4213    /// ```
4214    pub fn write_region_references(&self, targets: &[(&str, Selection)]) -> Result<()> {
4215        match &self.info {
4216            DatasetInfo::Writer { index, .. } => {
4217                let inner = borrow_inner(&self.file_inner);
4218                match &*inner {
4219                    H5FileInner::Writer(writer) => {
4220                        writer.write_region_references(*index, 0, targets)?;
4221                        Ok(())
4222                    }
4223                    _ => Err(Hdf5Error::InvalidState(
4224                        "file is no longer in write mode".into(),
4225                    )),
4226                }
4227            }
4228            DatasetInfo::Reader { .. } => {
4229                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
4230            }
4231        }
4232    }
4233
4234    /// Write revised region references over `targets` into elements
4235    /// `0..targets.len()` — `H5Rcreate_region` plus `H5Dwrite` of an
4236    /// `H5T_STD_REF` dataset.
4237    ///
4238    /// The dataset must have been created with
4239    /// [`std_region_references`](DatasetBuilder::std_region_references) or one
4240    /// of its two siblings, which make the same datatype. Each target is the
4241    /// path of an existing *dataset* and a [`Selection`] over it, which must
4242    /// fit that dataset's extent. What reaches the file is a global-heap blob
4243    /// holding the target's object header address (assigned when the file is
4244    /// finalized) and the serialized selection, and an element carrying the
4245    /// blob's id and its byte count. Elements left unwritten read back as null
4246    /// references.
4247    pub fn write_std_region_references(&self, targets: &[(&str, Selection)]) -> Result<()> {
4248        let targets: Vec<(&str, ReferenceTarget)> = targets
4249            .iter()
4250            .map(|(path, selection)| (*path, ReferenceTarget::Region(selection.clone())))
4251            .collect();
4252        self.write_revised_references(&targets)
4253    }
4254
4255    /// Write attribute references naming `targets` into elements
4256    /// `0..targets.len()` — `H5Rcreate_attr` plus `H5Dwrite` of an
4257    /// `H5T_STD_REF` dataset.
4258    ///
4259    /// Each target is the path of an existing object — a dataset, a group, or
4260    /// `/` for the root group — and the name of an attribute it already
4261    /// carries. There is no pre-1.12 form of this reference kind, so the
4262    /// dataset must have been created with
4263    /// [`attribute_references`](DatasetBuilder::attribute_references) or one of
4264    /// its two siblings. Elements left unwritten read back as null references.
4265    pub fn write_attribute_references(&self, targets: &[(&str, &str)]) -> Result<()> {
4266        let targets: Vec<(&str, ReferenceTarget)> = targets
4267            .iter()
4268            .map(|(path, name)| (*path, ReferenceTarget::Attribute((*name).to_string())))
4269            .collect();
4270        self.write_revised_references(&targets)
4271    }
4272
4273    /// Store `targets` as 1.12 reference elements, whatever mix of kinds they
4274    /// are: the one path both revised-reference writers take.
4275    fn write_revised_references(&self, targets: &[(&str, ReferenceTarget)]) -> Result<()> {
4276        match &self.info {
4277            DatasetInfo::Writer { index, .. } => {
4278                let inner = borrow_inner(&self.file_inner);
4279                match &*inner {
4280                    H5FileInner::Writer(writer) => {
4281                        writer.write_revised_references(*index, 0, targets)?;
4282                        Ok(())
4283                    }
4284                    _ => Err(Hdf5Error::InvalidState(
4285                        "file is no longer in write mode".into(),
4286                    )),
4287                }
4288            }
4289            DatasetInfo::Reader { .. } => {
4290                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
4291            }
4292        }
4293    }
4294
4295    /// Read a reference dataset's elements, each resolved to the object it
4296    /// names.
4297    ///
4298    /// Every reference kind is read: the pre-1.12 pair h5py writes —
4299    /// `Reference` (an object header address) and `RegionReference` (a heap id
4300    /// whose heap object holds the target plus a serialized selection) — and
4301    /// the 1.12 `H5T_STD_REF` trio, `H5R_OBJECT2`, `H5R_DATASET_REGION2` and
4302    /// `H5R_ATTR`. An object reference comes back as [`Reference::Object`]
4303    /// carrying the target's path, a region reference as
4304    /// [`Reference::Region`], whose [`bounds`](Reference::bounds) is the
4305    /// selection's bounding box — libhdf5's `H5Sget_select_bounds` — and an
4306    /// attribute reference as [`Reference::Attr`], which adds the attribute's
4307    /// name.
4308    ///
4309    /// A 1.12 reference written into a file other than its target's carries
4310    /// that file's name, and [`Reference::file`] reports it; the path is then
4311    /// a path inside that file, resolved by opening it under the name the
4312    /// reference carries, and `None` when nothing is there.
4313    ///
4314    /// ```no_run
4315    /// # use rust_hdf5::H5File;
4316    /// let file = H5File::open("refs.h5").unwrap();
4317    /// for r in file.dataset("refs").unwrap().read_references().unwrap() {
4318    ///     println!("{:?} {:?}", r.path(), r.bounds());
4319    /// }
4320    /// ```
4321    pub fn read_references(&self) -> Result<Vec<Reference>> {
4322        match &self.info {
4323            DatasetInfo::Reader { name, .. } => {
4324                let mut inner = borrow_inner_mut(&self.file_inner);
4325                match &mut *inner {
4326                    H5FileInner::Reader(reader) => Ok(reader.read_references(name)?),
4327                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
4328                }
4329            }
4330            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4331                "cannot read references from a dataset in write mode".into(),
4332            )),
4333        }
4334    }
4335
4336    /// Read a string dataset, fixed-width or variable-length, as one `String`
4337    /// per element.
4338    ///
4339    /// The width of a `FixedString` dataset is whatever the file says, so a
4340    /// 24-byte label column and a 100-byte one are read by the same call. The
4341    /// padding rule the datatype declares decides where each element ends —
4342    /// null-terminated (0), null-padded (1) or space-padded (2) — and its
4343    /// character set decides how the remaining bytes are decoded: ASCII (0)
4344    /// requires 7-bit bytes, UTF-8 (1) requires valid UTF-8. An element that
4345    /// violates either is an error naming the element, not a silent
4346    /// substitution; [`read_strings_lossy`](Self::read_strings_lossy) is the
4347    /// call that accepts such a file, replacing what it cannot decode.
4348    ///
4349    /// ```no_run
4350    /// # use rust_hdf5::H5File;
4351    /// let file = H5File::open("labels.h5").unwrap();
4352    /// let labels = file.dataset("names").unwrap().read_strings().unwrap();
4353    /// ```
4354    pub fn read_strings(&self) -> Result<Vec<String>> {
4355        self.read_strings_inner(false)
4356    }
4357
4358    /// [`read_strings`](Self::read_strings), but bytes that do not decode
4359    /// under the dataset's character set become U+FFFD instead of an error.
4360    ///
4361    /// Producers do mislabel the character set — a file that declares ASCII
4362    /// while storing Latin-1 or UTF-8 bytes reads here and not there.
4363    pub fn read_strings_lossy(&self) -> Result<Vec<String>> {
4364        self.read_strings_inner(true)
4365    }
4366
4367    /// The single owner of string decoding for both string datatypes: the
4368    /// element bytes are found differently, the padding and character-set
4369    /// rules that turn them into a `String` are the same.
4370    fn read_strings_inner(&self, lossy: bool) -> Result<Vec<String>> {
4371        if matches!(self.info, DatasetInfo::Writer { .. }) {
4372            return Err(Hdf5Error::InvalidState(
4373                "cannot read strings from a dataset in write mode".into(),
4374            ));
4375        }
4376        match self.datatype()? {
4377            DatatypeMessage::VarLenString { charset, .. } => self
4378                .read_vlen_bytes()?
4379                .iter()
4380                .enumerate()
4381                .map(|(i, bytes)| decode_string(bytes, charset, lossy, i))
4382                .collect(),
4383            DatatypeMessage::FixedString {
4384                size,
4385                padding,
4386                charset,
4387            } => {
4388                let width = size as usize;
4389                if width == 0 {
4390                    // A corrupt file can declare it; `chunks_exact(0)` panics.
4391                    return Err(Hdf5Error::InvalidState(
4392                        "fixed-string datatype has zero width".into(),
4393                    ));
4394                }
4395                // `read_raw_bytes` returns `product(dims) * width` bytes, so
4396                // `chunks_exact` leaves no remainder.
4397                let raw = self.read_raw_bytes()?;
4398                raw.chunks_exact(width)
4399                    .enumerate()
4400                    .map(|(i, elem)| {
4401                        decode_string(trim_fixed_string(elem, padding, i)?, charset, lossy, i)
4402                    })
4403                    .collect()
4404            }
4405            other => Err(Hdf5Error::InvalidState(format!(
4406                "read_strings is only for string datasets, this one is {other:?}"
4407            ))),
4408        }
4409    }
4410
4411    /// Read the entire dataset as a typed vector.
4412    ///
4413    /// The raw bytes are read from the file and reinterpreted as `T`. The
4414    /// caller must ensure that `T` matches the datatype used when the dataset
4415    /// was written.
4416    ///
4417    /// # Errors
4418    ///
4419    /// Returns an error if:
4420    /// - The file is in write mode.
4421    /// - The raw data size is not a multiple of `T::element_size()`.
4422    pub fn read_raw<T: H5Type>(&self) -> Result<Vec<T>> {
4423        match &self.info {
4424            DatasetInfo::Reader {
4425                name, element_size, ..
4426            } => {
4427                if T::element_size() != *element_size {
4428                    return Err(Hdf5Error::TypeMismatch(format!(
4429                        "read type has element size {} but dataset has element size {}",
4430                        T::element_size(),
4431                        element_size,
4432                    )));
4433                }
4434
4435                let datatype = self.datatype()?;
4436                let mut inner = borrow_inner_mut(&self.file_inner);
4437                let H5FileInner::Reader(reader) = &mut *inner else {
4438                    return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
4439                };
4440                let total = reader.dataset_raw_size(name)? as usize;
4441                if !total.is_multiple_of(T::element_size()) {
4442                    return Err(Hdf5Error::TypeMismatch(format!(
4443                        "raw data size {total} is not a multiple of element size {}",
4444                        T::element_size(),
4445                    )));
4446                }
4447
4448                // The image is read into the vector this returns, so the
4449                // bytes are touched once rather than being zeroed, read, and
4450                // then copied into a second buffer of the same size.
4451                read_image_into_new(total / T::element_size(), |image| {
4452                    reader.read_dataset_raw_into_dst(name, image, ReadDst::Fresh)?;
4453                    to_host_byte_order(image, &datatype, T::element_size())
4454                })
4455            }
4456            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4457                "cannot read from a dataset in write mode".into(),
4458            )),
4459        }
4460    }
4461
4462    /// View the entire dataset as `&[T]` pointing straight into the file's
4463    /// memory map — no read, no copy, no allocation.
4464    ///
4465    /// The returned [`MappedView<T>`](crate::MappedView) dereferences to
4466    /// `&[T]` holding exactly what [`read_raw`](Self::read_raw) would have
4467    /// returned, bit for bit.
4468    ///
4469    /// # When it works
4470    ///
4471    /// The file must be open read-only and mapped (which a read-only open
4472    /// does whenever the OS allows), the dataset's raw data must be one
4473    /// contiguous stretch of that file, and its stored elements must already
4474    /// be the host image of a `T` — same width, host byte order, significant
4475    /// bits filling the element. That is the ordinary case for an
4476    /// uncompressed, non-chunked numeric dataset written by either this crate
4477    /// or libhdf5.
4478    ///
4479    /// # When it refuses
4480    ///
4481    /// Zero-copy is a contract, not an optimization: when the bytes cannot be
4482    /// handed over as they lie, this returns
4483    /// [`Hdf5Error::NotViewable`] naming the
4484    /// reason and never quietly falls back to copying. Every
4485    /// [`ViewRefusal`](crate::ViewRefusal) is a case where
4486    /// [`read_raw`](Self::read_raw) still works: the file is not mapped (an
4487    /// open that holds no shared lock never maps), the layout is chunked,
4488    /// compact, virtual, or external, no storage is
4489    /// allocated (the dataset reads as its fill value), `T` is the wrong
4490    /// width, the stored elements need a byte-order swap or bit unpacking,
4491    /// the data lands at an offset `T`'s alignment does not permit, or the
4492    /// image runs past the end of the map.
4493    ///
4494    /// # Snapshot semantics
4495    ///
4496    /// The view owns a share of the map rather than borrowing the file
4497    /// handle, so it stays readable after the dataset and the file are
4498    /// dropped, and after a SWMR refresh has retaken the map — a live view
4499    /// keeps showing the file as it was when *its* map was taken, while the
4500    /// refreshed handle reads the new one. Nothing about a view is
4501    /// invalidated by anything this process does. The share carries the
4502    /// shared file lock the map was taken under, so for as long as any view
4503    /// is alive a writer that honours locks cannot open the file, whether or
4504    /// not the reader that took the map is still open.
4505    ///
4506    /// # Truncation
4507    ///
4508    /// The pages are the file's own. Another process writing the file in
4509    /// place is seen through the view, and one *truncating* it under the map
4510    /// faults with `SIGBUS` on the pages that went away. The shared lock the
4511    /// view keeps is what stands between the map and such a writer; one that
4512    /// waives locks ([`FileLocking::Disabled`](crate::FileLocking::Disabled),
4513    /// or a filesystem without them) is outside what any guard inside this
4514    /// process can see.
4515    ///
4516    /// ```no_run
4517    /// # use rust_hdf5::H5File;
4518    /// let file = H5File::open("data.h5")?;
4519    /// let ds = file.dataset("matrix")?;
4520    /// let view = ds.read_mapped::<f64>()?;
4521    /// let total: f64 = view.iter().sum();
4522    /// # Ok::<(), rust_hdf5::Hdf5Error>(())
4523    /// ```
4524    #[cfg(feature = "mmap")]
4525    pub fn read_mapped<T: H5Type>(&self) -> Result<crate::mapped::MappedView<T>> {
4526        self.mapped_view(crate::mapped::ViewRange::Whole)
4527    }
4528
4529    /// View a contiguous sub-range of the dataset as `&[T]` pointing straight
4530    /// into the file's memory map.
4531    ///
4532    /// `starts` and `counts` name the same N-dimensional selection
4533    /// [`read_slice`](Self::read_slice) takes, and the view holds exactly what
4534    /// that call would have returned — but only when the selection is one
4535    /// contiguous run of the stored image: a trailing group of dimensions
4536    /// taken whole, the dimension before it taken as one span, and a single
4537    /// index along every dimension before that. Anything else steps over
4538    /// elements a single slice cannot skip, and is refused with
4539    /// [`ViewRefusal::Range`](crate::ViewRefusal::Range) rather than gathered
4540    /// into a copy.
4541    ///
4542    /// Everything [`read_mapped`](Self::read_mapped) documents about when a
4543    /// dataset can be viewed, snapshot semantics, and truncation applies here
4544    /// unchanged.
4545    #[cfg(feature = "mmap")]
4546    pub fn read_mapped_slice<T: H5Type>(
4547        &self,
4548        starts: &[usize],
4549        counts: &[usize],
4550    ) -> Result<crate::mapped::MappedView<T>> {
4551        self.mapped_view(crate::mapped::ViewRange::Slab { starts, counts })
4552    }
4553
4554    /// The one route from a dataset handle to the file's map: ask the reader
4555    /// that owns the dataset for the facts, and hand them to
4556    /// [`crate::mapped::view`], which is the only thing that can turn them
4557    /// into a view.
4558    #[cfg(feature = "mmap")]
4559    fn mapped_view<T: H5Type>(
4560        &self,
4561        range: crate::mapped::ViewRange<'_>,
4562    ) -> Result<crate::mapped::MappedView<T>> {
4563        let DatasetInfo::Reader { name, .. } = &self.info else {
4564            return Err(Hdf5Error::InvalidState(
4565                "cannot read from a dataset in write mode".into(),
4566            ));
4567        };
4568        let mut inner = borrow_inner_mut(&self.file_inner);
4569        let H5FileInner::Reader(reader) = &mut *inner else {
4570            return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
4571        };
4572        let src = reader.dataset_view_source(name)?;
4573        crate::mapped::view::<T>(&src, range).map_err(Hdf5Error::NotViewable)
4574    }
4575
4576    /// Read the raw byte image of a dataset without an `H5Type` carrier.
4577    ///
4578    /// The counterpart to [`write_raw_bytes`](Self::write_raw_bytes): returns
4579    /// the element bytes verbatim regardless of the on-disk element type, so a
4580    /// runtime [`CompoundType`](crate::types::CompoundType) whose records have
4581    /// no matching Rust primitive can be read back and decoded by the caller.
4582    pub fn read_raw_bytes(&self) -> Result<Vec<u8>> {
4583        match &self.info {
4584            DatasetInfo::Reader { name, .. } => {
4585                let mut inner = borrow_inner_mut(&self.file_inner);
4586                match &mut *inner {
4587                    H5FileInner::Reader(reader) => Ok(reader.read_dataset_raw(name)?),
4588                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
4589                }
4590            }
4591            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4592                "cannot read from a dataset in write mode".into(),
4593            )),
4594        }
4595    }
4596
4597    /// Read a numeric dataset as `T`, converting each element from the
4598    /// on-disk datatype.
4599    ///
4600    /// Unlike [`read_raw`](Self::read_raw), which requires `T`'s size to match
4601    /// the stored element size exactly, this inspects the dataset's datatype
4602    /// message — class, signedness, byte order, width — and converts per
4603    /// element:
4604    ///
4605    /// - integer → integer: checked; a stored value that does not fit in `T`
4606    ///   is an error naming the element index and value, never a silent wrap.
4607    /// - `f32` source → `f64`: exact widening.
4608    /// - `f64` source → `f32`, float → integer, and integer → float are
4609    ///   rejected as [`TypeMismatch`](Hdf5Error::TypeMismatch).
4610    ///
4611    /// Big-endian sources are decoded according to the datatype's byte order,
4612    /// which [`read_raw`](Self::read_raw)'s size-only check would misread.
4613    ///
4614    /// ```no_run
4615    /// # use rust_hdf5::H5File;
4616    /// let file = H5File::open("data.h5").unwrap();
4617    /// let ds = file.dataset("counts").unwrap(); // stored as e.g. i16
4618    /// let counts = ds.read_numeric_as::<i64>().unwrap();
4619    /// ```
4620    pub fn read_numeric_as<T: ReadNumeric>(&self) -> Result<Vec<T>> {
4621        match &self.info {
4622            DatasetInfo::Reader { name, .. } => {
4623                let (kind, raw) = {
4624                    let mut inner = borrow_inner_mut(&self.file_inner);
4625                    match &mut *inner {
4626                        H5FileInner::Reader(reader) => {
4627                            let info = reader
4628                                .dataset_info(name)
4629                                .ok_or_else(|| Hdf5Error::NotFound(name.clone()))?;
4630                            let kind = numeric::classify(&info.datatype)?;
4631                            (kind, reader.read_dataset_raw(name)?)
4632                        }
4633                        _ => {
4634                            return Err(Hdf5Error::InvalidState("file is not in read mode".into()))
4635                        }
4636                    }
4637                };
4638                numeric::convert(kind, &raw)
4639            }
4640            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4641                "cannot read from a dataset in write mode".into(),
4642            )),
4643        }
4644    }
4645
4646    /// Read a slice (hyperslab) of a numeric dataset as `T`, with the same
4647    /// per-element datatype conversion as
4648    /// [`read_numeric_as`](Self::read_numeric_as).
4649    ///
4650    /// `starts` and `counts` define the N-dimensional selection exactly as in
4651    /// [`read_slice`](Self::read_slice).
4652    pub fn read_numeric_slice_as<T: ReadNumeric>(
4653        &self,
4654        starts: &[usize],
4655        counts: &[usize],
4656    ) -> Result<Vec<T>> {
4657        match &self.info {
4658            DatasetInfo::Reader { name, .. } => {
4659                let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
4660                let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
4661                let (kind, raw) = {
4662                    let mut inner = borrow_inner_mut(&self.file_inner);
4663                    match &mut *inner {
4664                        H5FileInner::Reader(reader) => {
4665                            let info = reader
4666                                .dataset_info(name)
4667                                .ok_or_else(|| Hdf5Error::NotFound(name.clone()))?;
4668                            let kind = numeric::classify(&info.datatype)?;
4669                            (kind, reader.read_slice(name, &starts_u64, &counts_u64)?)
4670                        }
4671                        _ => {
4672                            return Err(Hdf5Error::InvalidState("file is not in read mode".into()))
4673                        }
4674                    }
4675                };
4676                numeric::convert(kind, &raw)
4677            }
4678            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4679                "cannot read_slice from a dataset in write mode".into(),
4680            )),
4681        }
4682    }
4683
4684    /// Read the whole dataset into a caller-provided buffer, with no allocation.
4685    ///
4686    /// `out` must have exactly `product(dims)` elements (the dataset's element
4687    /// count) and `T::element_size()` must match the dataset's on-disk element
4688    /// size, otherwise an error is returned and `out` is left unspecified. The
4689    /// zero-copy counterpart of [`read_raw`](Self::read_raw): the bytes are read
4690    /// straight into `out` rather than into a fresh `Vec`, so a pinned /
4691    /// page-locked host buffer can be filled in one pass and DMA'd to a GPU
4692    /// without the extra staging copy a `read_raw` + copy-into-pinned would
4693    /// incur. Works for every layout (contiguous, compact, and chunked under
4694    /// any index); for chunked data each decoded chunk is scattered directly
4695    /// into `out`.
4696    ///
4697    /// ```no_run
4698    /// # use rust_hdf5::H5File;
4699    /// let file = H5File::open("data.h5").unwrap();
4700    /// let ds = file.dataset("frames").unwrap();
4701    /// let n: usize = ds.shape().iter().product();
4702    /// let mut buf = vec![0u16; n];           // or a pinned host allocation
4703    /// ds.read_raw_into(&mut buf).unwrap();
4704    /// ```
4705    pub fn read_raw_into<T: H5Type>(&self, out: &mut [T]) -> Result<()> {
4706        match &self.info {
4707            DatasetInfo::Reader {
4708                name, element_size, ..
4709            } => {
4710                if T::element_size() != *element_size {
4711                    return Err(Hdf5Error::TypeMismatch(format!(
4712                        "read type has element size {} but dataset has element size {}",
4713                        T::element_size(),
4714                        element_size,
4715                    )));
4716                }
4717                let datatype = self.datatype()?;
4718                // Safety: `T: H5Type` is a `Copy` POD numeric with a defined
4719                // byte representation; every bit pattern the read writes is a
4720                // valid `T`. The byte view borrows `out` exclusively for this
4721                // call, and `out.len() * element_size` cannot overflow because
4722                // it is the byte length of an existing slice (<= isize::MAX).
4723                let byte_len = out.len() * T::element_size();
4724                let bytes = unsafe {
4725                    std::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut u8, byte_len)
4726                };
4727                {
4728                    let mut inner = borrow_inner_mut(&self.file_inner);
4729                    match &mut *inner {
4730                        H5FileInner::Reader(reader) => reader.read_dataset_raw_into(name, bytes)?,
4731                        _ => {
4732                            return Err(Hdf5Error::InvalidState("file is not in read mode".into()))
4733                        }
4734                    }
4735                }
4736                to_host_byte_order(bytes, &datatype, T::element_size())
4737            }
4738            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4739                "cannot read from a dataset in write mode".into(),
4740            )),
4741        }
4742    }
4743
4744    /// Read a hyperslab into a caller-provided buffer, with no allocation.
4745    ///
4746    /// `out` must have exactly `product(counts)` elements and
4747    /// `T::element_size()` must match the dataset's element size. The zero-copy
4748    /// counterpart of [`read_slice`](Self::read_slice) and the slice analogue of
4749    /// [`read_raw_into`](Self::read_raw_into): only chunks overlapping the
4750    /// selection are read, and the selected bytes land directly in `out` — the
4751    /// entry point for reading one frame / block straight into a pinned host
4752    /// buffer for an H2D transfer.
4753    ///
4754    /// ```no_run
4755    /// # use rust_hdf5::H5File;
4756    /// let file = H5File::open("vol.h5").unwrap();
4757    /// let ds = file.dataset("vol").unwrap();   // shape [nz, ny, nx]
4758    /// let (ny, nx) = (ds.shape()[1], ds.shape()[2]);
4759    /// let mut frame = vec![0f32; ny * nx];     // or a pinned host allocation
4760    /// ds.read_slice_into(&mut frame, &[5, 0, 0], &[1, ny, nx]).unwrap();
4761    /// ```
4762    pub fn read_slice_into<T: H5Type>(
4763        &self,
4764        out: &mut [T],
4765        starts: &[usize],
4766        counts: &[usize],
4767    ) -> Result<()> {
4768        match &self.info {
4769            DatasetInfo::Reader {
4770                name, element_size, ..
4771            } => {
4772                if T::element_size() != *element_size {
4773                    return Err(Hdf5Error::TypeMismatch(format!(
4774                        "read type has element size {} but dataset has element size {}",
4775                        T::element_size(),
4776                        element_size,
4777                    )));
4778                }
4779                let datatype = self.datatype()?;
4780                let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
4781                let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
4782                // Safety: see `read_raw_into` — `T: H5Type` POD, exclusive
4783                // borrow of `out`, byte length within bounds.
4784                let byte_len = out.len() * T::element_size();
4785                let bytes = unsafe {
4786                    std::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut u8, byte_len)
4787                };
4788                {
4789                    let mut inner = borrow_inner_mut(&self.file_inner);
4790                    match &mut *inner {
4791                        H5FileInner::Reader(reader) => {
4792                            reader.read_slice_into(name, &starts_u64, &counts_u64, bytes)?
4793                        }
4794                        _ => {
4795                            return Err(Hdf5Error::InvalidState("file is not in read mode".into()))
4796                        }
4797                    }
4798                }
4799                to_host_byte_order(bytes, &datatype, T::element_size())
4800            }
4801            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4802                "cannot read from a dataset in write mode".into(),
4803            )),
4804        }
4805    }
4806}
4807
4808// ---------------------------------------------------------------------------
4809// Datatype-aware numeric conversion (read_numeric_as)
4810// ---------------------------------------------------------------------------
4811
4812/// Marker trait for the Rust types [`H5Dataset::read_numeric_as`] can convert
4813/// into: the integer primitives (checked, never wrapping) plus `f32`/`f64`
4814/// (widening only).
4815///
4816/// Sealed — the conversion policy is part of the library contract, so the
4817/// trait cannot be implemented outside this crate.
4818pub trait ReadNumeric: numeric::Sealed {}
4819impl<T: numeric::Sealed> ReadNumeric for T {}
4820
4821pub(crate) mod numeric {
4822    //! Per-element decode + checked conversion for `read_numeric_as` (and the
4823    //! attribute counterpart `H5Attribute::read_numeric_as`).
4824
4825    use crate::error::{Hdf5Error, Result};
4826    use crate::format::messages::datatype::{ByteOrder, DatatypeMessage, IeeeFormat};
4827
4828    /// The on-disk element shape `classify` accepted.
4829    #[derive(Clone, Copy)]
4830    pub enum SourceKind {
4831        Int {
4832            size: usize,
4833            signed: bool,
4834            byte_order: ByteOrder,
4835        },
4836        F16(ByteOrder),
4837        F32(ByteOrder),
4838        F64(ByteOrder),
4839    }
4840
4841    impl SourceKind {
4842        fn element_size(self) -> usize {
4843            match self {
4844                SourceKind::Int { size, .. } => size,
4845                SourceKind::F16(_) => 2,
4846                SourceKind::F32(_) => 4,
4847                SourceKind::F64(_) => 8,
4848            }
4849        }
4850    }
4851
4852    /// Widen an IEEE 754 binary16 bit pattern to `f32`, which represents every
4853    /// half — including subnormals, infinities and NaN payloads — exactly.
4854    ///
4855    /// Rust has no stable `f16` to convert through.
4856    fn f16_bits_to_f32(bits: u16) -> f32 {
4857        let sign = u32::from(bits >> 15);
4858        let exponent = u32::from((bits >> 10) & 0x1f);
4859        let mantissa = u32::from(bits & 0x03ff);
4860        if exponent == 0 {
4861            // Zero and subnormals: the value is mantissa * 2^-24, which is a
4862            // normal f32 for every mantissa, so the multiply is exact. Going
4863            // through the sign separately keeps -0.0.
4864            let magnitude = mantissa as f32 * (1.0 / 16_777_216.0);
4865            return if sign == 1 { -magnitude } else { magnitude };
4866        }
4867        let out = if exponent == 0x1f {
4868            // Infinity and NaN; shifting the mantissa maps the quiet bit onto
4869            // f32's quiet bit and preserves the rest of the payload.
4870            (sign << 31) | 0x7f80_0000 | (mantissa << 13)
4871        } else {
4872            // Normal: rebias the exponent (127 - 15) and left-align the
4873            // mantissa.
4874            (sign << 31) | ((exponent + 112) << 23) | (mantissa << 13)
4875        };
4876        f32::from_bits(out)
4877    }
4878
4879    /// Map a datatype message to a supported numeric source shape.
4880    ///
4881    /// Accepts standard-width integers (1/2/4/8 bytes, full precision, zero
4882    /// bit offset) and IEEE binary32/binary64 floats; everything else is a
4883    /// `TypeMismatch` naming what was found.
4884    pub fn classify(dt: &DatatypeMessage) -> Result<SourceKind> {
4885        match *dt {
4886            DatatypeMessage::FixedPoint {
4887                size,
4888                byte_order,
4889                signed,
4890                bit_offset,
4891                bit_precision,
4892            } => {
4893                if !matches!(size, 1 | 2 | 4 | 8)
4894                    || bit_offset != 0
4895                    || u32::from(bit_precision) != size * 8
4896                {
4897                    return Err(Hdf5Error::TypeMismatch(format!(
4898                        "fixed-point datatype (size {size}, bit offset {bit_offset}, \
4899                         precision {bit_precision}) is not a standard-width integer",
4900                    )));
4901                }
4902                Ok(SourceKind::Int {
4903                    size: size as usize,
4904                    signed,
4905                    byte_order,
4906                })
4907            }
4908            DatatypeMessage::BitField {
4909                size,
4910                byte_order,
4911                bit_offset,
4912                bit_precision,
4913            } => {
4914                // A bit field has no signed form; a full-width one is the
4915                // unsigned integer of the stored width. A narrower one would
4916                // need a shift-and-mask conversion this path does not model.
4917                if !matches!(size, 1 | 2 | 4 | 8)
4918                    || bit_offset != 0
4919                    || u32::from(bit_precision) != size * 8
4920                {
4921                    return Err(Hdf5Error::TypeMismatch(format!(
4922                        "bit-field datatype (size {size}, bit offset {bit_offset}, \
4923                         precision {bit_precision}) is not a whole-width bit field",
4924                    )));
4925                }
4926                Ok(SourceKind::Int {
4927                    size: size as usize,
4928                    signed: false,
4929                    byte_order,
4930                })
4931            }
4932            DatatypeMessage::FloatingPoint {
4933                size,
4934                byte_order,
4935                exponent_size,
4936                mantissa_size,
4937                ..
4938            } => match dt.ieee_format() {
4939                Some(IeeeFormat::Binary16) => Ok(SourceKind::F16(byte_order)),
4940                Some(IeeeFormat::Binary32) => Ok(SourceKind::F32(byte_order)),
4941                Some(IeeeFormat::Binary64) => Ok(SourceKind::F64(byte_order)),
4942                None => Err(Hdf5Error::TypeMismatch(format!(
4943                    "floating-point datatype (size {size}, exponent {exponent_size} bits, \
4944                     mantissa {mantissa_size} bits) is not an IEEE 754 interchange format",
4945                ))),
4946            },
4947            ref other => Err(Hdf5Error::TypeMismatch(format!(
4948                "dataset datatype '{other}' is not numeric",
4949            ))),
4950        }
4951    }
4952
4953    /// Decode and convert every element of `raw` into `T`.
4954    ///
4955    /// One loop per source width and lane type, so an element is a
4956    /// fixed-size load, a sign extension and a range check, and the
4957    /// `TypeMismatch` for an element that does not fit is built only when
4958    /// one is found. An empty input converts to an empty vector whatever
4959    /// the classes.
4960    pub fn convert<T: Sealed>(kind: SourceKind, raw: &[u8]) -> Result<Vec<T>> {
4961        let size = kind.element_size();
4962        if !raw.len().is_multiple_of(size) {
4963            return Err(Hdf5Error::TypeMismatch(format!(
4964                "raw data size {} is not a multiple of element size {size}",
4965                raw.len(),
4966            )));
4967        }
4968        let mut out = vec![T::default(); raw.len() / size];
4969        match kind {
4970            SourceKind::Int {
4971                size,
4972                signed,
4973                byte_order,
4974            } => {
4975                let le = byte_order == ByteOrder::LittleEndian;
4976                match (size, signed) {
4977                    (1, true) => ints::<1, i64, T>(raw, le, &mut out),
4978                    (1, false) => ints::<1, u64, T>(raw, le, &mut out),
4979                    (2, true) => ints::<2, i64, T>(raw, le, &mut out),
4980                    (2, false) => ints::<2, u64, T>(raw, le, &mut out),
4981                    (4, true) => ints::<4, i64, T>(raw, le, &mut out),
4982                    (4, false) => ints::<4, u64, T>(raw, le, &mut out),
4983                    (8, true) => ints::<8, i64, T>(raw, le, &mut out),
4984                    (8, false) => ints::<8, u64, T>(raw, le, &mut out),
4985                    _ => unreachable!("classify admits 1-, 2-, 4- and 8-byte integers only"),
4986                }
4987            }
4988            SourceKind::F16(order) => floats::<2, T>(raw, order, &mut out, |b| {
4989                T::from_f32(f16_bits_to_f32(u16::from_le_bytes(b)))
4990            }),
4991            SourceKind::F32(order) => {
4992                floats::<4, T>(raw, order, &mut out, |b| T::from_f32(f32::from_le_bytes(b)))
4993            }
4994            SourceKind::F64(order) => {
4995                floats::<8, T>(raw, order, &mut out, |b| T::from_f64(f64::from_le_bytes(b)))
4996            }
4997        }?;
4998        Ok(out)
4999    }
5000
5001    /// The integer elements of `raw`, `N` bytes each, through lane `L`.
5002    fn ints<const N: usize, L: IntLane, T: Sealed>(
5003        raw: &[u8],
5004        le: bool,
5005        out: &mut [T],
5006    ) -> Result<()> {
5007        let (chunks, _) = raw.as_chunks::<N>();
5008        for (index, (o, &chunk)) in out.iter_mut().zip(chunks).enumerate() {
5009            let mut bytes = chunk;
5010            if !le {
5011                bytes.reverse();
5012            }
5013            *o = L::load(bytes).into_target(index)?;
5014        }
5015        Ok(())
5016    }
5017
5018    /// The float elements of `raw`, `N` bytes each, `decode` taking each
5019    /// one's little-endian bytes.
5020    fn floats<const N: usize, T: Sealed>(
5021        raw: &[u8],
5022        order: ByteOrder,
5023        out: &mut [T],
5024        decode: impl Fn([u8; N]) -> Result<T>,
5025    ) -> Result<()> {
5026        let (chunks, _) = raw.as_chunks::<N>();
5027        for (o, &chunk) in out.iter_mut().zip(chunks) {
5028            let mut bytes = chunk;
5029            if order == ByteOrder::BigEndian {
5030                bytes.reverse();
5031            }
5032            *o = decode(bytes)?;
5033        }
5034        Ok(())
5035    }
5036
5037    /// The lane an integer source decodes to: `i64` for a signed source,
5038    /// `u64` for an unsigned one, so every standard width — u64::MAX
5039    /// included — is held without loss.
5040    pub trait IntLane: Copy {
5041        /// The value of an element's little-endian `bytes`, `N` in 1..=8.
5042        fn load<const N: usize>(bytes: [u8; N]) -> Self;
5043        fn into_target<T: Sealed>(self, index: usize) -> Result<T>;
5044    }
5045
5046    impl IntLane for u64 {
5047        fn load<const N: usize>(bytes: [u8; N]) -> Self {
5048            let mut padded = [0u8; 8];
5049            padded[..N].copy_from_slice(&bytes);
5050            u64::from_le_bytes(padded)
5051        }
5052        fn into_target<T: Sealed>(self, index: usize) -> Result<T> {
5053            T::from_u64(self, index)
5054        }
5055    }
5056
5057    impl IntLane for i64 {
5058        fn load<const N: usize>(bytes: [u8; N]) -> Self {
5059            // Arithmetic right shift sign-extends the low `N` bytes.
5060            let shift = 64 - 8 * N as u32;
5061            ((u64::load(bytes) << shift) as i64) >> shift
5062        }
5063        fn into_target<T: Sealed>(self, index: usize) -> Result<T> {
5064            T::from_i64(self, index)
5065        }
5066    }
5067
5068    /// The sealed half of `ReadNumeric`: how one source element becomes a
5069    /// `Self`, or a `TypeMismatch` explaining why it cannot. `index` is the
5070    /// element's position, for the message.
5071    pub trait Sealed: Copy + Default {
5072        fn from_i64(v: i64, index: usize) -> Result<Self>;
5073        fn from_u64(v: u64, index: usize) -> Result<Self>;
5074        fn from_f32(v: f32) -> Result<Self>;
5075        fn from_f64(v: f64) -> Result<Self>;
5076    }
5077
5078    macro_rules! int_targets {
5079        ($($t:ty),* $(,)?) => {$(
5080            impl Sealed for $t {
5081                fn from_i64(v: i64, index: usize) -> Result<Self> {
5082                    <$t>::try_from(v).map_err(|_| int_overflow::<$t>(v, index))
5083                }
5084                fn from_u64(v: u64, index: usize) -> Result<Self> {
5085                    <$t>::try_from(v).map_err(|_| int_overflow::<$t>(v, index))
5086                }
5087                fn from_f32(_: f32) -> Result<Self> {
5088                    Err(float_as_int::<$t>())
5089                }
5090                fn from_f64(_: f64) -> Result<Self> {
5091                    Err(float_as_int::<$t>())
5092                }
5093            }
5094        )*};
5095    }
5096    int_targets!(i8, i16, i32, i64, u8, u16, u32, u64);
5097
5098    // Not in the macro: `i128::try_from` and `u128::try_from(u64)` are
5099    // infallible, which trips clippy::unnecessary_fallible_conversions.
5100    impl Sealed for i128 {
5101        fn from_i64(v: i64, _index: usize) -> Result<Self> {
5102            Ok(Self::from(v))
5103        }
5104        fn from_u64(v: u64, _index: usize) -> Result<Self> {
5105            Ok(Self::from(v))
5106        }
5107        fn from_f32(_: f32) -> Result<Self> {
5108            Err(float_as_int::<i128>())
5109        }
5110        fn from_f64(_: f64) -> Result<Self> {
5111            Err(float_as_int::<i128>())
5112        }
5113    }
5114
5115    impl Sealed for u128 {
5116        fn from_i64(v: i64, index: usize) -> Result<Self> {
5117            Self::try_from(v).map_err(|_| int_overflow::<u128>(v, index))
5118        }
5119        fn from_u64(v: u64, _index: usize) -> Result<Self> {
5120            Ok(Self::from(v))
5121        }
5122        fn from_f32(_: f32) -> Result<Self> {
5123            Err(float_as_int::<u128>())
5124        }
5125        fn from_f64(_: f64) -> Result<Self> {
5126            Err(float_as_int::<u128>())
5127        }
5128    }
5129
5130    fn int_overflow<T>(v: impl std::fmt::Display, index: usize) -> Hdf5Error {
5131        Hdf5Error::TypeMismatch(format!(
5132            "value {v} at element {index} does not fit in {}",
5133            std::any::type_name::<T>()
5134        ))
5135    }
5136
5137    fn float_as_int<T>() -> Hdf5Error {
5138        Hdf5Error::TypeMismatch(format!(
5139            "cannot read a floating-point dataset as {}; read as f64 and convert explicitly",
5140            std::any::type_name::<T>()
5141        ))
5142    }
5143
5144    impl Sealed for f32 {
5145        fn from_i64(_: i64, _index: usize) -> Result<Self> {
5146            Err(int_as_f32())
5147        }
5148        fn from_u64(_: u64, _index: usize) -> Result<Self> {
5149            Err(int_as_f32())
5150        }
5151        fn from_f32(v: f32) -> Result<Self> {
5152            Ok(v)
5153        }
5154        fn from_f64(_: f64) -> Result<Self> {
5155            Err(Hdf5Error::TypeMismatch(
5156                "narrowing an f64 dataset to f32 loses precision; read as f64".into(),
5157            ))
5158        }
5159    }
5160
5161    fn int_as_f32() -> Hdf5Error {
5162        Hdf5Error::TypeMismatch(
5163            "cannot read an integer dataset as f32; read as an integer type and convert \
5164             explicitly"
5165                .into(),
5166        )
5167    }
5168
5169    impl Sealed for f64 {
5170        fn from_i64(_: i64, _index: usize) -> Result<Self> {
5171            Err(int_as_f64())
5172        }
5173        fn from_u64(_: u64, _index: usize) -> Result<Self> {
5174            Err(int_as_f64())
5175        }
5176        // Every f32 is exactly representable as f64.
5177        fn from_f32(v: f32) -> Result<Self> {
5178            Ok(f64::from(v))
5179        }
5180        fn from_f64(v: f64) -> Result<Self> {
5181            Ok(v)
5182        }
5183    }
5184
5185    fn int_as_f64() -> Hdf5Error {
5186        Hdf5Error::TypeMismatch(
5187            "cannot read an integer dataset as f64; integers above 2^53 lose precision — \
5188             read as an integer type and convert explicitly"
5189                .into(),
5190        )
5191    }
5192
5193    #[cfg(test)]
5194    mod tests {
5195        use super::*;
5196
5197        /// Every binary16 bit pattern class widens to the f32 with the same
5198        /// value: zeros keep their sign, subnormals stay exact, infinities and
5199        /// NaN payloads survive.
5200        #[test]
5201        fn f16_widening_is_exact() {
5202            let cases: [(u16, f32); 10] = [
5203                (0x0000, 0.0),
5204                (0x3c00, 1.0),
5205                (0xc000, -2.0),
5206                (0x3555, 0.333_251_95),   // nearest half to 1/3
5207                (0x0001, 5.960_464_5e-8), // smallest subnormal, 2^-24
5208                (0x03ff, 6.097_555e-5),   // largest subnormal
5209                (0x0400, 6.103_515_6e-5), // smallest normal
5210                (0x7bff, 65504.0),        // largest finite
5211                (0x7c00, f32::INFINITY),
5212                (0xfc00, f32::NEG_INFINITY),
5213            ];
5214            for (bits, expected) in cases {
5215                let got = f16_bits_to_f32(bits);
5216                assert_eq!(got, expected, "0x{bits:04x} widened to {got}");
5217            }
5218
5219            let neg_zero = f16_bits_to_f32(0x8000);
5220            assert_eq!(neg_zero, 0.0);
5221            assert!(neg_zero.is_sign_negative(), "-0.0 lost its sign");
5222
5223            let nan = f16_bits_to_f32(0x7e01);
5224            assert!(nan.is_nan());
5225            // The quiet bit and the payload land in f32's mantissa.
5226            assert_eq!(nan.to_bits(), 0x7fc0_2000);
5227        }
5228
5229        #[test]
5230        fn f16_source_converts_and_honors_byte_order() {
5231            let kind = classify(&DatatypeMessage::f16_type()).unwrap();
5232            // 1.0, -2.0, 0.333..., 65504
5233            let raw = [0x00, 0x3c, 0x00, 0xc0, 0x55, 0x35, 0xff, 0x7b];
5234            assert_eq!(
5235                convert::<f32>(kind, &raw).unwrap(),
5236                vec![1.0, -2.0, 0.333_251_95, 65504.0]
5237            );
5238            assert_eq!(
5239                convert::<f64>(kind, &raw).unwrap(),
5240                vec![1.0, -2.0, 0.333_251_953_125, 65504.0]
5241            );
5242
5243            let DatatypeMessage::FloatingPoint { .. } = DatatypeMessage::f16_type() else {
5244                unreachable!()
5245            };
5246            let mut be = DatatypeMessage::f16_type();
5247            if let DatatypeMessage::FloatingPoint { byte_order, .. } = &mut be {
5248                *byte_order = ByteOrder::BigEndian;
5249            }
5250            let be_kind = classify(&be).unwrap();
5251            assert_eq!(convert::<f32>(be_kind, &[0x3c, 0x00]).unwrap(), vec![1.0]);
5252        }
5253
5254        /// A float whose layout is not an interchange format is refused, not
5255        /// reinterpreted.
5256        #[test]
5257        fn non_ieee_float_is_refused() {
5258            let mut odd = DatatypeMessage::f32_type();
5259            if let DatatypeMessage::FloatingPoint { exponent_bias, .. } = &mut odd {
5260                *exponent_bias = 63;
5261            }
5262            assert!(odd.ieee_format().is_none());
5263            let err = classify(&odd).err().expect("non-IEEE float was accepted");
5264            assert!(
5265                err.to_string().contains("IEEE 754 interchange format"),
5266                "unexpected error: {err}"
5267            );
5268        }
5269    }
5270}
5271
5272#[cfg(test)]
5273mod tests {
5274    use crate::H5File;
5275    use std::path::PathBuf;
5276
5277    fn temp_path(name: &str) -> PathBuf {
5278        // Include PID + a per-call atomic counter so that concurrent
5279        // cargo invocations and any kernel-level "lock not yet
5280        // released" races between sequential opens cannot collide.
5281        use std::sync::atomic::{AtomicU64, Ordering};
5282        static COUNTER: AtomicU64 = AtomicU64::new(0);
5283        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
5284        std::env::temp_dir().join(format!(
5285            "hdf5_dataset_test_{}_{}_{}.h5",
5286            name,
5287            std::process::id(),
5288            n
5289        ))
5290    }
5291
5292    #[test]
5293    fn runtime_compound_via_datatype_override_and_raw_bytes() {
5294        use crate::format::messages::datatype::DatatypeMessage;
5295        use crate::types::{CompoundType, H5Type};
5296
5297        let path = temp_path("compound_raw");
5298        // A 12-byte packed compound with NO matching Rust primitive carrier,
5299        // so it can only be written through the datatype() override +
5300        // write_raw_bytes path (the runtime-CompoundType use case).
5301        let ct = CompoundType {
5302            members: vec![
5303                ("id".to_string(), i32::hdf5_type(), 0),
5304                ("val".to_string(), f64::hdf5_type(), 4),
5305            ],
5306            total_size: 12,
5307        };
5308        let recs: [(i32, f64); 3] = [(1, 2.5), (2, 3.5), (3, -4.0)];
5309        let mut bytes = Vec::new();
5310        for (id, val) in recs {
5311            bytes.extend_from_slice(&id.to_le_bytes());
5312            bytes.extend_from_slice(&val.to_le_bytes());
5313        }
5314
5315        {
5316            let file = H5File::create(&path).unwrap();
5317            let ds = file
5318                .new_dataset::<u8>()
5319                .datatype(ct.to_datatype())
5320                .shape([recs.len()])
5321                .create("records")
5322                .unwrap();
5323            ds.write_raw_bytes(&bytes).unwrap();
5324            file.close().unwrap();
5325        }
5326        {
5327            let file = H5File::open(&path).unwrap();
5328            let ds = file.dataset("records").unwrap();
5329            // The on-disk element type is the compound we specified (size 12),
5330            // not the u8 carrier.
5331            match ds.datatype().unwrap() {
5332                DatatypeMessage::Compound { size, members } => {
5333                    assert_eq!(size, 12);
5334                    assert_eq!(members.len(), 2);
5335                    assert_eq!(members[0].name, "id");
5336                    assert_eq!(members[0].offset, 0);
5337                    assert_eq!(members[1].name, "val");
5338                    assert_eq!(members[1].offset, 4);
5339                }
5340                other => panic!("expected compound datatype, got {other:?}"),
5341            }
5342            assert_eq!(ds.read_raw_bytes().unwrap(), bytes);
5343        }
5344        std::fs::remove_file(&path).ok();
5345    }
5346
5347    #[test]
5348    fn builder_requires_shape() {
5349        let path = temp_path("no_shape");
5350        let file = H5File::create(&path).unwrap();
5351        let result = file.new_dataset::<u8>().create("data");
5352        assert!(result.is_err());
5353        std::fs::remove_file(&path).ok();
5354    }
5355
5356    // The last chunk along a *fixed* dimension covers more elements than the
5357    // extent has, so growing the dataspace to the chunk's far edge asks for
5358    // more than the declared maximum. Before the clamp the chunk was written
5359    // and then the call failed on that extend, leaving the bytes in the file
5360    // and the caller an error.
5361    #[test]
5362    fn a_partial_edge_chunk_does_not_grow_past_the_declared_maximum() {
5363        let path = temp_path("edge_chunk_extent");
5364        // Extensible array: dimension 1 is unlimited, dimension 0 is fixed at
5365        // 10 and not a multiple of the chunk's 4.
5366        let file = H5File::create(&path).unwrap();
5367        let ds = file
5368            .new_dataset::<i32>()
5369            .shape([10usize, 4])
5370            .max_shape(&[Some(10), None])
5371            .chunk(&[4, 4])
5372            .create("grid")
5373            .unwrap();
5374        let chunk: Vec<u8> = (0i32..16).flat_map(|v| v.to_le_bytes()).collect();
5375        // Chunk row 2 spans elements 8..12 of a dimension that stops at 10.
5376        ds.write_chunk_at(&[2, 0], &chunk).unwrap();
5377        assert_eq!(ds.shape(), vec![10, 4]);
5378        file.close().unwrap();
5379
5380        let file = H5File::open(&path).unwrap();
5381        let back = file.dataset("grid").unwrap().read_raw::<i32>().unwrap();
5382        assert_eq!(back.len(), 40);
5383        assert_eq!(&back[32..40], &[0, 1, 2, 3, 4, 5, 6, 7]);
5384        drop(file);
5385        std::fs::remove_file(&path).ok();
5386    }
5387
5388    // The cap is in `write_chunk_at_inner`, so it belongs to every chunk index
5389    // whose write reaches the extend below it — the v2 B-tree as much as the
5390    // extensible array. A rank-3 dataset with two unlimited dimensions gets
5391    // that index, and its third, fixed dimension is where the last chunk
5392    // overhangs. (The fixed array and the implicit index return before the
5393    // extend: their shape cannot grow at all. The version-1 B-tree does reach
5394    // it — `tests/legacy_append.rs` carries that case, which needs a classic
5395    // file.)
5396    #[test]
5397    fn the_edge_write_cap_holds_for_the_v2_btree_index() {
5398        let path = temp_path("edge_chunk_bt2");
5399        let file = H5File::create(&path).unwrap();
5400        let ds = file
5401            .new_dataset::<i32>()
5402            .shape([10usize, 4, 4])
5403            .max_shape(&[Some(10), None, None])
5404            .chunk(&[4, 4, 4])
5405            .create("cube")
5406            .unwrap();
5407        let chunk: Vec<u8> = (0i32..64).flat_map(|v| v.to_le_bytes()).collect();
5408        // Chunk plane 2 spans elements 8..12 of a dimension that stops at 10.
5409        ds.write_chunk_at(&[2, 0, 0], &chunk).unwrap();
5410        assert_eq!(ds.shape(), vec![10, 4, 4]);
5411        file.close().unwrap();
5412
5413        let file = H5File::open(&path).unwrap();
5414        let back = file.dataset("cube").unwrap().read_raw::<i32>().unwrap();
5415        assert_eq!(back.len(), 160);
5416        // Rows 8 and 9 of the written plane, 16 elements each.
5417        assert_eq!(&back[128..160], &(0i32..32).collect::<Vec<_>>()[..]);
5418        drop(file);
5419        std::fs::remove_file(&path).ok();
5420    }
5421
5422    // The cap guards a chunk-coordinate write, and neither an externally
5423    // stored nor a virtual dataset has chunk coordinates to guard: both are
5424    // contiguous storage classes, refused at build together with chunked
5425    // storage, and `write_chunk_at` refuses what is not chunked. So the path
5426    // the case above exercises cannot be entered for either — asserted here
5427    // rather than left to inspection, since both classes route their raw bytes
5428    // through the same writer as the chunk grid does.
5429    #[test]
5430    fn an_external_or_virtual_dataset_never_reaches_the_edge_write_cap() {
5431        use crate::Selection;
5432        let dir = std::env::temp_dir().join(format!(
5433            "rust_hdf5_edge_cap_{}_{}",
5434            std::process::id(),
5435            temp_path("x").file_name().unwrap().to_string_lossy()
5436        ));
5437        std::fs::create_dir_all(&dir).unwrap();
5438        let path = dir.join("edge_cap.h5");
5439        let file = H5File::create(&path).unwrap();
5440        let payload = dir.join("payload.raw");
5441
5442        // Chunked storage and these two are mutually exclusive at build.
5443        for (which, res) in [
5444            (
5445                "external",
5446                file.new_dataset::<i32>()
5447                    .shape([10usize])
5448                    .external(&[(payload.to_str().unwrap(), 0, 40)])
5449                    .chunk(&[4])
5450                    .create("a"),
5451            ),
5452            (
5453                "virtual",
5454                file.new_dataset::<i32>()
5455                    .shape([10usize])
5456                    .virtual_mapping(Selection::All, "src.h5", "src", Selection::All)
5457                    .chunk(&[4])
5458                    .create("b"),
5459            ),
5460        ] {
5461            match res {
5462                Ok(_) => panic!("a {which} dataset cannot also be chunked"),
5463                Err(e) => assert!(e.to_string().contains("chunked"), "{which}: {e}"),
5464            }
5465        }
5466
5467        // And the coordinate write itself is refused on both, with the extent
5468        // left exactly where it was.
5469        let ext = file
5470            .new_dataset::<i32>()
5471            .shape([10usize])
5472            .external(&[(payload.to_str().unwrap(), 0, 40)])
5473            .create("outside")
5474            .unwrap();
5475        let vds = file
5476            .new_dataset::<i32>()
5477            .shape([10usize])
5478            .virtual_mapping(Selection::All, "src.h5", "src", Selection::All)
5479            .create("elsewhere")
5480            .unwrap();
5481        let chunk: Vec<u8> = (0i32..4).flat_map(|v| v.to_le_bytes()).collect();
5482        for (which, ds) in [("external", &ext), ("virtual", &vds)] {
5483            let err = ds.write_chunk_at(&[2], &chunk).unwrap_err().to_string();
5484            assert!(err.contains("only for chunked datasets"), "{which}: {err}");
5485            let err = ds
5486                .write_chunk_raw_at(&[2], &chunk, 0)
5487                .unwrap_err()
5488                .to_string();
5489            assert!(err.contains("only for chunked datasets"), "{which}: {err}");
5490            assert_eq!(ds.shape(), vec![10], "{which}");
5491        }
5492        file.close().unwrap();
5493        std::fs::remove_dir_all(&dir).ok();
5494    }
5495
5496    #[test]
5497    fn write_raw_size_mismatch() {
5498        let path = temp_path("size_mismatch");
5499        let file = H5File::create(&path).unwrap();
5500        let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
5501        // Provide 3 elements instead of 4
5502        let result = ds.write_raw(&[1u8, 2, 3]);
5503        assert!(result.is_err());
5504        std::fs::remove_file(&path).ok();
5505    }
5506
5507    // A: a filter set without explicit chunk dimensions must auto-chunk (whole
5508    // dataset = one chunk) rather than silently drop the filter on the
5509    // contiguous path. write_raw then populates that single chunk.
5510    #[cfg(feature = "deflate")]
5511    #[test]
5512    fn filter_without_chunk_autochunks_and_roundtrips() {
5513        let path = temp_path("autochunk_filter");
5514        let data: Vec<i32> = (0..8).collect();
5515        {
5516            let file = H5File::create(&path).unwrap();
5517            let ds = file
5518                .new_dataset::<i32>()
5519                .deflate(6)
5520                .shape([8])
5521                .create("seq")
5522                .unwrap();
5523            ds.write_raw(&data).unwrap();
5524            file.close().unwrap();
5525        }
5526        {
5527            let file = H5File::open(&path).unwrap();
5528            let ds = file.dataset("seq").unwrap();
5529            // The filter forced chunked storage: a single whole-dataset chunk.
5530            assert!(
5531                ds.is_chunked(),
5532                "auto-chunk did not produce chunked storage"
5533            );
5534            assert_eq!(ds.chunk_dims(), Some(vec![8]));
5535            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
5536        }
5537        std::fs::remove_file(&path).ok();
5538    }
5539
5540    // B: write_raw on an explicitly chunked + compressed dataset scatters the
5541    // full row-major image across a multi-chunk grid, including edge chunks
5542    // (7/3 -> 3,3,1 along dim0; 5/2 -> 2,2,1 along dim1).
5543    #[cfg(feature = "deflate")]
5544    #[test]
5545    fn an_edge_chunk_pads_with_zero_not_the_chunk_written_before_it() {
5546        // 4x5 over a 2x3 chunk: the second chunk of each row covers only two
5547        // of its three columns, so a third of it is padding. The image write
5548        // stages every chunk of a shape like this through one reused buffer,
5549        // and the padding has to reach the file as zero rather than as
5550        // whatever the chunk before it left in that buffer.
5551        let path = temp_path("edge_chunk_padding");
5552        let data: Vec<i32> = (1..=20).collect(); // no zeros of its own
5553        {
5554            let file = H5File::create(&path).unwrap();
5555            let ds = file
5556                .new_dataset::<i32>()
5557                .shape([4, 5])
5558                .chunk(&[2, 3])
5559                .create("grid")
5560                .unwrap();
5561            ds.write_raw(&data).unwrap();
5562            file.close().unwrap();
5563        }
5564        {
5565            let file = H5File::open(&path).unwrap();
5566            let ds = file.dataset("grid").unwrap();
5567            let as_i32 = |bytes: Vec<u8>| -> Vec<i32> {
5568                bytes
5569                    .as_chunks::<4>()
5570                    .0
5571                    .iter()
5572                    .map(|b| i32::from_le_bytes(*b))
5573                    .collect()
5574            };
5575            // The chunk that precedes each edge chunk is full, so a leak would
5576            // show as its 3rd and 6th elements (3 and 8, then 13 and 18).
5577            assert_eq!(
5578                as_i32(ds.read_chunk_raw_at(&[0, 0]).unwrap().0),
5579                [1, 2, 3, 6, 7, 8]
5580            );
5581            assert_eq!(
5582                as_i32(ds.read_chunk_raw_at(&[0, 1]).unwrap().0),
5583                [4, 5, 0, 9, 10, 0]
5584            );
5585            assert_eq!(
5586                as_i32(ds.read_chunk_raw_at(&[1, 1]).unwrap().0),
5587                [14, 15, 0, 19, 20, 0]
5588            );
5589            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
5590        }
5591        std::fs::remove_file(&path).ok();
5592    }
5593
5594    #[test]
5595    #[cfg(feature = "deflate")]
5596    fn write_raw_multichunk_edge_roundtrips() {
5597        let path = temp_path("multichunk_edge");
5598        let data: Vec<i32> = (0..35).collect(); // 7 x 5 row-major
5599        {
5600            let file = H5File::create(&path).unwrap();
5601            let ds = file
5602                .new_dataset::<i32>()
5603                .shape([7, 5])
5604                .chunk(&[3, 2])
5605                .deflate(4)
5606                .create("grid")
5607                .unwrap();
5608            ds.write_raw(&data).unwrap();
5609            file.close().unwrap();
5610        }
5611        {
5612            let file = H5File::open(&path).unwrap();
5613            let ds = file.dataset("grid").unwrap();
5614            assert_eq!(ds.shape(), vec![7, 5]);
5615            assert_eq!(ds.chunk_dims(), Some(vec![3, 2]));
5616            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
5617        }
5618        std::fs::remove_file(&path).ok();
5619    }
5620
5621    // B: write_raw on an unfiltered chunked dataset (previously rejected with
5622    // "use write_chunk for chunked datasets") now gathers and round-trips.
5623    #[test]
5624    fn write_raw_unfiltered_chunked_roundtrips() {
5625        let path = temp_path("chunked_unfiltered");
5626        let data: Vec<f64> = (0..12).map(|i| i as f64 * 1.5).collect(); // 4 x 3
5627        {
5628            let file = H5File::create(&path).unwrap();
5629            let ds = file
5630                .new_dataset::<f64>()
5631                .shape([4, 3])
5632                .chunk(&[2, 2])
5633                .create("m")
5634                .unwrap();
5635            ds.write_raw(&data).unwrap();
5636            file.close().unwrap();
5637        }
5638        {
5639            let file = H5File::open(&path).unwrap();
5640            let ds = file.dataset("m").unwrap();
5641            assert_eq!(ds.chunk_dims(), Some(vec![2, 2]));
5642            assert_eq!(ds.read_raw::<f64>().unwrap(), data);
5643        }
5644        std::fs::remove_file(&path).ok();
5645    }
5646
5647    // write_raw on an extensible-array (unlimited first dim) compressed dataset
5648    // drives write_full_image_chunked's EA branch, which gathers chunks and
5649    // compresses them through the windowed batch path. Round-trips the full
5650    // image, including a partial edge chunk along the unlimited dimension.
5651    #[cfg(feature = "deflate")]
5652    #[test]
5653    fn write_raw_ea_compressed_roundtrips() {
5654        let path = temp_path("write_raw_ea_deflate");
5655        let data: Vec<i32> = (0..20).collect(); // 5 x 4 row-major
5656        {
5657            let file = H5File::create(&path).unwrap();
5658            let ds = file
5659                .new_dataset::<i32>()
5660                .shape([5, 4])
5661                .chunk(&[2, 4])
5662                .max_shape(&[None, Some(4)]) // unlimited dim 0 -> extensible array
5663                .deflate(5)
5664                .create("stream")
5665                .unwrap();
5666            ds.write_raw(&data).unwrap();
5667            file.close().unwrap();
5668        }
5669        {
5670            let file = H5File::open(&path).unwrap();
5671            let ds = file.dataset("stream").unwrap();
5672            assert_eq!(ds.shape(), vec![5, 4]);
5673            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
5674        }
5675        std::fs::remove_file(&path).ok();
5676    }
5677
5678    // 3D chunked Full read with partial-edge chunks in every dimension. This
5679    // drives copy_chunk_to_output's multi-dim run-memcpy path with two outer
5680    // dimensions, exercising the nested outer-coordinate carry and the
5681    // last-axis edge clamp (chunks hang off the high edge in all three axes).
5682    #[cfg(feature = "deflate")]
5683    #[test]
5684    fn read_full_3d_chunked_edge_roundtrips() {
5685        let path = temp_path("full_3d_chunked_edge");
5686        // shape 5x4x3, chunk 2x3x2 -> ceil gives 3x2x2 chunks; the last chunk
5687        // along each axis is partial (1, 1, and 1 element respectively).
5688        let total: usize = 5 * 4 * 3;
5689        let data: Vec<i32> = (0..total as i32).collect();
5690        {
5691            let file = H5File::create(&path).unwrap();
5692            let ds = file
5693                .new_dataset::<i32>()
5694                .shape([5, 4, 3])
5695                .chunk(&[2, 3, 2])
5696                .deflate(4)
5697                .create("vol")
5698                .unwrap();
5699            ds.write_raw(&data).unwrap();
5700            file.close().unwrap();
5701        }
5702        {
5703            let file = H5File::open(&path).unwrap();
5704            let ds = file.dataset("vol").unwrap();
5705            assert_eq!(ds.shape(), vec![5, 4, 3]);
5706            assert_eq!(ds.chunk_dims(), Some(vec![2, 3, 2]));
5707            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
5708        }
5709        std::fs::remove_file(&path).ok();
5710    }
5711
5712    #[test]
5713    fn roundtrip_u8_1d() {
5714        let path = temp_path("rt_u8_1d");
5715        let data: Vec<u8> = (0..10).collect();
5716
5717        {
5718            let file = H5File::create(&path).unwrap();
5719            let ds = file.new_dataset::<u8>().shape([10]).create("seq").unwrap();
5720            ds.write_raw(&data).unwrap();
5721            file.close().unwrap();
5722        }
5723
5724        {
5725            let file = H5File::open(&path).unwrap();
5726            let ds = file.dataset("seq").unwrap();
5727            assert_eq!(ds.shape(), vec![10]);
5728            let readback = ds.read_raw::<u8>().unwrap();
5729            assert_eq!(readback, data);
5730        }
5731
5732        std::fs::remove_file(&path).ok();
5733    }
5734
5735    #[test]
5736    fn roundtrip_i32_2d() {
5737        let path = temp_path("rt_i32_2d");
5738        let data: Vec<i32> = vec![-1, 0, 1, 2, 3, 4];
5739
5740        {
5741            let file = H5File::create(&path).unwrap();
5742            let ds = file
5743                .new_dataset::<i32>()
5744                .shape([2, 3])
5745                .create("matrix")
5746                .unwrap();
5747            ds.write_raw(&data).unwrap();
5748            file.close().unwrap();
5749        }
5750
5751        {
5752            let file = H5File::open(&path).unwrap();
5753            let ds = file.dataset("matrix").unwrap();
5754            assert_eq!(ds.shape(), vec![2, 3]);
5755            let readback = ds.read_raw::<i32>().unwrap();
5756            assert_eq!(readback, data);
5757        }
5758
5759        std::fs::remove_file(&path).ok();
5760    }
5761
5762    #[test]
5763    fn roundtrip_f64_3d() {
5764        let path = temp_path("rt_f64_3d");
5765        let data: Vec<f64> = (0..24).map(|i| i as f64 * 0.5).collect();
5766
5767        {
5768            let file = H5File::create(&path).unwrap();
5769            let ds = file
5770                .new_dataset::<f64>()
5771                .shape([2, 3, 4])
5772                .create("cube")
5773                .unwrap();
5774            ds.write_raw(&data).unwrap();
5775            file.close().unwrap();
5776        }
5777
5778        {
5779            let file = H5File::open(&path).unwrap();
5780            let ds = file.dataset("cube").unwrap();
5781            assert_eq!(ds.shape(), vec![2, 3, 4]);
5782            let readback = ds.read_raw::<f64>().unwrap();
5783            assert_eq!(readback, data);
5784        }
5785
5786        std::fs::remove_file(&path).ok();
5787    }
5788
5789    #[test]
5790    fn cannot_read_in_write_mode() {
5791        let path = temp_path("no_read_write");
5792        let file = H5File::create(&path).unwrap();
5793        let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
5794        ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
5795        let result = ds.read_raw::<u8>();
5796        assert!(result.is_err());
5797        std::fs::remove_file(&path).ok();
5798    }
5799
5800    #[test]
5801    fn cannot_write_in_read_mode() {
5802        let path = temp_path("no_write_read");
5803
5804        {
5805            let file = H5File::create(&path).unwrap();
5806            let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
5807            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
5808            file.close().unwrap();
5809        }
5810
5811        {
5812            let file = H5File::open(&path).unwrap();
5813            let ds = file.dataset("x").unwrap();
5814            let result = ds.write_raw(&[5u8, 6, 7, 8]);
5815            assert!(result.is_err());
5816        }
5817
5818        std::fs::remove_file(&path).ok();
5819    }
5820
5821    #[test]
5822    fn numeric_attr_roundtrip() {
5823        let path = temp_path("num_attr");
5824        {
5825            let file = H5File::create(&path).unwrap();
5826            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
5827            ds.write_raw(&[1.0f32; 4]).unwrap();
5828
5829            let a1 = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
5830            a1.write_numeric(&1.2345f64).unwrap();
5831
5832            let a2 = ds.new_attr::<i32>().shape(()).create("count").unwrap();
5833            a2.write_numeric(&42i32).unwrap();
5834
5835            file.close().unwrap();
5836        }
5837        {
5838            let file = H5File::open(&path).unwrap();
5839            let ds = file.dataset("data").unwrap();
5840
5841            let scale = ds.attr("scale").unwrap();
5842            let val: f64 = scale.read_numeric().unwrap();
5843            assert!((val - 1.2345).abs() < 1e-10);
5844
5845            let count = ds.attr("count").unwrap();
5846            let val: i32 = count.read_numeric().unwrap();
5847            assert_eq!(val, 42);
5848        }
5849        std::fs::remove_file(&path).ok();
5850    }
5851
5852    #[test]
5853    fn array_attr_roundtrip() {
5854        let path = temp_path("array_attr");
5855        let offsets = [10i32, -20, 30];
5856        {
5857            let file = H5File::create(&path).unwrap();
5858            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
5859            ds.write_raw(&[1.0f32; 4]).unwrap();
5860
5861            // 1-D int32 array attribute (NDArrayDimOffset-style).
5862            let a = ds
5863                .new_attr::<i32>()
5864                .shape([3])
5865                .create("dim_offset")
5866                .unwrap();
5867            a.write_array(&offsets).unwrap();
5868
5869            // Wrong element count is rejected.
5870            let bad = ds.new_attr::<i32>().shape([3]).create("bad").unwrap();
5871            assert!(bad.write_array(&[1i32, 2]).is_err());
5872
5873            file.close().unwrap();
5874        }
5875        {
5876            let file = H5File::open(&path).unwrap();
5877            let ds = file.dataset("data").unwrap();
5878            let a = ds.attr("dim_offset").unwrap();
5879            let raw = a.read_raw().unwrap();
5880            assert_eq!(raw.len(), 3 * 4);
5881            let got: Vec<i32> = raw
5882                .as_chunks::<4>()
5883                .0
5884                .iter()
5885                .map(|b| i32::from_le_bytes(*b))
5886                .collect();
5887            assert_eq!(got, offsets);
5888        }
5889        std::fs::remove_file(&path).ok();
5890    }
5891
5892    #[test]
5893    fn attr_datatype_exposes_class_and_sign() {
5894        // H5Attribute::datatype() must report the stored datatype class and
5895        // signedness so a generic attr->metadata mapper need not infer it from
5896        // the byte width (the HDF5-L1 adapter blocker this accessor unblocks).
5897        use crate::format::messages::datatype::DatatypeMessage;
5898
5899        let path = temp_path("attr_datatype");
5900        {
5901            let file = H5File::create(&path).unwrap();
5902            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
5903            ds.new_attr::<f64>()
5904                .shape(())
5905                .create("scale")
5906                .unwrap()
5907                .write_numeric(&1.5f64)
5908                .unwrap();
5909            ds.new_attr::<i32>()
5910                .shape(())
5911                .create("count")
5912                .unwrap()
5913                .write_numeric(&7i32)
5914                .unwrap();
5915            file.close().unwrap();
5916        }
5917        {
5918            let file = H5File::open(&path).unwrap();
5919            let ds = file.dataset("data").unwrap();
5920
5921            match ds.attr("scale").unwrap().datatype().unwrap() {
5922                DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 8),
5923                other => panic!("expected FloatingPoint for f64 attr, got {other:?}"),
5924            }
5925
5926            match ds.attr("count").unwrap().datatype().unwrap() {
5927                DatatypeMessage::FixedPoint { size, signed, .. } => {
5928                    assert_eq!(size, 4);
5929                    assert!(signed, "i32 attr must be signed");
5930                }
5931                other => panic!("expected FixedPoint for i32 attr, got {other:?}"),
5932            }
5933        }
5934        std::fs::remove_file(&path).ok();
5935    }
5936
5937    #[test]
5938    fn attr_datatype_in_write_mode_errors() {
5939        let path = temp_path("attr_datatype_write_mode");
5940        let file = H5File::create(&path).unwrap();
5941        let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
5942        let attr = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
5943        assert!(attr.datatype().is_err());
5944        std::fs::remove_file(&path).ok();
5945    }
5946
5947    #[test]
5948    fn cannot_create_dataset_in_read_mode() {
5949        let path = temp_path("no_create_read");
5950
5951        {
5952            let _file = H5File::create(&path).unwrap();
5953        }
5954
5955        {
5956            let file = H5File::open(&path).unwrap();
5957            let result = file.new_dataset::<u8>().shape([4]).create("x");
5958            assert!(result.is_err());
5959        }
5960
5961        std::fs::remove_file(&path).ok();
5962    }
5963
5964    #[test]
5965    fn shape_accessor() {
5966        let path = temp_path("shape_acc");
5967
5968        let file = H5File::create(&path).unwrap();
5969        let ds = file
5970            .new_dataset::<f32>()
5971            .shape([5, 10, 3])
5972            .create("tensor")
5973            .unwrap();
5974        assert_eq!(ds.shape(), vec![5, 10, 3]);
5975
5976        std::fs::remove_file(&path).ok();
5977    }
5978
5979    #[test]
5980    fn slice_roundtrip_2d() {
5981        let path = temp_path("slice_2d");
5982
5983        // Create a 4x5 dataset, write full, then read a slice
5984        let data: Vec<i32> = (0..20).collect();
5985        {
5986            let file = H5File::create(&path).unwrap();
5987            let ds = file
5988                .new_dataset::<i32>()
5989                .shape([4, 5])
5990                .create("mat")
5991                .unwrap();
5992            ds.write_raw(&data).unwrap();
5993            file.close().unwrap();
5994        }
5995        {
5996            let file = H5File::open(&path).unwrap();
5997            let ds = file.dataset("mat").unwrap();
5998            // Read rows 1..3, cols 2..4 (2x2 slice)
5999            let slice = ds.read_slice::<i32>(&[1, 2], &[2, 2]).unwrap();
6000            // Row 1: [5,6,7,8,9] -> cols 2..4 = [7,8]
6001            // Row 2: [10,11,12,13,14] -> cols 2..4 = [12,13]
6002            assert_eq!(slice, vec![7, 8, 12, 13]);
6003        }
6004
6005        std::fs::remove_file(&path).ok();
6006    }
6007
6008    // H2D zero-alloc reads. `read_raw_into` / `read_slice_into` fill a
6009    // caller-provided buffer and MUST produce byte-for-byte the same data as
6010    // their Vec-returning counterparts (`read_raw` / `read_slice`) on every
6011    // creatable layout, since both now share one buffer-filling core.
6012    fn assert_into_matches<T>(ds: &super::H5Dataset, starts: &[usize], counts: &[usize])
6013    where
6014        T: crate::types::H5Type + Copy + std::fmt::Debug + PartialEq + Default,
6015    {
6016        let n: usize = ds.shape().iter().product();
6017        let want_full = ds.read_raw::<T>().unwrap();
6018        let mut got_full = vec![T::default(); n];
6019        ds.read_raw_into::<T>(&mut got_full).unwrap();
6020        assert_eq!(got_full, want_full, "read_raw_into != read_raw");
6021
6022        let want_slice = ds.read_slice::<T>(starts, counts).unwrap();
6023        let sn: usize = counts.iter().product();
6024        let mut got_slice = vec![T::default(); sn];
6025        ds.read_slice_into::<T>(&mut got_slice, starts, counts)
6026            .unwrap();
6027        assert_eq!(got_slice, want_slice, "read_slice_into != read_slice");
6028    }
6029
6030    #[test]
6031    fn read_into_matches_vec_contiguous() {
6032        let path = temp_path("into_contig");
6033        let data: Vec<i32> = (0..20).collect(); // 4 x 5 contiguous
6034        {
6035            let file = H5File::create(&path).unwrap();
6036            let ds = file
6037                .new_dataset::<i32>()
6038                .shape([4, 5])
6039                .create("mat")
6040                .unwrap();
6041            ds.write_raw(&data).unwrap();
6042            file.close().unwrap();
6043        }
6044        {
6045            let file = H5File::open(&path).unwrap();
6046            let ds = file.dataset("mat").unwrap();
6047            assert_eq!(ds.chunk_dims(), None);
6048            assert_into_matches::<i32>(&ds, &[1, 2], &[2, 2]);
6049        }
6050        std::fs::remove_file(&path).ok();
6051    }
6052
6053    #[test]
6054    fn read_into_matches_vec_chunked_unfiltered() {
6055        let path = temp_path("into_chunk");
6056        let data: Vec<f64> = (0..35).map(|i| i as f64 * 1.5).collect(); // 7 x 5
6057        {
6058            let file = H5File::create(&path).unwrap();
6059            let ds = file
6060                .new_dataset::<f64>()
6061                .shape([7, 5])
6062                .chunk(&[3, 2]) // multi-chunk grid with edge chunks
6063                .create("grid")
6064                .unwrap();
6065            ds.write_raw(&data).unwrap();
6066            file.close().unwrap();
6067        }
6068        {
6069            let file = H5File::open(&path).unwrap();
6070            let ds = file.dataset("grid").unwrap();
6071            assert_eq!(ds.chunk_dims(), Some(vec![3, 2]));
6072            // Slice spans multiple chunks (rows 2..5, cols 1..4).
6073            assert_into_matches::<f64>(&ds, &[2, 1], &[3, 3]);
6074        }
6075        std::fs::remove_file(&path).ok();
6076    }
6077
6078    #[test]
6079    fn read_into_matches_vec_single_chunk() {
6080        let path = temp_path("into_single_chunk");
6081        let data: Vec<i32> = (0..12).collect(); // 3 x 4, one chunk covers all
6082        {
6083            let file = H5File::create(&path).unwrap();
6084            let ds = file
6085                .new_dataset::<i32>()
6086                .shape([3, 4])
6087                .chunk(&[3, 4]) // chunk == shape -> SingleChunk index
6088                .create("g")
6089                .unwrap();
6090            ds.write_raw(&data).unwrap();
6091            file.close().unwrap();
6092        }
6093        {
6094            let file = H5File::open(&path).unwrap();
6095            let ds = file.dataset("g").unwrap();
6096            assert_eq!(ds.chunk_dims(), Some(vec![3, 4]));
6097            assert_into_matches::<i32>(&ds, &[1, 1], &[2, 2]);
6098        }
6099        std::fs::remove_file(&path).ok();
6100    }
6101
6102    #[cfg(feature = "deflate")]
6103    #[test]
6104    fn read_into_matches_vec_chunked_deflate() {
6105        let path = temp_path("into_chunk_deflate");
6106        let data: Vec<i32> = (0..35).collect(); // 7 x 5
6107        {
6108            let file = H5File::create(&path).unwrap();
6109            let ds = file
6110                .new_dataset::<i32>()
6111                .shape([7, 5])
6112                .chunk(&[3, 2])
6113                .deflate(4)
6114                .create("grid")
6115                .unwrap();
6116            ds.write_raw(&data).unwrap();
6117            file.close().unwrap();
6118        }
6119        {
6120            let file = H5File::open(&path).unwrap();
6121            let ds = file.dataset("grid").unwrap();
6122            assert_eq!(ds.chunk_dims(), Some(vec![3, 2]));
6123            assert_into_matches::<i32>(&ds, &[2, 1], &[3, 3]);
6124        }
6125        std::fs::remove_file(&path).ok();
6126    }
6127
6128    #[test]
6129    fn read_into_wrong_buffer_size_rejected() {
6130        let path = temp_path("into_badlen");
6131        let data: Vec<i32> = (0..20).collect(); // 4 x 5
6132        {
6133            let file = H5File::create(&path).unwrap();
6134            let ds = file
6135                .new_dataset::<i32>()
6136                .shape([4, 5])
6137                .create("mat")
6138                .unwrap();
6139            ds.write_raw(&data).unwrap();
6140            file.close().unwrap();
6141        }
6142        {
6143            let file = H5File::open(&path).unwrap();
6144            let ds = file.dataset("mat").unwrap();
6145
6146            // Too small / too large full-read buffers are both rejected.
6147            let mut small = vec![0i32; 19];
6148            assert!(ds.read_raw_into::<i32>(&mut small).is_err());
6149            let mut large = vec![0i32; 21];
6150            assert!(ds.read_raw_into::<i32>(&mut large).is_err());
6151
6152            // Slice buffer must be exactly product(counts) = 4.
6153            let mut bad_slice = vec![0i32; 3];
6154            assert!(ds
6155                .read_slice_into::<i32>(&mut bad_slice, &[1, 2], &[2, 2])
6156                .is_err());
6157            // The correctly sized slice buffer succeeds.
6158            let mut ok_slice = vec![0i32; 4];
6159            assert!(ds
6160                .read_slice_into::<i32>(&mut ok_slice, &[1, 2], &[2, 2])
6161                .is_ok());
6162        }
6163        std::fs::remove_file(&path).ok();
6164    }
6165
6166    #[test]
6167    fn read_into_wrong_element_size_rejected() {
6168        let path = temp_path("into_badtype");
6169        let data: Vec<i32> = (0..20).collect(); // element size 4
6170        {
6171            let file = H5File::create(&path).unwrap();
6172            let ds = file
6173                .new_dataset::<i32>()
6174                .shape([4, 5])
6175                .create("mat")
6176                .unwrap();
6177            ds.write_raw(&data).unwrap();
6178            file.close().unwrap();
6179        }
6180        {
6181            let file = H5File::open(&path).unwrap();
6182            let ds = file.dataset("mat").unwrap();
6183            // u8 (size 1) and i64 (size 8) mismatch the dataset's 4-byte
6184            // element size -> TypeMismatch, even with a "correctly sized" Vec.
6185            let mut as_u8 = vec![0u8; 20];
6186            assert!(matches!(
6187                ds.read_raw_into::<u8>(&mut as_u8),
6188                Err(crate::Hdf5Error::TypeMismatch(_))
6189            ));
6190            let mut as_i64 = vec![0i64; 20];
6191            assert!(matches!(
6192                ds.read_slice_into::<i64>(&mut as_i64, &[0, 0], &[4, 5]),
6193                Err(crate::Hdf5Error::TypeMismatch(_))
6194            ));
6195        }
6196        std::fs::remove_file(&path).ok();
6197    }
6198
6199    #[test]
6200    fn write_slice_2d() {
6201        let path = temp_path("write_slice_2d");
6202
6203        {
6204            let file = H5File::create(&path).unwrap();
6205            let ds = file
6206                .new_dataset::<f32>()
6207                .shape([3, 4])
6208                .create("data")
6209                .unwrap();
6210            ds.write_raw(&[0.0f32; 12]).unwrap();
6211            // Overwrite a 2x2 sub-region
6212            ds.write_slice(&[1, 1], &[2, 2], &[10.0f32, 20.0, 30.0, 40.0])
6213                .unwrap();
6214            file.close().unwrap();
6215        }
6216        {
6217            let file = H5File::open(&path).unwrap();
6218            let ds = file.dataset("data").unwrap();
6219            let full = ds.read_raw::<f32>().unwrap();
6220            // Row 0: [0,0,0,0]
6221            // Row 1: [0,10,20,0]
6222            // Row 2: [0,30,40,0]
6223            assert_eq!(
6224                full,
6225                vec![0.0, 0.0, 0.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 30.0, 40.0, 0.0,]
6226            );
6227        }
6228
6229        std::fs::remove_file(&path).ok();
6230    }
6231
6232    /// One 2x4 i32 chunk whose every element is `v`.
6233    fn chunk_of(v: i32) -> Vec<u8> {
6234        (0..8).flat_map(|_| v.to_le_bytes()).collect()
6235    }
6236
6237    /// Write chunk (0,0) `rewrites` times — each time with a different value,
6238    /// so no write can be skipped — and return the closed file's size along
6239    /// with what the chunk reads back as.
6240    fn rewrite_chunk(
6241        tag: &str,
6242        rewrites: i32,
6243        build: impl Fn(&H5File) -> crate::H5Dataset,
6244    ) -> (u64, i32) {
6245        let path = temp_path(tag);
6246        {
6247            let file = H5File::create(&path).unwrap();
6248            let ds = build(&file);
6249            for v in 1..=rewrites {
6250                ds.write_chunk_at(&[0, 0], &chunk_of(v)).unwrap();
6251            }
6252            file.close().unwrap();
6253        }
6254        let size = std::fs::metadata(&path).unwrap().len();
6255        let first = {
6256            let file = H5File::open(&path).unwrap();
6257            file.dataset("d").unwrap().read_raw::<i32>().unwrap()[0]
6258        };
6259        std::fs::remove_file(&path).ok();
6260        (size, first)
6261    }
6262
6263    // An unfiltered chunk's stored size is fixed by the chunk shape, so
6264    // rewriting it must overwrite the block it already occupies rather than
6265    // abandoning it and appending a new one (libhdf5 H5D__chunk_flush_entry
6266    // leaves must_alloc false for exactly this case). The file must therefore
6267    // be byte-identical in size no matter how many times the chunk is written.
6268    #[test]
6269    fn rewriting_an_unfiltered_extensible_array_chunk_stays_in_place() {
6270        let build = |f: &H5File| {
6271            f.new_dataset::<i32>()
6272                .shape([2, 4])
6273                .chunk(&[2, 4])
6274                .max_shape(&[None, Some(4)])
6275                .create("d")
6276                .unwrap()
6277        };
6278        let (once, _) = rewrite_chunk("rewrite_ea_1", 1, build);
6279        let (many, last) = rewrite_chunk("rewrite_ea_8", 8, build);
6280        assert_eq!(many, once, "8 rewrites grew the file past a single write");
6281        assert_eq!(last, 8, "the last write must be the one that survives");
6282    }
6283
6284    #[test]
6285    fn rewriting_an_unfiltered_fixed_array_chunk_stays_in_place() {
6286        let build = |f: &H5File| {
6287            f.new_dataset::<i32>()
6288                .shape([2, 4])
6289                .chunk(&[2, 4])
6290                .create("d")
6291                .unwrap()
6292        };
6293        let (once, _) = rewrite_chunk("rewrite_fa_1", 1, build);
6294        let (many, last) = rewrite_chunk("rewrite_fa_8", 8, build);
6295        assert_eq!(many, once, "8 rewrites grew the file past a single write");
6296        assert_eq!(last, 8);
6297    }
6298
6299    #[test]
6300    fn rewriting_an_unfiltered_btree_v2_chunk_stays_in_place() {
6301        let build = |f: &H5File| {
6302            f.new_dataset::<i32>()
6303                .shape([2, 4])
6304                .chunk(&[2, 4])
6305                .max_shape(&[None, None])
6306                .create("d")
6307                .unwrap()
6308        };
6309        let (once, _) = rewrite_chunk("rewrite_bt2_1", 1, build);
6310        let (many, last) = rewrite_chunk("rewrite_bt2_8", 8, build);
6311        assert_eq!(many, once, "8 rewrites grew the file past a single write");
6312        assert_eq!(last, 8);
6313    }
6314
6315    // A flush re-serializes the whole v2 B-tree over the dataset's node-block
6316    // pool. Every node is the same size, so the blocks already on disk are
6317    // reused and repeated flushes cost nothing; sizing the root to its record
6318    // count instead would relocate it each time and orphan the block it left.
6319    #[test]
6320    fn repeated_flushes_do_not_grow_a_btree_v2_index() {
6321        let flush_n = |label: &str, flushes: usize| -> u64 {
6322            let path = temp_path(label);
6323            {
6324                let file = H5File::create(&path).unwrap();
6325                let ds = file
6326                    .new_dataset::<i32>()
6327                    .shape([2, 4])
6328                    .chunk(&[2, 4])
6329                    .max_shape(&[None, None])
6330                    .create("d")
6331                    .unwrap();
6332                let bytes: Vec<u8> = (0..8i32).flat_map(|v| v.to_le_bytes()).collect();
6333                ds.write_chunk_at(&[0, 0], &bytes).unwrap();
6334                for _ in 0..flushes {
6335                    ds.flush().unwrap();
6336                }
6337                file.close().unwrap();
6338            }
6339            let size = std::fs::metadata(&path).unwrap().len();
6340            // The data must survive every rewrite of the index.
6341            {
6342                let file = H5File::open(&path).unwrap();
6343                let ds = file.dataset("d").unwrap();
6344                assert_eq!(ds.read_raw::<i32>().unwrap(), (0..8).collect::<Vec<i32>>());
6345            }
6346            std::fs::remove_file(&path).ok();
6347            size
6348        };
6349        assert_eq!(
6350            flush_n("bt2_flush_8", 8),
6351            flush_n("bt2_flush_1", 1),
6352            "8 index flushes grew the file past a single one"
6353        );
6354    }
6355
6356    // A filtered chunk whose compressed size changes cannot stay put, so it
6357    // moves and releases its old block (libhdf5 H5D__chunk_file_alloc calls
6358    // H5MF_xfree). Alternating between two payloads of different compressed
6359    // size must therefore keep reusing the same two blocks instead of
6360    // appending a fresh one each time.
6361    #[cfg(feature = "deflate")]
6362    #[test]
6363    fn rewriting_a_filtered_chunk_recycles_the_released_block() {
6364        // All-equal elements deflate to far fewer bytes than a varied payload,
6365        // so the two writes below land at different stored sizes.
6366        let flat: Vec<u8> = (0..8).flat_map(|_| 7i32.to_le_bytes()).collect();
6367        let varied: Vec<u8> = (0..8i32)
6368            .flat_map(|i| i.wrapping_mul(0x5bd1_e995).to_le_bytes())
6369            .collect();
6370
6371        let sizes: Vec<u64> = [1usize, 8]
6372            .iter()
6373            .map(|&rounds| {
6374                let path = temp_path(&format!("rewrite_filtered_{rounds}"));
6375                {
6376                    let file = H5File::create(&path).unwrap();
6377                    let ds = file
6378                        .new_dataset::<i32>()
6379                        .shape([2, 4])
6380                        .chunk(&[2, 4])
6381                        .max_shape(&[None, Some(4)])
6382                        .deflate(6)
6383                        .create("d")
6384                        .unwrap();
6385                    for _ in 0..rounds {
6386                        ds.write_chunk_at(&[0, 0], &flat).unwrap();
6387                        ds.write_chunk_at(&[0, 0], &varied).unwrap();
6388                    }
6389                    file.close().unwrap();
6390                }
6391                let size = std::fs::metadata(&path).unwrap().len();
6392                {
6393                    let file = H5File::open(&path).unwrap();
6394                    let got = file.dataset("d").unwrap().read_raw::<i32>().unwrap();
6395                    let want: Vec<i32> = (0..8i32).map(|i| i.wrapping_mul(0x5bd1_e995)).collect();
6396                    assert_eq!(got, want, "the last write must survive the round trip");
6397                }
6398                std::fs::remove_file(&path).ok();
6399                size
6400            })
6401            .collect();
6402
6403        assert_eq!(
6404            sizes[1], sizes[0],
6405            "8 alternating rewrites grew the file past a single pair"
6406        );
6407    }
6408
6409    #[test]
6410    fn write_slice_out_of_bounds_rejected() {
6411        let path = temp_path("write_slice_oob");
6412        let file = H5File::create(&path).unwrap();
6413        let ds = file.new_dataset::<i32>().shape([4]).create("d").unwrap();
6414        ds.write_raw(&[0i32; 4]).unwrap();
6415        // start 2 + count 6 = 8 > extent 4 -> must error, not corrupt.
6416        assert!(ds.write_slice(&[2], &[6], &[9i32; 6]).is_err());
6417        // An in-bounds slice still works.
6418        assert!(ds.write_slice(&[1], &[2], &[7i32, 8]).is_ok());
6419        std::fs::remove_file(&path).ok();
6420    }
6421
6422    #[test]
6423    fn duplicate_dataset_name_rejected() {
6424        let path = temp_path("dup_name");
6425        let file = H5File::create(&path).unwrap();
6426        let _ = file.new_dataset::<i32>().shape([2]).create("d").unwrap();
6427        assert!(file.new_dataset::<i32>().shape([2]).create("d").is_err());
6428        std::fs::remove_file(&path).ok();
6429    }
6430
6431    #[test]
6432    fn extend_cannot_shrink() {
6433        let path = temp_path("extend_shrink");
6434        let file = H5File::create(&path).unwrap();
6435        let ds = file
6436            .new_dataset::<i32>()
6437            .shape([0])
6438            .chunk(&[2])
6439            .max_shape(&[None])
6440            .create("d")
6441            .unwrap();
6442        ds.append(&[1i32, 2, 3, 4]).unwrap();
6443        // Shrinking below the written extent must be rejected.
6444        assert!(ds.extend(&[2]).is_err());
6445        // Growing is fine.
6446        assert!(ds.extend(&[6]).is_ok());
6447        std::fs::remove_file(&path).ok();
6448    }
6449
6450    #[test]
6451    fn attr_read_roundtrip() {
6452        use crate::types::VarLenUnicode;
6453        let path = temp_path("attr_read");
6454
6455        {
6456            let file = H5File::create(&path).unwrap();
6457            let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
6458            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
6459            let a1 = ds
6460                .new_attr::<VarLenUnicode>()
6461                .shape(())
6462                .create("units")
6463                .unwrap();
6464            a1.write_string("meters").unwrap();
6465            let a2 = ds
6466                .new_attr::<VarLenUnicode>()
6467                .shape(())
6468                .create("desc")
6469                .unwrap();
6470            a2.write_string("test data").unwrap();
6471            file.close().unwrap();
6472        }
6473        {
6474            let file = H5File::open(&path).unwrap();
6475            let ds = file.dataset("data").unwrap();
6476
6477            let names = ds.attr_names().unwrap();
6478            assert!(names.contains(&"units".to_string()));
6479            assert!(names.contains(&"desc".to_string()));
6480
6481            let units = ds.attr("units").unwrap();
6482            assert_eq!(units.read_string().unwrap(), "meters");
6483
6484            let desc = ds.attr("desc").unwrap();
6485            assert_eq!(desc.read_string().unwrap(), "test data");
6486        }
6487
6488        std::fs::remove_file(&path).ok();
6489    }
6490
6491    #[test]
6492    fn type_mismatch_element_size() {
6493        let path = temp_path("type_mismatch");
6494
6495        {
6496            let file = H5File::create(&path).unwrap();
6497            let ds = file.new_dataset::<f64>().shape([4]).create("data").unwrap();
6498            ds.write_raw(&[1.0f64, 2.0, 3.0, 4.0]).unwrap();
6499            file.close().unwrap();
6500        }
6501
6502        {
6503            let file = H5File::open(&path).unwrap();
6504            let ds = file.dataset("data").unwrap();
6505            // Try to read as u8 (element_size = 1) from a f64 dataset (element_size = 8)
6506            let result = ds.read_raw::<u8>();
6507            assert!(result.is_err());
6508        }
6509
6510        std::fs::remove_file(&path).ok();
6511    }
6512
6513    #[test]
6514    fn dataset_survives_file_move() {
6515        let path = temp_path("ds_survives");
6516
6517        let ds = {
6518            let file = H5File::create(&path).unwrap();
6519            file.new_dataset::<u8>().shape([4]).create("x").unwrap()
6520        };
6521        // file is dropped here, but ds still holds Rc to the inner state
6522        ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
6523        // The writer will finalize on drop of the last Rc
6524
6525        std::fs::remove_file(&path).ok();
6526    }
6527
6528    #[test]
6529    fn new_attr_scalar_string() {
6530        use crate::types::VarLenUnicode;
6531
6532        let path = temp_path("attr_scalar_string");
6533        {
6534            let file = H5File::create(&path).unwrap();
6535            let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
6536            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
6537
6538            let attr = ds
6539                .new_attr::<VarLenUnicode>()
6540                .shape(())
6541                .create("name")
6542                .unwrap();
6543            attr.write_scalar(&VarLenUnicode("test_value".to_string()))
6544                .unwrap();
6545
6546            file.close().unwrap();
6547        }
6548
6549        // Verify the file is still valid and readable
6550        {
6551            use crate::format::messages::datatype::DatatypeMessage;
6552            let file = H5File::open(&path).unwrap();
6553            let ds = file.dataset("data").unwrap();
6554            assert_eq!(ds.shape(), vec![4]);
6555            let readback = ds.read_raw::<u8>().unwrap();
6556            assert_eq!(readback, vec![1u8, 2, 3, 4]);
6557
6558            // The string attribute is stored as a true variable-length string
6559            // (not fixed-length) and round-trips its value.
6560            let attr = ds.attr("name").unwrap();
6561            assert!(
6562                matches!(
6563                    attr.datatype().unwrap(),
6564                    DatatypeMessage::VarLenString { .. }
6565                ),
6566                "string attribute should have a variable-length string datatype"
6567            );
6568            assert_eq!(attr.read_string().unwrap(), "test_value");
6569        }
6570
6571        std::fs::remove_file(&path).ok();
6572    }
6573
6574    #[test]
6575    fn all_numeric_types_roundtrip() {
6576        let path = temp_path("all_types");
6577
6578        {
6579            let file = H5File::create(&path).unwrap();
6580
6581            let ds = file.new_dataset::<u8>().shape([2]).create("u8").unwrap();
6582            ds.write_raw(&[1u8, 2]).unwrap();
6583
6584            let ds = file.new_dataset::<i8>().shape([2]).create("i8").unwrap();
6585            ds.write_raw(&[-1i8, 1]).unwrap();
6586
6587            let ds = file.new_dataset::<u16>().shape([2]).create("u16").unwrap();
6588            ds.write_raw(&[100u16, 200]).unwrap();
6589
6590            let ds = file.new_dataset::<i16>().shape([2]).create("i16").unwrap();
6591            ds.write_raw(&[-100i16, 100]).unwrap();
6592
6593            let ds = file.new_dataset::<u32>().shape([2]).create("u32").unwrap();
6594            ds.write_raw(&[1000u32, 2000]).unwrap();
6595
6596            let ds = file.new_dataset::<i32>().shape([2]).create("i32").unwrap();
6597            ds.write_raw(&[-1000i32, 1000]).unwrap();
6598
6599            let ds = file.new_dataset::<u64>().shape([2]).create("u64").unwrap();
6600            ds.write_raw(&[10000u64, 20000]).unwrap();
6601
6602            let ds = file.new_dataset::<i64>().shape([2]).create("i64").unwrap();
6603            ds.write_raw(&[-10000i64, 10000]).unwrap();
6604
6605            let ds = file.new_dataset::<f32>().shape([2]).create("f32").unwrap();
6606            ds.write_raw(&[1.5f32, 2.5]).unwrap();
6607
6608            let ds = file.new_dataset::<f64>().shape([2]).create("f64").unwrap();
6609            ds.write_raw(&[1.23456f64, 7.89012]).unwrap();
6610
6611            file.close().unwrap();
6612        }
6613
6614        {
6615            let file = H5File::open(&path).unwrap();
6616
6617            assert_eq!(
6618                file.dataset("u8").unwrap().read_raw::<u8>().unwrap(),
6619                vec![1u8, 2]
6620            );
6621            assert_eq!(
6622                file.dataset("i8").unwrap().read_raw::<i8>().unwrap(),
6623                vec![-1i8, 1]
6624            );
6625            assert_eq!(
6626                file.dataset("u16").unwrap().read_raw::<u16>().unwrap(),
6627                vec![100u16, 200]
6628            );
6629            assert_eq!(
6630                file.dataset("i16").unwrap().read_raw::<i16>().unwrap(),
6631                vec![-100i16, 100]
6632            );
6633            assert_eq!(
6634                file.dataset("u32").unwrap().read_raw::<u32>().unwrap(),
6635                vec![1000u32, 2000]
6636            );
6637            assert_eq!(
6638                file.dataset("i32").unwrap().read_raw::<i32>().unwrap(),
6639                vec![-1000i32, 1000]
6640            );
6641            assert_eq!(
6642                file.dataset("u64").unwrap().read_raw::<u64>().unwrap(),
6643                vec![10000u64, 20000]
6644            );
6645            assert_eq!(
6646                file.dataset("i64").unwrap().read_raw::<i64>().unwrap(),
6647                vec![-10000i64, 10000]
6648            );
6649            assert_eq!(
6650                file.dataset("f32").unwrap().read_raw::<f32>().unwrap(),
6651                vec![1.5f32, 2.5]
6652            );
6653            assert_eq!(
6654                file.dataset("f64").unwrap().read_raw::<f64>().unwrap(),
6655                vec![1.23456f64, 7.89012]
6656            );
6657        }
6658
6659        std::fs::remove_file(&path).ok();
6660    }
6661
6662    #[test]
6663    fn append_chunked_roundtrip() {
6664        let path = temp_path("append_chunked");
6665
6666        {
6667            let file = H5File::create(&path).unwrap();
6668            let ds = file
6669                .new_dataset::<f64>()
6670                .shape([0, 3])
6671                .chunk(&[1, 3])
6672                .max_shape(&[None, Some(3)])
6673                .create("data")
6674                .unwrap();
6675
6676            // Append one frame
6677            ds.append(&[1.0f64, 2.0, 3.0]).unwrap();
6678            // Append two frames at once
6679            ds.append(&[4.0f64, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap();
6680
6681            file.close().unwrap();
6682        }
6683
6684        {
6685            let file = H5File::open(&path).unwrap();
6686            let ds = file.dataset("data").unwrap();
6687            assert_eq!(ds.shape(), vec![3, 3]);
6688            let all = ds.read_raw::<f64>().unwrap();
6689            assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
6690        }
6691
6692        std::fs::remove_file(&path).ok();
6693    }
6694
6695    #[test]
6696    fn append_1d_chunked() {
6697        let path = temp_path("append_1d");
6698
6699        {
6700            let file = H5File::create(&path).unwrap();
6701            let ds = file
6702                .new_dataset::<i32>()
6703                .shape([0])
6704                .chunk(&[4])
6705                .max_shape(&[None])
6706                .create("values")
6707                .unwrap();
6708
6709            ds.append(&[10i32, 20, 30]).unwrap(); // partial chunk
6710            ds.append(&[40i32]).unwrap(); // fills chunk boundary
6711            ds.append(&[50i32, 60, 70, 80]).unwrap(); // full chunk
6712
6713            file.close().unwrap();
6714        }
6715
6716        {
6717            let file = H5File::open(&path).unwrap();
6718            let ds = file.dataset("values").unwrap();
6719            assert_eq!(ds.shape(), vec![8]);
6720            let all = ds.read_raw::<i32>().unwrap();
6721            assert_eq!(all, vec![10, 20, 30, 40, 50, 60, 70, 80]);
6722        }
6723
6724        std::fs::remove_file(&path).ok();
6725    }
6726
6727    #[test]
6728    fn append_partial_chunk_flushed_on_close() {
6729        let path = temp_path("append_partial_close");
6730
6731        {
6732            let file = H5File::create(&path).unwrap();
6733            let ds = file
6734                .new_dataset::<f64>()
6735                .shape([0])
6736                .chunk(&[4])
6737                .max_shape(&[None])
6738                .create("vals")
6739                .unwrap();
6740
6741            // Append 5 elements: chunk 0 = full [1,2,3,4], chunk 1 = partial [5,0,0,0]
6742            ds.append(&[1.0f64, 2.0, 3.0, 4.0, 5.0]).unwrap();
6743            file.close().unwrap();
6744        }
6745
6746        {
6747            let file = H5File::open(&path).unwrap();
6748            let ds = file.dataset("vals").unwrap();
6749            assert_eq!(ds.shape(), vec![5]);
6750            let all = ds.read_raw::<f64>().unwrap();
6751            // The full dataset is 2 chunks * 4 = 8 elements; shape says 5
6752            // read_raw reads total shape elements
6753            assert_eq!(all.len(), 5);
6754            assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
6755        }
6756
6757        std::fs::remove_file(&path).ok();
6758    }
6759
6760    /// An append that leaves its chunk partial is buffered until close, and
6761    /// the flush has to keep the frames that chunk already holds. It built a
6762    /// fresh fill-value chunk around the buffered frame instead, so reopening
6763    /// a file and appending one row erased every earlier row of that chunk
6764    /// (issue #3). Four sessions: the second lands beside an existing row, the
6765    /// third closes chunk 0 and opens chunk 1, the fourth lands beside the row
6766    /// the third left in chunk 1.
6767    #[test]
6768    fn append_after_reopen_keeps_the_partial_chunk_it_lands_in() {
6769        let path = temp_path("append_reopen_partial");
6770
6771        {
6772            let file = H5File::create(&path).unwrap();
6773            let ds = file
6774                .new_dataset::<i32>()
6775                .shape([0, 3])
6776                .chunk(&[4, 3])
6777                .max_shape(&[None, Some(3)])
6778                .create("values")
6779                .unwrap();
6780            ds.append(&[1, 2, 3]).unwrap();
6781            file.close().unwrap();
6782        }
6783        for rows in [
6784            vec![4, 5, 6],
6785            vec![7, 8, 9, 10, 11, 12, 13, 14, 15],
6786            vec![16, 17, 18],
6787        ] {
6788            let file = H5File::open_rw(&path).unwrap();
6789            file.dataset_writer("values")
6790                .unwrap()
6791                .append(&rows)
6792                .unwrap();
6793            file.close().unwrap();
6794        }
6795
6796        let file = H5File::open(&path).unwrap();
6797        let ds = file.dataset("values").unwrap();
6798        assert_eq!(ds.shape(), vec![6, 3]);
6799        assert_eq!(
6800            ds.read_raw::<i32>().unwrap(),
6801            (1..=18).collect::<Vec<i32>>()
6802        );
6803        std::fs::remove_file(&path).ok();
6804    }
6805
6806    #[cfg(feature = "deflate")]
6807    #[test]
6808    fn vlen_append_after_reopen_filtered() {
6809        // Reopen + append into a partially-written *compressed* vlen chunk
6810        // (index-block chunk). Exercises filtered-index-block reconstruction
6811        // in open_append plus filtered read-modify-write.
6812        let path = temp_path("vlen_reopen_filtered");
6813        {
6814            let file = H5File::create(&path).unwrap();
6815            file.create_appendable_vlen_dataset(
6816                "strs",
6817                4,
6818                Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
6819            )
6820            .unwrap();
6821            file.append_vlen_strings("strs", &["alpha", "beta", "gamma"])
6822                .unwrap();
6823            file.close().unwrap();
6824        }
6825        {
6826            let file = H5File::open_rw(&path).unwrap();
6827            file.append_vlen_strings("strs", &["delta"]).unwrap();
6828            file.close().unwrap();
6829        }
6830        {
6831            let file = H5File::open(&path).unwrap();
6832            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
6833            assert_eq!(
6834                got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
6835                vec!["alpha", "beta", "gamma", "delta"]
6836            );
6837        }
6838        std::fs::remove_file(&path).ok();
6839    }
6840
6841    #[test]
6842    fn vlen_append_after_reopen_data_block() {
6843        // Reopen + append into a partial chunk that lives in an extensible-
6844        // array *data block* (chunk index >= idx_blk_elmts). Exercises
6845        // data-block resolution in read_chunk_if_present and write_chunk.
6846        let path = temp_path("vlen_reopen_datablk");
6847        let labels: Vec<String> = (0..9).map(|i| format!("s{i}")).collect();
6848        {
6849            let file = H5File::create(&path).unwrap();
6850            file.create_appendable_vlen_dataset("strs", 2, None)
6851                .unwrap();
6852            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
6853            file.append_vlen_strings("strs", &refs).unwrap();
6854            file.close().unwrap();
6855        }
6856        {
6857            let file = H5File::open_rw(&path).unwrap();
6858            file.append_vlen_strings("strs", &["s9"]).unwrap();
6859            file.close().unwrap();
6860        }
6861        {
6862            let file = H5File::open(&path).unwrap();
6863            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
6864            let want: Vec<String> = (0..10).map(|i| format!("s{i}")).collect();
6865            assert_eq!(got, want);
6866        }
6867        std::fs::remove_file(&path).ok();
6868    }
6869
6870    #[test]
6871    fn vlen_append_after_reopen_super_block() {
6872        // Reopen + append into a partial chunk whose index falls in an
6873        // extensible-array *super block* (chunk index 244 with the default
6874        // EA geometry: idx_blk_elmts=4, data_blk_min_elmts=16,
6875        // sup_blk_min_data_ptrs=4 -> chunks 0..=243 are reached via the
6876        // index block or its direct data blocks, so chunk 244 is reached
6877        // via a super block read from disk). Exercises the ViaSblk branch
6878        // of read_chunk_if_present.
6879        let path = temp_path("vlen_reopen_super");
6880        // 489 strings, chunk size 2 -> chunk 244 holds one string only
6881        // (partially filled) and is flushed to disk on close.
6882        let labels: Vec<String> = (0..489).map(|i| format!("v{i}")).collect();
6883        {
6884            let file = H5File::create(&path).unwrap();
6885            file.create_appendable_vlen_dataset("strs", 2, None)
6886                .unwrap();
6887            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
6888            file.append_vlen_strings("strs", &refs).unwrap();
6889            file.close().unwrap();
6890        }
6891        {
6892            let file = H5File::open_rw(&path).unwrap();
6893            file.append_vlen_strings("strs", &["v489"]).unwrap();
6894            file.close().unwrap();
6895        }
6896        {
6897            let file = H5File::open(&path).unwrap();
6898            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
6899            let want: Vec<String> = (0..490).map(|i| format!("v{i}")).collect();
6900            assert_eq!(got, want);
6901        }
6902        std::fs::remove_file(&path).ok();
6903    }
6904
6905    #[cfg(feature = "deflate")]
6906    #[test]
6907    fn vlen_append_after_reopen_filtered_data_block() {
6908        // The hardest path: compressed + chunk in a data block + partial
6909        // read-modify-write across a reopen.
6910        let path = temp_path("vlen_reopen_filt_datablk");
6911        let labels: Vec<String> = (0..9).map(|i| format!("item{i:02}")).collect();
6912        {
6913            let file = H5File::create(&path).unwrap();
6914            file.create_appendable_vlen_dataset(
6915                "strs",
6916                2,
6917                Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
6918            )
6919            .unwrap();
6920            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
6921            file.append_vlen_strings("strs", &refs).unwrap();
6922            file.close().unwrap();
6923        }
6924        {
6925            let file = H5File::open_rw(&path).unwrap();
6926            file.append_vlen_strings("strs", &["item09"]).unwrap();
6927            file.close().unwrap();
6928        }
6929        {
6930            let file = H5File::open(&path).unwrap();
6931            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
6932            let want: Vec<String> = (0..10).map(|i| format!("item{i:02}")).collect();
6933            assert_eq!(got, want);
6934        }
6935        std::fs::remove_file(&path).ok();
6936    }
6937
6938    #[test]
6939    fn group_nx_class_attribute_roundtrip() {
6940        // Non-root groups carry attributes (NeXus `NX_class`) in their
6941        // own object header, and the reader reads them back by path.
6942        let path = temp_path("group_nx_class");
6943        {
6944            let file = H5File::create(&path).unwrap();
6945            let entry = file.create_group("entry").unwrap();
6946            entry.set_attr_string("NX_class", "NXentry").unwrap();
6947            let det = entry.create_group("detector").unwrap();
6948            det.set_attr_string("NX_class", "NXdetector").unwrap();
6949            det.set_attr_numeric("frame_count", &7i32).unwrap();
6950            det.new_dataset::<f32>()
6951                .shape([4])
6952                .create("data")
6953                .unwrap()
6954                .write_raw(&[1.0f32; 4])
6955                .unwrap();
6956            file.close().unwrap();
6957        }
6958        {
6959            let file = H5File::open(&path).unwrap();
6960            let entry = file.root_group().group("entry").unwrap();
6961            assert_eq!(entry.attr_string("NX_class").unwrap(), "NXentry");
6962            let det = entry.group("detector").unwrap();
6963            assert_eq!(det.attr_string("NX_class").unwrap(), "NXdetector");
6964            let names = det.attr_names().unwrap();
6965            assert!(names.contains(&"NX_class".to_string()));
6966            assert!(names.contains(&"frame_count".to_string()));
6967        }
6968        std::fs::remove_file(&path).ok();
6969    }
6970
6971    #[test]
6972    fn ea_super_block_roundtrip() {
6973        // 2000 chunks span several extensible-array super blocks. Before
6974        // super-block support the writer errored at chunk index 228.
6975        let path = temp_path("ea_super_rt");
6976        {
6977            let file = H5File::create(&path).unwrap();
6978            let ds = file
6979                .new_dataset::<i32>()
6980                .shape([0])
6981                .chunk(&[1])
6982                .max_shape(&[None])
6983                .create("v")
6984                .unwrap();
6985            ds.append(&(0..2000).collect::<Vec<i32>>()).unwrap();
6986            file.close().unwrap();
6987        }
6988        {
6989            let file = H5File::open(&path).unwrap();
6990            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
6991            assert_eq!(v.len(), 2000);
6992            assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
6993        }
6994        std::fs::remove_file(&path).ok();
6995    }
6996
6997    #[cfg(feature = "deflate")]
6998    #[test]
6999    fn ea_filtered_super_block_roundtrip() {
7000        // Compressed chunks across super blocks.
7001        let path = temp_path("ea_filt_super");
7002        {
7003            let file = H5File::create(&path).unwrap();
7004            let ds = file
7005                .new_dataset::<i32>()
7006                .shape([0])
7007                .chunk(&[1])
7008                .max_shape(&[None])
7009                .deflate(4)
7010                .create("v")
7011                .unwrap();
7012            ds.append(&(0..600).collect::<Vec<i32>>()).unwrap();
7013            file.close().unwrap();
7014        }
7015        {
7016            let file = H5File::open(&path).unwrap();
7017            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
7018            assert_eq!(v, (0..600).collect::<Vec<i32>>());
7019        }
7020        std::fs::remove_file(&path).ok();
7021    }
7022
7023    #[test]
7024    fn ea_super_block_open_append() {
7025        // Reopen a dataset and append chunks that fall in super blocks.
7026        let path = temp_path("ea_super_append");
7027        {
7028            let file = H5File::create(&path).unwrap();
7029            let ds = file
7030                .new_dataset::<i32>()
7031                .shape([0])
7032                .chunk(&[1])
7033                .max_shape(&[None])
7034                .create("v")
7035                .unwrap();
7036            ds.append(&(0..300).collect::<Vec<i32>>()).unwrap();
7037            file.close().unwrap();
7038        }
7039        {
7040            let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
7041            let idx = w.dataset_index("v").unwrap();
7042            for c in 300..900u64 {
7043                w.write_chunk(idx, c, &(c as i32).to_le_bytes()).unwrap();
7044            }
7045            w.extend_dataset(idx, &[900]).unwrap();
7046            w.close().unwrap();
7047        }
7048        {
7049            let file = H5File::open(&path).unwrap();
7050            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
7051            assert_eq!(v.len(), 900);
7052            assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
7053        }
7054        std::fs::remove_file(&path).ok();
7055    }
7056
7057    // Two or more unlimited dimensions select the v2 B-tree index; with a
7058    // filter its records become type 11, carrying each chunk's stored size and
7059    // mask. The payload is highly compressible, so the chunks really are
7060    // stored smaller than the extent — the file would be at least
7061    // 6*8*4 = 192 bytes of raw chunk data otherwise.
7062    #[cfg(feature = "deflate")]
7063    #[test]
7064    fn compressed_multi_unlimited_dataset_roundtrips() {
7065        let path = temp_path("bt2_filtered");
7066        {
7067            let file = H5File::create(&path).unwrap();
7068            let ds = file
7069                .new_dataset::<i32>()
7070                .shape([6, 8])
7071                .chunk(&[2, 4])
7072                .max_shape(&[None, None])
7073                .deflate(6)
7074                .create("d")
7075                .unwrap();
7076            ds.write_slice(&[0, 0], &[6, 8], &[7i32; 48]).unwrap();
7077            // A partial write forces a decompress-patch-recompress of one
7078            // chunk, whose new compressed size may not fit its old block.
7079            ds.write_slice(&[1, 1], &[2, 2], &[1i32, 2, 3, 4]).unwrap();
7080            file.close().unwrap();
7081        }
7082        {
7083            let file = H5File::open(&path).unwrap();
7084            let ds = file.dataset("d").unwrap();
7085            assert_eq!(ds.shape(), vec![6, 8]);
7086            let mut want = vec![7i32; 48];
7087            want[9] = 1;
7088            want[10] = 2;
7089            want[17] = 3;
7090            want[18] = 4;
7091            assert_eq!(ds.read_raw::<i32>().unwrap(), want);
7092        }
7093        std::fs::remove_file(&path).ok();
7094    }
7095
7096    #[test]
7097    fn btree_v2_multi_unlimited_roundtrip() {
7098        // A dataset with two unlimited dimensions uses the v2 B-tree chunk
7099        // index; chunks are written by grid coordinates with write_chunk_at.
7100        let path = temp_path("bt2_multi");
7101        {
7102            let file = H5File::create(&path).unwrap();
7103            let ds = file
7104                .new_dataset::<i32>()
7105                .shape([0, 0])
7106                .chunk(&[2, 2])
7107                .max_shape(&[None, None])
7108                .create("grid")
7109                .unwrap();
7110            assert!(ds.is_chunked());
7111            // 4x4 logical grid, value[r][c] = r*4 + c, in 2x2 chunks.
7112            for cr in 0..2usize {
7113                for cc in 0..2usize {
7114                    let mut bytes = Vec::new();
7115                    for i in 0..2usize {
7116                        for j in 0..2usize {
7117                            let v = ((cr * 2 + i) * 4 + (cc * 2 + j)) as i32;
7118                            bytes.extend_from_slice(&v.to_le_bytes());
7119                        }
7120                    }
7121                    ds.write_chunk_at(&[cr, cc], &bytes).unwrap();
7122                }
7123            }
7124            file.close().unwrap();
7125        }
7126        {
7127            let file = H5File::open(&path).unwrap();
7128            let ds = file.dataset("grid").unwrap();
7129            assert_eq!(ds.shape(), vec![4, 4]);
7130            assert_eq!(ds.read_raw::<i32>().unwrap(), (0..16).collect::<Vec<i32>>());
7131        }
7132        std::fs::remove_file(&path).ok();
7133    }
7134
7135    #[test]
7136    fn subframe_chunking_roundtrip() {
7137        // A chunk smaller than a frame: shape [N,8,8], chunk [1,4,4], so each
7138        // frame is tiled into a 2x2 grid of 4x4 chunks. write_chunk_at takes
7139        // the chunk-grid coordinates.
7140        let path = temp_path("subframe");
7141        {
7142            let file = H5File::create(&path).unwrap();
7143            let ds = file
7144                .new_dataset::<i32>()
7145                .shape([0, 8, 8])
7146                .chunk(&[1, 4, 4])
7147                .max_shape(&[None, Some(8), Some(8)])
7148                .create("v")
7149                .unwrap();
7150            for f in 0..3usize {
7151                for cr in 0..2usize {
7152                    for cc in 0..2usize {
7153                        let mut bytes = Vec::new();
7154                        for i in 0..4usize {
7155                            for j in 0..4usize {
7156                                let v = (f * 64 + (cr * 4 + i) * 8 + (cc * 4 + j)) as i32;
7157                                bytes.extend_from_slice(&v.to_le_bytes());
7158                            }
7159                        }
7160                        ds.write_chunk_at(&[f, cr, cc], &bytes).unwrap();
7161                    }
7162                }
7163            }
7164            file.close().unwrap();
7165        }
7166        {
7167            let file = H5File::open(&path).unwrap();
7168            let ds = file.dataset("v").unwrap();
7169            assert_eq!(ds.shape(), vec![3, 8, 8]);
7170            assert_eq!(
7171                ds.read_raw::<i32>().unwrap(),
7172                (0..192).collect::<Vec<i32>>()
7173            );
7174        }
7175        std::fs::remove_file(&path).ok();
7176    }
7177
7178    #[test]
7179    fn fill_value_contiguous_roundtrip() {
7180        let path = temp_path("fill_value_contig");
7181        {
7182            let file = H5File::create(&path).unwrap();
7183            let ds = file
7184                .new_dataset::<f32>()
7185                .shape([4])
7186                .fill_value(2.5f32)
7187                .create("data")
7188                .unwrap();
7189            ds.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
7190            file.close().unwrap();
7191        }
7192        // open_append decodes the fill-value message back from the header.
7193        {
7194            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
7195            let idx = writer.dataset_index("data").unwrap();
7196            assert_eq!(
7197                writer.ds(idx).lock().fill_value,
7198                Some(2.5f32.to_le_bytes().to_vec())
7199            );
7200        }
7201        // Data still reads back correctly.
7202        {
7203            let file = H5File::open(&path).unwrap();
7204            let ds = file.dataset("data").unwrap();
7205            assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
7206        }
7207        std::fs::remove_file(&path).ok();
7208    }
7209
7210    /// Early allocation on a fixed unfiltered shape selects the implicit
7211    /// index, and "implicit" is literal: the file holds no index structure
7212    /// at all, only a version-4 layout message of index type 2 pointing at
7213    /// the run of chunk space the create allocated.
7214    #[test]
7215    fn early_allocation_writes_the_implicit_index() {
7216        let path = temp_path("implicit_index");
7217        {
7218            let file = H5File::create(&path).unwrap();
7219            let ds = file
7220                .new_dataset::<i32>()
7221                .shape([16])
7222                .chunk(&[4])
7223                .early_allocation()
7224                .create("data")
7225                .unwrap();
7226            ds.write_raw(&(0..16i32).collect::<Vec<_>>()).unwrap();
7227            file.close().unwrap();
7228        }
7229        let bytes = std::fs::read(&path).unwrap();
7230        for magic in [b"EAHD", b"EAIB", b"FAHD", b"FADB", b"BTHD", b"TREE"] {
7231            assert!(
7232                !bytes.windows(4).any(|w| w == magic),
7233                "{} appears in a file whose chunk index is supposed to be no \
7234                 structure at all",
7235                String::from_utf8_lossy(magic)
7236            );
7237        }
7238        {
7239            let file = H5File::open(&path).unwrap();
7240            let ds = file.dataset("data").unwrap();
7241            assert_eq!(
7242                ds.read_raw::<i32>().unwrap(),
7243                (0..16i32).collect::<Vec<_>>()
7244            );
7245        }
7246        std::fs::remove_file(&path).ok();
7247    }
7248
7249    /// Two of the conditions are conditions: an unlimited dimension or a
7250    /// filter each send the dataset to the index libhdf5 would pick
7251    /// instead, early allocation or not. (The third — one whole-dataset
7252    /// chunk — sends it to the single-chunk index instead of Fixed Array;
7253    /// see `one_whole_dataset_chunk_writes_the_single_chunk_index`.)
7254    #[test]
7255    #[cfg(feature = "deflate")]
7256    fn early_allocation_only_picks_implicit_where_libhdf5_does() {
7257        // Every case writes and reads back its data, so a mis-selected index
7258        // shows up as wrong bytes and not just as a different structure.
7259        for (which, magic) in [("unlimited", b"EAHD"), ("filtered", b"FAHD")] {
7260            let path = temp_path("implicit_not");
7261            {
7262                let file = H5File::create(&path).unwrap();
7263                let builder = file.new_dataset::<i32>().shape([16]);
7264                let builder = match which {
7265                    "unlimited" => builder.chunk(&[4]).max_shape(&[None]),
7266                    _ => builder.chunk(&[4]).deflate(6),
7267                };
7268                let ds = builder.early_allocation().create("data").unwrap();
7269                ds.write_raw(&(0..16i32).collect::<Vec<_>>()).unwrap();
7270                file.close().unwrap();
7271            }
7272            let bytes = std::fs::read(&path).unwrap();
7273            assert!(
7274                bytes.windows(4).any(|w| w == magic),
7275                "{which}: expected a {} index",
7276                String::from_utf8_lossy(magic)
7277            );
7278            let file = H5File::open(&path).unwrap();
7279            assert_eq!(
7280                file.dataset("data").unwrap().read_raw::<i32>().unwrap(),
7281                (0..16i32).collect::<Vec<_>>(),
7282                "{which}"
7283            );
7284            std::fs::remove_file(&path).ok();
7285        }
7286    }
7287
7288    /// One whole-dataset chunk always selects the single-chunk index —
7289    /// ahead of Fixed Array, and ahead of Implicit too, whether or not early
7290    /// allocation was requested (`H5D__layout_set_latest_indexing` checks it
7291    /// unconditionally). Like Implicit, "single chunk" is literal: no index
7292    /// structure at all, just the one chunk's address — and, unfiltered and
7293    /// early-allocated, that address exists before anything is written — in
7294    /// the layout message directly.
7295    #[test]
7296    fn one_whole_dataset_chunk_writes_the_single_chunk_index() {
7297        for early in [false, true] {
7298            let path = temp_path("single_chunk_index");
7299            {
7300                let file = H5File::create(&path).unwrap();
7301                let builder = file.new_dataset::<i32>().shape([16]).chunk(&[16]);
7302                let builder = if early {
7303                    builder.early_allocation()
7304                } else {
7305                    builder
7306                };
7307                let ds = builder.create("data").unwrap();
7308                ds.write_raw(&(0..16i32).collect::<Vec<_>>()).unwrap();
7309                file.close().unwrap();
7310            }
7311            let bytes = std::fs::read(&path).unwrap();
7312            for magic in [b"EAHD", b"EAIB", b"FAHD", b"FADB", b"BTHD", b"TREE"] {
7313                assert!(
7314                    !bytes.windows(4).any(|w| w == magic),
7315                    "early={early}: {} appears in a file whose chunk index is \
7316                     supposed to be no structure at all",
7317                    String::from_utf8_lossy(magic)
7318                );
7319            }
7320            let file = H5File::open(&path).unwrap();
7321            assert_eq!(
7322                file.dataset("data").unwrap().read_raw::<i32>().unwrap(),
7323                (0..16i32).collect::<Vec<_>>(),
7324                "early={early}"
7325            );
7326            std::fs::remove_file(&path).ok();
7327        }
7328    }
7329
7330    /// An implicitly indexed dataset's chunks all exist from create, so an
7331    /// unwritten one reads back as the fill value — and the fill value is
7332    /// tiled over the whole run at create, not per chunk on demand.
7333    #[test]
7334    fn implicit_index_fills_every_chunk_at_create() {
7335        let path = temp_path("implicit_fill");
7336        {
7337            let file = H5File::create(&path).unwrap();
7338            let ds = file
7339                .new_dataset::<i32>()
7340                .shape([8])
7341                .chunk(&[4])
7342                .early_allocation()
7343                .fill_value(-3i32)
7344                .create("data")
7345                .unwrap();
7346            // Only chunk 0.
7347            let chunk: Vec<u8> = [1i32, 2, 3, 4]
7348                .iter()
7349                .flat_map(|v| v.to_le_bytes())
7350                .collect();
7351            ds.write_chunk(0, &chunk).unwrap();
7352            file.close().unwrap();
7353        }
7354        let file = H5File::open(&path).unwrap();
7355        let ds = file.dataset("data").unwrap();
7356        assert_eq!(
7357            ds.read_raw::<i32>().unwrap(),
7358            vec![1, 2, 3, 4, -3, -3, -3, -3]
7359        );
7360        std::fs::remove_file(&path).ok();
7361    }
7362
7363    /// A reopen has to reconstruct the run's address *and* its length from
7364    /// the layout message alone — there is no index structure to read it
7365    /// back from — or the close would rewrite the dataset as unallocated
7366    /// contiguous storage and drop every byte.
7367    #[test]
7368    fn implicit_index_survives_a_reopen() {
7369        let path = temp_path("implicit_reopen");
7370        {
7371            let file = H5File::create(&path).unwrap();
7372            file.new_dataset::<i32>()
7373                .shape([8])
7374                .chunk(&[4])
7375                .early_allocation()
7376                .create("data")
7377                .unwrap()
7378                .write_raw(&[0i32, 1, 2, 3, 4, 5, 6, 7])
7379                .unwrap();
7380            file.close().unwrap();
7381        }
7382        {
7383            let file = H5File::open_rw(&path).unwrap();
7384            let ds = file.dataset_writer("data").unwrap();
7385            let chunk: Vec<u8> = [10i32, 11, 12, 13]
7386                .iter()
7387                .flat_map(|v| v.to_le_bytes())
7388                .collect();
7389            ds.write_chunk(1, &chunk).unwrap();
7390            file.close().unwrap();
7391        }
7392        let file = H5File::open(&path).unwrap();
7393        assert_eq!(
7394            file.dataset("data").unwrap().read_raw::<i32>().unwrap(),
7395            vec![0, 1, 2, 3, 10, 11, 12, 13]
7396        );
7397        std::fs::remove_file(&path).ok();
7398    }
7399
7400    #[test]
7401    fn fill_value_chunked_roundtrip() {
7402        let path = temp_path("fill_value_chunked");
7403        {
7404            let file = H5File::create(&path).unwrap();
7405            let ds = file
7406                .new_dataset::<i32>()
7407                .shape([0])
7408                .chunk(&[4])
7409                .max_shape(&[None])
7410                .fill_value(-7i32)
7411                .create("vals")
7412                .unwrap();
7413            ds.append(&[1i32, 2, 3, 4]).unwrap();
7414            file.close().unwrap();
7415        }
7416        {
7417            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
7418            let idx = writer.dataset_index("vals").unwrap();
7419            assert_eq!(
7420                writer.ds(idx).lock().fill_value,
7421                Some((-7i32).to_le_bytes().to_vec())
7422            );
7423        }
7424        std::fs::remove_file(&path).ok();
7425    }
7426
7427    #[test]
7428    fn fill_value_bytes_takes_the_stored_type_in_its_declared_order() {
7429        use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
7430        let be12 = DatatypeMessage::FixedPoint {
7431            size: 2,
7432            byte_order: ByteOrder::BigEndian,
7433            signed: false,
7434            bit_offset: 0,
7435            bit_precision: 12,
7436        };
7437        let path = temp_path("fill_value_bytes");
7438        {
7439            let file = H5File::create(&path).unwrap();
7440            file.new_dataset::<u16>()
7441                .shape([0])
7442                .chunk(&[4])
7443                .max_shape(&[None])
7444                .datatype(be12.clone())
7445                .fill_value_bytes(&0x0abcu16.to_ne_bytes())
7446                .create("vals")
7447                .unwrap();
7448            let err = file
7449                .new_dataset::<u16>()
7450                .shape([4])
7451                .datatype(be12)
7452                .fill_value_bytes(&[1, 2, 3])
7453                .create("short");
7454            assert!(matches!(err, Err(crate::Hdf5Error::TypeMismatch(_))));
7455            file.close().unwrap();
7456        }
7457        {
7458            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
7459            let idx = writer.dataset_index("vals").unwrap();
7460            assert_eq!(writer.ds(idx).lock().fill_value, Some(vec![0x0a, 0xbc]));
7461            assert!(writer.dataset_index("short").is_none());
7462        }
7463        std::fs::remove_file(&path).ok();
7464    }
7465
7466    #[test]
7467    fn fill_value_read_missing_chunks() {
7468        // A chunked dataset with chunk 1 left unwritten must read that
7469        // gap back as the user-defined fill value, not zero.
7470        fn i32_bytes(vals: &[i32]) -> Vec<u8> {
7471            vals.iter().flat_map(|v| v.to_le_bytes()).collect()
7472        }
7473        let path = temp_path("fill_value_read_missing");
7474        {
7475            let file = H5File::create(&path).unwrap();
7476            let ds = file
7477                .new_dataset::<i32>()
7478                .shape([0])
7479                .chunk(&[2])
7480                .max_shape(&[None])
7481                .fill_value(-1i32)
7482                .create("vals")
7483                .unwrap();
7484            // chunk 0 = [10,20]; chunk 1 unwritten; chunk 2 = [50,60].
7485            ds.write_chunk(0, &i32_bytes(&[10, 20])).unwrap();
7486            ds.write_chunk(2, &i32_bytes(&[50, 60])).unwrap();
7487            ds.extend(&[6]).unwrap();
7488            file.close().unwrap();
7489        }
7490        {
7491            let file = H5File::open(&path).unwrap();
7492            let ds = file.dataset("vals").unwrap();
7493            let all = ds.read_raw::<i32>().unwrap();
7494            assert_eq!(all, vec![10, 20, -1, -1, 50, 60]);
7495        }
7496        std::fs::remove_file(&path).ok();
7497    }
7498
7499    #[test]
7500    fn fill_value_partial_chunk_padded_with_fill() {
7501        // A partial trailing chunk flushed at close must pad its unwritten
7502        // tail with the fill value. That pad sits beyond the logical shape,
7503        // so it is verified by scanning the on-disk chunk bytes directly.
7504        let path = temp_path("fill_value_partial_pad");
7505        {
7506            let file = H5File::create(&path).unwrap();
7507            let ds = file
7508                .new_dataset::<i32>()
7509                .shape([0])
7510                .chunk(&[4])
7511                .max_shape(&[None])
7512                .fill_value(-9i32)
7513                .create("vals")
7514                .unwrap();
7515            // 3 of 4 frames -> flushed as a partial chunk on close.
7516            ds.append(&[1i32, 2, 3]).unwrap();
7517            file.close().unwrap();
7518        }
7519        let bytes = std::fs::read(&path).unwrap();
7520        // Locate the chunk: i32 LE of [1, 2, 3] written contiguously.
7521        let needle: Vec<u8> = [1i32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
7522        let pos = bytes
7523            .windows(needle.len())
7524            .position(|w| w == needle)
7525            .expect("chunk data [1,2,3] not found in file");
7526        let pad = &bytes[pos + needle.len()..pos + needle.len() + 4];
7527        assert_eq!(
7528            pad,
7529            &(-9i32).to_le_bytes(),
7530            "partial chunk tail must be padded with fill value -9, got {:?}",
7531            pad
7532        );
7533        std::fs::remove_file(&path).ok();
7534    }
7535
7536    #[test]
7537    fn vlen_append_after_reopen_preserves_existing() {
7538        // Reopening and appending into a partially-written vlen chunk must
7539        // read-modify-write: the strings already on disk must survive.
7540        let path = temp_path("vlen_append_reopen");
7541        {
7542            let file = H5File::create(&path).unwrap();
7543            file.create_appendable_vlen_dataset("strs", 4, None)
7544                .unwrap();
7545            // 3 of 4 frames -> flushed as a partial chunk on close.
7546            file.append_vlen_strings("strs", &["a", "b", "c"]).unwrap();
7547            file.close().unwrap();
7548        }
7549        {
7550            // Append a 4th string -> partial-chunk write into chunk 0.
7551            let file = H5File::open_rw(&path).unwrap();
7552            file.append_vlen_strings("strs", &["d"]).unwrap();
7553            file.close().unwrap();
7554        }
7555        {
7556            let file = H5File::open(&path).unwrap();
7557            let ds = file.dataset("strs").unwrap();
7558            let got = ds.read_vlen_strings().unwrap();
7559            assert_eq!(
7560                got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
7561                vec!["a", "b", "c", "d"]
7562            );
7563        }
7564        std::fs::remove_file(&path).ok();
7565    }
7566
7567    #[test]
7568    fn fill_value_size_mismatch_errors() {
7569        let path = temp_path("fill_value_mismatch");
7570        let writer = crate::io::writer::Hdf5Writer::create(&path).unwrap();
7571        let dt = <f64 as crate::types::H5Type>::hdf5_type();
7572        let idx = writer.create_dataset("d", dt, &[4u64]).unwrap();
7573        // f64 element size is 8; a 4-byte fill value must be rejected.
7574        assert!(writer.set_dataset_fill_value(idx, vec![0u8; 4]).is_err());
7575        // The correct width succeeds.
7576        writer.set_dataset_fill_value(idx, vec![0u8; 8]).unwrap();
7577        writer.close().unwrap();
7578        std::fs::remove_file(&path).ok();
7579    }
7580
7581    #[test]
7582    fn datatype_exposes_class_sign_and_byteorder() {
7583        // The byte width alone cannot tell u8 from i8 (both 1 byte) or i32
7584        // from f32 (both 4 bytes). datatype() must report the real class and
7585        // signedness so a reader does not have to guess from element_size.
7586        use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
7587
7588        let path = temp_path("datatype_accessor");
7589        {
7590            let file = H5File::create(&path).unwrap();
7591            file.new_dataset::<u8>().shape([3]).create("u8d").unwrap();
7592            file.new_dataset::<i8>().shape([3]).create("i8d").unwrap();
7593            file.new_dataset::<i32>().shape([3]).create("i32d").unwrap();
7594            file.new_dataset::<f32>().shape([3]).create("f32d").unwrap();
7595            file.close().unwrap();
7596        }
7597
7598        let file = H5File::open(&path).unwrap();
7599
7600        match file.dataset("u8d").unwrap().datatype().unwrap() {
7601            DatatypeMessage::FixedPoint {
7602                size,
7603                signed,
7604                byte_order,
7605                ..
7606            } => {
7607                assert_eq!(size, 1);
7608                assert!(!signed, "u8 must be unsigned");
7609                assert_eq!(byte_order, ByteOrder::LittleEndian);
7610            }
7611            other => panic!("expected FixedPoint for u8, got {other:?}"),
7612        }
7613
7614        match file.dataset("i8d").unwrap().datatype().unwrap() {
7615            DatatypeMessage::FixedPoint { size, signed, .. } => {
7616                assert_eq!(size, 1);
7617                assert!(signed, "i8 must be signed");
7618            }
7619            other => panic!("expected FixedPoint for i8, got {other:?}"),
7620        }
7621
7622        match file.dataset("i32d").unwrap().datatype().unwrap() {
7623            DatatypeMessage::FixedPoint { size, signed, .. } => {
7624                assert_eq!(size, 4);
7625                assert!(signed, "i32 must be signed");
7626            }
7627            other => panic!("expected FixedPoint for i32, got {other:?}"),
7628        }
7629
7630        match file.dataset("f32d").unwrap().datatype().unwrap() {
7631            DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 4),
7632            other => panic!("expected FloatingPoint for f32, got {other:?}"),
7633        }
7634
7635        std::fs::remove_file(&path).ok();
7636    }
7637
7638    #[test]
7639    fn datatype_in_write_mode_errors() {
7640        let path = temp_path("datatype_write_mode");
7641        let file = H5File::create(&path).unwrap();
7642        let ds = file.new_dataset::<f32>().shape([4]).create("d").unwrap();
7643        assert!(ds.datatype().is_err());
7644        std::fs::remove_file(&path).ok();
7645    }
7646
7647    // --- write_chunk_raw (HDF5 direct chunk write) ---------------------------
7648
7649    /// Extensible-array path: pre-compress with the dataset's pipeline, write
7650    /// the bytes verbatim via write_chunk_raw (filter_mask = 0), and confirm
7651    /// the data round-trips through the reader unchanged.
7652    #[cfg(feature = "deflate")]
7653    #[test]
7654    fn write_chunk_raw_ea_roundtrip_mask0() {
7655        use crate::format::messages::filter::{apply_filters, FilterPipeline};
7656        let path = temp_path("wcr_ea_mask0");
7657        let original: Vec<i32> = (0..12).collect();
7658        {
7659            let file = H5File::create(&path).unwrap();
7660            let ds = file
7661                .new_dataset::<i32>()
7662                .shape([0])
7663                .chunk(&[4])
7664                .max_shape(&[None])
7665                .deflate(4)
7666                .create("v")
7667                .unwrap();
7668            assert!(ds.is_chunked());
7669            let pipeline = FilterPipeline::deflate(4);
7670            for c in 0..3usize {
7671                let raw: Vec<u8> = original[c * 4..c * 4 + 4]
7672                    .iter()
7673                    .flat_map(|v| v.to_le_bytes())
7674                    .collect();
7675                let compressed = apply_filters(&pipeline, &raw).unwrap();
7676                ds.write_chunk_raw(c, &compressed, 0).unwrap();
7677            }
7678            ds.set_extent(&[12]).unwrap();
7679            file.close().unwrap();
7680        }
7681        {
7682            let file = H5File::open(&path).unwrap();
7683            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
7684            assert_eq!(v, original);
7685        }
7686        std::fs::remove_file(&path).ok();
7687    }
7688
7689    /// Fixed-array path (all dimensions bounded): same verbatim write through
7690    /// the linear-index dispatch, round-tripped through the reader.
7691    #[cfg(feature = "deflate")]
7692    #[test]
7693    fn write_chunk_raw_fixed_array_roundtrip_mask0() {
7694        use crate::format::messages::filter::{apply_filters, FilterPipeline};
7695        let path = temp_path("wcr_fa_mask0");
7696        let original: Vec<i32> = (0..12).collect();
7697        {
7698            let file = H5File::create(&path).unwrap();
7699            let ds = file
7700                .new_dataset::<i32>()
7701                .shape([12])
7702                .chunk(&[4])
7703                .deflate(4)
7704                .create("v")
7705                .unwrap();
7706            assert!(ds.is_chunked());
7707            let pipeline = FilterPipeline::deflate(4);
7708            for c in 0..3usize {
7709                let raw: Vec<u8> = original[c * 4..c * 4 + 4]
7710                    .iter()
7711                    .flat_map(|v| v.to_le_bytes())
7712                    .collect();
7713                let compressed = apply_filters(&pipeline, &raw).unwrap();
7714                ds.write_chunk_raw(c, &compressed, 0).unwrap();
7715            }
7716            file.close().unwrap();
7717        }
7718        {
7719            let file = H5File::open(&path).unwrap();
7720            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
7721            assert_eq!(v, original);
7722        }
7723        std::fs::remove_file(&path).ok();
7724    }
7725
7726    /// The caller-supplied filter_mask must reach the on-disk filtered index
7727    /// entry (not be hardcoded to 0). Store one chunk uncompressed in a
7728    /// filtered dataset with mask = 1 (deflate skipped), then reopen and decode
7729    /// the extensible-array filtered entry to read the mask back at the format
7730    /// level (independent of the data reader's mask handling).
7731    #[cfg(feature = "deflate")]
7732    #[test]
7733    fn write_chunk_raw_records_filter_mask() {
7734        let path = temp_path("wcr_records_mask");
7735        let raw: Vec<u8> = [10i32, 20, 30, 40]
7736            .iter()
7737            .flat_map(|v| v.to_le_bytes())
7738            .collect();
7739        assert_eq!(raw.len(), 16);
7740        {
7741            let file = H5File::create(&path).unwrap();
7742            let ds = file
7743                .new_dataset::<i32>()
7744                .shape([0])
7745                .chunk(&[4])
7746                .max_shape(&[None])
7747                .deflate(4)
7748                .create("v")
7749                .unwrap();
7750            // mask = 1: bit 0 set => filter 0 (deflate) was skipped, so the
7751            // chunk is stored uncompressed (its raw bytes).
7752            ds.write_chunk_raw(0, &raw, 1).unwrap();
7753            ds.set_extent(&[4]).unwrap();
7754            file.close().unwrap();
7755        }
7756        // Reopen the writer; open_append decodes the filtered index block from
7757        // disk, so the entry reflects exactly what was committed.
7758        {
7759            let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
7760            let idx = w.dataset_index("v").unwrap();
7761            let ds = w.ds(idx);
7762            let m = ds.lock();
7763            let entry = &m
7764                .chunked
7765                .as_ref()
7766                .unwrap()
7767                .filt_iblk
7768                .as_ref()
7769                .unwrap()
7770                .elements[0];
7771            assert_eq!(entry.filter_mask, 1, "filter_mask must round-trip to disk");
7772            assert_eq!(entry.nbytes, 16, "uncompressed chunk stored verbatim");
7773        }
7774        std::fs::remove_file(&path).ok();
7775    }
7776
7777    /// Reader honors a per-chunk filter_mask (EA): one chunk is stored
7778    /// compressed (mask 0), the next stored raw with deflate skipped (mask 1),
7779    /// in the same dataset. A correct reader skips deflate for chunk 1 only;
7780    /// ignoring the mask would feed raw bytes through inflate and corrupt them.
7781    /// A chunk whose stored stream decodes to less than its image places no
7782    /// run at all: the whole chunk reads as the fill value, whether the read
7783    /// laid the fill down first (a plan that leaves output uncovered — here the
7784    /// unallocated middle chunk) or fills only what nothing wrote. The decode
7785    /// writes into the output image itself, so the bytes a short image leaves
7786    /// behind are the ones this covers.
7787    #[cfg(feature = "deflate")]
7788    #[test]
7789    fn a_chunk_that_decodes_short_of_its_image_reads_as_fill() {
7790        use crate::format::messages::filter::{apply_filters, FilterPipeline};
7791        let path = temp_path("short_chunk_image_is_fill");
7792        let pipeline = FilterPipeline::deflate(4);
7793        {
7794            let file = H5File::create(&path).unwrap();
7795            let ds = file
7796                .new_dataset::<i32>()
7797                .shape([0])
7798                .chunk(&[4])
7799                .max_shape(&[None])
7800                .fill_value(-1i32)
7801                .deflate(4)
7802                .create("v")
7803                .unwrap();
7804            // Chunk 0 carries two elements where its image wants four.
7805            let short: Vec<u8> = [7i32, 8].iter().flat_map(|v| v.to_le_bytes()).collect();
7806            ds.write_chunk_raw(0, &apply_filters(&pipeline, &short).unwrap(), 0)
7807                .unwrap();
7808            // Chunk 1 is never written; chunk 2 is whole.
7809            let whole: Vec<u8> = [9i32, 10, 11, 12]
7810                .iter()
7811                .flat_map(|v| v.to_le_bytes())
7812                .collect();
7813            ds.write_chunk_raw(2, &apply_filters(&pipeline, &whole).unwrap(), 0)
7814                .unwrap();
7815            ds.set_extent(&[12]).unwrap();
7816            file.close().unwrap();
7817        }
7818        {
7819            let file = H5File::open(&path).unwrap();
7820            let ds = file.dataset("v").unwrap();
7821            assert_eq!(
7822                ds.read_raw::<i32>().unwrap(),
7823                vec![-1, -1, -1, -1, -1, -1, -1, -1, 9, 10, 11, 12]
7824            );
7825            // The same verdict when the plan covers every output byte, so no
7826            // fill goes down first: chunks 0 and 2 alone.
7827            assert_eq!(ds.read_slice::<i32>(&[0], &[4]).unwrap(), vec![-1; 4]);
7828            assert_eq!(
7829                ds.read_slice::<i32>(&[8], &[4]).unwrap(),
7830                vec![9, 10, 11, 12]
7831            );
7832        }
7833        std::fs::remove_file(&path).ok();
7834    }
7835
7836    /// The staged spelling of the case above: a selection that takes only part
7837    /// of the short chunk decodes it into a buffer sized from the layout, and
7838    /// what that buffer holds past the stream is cut off rather than kept — a
7839    /// run inside the decoded bytes is real data, a run reaching past them is
7840    /// fill.
7841    #[cfg(feature = "deflate")]
7842    #[test]
7843    fn a_staged_chunk_carries_only_what_its_stream_decoded() {
7844        use crate::format::messages::filter::{apply_filters, FilterPipeline};
7845        let path = temp_path("short_chunk_image_staged");
7846        let pipeline = FilterPipeline::deflate(4);
7847        {
7848            let file = H5File::create(&path).unwrap();
7849            let ds = file
7850                .new_dataset::<i32>()
7851                .shape([0])
7852                .chunk(&[4])
7853                .max_shape(&[None])
7854                .fill_value(-1i32)
7855                .deflate(4)
7856                .create("v")
7857                .unwrap();
7858            let short: Vec<u8> = [7i32, 8].iter().flat_map(|v| v.to_le_bytes()).collect();
7859            ds.write_chunk_raw(0, &apply_filters(&pipeline, &short).unwrap(), 0)
7860                .unwrap();
7861            ds.set_extent(&[4]).unwrap();
7862            file.close().unwrap();
7863        }
7864        {
7865            let file = H5File::open(&path).unwrap();
7866            let ds = file.dataset("v").unwrap();
7867            // Inside the decoded bytes.
7868            assert_eq!(ds.read_slice::<i32>(&[0], &[2]).unwrap(), vec![7, 8]);
7869            // Straddling their end: the run is not placed at all.
7870            assert_eq!(ds.read_slice::<i32>(&[1], &[2]).unwrap(), vec![-1, -1]);
7871        }
7872        std::fs::remove_file(&path).ok();
7873    }
7874
7875    #[cfg(feature = "deflate")]
7876    #[test]
7877    fn write_chunk_raw_ea_per_chunk_mask_roundtrip() {
7878        use crate::format::messages::filter::{apply_filters, FilterPipeline};
7879        let path = temp_path("wcr_ea_per_chunk_mask");
7880        let original: Vec<i32> = (0..8).collect();
7881        let pipeline = FilterPipeline::deflate(4);
7882        {
7883            let file = H5File::create(&path).unwrap();
7884            let ds = file
7885                .new_dataset::<i32>()
7886                .shape([0])
7887                .chunk(&[4])
7888                .max_shape(&[None])
7889                .deflate(4)
7890                .create("v")
7891                .unwrap();
7892            let raw0: Vec<u8> = original[0..4]
7893                .iter()
7894                .flat_map(|v| v.to_le_bytes())
7895                .collect();
7896            // chunk 0: compressed through the pipeline, mask 0.
7897            ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
7898                .unwrap();
7899            let raw1: Vec<u8> = original[4..8]
7900                .iter()
7901                .flat_map(|v| v.to_le_bytes())
7902                .collect();
7903            // chunk 1: stored uncompressed, mask 1 (deflate skipped).
7904            ds.write_chunk_raw(1, &raw1, 1).unwrap();
7905            ds.set_extent(&[8]).unwrap();
7906            file.close().unwrap();
7907        }
7908        {
7909            let file = H5File::open(&path).unwrap();
7910            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
7911            assert_eq!(v, original);
7912        }
7913        std::fs::remove_file(&path).ok();
7914    }
7915
7916    /// Reader honors a per-chunk filter_mask (fixed array): same mixed
7917    /// compressed/raw chunks as the EA case, through the fixed-array index.
7918    #[cfg(feature = "deflate")]
7919    #[test]
7920    fn write_chunk_raw_fixed_array_per_chunk_mask_roundtrip() {
7921        use crate::format::messages::filter::{apply_filters, FilterPipeline};
7922        let path = temp_path("wcr_fa_per_chunk_mask");
7923        let original: Vec<i32> = (0..8).collect();
7924        let pipeline = FilterPipeline::deflate(4);
7925        {
7926            let file = H5File::create(&path).unwrap();
7927            let ds = file
7928                .new_dataset::<i32>()
7929                .shape([8])
7930                .chunk(&[4])
7931                .deflate(4)
7932                .create("v")
7933                .unwrap();
7934            let raw0: Vec<u8> = original[0..4]
7935                .iter()
7936                .flat_map(|v| v.to_le_bytes())
7937                .collect();
7938            ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
7939                .unwrap();
7940            let raw1: Vec<u8> = original[4..8]
7941                .iter()
7942                .flat_map(|v| v.to_le_bytes())
7943                .collect();
7944            ds.write_chunk_raw(1, &raw1, 1).unwrap();
7945            file.close().unwrap();
7946        }
7947        {
7948            let file = H5File::open(&path).unwrap();
7949            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
7950            assert_eq!(v, original);
7951        }
7952        std::fs::remove_file(&path).ok();
7953    }
7954
7955    /// An unfiltered chunk index has no slot for a stored size or mask, so a
7956    /// direct chunk write must be rejected rather than silently dropping them.
7957    #[test]
7958    fn write_chunk_raw_rejects_unfiltered() {
7959        let path = temp_path("wcr_unfiltered");
7960        let file = H5File::create(&path).unwrap();
7961        let ds = file
7962            .new_dataset::<i32>()
7963            .shape([0])
7964            .chunk(&[4])
7965            .max_shape(&[None])
7966            .create("v")
7967            .unwrap();
7968        let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
7969        assert!(
7970            err.to_string().contains("filtered dataset"),
7971            "expected a filtered-dataset error, got: {err}"
7972        );
7973        std::fs::remove_file(&path).ok();
7974    }
7975
7976    /// Two or more unlimited dimensions leave no fixed chunk grid for a linear
7977    /// index to mean anything against, so the linear entry point points the
7978    /// caller at the coordinate-addressed one rather than guessing a grid.
7979    #[test]
7980    fn write_chunk_raw_sends_btree_v2_to_the_coordinate_form() {
7981        let path = temp_path("wcr_btree2");
7982        let file = H5File::create(&path).unwrap();
7983        let ds = file
7984            .new_dataset::<i32>()
7985            .shape([0, 0])
7986            .chunk(&[2, 2])
7987            .max_shape(&[None, None])
7988            .create("grid")
7989            .unwrap();
7990        let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
7991        assert!(
7992            err.to_string().contains("write_chunk_raw_at"),
7993            "expected a pointer to the coordinate form, got: {err}"
7994        );
7995        std::fs::remove_file(&path).ok();
7996    }
7997
7998    /// Direct chunk writes on a v2-B-tree index: the bytes are stored verbatim
7999    /// and the type-11 record carries their size and the caller's mask, so a
8000    /// chunk written with the pipeline skipped (mask 1) reads back as the raw
8001    /// bytes while one written compressed (mask 0) is decompressed.
8002    #[cfg(feature = "deflate")]
8003    #[test]
8004    fn write_chunk_raw_at_round_trips_on_btree_v2() {
8005        use crate::format::messages::filter::{apply_filters, FilterPipeline};
8006
8007        let path = temp_path("wcr_at_btree2");
8008        let raw0: Vec<u8> = (0..4i32).flat_map(|v| v.to_le_bytes()).collect();
8009        let raw1: Vec<u8> = (100..104i32).flat_map(|v| v.to_le_bytes()).collect();
8010        {
8011            let file = H5File::create(&path).unwrap();
8012            let ds = file
8013                .new_dataset::<i32>()
8014                .shape([0, 0])
8015                .chunk(&[2, 2])
8016                .max_shape(&[None, None])
8017                .deflate(6)
8018                .create("grid")
8019                .unwrap();
8020            let pipeline = FilterPipeline::deflate(6);
8021            // Chunk (0,0): pipeline already applied upstream, mask 0.
8022            ds.write_chunk_raw_at(&[0, 0], &apply_filters(&pipeline, &raw0).unwrap(), 0)
8023                .unwrap();
8024            // Chunk (1,1): stored uncompressed, mask 1 says filter 0 was skipped.
8025            ds.write_chunk_raw_at(&[1, 1], &raw1, 1).unwrap();
8026            file.close().unwrap();
8027        }
8028        let file = H5File::open(&path).unwrap();
8029        let ds = file.dataset("grid").unwrap();
8030        assert_eq!(ds.shape(), vec![4, 4]);
8031        let all = ds.read_raw::<i32>().unwrap();
8032        // Chunk (0,0) occupies rows 0..2, columns 0..2.
8033        assert_eq!([all[0], all[1], all[4], all[5]], [0, 1, 2, 3]);
8034        // Chunk (1,1) occupies rows 2..4, columns 2..4.
8035        assert_eq!([all[10], all[11], all[14], all[15]], [100, 101, 102, 103]);
8036        drop(file);
8037        std::fs::remove_file(&path).ok();
8038    }
8039
8040    /// The coordinate form is not BT2-only: it addresses an extensible- or
8041    /// fixed-array dataset's grid just as well, and records the same mask.
8042    #[cfg(feature = "deflate")]
8043    #[test]
8044    fn write_chunk_raw_at_round_trips_on_the_array_indexes() {
8045        for (label, max_shape) in [
8046            ("wcr_at_ea", Some(vec![None, Some(4usize)])),
8047            ("wcr_at_fa", None),
8048        ] {
8049            let path = temp_path(label);
8050            let raw: Vec<u8> = (0..8i32).flat_map(|v| v.to_le_bytes()).collect();
8051            {
8052                let file = H5File::create(&path).unwrap();
8053                let mut b = file
8054                    .new_dataset::<i32>()
8055                    .shape([4usize, 4])
8056                    .chunk(&[2, 4])
8057                    .deflate(6);
8058                if let Some(ref ms) = max_shape {
8059                    b = b.max_shape(ms);
8060                }
8061                let ds = b.create("grid").unwrap();
8062                // Row-of-chunks 1, stored uncompressed with filter 0 skipped.
8063                ds.write_chunk_raw_at(&[1, 0], &raw, 1).unwrap();
8064                file.close().unwrap();
8065            }
8066            let file = H5File::open(&path).unwrap();
8067            let ds = file.dataset("grid").unwrap();
8068            let all = ds.read_raw::<i32>().unwrap();
8069            assert_eq!(&all[8..16], &(0..8).collect::<Vec<i32>>()[..], "{label}");
8070            drop(file);
8071            std::fs::remove_file(&path).ok();
8072        }
8073    }
8074
8075    /// A direct write hands over caller-supplied bytes, so the v2 B-tree's
8076    /// chunk-size field can overflow just as the array indexes' can. A 4-byte
8077    /// chunk gives chunk_size_len = 2 (max 65535).
8078    #[cfg(feature = "deflate")]
8079    #[test]
8080    fn write_chunk_raw_at_rejects_an_oversized_btree_v2_chunk() {
8081        let path = temp_path("wcr_at_oversized");
8082        let file = H5File::create(&path).unwrap();
8083        let ds = file
8084            .new_dataset::<i32>()
8085            .shape([0, 0])
8086            .chunk(&[1, 1])
8087            .max_shape(&[None, None])
8088            .deflate(4)
8089            .create("grid")
8090            .unwrap();
8091        let err = ds
8092            .write_chunk_raw_at(&[0, 0], &vec![0u8; 70000], 0)
8093            .unwrap_err();
8094        assert!(
8095            err.to_string().contains("does not fit"),
8096            "expected a chunk-size-field overflow error, got: {err}"
8097        );
8098        std::fs::remove_file(&path).ok();
8099    }
8100
8101    /// An unfiltered v2 B-tree record has no slot for a stored size or mask,
8102    /// the same reason the array indexes reject a direct write.
8103    #[test]
8104    fn write_chunk_raw_at_rejects_an_unfiltered_btree_v2() {
8105        let path = temp_path("wcr_at_unfiltered");
8106        let file = H5File::create(&path).unwrap();
8107        let ds = file
8108            .new_dataset::<i32>()
8109            .shape([0, 0])
8110            .chunk(&[2, 2])
8111            .max_shape(&[None, None])
8112            .create("grid")
8113            .unwrap();
8114        let err = ds.write_chunk_raw_at(&[0, 0], &[0u8; 16], 0).unwrap_err();
8115        assert!(
8116            err.to_string().contains("filtered dataset"),
8117            "expected a filtered-dataset error, got: {err}"
8118        );
8119        std::fs::remove_file(&path).ok();
8120    }
8121
8122    /// A stored size that does not fit the index's chunk-size field must error
8123    /// (libhdf5 H5D_CHUNK_ENCODE_SIZE_CHECK) instead of truncating silently.
8124    /// A 4-byte chunk (chunk[1] of i32) has chunk_size_len = 2 (max 65535), so
8125    /// a 70000-byte stored chunk overflows it.
8126    #[cfg(feature = "deflate")]
8127    #[test]
8128    fn write_chunk_raw_rejects_oversized_chunk() {
8129        let path = temp_path("wcr_oversized");
8130        let file = H5File::create(&path).unwrap();
8131        let ds = file
8132            .new_dataset::<i32>()
8133            .shape([0])
8134            .chunk(&[1])
8135            .max_shape(&[None])
8136            .deflate(4)
8137            .create("v")
8138            .unwrap();
8139        let err = ds.write_chunk_raw(0, &vec![0u8; 70000], 0).unwrap_err();
8140        assert!(
8141            err.to_string().contains("does not fit"),
8142            "expected a chunk-size-field overflow error, got: {err}"
8143        );
8144        std::fs::remove_file(&path).ok();
8145    }
8146
8147    // ---- issue #5: runtime-width fixed-string reading ----------------------
8148
8149    use crate::format::messages::datatype::{CompoundMember, DatatypeMessage};
8150
8151    /// Build a 1-D fixed-string dataset of `width` bytes per element from raw
8152    /// element images, optionally chunked and deflated.
8153    fn write_fixed_string_dataset(
8154        path: &std::path::Path,
8155        dt: DatatypeMessage,
8156        width: usize,
8157        elems: &[&[u8]],
8158        compressed: bool,
8159    ) {
8160        let mut raw = Vec::with_capacity(elems.len() * width);
8161        for e in elems {
8162            assert!(e.len() <= width);
8163            raw.extend_from_slice(e);
8164            raw.resize(raw.len() + (width - e.len()), 0);
8165        }
8166        let file = H5File::create(path).unwrap();
8167        let mut b = file.new_dataset::<u8>().datatype(dt).shape([elems.len()]);
8168        if compressed {
8169            b = b.chunk(&[2]).deflate(6);
8170        }
8171        let ds = b.create("labels").unwrap();
8172        ds.write_raw_bytes(&raw).unwrap();
8173        file.close().unwrap();
8174    }
8175
8176    /// The width is whatever the file says, so one call reads a 24-byte label
8177    /// column and a 100-byte one. Producers like VASP pick it per dataset.
8178    #[test]
8179    fn read_strings_handles_any_fixed_width() {
8180        for width in [4usize, 24, 100] {
8181            let path = temp_path(&format!("fixed_str_{width}"));
8182            write_fixed_string_dataset(
8183                &path,
8184                DatatypeMessage::fixed_string(width as u32),
8185                width,
8186                &[b"ab", b"cde", b""],
8187                false,
8188            );
8189            let file = H5File::open(&path).unwrap();
8190            let got = file.dataset("labels").unwrap().read_strings().unwrap();
8191            assert_eq!(got, vec!["ab", "cde", ""], "width {width}");
8192            std::fs::remove_file(&path).ok();
8193        }
8194    }
8195
8196    /// Each padding rule decides where the value ends. Null-terminated and
8197    /// null-padded both stop at the first NUL and ignore the bytes after it;
8198    /// space-padded strips only a tail of spaces, so an embedded NUL is
8199    /// content there.
8200    ///
8201    /// Checked against libhdf5 1.14.6: reading this same `"ab\0X\0\0"`
8202    /// null-padded element into a wider null-terminated destination gives
8203    /// `"ab"`, and reading a space-padded `"a\0b     "` gives `"a\0b"` —
8204    /// `H5T__conv_s_s` runs the same `!s[nchars]` loop for both null rules.
8205    #[test]
8206    fn read_strings_honors_every_padding_rule() {
8207        // "ab" then a NUL then trailing junk that both null rules must drop.
8208        let elem: &[u8] = b"ab\0X\0\0";
8209        for (padding, want) in [(0u8, "ab"), (1, "ab")] {
8210            let path = temp_path(&format!("fixed_pad_{padding}"));
8211            write_fixed_string_dataset(
8212                &path,
8213                DatatypeMessage::FixedString {
8214                    size: 6,
8215                    padding,
8216                    charset: 0,
8217                },
8218                6,
8219                &[elem],
8220                false,
8221            );
8222            let file = H5File::open(&path).unwrap();
8223            let got = file.dataset("labels").unwrap().read_strings().unwrap();
8224            assert_eq!(got, vec![want.to_string()], "padding {padding}");
8225            std::fs::remove_file(&path).ok();
8226        }
8227        // Space-padded keeps interior spaces and strips only the tail.
8228        let path = temp_path("fixed_pad_2");
8229        write_fixed_string_dataset(
8230            &path,
8231            DatatypeMessage::FixedString {
8232                size: 8,
8233                padding: 2,
8234                charset: 0,
8235            },
8236            8,
8237            &[b"a b     "],
8238            false,
8239        );
8240        let file = H5File::open(&path).unwrap();
8241        assert_eq!(
8242            file.dataset("labels").unwrap().read_strings().unwrap(),
8243            vec!["a b".to_string()]
8244        );
8245        std::fs::remove_file(&path).ok();
8246
8247        // ... and an embedded NUL, which no space rule marks as an end.
8248        let path = temp_path("fixed_pad_2_nul");
8249        write_fixed_string_dataset(
8250            &path,
8251            DatatypeMessage::FixedString {
8252                size: 8,
8253                padding: 2,
8254                charset: 0,
8255            },
8256            8,
8257            &[b"a\0b     "],
8258            false,
8259        );
8260        let file = H5File::open(&path).unwrap();
8261        assert_eq!(
8262            file.dataset("labels").unwrap().read_strings().unwrap(),
8263            vec!["a\0b".to_string()]
8264        );
8265        std::fs::remove_file(&path).ok();
8266    }
8267
8268    /// A reserved padding or character-set code is an error naming the element,
8269    /// not a guess.
8270    #[test]
8271    fn read_strings_rejects_reserved_datatype_codes() {
8272        for (padding, charset, want) in [(3u8, 0u8, "padding rule 3"), (0, 7, "character set 7")] {
8273            let path = temp_path(&format!("fixed_reserved_{padding}_{charset}"));
8274            write_fixed_string_dataset(
8275                &path,
8276                DatatypeMessage::FixedString {
8277                    size: 4,
8278                    padding,
8279                    charset,
8280                },
8281                4,
8282                &[b"ab"],
8283                false,
8284            );
8285            let file = H5File::open(&path).unwrap();
8286            let err = file
8287                .dataset("labels")
8288                .unwrap()
8289                .read_strings()
8290                .unwrap_err()
8291                .to_string();
8292            assert!(err.contains(want), "got: {err}");
8293            std::fs::remove_file(&path).ok();
8294        }
8295    }
8296
8297    /// The typed read paths reinterpret the element image, so the stored order
8298    /// has to be the host's first. A scalar is swapped; a composite cannot be
8299    /// (its members have their own orders and offsets) and is refused.
8300    #[test]
8301    fn to_host_byte_order_converts_scalars_and_refuses_composites() {
8302        use crate::dataset::{to_host_byte_order, HOST_BYTE_ORDER};
8303        use crate::format::messages::datatype::ByteOrder;
8304
8305        let foreign = match HOST_BYTE_ORDER {
8306            ByteOrder::LittleEndian => ByteOrder::BigEndian,
8307            ByteOrder::BigEndian => ByteOrder::LittleEndian,
8308        };
8309        let int = |order, size| DatatypeMessage::FixedPoint {
8310            size,
8311            byte_order: order,
8312            signed: false,
8313            bit_offset: 0,
8314            bit_precision: (size * 8) as u16,
8315        };
8316
8317        // Foreign order: each element is reversed, elementwise.
8318        let mut buf = [1u8, 2, 3, 4, 5, 6, 7, 8];
8319        to_host_byte_order(&mut buf, &int(foreign, 4), 4).unwrap();
8320        assert_eq!(buf, [4, 3, 2, 1, 8, 7, 6, 5]);
8321
8322        // Host order: untouched.
8323        let mut buf = [1u8, 2, 3, 4];
8324        to_host_byte_order(&mut buf, &int(HOST_BYTE_ORDER, 4), 4).unwrap();
8325        assert_eq!(buf, [1, 2, 3, 4]);
8326
8327        // One byte wide: no order to convert.
8328        let mut buf = [1u8, 2, 3, 4];
8329        to_host_byte_order(&mut buf, &int(foreign, 1), 1).unwrap();
8330        assert_eq!(buf, [1, 2, 3, 4]);
8331
8332        // An enum stores its values in its base type's order.
8333        let mut buf = [1u8, 2];
8334        let enumeration = DatatypeMessage::Enum {
8335            base: Box::new(int(foreign, 2)),
8336            members: Vec::new(),
8337        };
8338        to_host_byte_order(&mut buf, &enumeration, 2).unwrap();
8339        assert_eq!(buf, [2, 1]);
8340
8341        // A string has no byte order at all.
8342        let mut buf = *b"abcd";
8343        to_host_byte_order(&mut buf, &DatatypeMessage::fixed_string(4), 4).unwrap();
8344        assert_eq!(&buf, b"abcd");
8345
8346        // A compound whose members are all host-order is reinterpretable.
8347        let compound = |order| DatatypeMessage::Compound {
8348            size: 4,
8349            members: vec![CompoundMember {
8350                name: "x".into(),
8351                offset: 0,
8352                datatype: int(order, 4),
8353            }],
8354        };
8355        let mut buf = [1u8, 2, 3, 4];
8356        to_host_byte_order(&mut buf, &compound(HOST_BYTE_ORDER), 4).unwrap();
8357        assert_eq!(buf, [1, 2, 3, 4]);
8358
8359        // One that is not says so, rather than handing back the raw bytes.
8360        let mut buf = [1u8, 2, 3, 4];
8361        let err = to_host_byte_order(&mut buf, &compound(foreign), 4)
8362            .expect_err("a foreign-order compound was reinterpreted")
8363            .to_string();
8364        assert!(err.contains("read_raw_bytes"), "got: {err}");
8365        assert_eq!(buf, [1, 2, 3, 4], "the refused image is left alone");
8366    }
8367
8368    /// The write direction answers for exactly the types the read direction
8369    /// does — same classifier — and borrows the caller's bytes whenever the
8370    /// declared order is already the host's.
8371    #[test]
8372    fn to_stored_byte_order_converts_scalars_and_refuses_composites() {
8373        use crate::dataset::{to_stored_byte_order, FOREIGN_BYTE_ORDER, HOST_BYTE_ORDER};
8374        use std::borrow::Cow;
8375
8376        let int = |order, size| DatatypeMessage::FixedPoint {
8377            size,
8378            byte_order: order,
8379            signed: false,
8380            bit_offset: 0,
8381            bit_precision: (size * 8) as u16,
8382        };
8383
8384        // Declared foreign: each element is reversed on the way out.
8385        let host = [1u8, 2, 3, 4, 5, 6, 7, 8];
8386        let stored = to_stored_byte_order(&host, &int(FOREIGN_BYTE_ORDER, 4), 4).unwrap();
8387        assert_eq!(&*stored, &[4, 3, 2, 1, 8, 7, 6, 5]);
8388        assert!(matches!(stored, Cow::Owned(_)), "a swap needs its own copy");
8389
8390        // Declared host order: handed through without a copy.
8391        let stored = to_stored_byte_order(&host, &int(HOST_BYTE_ORDER, 4), 4).unwrap();
8392        assert!(matches!(stored, Cow::Borrowed(_)), "no copy without a swap");
8393        assert_eq!(&*stored, &host);
8394
8395        // One byte wide: no order to lay out.
8396        let stored = to_stored_byte_order(&host, &int(FOREIGN_BYTE_ORDER, 1), 1).unwrap();
8397        assert_eq!(&*stored, &host);
8398
8399        // An enum stores its values in its base type's order.
8400        let enumeration = DatatypeMessage::Enum {
8401            base: Box::new(int(FOREIGN_BYTE_ORDER, 2)),
8402            members: Vec::new(),
8403        };
8404        let stored = to_stored_byte_order(&[1u8, 2], &enumeration, 2).unwrap();
8405        assert_eq!(&*stored, &[2, 1]);
8406
8407        // A compound cannot be laid out as a unit; one that declares the
8408        // foreign order for a member is refused, not written host-order.
8409        let compound = |order| DatatypeMessage::Compound {
8410            size: 4,
8411            members: vec![CompoundMember {
8412                name: "x".into(),
8413                offset: 0,
8414                datatype: int(order, 4),
8415            }],
8416        };
8417        let stored = to_stored_byte_order(&[1u8, 2, 3, 4], &compound(HOST_BYTE_ORDER), 4).unwrap();
8418        assert_eq!(&*stored, &[1, 2, 3, 4]);
8419        let err = to_stored_byte_order(&[1u8, 2, 3, 4], &compound(FOREIGN_BYTE_ORDER), 4)
8420            .expect_err("a foreign-order compound was written from host bytes")
8421            .to_string();
8422        assert!(err.contains("write_raw_bytes"), "got: {err}");
8423    }
8424
8425    /// The declared character set is enforced: a byte that cannot be decoded is
8426    /// an error naming the element, and the lossy call is what accepts the file
8427    /// instead of a silent substitution here.
8428    #[test]
8429    fn read_strings_enforces_the_character_set_and_lossy_does_not() {
8430        // Latin-1 "é" (0xE9) in a dataset that declares ASCII, and a lone 0xFF
8431        // in one that declares UTF-8.
8432        for (charset, bytes, want) in [
8433            (0u8, b"caf\xe9".as_slice(), "ASCII character set"),
8434            (1, b"a\xff".as_slice(), "not valid UTF-8"),
8435        ] {
8436            let path = temp_path(&format!("fixed_charset_{charset}"));
8437            write_fixed_string_dataset(
8438                &path,
8439                DatatypeMessage::FixedString {
8440                    size: 6,
8441                    padding: 1,
8442                    charset,
8443                },
8444                6,
8445                &[b"ok", bytes],
8446                false,
8447            );
8448            let file = H5File::open(&path).unwrap();
8449            let ds = file.dataset("labels").unwrap();
8450            let err = ds.read_strings().unwrap_err().to_string();
8451            assert!(err.contains(want) && err.contains("string 1"), "got: {err}");
8452            let lossy = ds.read_strings_lossy().unwrap();
8453            assert_eq!(lossy[0], "ok");
8454            assert_eq!(
8455                lossy[1].chars().next().unwrap(),
8456                if charset == 0 { 'c' } else { 'a' }
8457            );
8458            std::fs::remove_file(&path).ok();
8459        }
8460    }
8461
8462    /// Valid multi-byte UTF-8 survives, and the trailing NUL padding does not
8463    /// split a character.
8464    #[test]
8465    fn read_strings_reads_utf8_fixed_strings() {
8466        let path = temp_path("fixed_utf8");
8467        write_fixed_string_dataset(
8468            &path,
8469            DatatypeMessage::fixed_string_utf8(12),
8470            12,
8471            &["héllo".as_bytes(), "안녕".as_bytes()],
8472            false,
8473        );
8474        let file = H5File::open(&path).unwrap();
8475        assert_eq!(
8476            file.dataset("labels").unwrap().read_strings().unwrap(),
8477            vec!["héllo".to_string(), "안녕".to_string()]
8478        );
8479        std::fs::remove_file(&path).ok();
8480    }
8481
8482    /// The decode sits on the decoded raw-data path, so a chunked and deflated
8483    /// dataset reads the same as a contiguous one.
8484    #[cfg(feature = "deflate")]
8485    #[test]
8486    fn read_strings_reads_a_compressed_fixed_string_dataset() {
8487        let path = temp_path("fixed_str_deflate");
8488        write_fixed_string_dataset(
8489            &path,
8490            DatatypeMessage::fixed_string(16),
8491            16,
8492            &[b"alpha", b"beta", b"gamma", b"delta", b"epsilon"],
8493            true,
8494        );
8495        let file = H5File::open(&path).unwrap();
8496        assert_eq!(
8497            file.dataset("labels").unwrap().read_strings().unwrap(),
8498            vec!["alpha", "beta", "gamma", "delta", "epsilon"]
8499        );
8500        std::fs::remove_file(&path).ok();
8501    }
8502
8503    /// One call covers both string datatypes, so a caller need not branch on
8504    /// which one the file used.
8505    #[test]
8506    fn read_strings_also_reads_variable_length_strings() {
8507        let path = temp_path("read_strings_vlen");
8508        {
8509            let file = H5File::create(&path).unwrap();
8510            file.write_vlen_strings("names", &["alpha", "", "안녕"])
8511                .unwrap();
8512            file.close().unwrap();
8513        }
8514        let file = H5File::open(&path).unwrap();
8515        assert_eq!(
8516            file.dataset("names").unwrap().read_strings().unwrap(),
8517            vec!["alpha".to_string(), String::new(), "안녕".to_string()]
8518        );
8519        std::fs::remove_file(&path).ok();
8520    }
8521
8522    /// A file declaring a zero-width fixed string is an error, not the panic
8523    /// `chunks_exact(0)` would raise. Nothing in this crate writes one, so the
8524    /// test patches the width in the encoded datatype message down to zero and
8525    /// re-stamps the object header's checksum over the result.
8526    #[test]
8527    fn read_strings_rejects_a_zero_width_fixed_string_dataset() {
8528        use crate::format::checksum::checksum_metadata;
8529        use crate::format::object_header::OHDR_SIGNATURE;
8530
8531        let path = temp_path("fixed_str_zero_width");
8532        write_fixed_string_dataset(
8533            &path,
8534            DatatypeMessage::fixed_string(37),
8535            37,
8536            &[b"ab", b"cd"],
8537            false,
8538        );
8539
8540        // Version 1 string datatype: class|version, padding|charset, two
8541        // reserved bytes, then the width as a little-endian u32. The width is
8542        // 37 so the eight bytes occur once in the file.
8543        let mut bytes = std::fs::read(&path).unwrap();
8544        let needle = [0x13u8, 0, 0, 0, 37, 0, 0, 0];
8545        let at = bytes
8546            .windows(needle.len())
8547            .position(|w| w == needle)
8548            .expect("encoded fixed-string datatype message");
8549        assert!(
8550            !bytes[at + 1..].windows(needle.len()).any(|w| w == needle),
8551            "the datatype message pattern is not unique in the file"
8552        );
8553
8554        // The enclosing v2 object header ends in a checksum over everything
8555        // from its signature onwards; find the offset where the stored value
8556        // still agrees, so the patched header can be re-stamped there.
8557        let ohdr = bytes[..at]
8558            .windows(4)
8559            .rposition(|w| w == OHDR_SIGNATURE)
8560            .expect("enclosing object header");
8561        let cksum_at = (at + needle.len()..bytes.len() - 4)
8562            .find(|&e| {
8563                u32::from_le_bytes(bytes[e..e + 4].try_into().unwrap())
8564                    == checksum_metadata(&bytes[ohdr..e])
8565            })
8566            .expect("object header checksum");
8567
8568        bytes[at + 4..at + 8].copy_from_slice(&0u32.to_le_bytes());
8569        let fixed = checksum_metadata(&bytes[ohdr..cksum_at]);
8570        bytes[cksum_at..cksum_at + 4].copy_from_slice(&fixed.to_le_bytes());
8571        std::fs::write(&path, &bytes).unwrap();
8572
8573        let file = H5File::open(&path).unwrap();
8574        let err = file
8575            .dataset("labels")
8576            .unwrap()
8577            .read_strings()
8578            .unwrap_err()
8579            .to_string();
8580        assert!(err.contains("zero width"), "got: {err}");
8581        std::fs::remove_file(&path).ok();
8582    }
8583
8584    /// A non-string dataset is an error, not an attempt to reinterpret bytes.
8585    #[test]
8586    fn read_strings_rejects_a_non_string_dataset() {
8587        let path = temp_path("read_strings_numeric");
8588        {
8589            let file = H5File::create(&path).unwrap();
8590            let ds = file.new_dataset::<i32>().shape([3]).create("nums").unwrap();
8591            ds.write_raw(&[1i32, 2, 3]).unwrap();
8592            file.close().unwrap();
8593        }
8594        let file = H5File::open(&path).unwrap();
8595        let err = file
8596            .dataset("nums")
8597            .unwrap()
8598            .read_strings()
8599            .unwrap_err()
8600            .to_string();
8601        assert!(err.contains("only for string datasets"), "got: {err}");
8602        std::fs::remove_file(&path).ok();
8603    }
8604
8605    // ---- issue #6: random updates to vlen string datasets ------------------
8606
8607    /// One element changes; the extent and every other element stay as they
8608    /// were, on a contiguous vlen dataset.
8609    #[test]
8610    fn write_vlen_strings_slice_replaces_one_element() {
8611        let path = temp_path("vlen_slice_contig");
8612        {
8613            let file = H5File::create(&path).unwrap();
8614            file.write_vlen_strings("notes", &["a", "b", "c", "d"])
8615                .unwrap();
8616            file.close().unwrap();
8617        }
8618        {
8619            let file = H5File::open_rw(&path).unwrap();
8620            file.dataset_writer("notes")
8621                .unwrap()
8622                .write_vlen_strings_slice(1, &["replacement"])
8623                .unwrap();
8624            file.close().unwrap();
8625        }
8626        let file = H5File::open(&path).unwrap();
8627        let ds = file.dataset("notes").unwrap();
8628        assert_eq!(ds.shape(), vec![4]);
8629        assert_eq!(
8630            ds.read_vlen_strings().unwrap(),
8631            vec!["a", "replacement", "c", "d"]
8632        );
8633        std::fs::remove_file(&path).ok();
8634    }
8635
8636    /// The same on an appendable chunked dataset, across a reopen, over a range
8637    /// that spans a chunk boundary.
8638    #[test]
8639    fn write_vlen_strings_slice_spans_chunks_after_reopen() {
8640        let path = temp_path("vlen_slice_chunked");
8641        {
8642            let file = H5File::create(&path).unwrap();
8643            file.create_appendable_vlen_dataset("notes", 2, None)
8644                .unwrap();
8645            let all: Vec<String> = (0..6).map(|i| format!("v{i}")).collect();
8646            let refs: Vec<&str> = all.iter().map(|s| s.as_str()).collect();
8647            file.append_vlen_strings("notes", &refs).unwrap();
8648            file.close().unwrap();
8649        }
8650        {
8651            // Elements 1..4 cross the 2-element chunk boundary twice.
8652            let file = H5File::open_rw(&path).unwrap();
8653            file.dataset_writer("notes")
8654                .unwrap()
8655                .write_vlen_strings_slice(1, &["x", "y", "z"])
8656                .unwrap();
8657            file.close().unwrap();
8658        }
8659        let file = H5File::open(&path).unwrap();
8660        let ds = file.dataset("notes").unwrap();
8661        assert_eq!(ds.shape(), vec![6]);
8662        assert_eq!(
8663            ds.read_vlen_strings().unwrap(),
8664            vec!["v0", "x", "y", "z", "v4", "v5"]
8665        );
8666        std::fs::remove_file(&path).ok();
8667    }
8668
8669    /// Elements the append buffer still holds are not on disk yet; the
8670    /// update flushes them to their chunks first, so the flush at close has
8671    /// nothing left to write the pre-update reference over.
8672    #[test]
8673    fn write_vlen_strings_slice_updates_buffered_elements() {
8674        let path = temp_path("vlen_slice_buffered");
8675        {
8676            let file = H5File::create(&path).unwrap();
8677            file.create_appendable_vlen_dataset("notes", 4, None)
8678                .unwrap();
8679            // 3 of a 4-element chunk: all three stay in the append buffer.
8680            file.append_vlen_strings("notes", &["a", "b", "c"]).unwrap();
8681            file.dataset_writer("notes")
8682                .unwrap()
8683                .write_vlen_strings_slice(1, &["patched"])
8684                .unwrap();
8685            file.close().unwrap();
8686        }
8687        let file = H5File::open(&path).unwrap();
8688        assert_eq!(
8689            file.dataset("notes").unwrap().read_vlen_strings().unwrap(),
8690            vec!["a", "patched", "c"]
8691        );
8692        std::fs::remove_file(&path).ok();
8693    }
8694
8695    /// A range past the end is rejected before anything is written, and an
8696    /// empty batch costs the file nothing — without the early return it would
8697    /// still allocate and write an empty global-heap collection.
8698    #[test]
8699    fn write_vlen_strings_slice_checks_its_range() {
8700        let build = |name: &str, empty_call: bool| {
8701            let path = temp_path(name);
8702            let file = H5File::create(&path).unwrap();
8703            file.write_vlen_strings("notes", &["a", "b"]).unwrap();
8704            let ds = file.dataset_writer("notes").unwrap();
8705            let err = ds
8706                .write_vlen_strings_slice(1, &["x", "y"])
8707                .unwrap_err()
8708                .to_string();
8709            assert!(
8710                err.contains("outside the dataset's 2 elements"),
8711                "got: {err}"
8712            );
8713            if empty_call {
8714                ds.write_vlen_strings_slice(0, &[]).unwrap();
8715            }
8716            file.close().unwrap();
8717            path
8718        };
8719
8720        let with_empty = build("vlen_slice_range", true);
8721        let control = build("vlen_slice_range_control", false);
8722        assert_eq!(
8723            std::fs::metadata(&with_empty).unwrap().len(),
8724            std::fs::metadata(&control).unwrap().len(),
8725            "the rejected and empty calls must leave the file untouched"
8726        );
8727
8728        let file = H5File::open(&with_empty).unwrap();
8729        assert_eq!(
8730            file.dataset("notes").unwrap().read_vlen_strings().unwrap(),
8731            vec!["a", "b"]
8732        );
8733        std::fs::remove_file(&with_empty).ok();
8734        std::fs::remove_file(&control).ok();
8735    }
8736
8737    /// The element offset is one-dimensional, so a multi-dimensional dataset is
8738    /// rejected rather than silently indexed along the first axis.
8739    #[test]
8740    fn write_vlen_strings_slice_rejects_a_multidimensional_dataset() {
8741        let path = temp_path("vlen_slice_2d");
8742        let file = H5File::create(&path).unwrap();
8743        let ds = file
8744            .new_dataset::<u8>()
8745            .datatype(DatatypeMessage::vlen_string_utf8())
8746            .shape([2, 3])
8747            .create("grid")
8748            .unwrap();
8749        let err = ds
8750            .write_vlen_strings_slice(0, &["x"])
8751            .unwrap_err()
8752            .to_string();
8753        assert!(err.contains("1-dimension datasets"), "got: {err}");
8754        file.close().unwrap();
8755        std::fs::remove_file(&path).ok();
8756    }
8757
8758    /// A `&str` is UTF-8, so writing a non-ASCII one into a dataset that
8759    /// declares the ASCII character set would mislabel the bytes.
8760    #[test]
8761    fn write_vlen_strings_slice_enforces_the_ascii_character_set() {
8762        let path = temp_path("vlen_slice_ascii");
8763        let file = H5File::create(&path).unwrap();
8764        let ds = file
8765            .new_dataset::<u8>()
8766            .datatype(DatatypeMessage::vlen_string_ascii())
8767            .shape([3])
8768            .create("notes")
8769            .unwrap();
8770        let err = ds
8771            .write_vlen_strings_slice(0, &["ok", "안녕"])
8772            .unwrap_err()
8773            .to_string();
8774        assert!(
8775            err.contains("string 1") && err.contains("is not ASCII"),
8776            "got: {err}"
8777        );
8778        ds.write_vlen_strings_slice(0, &["ok", "fine"]).unwrap();
8779        file.close().unwrap();
8780        std::fs::remove_file(&path).ok();
8781    }
8782
8783    /// A numeric dataset is rejected: its elements are not vlen references and
8784    /// writing one would corrupt the column.
8785    #[test]
8786    fn write_vlen_strings_slice_rejects_a_non_vlen_dataset() {
8787        let path = temp_path("vlen_slice_numeric");
8788        let file = H5File::create(&path).unwrap();
8789        let ds = file.new_dataset::<i32>().shape([3]).create("nums").unwrap();
8790        ds.write_raw(&[1i32, 2, 3]).unwrap();
8791        let err = ds
8792            .write_vlen_strings_slice(0, &["x"])
8793            .unwrap_err()
8794            .to_string();
8795        assert!(
8796            err.contains("only for variable-length string datasets"),
8797            "got: {err}"
8798        );
8799        file.close().unwrap();
8800        std::fs::remove_file(&path).ok();
8801    }
8802
8803    // ---- superseded global heap objects (libhdf5 H5HG_remove parity) -------
8804
8805    /// Repeatedly replacing the same element must not grow the file per
8806    /// update: the collection each update supersedes is freed and the next
8807    /// update's collection lands in that block. Without the release every
8808    /// update costs another `H5HG_MINALLOC` (4096) bytes.
8809    #[test]
8810    fn write_vlen_strings_slice_reuses_the_freed_heap_block() {
8811        let size_after = |updates: usize| {
8812            let path = temp_path(&format!("vlen_slice_heap_reuse_{updates}"));
8813            let file = H5File::create(&path).unwrap();
8814            file.write_vlen_strings("notes", &["a", "b"]).unwrap();
8815            let ds = file.dataset_writer("notes").unwrap();
8816            for i in 0..updates {
8817                ds.write_vlen_strings_slice(0, &[&format!("update {i}")])
8818                    .unwrap();
8819            }
8820            file.close().unwrap();
8821            let n = std::fs::metadata(&path).unwrap().len();
8822            let read = H5File::open(&path).unwrap();
8823            assert_eq!(
8824                read.dataset("notes").unwrap().read_vlen_strings().unwrap(),
8825                vec![format!("update {}", updates - 1), "b".to_string()]
8826            );
8827            drop(read);
8828            std::fs::remove_file(&path).ok();
8829            n
8830        };
8831
8832        // The allocator settles once a freed block is available to reuse, so
8833        // every count past that produces the same file.
8834        let settled = size_after(3);
8835        assert_eq!(size_after(20), settled, "20 updates against 3");
8836        assert_eq!(size_after(50), settled, "50 updates against 3");
8837    }
8838
8839    /// An empty string is stored as a real heap object under a reference whose
8840    /// sequence length is zero, so the release must go by the address, not the
8841    /// length — a length test strands the object and its collection forever.
8842    #[test]
8843    fn write_vlen_strings_slice_frees_an_empty_strings_object() {
8844        let size_after = |updates: usize| {
8845            let path = temp_path(&format!("vlen_slice_empty_reuse_{updates}"));
8846            let file = H5File::create(&path).unwrap();
8847            file.write_vlen_strings("notes", &["", "b"]).unwrap();
8848            let ds = file.dataset_writer("notes").unwrap();
8849            for _ in 0..updates {
8850                ds.write_vlen_strings_slice(0, &[""]).unwrap();
8851            }
8852            file.close().unwrap();
8853            let n = std::fs::metadata(&path).unwrap().len();
8854            let read = H5File::open(&path).unwrap();
8855            assert_eq!(
8856                read.dataset("notes").unwrap().read_vlen_strings().unwrap(),
8857                vec!["".to_string(), "b".to_string()]
8858            );
8859            drop(read);
8860            std::fs::remove_file(&path).ok();
8861            n
8862        };
8863
8864        let settled = size_after(3);
8865        assert_eq!(size_after(20), settled, "20 empty updates against 3");
8866        assert_eq!(size_after(50), settled, "50 empty updates against 3");
8867    }
8868
8869    /// The elements the update does not name keep their strings, so freeing
8870    /// the superseded objects must not disturb the collection's survivors.
8871    #[test]
8872    fn write_vlen_strings_slice_keeps_the_untouched_strings_readable() {
8873        let path = temp_path("vlen_slice_heap_survivors");
8874        {
8875            let file = H5File::create(&path).unwrap();
8876            file.write_vlen_strings("notes", &["a", "b", "c", "d"])
8877                .unwrap();
8878            let ds = file.dataset_writer("notes").unwrap();
8879            // Two updates inside the one collection the create wrote, so the
8880            // second reads a collection the first already rewrote.
8881            ds.write_vlen_strings_slice(1, &["B"]).unwrap();
8882            ds.write_vlen_strings_slice(3, &["D"]).unwrap();
8883            file.close().unwrap();
8884        }
8885        let file = H5File::open(&path).unwrap();
8886        assert_eq!(
8887            file.dataset("notes").unwrap().read_vlen_strings().unwrap(),
8888            vec!["a", "B", "c", "D"]
8889        );
8890        std::fs::remove_file(&path).ok();
8891    }
8892
8893    /// Replacing every element of a chunked dataset empties the collection the
8894    /// append wrote, and the file must still read back correctly after its
8895    /// block goes to the allocator.
8896    #[test]
8897    fn write_vlen_strings_slice_frees_an_emptied_collection() {
8898        let path = temp_path("vlen_slice_heap_emptied");
8899        {
8900            let file = H5File::create(&path).unwrap();
8901            file.create_appendable_vlen_dataset("notes", 2, None)
8902                .unwrap();
8903            file.append_vlen_strings("notes", &["p", "q", "r", "s"])
8904                .unwrap();
8905            file.close().unwrap();
8906        }
8907        {
8908            let file = H5File::open_rw(&path).unwrap();
8909            file.dataset_writer("notes")
8910                .unwrap()
8911                .write_vlen_strings_slice(0, &["w", "x", "y", "z"])
8912                .unwrap();
8913            file.close().unwrap();
8914        }
8915        let file = H5File::open(&path).unwrap();
8916        assert_eq!(
8917            file.dataset("notes").unwrap().read_vlen_strings().unwrap(),
8918            vec!["w", "x", "y", "z"]
8919        );
8920        std::fs::remove_file(&path).ok();
8921    }
8922
8923    /// A collection larger than the 4096-byte minimum must keep its size when
8924    /// an object leaves it. Re-encoding at the natural size instead shrinks
8925    /// what the header declares, so the block's tail stops being part of the
8926    /// collection and the eventual free returns less than was allocated —
8927    /// stranding the difference on every cycle.
8928    #[test]
8929    fn write_vlen_strings_slice_keeps_an_oversized_collections_block_whole() {
8930        let big = |tag: char| std::iter::repeat_n(tag, 2000).collect::<String>();
8931        let size_after = |cycles: usize| {
8932            let path = temp_path(&format!("vlen_slice_heap_big_{cycles}"));
8933            let file = H5File::create(&path).unwrap();
8934            let seed: Vec<String> = "abcd".chars().map(big).collect();
8935            let refs: Vec<&str> = seed.iter().map(|s| s.as_str()).collect();
8936            // Four 2000-byte strings do not fit the 4096-byte minimum, so this
8937            // is one collection well above it.
8938            file.write_vlen_strings("notes", &refs).unwrap();
8939            let ds = file.dataset_writer("notes").unwrap();
8940            for _ in 0..cycles {
8941                // Partially empty the collection, then finish it off: the
8942                // block is freed only after it has been rewritten once.
8943                let head = big('x');
8944                ds.write_vlen_strings_slice(0, &[&head]).unwrap();
8945                let tail: Vec<String> = "yzw".chars().map(big).collect();
8946                let tail_refs: Vec<&str> = tail.iter().map(|s| s.as_str()).collect();
8947                ds.write_vlen_strings_slice(1, &tail_refs).unwrap();
8948            }
8949            file.close().unwrap();
8950            let n = std::fs::metadata(&path).unwrap().len();
8951            let read = H5File::open(&path).unwrap();
8952            assert_eq!(
8953                read.dataset("notes").unwrap().read_vlen_strings().unwrap(),
8954                vec![big('x'), big('y'), big('z'), big('w')]
8955            );
8956            drop(read);
8957            std::fs::remove_file(&path).ok();
8958            n
8959        };
8960
8961        let settled = size_after(4);
8962        assert_eq!(size_after(30), settled, "30 cycles against 4");
8963    }
8964
8965    /// An element still in the append buffer has never been on disk, so its
8966    /// superseded object has to be found in the buffer or it is stranded.
8967    #[test]
8968    fn write_vlen_strings_slice_releases_a_buffered_elements_object() {
8969        let path = temp_path("vlen_slice_heap_buffered");
8970        let file = H5File::create(&path).unwrap();
8971        file.create_appendable_vlen_dataset("notes", 4, None)
8972            .unwrap();
8973        file.append_vlen_strings("notes", &["a", "b", "c"]).unwrap();
8974        let ds = file.dataset_writer("notes").unwrap();
8975        for i in 0..20 {
8976            ds.write_vlen_strings_slice(1, &[&format!("patch {i}")])
8977                .unwrap();
8978        }
8979        file.close().unwrap();
8980
8981        let file = H5File::open(&path).unwrap();
8982        assert_eq!(
8983            file.dataset("notes").unwrap().read_vlen_strings().unwrap(),
8984            vec!["a", "patch 19", "c"]
8985        );
8986        let size = std::fs::metadata(&path).unwrap().len();
8987        std::fs::remove_file(&path).ok();
8988        assert!(
8989            size < 20 * 4096,
8990            "20 buffered updates left {size} bytes, one collection per update"
8991        );
8992    }
8993
8994    /// Regression: a typed `write_slice` into rows the append buffer still
8995    /// held wrote the chunks, and the flush at close wrote the stale buffered
8996    /// rows back over it — write 99, read 50. The slice now flushes the
8997    /// buffer first, making the chunks the single authority for those rows.
8998    #[test]
8999    fn write_slice_into_the_buffered_tail_survives_close() {
9000        let path = temp_path("slice_into_buffered_tail");
9001        {
9002            let file = H5File::create(&path).unwrap();
9003            let ds = file
9004                .new_dataset::<i32>()
9005                .shape([0])
9006                .chunk(&[4])
9007                .max_shape(&[None])
9008                .create("d")
9009                .unwrap();
9010            // 6 rows: 4 land in chunk 0, rows 4 and 5 stay buffered.
9011            ds.append(&[10, 11, 12, 13, 50, 51]).unwrap();
9012            ds.write_slice(&[4], &[1], &[99]).unwrap();
9013            file.close().unwrap();
9014        }
9015        {
9016            let file = H5File::open(&path).unwrap();
9017            let ds = file.dataset("d").unwrap();
9018            assert_eq!(ds.read_raw::<i32>().unwrap(), vec![10, 11, 12, 13, 99, 51]);
9019        }
9020        std::fs::remove_file(&path).ok();
9021    }
9022
9023    /// Extending a dataset while appends sit in the buffer must not move
9024    /// them: the buffer records the absolute row its frames belong to, so
9025    /// the flush at close lands them there, and the grown region reads as
9026    /// fill.
9027    #[test]
9028    fn extend_does_not_move_buffered_appends() {
9029        let path = temp_path("extend_keeps_buffered_rows");
9030        {
9031            let file = H5File::create(&path).unwrap();
9032            let ds = file
9033                .new_dataset::<i32>()
9034                .shape([0])
9035                .chunk(&[4])
9036                .max_shape(&[None])
9037                .create("d")
9038                .unwrap();
9039            ds.append(&[10, 11, 12, 13, 50, 51]).unwrap(); // rows 4, 5 buffered
9040            ds.extend(&[10]).unwrap();
9041            file.close().unwrap();
9042        }
9043        {
9044            let file = H5File::open(&path).unwrap();
9045            let ds = file.dataset("d").unwrap();
9046            assert_eq!(
9047                ds.read_raw::<i32>().unwrap(),
9048                vec![10, 11, 12, 13, 50, 51, 0, 0, 0, 0]
9049            );
9050        }
9051        std::fs::remove_file(&path).ok();
9052    }
9053
9054    /// Regression: appends to a v2 B-tree indexed dataset (two unlimited
9055    /// dimensions) buffered fine but close() failed "not a chunked dataset"
9056    /// and lost the buffered rows — the append's chunk writes required the
9057    /// extensible-array index. They now go through the index-generic
9058    /// hyperslab engine.
9059    #[test]
9060    fn append_to_a_btree_v2_dataset_survives_close() {
9061        let path = temp_path("append_bt2_close");
9062        {
9063            let file = H5File::create(&path).unwrap();
9064            let ds = file
9065                .new_dataset::<i32>()
9066                .shape([0, 3])
9067                .chunk(&[4, 3])
9068                .max_shape(&[None, None])
9069                .create("d")
9070                .unwrap();
9071            // One buffered row, then a batch that crosses the chunk
9072            // boundary: 4 rows fill chunk band 0, one row stays buffered
9073            // for the flush at close.
9074            ds.append(&[1, 2, 3]).unwrap();
9075            ds.append(&(4..=15).collect::<Vec<i32>>()).unwrap();
9076            file.close().unwrap();
9077        }
9078        {
9079            let file = H5File::open(&path).unwrap();
9080            let ds = file.dataset("d").unwrap();
9081            assert_eq!(ds.shape(), vec![5, 3]);
9082            assert_eq!(
9083                ds.read_raw::<i32>().unwrap(),
9084                (1..=15).collect::<Vec<i32>>()
9085            );
9086        }
9087        std::fs::remove_file(&path).ok();
9088    }
9089
9090    /// A chunk row narrower than the frame row is legal geometry (libhdf5
9091    /// creates it); appended frames must be scattered across the row's
9092    /// tiles at the chunk stride, not packed at the frame stride.
9093    #[test]
9094    fn append_scatters_frames_across_narrow_chunk_tiles() {
9095        let path = temp_path("append_narrow_chunks");
9096        {
9097            let file = H5File::create(&path).unwrap();
9098            let ds = file
9099                .new_dataset::<i32>()
9100                .shape([0, 8])
9101                .chunk(&[2, 4])
9102                .max_shape(&[None, Some(8)])
9103                .create("d")
9104                .unwrap();
9105            // 3 rows of 8: rows 0..2 complete chunk band 0 (two tiles),
9106            // row 2 is flushed partial at close.
9107            ds.append(&(0..24).collect::<Vec<i32>>()).unwrap();
9108            file.close().unwrap();
9109        }
9110        {
9111            let file = H5File::open(&path).unwrap();
9112            let ds = file.dataset("d").unwrap();
9113            assert_eq!(ds.shape(), vec![3, 8]);
9114            assert_eq!(ds.read_raw::<i32>().unwrap(), (0..24).collect::<Vec<i32>>());
9115        }
9116        std::fs::remove_file(&path).ok();
9117    }
9118
9119    /// A fixed-array dataset has no room to grow: appending must surface an
9120    /// error naming the chunk grid, not lose rows silently. (Before the
9121    /// index-generic append it failed as "not a chunked dataset".)
9122    #[test]
9123    fn append_to_a_full_fixed_array_dataset_errors() {
9124        let path = temp_path("append_fa_errors");
9125        let file = H5File::create(&path).unwrap();
9126        let ds = file
9127            .new_dataset::<i32>()
9128            .shape([4, 3])
9129            .chunk(&[2, 3])
9130            .create("d")
9131            .unwrap();
9132        let err = ds.append(&(0..6).collect::<Vec<i32>>()).unwrap_err();
9133        assert!(
9134            err.to_string().contains("chunk grid"),
9135            "unexpected error: {err}"
9136        );
9137        file.close().unwrap();
9138        std::fs::remove_file(&path).ok();
9139    }
9140
9141    /// A finite max_shape above the current shape used to be dropped on the
9142    /// fixed-array path: the array was sized from the current dims and the
9143    /// stored dataspace had no maximum, so growth failed. The array is now
9144    /// sized from the maximum's chunk grid (libhdf5 `max_nchunks`), so a
9145    /// fixed-max dataset appends up to its maximum and roundtrips.
9146    #[test]
9147    fn fixed_array_with_a_larger_max_shape_grows_and_survives_close() {
9148        let path = temp_path("fa_growable_dim0");
9149        {
9150            let file = H5File::create(&path).unwrap();
9151            let ds = file
9152                .new_dataset::<i32>()
9153                .shape([4, 3])
9154                .chunk(&[2, 3])
9155                .max_shape(&[Some(10), Some(3)])
9156                .create("d")
9157                .unwrap();
9158            ds.write_raw(&(0..12).collect::<Vec<i32>>()).unwrap();
9159            ds.append(&(12..18).collect::<Vec<i32>>()).unwrap();
9160            file.close().unwrap();
9161        }
9162        {
9163            let file = H5File::open(&path).unwrap();
9164            let ds = file.dataset("d").unwrap();
9165            assert_eq!(ds.shape(), vec![6, 3]);
9166            assert_eq!(ds.read_raw::<i32>().unwrap(), (0..18).collect::<Vec<i32>>());
9167        }
9168        std::fs::remove_file(&path).ok();
9169    }
9170
9171    /// The multiplier-dimension boundary: growing a dimension other than 0
9172    /// changes the current chunk grid but not the index grid. Chunk slots
9173    /// must come from the maximum's grid (libhdf5 `max_down_chunks`), or the
9174    /// chunks written before the extend are looked up under different
9175    /// indices after it.
9176    #[test]
9177    fn fixed_array_growable_inner_dimension_keeps_chunk_slots() {
9178        let path = temp_path("fa_growable_dim1");
9179        {
9180            let file = H5File::create(&path).unwrap();
9181            let ds = file
9182                .new_dataset::<i32>()
9183                .shape([4, 3])
9184                .chunk(&[2, 3])
9185                .max_shape(&[Some(4), Some(9)])
9186                .create("d")
9187                .unwrap();
9188            ds.write_raw(&(0..12).collect::<Vec<i32>>()).unwrap();
9189            ds.extend(&[4, 6]).unwrap();
9190            ds.write_slice(&[0, 3], &[4, 3], &(12..24).collect::<Vec<i32>>())
9191                .unwrap();
9192            file.close().unwrap();
9193        }
9194        {
9195            let file = H5File::open(&path).unwrap();
9196            let ds = file.dataset("d").unwrap();
9197            assert_eq!(ds.shape(), vec![4, 6]);
9198            // Row-major [4,6]: row r is [r*3 .. r*3+3) from the first write
9199            // then [12 + r*3 ..) from the second.
9200            let mut expect = Vec::new();
9201            for r in 0i32..4 {
9202                expect.extend((r * 3)..(r * 3 + 3));
9203                expect.extend((12 + r * 3)..(12 + r * 3 + 3));
9204            }
9205            assert_eq!(ds.read_raw::<i32>().unwrap(), expect);
9206        }
9207        std::fs::remove_file(&path).ok();
9208    }
9209
9210    /// Growth boundaries: past the stored maximum is rejected, and a dataset
9211    /// without a stored maximum is fixed at its extent (libhdf5 defaults
9212    /// maxdims to dims at creation).
9213    #[test]
9214    fn extend_beyond_the_maximum_is_rejected() {
9215        let path = temp_path("extend_beyond_max");
9216        let file = H5File::create(&path).unwrap();
9217        let ds = file
9218            .new_dataset::<i32>()
9219            .shape([4, 3])
9220            .chunk(&[2, 3])
9221            .max_shape(&[Some(6), Some(3)])
9222            .create("d")
9223            .unwrap();
9224        ds.extend(&[6, 3]).unwrap();
9225        let err = ds.extend(&[8, 3]).unwrap_err();
9226        assert!(
9227            err.to_string().contains("exceeds the maximum"),
9228            "unexpected error: {err}"
9229        );
9230        file.close().unwrap();
9231        std::fs::remove_file(&path).ok();
9232    }
9233
9234    /// An unlimited dimension other than 0 has no fixed linear slot without
9235    /// libhdf5's extensible-array swizzling; `chunk_grid::linear_index` now
9236    /// implements that swizzle for any dimension, so this creates cleanly
9237    /// and every extend keeps writing new chunks to new slots, never
9238    /// re-addressing one already on disk.
9239    #[test]
9240    fn builder_accepts_an_unlimited_inner_dimension() {
9241        let path = temp_path("unlimited_inner_dim");
9242        let file = H5File::create(&path).unwrap();
9243        let ds = file
9244            .new_dataset::<i32>()
9245            .shape([4, 0])
9246            .chunk(&[2, 2])
9247            .max_shape(&[Some(4), None])
9248            .create("d")
9249            .unwrap();
9250        assert_eq!(ds.shape(), vec![4, 0]);
9251
9252        // Write, then extend and write again: if the linear index were
9253        // recomputed from the *current* extent instead of the maximum one,
9254        // the second extend would shift every slot number and the first
9255        // write's chunks would decode under the wrong coordinates below.
9256        ds.extend(&[4, 2]).unwrap();
9257        ds.write_slice(&[0, 0], &[4, 2], &[1, 2, 3, 4, 5, 6, 7, 8])
9258            .unwrap();
9259        ds.extend(&[4, 4]).unwrap();
9260        ds.write_slice(&[0, 2], &[4, 2], &[9, 10, 11, 12, 13, 14, 15, 16])
9261            .unwrap();
9262
9263        file.close().unwrap();
9264        let file = H5File::open(&path).unwrap();
9265        let ds = file.dataset("d").unwrap();
9266        assert_eq!(
9267            ds.read_slice::<i32>(&[0, 0], &[4, 4]).unwrap(),
9268            vec![1, 2, 9, 10, 3, 4, 11, 12, 5, 6, 13, 14, 7, 8, 15, 16]
9269        );
9270        std::fs::remove_file(&path).ok();
9271    }
9272
9273    /// Regression: a chunk wider than a fixed max dimension used to be
9274    /// accepted, and appends then packed rows at the chunk stride — writing
9275    /// [1, 2, 3, 4] and reading back [1, 2, 0, 0]. libhdf5 rejects the
9276    /// geometry at create (`H5D__chunk_construct`); so do we now.
9277    #[test]
9278    fn builder_rejects_a_chunk_wider_than_a_fixed_max_dimension() {
9279        let path = temp_path("builder_chunk_wider_than_max");
9280        let file = H5File::create(&path).unwrap();
9281        let err = match file
9282            .new_dataset::<i32>()
9283            .shape([0, 2])
9284            .chunk(&[2, 4])
9285            .max_shape(&[None, Some(2)])
9286            .create("v5")
9287        {
9288            Ok(_) => panic!("create accepted a chunk wider than the fixed max dimension"),
9289            Err(e) => e,
9290        };
9291        assert!(
9292            err.to_string().contains("maximum dimension size"),
9293            "unexpected error: {err}"
9294        );
9295        file.close().unwrap();
9296        std::fs::remove_file(&path).ok();
9297    }
9298
9299    /// Boundary: `fits in T` vs `does not fit in T`, for both the too-large
9300    /// (u64::MAX → i64) and the negative-to-unsigned (−1 → u32) directions.
9301    #[test]
9302    fn numeric_int_checked_conversion_boundaries() {
9303        let path = temp_path("numeric_int_bounds");
9304        {
9305            let file = H5File::create(&path).unwrap();
9306            let ds = file.new_dataset::<u64>().shape([2]).create("u").unwrap();
9307            ds.write_raw(&[1u64, u64::MAX]).unwrap();
9308            let ds = file.new_dataset::<i32>().shape([2]).create("i").unwrap();
9309            ds.write_raw(&[-1i32, 5]).unwrap();
9310            file.close().unwrap();
9311        }
9312        let file = H5File::open(&path).unwrap();
9313
9314        let u = file.dataset("u").unwrap();
9315        assert_eq!(u.read_numeric_as::<u64>().unwrap(), vec![1, u64::MAX]);
9316        assert_eq!(
9317            u.read_numeric_as::<i128>().unwrap(),
9318            vec![1, i128::from(u64::MAX)]
9319        );
9320        let err = u.read_numeric_as::<i64>().unwrap_err();
9321        assert!(
9322            err.to_string()
9323                .contains("value 18446744073709551615 at element 1 does not fit in i64"),
9324            "unexpected error: {err}"
9325        );
9326
9327        let i = file.dataset("i").unwrap();
9328        assert_eq!(i.read_numeric_as::<i64>().unwrap(), vec![-1, 5]);
9329        let err = i.read_numeric_as::<u32>().unwrap_err();
9330        assert!(
9331            err.to_string()
9332                .contains("value -1 at element 0 does not fit in u32"),
9333            "unexpected error: {err}"
9334        );
9335        std::fs::remove_file(&path).ok();
9336    }
9337
9338    /// Boundary: f32 → f64 is exact widening; f64 → f32 is rejected.
9339    #[test]
9340    fn numeric_float_widening_only() {
9341        let path = temp_path("numeric_float");
9342        {
9343            let file = H5File::create(&path).unwrap();
9344            let ds = file.new_dataset::<f32>().shape([2]).create("f4").unwrap();
9345            ds.write_raw(&[1.5f32, -2.25]).unwrap();
9346            let ds = file.new_dataset::<f64>().shape([1]).create("f8").unwrap();
9347            ds.write_raw(&[3.75f64]).unwrap();
9348            file.close().unwrap();
9349        }
9350        let file = H5File::open(&path).unwrap();
9351
9352        let f4 = file.dataset("f4").unwrap();
9353        assert_eq!(f4.read_numeric_as::<f32>().unwrap(), vec![1.5, -2.25]);
9354        assert_eq!(f4.read_numeric_as::<f64>().unwrap(), vec![1.5, -2.25]);
9355
9356        let f8 = file.dataset("f8").unwrap();
9357        assert_eq!(f8.read_numeric_as::<f64>().unwrap(), vec![3.75]);
9358        let err = f8.read_numeric_as::<f32>().unwrap_err();
9359        assert!(
9360            err.to_string().contains("narrowing"),
9361            "unexpected error: {err}"
9362        );
9363        std::fs::remove_file(&path).ok();
9364    }
9365
9366    /// Boundary: cross-class conversions (float ↔ integer) are rejected in
9367    /// both directions, and a non-numeric datatype is rejected at classify.
9368    #[test]
9369    fn numeric_cross_class_and_non_numeric_rejected() {
9370        let path = temp_path("numeric_cross_class");
9371        {
9372            let file = H5File::create(&path).unwrap();
9373            let ds = file.new_dataset::<f64>().shape([1]).create("f8").unwrap();
9374            ds.write_raw(&[1.0f64]).unwrap();
9375            let ds = file.new_dataset::<i32>().shape([1]).create("i4").unwrap();
9376            ds.write_raw(&[7i32]).unwrap();
9377            file.write_vlen_strings("s", &["a", "b"]).unwrap();
9378            file.close().unwrap();
9379        }
9380        let file = H5File::open(&path).unwrap();
9381
9382        let err = file
9383            .dataset("f8")
9384            .unwrap()
9385            .read_numeric_as::<i64>()
9386            .unwrap_err();
9387        assert!(
9388            err.to_string().contains("floating-point dataset as i64"),
9389            "unexpected error: {err}"
9390        );
9391        let err = file
9392            .dataset("i4")
9393            .unwrap()
9394            .read_numeric_as::<f64>()
9395            .unwrap_err();
9396        assert!(
9397            err.to_string().contains("integer dataset as f64"),
9398            "unexpected error: {err}"
9399        );
9400        let err = file
9401            .dataset("s")
9402            .unwrap()
9403            .read_numeric_as::<i64>()
9404            .unwrap_err();
9405        assert!(
9406            err.to_string().contains("is not numeric"),
9407            "unexpected error: {err}"
9408        );
9409        std::fs::remove_file(&path).ok();
9410    }
9411
9412    /// Boundary: big-endian sources decode per the datatype's byte order.
9413    /// Unit-level (the writer only emits little-endian): feed `convert` a
9414    /// big-endian datatype plus big-endian bytes directly.
9415    #[test]
9416    fn numeric_big_endian_decode() {
9417        use super::numeric;
9418        use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
9419
9420        let dt = DatatypeMessage::FixedPoint {
9421            size: 4,
9422            byte_order: ByteOrder::BigEndian,
9423            signed: true,
9424            bit_offset: 0,
9425            bit_precision: 32,
9426        };
9427        let mut raw = Vec::new();
9428        raw.extend_from_slice(&(-2i32).to_be_bytes());
9429        raw.extend_from_slice(&(100_000i32).to_be_bytes());
9430        let kind = numeric::classify(&dt).unwrap();
9431        assert_eq!(
9432            numeric::convert::<i64>(kind, &raw).unwrap(),
9433            vec![-2, 100_000]
9434        );
9435
9436        let dt = DatatypeMessage::FloatingPoint {
9437            size: 8,
9438            byte_order: ByteOrder::BigEndian,
9439            sign_location: 63,
9440            bit_offset: 0,
9441            bit_precision: 64,
9442            exponent_location: 52,
9443            exponent_size: 11,
9444            mantissa_location: 0,
9445            mantissa_size: 52,
9446            exponent_bias: 1023,
9447        };
9448        let raw = (-2.25f64).to_be_bytes();
9449        let kind = numeric::classify(&dt).unwrap();
9450        assert_eq!(numeric::convert::<f64>(kind, &raw).unwrap(), vec![-2.25]);
9451    }
9452
9453    /// `H5Attribute::read_numeric` validates the stored datatype before
9454    /// reinterpreting bytes: cross-width, cross-class, and non-numeric
9455    /// attributes error instead of returning bit-garbage, while the exact
9456    /// type and the HBool / complex-compound paths keep working.
9457    #[test]
9458    fn attr_read_numeric_validates_datatype() {
9459        use crate::types::{Complex64, HBool, VarLenUnicode};
9460        let path = temp_path("attr_read_numeric_validate");
9461        {
9462            let file = H5File::create(&path).unwrap();
9463            let ds = file.new_dataset::<f32>().shape([2]).create("d").unwrap();
9464            ds.write_raw(&[1.0f32; 2]).unwrap();
9465            let a = ds.new_attr::<f64>().shape(()).create("f8").unwrap();
9466            a.write_numeric(&1.5f64).unwrap();
9467            let a = ds.new_attr::<i32>().shape(()).create("i4").unwrap();
9468            a.write_numeric(&-7i32).unwrap();
9469            let a = ds.new_attr::<HBool>().shape(()).create("b").unwrap();
9470            a.write_numeric(&HBool::from(true)).unwrap();
9471            let a = ds.new_attr::<Complex64>().shape(()).create("z").unwrap();
9472            a.write_numeric(&Complex64 { re: 1.0, im: -2.0 }).unwrap();
9473            let a = ds
9474                .new_attr::<VarLenUnicode>()
9475                .shape(())
9476                .create("s")
9477                .unwrap();
9478            a.write_scalar(&VarLenUnicode("text".into())).unwrap();
9479            file.close().unwrap();
9480        }
9481        let file = H5File::open(&path).unwrap();
9482        let ds = file.dataset("d").unwrap();
9483
9484        let f8 = ds.attr("f8").unwrap();
9485        assert_eq!(f8.read_numeric::<f64>().unwrap(), 1.5);
9486        // Previously returned the low half of the f64 image as an f32.
9487        let err = f8.read_numeric::<f32>().unwrap_err();
9488        assert!(
9489            err.to_string().contains("read_numeric_as"),
9490            "unexpected error: {err}"
9491        );
9492        assert!(f8.read_numeric::<i64>().is_err());
9493
9494        let i4 = ds.attr("i4").unwrap();
9495        assert_eq!(i4.read_numeric::<i32>().unwrap(), -7);
9496        assert!(i4.read_numeric::<u32>().is_err());
9497
9498        assert!(bool::from(
9499            ds.attr("b").unwrap().read_numeric::<HBool>().unwrap()
9500        ));
9501        let z = ds.attr("z").unwrap().read_numeric::<Complex64>().unwrap();
9502        assert_eq!((z.re, z.im), (1.0, -2.0));
9503
9504        // A vlen string attribute: read_numeric used to transmute the heap
9505        // reference bytes into the requested type.
9506        let s = ds.attr("s").unwrap();
9507        assert!(s.read_numeric::<f64>().is_err());
9508        assert!(s.read_numeric_as::<f64>().is_err());
9509        std::fs::remove_file(&path).ok();
9510    }
9511
9512    /// The attribute conversion read applies the dataset rules: checked
9513    /// int → int naming index and value on overflow, widening-only floats,
9514    /// cross-class rejected; an array attribute converts every element.
9515    #[test]
9516    fn attr_read_numeric_as_converts() {
9517        let path = temp_path("attr_read_numeric_as");
9518        {
9519            let file = H5File::create(&path).unwrap();
9520            let ds = file.new_dataset::<f32>().shape([2]).create("d").unwrap();
9521            ds.write_raw(&[1.0f32; 2]).unwrap();
9522            let a = ds.new_attr::<i32>().shape(()).create("i4").unwrap();
9523            a.write_numeric(&-7i32).unwrap();
9524            let a = ds.new_attr::<u64>().shape(()).create("u8max").unwrap();
9525            a.write_numeric(&u64::MAX).unwrap();
9526            let a = ds.new_attr::<i16>().shape([3]).create("arr").unwrap();
9527            a.write_array(&[1i16, -2, 3]).unwrap();
9528            file.close().unwrap();
9529        }
9530        let file = H5File::open(&path).unwrap();
9531        let ds = file.dataset("d").unwrap();
9532        assert_eq!(
9533            ds.attr("i4").unwrap().read_numeric_as::<i64>().unwrap(),
9534            vec![-7]
9535        );
9536        let err = ds
9537            .attr("u8max")
9538            .unwrap()
9539            .read_numeric_as::<i64>()
9540            .unwrap_err();
9541        assert!(
9542            err.to_string().contains("does not fit in i64"),
9543            "unexpected error: {err}"
9544        );
9545        assert_eq!(
9546            ds.attr("arr").unwrap().read_numeric_as::<i32>().unwrap(),
9547            vec![1, -2, 3]
9548        );
9549        assert!(ds.attr("i4").unwrap().read_numeric_as::<f64>().is_err());
9550        std::fs::remove_file(&path).ok();
9551    }
9552
9553    /// The hyperslab variant applies the same conversion to a sub-selection.
9554    #[test]
9555    fn numeric_slice_conversion() {
9556        let path = temp_path("numeric_slice");
9557        {
9558            let file = H5File::create(&path).unwrap();
9559            let ds = file.new_dataset::<i16>().shape([2, 3]).create("m").unwrap();
9560            ds.write_raw(&[1i16, 2, 3, 4, 5, 6]).unwrap();
9561            file.close().unwrap();
9562        }
9563        let file = H5File::open(&path).unwrap();
9564        let m = file.dataset("m").unwrap();
9565        assert_eq!(
9566            m.read_numeric_slice_as::<i32>(&[0, 1], &[2, 2]).unwrap(),
9567            vec![2, 3, 5, 6]
9568        );
9569        std::fs::remove_file(&path).ok();
9570    }
9571
9572    /// A NULL dataspace round-trips through the public API as `is_null() ==
9573    /// true`, `shape() == []`, and `read_raw_bytes()` empty — and stays
9574    /// distinguishable from a scalar dataset, which shares the same empty
9575    /// `shape()` but holds exactly one element.
9576    #[test]
9577    fn null_dataspace_distinct_from_scalar() {
9578        let path = temp_path("null_vs_scalar");
9579        {
9580            let file = H5File::create(&path).unwrap();
9581            file.new_dataset::<i32>().null().create("empty").unwrap();
9582            let scalar = file.new_dataset::<i32>().scalar().create("scalar").unwrap();
9583            scalar.write_raw(&[42i32]).unwrap();
9584            file.close().unwrap();
9585        }
9586        let file = H5File::open(&path).unwrap();
9587
9588        let empty = file.dataset("empty").unwrap();
9589        assert!(empty.is_null());
9590        assert_eq!(empty.shape(), Vec::<usize>::new());
9591        assert_eq!(empty.total_elements(), 0);
9592        assert_eq!(empty.read_raw_bytes().unwrap(), Vec::<u8>::new());
9593
9594        let scalar = file.dataset("scalar").unwrap();
9595        assert!(!scalar.is_null());
9596        assert_eq!(scalar.shape(), Vec::<usize>::new());
9597        assert_eq!(scalar.total_elements(), 1);
9598        assert_eq!(scalar.read_raw::<i32>().unwrap(), vec![42]);
9599
9600        std::fs::remove_file(&path).ok();
9601    }
9602
9603    /// A NULL dataspace dataset rejects writes outright — there is nothing
9604    /// to write into — rather than silently accepting a scalar-shaped
9605    /// write against unallocated storage.
9606    #[test]
9607    fn null_dataspace_rejects_writes() {
9608        let path = temp_path("null_write_rejected");
9609        let file = H5File::create(&path).unwrap();
9610        let ds = file.new_dataset::<i32>().null().create("empty").unwrap();
9611        assert!(ds.write_raw(&[1i32]).is_err());
9612        assert!(ds.write_raw_bytes(&[0u8; 4]).is_err());
9613        file.close().unwrap();
9614        std::fs::remove_file(&path).ok();
9615    }
9616
9617    /// `.null()` combined with `.chunk()` or a fill value is rejected at
9618    /// `create()` rather than silently dropping the conflicting option —
9619    /// a NULL dataspace can never be chunked or filtered upstream.
9620    #[test]
9621    fn null_dataspace_rejects_chunking_and_fill_value() {
9622        let path = temp_path("null_chunk_rejected");
9623        let file = H5File::create(&path).unwrap();
9624        assert!(file
9625            .new_dataset::<i32>()
9626            .null()
9627            .chunk(&[4])
9628            .create("a")
9629            .is_err());
9630        assert!(file
9631            .new_dataset::<i32>()
9632            .null()
9633            .fill_value(7i32)
9634            .create("b")
9635            .is_err());
9636        file.close().unwrap();
9637        std::fs::remove_file(&path).ok();
9638    }
9639
9640    /// A committed type is resolved before the dataset is created, so a name
9641    /// that is not one — or one paired with object references, which would
9642    /// make the stored type disagree with the payload — leaves no dataset
9643    /// behind.
9644    #[test]
9645    fn a_committed_type_that_cannot_be_shared_creates_no_dataset() {
9646        use crate::format::messages::datatype::DatatypeMessage;
9647
9648        let path = temp_path("committed_refused");
9649        let file = H5File::create(&path).unwrap();
9650        file.commit_datatype("t", DatatypeMessage::i32_type())
9651            .unwrap();
9652
9653        assert!(file
9654            .new_dataset::<i32>()
9655            .committed_type("absent")
9656            .shape([2usize])
9657            .create("a")
9658            .is_err());
9659        assert!(file
9660            .new_dataset::<u64>()
9661            .committed_type("t")
9662            .object_references()
9663            .shape([2usize])
9664            .create("b")
9665            .is_err());
9666        // A dataset already exists under that name, so the type cannot take
9667        // it either.
9668        file.new_dataset::<i32>()
9669            .shape([2usize])
9670            .create("taken")
9671            .unwrap();
9672        assert!(file
9673            .commit_datatype("taken", DatatypeMessage::i32_type())
9674            .is_err());
9675        assert!(file
9676            .commit_datatype("t", DatatypeMessage::f64_type())
9677            .is_err());
9678
9679        assert_eq!(file.dataset_names(), vec!["taken".to_string()]);
9680        assert_eq!(file.named_datatype_names(), vec!["t".to_string()]);
9681        file.close().unwrap();
9682        std::fs::remove_file(&path).ok();
9683    }
9684
9685    /// Deleting the group that named a committed datatype takes the name with
9686    /// it: nothing in the file reaches the type, so it is not written, the
9687    /// name is free again, and it can no longer be shared by that name.
9688    #[test]
9689    fn deleting_a_group_takes_the_committed_datatypes_it_named() {
9690        use crate::format::messages::datatype::DatatypeMessage;
9691
9692        let path = temp_path("committed_group_deleted");
9693        {
9694            let file = H5File::create(&path).unwrap();
9695            let types = file.create_group("types").unwrap();
9696            types
9697                .commit_datatype("t", DatatypeMessage::i32_type())
9698                .unwrap();
9699            assert_eq!(file.named_datatype_names(), vec!["types/t".to_string()]);
9700
9701            file.delete_group("types").unwrap();
9702            assert!(file.named_datatype_names().is_empty());
9703            assert!(file
9704                .new_dataset::<i32>()
9705                .committed_type("types/t")
9706                .shape([2usize])
9707                .create("d")
9708                .is_err());
9709
9710            // The name is free, so a new group may take it back.
9711            let types = file.create_group("types").unwrap();
9712            types
9713                .commit_datatype("t", DatatypeMessage::f64_type())
9714                .unwrap();
9715            file.close().unwrap();
9716        }
9717        let file = H5File::open(&path).unwrap();
9718        assert_eq!(file.named_datatype_names(), vec!["types/t".to_string()]);
9719        assert_eq!(
9720            file.named_datatype("types/t").unwrap().datatype().unwrap(),
9721            crate::format::messages::datatype::DatatypeMessage::f64_type()
9722        );
9723        drop(file);
9724        std::fs::remove_file(&path).ok();
9725    }
9726
9727    /// The layout class the reader sees for `name`, plus the image a compact
9728    /// layout carries — what distinguishes compact storage from contiguous
9729    /// storage that happens to hold the same bytes.
9730    fn compact_image(path: &std::path::Path, name: &str) -> Option<Vec<u8>> {
9731        use crate::format::messages::data_layout::DataLayoutMessage;
9732        let mut reader = crate::io::reader::Hdf5Reader::open(path).unwrap();
9733        match &reader.dataset_info(name).unwrap().layout {
9734            DataLayoutMessage::Compact { data } => Some(data.clone()),
9735            other => panic!("{name}: expected a compact layout, got {other:?}"),
9736        }
9737    }
9738
9739    /// `.compact()` puts the raw data inside the data layout message: the
9740    /// dataset has no data block of its own, and the image the layout carries
9741    /// is what a read returns.
9742    #[test]
9743    fn a_compact_dataset_stores_its_data_in_the_layout_message() {
9744        let path = temp_path("compact_roundtrip");
9745        let values: Vec<i32> = (0..16).collect();
9746        {
9747            let file = H5File::create(&path).unwrap();
9748            file.new_dataset::<i32>()
9749                .shape([16usize])
9750                .compact()
9751                .create("d")
9752                .unwrap()
9753                .write_raw(&values)
9754                .unwrap();
9755            file.close().unwrap();
9756        }
9757
9758        let image = compact_image(&path, "d").unwrap();
9759        assert_eq!(
9760            image,
9761            values
9762                .iter()
9763                .flat_map(|v| v.to_le_bytes())
9764                .collect::<Vec<_>>()
9765        );
9766
9767        let file = H5File::open(&path).unwrap();
9768        let ds = file.dataset("d").unwrap();
9769        assert_eq!(ds.shape(), vec![16]);
9770        assert_eq!(ds.read_raw::<i32>().unwrap(), values);
9771        std::fs::remove_file(&path).ok();
9772    }
9773
9774    /// A compact dataset created inside a group is linked from that group,
9775    /// not from the root: its create path goes through the same parent
9776    /// resolution every other layout uses.
9777    #[test]
9778    fn a_compact_dataset_lands_in_its_group() {
9779        let path = temp_path("compact_in_group");
9780        {
9781            let file = H5File::create(&path).unwrap();
9782            let g = file.root_group().create_group("g").unwrap();
9783            g.new_dataset::<u8>()
9784                .shape([4usize])
9785                .compact()
9786                .create("d")
9787                .unwrap()
9788                .write_raw(&[1u8, 2, 3, 4])
9789                .unwrap();
9790            file.close().unwrap();
9791        }
9792        assert_eq!(compact_image(&path, "/g/d").unwrap(), vec![1u8, 2, 3, 4]);
9793
9794        let file = H5File::open(&path).unwrap();
9795        assert_eq!(
9796            file.dataset("/g/d").unwrap().read_raw::<u8>().unwrap(),
9797            vec![1u8, 2, 3, 4]
9798        );
9799        std::fs::remove_file(&path).ok();
9800    }
9801
9802    /// A compact dataset's storage is the image itself, so the fill value has
9803    /// to be tiled into it at create — `H5D__compact_fill`'s job. An unwritten
9804    /// element must read back as the fill value, not as zero.
9805    #[test]
9806    fn an_unwritten_compact_dataset_reads_back_as_its_fill_value() {
9807        let path = temp_path("compact_fill");
9808        {
9809            let file = H5File::create(&path).unwrap();
9810            file.new_dataset::<i32>()
9811                .shape([4usize])
9812                .compact()
9813                .fill_value(-7i32)
9814                .create("d")
9815                .unwrap();
9816            file.close().unwrap();
9817        }
9818        let file = H5File::open(&path).unwrap();
9819        assert_eq!(
9820            file.dataset("d").unwrap().read_raw::<i32>().unwrap(),
9821            vec![-7i32; 4]
9822        );
9823        std::fs::remove_file(&path).ok();
9824    }
9825
9826    /// The image is the layout message, so anything that rewrites the header
9827    /// rewrites the data with it. Reopening and attaching an attribute makes
9828    /// the header stale; the rebuilt one must still carry the image rather
9829    /// than fall back to an unallocated contiguous layout.
9830    #[test]
9831    fn a_reopened_compact_dataset_keeps_its_image() {
9832        let path = temp_path("compact_reopen");
9833        let values: Vec<i32> = (100..108).collect();
9834        {
9835            let file = H5File::create(&path).unwrap();
9836            file.new_dataset::<i32>()
9837                .shape([8usize])
9838                .compact()
9839                .create("d")
9840                .unwrap()
9841                .write_raw(&values)
9842                .unwrap();
9843            file.close().unwrap();
9844        }
9845        {
9846            let file = H5File::open_rw(&path).unwrap();
9847            file.dataset_writer("d")
9848                .unwrap()
9849                .new_attr::<i32>()
9850                .shape(())
9851                .create("note")
9852                .unwrap()
9853                .write_numeric(&1i32)
9854                .unwrap();
9855            file.close().unwrap();
9856        }
9857        assert_eq!(
9858            compact_image(&path, "d").unwrap(),
9859            values
9860                .iter()
9861                .flat_map(|v| v.to_le_bytes())
9862                .collect::<Vec<_>>()
9863        );
9864
9865        let file = H5File::open(&path).unwrap();
9866        assert_eq!(
9867            file.dataset("d").unwrap().read_raw::<i32>().unwrap(),
9868            values
9869        );
9870        std::fs::remove_file(&path).ok();
9871    }
9872
9873    /// The ceiling is what a data layout message can hold, so it is checked
9874    /// in bytes and names them: the largest image that fits is accepted and
9875    /// one element more is refused.
9876    #[test]
9877    fn the_compact_ceiling_is_checked_in_bytes() {
9878        let path = temp_path("compact_ceiling");
9879        let file = H5File::create(&path).unwrap();
9880
9881        let fits = crate::MAX_COMPACT_DATA / 4;
9882        file.new_dataset::<i32>()
9883            .shape([fits])
9884            .compact()
9885            .create("fits")
9886            .unwrap();
9887
9888        let over = fits + 1;
9889        let err = match file
9890            .new_dataset::<i32>()
9891            .shape([over])
9892            .compact()
9893            .create("over")
9894        {
9895            Err(e) => e.to_string(),
9896            Ok(_) => panic!("an image {} bytes wide must be refused", over * 4),
9897        };
9898        assert!(
9899            err.contains(&(over * 4).to_string())
9900                && err.contains(&crate::MAX_COMPACT_DATA.to_string()),
9901            "the error must name both sizes: {err}"
9902        );
9903
9904        file.close().unwrap();
9905        std::fs::remove_file(&path).ok();
9906    }
9907
9908    /// Compact storage has no chunk grid to filter and no room to grow, so
9909    /// each conflicting option is refused at `create()` rather than silently
9910    /// overriding the layout the way `H5Pset_chunk` does.
9911    #[test]
9912    fn compact_rejects_chunking_filters_growth_and_a_null_dataspace() {
9913        let path = temp_path("compact_rejects");
9914        let file = H5File::create(&path).unwrap();
9915        assert!(file
9916            .new_dataset::<i32>()
9917            .shape([4usize])
9918            .compact()
9919            .chunk(&[4])
9920            .create("a")
9921            .is_err());
9922        assert!(file
9923            .new_dataset::<i32>()
9924            .shape([4usize])
9925            .compact()
9926            .deflate(4)
9927            .create("b")
9928            .is_err());
9929        assert!(file
9930            .new_dataset::<i32>()
9931            .shape([4usize])
9932            .compact()
9933            .max_shape(&[None])
9934            .create("c")
9935            .is_err());
9936        assert!(file
9937            .new_dataset::<i32>()
9938            .shape([4usize])
9939            .compact()
9940            .max_shape(&[Some(8)])
9941            .create("d")
9942            .is_err());
9943        assert!(file
9944            .new_dataset::<i32>()
9945            .null()
9946            .compact()
9947            .create("e")
9948            .is_err());
9949        file.close().unwrap();
9950        std::fs::remove_file(&path).ok();
9951    }
9952
9953    /// The filter pipeline the reader decodes from `name`'s header.
9954    fn stored_pipeline(
9955        path: &std::path::Path,
9956        name: &str,
9957    ) -> crate::format::messages::filter::FilterPipeline {
9958        let mut reader = crate::io::reader::Hdf5Reader::open(path).unwrap();
9959        reader
9960            .dataset_info(name)
9961            .unwrap()
9962            .filter_pipeline
9963            .clone()
9964            .unwrap_or_else(|| panic!("{name}: no filter pipeline"))
9965    }
9966
9967    /// Shuffle is a permutation, not a compressor, so it is a pipeline on its
9968    /// own — `H5Pset_shuffle` with nothing behind it. Its stage must reach the
9969    /// header, and the reader must unpermute what it wrote.
9970    #[test]
9971    fn shuffle_alone_is_a_filter_pipeline() {
9972        use crate::format::messages::filter::{FilterPipeline, FILTER_SHUFFLE};
9973        let path = temp_path("shuffle_alone");
9974        let values: Vec<i32> = (0..64).collect();
9975        {
9976            let file = H5File::create(&path).unwrap();
9977            file.new_dataset::<i32>()
9978                .shape([64usize])
9979                .chunk(&[16])
9980                .shuffle()
9981                .create("d")
9982                .unwrap()
9983                .write_raw(&values)
9984                .unwrap();
9985            file.close().unwrap();
9986        }
9987        let pipeline = stored_pipeline(&path, "d");
9988        assert_eq!(pipeline, FilterPipeline::shuffle(4));
9989        assert_eq!(pipeline.filters[0].id, FILTER_SHUFFLE);
9990
9991        let file = H5File::open(&path).unwrap();
9992        assert_eq!(
9993            file.dataset("d").unwrap().read_raw::<i32>().unwrap(),
9994            values
9995        );
9996        std::fs::remove_file(&path).ok();
9997    }
9998
9999    /// `.shuffle()` and `.deflate()` are separate stages that compose, and
10000    /// `.shuffle_deflate()` is the shorthand for both — one pipeline, built
10001    /// once, whichever way it was asked for.
10002    #[cfg(feature = "deflate")]
10003    #[test]
10004    fn shuffle_composes_with_deflate() {
10005        let path = temp_path("shuffle_then_deflate");
10006        let values: Vec<i32> = (0..64).collect();
10007        {
10008            let file = H5File::create(&path).unwrap();
10009            for (name, ds) in [
10010                ("split", file.new_dataset::<i32>().shuffle().deflate(6)),
10011                ("combined", file.new_dataset::<i32>().shuffle_deflate(6)),
10012            ] {
10013                ds.shape([64usize])
10014                    .chunk(&[16])
10015                    .create(name)
10016                    .unwrap()
10017                    .write_raw(&values)
10018                    .unwrap();
10019            }
10020            file.close().unwrap();
10021        }
10022        assert_eq!(
10023            stored_pipeline(&path, "split"),
10024            crate::format::messages::filter::FilterPipeline::shuffle_deflate(4, 6)
10025        );
10026        assert_eq!(
10027            stored_pipeline(&path, "split"),
10028            stored_pipeline(&path, "combined")
10029        );
10030
10031        let file = H5File::open(&path).unwrap();
10032        for name in ["split", "combined"] {
10033            assert_eq!(
10034                file.dataset(name).unwrap().read_raw::<i32>().unwrap(),
10035                values,
10036                "{name}"
10037            );
10038        }
10039        std::fs::remove_file(&path).ok();
10040    }
10041
10042    /// The width shuffle permutes by is the stored element's, which a
10043    /// `datatype` override moves away from the carrier type `T`: recording
10044    /// `T`'s width would permute a 4-byte element as four 1-byte ones and
10045    /// hand libhdf5 a chunk it unshuffles into different bytes.
10046    #[test]
10047    fn shuffle_records_the_stored_element_width() {
10048        use crate::format::messages::datatype::DatatypeMessage;
10049        use crate::format::messages::filter::FilterPipeline;
10050        let path = temp_path("shuffle_override_width");
10051        let bytes: Vec<u8> = (0..16u8).collect();
10052        {
10053            let file = H5File::create(&path).unwrap();
10054            file.new_dataset::<u8>()
10055                .datatype(DatatypeMessage::i32_type())
10056                .shape([4usize])
10057                .chunk(&[4])
10058                .shuffle()
10059                .create("d")
10060                .unwrap()
10061                .write_raw_bytes(&bytes)
10062                .unwrap();
10063            file.close().unwrap();
10064        }
10065        assert_eq!(stored_pipeline(&path, "d"), FilterPipeline::shuffle(4));
10066
10067        let file = H5File::open(&path).unwrap();
10068        assert_eq!(
10069            file.dataset("d").unwrap().read_raw::<i32>().unwrap(),
10070            bytes
10071                .chunks(4)
10072                .map(|c| i32::from_le_bytes(c.try_into().unwrap()))
10073                .collect::<Vec<_>>()
10074        );
10075        std::fs::remove_file(&path).ok();
10076    }
10077
10078    /// A write into a virtual dataset is refused by name rather than landing
10079    /// somewhere no reader would look: libhdf5 pushes such a write through
10080    /// the mapping into the source dataset (`H5D__virtual_write`), which this
10081    /// writer does not do.
10082    #[test]
10083    fn a_virtual_dataset_refuses_every_write() {
10084        use crate::Selection;
10085        let path = temp_path("vds_write_refused");
10086        let file = H5File::create(&path).unwrap();
10087        let ds = file
10088            .new_dataset::<i32>()
10089            .shape([16usize])
10090            .virtual_mapping(Selection::All, "src.h5", "src", Selection::All)
10091            .create("vds")
10092            .unwrap();
10093        for err in [
10094            ds.write_raw(&(0..16i32).collect::<Vec<_>>()).unwrap_err(),
10095            ds.write_slice(&[0], &[2], &[1i32, 2]).unwrap_err(),
10096        ] {
10097            let msg = err.to_string();
10098            assert!(msg.contains("virtual dataset"), "{msg}");
10099        }
10100        file.close().unwrap();
10101        std::fs::remove_file(&path).ok();
10102    }
10103
10104    /// A `%b` substitution only means something when the virtual selection is
10105    /// unlimited and the source selection is not: that is the shape where
10106    /// each block draws from a different source dataset. On any other mapping
10107    /// there is only one block, so `H5D_virtual_check_mapping_post` refuses
10108    /// the specifier — and an illegal conversion is refused wherever it
10109    /// appears.
10110    #[test]
10111    fn a_printf_source_name_needs_the_mapping_shape_that_uses_it() {
10112        use crate::Selection;
10113        let path = temp_path("vds_printf");
10114        let file = H5File::create(&path).unwrap();
10115        for (f, d) in [("src_%b.h5", "src"), ("src.h5", "block_%b")] {
10116            let err = match file
10117                .new_dataset::<i32>()
10118                .shape([16usize])
10119                .virtual_mapping(Selection::All, f, d, Selection::All)
10120                .create("vds")
10121            {
10122                Ok(_) => panic!("a bounded mapping has one block, so %b names nothing"),
10123                Err(e) => e.to_string(),
10124            };
10125            assert!(err.contains("printf specifier"), "{err}");
10126        }
10127        // `%z` is not a conversion libhdf5 has, in any mapping shape.
10128        let err = match file
10129            .new_dataset::<i32>()
10130            .shape([1usize, 2])
10131            .max_shape(&[None, Some(2)])
10132            .virtual_mapping(unlimited_rows(), "src_%z.h5", "src", Selection::All)
10133            .create("vds_bad")
10134        {
10135            Ok(_) => panic!("%z is not a legal conversion"),
10136            Err(e) => e.to_string(),
10137        };
10138        assert!(err.contains("invalid format specifier"), "{err}");
10139        file.close().unwrap();
10140        std::fs::remove_file(&path).ok();
10141    }
10142
10143    /// A printf mapping stitches one source dataset per block of its
10144    /// unlimited virtual selection, and the extent stops at the first block
10145    /// with no source (`H5D__virtual_set_extent_unlim`'s printf arm, at the
10146    /// default `H5D_VDS_LAST_AVAILABLE` view and `printf_gap` 0).
10147    #[test]
10148    fn a_printf_mapping_stitches_one_source_per_block() {
10149        use crate::Selection;
10150        let path = temp_path("vds_printf_blocks");
10151        {
10152            let file = H5File::create(&path).unwrap();
10153            for (b, base) in [(0, 0i32), (1, 100), (3, 300)] {
10154                file.new_dataset::<i32>()
10155                    .shape([2usize])
10156                    .create(&format!("block{b}"))
10157                    .unwrap()
10158                    .write_raw(&[base, base + 1])
10159                    .unwrap();
10160            }
10161            file.new_dataset::<i32>()
10162                .shape([1usize, 2])
10163                .max_shape(&[None, Some(2)])
10164                .virtual_mapping(unlimited_rows(), ".", "block%b", Selection::All)
10165                .create("vds")
10166                .unwrap();
10167            file.close().unwrap();
10168        }
10169        let file = H5File::open(&path).unwrap();
10170        let ds = file.dataset("vds").unwrap();
10171        // `block2` is missing, so `block3` is never reached: two rows.
10172        assert_eq!(ds.shape(), vec![2, 2]);
10173        assert_eq!(ds.read_raw::<i32>().unwrap(), vec![0, 1, 100, 101]);
10174        drop(file);
10175        std::fs::remove_file(&path).ok();
10176    }
10177
10178    /// A mapping whose source cannot be opened reads back as the fill value,
10179    /// not as an error: `H5D__virtual_open_source_dset` accepts a null source
10180    /// file and clears the error stack for a missing source dataset
10181    /// (H5Dvirtual.c:877-909), so `H5D__virtual_read_one` finds no projected
10182    /// memory space and reads nothing for it (H5Dvirtual.c:2661-2665).
10183    #[test]
10184    fn a_source_that_cannot_be_opened_reads_as_the_fill_value() {
10185        use crate::{Hyperslab, HyperslabBlock, Selection};
10186        let block = |start: u64, end: u64| Selection::Hyperslab {
10187            rank: 1,
10188            form: Hyperslab::Blocks(vec![HyperslabBlock {
10189                start: vec![start],
10190                end: vec![end],
10191            }]),
10192        };
10193        let path = temp_path("vds_absent_source");
10194        {
10195            let file = H5File::create(&path).unwrap();
10196            file.new_dataset::<i32>()
10197                .shape([4usize])
10198                .create("here")
10199                .unwrap()
10200                .write_raw(&[1i32, 2, 3, 4])
10201                .unwrap();
10202            file.new_dataset::<i32>()
10203                .shape([12usize])
10204                .fill_value(-3i32)
10205                .virtual_mapping(block(0, 3), ".", "here", block(0, 3))
10206                // A dataset that is not in this file.
10207                .virtual_mapping(block(4, 7), ".", "absent", block(0, 3))
10208                // A file that does not exist beside this one.
10209                .virtual_mapping(block(8, 11), "no_such_vds_source.h5", "src", block(0, 3))
10210                .create("vds")
10211                .unwrap();
10212            file.close().unwrap();
10213        }
10214        let file = H5File::open(&path).unwrap();
10215        let ds = file.dataset("vds").unwrap();
10216        assert_eq!(
10217            ds.read_raw::<i32>().unwrap(),
10218            vec![1, 2, 3, 4, -3, -3, -3, -3, -3, -3, -3, -3]
10219        );
10220        // The same rule on the slice path, which stitches the whole image
10221        // before extracting the region.
10222        assert_eq!(
10223            ds.read_slice::<i32>(&[2], &[6]).unwrap(),
10224            vec![3, 4, -3, -3, -3, -3]
10225        );
10226        drop(file);
10227        std::fs::remove_file(&path).ok();
10228    }
10229
10230    /// `%%` is an escaped literal `%`, not a substitution: the mapping is an
10231    /// ordinary bounded one, and the source it resolves against is the name
10232    /// with a single `%` in it.
10233    #[test]
10234    fn an_escaped_percent_is_a_literal_in_a_source_name() {
10235        use crate::Selection;
10236        let path = temp_path("vds_escaped_pct");
10237        {
10238            let file = H5File::create(&path).unwrap();
10239            file.new_dataset::<i32>()
10240                .shape([4usize])
10241                .create("od%d")
10242                .unwrap()
10243                .write_raw(&[5i32, 6, 7, 8])
10244                .unwrap();
10245            file.new_dataset::<i32>()
10246                .shape([4usize])
10247                .virtual_mapping(Selection::All, ".", "od%%d", Selection::All)
10248                .create("vds")
10249                .unwrap();
10250            file.close().unwrap();
10251        }
10252        let file = H5File::open(&path).unwrap();
10253        let ds = file.dataset("vds").unwrap();
10254        assert_eq!(ds.read_raw::<i32>().unwrap(), vec![5, 6, 7, 8]);
10255        // The stored name keeps its escape; only the resolution unescapes.
10256        assert_eq!(ds.virtual_mappings().unwrap()[0].source_dset_name, "od%%d");
10257        drop(file);
10258        std::fs::remove_file(&path).ok();
10259    }
10260
10261    /// An unlimited mapping takes its extent from the source it names, so a
10262    /// virtual dataset written with one reports the source's rows, not the
10263    /// seed extent its dataspace message stores
10264    /// (`H5D__virtual_set_extent_unlim`). Same file, so the resolution runs
10265    /// without opening another one.
10266    #[test]
10267    fn an_unlimited_mapping_takes_its_extent_from_its_source() {
10268        let path = temp_path("vds_unlimited");
10269        {
10270            let file = H5File::create(&path).unwrap();
10271            file.new_dataset::<i32>()
10272                .shape([5usize, 2])
10273                .chunk(&[5, 2])
10274                .max_shape(&[None, Some(2)])
10275                .create("src")
10276                .unwrap()
10277                .write_raw(&(0..10i32).collect::<Vec<_>>())
10278                .unwrap();
10279            file.new_dataset::<i32>()
10280                .shape([1usize, 2])
10281                .max_shape(&[None, Some(2)])
10282                .virtual_mapping(unlimited_rows(), ".", "src", unlimited_rows())
10283                .create("vds")
10284                .unwrap();
10285            file.close().unwrap();
10286        }
10287        let file = H5File::open(&path).unwrap();
10288        let ds = file.dataset("vds").unwrap();
10289        assert_eq!(ds.shape(), vec![5, 2]);
10290        assert_eq!(ds.read_raw::<i32>().unwrap(), (0..10).collect::<Vec<i32>>());
10291        drop(file);
10292        std::fs::remove_file(&path).ok();
10293    }
10294
10295    /// The blocks-0/1/3 printf file every dataset-access test below reads,
10296    /// laid out exactly like `a_printf_mapping_stitches_one_source_per_block`
10297    /// so the gap is the only thing that changes.
10298    fn printf_gap_file(tag: &str) -> std::path::PathBuf {
10299        use crate::Selection;
10300        let path = temp_path(tag);
10301        let file = H5File::create(&path).unwrap();
10302        for (b, base) in [(0, 0i32), (1, 100), (3, 300)] {
10303            file.new_dataset::<i32>()
10304                .shape([2usize])
10305                .create(&format!("block{b}"))
10306                .unwrap()
10307                .write_raw(&[base, base + 1])
10308                .unwrap();
10309        }
10310        file.new_dataset::<i32>()
10311            .shape([1usize, 2])
10312            .max_shape(&[None, Some(2)])
10313            .fill_value(-7i32)
10314            .virtual_mapping(unlimited_rows(), ".", "block%b", Selection::All)
10315            .create("vds")
10316            .unwrap();
10317        file.close().unwrap();
10318        path
10319    }
10320
10321    /// `H5Pset_virtual_printf_gap` lets the block scan look past a missing
10322    /// source, and the blocks it looked past stay inside the extent reading
10323    /// as the fill value (H5Dvirtual.c:1519-1614, :2661-2665). Measured
10324    /// against libhdf5 1.14.6 through h5py's `h5p.PropDAID`: gap 0 gives
10325    /// two rows, gap 1 and gap 2 both give four with row 2 filled.
10326    #[test]
10327    fn a_printf_gap_looks_past_the_missing_block() {
10328        use crate::DatasetAccess;
10329        let path = printf_gap_file("vds_printf_gap");
10330        let file = H5File::open(&path).unwrap();
10331        for (gap, shape, data) in [
10332            (0u64, vec![2usize, 2], vec![0i32, 1, 100, 101]),
10333            (1, vec![4, 2], vec![0, 1, 100, 101, -7, -7, 300, 301]),
10334            (2, vec![4, 2], vec![0, 1, 100, 101, -7, -7, 300, 301]),
10335        ] {
10336            let ds = file
10337                .dataset_with("vds", DatasetAccess::new().virtual_printf_gap(gap))
10338                .unwrap();
10339            assert_eq!(ds.shape(), shape, "gap {gap}");
10340            assert_eq!(ds.read_raw::<i32>().unwrap(), data, "gap {gap}");
10341        }
10342        // Back to the default: the extent follows the properties the open
10343        // names, in both directions.
10344        let ds = file.dataset("vds").unwrap();
10345        assert_eq!(ds.shape(), vec![2, 2]);
10346        assert_eq!(ds.read_raw::<i32>().unwrap(), vec![0, 1, 100, 101]);
10347        drop(file);
10348        std::fs::remove_file(&path).ok();
10349    }
10350
10351    /// A relatively-named source is found next to the virtual dataset even
10352    /// when the process is somewhere else entirely: `H5F_prefix_open_file`
10353    /// tries the primary file's `H5F_EXTPATH` — the directory it was opened
10354    /// from — before the bare relative name against the working directory
10355    /// (H5Fint.c:952-977). Measured against libhdf5 1.14.6 through h5py: a
10356    /// `VirtualSource("src.h5", ...)` beside its VDS reads its data with
10357    /// `HDF5_VDS_PREFIX` unset and the working directory elsewhere; before
10358    /// the reader took that step it read back all fill value.
10359    #[test]
10360    fn a_relative_source_resolves_next_to_the_virtual_dataset() {
10361        use crate::Selection;
10362        let dir = std::env::temp_dir().join(format!(
10363            "rust_hdf5_vds_beside_{}_{:?}",
10364            std::process::id(),
10365            std::thread::current().id()
10366        ));
10367        std::fs::create_dir_all(&dir).unwrap();
10368        {
10369            let file = H5File::create(dir.join("src.h5")).unwrap();
10370            file.new_dataset::<i32>()
10371                .shape([2usize, 4])
10372                .create("data")
10373                .unwrap()
10374                .write_raw(&(0..8i32).collect::<Vec<_>>())
10375                .unwrap();
10376            file.close().unwrap();
10377        }
10378        {
10379            let file = H5File::create(dir.join("v.h5")).unwrap();
10380            file.new_dataset::<i32>()
10381                .shape([2usize, 4])
10382                .fill_value(-9i32)
10383                // Named relatively, as h5py's `VirtualSource("src.h5", ...)`
10384                // stores it — nothing in the file says where it lives.
10385                .virtual_mapping(Selection::All, "src.h5", "data", Selection::All)
10386                .create("v")
10387                .unwrap();
10388            file.close().unwrap();
10389        }
10390        // The working directory is the crate root under `cargo test`, not
10391        // `dir`, so only the extpath step can find `src.h5`.
10392        assert_ne!(std::env::current_dir().unwrap(), dir);
10393        let file = H5File::open(dir.join("v.h5")).unwrap();
10394        let ds = file.dataset("v").unwrap();
10395        assert_eq!(ds.read_raw::<i32>().unwrap(), (0..8i32).collect::<Vec<_>>());
10396        drop(file);
10397        std::fs::remove_dir_all(&dir).ok();
10398    }
10399
10400    /// The first open of a virtual dataset fixes its access properties for
10401    /// every open that overlaps it: only the open that finds no shared info
10402    /// in `H5FO_opened` runs `H5D__open_oid(dataset, dapl_id)`, and a later
10403    /// one just points at that shared info without ever reading its own dapl
10404    /// (H5Dint.c:1496-1500, :1523-1528) — the view and the gap live in the
10405    /// shared layout storage `H5D__virtual_init` filled from that first dapl
10406    /// (H5Dvirtual.c:2178-2188). Measured against libhdf5 1.14.6 and 2.0.0
10407    /// through `h5d.open(..., dapl=...)` on the printf-gap VDS below: opening
10408    /// gap 0 then gap 1 gives both handles two rows; with every handle closed,
10409    /// opening gap 1 then gap 0 gives both four rows and the gap row reads as
10410    /// fill; with every handle closed again, gap 0 alone is back to two rows.
10411    #[test]
10412    fn the_first_open_of_a_virtual_dataset_fixes_the_properties_for_later_opens() {
10413        use crate::DatasetAccess;
10414        let path = printf_gap_file("vds_printf_first_open");
10415        let file = H5File::open(&path).unwrap();
10416        let gap = |g: u64| DatasetAccess::new().virtual_printf_gap(g);
10417        {
10418            let a = file.dataset_with("vds", gap(0)).unwrap();
10419            let b = file.dataset_with("vds", gap(1)).unwrap();
10420            assert_eq!(a.shape(), vec![2, 2]);
10421            assert_eq!(b.shape(), vec![2, 2]);
10422            assert_eq!(b.read_raw::<i32>().unwrap(), vec![0, 1, 100, 101]);
10423        }
10424        {
10425            let c = file.dataset_with("vds", gap(1)).unwrap();
10426            let d = file.dataset_with("vds", gap(0)).unwrap();
10427            assert_eq!(c.shape(), vec![4, 2]);
10428            assert_eq!(d.shape(), vec![4, 2]);
10429            assert_eq!(
10430                d.read_raw::<i32>().unwrap(),
10431                vec![0, 1, 100, 101, -7, -7, 300, 301]
10432            );
10433        }
10434        let e = file.dataset_with("vds", gap(0)).unwrap();
10435        assert_eq!(e.shape(), vec![2, 2]);
10436        assert_eq!(e.read_raw::<i32>().unwrap(), vec![0, 1, 100, 101]);
10437        drop(e);
10438        drop(file);
10439        std::fs::remove_file(&path).ok();
10440    }
10441
10442    /// `H5D_VDS_FIRST_MISSING` ignores the printf gap: `H5D__virtual_init`
10443    /// reads the gap property only under `H5D_VDS_LAST_AVAILABLE` and forces
10444    /// it to 0 otherwise (H5Dvirtual.c:2182-2188). Measured against libhdf5
10445    /// 1.14.6: gap 2 under this view still gives two rows.
10446    #[test]
10447    fn the_first_missing_view_ignores_the_printf_gap() {
10448        use crate::{DatasetAccess, VirtualView};
10449        let path = printf_gap_file("vds_printf_first_missing");
10450        let file = H5File::open(&path).unwrap();
10451        for gap in [0u64, 2] {
10452            let ds = file
10453                .dataset_with(
10454                    "vds",
10455                    DatasetAccess::new()
10456                        .virtual_view(VirtualView::FirstMissing)
10457                        .virtual_printf_gap(gap),
10458                )
10459                .unwrap();
10460            assert_eq!(ds.shape(), vec![2, 2], "gap {gap}");
10461            assert_eq!(
10462                ds.read_raw::<i32>().unwrap(),
10463                vec![0, 1, 100, 101],
10464                "gap {gap}"
10465            );
10466        }
10467        drop(file);
10468        std::fs::remove_file(&path).ok();
10469    }
10470
10471    /// On a mapping unlimited on both sides the view is
10472    /// `H5S_hyper_get_clip_extent_match`'s `incl_trail`
10473    /// (H5Dvirtual.c:1447-1451): with a stride wider than its block, the
10474    /// extent under `H5D_VDS_LAST_AVAILABLE` ends at the last mapped row,
10475    /// and under `H5D_VDS_FIRST_MISSING` it runs on to where the next block
10476    /// would start. Measured against libhdf5 1.14.6 over a three-row source
10477    /// with stride 3 and block 2: two rows and three rows.
10478    #[test]
10479    fn the_view_decides_whether_a_trailing_gap_is_inside_the_extent() {
10480        use crate::format::selection::UNLIMITED;
10481        use crate::{DatasetAccess, Hyperslab, RegularHyperslab, Selection, VirtualView};
10482        let strided = || Selection::Hyperslab {
10483            rank: 2,
10484            form: Hyperslab::Regular(RegularHyperslab {
10485                start: vec![0, 0],
10486                stride: vec![3, 1],
10487                count: vec![UNLIMITED, 1],
10488                block: vec![2, 2],
10489            }),
10490        };
10491        let path = temp_path("vds_view_trail");
10492        {
10493            let file = H5File::create(&path).unwrap();
10494            file.new_dataset::<i32>()
10495                .shape([3usize, 2])
10496                .max_shape(&[None, Some(2)])
10497                .chunk(&[1, 2])
10498                .create("src")
10499                .unwrap()
10500                .write_raw(&(0..6i32).collect::<Vec<_>>())
10501                .unwrap();
10502            file.new_dataset::<i32>()
10503                .shape([1usize, 2])
10504                .max_shape(&[None, Some(2)])
10505                .fill_value(-9i32)
10506                .virtual_mapping(strided(), ".", "src", strided())
10507                .create("vds")
10508                .unwrap();
10509            file.close().unwrap();
10510        }
10511        let file = H5File::open(&path).unwrap();
10512        {
10513            let last = file.dataset("vds").unwrap();
10514            assert_eq!(last.shape(), vec![2, 2]);
10515            assert_eq!(last.read_raw::<i32>().unwrap(), vec![0, 1, 2, 3]);
10516        }
10517        // The handle above is gone, so this open is the one that resolves.
10518        let first = file
10519            .dataset_with(
10520                "vds",
10521                DatasetAccess::new().virtual_view(VirtualView::FirstMissing),
10522            )
10523            .unwrap();
10524        assert_eq!(first.shape(), vec![3, 2]);
10525        assert_eq!(first.read_raw::<i32>().unwrap(), vec![0, 1, 2, 3, -9, -9]);
10526        drop(file);
10527        std::fs::remove_file(&path).ok();
10528    }
10529
10530    /// The property list reads back what was set (`H5Pget_virtual_view`,
10531    /// `H5Pget_virtual_printf_gap`), and the gap `HSIZE_UNDEF` that
10532    /// `H5Pset_virtual_printf_gap` refuses is refused by the open that would
10533    /// have used it.
10534    #[test]
10535    fn the_access_property_list_reads_back_and_refuses_hsize_undef() {
10536        use crate::{DatasetAccess, VirtualView};
10537        let plist = DatasetAccess::new();
10538        assert_eq!(plist.view(), VirtualView::LastAvailable);
10539        assert_eq!(plist.printf_gap(), 0);
10540        let set = plist
10541            .virtual_view(VirtualView::FirstMissing)
10542            .virtual_printf_gap(4);
10543        assert_eq!(set.view(), VirtualView::FirstMissing);
10544        // The *property* keeps what was set even though the resolution under
10545        // this view scans with 0.
10546        assert_eq!(set.printf_gap(), 4);
10547
10548        let path = printf_gap_file("vds_printf_gap_undef");
10549        let file = H5File::open(&path).unwrap();
10550        let err = match file.dataset_with("vds", DatasetAccess::new().virtual_printf_gap(u64::MAX))
10551        {
10552            Ok(_) => panic!("HSIZE_UNDEF is not a valid printf gap size"),
10553            Err(e) => e.to_string(),
10554        };
10555        assert!(err.contains("HSIZE_UNDEF"), "{err}");
10556        drop(file);
10557        std::fs::remove_file(&path).ok();
10558    }
10559
10560    /// The rank-2 `count = (H5S_UNLIMITED, 1)`, `block = (1, 2)` selection
10561    /// both sides of an unlimited row-wise mapping use.
10562    fn unlimited_rows() -> crate::Selection {
10563        use crate::format::selection::UNLIMITED;
10564        use crate::{Hyperslab, RegularHyperslab, Selection};
10565        Selection::Hyperslab {
10566            rank: 2,
10567            form: Hyperslab::Regular(RegularHyperslab {
10568                start: vec![0, 0],
10569                stride: vec![1, 1],
10570                count: vec![UNLIMITED, 1],
10571                block: vec![1, 2],
10572            }),
10573        }
10574    }
10575
10576    /// An unlimited virtual selection over a *limited* source selection is
10577    /// the printf shape, and without a `%b` in a source name there is no
10578    /// second dataset to fill the second block —
10579    /// `H5D_virtual_check_mapping_post` refuses it, and so does this.
10580    #[test]
10581    fn an_unlimited_virtual_selection_over_a_limited_source_is_refused() {
10582        use crate::Selection;
10583        let path = temp_path("vds_unlim_limited_src");
10584        let file = H5File::create(&path).unwrap();
10585        let err = match file
10586            .new_dataset::<i32>()
10587            .shape([1usize, 2])
10588            .max_shape(&[None, Some(2)])
10589            .virtual_mapping(unlimited_rows(), "src.h5", "src", Selection::All)
10590            .create("vds")
10591        {
10592            Ok(_) => panic!("no printf substitution names the mapping's later blocks"),
10593            Err(e) => e.to_string(),
10594        };
10595        assert!(err.contains("printf"), "{err}");
10596        file.close().unwrap();
10597        std::fs::remove_file(&path).ok();
10598    }
10599
10600    /// A virtual dataset stores nothing of its own, so it cannot also be one
10601    /// of the storage classes that do.
10602    #[test]
10603    fn a_virtual_dataset_cannot_also_be_chunked_or_external() {
10604        use crate::Selection;
10605        let path = temp_path("vds_exclusive");
10606        let file = H5File::create(&path).unwrap();
10607        let builder = || {
10608            file.new_dataset::<i32>().shape([16usize]).virtual_mapping(
10609                Selection::All,
10610                "src.h5",
10611                "src",
10612                Selection::All,
10613            )
10614        };
10615        for (which, res) in [
10616            ("chunked", builder().chunk(&[4]).create("a")),
10617            ("compact", builder().compact().create("b")),
10618            (
10619                "external",
10620                builder().external(&[("x.raw", 0, 64)]).create("c"),
10621            ),
10622            ("references", builder().object_references().create("d")),
10623        ] {
10624            match res {
10625                Ok(_) => panic!("a virtual dataset cannot also be {which}"),
10626                Err(e) => assert!(e.to_string().contains("virtual dataset"), "{which}: {e}"),
10627            }
10628        }
10629        file.close().unwrap();
10630        std::fs::remove_file(&path).ok();
10631    }
10632
10633    /// Deleting a virtual dataset frees the global heap object its mapping
10634    /// list lived in — `H5D__virtual_delete` reaches `H5HG_remove` — so the
10635    /// next dataset's own heap object reuses that space instead of the file
10636    /// growing by a whole collection per deleted virtual dataset.
10637    #[test]
10638    fn deleting_a_virtual_dataset_frees_its_mapping_list() {
10639        use crate::Selection;
10640        let path = temp_path("vds_delete");
10641        let baseline = {
10642            let file = H5File::create(&path).unwrap();
10643            file.new_dataset::<i32>()
10644                .shape([16usize])
10645                .virtual_mapping(Selection::All, "src.h5", "src", Selection::All)
10646                .create("vds")
10647                .unwrap();
10648            file.delete_dataset("vds").unwrap();
10649            file.close().unwrap();
10650            std::fs::metadata(&path).unwrap().len()
10651        };
10652        std::fs::remove_file(&path).ok();
10653
10654        // Ten more create-then-delete rounds must land on the same file size:
10655        // each round's heap object is removed, its collection becomes empty
10656        // and returns to the allocator, and the next round takes it back.
10657        let file = H5File::create(&path).unwrap();
10658        for i in 0..10 {
10659            let name = format!("vds{i}");
10660            file.new_dataset::<i32>()
10661                .shape([16usize])
10662                .virtual_mapping(Selection::All, "src.h5", "src", Selection::All)
10663                .create(&name)
10664                .unwrap();
10665            file.delete_dataset(&name).unwrap();
10666        }
10667        file.close().unwrap();
10668        assert_eq!(std::fs::metadata(&path).unwrap().len(), baseline);
10669        std::fs::remove_file(&path).ok();
10670    }
10671
10672    /// [`H5Dataset::storage_layout`] tells the four classes apart — the
10673    /// negative case for any one class is simply that it is not another.
10674    #[test]
10675    fn storage_layout_reports_each_class() {
10676        use crate::StorageLayout;
10677        let path = temp_path("storage_layout");
10678        {
10679            let file = H5File::create(&path).unwrap();
10680            file.new_dataset::<i32>()
10681                .shape([4usize])
10682                .create("contig")
10683                .unwrap();
10684            file.new_dataset::<i32>()
10685                .shape([4usize])
10686                .compact()
10687                .create("compact")
10688                .unwrap();
10689            file.new_dataset::<i32>()
10690                .shape([8usize])
10691                .chunk(&[4])
10692                .create("chunked")
10693                .unwrap();
10694            file.close().unwrap();
10695        }
10696        let file = H5File::open(&path).unwrap();
10697        assert_eq!(
10698            file.dataset("contig").unwrap().storage_layout().unwrap(),
10699            StorageLayout::Contiguous
10700        );
10701        assert_eq!(
10702            file.dataset("compact").unwrap().storage_layout().unwrap(),
10703            StorageLayout::Compact
10704        );
10705        assert_eq!(
10706            file.dataset("chunked").unwrap().storage_layout().unwrap(),
10707            StorageLayout::Chunked
10708        );
10709    }
10710
10711    /// Read-mode-only accessor, matching `datatype()`'s own contract.
10712    #[test]
10713    fn storage_layout_errors_in_write_mode() {
10714        let path = temp_path("storage_layout_write_mode");
10715        let file = H5File::create(&path).unwrap();
10716        let ds = file
10717            .new_dataset::<i32>()
10718            .shape([4usize])
10719            .create("data")
10720            .unwrap();
10721        assert!(ds.storage_layout().is_err());
10722        file.close().unwrap();
10723    }
10724
10725    /// [`H5Dataset::chunk_index`] reports the real on-disk index kind
10726    /// (extensible array for one unlimited dimension, version-1 B-tree
10727    /// under a legacy libver bound) and `None` for an unchunked dataset —
10728    /// the negative case.
10729    #[test]
10730    fn chunk_index_reports_the_stored_kind() {
10731        use crate::ChunkIndex;
10732        let path = temp_path("chunk_index");
10733        {
10734            let file = H5File::create(&path).unwrap();
10735            file.new_dataset::<i32>()
10736                .shape([4usize])
10737                .create("contig")
10738                .unwrap();
10739            file.new_dataset::<i32>()
10740                .shape([16usize])
10741                .chunk(&[4])
10742                .max_shape(&[None])
10743                .create("earray")
10744                .unwrap();
10745            file.set_libver_latest(false).unwrap();
10746            file.new_dataset::<i32>()
10747                .shape([8usize])
10748                .chunk(&[4])
10749                .max_shape(&[None])
10750                .create("btree1")
10751                .unwrap();
10752            file.close().unwrap();
10753        }
10754        let file = H5File::open(&path).unwrap();
10755        assert_eq!(file.dataset("contig").unwrap().chunk_index().unwrap(), None);
10756        assert_eq!(
10757            file.dataset("earray").unwrap().chunk_index().unwrap(),
10758            Some(ChunkIndex::ExtensibleArray)
10759        );
10760        assert_eq!(
10761            file.dataset("btree1").unwrap().chunk_index().unwrap(),
10762            Some(ChunkIndex::BtreeV1)
10763        );
10764    }
10765
10766    /// [`H5Dataset::filters`] reports the stored pipeline in order — and
10767    /// the negative case: an unfiltered dataset reports an empty pipeline,
10768    /// not an error.
10769    #[test]
10770    fn filters_reports_the_stored_pipeline() {
10771        use crate::format::messages::filter::{FILTER_DEFLATE, FILTER_SHUFFLE, FLAG_OPTIONAL};
10772        let path = temp_path("filters");
10773        {
10774            let file = H5File::create(&path).unwrap();
10775            file.new_dataset::<i32>()
10776                .shape([16usize])
10777                .create("unfiltered")
10778                .unwrap();
10779            file.new_dataset::<i32>()
10780                .shape([16usize])
10781                .chunk(&[4])
10782                .shuffle()
10783                .deflate(6)
10784                .create("filtered")
10785                .unwrap();
10786            file.close().unwrap();
10787        }
10788        let file = H5File::open(&path).unwrap();
10789        assert_eq!(
10790            file.dataset("unfiltered").unwrap().filters().unwrap(),
10791            Vec::new()
10792        );
10793        let filters = file.dataset("filtered").unwrap().filters().unwrap();
10794        assert_eq!(filters.len(), 2);
10795        assert_eq!(filters[0].id, FILTER_SHUFFLE);
10796        assert_eq!(filters[0].flags, FLAG_OPTIONAL);
10797        assert_eq!(filters[1].id, FILTER_DEFLATE);
10798        assert_eq!(filters[1].cd_values, vec![6]);
10799    }
10800
10801    /// [`H5Dataset::fill_value`] reports the explicit bytes for a dataset
10802    /// created with `.fill_value(...)`, and the negative case: a dataset
10803    /// with no fill value set reports [`FillValue::Default`], not an error.
10804    /// `FillValue::Undefined` has no constructor on either this crate's
10805    /// writer or h5py's public API, so it is not exercised here.
10806    #[test]
10807    fn fill_value_reports_the_stored_value() {
10808        use crate::FillValue;
10809        let path = temp_path("fill_value");
10810        {
10811            let file = H5File::create(&path).unwrap();
10812            file.new_dataset::<i32>()
10813                .shape([4usize])
10814                .create("unset")
10815                .unwrap();
10816            file.new_dataset::<i32>()
10817                .shape([4usize])
10818                .fill_value(-7i32)
10819                .create("set")
10820                .unwrap();
10821            file.close().unwrap();
10822        }
10823        let file = H5File::open(&path).unwrap();
10824        assert_eq!(
10825            file.dataset("unset").unwrap().fill_value().unwrap(),
10826            FillValue::Default
10827        );
10828        assert_eq!(
10829            file.dataset("set").unwrap().fill_value().unwrap(),
10830            FillValue::UserDefined((-7i32).to_le_bytes().to_vec())
10831        );
10832    }
10833
10834    /// The three `H5D__efl_construct` / `H5Pset_external` rules an
10835    /// `H5O_EFL_UNLIMITED` slot lives inside: it may only be the last slot,
10836    /// an unlimited dataspace must have one, and only the first dimension
10837    /// may be extendible.
10838    #[test]
10839    fn the_unlimited_external_slot_keeps_its_three_rules() {
10840        use crate::format::messages::external_file_list::UNLIMITED;
10841        let path = temp_path("efl_unlim_rules");
10842        let file = H5File::create(&path).unwrap();
10843
10844        // "previous file size is unlimited": nothing behind an unlimited slot
10845        // could ever be reached.
10846        let err = match file
10847            .new_dataset::<i32>()
10848            .shape([8usize])
10849            .external(&[("a.raw", 0, UNLIMITED), ("b.raw", 0, 32)])
10850            .create("mid")
10851        {
10852            Ok(_) => panic!("an unlimited slot absorbs everything behind it"),
10853            Err(e) => e.to_string(),
10854        };
10855        assert!(err.contains("only be the last"), "{err}");
10856
10857        // "unlimited dataspace but finite storage".
10858        let err = match file
10859            .new_dataset::<i32>()
10860            .shape([8usize])
10861            .max_shape(&[None])
10862            .external(&[("a.raw", 0, 32)])
10863            .create("finite")
10864        {
10865            Ok(_) => panic!("no finite reservation covers an unlimited extent"),
10866            Err(e) => e.to_string(),
10867        };
10868        assert!(err.contains("unlimited dataspace"), "{err}");
10869
10870        // "only the first dimension can be extendible".
10871        let err = match file
10872            .new_dataset::<i32>()
10873            .shape([2usize, 4])
10874            .max_shape(&[Some(2), None])
10875            .external(&[("a.raw", 0, UNLIMITED)])
10876            .create("dim1")
10877        {
10878            Ok(_) => panic!("only the slowest-varying dimension may be extendible"),
10879            Err(e) => e.to_string(),
10880        };
10881        assert!(err.contains("only the first dimension"), "{err}");
10882
10883        // And the legal shape: an unlimited last slot under an unlimited
10884        // first dimension.
10885        file.new_dataset::<i32>()
10886            .shape([8usize])
10887            .max_shape(&[None])
10888            .external(&[("a.raw", 0, 16), ("b.raw", 0, UNLIMITED)])
10889            .create("ok")
10890            .unwrap()
10891            .write_raw(&(0..8i32).collect::<Vec<_>>())
10892            .unwrap();
10893        file.close().unwrap();
10894        std::fs::remove_file(&path).ok();
10895        for raw in ["a.raw", "b.raw"] {
10896            std::fs::remove_file(raw).ok();
10897        }
10898    }
10899
10900    /// An unlimited slot reserves nothing, so a read of it is bounded by the
10901    /// dataset's extent and by what the file physically holds: the tail past
10902    /// the end of a short raw file reads back as zero, exactly as
10903    /// `H5D__efl_read` fills it.
10904    #[test]
10905    fn an_unlimited_external_slot_reads_zero_past_the_end_of_its_file() {
10906        use crate::format::messages::external_file_list::UNLIMITED;
10907        let dir = std::env::temp_dir().join(format!("rh5_efl_short_{}", std::process::id()));
10908        std::fs::create_dir_all(&dir).unwrap();
10909        let path = dir.join("f.h5");
10910        let raw = dir.join("short.raw");
10911        // Four elements' worth of bytes for an eight-element dataset.
10912        std::fs::write(&raw, [0u8; 16]).unwrap();
10913        {
10914            let file = H5File::create(&path).unwrap();
10915            file.new_dataset::<i32>()
10916                .shape([8usize])
10917                .max_shape(&[None])
10918                .external(&[(raw.to_str().unwrap(), 0, UNLIMITED)])
10919                .create("data")
10920                .unwrap();
10921            file.close().unwrap();
10922        }
10923        let file = H5File::open(&path).unwrap();
10924        assert_eq!(
10925            file.dataset("data").unwrap().read_raw::<i32>().unwrap(),
10926            vec![0i32; 8]
10927        );
10928        drop(file);
10929        std::fs::remove_dir_all(&dir).ok();
10930    }
10931
10932    /// [`H5Dataset::external_files`] reports the stored segment list in
10933    /// order, and the negative case: a dataset whose data lives in this
10934    /// file reports an empty list, not an error.
10935    #[test]
10936    fn external_files_reports_the_stored_segments() {
10937        let path = temp_path("external_files");
10938        {
10939            let file = H5File::create(&path).unwrap();
10940            file.new_dataset::<i32>()
10941                .shape([4usize])
10942                .create("contig")
10943                .unwrap();
10944            file.new_dataset::<i32>()
10945                .shape([16usize])
10946                .external(&[("a.raw", 0, 32), ("b.raw", 8, 32)])
10947                .create("external")
10948                .unwrap();
10949            file.close().unwrap();
10950        }
10951        let file = H5File::open(&path).unwrap();
10952        assert_eq!(
10953            file.dataset("contig").unwrap().external_files().unwrap(),
10954            Vec::new()
10955        );
10956        let segments = file.dataset("external").unwrap().external_files().unwrap();
10957        assert_eq!(segments.len(), 2);
10958        assert_eq!(segments[0].name, "a.raw");
10959        assert_eq!(segments[0].offset, 0);
10960        assert_eq!(segments[0].size, 32);
10961        assert_eq!(segments[1].name, "b.raw");
10962        assert_eq!(segments[1].offset, 8);
10963        assert_eq!(segments[1].size, 32);
10964    }
10965
10966    /// [`H5Dataset::max_shape`] reports an unlimited axis as `None` and a
10967    /// fixed one as its current size — and the negative case: a dataset
10968    /// with no maximum-dimensions message reports max == current, not an
10969    /// error.
10970    #[test]
10971    fn max_shape_reports_unlimited_and_fixed_axes() {
10972        let path = temp_path("max_shape");
10973        {
10974            let file = H5File::create(&path).unwrap();
10975            file.new_dataset::<i32>()
10976                .shape([4usize, 8])
10977                .create("fixed")
10978                .unwrap();
10979            file.new_dataset::<i32>()
10980                .shape([4usize, 8])
10981                .chunk(&[2, 8])
10982                .max_shape(&[None, Some(8)])
10983                .create("unlimited")
10984                .unwrap();
10985            file.close().unwrap();
10986        }
10987        let file = H5File::open(&path).unwrap();
10988        assert_eq!(
10989            file.dataset("fixed").unwrap().max_shape().unwrap(),
10990            vec![Some(4), Some(8)]
10991        );
10992        assert_eq!(
10993            file.dataset("unlimited").unwrap().max_shape().unwrap(),
10994            vec![None, Some(8)]
10995        );
10996    }
10997
10998    /// [`H5Dataset::virtual_mappings`] reports the stored source/virtual
10999    /// mapping list in order, and the negative case: a dataset with no
11000    /// virtual layout reports an empty list, not an error.
11001    #[test]
11002    fn virtual_mappings_reports_the_stored_mappings() {
11003        use crate::Selection;
11004        let path = temp_path("virtual_mappings");
11005        {
11006            let file = H5File::create(&path).unwrap();
11007            file.new_dataset::<i32>()
11008                .shape([4usize])
11009                .create("plain")
11010                .unwrap();
11011            file.new_dataset::<i32>()
11012                .shape([16usize])
11013                .virtual_mapping(Selection::All, "src.h5", "src", Selection::All)
11014                .create("vds")
11015                .unwrap();
11016            file.close().unwrap();
11017        }
11018        let file = H5File::open(&path).unwrap();
11019        assert_eq!(
11020            file.dataset("plain").unwrap().virtual_mappings().unwrap(),
11021            Vec::new()
11022        );
11023        let mappings = file.dataset("vds").unwrap().virtual_mappings().unwrap();
11024        assert_eq!(mappings.len(), 1);
11025        assert_eq!(mappings[0].source_file_name, "src.h5");
11026        assert_eq!(mappings[0].source_dset_name, "src");
11027        assert_eq!(mappings[0].source_selection, Selection::All);
11028        assert_eq!(mappings[0].virtual_selection, Selection::All);
11029    }
11030}