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::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/// One chunk's bytes on the way to the file, and who filtered them.
1460///
1461/// This is what separates a normal chunk write from a direct one; everything
1462/// else about placing a chunk is identical, so the two share a single dispatch.
1463#[derive(Clone, Copy)]
1464enum ChunkBytes<'a> {
1465    /// The chunk's raw bytes; the dataset's filter pipeline runs before they
1466    /// are stored.
1467    Unfiltered(&'a [u8]),
1468    /// Bytes already in their stored form, with `filter_mask` naming the
1469    /// filters that were skipped.
1470    Prefiltered { data: &'a [u8], filter_mask: u32 },
1471}
1472
1473/// The byte order this build reads and writes natively.
1474pub(crate) const HOST_BYTE_ORDER: ByteOrder = if cfg!(target_endian = "big") {
1475    ByteOrder::BigEndian
1476} else {
1477    ByteOrder::LittleEndian
1478};
1479
1480/// The byte order this build does not read or write natively.
1481pub(crate) const FOREIGN_BYTE_ORDER: ByteOrder = match HOST_BYTE_ORDER {
1482    ByteOrder::LittleEndian => ByteOrder::BigEndian,
1483    ByteOrder::BigEndian => ByteOrder::LittleEndian,
1484};
1485
1486/// What a typed access has to do with an element image of a given datatype.
1487#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1488enum ByteOrderAction {
1489    /// Stored order is the host's: the image is already the typed value.
1490    Keep,
1491    /// The whole element is one scalar in the foreign order: reverse it.
1492    SwapElements,
1493    /// A composite storing something in the foreign order.
1494    Refuse,
1495}
1496
1497/// Classify a datatype for a typed access of element width `width`.
1498///
1499/// The single owner of the rule; both directions ask it, so a type a read
1500/// converts is exactly a type a write converts.
1501///
1502/// A composite element cannot be swapped as a unit — its members have their
1503/// own orders and offsets — so one that touches the foreign order is refused
1504/// rather than silently passed through in the wrong order.
1505fn byte_order_action(datatype: &DatatypeMessage, width: usize) -> ByteOrderAction {
1506    match datatype.scalar_byte_order() {
1507        Some(order) if order == FOREIGN_BYTE_ORDER && width > 1 => ByteOrderAction::SwapElements,
1508        Some(_) => ByteOrderAction::Keep,
1509        None if datatype.contains_byte_order(FOREIGN_BYTE_ORDER) => ByteOrderAction::Refuse,
1510        None => ByteOrderAction::Keep,
1511    }
1512}
1513
1514/// Why the stored image of an element is not already the host image of a
1515/// value of width `width` — `None` when it is, and a copying read would only
1516/// be memcpy-ing bytes it does not touch.
1517///
1518/// The two ways a stored element can need work before it is a value are the
1519/// two conversions a copying read performs in place: a byte-order swap
1520/// ([`to_host_byte_order`]) and the n-bit/scale-offset unpacking
1521/// (`Hdf5Reader::apply_post_filter_conversion`). Asking one question of both
1522/// is what lets a zero-copy view refuse exactly the datatypes a copying read
1523/// would have had to rewrite.
1524#[cfg(feature = "mmap")]
1525pub(crate) fn stored_image_mismatch(
1526    datatype: &DatatypeMessage,
1527    width: usize,
1528) -> Option<&'static str> {
1529    match byte_order_action(datatype, width) {
1530        ByteOrderAction::SwapElements => return Some("they are stored in the foreign byte order"),
1531        ByteOrderAction::Refuse => {
1532            return Some("it is a composite storing members in the foreign byte order")
1533        }
1534        ByteOrderAction::Keep => {}
1535    }
1536    if crate::format::nbit_scaleoffset::datatype_needs_bit_conversion(datatype) {
1537        return Some("the significant bits do not fill the stored element");
1538    }
1539    None
1540}
1541
1542/// Put a raw element image into host byte order, in place, for a typed read.
1543///
1544/// Every path that reinterprets the on-disk image as `T` — `read_raw`,
1545/// `read_slice`, `read_raw_into`, `read_slice_into` and their SWMR
1546/// counterparts — passes through here. Reinterpretation only yields the
1547/// stored value when the stored order is the host's.
1548///
1549/// A refused datatype is one no reinterpretation can decode;
1550/// [`H5Dataset::read_raw_bytes`] hands over the image for the caller to
1551/// decode member by member.
1552///
1553/// `width` is the element size, already checked equal to `T::element_size()`.
1554pub(crate) fn to_host_byte_order(
1555    bytes: &mut [u8],
1556    datatype: &DatatypeMessage,
1557    width: usize,
1558) -> Result<()> {
1559    match byte_order_action(datatype, width) {
1560        ByteOrderAction::Keep => {}
1561        ByteOrderAction::SwapElements => {
1562            for elem in bytes.chunks_exact_mut(width) {
1563                elem.reverse();
1564            }
1565        }
1566        ByteOrderAction::Refuse => {
1567            return Err(Hdf5Error::TypeMismatch(format!(
1568                "dataset datatype {datatype} stores {FOREIGN_BYTE_ORDER:?} values, which a \
1569                 typed read cannot reinterpret element by element; read_raw_bytes() returns \
1570                 the image to decode member by member"
1571            )))
1572        }
1573    }
1574    Ok(())
1575}
1576
1577/// The stored byte image of each variable-length sequence in a batch.
1578///
1579/// The vlen writers take `&[&[T]]` and store one global-heap object per
1580/// sequence, so each sequence needs the same host-image-to-stored-image step
1581/// [`to_stored_byte_order`] performs for a fixed-shape write — a `T` is
1582/// written from its host bytes, and `T::hdf5_type()` declares little-endian.
1583/// Borrows on a little-endian host, which is every machine that does not have
1584/// to swap.
1585pub(crate) fn vlen_sequence_images<'a, T: H5Type>(
1586    items: &'a [&'a [T]],
1587) -> Result<Vec<std::borrow::Cow<'a, [u8]>>> {
1588    let base = T::hdf5_type();
1589    items
1590        .iter()
1591        .map(|item| {
1592            // Safety: the same contract `write_raw` relies on — `T: Copy +
1593            // 'static` is a numeric primitive whose byte image is its value —
1594            // and the extent comes from the slice itself, so it cannot name
1595            // memory past it. The result borrows `items` and outlives nothing.
1596            let host = unsafe {
1597                std::slice::from_raw_parts(item.as_ptr() as *const u8, std::mem::size_of_val(*item))
1598            };
1599            to_stored_byte_order(host, &base, T::element_size())
1600        })
1601        .collect()
1602}
1603
1604/// Put a typed value's host-order image into the order the datatype declares.
1605///
1606/// The write-side counterpart of [`to_host_byte_order`], and the one place
1607/// every path that hands a `&[T]` to the file — `write_raw`, `write_slice`,
1608/// `append`, and the builder's fill value — turns those bytes into stored
1609/// bytes. A `T` is written from its host image, so a dataset declaring the
1610/// foreign order would otherwise hold host bytes under that declaration: a
1611/// file that is wrong by its own header.
1612///
1613/// Borrows when the declared order is the host's, which is every write that
1614/// does not set a [`datatype`](DatasetBuilder::datatype) override.
1615///
1616/// `width` is the element size, already checked equal to `T::element_size()`.
1617pub(crate) fn to_stored_byte_order<'a>(
1618    bytes: &'a [u8],
1619    datatype: &DatatypeMessage,
1620    width: usize,
1621) -> Result<std::borrow::Cow<'a, [u8]>> {
1622    match byte_order_action(datatype, width) {
1623        ByteOrderAction::Keep => Ok(std::borrow::Cow::Borrowed(bytes)),
1624        ByteOrderAction::SwapElements => {
1625            let mut owned = bytes.to_vec();
1626            for elem in owned.chunks_exact_mut(width) {
1627                elem.reverse();
1628            }
1629            Ok(std::borrow::Cow::Owned(owned))
1630        }
1631        ByteOrderAction::Refuse => Err(Hdf5Error::TypeMismatch(format!(
1632            "dataset datatype {datatype} stores {FOREIGN_BYTE_ORDER:?} values, which a typed \
1633             write cannot lay out element by element; write_raw_bytes() takes the image the \
1634             caller encodes member by member"
1635        ))),
1636    }
1637}
1638
1639/// Strip a fixed-string element's padding, leaving the bytes that carry the
1640/// value.
1641///
1642/// The rule itself lives with the datatype message
1643/// ([`fixed_string_content`]); this adds the element index a reserved padding
1644/// rule needs to be reported against.
1645fn trim_fixed_string(elem: &[u8], padding: u8, index: usize) -> Result<&[u8]> {
1646    crate::format::messages::datatype::fixed_string_content(elem, padding).ok_or_else(|| {
1647        Hdf5Error::InvalidState(format!(
1648            "string {index} uses padding rule {padding}, which the format reserves"
1649        ))
1650    })
1651}
1652
1653/// Decode one string element's bytes under the datatype's character set.
1654///
1655/// `lossy` replaces what it cannot decode with U+FFFD instead of failing;
1656/// `index` names the element in the error otherwise.
1657fn decode_string(bytes: &[u8], charset: u8, lossy: bool, index: usize) -> Result<String> {
1658    if lossy {
1659        return Ok(String::from_utf8_lossy(bytes).into_owned());
1660    }
1661    match charset {
1662        // ASCII. Bytes are 7-bit, which makes them UTF-8 as well.
1663        0 => match bytes.iter().position(|&b| b >= 0x80) {
1664            None => Ok(String::from_utf8_lossy(bytes).into_owned()),
1665            Some(at) => Err(Hdf5Error::InvalidState(format!(
1666                "string {index} declares the ASCII character set but byte {at} is {:#04x}",
1667                bytes[at]
1668            ))),
1669        },
1670        1 => String::from_utf8(bytes.to_vec()).map_err(|e| {
1671            Hdf5Error::InvalidState(format!(
1672                "string {index} declares UTF-8 but is not valid UTF-8: {e}"
1673            ))
1674        }),
1675        other => Err(Hdf5Error::InvalidState(format!(
1676            "string {index} uses character set {other}, which the format reserves"
1677        ))),
1678    }
1679}
1680
1681/// A dataset's storage layout class (read mode only) — `H5Pget_layout`'s
1682/// four values.
1683///
1684/// Distinct from [`ChunkIndex`], which names the structure a `Chunked`
1685/// layout's index uses; this only says which of the four storage classes
1686/// the dataset was created with.
1687#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1688pub enum StorageLayout {
1689    /// Raw data stored inline in the object header.
1690    Compact,
1691    /// Raw data in a single contiguous block — or, when the dataset also
1692    /// carries an external file list, in one or more blocks of an outside
1693    /// file instead ([`H5Dataset::external_files`]); the layout itself
1694    /// still reports `Contiguous` either way.
1695    Contiguous,
1696    /// Raw data split into fixed-size chunks, each independently
1697    /// allocated. [`H5Dataset::chunk_dims`] gives the chunk shape,
1698    /// [`H5Dataset::chunk_index`] the index structure.
1699    Chunked,
1700    /// No raw data of its own: every element comes from another dataset,
1701    /// possibly in another file ([`H5Dataset::virtual_mappings`]).
1702    Virtual,
1703}
1704
1705/// The chunk index structure a chunked dataset uses on disk (read mode
1706/// only) — which of libhdf5's chunk-lookup structures the layout message
1707/// names.
1708///
1709/// `BtreeV1` belongs to the version-3 chunked layout message (the only
1710/// index a file whose superblock predates version 2 can carry); the other
1711/// five are what a version-4 message's index-type byte selects, per
1712/// `H5D__layout_set_latest_indexing` (H5Dlayout.c).
1713#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1714pub enum ChunkIndex {
1715    /// Version-1 B-tree — the classic chunk index, and the only one a
1716    /// version-3 chunked layout message can carry.
1717    BtreeV1,
1718    /// Version-2 B-tree — two or more unlimited dimensions.
1719    BtreeV2,
1720    /// A single index entry for a dataset whose one chunk covers the whole
1721    /// dataspace (`dims == max_dims == chunk_dims`).
1722    SingleChunk,
1723    /// No index structure at all: chunk addresses are computed
1724    /// arithmetically over a contiguous run (no filter, early allocation).
1725    Implicit,
1726    /// Fixed-size array — a fixed shape needing per-chunk bookkeeping.
1727    FixedArray,
1728    /// Extensible array — exactly one unlimited dimension.
1729    ExtensibleArray,
1730}
1731
1732/// A dataset's fill-value state (read mode only) — `H5Pfill_value_defined`'s
1733/// tri-state (`H5D_fill_value_t`).
1734#[derive(Debug, Clone, PartialEq, Eq)]
1735pub enum FillValue {
1736    /// No fill value has ever been set: unwritten elements read back
1737    /// zero-filled, and no fill-value message named an explicit value.
1738    Default,
1739    /// The fill value was explicitly disabled: unallocated storage is never
1740    /// fill-initialized.
1741    Undefined,
1742    /// An explicit fill value, one element wide.
1743    UserDefined(Vec<u8>),
1744}
1745
1746/// When a dataset's fill value is written into allocated storage —
1747/// `H5Pset_fill_time`/`H5Pget_fill_time`'s `H5D_fill_time_t`.
1748///
1749/// Distinct from [`FillValue`], which says *what* the fill value is; this
1750/// says *when* it is written. The two agree everywhere except a dataset with
1751/// no fill value of its own: there, `Alloc` writes the default fill (zeros)
1752/// at allocation and `IfSet` writes nothing into space that already reads as
1753/// zeros — indistinguishable on disk in the value itself, only in this byte.
1754#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1755pub enum FillTime {
1756    /// Fill at allocation regardless of whether a fill value was ever set.
1757    Alloc,
1758    /// Never write the fill value into allocated storage.
1759    Never,
1760    /// Fill at allocation only when a fill value was set — the default
1761    /// every dataset gets unless [`fill_time`](DatasetBuilder::fill_time)
1762    /// says otherwise.
1763    IfSet,
1764}
1765
1766impl FillTime {
1767    /// The on-disk `H5D_fill_time_t` byte this variant is — what the writer
1768    /// stores and the fill-value message's write-time field carries.
1769    fn wire_byte(self) -> u8 {
1770        match self {
1771            Self::Alloc => 0,
1772            Self::Never => 1,
1773            Self::IfSet => 2,
1774        }
1775    }
1776}
1777
1778/// When a dataset's raw-data storage is allocated —
1779/// `H5Pset_alloc_time`/`H5Pget_alloc_time`'s `H5D_alloc_time_t`, read back
1780/// from the same fill-value message [`FillTime`] is.
1781///
1782/// `H5P__set_layout` (H5Pdcpl.c) picks this from the dataset's storage
1783/// class (`H5D_ALLOC_TIME_DEFAULT` per layout — compact is `Early`,
1784/// chunked and virtual are `Incr`, contiguous is `Late`). The one override
1785/// this crate's builder offers is [`DatasetBuilder::early_allocation`],
1786/// which a chunked dataset reads back as `Early` where the writer took it
1787/// up: an implicit index, or a single unfiltered chunk.
1788/// [`H5Dataset::alloc_time`] reads back what the writer declared.
1789#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1790pub enum AllocTime {
1791    /// Space is allocated as soon as the dataset is created.
1792    Early,
1793    /// Space is allocated when data is first written.
1794    Late,
1795    /// Space is allocated incrementally, as chunks (or virtual source
1796    /// datasets) are written.
1797    Incr,
1798}
1799
1800/// Which mapped data an unlimited virtual dataset's extent covers —
1801/// libhdf5's `H5D_vds_view_t`, set with `H5Pset_virtual_view` and read back
1802/// with `H5Pget_virtual_view` (H5Pdapl.c:1067, :1102).
1803///
1804/// A *dataset access* property: it is never stored in the file, so it says
1805/// how *this* open reads a virtual dataset, not what its writer intended.
1806#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1807pub enum VirtualView {
1808    /// `H5D_VDS_LAST_AVAILABLE` — the extent reaches the end of the last
1809    /// mapped block that has a source, so a gap before it reads as the fill
1810    /// value. libhdf5's default (`H5D_ACS_VDS_VIEW_DEF`, H5Pdapl.c:62).
1811    #[default]
1812    LastAvailable,
1813    /// `H5D_VDS_FIRST_MISSING` — the extent stops where the first missing
1814    /// mapped block begins, so no unmapped block is inside it.
1815    ///
1816    /// Under this view libhdf5 ignores
1817    /// [`virtual_printf_gap`](DatasetAccess::virtual_printf_gap) entirely:
1818    /// `H5D__virtual_init` reads the gap property only for
1819    /// [`LastAvailable`](Self::LastAvailable) and forces it to 0 otherwise
1820    /// (H5Dvirtual.c:2182-2188).
1821    FirstMissing,
1822}
1823
1824/// The dataset *access* properties this crate models — libhdf5's
1825/// `H5P_DATASET_ACCESS` property list, as much of it as affects reading.
1826///
1827/// [`virtual_view`](Self::virtual_view) and
1828/// [`virtual_printf_gap`](Self::virtual_printf_gap) govern how a virtual
1829/// dataset's extent is resolved when it is opened
1830/// (`H5D__virtual_set_extent_unlim`, H5Dvirtual.c:1386);
1831/// [`virtual_prefix`](Self::virtual_prefix) and
1832/// [`efile_prefix`](Self::efile_prefix) say where the *other files* a
1833/// dataset's data lives in are looked for. None of them is stored in the
1834/// file: opening a dataset without naming them reads it exactly as
1835/// libhdf5's default dapl does.
1836///
1837/// Pass one to [`H5File::dataset_with`](crate::H5File::dataset_with).
1838///
1839/// ```no_run
1840/// use rust_hdf5::{DatasetAccess, H5File, VirtualView};
1841///
1842/// let file = H5File::open("vds.h5").unwrap();
1843/// let access = DatasetAccess::new()
1844///     .virtual_view(VirtualView::LastAvailable)
1845///     .virtual_printf_gap(2);
1846/// let ds = file.dataset_with("vds", access).unwrap();
1847/// ```
1848#[derive(Debug, Clone, PartialEq, Eq, Default)]
1849pub struct DatasetAccess {
1850    view: VirtualView,
1851    printf_gap: u64,
1852    virtual_prefix: Option<String>,
1853    efile_prefix: Option<String>,
1854}
1855
1856impl DatasetAccess {
1857    /// A property list holding libhdf5's defaults —
1858    /// [`VirtualView::LastAvailable`] and a printf gap of 0, the values
1859    /// `H5D_ACS_VDS_VIEW_DEF` and `H5D_ACS_VDS_PRINTF_GAP_DEF` register
1860    /// (H5Pdapl.c:62, :67).
1861    pub fn new() -> Self {
1862        Self::default()
1863    }
1864
1865    /// `H5Pset_virtual_view` (H5Pdapl.c:1067). The two legal values are the
1866    /// two [`VirtualView`] variants, so the "not a valid bounds option"
1867    /// argument check that call makes has nothing to reject here.
1868    pub fn virtual_view(mut self, view: VirtualView) -> Self {
1869        self.view = view;
1870        self
1871    }
1872
1873    /// `H5Pset_virtual_printf_gap` (H5Pdapl.c:1207): how many consecutive
1874    /// missing printf-named source datasets the extent resolution looks past
1875    /// before it stops. 0 — the default — stops at the first one missing.
1876    ///
1877    /// `u64::MAX` is libhdf5's `HSIZE_UNDEF`, which that call rejects as "not
1878    /// a valid printf gap size"; here the rejection surfaces from the open
1879    /// that uses the property, since a builder method has no way to report
1880    /// it.
1881    pub fn virtual_printf_gap(mut self, gap: u64) -> Self {
1882        self.printf_gap = gap;
1883        self
1884    }
1885
1886    /// `H5Pget_virtual_view` (H5Pdapl.c:1102).
1887    pub fn view(&self) -> VirtualView {
1888        self.view
1889    }
1890
1891    /// `H5Pset_virtual_prefix` (H5Pdapl.c:1478): a directory a virtual
1892    /// dataset's *source file names* are looked for under, before the
1893    /// virtual file's own directory and after `HDF5_VDS_PREFIX`.
1894    ///
1895    /// It is the third step of `H5F_prefix_open_file`'s search order
1896    /// (H5Fint.c:938-950), and it is reached only when `HDF5_VDS_PREFIX` is
1897    /// unset or empty: `H5D__build_file_prefix` reads the environment first
1898    /// and falls back to this property (H5Dint.c:1077-1082), so an
1899    /// environment prefix shadows this one outright rather than being tried
1900    /// alongside it.
1901    ///
1902    /// A leading `${ORIGIN}` stands for the directory holding the virtual
1903    /// dataset's own file (H5Dint.c:1105-1113), and `"."` or `""` means "no
1904    /// prefix" (:1096-1100), both exactly as for the environment variable.
1905    ///
1906    /// Like the other two, this is a *dataset access* property that is never
1907    /// stored in the file, and the first open of a virtual dataset fixes it
1908    /// for every open that overlaps it.
1909    pub fn virtual_prefix(mut self, prefix: impl Into<String>) -> Self {
1910        self.virtual_prefix = Some(prefix.into());
1911        self
1912    }
1913
1914    /// `H5Pset_efile_prefix` (H5Pdapl.c:1392): a directory the *raw data
1915    /// files* of a dataset stored through an external file list are looked
1916    /// for under.
1917    ///
1918    /// This one takes no search at all, unlike the other two prefixes:
1919    /// `H5D__efl_read` joins the prefix to the stored name with
1920    /// `H5_combine_path` and opens exactly that one path (H5Defl.c:315-317).
1921    /// With no prefix in force the stored name is used as written, so a
1922    /// relative one resolves against the *process's current directory* and
1923    /// not against the directory holding the HDF5 file — measured under
1924    /// libhdf5 1.14.6 and 2.0.0: a raw data file next to the HDF5 file is
1925    /// not found, while the same name under the current directory is.
1926    ///
1927    /// It shares [`virtual_prefix`](Self::virtual_prefix)'s expansion rules,
1928    /// because both are built by `H5D__build_file_prefix`: `HDF5_EXTFILE_PREFIX`
1929    /// shadows this property outright rather than merely preceding it
1930    /// (H5Dint.c:1084-1090), a leading `${ORIGIN}` stands for the directory
1931    /// holding the HDF5 file (:1105-1113), and `"."` or `""` means no prefix
1932    /// (:1098-1102).
1933    ///
1934    /// # A second open must name the same one
1935    ///
1936    /// Where a mismatched [`virtual_prefix`](Self::virtual_prefix) is
1937    /// silently ignored by the second open, a mismatched external file prefix
1938    /// is an *error*: `H5D_open` compares the expanded prefix against the one
1939    /// the already-open dataset resolved under and refuses the open when they
1940    /// differ (H5Dint.c:1533-1545). Expanded, so two opens that differ only
1941    /// in a property the environment shadows still agree. Closing every
1942    /// handle releases the answer, and the next open sets its own.
1943    pub fn efile_prefix(mut self, prefix: impl Into<String>) -> Self {
1944        self.efile_prefix = Some(prefix.into());
1945        self
1946    }
1947
1948    /// `H5Pget_virtual_printf_gap` (H5Pdapl.c:1243) — the value set, not the
1949    /// one the extent resolution ends up using; see
1950    /// [`VirtualView::FirstMissing`].
1951    pub fn printf_gap(&self) -> u64 {
1952        self.printf_gap
1953    }
1954
1955    /// `H5Pget_virtual_prefix` (H5Pdapl.c:1510) — the property as set, before
1956    /// `HDF5_VDS_PREFIX` gets to shadow it and before `${ORIGIN}` is
1957    /// expanded. `None` is `H5D_ACS_VDS_PREFIX_DEF`, a null prefix
1958    /// (H5Pdapl.c:72).
1959    pub fn virtual_prefix_value(&self) -> Option<&str> {
1960        self.virtual_prefix.as_deref()
1961    }
1962
1963    /// `H5Pget_efile_prefix` (H5Pdapl.c:1422) — the property as set, before
1964    /// `HDF5_EXTFILE_PREFIX` gets to shadow it and before `${ORIGIN}` is
1965    /// expanded. `None` is `H5D_ACS_EFILE_PREFIX_DEF`, a null prefix
1966    /// (H5Pdapl.c:90).
1967    pub fn efile_prefix_value(&self) -> Option<&str> {
1968        self.efile_prefix.as_deref()
1969    }
1970
1971    /// The printf gap `H5D__virtual_set_extent_unlim` actually scans with:
1972    /// the property under [`VirtualView::LastAvailable`], and 0 under
1973    /// [`VirtualView::FirstMissing`], because `H5D__virtual_init` only reads
1974    /// the property in the first case (H5Dvirtual.c:2182-2188).
1975    ///
1976    /// The single owner of that rule — the resolution never reads
1977    /// [`printf_gap`](Self::printf_gap) directly.
1978    pub(crate) fn effective_printf_gap(&self) -> u64 {
1979        match self.view {
1980            VirtualView::LastAvailable => self.printf_gap,
1981            VirtualView::FirstMissing => 0,
1982        }
1983    }
1984
1985    /// Reject what `H5Pset_virtual_printf_gap` rejects, at the open that uses
1986    /// the property.
1987    pub(crate) fn validate(&self) -> Result<()> {
1988        if self.printf_gap == u64::MAX {
1989            return Err(Hdf5Error::InvalidState(
1990                "virtual_printf_gap(u64::MAX) is libhdf5's HSIZE_UNDEF, which \
1991                 H5Pset_virtual_printf_gap refuses as \"not a valid printf gap size\""
1992                    .into(),
1993            ));
1994        }
1995        Ok(())
1996    }
1997}
1998
1999/// Elements a selection of `counts` holds, refusing a product that overflows
2000/// `usize` rather than wrapping it into a small allocation.
2001fn element_count(counts: &[u64]) -> Result<usize> {
2002    counts
2003        .iter()
2004        .try_fold(1usize, |acc, &c| {
2005            usize::try_from(c).ok().and_then(|c| acc.checked_mul(c))
2006        })
2007        .ok_or_else(|| {
2008            Hdf5Error::InvalidState(format!("selection {counts:?} has more elements than usize"))
2009        })
2010}
2011
2012impl H5Dataset {
2013    /// Create a reader-mode dataset handle (called internally by `H5File::dataset`).
2014    pub(crate) fn new_reader(
2015        file_inner: SharedInner,
2016        name: String,
2017        shape: Vec<usize>,
2018        element_size: usize,
2019        open: Option<crate::io::reader::DatasetOpenToken>,
2020    ) -> Self {
2021        Self {
2022            file_inner,
2023            info: DatasetInfo::Reader {
2024                name,
2025                shape,
2026                element_size,
2027            },
2028            _open: open,
2029        }
2030    }
2031
2032    /// Create a writer-mode dataset handle for an already-created dataset
2033    /// (called internally by [`H5File::dataset_writer`](crate::file::H5File::dataset_writer)).
2034    ///
2035    /// Reconstructs the same handle `new_dataset().create()` returns, so the
2036    /// reopened dataset supports attribute writes and chunk appends.
2037    ///
2038    /// `is_null` is always `false` here: reopening an existing NULL-dataspace
2039    /// dataset for further writes is not a case this constructor's caller
2040    /// distinguishes (a NULL dataset has nothing to append or chunk-write in
2041    /// the first place).
2042    pub(crate) fn new_writer(
2043        file_inner: SharedInner,
2044        index: usize,
2045        parts: crate::io::writer::DatasetHandleParts,
2046    ) -> Self {
2047        Self {
2048            file_inner,
2049            info: DatasetInfo::Writer {
2050                index,
2051                shape: parts.shape,
2052                element_size: parts.element_size,
2053                chunk_index: parts.chunk_index,
2054                is_null: false,
2055            },
2056            _open: parts.open,
2057        }
2058    }
2059
2060    /// Return the dataset dimensions.
2061    pub fn shape(&self) -> Vec<usize> {
2062        match &self.info {
2063            DatasetInfo::Writer { shape, .. } => shape.clone(),
2064            DatasetInfo::Reader { shape, .. } => shape.clone(),
2065        }
2066    }
2067
2068    /// Return the number of dimensions (rank) of the dataset.
2069    pub fn ndims(&self) -> usize {
2070        match &self.info {
2071            DatasetInfo::Writer { shape, .. } => shape.len(),
2072            DatasetInfo::Reader { shape, .. } => shape.len(),
2073        }
2074    }
2075
2076    /// Return the total number of elements in the dataset.
2077    ///
2078    /// 0 for a NULL dataspace ([`is_null`](Self::is_null)) — unlike a scalar,
2079    /// whose `shape()` is the same empty `Vec` but which holds exactly one
2080    /// element, so `shape().iter().product()` cannot be used here.
2081    pub fn total_elements(&self) -> usize {
2082        if self.is_null() {
2083            return 0;
2084        }
2085        match &self.info {
2086            DatasetInfo::Writer { shape, .. } => shape.iter().product(),
2087            DatasetInfo::Reader { shape, .. } => shape.iter().product(),
2088        }
2089    }
2090
2091    /// Return the size of one element in bytes.
2092    pub fn element_size(&self) -> usize {
2093        match &self.info {
2094            DatasetInfo::Writer { element_size, .. } => *element_size,
2095            DatasetInfo::Reader { element_size, .. } => *element_size,
2096        }
2097    }
2098
2099    /// Return whether this dataset has the NULL dataspace: no elements at
2100    /// all, distinct from a scalar dataset (rank 0, exactly one element) —
2101    /// both report the same empty [`shape`](Self::shape). See
2102    /// [`DatasetBuilder::null`].
2103    pub fn is_null(&self) -> bool {
2104        match &self.info {
2105            DatasetInfo::Writer { is_null, .. } => *is_null,
2106            DatasetInfo::Reader { name, .. } => {
2107                let mut inner = borrow_inner_mut(&self.file_inner);
2108                match &mut *inner {
2109                    H5FileInner::Reader(reader) => reader
2110                        .dataset_info(name)
2111                        .map(|info| info.dataspace.is_null())
2112                        .unwrap_or(false),
2113                    _ => false,
2114                }
2115            }
2116        }
2117    }
2118
2119    /// Return the element datatype as parsed from the file (read mode only).
2120    ///
2121    /// Unlike [`element_size`](Self::element_size), which reports only the
2122    /// byte width, this exposes the full datatype: its class (integer vs
2123    /// floating-point vs string vs compound …), signedness, byte order and
2124    /// bit precision. Callers that must reconstruct the exact stored type —
2125    /// for example to map it to a NumPy / Arrow dtype — should use this
2126    /// instead of inferring a type from the byte width, which cannot
2127    /// distinguish `u8` from `i8` (both 1 byte) or `i32` from `f32` (both 4
2128    /// bytes).
2129    ///
2130    /// # Errors
2131    ///
2132    /// Returns an error if the file is in write mode, or if the dataset can
2133    /// no longer be found in the reader's metadata.
2134    ///
2135    /// ```no_run
2136    /// # use rust_hdf5::{H5File, DatatypeMessage};
2137    /// let file = H5File::open("data.h5").unwrap();
2138    /// let ds = file.dataset("image").unwrap();
2139    /// match ds.datatype().unwrap() {
2140    ///     DatatypeMessage::FixedPoint { size, signed, .. } => {
2141    ///         println!("integer: {} bytes, signed={}", size, signed);
2142    ///     }
2143    ///     DatatypeMessage::FloatingPoint { size, .. } => {
2144    ///         println!("float: {} bytes", size);
2145    ///     }
2146    ///     other => println!("other type: {other}"),
2147    /// }
2148    /// ```
2149    pub fn datatype(&self) -> Result<DatatypeMessage> {
2150        match &self.info {
2151            DatasetInfo::Reader { name, .. } => {
2152                let mut inner = borrow_inner_mut(&self.file_inner);
2153                match &mut *inner {
2154                    H5FileInner::Reader(reader) => reader
2155                        .dataset_info(name)
2156                        .map(|info| info.datatype.clone())
2157                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2158                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2159                }
2160            }
2161            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2162                "datatype() is only available in read mode".into(),
2163            )),
2164        }
2165    }
2166
2167    /// Return the chunk dimensions, if this is a chunked dataset.
2168    pub fn chunk_dims(&self) -> Option<Vec<usize>> {
2169        match &self.info {
2170            DatasetInfo::Reader { name, .. } => {
2171                let mut inner = borrow_inner_mut(&self.file_inner);
2172                if let H5FileInner::Reader(reader) = &mut *inner {
2173                    if let Some(info) = reader.dataset_info(name) {
2174                        use crate::format::messages::data_layout::DataLayoutMessage;
2175                        let chunk_dims = match &info.layout {
2176                            DataLayoutMessage::ChunkedV4 { chunk_dims, .. }
2177                            | DataLayoutMessage::ChunkedV3 { chunk_dims, .. } => Some(chunk_dims),
2178                            _ => None,
2179                        };
2180                        if let Some(chunk_dims) = chunk_dims {
2181                            // Strip trailing element-size dimension
2182                            return Some(
2183                                chunk_dims[..chunk_dims.len() - 1]
2184                                    .iter()
2185                                    .map(|&d| d as usize)
2186                                    .collect(),
2187                            );
2188                        }
2189                    }
2190                }
2191                None
2192            }
2193            DatasetInfo::Writer { .. } => None,
2194        }
2195    }
2196
2197    /// Return whether this is a chunked dataset.
2198    pub fn is_chunked(&self) -> bool {
2199        match &self.info {
2200            DatasetInfo::Writer { chunk_index, .. } => chunk_index.is_some(),
2201            DatasetInfo::Reader { name, .. } => {
2202                let mut inner = borrow_inner_mut(&self.file_inner);
2203                match &mut *inner {
2204                    H5FileInner::Reader(reader) => {
2205                        if let Some(info) = reader.dataset_info(name) {
2206                            use crate::format::messages::data_layout::DataLayoutMessage;
2207                            matches!(
2208                                info.layout,
2209                                DataLayoutMessage::ChunkedV4 { .. }
2210                                    | DataLayoutMessage::ChunkedV3 { .. }
2211                            )
2212                        } else {
2213                            false
2214                        }
2215                    }
2216                    _ => false,
2217                }
2218            }
2219        }
2220    }
2221
2222    /// Return the dataset's storage layout class (read mode only).
2223    ///
2224    /// # Errors
2225    ///
2226    /// Returns an error if the file is in write mode, or if the dataset can
2227    /// no longer be found in the reader's metadata.
2228    pub fn storage_layout(&self) -> Result<StorageLayout> {
2229        match &self.info {
2230            DatasetInfo::Reader { name, .. } => {
2231                let mut inner = borrow_inner_mut(&self.file_inner);
2232                match &mut *inner {
2233                    H5FileInner::Reader(reader) => {
2234                        use crate::format::messages::data_layout::DataLayoutMessage;
2235                        reader
2236                            .dataset_info(name)
2237                            .map(|info| match &info.layout {
2238                                DataLayoutMessage::Compact { .. } => StorageLayout::Compact,
2239                                DataLayoutMessage::Contiguous { .. } => StorageLayout::Contiguous,
2240                                DataLayoutMessage::ChunkedV3 { .. }
2241                                | DataLayoutMessage::ChunkedV4 { .. } => StorageLayout::Chunked,
2242                                DataLayoutMessage::Virtual { .. } => StorageLayout::Virtual,
2243                            })
2244                            .ok_or_else(|| Hdf5Error::NotFound(name.clone()))
2245                    }
2246                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2247                }
2248            }
2249            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2250                "storage_layout() is only available in read mode".into(),
2251            )),
2252        }
2253    }
2254
2255    /// Return the chunk index structure this dataset's layout uses, or
2256    /// `None` for a dataset that is not chunked (read mode only).
2257    ///
2258    /// # Errors
2259    ///
2260    /// Returns an error if the file is in write mode, or if the dataset can
2261    /// no longer be found in the reader's metadata.
2262    pub fn chunk_index(&self) -> Result<Option<ChunkIndex>> {
2263        match &self.info {
2264            DatasetInfo::Reader { name, .. } => {
2265                let mut inner = borrow_inner_mut(&self.file_inner);
2266                match &mut *inner {
2267                    H5FileInner::Reader(reader) => {
2268                        use crate::format::messages::data_layout::{
2269                            ChunkIndexType, DataLayoutMessage,
2270                        };
2271                        reader
2272                            .dataset_info(name)
2273                            .map(|info| match &info.layout {
2274                                DataLayoutMessage::ChunkedV3 { .. } => Some(ChunkIndex::BtreeV1),
2275                                DataLayoutMessage::ChunkedV4 { index_type, .. } => {
2276                                    Some(match index_type {
2277                                        ChunkIndexType::SingleChunk => ChunkIndex::SingleChunk,
2278                                        ChunkIndexType::Implicit => ChunkIndex::Implicit,
2279                                        ChunkIndexType::FixedArray => ChunkIndex::FixedArray,
2280                                        ChunkIndexType::ExtensibleArray => {
2281                                            ChunkIndex::ExtensibleArray
2282                                        }
2283                                        ChunkIndexType::BTreeV2 => ChunkIndex::BtreeV2,
2284                                    })
2285                                }
2286                                _ => None,
2287                            })
2288                            .ok_or_else(|| Hdf5Error::NotFound(name.clone()))
2289                    }
2290                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2291                }
2292            }
2293            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2294                "chunk_index() is only available in read mode".into(),
2295            )),
2296        }
2297    }
2298
2299    /// Return this dataset's filter pipeline (read mode only), in
2300    /// application order. Empty when the dataset has no filter pipeline
2301    /// message at all — an unfiltered dataset, not an error.
2302    ///
2303    /// # Errors
2304    ///
2305    /// Returns an error if the file is in write mode, or if the dataset can
2306    /// no longer be found in the reader's metadata.
2307    pub fn filters(&self) -> Result<Vec<Filter>> {
2308        match &self.info {
2309            DatasetInfo::Reader { name, .. } => {
2310                let mut inner = borrow_inner_mut(&self.file_inner);
2311                match &mut *inner {
2312                    H5FileInner::Reader(reader) => reader
2313                        .dataset_info(name)
2314                        .map(|info| {
2315                            info.filter_pipeline
2316                                .as_ref()
2317                                .map(|fp| fp.filters.clone())
2318                                .unwrap_or_default()
2319                        })
2320                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2321                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2322                }
2323            }
2324            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2325                "filters() is only available in read mode".into(),
2326            )),
2327        }
2328    }
2329
2330    /// Return this dataset's fill-value state (read mode only).
2331    ///
2332    /// # Errors
2333    ///
2334    /// Returns an error if the file is in write mode, or if the dataset can
2335    /// no longer be found in the reader's metadata.
2336    pub fn fill_value(&self) -> Result<FillValue> {
2337        match &self.info {
2338            DatasetInfo::Reader { name, .. } => {
2339                let mut inner = borrow_inner_mut(&self.file_inner);
2340                match &mut *inner {
2341                    H5FileInner::Reader(reader) => reader
2342                        .dataset_info(name)
2343                        .map(|info| match info.fill_defined {
2344                            0 => FillValue::Undefined,
2345                            2 => {
2346                                FillValue::UserDefined(info.fill_value.clone().unwrap_or_default())
2347                            }
2348                            _ => FillValue::Default,
2349                        })
2350                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2351                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2352                }
2353            }
2354            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2355                "fill_value() is only available in read mode".into(),
2356            )),
2357        }
2358    }
2359
2360    /// Return when this dataset's fill value is written into allocated
2361    /// storage (read mode only) — `H5Pget_fill_time`.
2362    ///
2363    /// # Errors
2364    ///
2365    /// Returns an error if the file is in write mode, or if the dataset can
2366    /// no longer be found in the reader's metadata.
2367    pub fn fill_time(&self) -> Result<FillTime> {
2368        match &self.info {
2369            DatasetInfo::Reader { name, .. } => {
2370                let mut inner = borrow_inner_mut(&self.file_inner);
2371                match &mut *inner {
2372                    H5FileInner::Reader(reader) => reader
2373                        .dataset_info(name)
2374                        .map(|info| match info.fill_write_time {
2375                            0 => FillTime::Alloc,
2376                            1 => FillTime::Never,
2377                            _ => FillTime::IfSet,
2378                        })
2379                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2380                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2381                }
2382            }
2383            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2384                "fill_time() is only available in read mode".into(),
2385            )),
2386        }
2387    }
2388
2389    /// Return when this dataset's raw-data storage is allocated (read mode
2390    /// only) — `H5Pget_alloc_time`.
2391    ///
2392    /// # Errors
2393    ///
2394    /// Returns an error if the file is in write mode, or if the dataset can
2395    /// no longer be found in the reader's metadata.
2396    pub fn alloc_time(&self) -> Result<AllocTime> {
2397        match &self.info {
2398            DatasetInfo::Reader { name, .. } => {
2399                let mut inner = borrow_inner_mut(&self.file_inner);
2400                match &mut *inner {
2401                    H5FileInner::Reader(reader) => reader
2402                        .dataset_info(name)
2403                        .map(|info| match info.alloc_time {
2404                            1 => AllocTime::Early,
2405                            3 => AllocTime::Incr,
2406                            _ => AllocTime::Late,
2407                        })
2408                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2409                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2410                }
2411            }
2412            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2413                "alloc_time() is only available in read mode".into(),
2414            )),
2415        }
2416    }
2417
2418    /// Return this dataset's external raw-data file segments (read mode
2419    /// only), in the order the dataset's logical byte range concatenates
2420    /// them. Empty for a dataset whose data lives in this file.
2421    ///
2422    /// # Errors
2423    ///
2424    /// Returns an error if the file is in write mode, or if the dataset can
2425    /// no longer be found in the reader's metadata.
2426    pub fn external_files(&self) -> Result<Vec<ExternalFileSegment>> {
2427        match &self.info {
2428            DatasetInfo::Reader { name, .. } => {
2429                let mut inner = borrow_inner_mut(&self.file_inner);
2430                match &mut *inner {
2431                    H5FileInner::Reader(reader) => reader
2432                        .dataset_info(name)
2433                        .map(|info| info.external_files.clone())
2434                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2435                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2436                }
2437            }
2438            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2439                "external_files() is only available in read mode".into(),
2440            )),
2441        }
2442    }
2443
2444    /// Return this dataset's maximum dimension sizes (read mode only):
2445    /// `None` in a dimension marks that axis unlimited. A dataset with no
2446    /// maximum-dimensions message reports its current shape (max == current
2447    /// — the upstream convention for a fixed-extent dataset).
2448    ///
2449    /// # Errors
2450    ///
2451    /// Returns an error if the file is in write mode, or if the dataset can
2452    /// no longer be found in the reader's metadata.
2453    pub fn max_shape(&self) -> Result<Vec<Option<usize>>> {
2454        match &self.info {
2455            DatasetInfo::Reader { name, .. } => {
2456                let mut inner = borrow_inner_mut(&self.file_inner);
2457                match &mut *inner {
2458                    H5FileInner::Reader(reader) => reader
2459                        .dataset_info(name)
2460                        .map(|info| match &info.dataspace.max_dims {
2461                            Some(max_dims) => max_dims
2462                                .iter()
2463                                .map(|&d| (d != u64::MAX).then_some(d as usize))
2464                                .collect(),
2465                            None => info
2466                                .dataspace
2467                                .dims
2468                                .iter()
2469                                .map(|&d| Some(d as usize))
2470                                .collect(),
2471                        })
2472                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2473                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2474                }
2475            }
2476            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2477                "max_shape() is only available in read mode".into(),
2478            )),
2479        }
2480    }
2481
2482    /// Return this dataset's virtual-dataset source/virtual mappings (read
2483    /// mode only), in on-disk order. Empty for any dataset whose layout is
2484    /// not virtual, and for a virtual dataset that has no mappings yet.
2485    ///
2486    /// # Errors
2487    ///
2488    /// Returns an error if the file is in write mode, or if the dataset can
2489    /// no longer be found in the reader's metadata.
2490    pub fn virtual_mappings(&self) -> Result<Vec<VirtualMapping>> {
2491        match &self.info {
2492            DatasetInfo::Reader { name, .. } => {
2493                let mut inner = borrow_inner_mut(&self.file_inner);
2494                match &mut *inner {
2495                    H5FileInner::Reader(reader) => reader
2496                        .dataset_info(name)
2497                        .map(|info| {
2498                            info.virtual_mappings
2499                                .as_ref()
2500                                .map(|vml| vml.mappings.clone())
2501                                .unwrap_or_default()
2502                        })
2503                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
2504                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2505                }
2506            }
2507            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2508                "virtual_mappings() is only available in read mode".into(),
2509            )),
2510        }
2511    }
2512
2513    /// Return the names of all attributes on this dataset (read mode only).
2514    pub fn attr_names(&self) -> Result<Vec<String>> {
2515        match &self.info {
2516            DatasetInfo::Reader { name, .. } => {
2517                let mut inner = borrow_inner_mut(&self.file_inner);
2518                match &mut *inner {
2519                    H5FileInner::Reader(reader) => Ok(reader.dataset_attr_names(name)?),
2520                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2521                }
2522            }
2523            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2524                "attr_names not available in write mode".into(),
2525            )),
2526        }
2527    }
2528
2529    /// Why the attribute `attr_name` on this dataset cannot be read, or `None`
2530    /// when it can be.
2531    ///
2532    /// An attribute whose message this crate cannot decode is still listed by
2533    /// [`attr_names`](Self::attr_names) — the object header carries it — and
2534    /// this says what stands in the way. Opening it through
2535    /// [`attr`](Self::attr) fails with the same text.
2536    pub fn attr_unreadable_reason(&self, attr_name: &str) -> Result<Option<String>> {
2537        match &self.info {
2538            DatasetInfo::Reader { name, .. } => {
2539                let mut inner = borrow_inner_mut(&self.file_inner);
2540                match &mut *inner {
2541                    H5FileInner::Reader(reader) => Ok(reader
2542                        .dataset_attr_unreadable_reason(name, attr_name)
2543                        .map(str::to_string)),
2544                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2545                }
2546            }
2547            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2548                "attr_unreadable_reason not available in write mode".into(),
2549            )),
2550        }
2551    }
2552
2553    /// Why this dataset's attribute *set* cannot be listed, or `None` when it
2554    /// can be.
2555    ///
2556    /// The object-scope counterpart of
2557    /// [`attr_unreadable_reason`](Self::attr_unreadable_reason). A dense
2558    /// attribute set is indexed by name hash, so a heap or index that will not
2559    /// read yields no names to hang a per-attribute reason on;
2560    /// [`attr_names`](Self::attr_names) then returns the failure rather than a
2561    /// short list, and this reports it without an attribute name.
2562    pub fn attrs_unreadable_reason(&self) -> Result<Option<String>> {
2563        match &self.info {
2564            DatasetInfo::Reader { name, .. } => {
2565                let mut inner = borrow_inner_mut(&self.file_inner);
2566                match &mut *inner {
2567                    H5FileInner::Reader(reader) => Ok(reader
2568                        .dataset_attrs_unreadable_reason(name)
2569                        .map(str::to_string)),
2570                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2571                }
2572            }
2573            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2574                "attrs_unreadable_reason not available in write mode".into(),
2575            )),
2576        }
2577    }
2578
2579    /// This dataset's own compact-vs-dense attribute storage — the
2580    /// equivalent of `h5py.h5o.get_info(did.id).meta_size.attr.index_size`
2581    /// being nonzero (read mode only).
2582    pub fn attr_storage(&self) -> Result<AttributeStorage> {
2583        match &self.info {
2584            DatasetInfo::Reader { name, .. } => {
2585                let mut inner = borrow_inner_mut(&self.file_inner);
2586                match &mut *inner {
2587                    H5FileInner::Reader(reader) => Ok(reader.dataset_attr_storage(name)?),
2588                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2589                }
2590            }
2591            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2592                "attr_storage not available in write mode".into(),
2593            )),
2594        }
2595    }
2596
2597    /// This dataset's own object-header attribute count — the equivalent of
2598    /// `h5py.h5o.get_info(did.id).num_attrs` (read mode only).
2599    pub fn header_attr_count(&self) -> Result<u64> {
2600        match &self.info {
2601            DatasetInfo::Reader { name, .. } => {
2602                let mut inner = borrow_inner_mut(&self.file_inner);
2603                match &mut *inner {
2604                    H5FileInner::Reader(reader) => Ok(reader.dataset_header_attr_count(name)?),
2605                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2606                }
2607            }
2608            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2609                "header_attr_count not available in write mode".into(),
2610            )),
2611        }
2612    }
2613
2614    /// Open an attribute by name (read mode only).
2615    pub fn attr(&self, attr_name: &str) -> Result<crate::attribute::H5Attribute> {
2616        match &self.info {
2617            DatasetInfo::Reader { name, .. } => {
2618                let mut inner = borrow_inner_mut(&self.file_inner);
2619                match &mut *inner {
2620                    H5FileInner::Reader(reader) => {
2621                        let attr_msg = reader.dataset_attr(name, attr_name)?.clone();
2622                        Ok(crate::attribute::H5Attribute::new_reader(
2623                            clone_inner(&self.file_inner),
2624                            attr_msg,
2625                        ))
2626                    }
2627                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
2628                }
2629            }
2630            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
2631                "attr() not available in write mode".into(),
2632            )),
2633        }
2634    }
2635
2636    /// Start building a new attribute on this dataset.
2637    ///
2638    /// Returns a fluent builder. Call `.shape(())` for a scalar attribute
2639    /// and `.create("name")` to finalize.
2640    ///
2641    /// # Example
2642    ///
2643    /// ```no_run
2644    /// # use rust_hdf5::H5File;
2645    /// # use rust_hdf5::types::VarLenUnicode;
2646    /// let file = H5File::create("attr.h5").unwrap();
2647    /// let ds = file.new_dataset::<f32>().shape(&[10]).create("data").unwrap();
2648    /// let attr = ds.new_attr::<VarLenUnicode>().shape(()).create("units").unwrap();
2649    /// attr.write_scalar(&VarLenUnicode("meters".to_string())).unwrap();
2650    /// ```
2651    pub fn new_attr<T: 'static>(&self) -> AttrBuilder<'_, T> {
2652        let ds_index = match &self.info {
2653            DatasetInfo::Writer { index, .. } => *index,
2654            DatasetInfo::Reader { .. } => {
2655                // Reader mode: we'll return a builder that will error on create.
2656                // Using usize::MAX as sentinel.
2657                usize::MAX
2658            }
2659        };
2660        AttrBuilder::new(&self.file_inner, ds_index)
2661    }
2662
2663    /// Add a scalar attribute to this dataset whose datatype and raw value the
2664    /// caller supplies — the dataset form of
2665    /// [`H5File::set_attr_typed`](crate::H5File::set_attr_typed), for a type
2666    /// [`new_attr`](Self::new_attr) has no Rust carrier for, such as a
2667    /// fixed-length string sized by `H5Tset_size`.
2668    pub fn set_attr_typed(
2669        &self,
2670        name: &str,
2671        datatype: DatatypeMessage,
2672        value: Vec<u8>,
2673    ) -> Result<()> {
2674        let attr = crate::attribute::typed_scalar_attr(name, datatype, value)?;
2675        let DatasetInfo::Writer { index, .. } = &self.info else {
2676            return Err(Hdf5Error::InvalidState(
2677                "cannot write attributes in read mode".into(),
2678            ));
2679        };
2680        match &*borrow_inner(&self.file_inner) {
2681            H5FileInner::Writer(writer) => {
2682                writer.add_dataset_attribute(*index, attr)?;
2683                Ok(())
2684            }
2685            H5FileInner::Reader(_) => Err(Hdf5Error::InvalidState(
2686                "cannot write attributes in read mode".into(),
2687            )),
2688            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
2689        }
2690    }
2691
2692    /// Write a typed slice holding the dataset's whole image.
2693    ///
2694    /// The slice length must match the total number of elements declared by
2695    /// the dataset shape. The data is reinterpreted as raw bytes and written
2696    /// to the file: to the contiguous data block, or — for a chunked dataset —
2697    /// scattered across its chunk grid, through the filter pipeline if one is
2698    /// set. To write only part of a dataset, use
2699    /// [`write_slice`](Self::write_slice).
2700    ///
2701    /// # Errors
2702    ///
2703    /// Returns an error if:
2704    /// - The file is in read mode.
2705    /// - The data length does not match the declared shape.
2706    pub fn write_raw<T: H5Type>(&self, data: &[T]) -> Result<()> {
2707        match &self.info {
2708            DatasetInfo::Writer {
2709                index,
2710                shape,
2711                element_size,
2712                chunk_index,
2713                is_null,
2714            } => {
2715                if *is_null {
2716                    return Err(Hdf5Error::InvalidState(
2717                        "cannot write to a NULL dataspace dataset".into(),
2718                    ));
2719                }
2720                let total_elements: usize = shape.iter().product();
2721                if data.len() != total_elements {
2722                    return Err(Hdf5Error::InvalidState(format!(
2723                        "data length {} does not match dataset size {}",
2724                        data.len(),
2725                        total_elements,
2726                    )));
2727                }
2728
2729                // Verify element size matches
2730                if T::element_size() != *element_size {
2731                    return Err(Hdf5Error::TypeMismatch(format!(
2732                        "write type has element size {} but dataset expects {}",
2733                        T::element_size(),
2734                        element_size,
2735                    )));
2736                }
2737
2738                // Safety: T: Copy + 'static (numeric primitive) with well-defined
2739                // byte representation. The resulting slice borrows `data` and
2740                // lives only as long as this block.
2741                let byte_len = data.len() * T::element_size();
2742                let host =
2743                    unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
2744                let datatype = {
2745                    let inner = borrow_inner(&self.file_inner);
2746                    match &*inner {
2747                        H5FileInner::Writer(writer) => writer.dataset_datatype(*index),
2748                        _ => {
2749                            return Err(Hdf5Error::InvalidState(
2750                                "file is no longer in write mode".into(),
2751                            ))
2752                        }
2753                    }
2754                };
2755                let stored = to_stored_byte_order(host, &datatype, T::element_size())?;
2756
2757                if let Some(kind) = *chunk_index {
2758                    // A chunked dataset has no contiguous data block; scatter
2759                    // the full row-major image into its chunk grid and write
2760                    // each chunk through the dataset's filter pipeline.
2761                    return self.write_full_image_chunked(*index, kind, &stored, *element_size);
2762                }
2763
2764                let inner = borrow_inner(&self.file_inner);
2765                match &*inner {
2766                    H5FileInner::Writer(writer) => {
2767                        writer.write_dataset_raw(*index, &stored)?;
2768                        Ok(())
2769                    }
2770                    _ => Err(Hdf5Error::InvalidState(
2771                        "file is no longer in write mode".into(),
2772                    )),
2773                }
2774            }
2775            DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
2776                "cannot write to a dataset opened in read mode".into(),
2777            )),
2778        }
2779    }
2780
2781    /// Write the raw byte image of the whole dataset directly.
2782    ///
2783    /// Takes the same layouts as [`write_raw`](Self::write_raw): a contiguous
2784    /// data block, or a chunk grid the image is scattered across.
2785    ///
2786    /// Unlike [`write_raw`](Self::write_raw), this is not generic over an
2787    /// `H5Type` carrier, so it works for element types that have no matching
2788    /// Rust primitive — in particular a runtime
2789    /// [`CompoundType`](crate::types::CompoundType) of arbitrary size set via
2790    /// [`DatasetBuilder::datatype`]. `bytes.len()` must equal
2791    /// `product(shape) * element_size`, where `element_size` is taken from the
2792    /// dataset's on-disk datatype.
2793    ///
2794    /// ```no_run
2795    /// # use rust_hdf5::H5File;
2796    /// # use rust_hdf5::types::{CompoundType, H5Type};
2797    /// let file = H5File::create("c.h5").unwrap();
2798    /// let ct = CompoundType {
2799    ///     members: vec![
2800    ///         ("id".to_string(), i32::hdf5_type(), 0),
2801    ///         ("val".to_string(), f64::hdf5_type(), 4),
2802    ///     ],
2803    ///     total_size: 12,
2804    /// };
2805    /// let ds = file
2806    ///     .new_dataset::<u8>()
2807    ///     .datatype(ct.to_datatype())
2808    ///     .shape(&[2])
2809    ///     .create("records")
2810    ///     .unwrap();
2811    /// let mut bytes = Vec::new();
2812    /// bytes.extend_from_slice(&1i32.to_le_bytes());
2813    /// bytes.extend_from_slice(&2.5f64.to_le_bytes());
2814    /// bytes.extend_from_slice(&2i32.to_le_bytes());
2815    /// bytes.extend_from_slice(&3.5f64.to_le_bytes());
2816    /// ds.write_raw_bytes(&bytes).unwrap();
2817    /// ```
2818    pub fn write_raw_bytes(&self, bytes: &[u8]) -> Result<()> {
2819        match &self.info {
2820            DatasetInfo::Writer {
2821                index,
2822                shape,
2823                element_size,
2824                chunk_index,
2825                is_null,
2826            } => {
2827                if *is_null {
2828                    return Err(Hdf5Error::InvalidState(
2829                        "cannot write to a NULL dataspace dataset".into(),
2830                    ));
2831                }
2832                let expected: usize = shape.iter().product::<usize>() * *element_size;
2833                if bytes.len() != expected {
2834                    return Err(Hdf5Error::InvalidState(format!(
2835                        "raw byte length {} does not match dataset size {} \
2836                         (product(shape) * element_size {})",
2837                        bytes.len(),
2838                        expected,
2839                        element_size,
2840                    )));
2841                }
2842                if let Some(kind) = *chunk_index {
2843                    // Scatter the full row-major image into the chunk grid
2844                    // (same path as write_raw, carrier-agnostic bytes).
2845                    return self.write_full_image_chunked(*index, kind, bytes, *element_size);
2846                }
2847                let inner = borrow_inner(&self.file_inner);
2848                match &*inner {
2849                    H5FileInner::Writer(writer) => {
2850                        writer.write_dataset_raw(*index, bytes)?;
2851                        Ok(())
2852                    }
2853                    _ => Err(Hdf5Error::InvalidState(
2854                        "file is no longer in write mode".into(),
2855                    )),
2856                }
2857            }
2858            DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
2859                "cannot write to a dataset opened in read mode".into(),
2860            )),
2861        }
2862    }
2863
2864    /// Scatter a full row-major dataset image into its chunk grid, writing
2865    /// every chunk through the dataset's filter pipeline.
2866    ///
2867    /// This is the chunked counterpart of a single contiguous `write_dataset_raw`
2868    /// — it is how [`write_raw`](Self::write_raw) and
2869    /// [`write_raw_bytes`](Self::write_raw_bytes) populate a chunked dataset
2870    /// (including the single auto-chunk created when a filter is set without
2871    /// explicit chunk dimensions). Edge chunks are zero-padded to the full
2872    /// chunk footprint, exactly as libhdf5 stores them.
2873    fn write_full_image_chunked(
2874        &self,
2875        index: usize,
2876        kind: ChunkIndexKind,
2877        bytes: &[u8],
2878        element_size: usize,
2879    ) -> Result<()> {
2880        let inner = borrow_inner(&self.file_inner);
2881        let writer = match &*inner {
2882            H5FileInner::Writer(w) => w,
2883            _ => {
2884                return Err(Hdf5Error::InvalidState(
2885                    "file is no longer in write mode".into(),
2886                ))
2887            }
2888        };
2889        // Whole-operation guard: the flush, the grid snapshot and the chunk
2890        // writes below must not interleave with a concurrent same-dataset
2891        // operation.
2892        let cell = writer.ds(index);
2893        let _op = cell.op.lock();
2894        // A buffered append tail would flush over the image at close; hand
2895        // it to the chunks first, the image below overwrites everything.
2896        writer.flush_append_buffer(index)?;
2897        let chunk_dims = writer
2898            .dataset_chunk_dims(index)
2899            .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
2900            .to_vec();
2901        let dims = writer.dataset_dims(index).to_vec();
2902        let rank = dims.len();
2903
2904        // Chunk grid: number of chunks along each dimension (row-major).
2905        let mut grid = vec![0u64; rank];
2906        for d in 0..rank {
2907            grid[d] = if chunk_dims[d] > 0 {
2908                dims[d].div_ceil(chunk_dims[d])
2909            } else {
2910                0
2911            };
2912        }
2913        let total_chunks: u64 = grid.iter().product();
2914
2915        // Decode the iteration counter into row-major coordinates over the
2916        // *current* image's chunk grid. This is only an odometer over the
2917        // chunks the image spans — the slot a chunk is recorded under comes
2918        // from the index grid (`Hdf5Writer::chunk_slot`), which the maximum
2919        // extent decides.
2920        let coords_of = |linear: u64| -> Vec<u64> {
2921            let mut rem = linear;
2922            let mut coords = vec![0u64; rank];
2923            for d in (0..rank).rev() {
2924                coords[d] = rem % grid[d];
2925                rem /= grid[d];
2926            }
2927            coords
2928        };
2929
2930        // The batch entry points exist for one reason: to run the filter
2931        // pipeline over a window of chunks in parallel. An unfiltered dataset
2932        // has no pipeline to run, so it takes the plain per-chunk owner
2933        // whatever its index is, and only a filtered extensible or fixed array
2934        // — the two indexes with a batch entry point — takes the window below.
2935        let batched = writer.dataset_is_filtered(index)
2936            && matches!(
2937                kind,
2938                ChunkIndexKind::ExtensibleArray | ChunkIndexKind::FixedArray
2939            );
2940        if !batched {
2941            // One staging buffer for the whole image, reused chunk after
2942            // chunk: a chunk that already sits as one complete run of `bytes`
2943            // needs no staging at all and goes to the file straight out of the
2944            // caller's slice, so only an n-D interleave or a short edge pays
2945            // for a gather.
2946            let mut staging = Vec::new();
2947            for linear in 0..total_chunks {
2948                let coords = coords_of(linear);
2949                let chunk =
2950                    match Self::contiguous_chunk_span(&dims, &chunk_dims, &coords, element_size) {
2951                        Some(span) => &bytes[span],
2952                        None => {
2953                            Self::gather_chunk_into(
2954                                &mut staging,
2955                                bytes,
2956                                &dims,
2957                                &chunk_dims,
2958                                &coords,
2959                                element_size,
2960                            );
2961                            &staging[..]
2962                        }
2963                    };
2964                writer.write_chunk_at_coords(index, &coords, chunk)?;
2965            }
2966        } else {
2967            // Hand the pipeline a window of chunks so it compresses them in
2968            // parallel (with the `parallel` feature). A fixed-size window
2969            // bounds peak memory instead of materializing every chunk at once;
2970            // 256 keeps every rayon worker fed while capping the transient
2971            // buffers to window * chunk bytes. The compressors read the window
2972            // concurrently, so a gathered chunk here cannot share one reused
2973            // buffer the way the sequential path above does — but a chunk that
2974            // is already a complete run of `bytes` is borrowed, not copied.
2975            // The two indexes differ only in how a chunk is addressed: EA by
2976            // its linear grid index, FA by grid coordinates.
2977            const BATCH_WINDOW: u64 = 256;
2978            let mut start = 0u64;
2979            while start < total_chunks {
2980                let end = (start + BATCH_WINDOW).min(total_chunks);
2981                let items: Vec<(Vec<u64>, Cow<'_, [u8]>)> = (start..end)
2982                    .map(|counter| {
2983                        let coords = coords_of(counter);
2984                        let data = match Self::contiguous_chunk_span(
2985                            &dims,
2986                            &chunk_dims,
2987                            &coords,
2988                            element_size,
2989                        ) {
2990                            Some(span) => Cow::Borrowed(&bytes[span]),
2991                            None => {
2992                                let mut buf = Vec::new();
2993                                Self::gather_chunk_into(
2994                                    &mut buf,
2995                                    bytes,
2996                                    &dims,
2997                                    &chunk_dims,
2998                                    &coords,
2999                                    element_size,
3000                                );
3001                                Cow::Owned(buf)
3002                            }
3003                        };
3004                        (coords, data)
3005                    })
3006                    .collect();
3007                if kind == ChunkIndexKind::FixedArray {
3008                    let pairs: Vec<(&[u64], &[u8])> = items
3009                        .iter()
3010                        .map(|(c, d)| (c.as_slice(), d.as_ref()))
3011                        .collect();
3012                    writer.write_chunks_fixed_array_batch_inner(index, &pairs)?;
3013                } else {
3014                    let mut pairs: Vec<(u64, &[u8])> = Vec::with_capacity(items.len());
3015                    for (c, d) in &items {
3016                        pairs.push((writer.chunk_slot(index, c)?, d.as_ref()));
3017                    }
3018                    writer.write_chunks_batch_inner(index, &pairs)?;
3019                }
3020                start = end;
3021            }
3022        }
3023        Ok(())
3024    }
3025
3026    /// The byte range one chunk occupies in a row-major full-dataset image,
3027    /// for a chunk that needs no gather at all: its elements are one
3028    /// contiguous run of `source` *and* they fill the chunk shape exactly, so
3029    /// the bytes that go to the file are already sitting in the caller's
3030    /// buffer.
3031    ///
3032    /// Both halves hold when every dimension after the first spans the whole
3033    /// dataset (`chunk_dims[d] == dims[d]`, leaving nothing interleaved and no
3034    /// padding along those axes) and the chunk does not hang off the far edge
3035    /// of the first — which is every full chunk of a 1-D dataset. `None` means
3036    /// the chunk has to be gathered.
3037    fn contiguous_chunk_span(
3038        dims: &[u64],
3039        chunk_dims: &[u64],
3040        coords: &[u64],
3041        element_size: usize,
3042    ) -> Option<std::ops::Range<usize>> {
3043        let rank = dims.len();
3044        if rank == 0 || chunk_dims[1..] != dims[1..] {
3045            return None;
3046        }
3047        if (coords[0] + 1) * chunk_dims[0] > dims[0] {
3048            return None;
3049        }
3050        let plane: u64 = dims[1..].iter().product::<u64>() * element_size as u64;
3051        let start = usize::try_from(coords[0] * chunk_dims[0] * plane).ok()?;
3052        let len = usize::try_from(chunk_dims[0] * plane).ok()?;
3053        Some(start..start.checked_add(len)?)
3054    }
3055
3056    /// Gather one chunk's bytes from a row-major full-dataset image into
3057    /// `out`, replacing whatever it held.
3058    ///
3059    /// `coords` are the chunk's grid coordinates. `out` is left exactly
3060    /// `product(chunk_dims) * element_size` bytes long, holding the chunk's
3061    /// elements and zero where the chunk extends past the dataset edge — so a
3062    /// caller may hand the same buffer to one chunk after another.
3063    fn gather_chunk_into(
3064        out: &mut Vec<u8>,
3065        source: &[u8],
3066        dims: &[u64],
3067        chunk_dims: &[u64],
3068        coords: &[u64],
3069        element_size: usize,
3070    ) {
3071        let rank = dims.len();
3072        let chunk_elems: u64 = chunk_dims.iter().product();
3073        let chunk_bytes = chunk_elems as usize * element_size;
3074        if rank == 0 {
3075            // Scalar dataset: a single element, no chunking dimension.
3076            out.clear();
3077            out.resize(chunk_bytes, 0);
3078            if source.len() >= element_size {
3079                out[..element_size].copy_from_slice(&source[..element_size]);
3080            }
3081            return;
3082        }
3083
3084        // Actual extent of this chunk along each dimension (edge chunks are
3085        // smaller than the nominal chunk shape).
3086        let mut extent = vec![0u64; rank];
3087        for d in 0..rank {
3088            let start = coords[d] * chunk_dims[d];
3089            let end = ((coords[d] + 1) * chunk_dims[d]).min(dims[d]);
3090            extent[d] = end.saturating_sub(start);
3091        }
3092        // Size the buffer, then zero it only when this chunk leaves part of
3093        // its shape uncovered: a full chunk has every byte overwritten below,
3094        // while an edge chunk's padding must read as zero even though a
3095        // reused buffer still holds the previous chunk's bytes.
3096        if out.len() != chunk_bytes {
3097            out.clear();
3098            out.resize(chunk_bytes, 0);
3099        } else if extent != chunk_dims {
3100            out.fill(0);
3101        }
3102        if extent.contains(&0) {
3103            return; // nothing of the dataset falls in this chunk
3104        }
3105
3106        // Row-major strides (in elements) for the source (over `dims`) and the
3107        // destination chunk buffer (over `chunk_dims`).
3108        let mut src_stride = vec![1u64; rank];
3109        let mut dst_stride = vec![1u64; rank];
3110        for d in (0..rank - 1).rev() {
3111            src_stride[d] = src_stride[d + 1] * dims[d + 1];
3112            dst_stride[d] = dst_stride[d + 1] * chunk_dims[d + 1];
3113        }
3114
3115        // Copy one contiguous run along the last axis per outer multi-index.
3116        let last = rank - 1;
3117        let run = extent[last] as usize * element_size;
3118        let outer: u64 = extent[..last].iter().product::<u64>().max(1);
3119        let mut idx = vec![0u64; rank]; // local indices within the chunk extent
3120        for _ in 0..outer {
3121            let mut src_off = 0u64;
3122            let mut dst_off = 0u64;
3123            for d in 0..rank {
3124                let global = coords[d] * chunk_dims[d] + idx[d];
3125                src_off += global * src_stride[d];
3126                dst_off += idx[d] * dst_stride[d];
3127            }
3128            let s = src_off as usize * element_size;
3129            let dpos = dst_off as usize * element_size;
3130            out[dpos..dpos + run].copy_from_slice(&source[s..s + run]);
3131
3132            // Advance the multi-index over axes [0..last); the last axis is the
3133            // contiguous run handled above.
3134            let mut d = last;
3135            while d > 0 {
3136                d -= 1;
3137                idx[d] += 1;
3138                if idx[d] < extent[d] {
3139                    break;
3140                }
3141                idx[d] = 0;
3142            }
3143        }
3144    }
3145
3146    /// Write a single chunk to a chunked dataset.
3147    ///
3148    /// `chunk_idx` is the linear chunk index (typically the frame number for
3149    /// streaming datasets). `data` is the raw byte data for one chunk.
3150    ///
3151    /// For datasets with two or more unlimited dimensions (v2 B-tree index),
3152    /// use [`write_chunk_at`](Self::write_chunk_at) instead.
3153    pub fn write_chunk(&self, chunk_idx: usize, data: &[u8]) -> Result<()> {
3154        match &self.info {
3155            DatasetInfo::Writer {
3156                index, chunk_index, ..
3157            } => {
3158                let Some(kind) = *chunk_index else {
3159                    return Err(Hdf5Error::InvalidState(
3160                        "write_chunk is only for chunked datasets".into(),
3161                    ));
3162                };
3163                if kind == ChunkIndexKind::BtreeV2 {
3164                    return Err(Hdf5Error::InvalidState(
3165                        "this dataset uses a v2 B-tree chunk index; use write_chunk_at \
3166                         with the chunk's grid coordinates"
3167                            .into(),
3168                    ));
3169                }
3170
3171                let inner = borrow_inner(&self.file_inner);
3172                match &*inner {
3173                    H5FileInner::Writer(writer) => {
3174                        // One op: the slot decode and the write see the same
3175                        // extents.
3176                        let cell = writer.ds(*index);
3177                        let _op = cell.op.lock();
3178                        match kind {
3179                            // All four address a chunk by its grid
3180                            // coordinates, so the linear slot is decoded back
3181                            // into them.
3182                            ChunkIndexKind::FixedArray
3183                            | ChunkIndexKind::Implicit
3184                            | ChunkIndexKind::SingleChunk
3185                            | ChunkIndexKind::BtreeV1 => {
3186                                let coords =
3187                                    writer.chunk_coords_from_slot(*index, chunk_idx as u64)?;
3188                                writer.write_chunk_at_coords(*index, &coords, data)?;
3189                            }
3190                            _ => writer.write_chunk_inner(*index, chunk_idx as u64, data)?,
3191                        }
3192                        Ok(())
3193                    }
3194                    _ => Err(Hdf5Error::InvalidState(
3195                        "file is no longer in write mode".into(),
3196                    )),
3197                }
3198            }
3199            DatasetInfo::Reader { .. } => {
3200                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
3201            }
3202        }
3203    }
3204
3205    /// Write an already-filtered (pre-compressed) chunk **verbatim**, recording
3206    /// the caller-supplied `filter_mask`. The bytes are stored as-is without
3207    /// running the dataset's filter pipeline — the HDF5 "direct chunk write"
3208    /// (`H5Dwrite_chunk`, formerly `H5DOwrite_chunk`) operation.
3209    ///
3210    /// `chunk_idx` is the linear chunk index (the frame number for streaming
3211    /// datasets), exactly as for [`write_chunk`](Self::write_chunk). `data` is
3212    /// the already-filtered bytes of one chunk — its length is the *stored*
3213    /// (compressed) size, not the uncompressed chunk size.
3214    ///
3215    /// `filter_mask` is a bitfield: bit *i* set means filter *i* of the
3216    /// dataset's pipeline was **not** applied to this chunk and must be skipped
3217    /// on read. Pass 0 when the full pipeline was already applied upstream (the
3218    /// common case: a codec plugin handed you compressed frames).
3219    ///
3220    /// The dataset must be chunked **and** filtered; an unfiltered chunk index
3221    /// has no slot to record a stored size or mask. A v2-B-tree-indexed dataset
3222    /// (two or more unlimited dimensions) has no fixed chunk grid to linearize
3223    /// against, so address its chunks with
3224    /// [`write_chunk_raw_at`](Self::write_chunk_raw_at) instead.
3225    ///
3226    /// # Reading back
3227    ///
3228    /// Both this crate's reader and libhdf5/h5py honor the per-chunk
3229    /// `filter_mask`: a chunk written with any mask round-trips correctly, with
3230    /// the reader skipping exactly the filters the mask marks as not applied.
3231    pub fn write_chunk_raw(&self, chunk_idx: usize, data: &[u8], filter_mask: u32) -> Result<()> {
3232        match &self.info {
3233            DatasetInfo::Writer {
3234                index, chunk_index, ..
3235            } => {
3236                let Some(kind) = *chunk_index else {
3237                    return Err(Hdf5Error::InvalidState(
3238                        "write_chunk_raw is only for chunked datasets".into(),
3239                    ));
3240                };
3241                if kind == ChunkIndexKind::BtreeV2 {
3242                    return Err(Hdf5Error::InvalidState(
3243                        "this dataset uses a v2 B-tree chunk index; use \
3244                         write_chunk_raw_at with the chunk's grid coordinates"
3245                            .into(),
3246                    ));
3247                }
3248                if kind == ChunkIndexKind::Implicit {
3249                    return Err(Hdf5Error::InvalidState(
3250                        "this dataset uses the implicit chunk index, which stores \
3251                         every chunk at its full unfiltered size and has nowhere to \
3252                         record a stored size or a filter mask"
3253                            .into(),
3254                    ));
3255                }
3256
3257                let inner = borrow_inner(&self.file_inner);
3258                match &*inner {
3259                    H5FileInner::Writer(writer) => {
3260                        // One op: the slot decode and the write see the same
3261                        // extents.
3262                        let cell = writer.ds(*index);
3263                        let _op = cell.op.lock();
3264                        if kind == ChunkIndexKind::FixedArray {
3265                            // Fixed-array dataset: decode the index-grid slot
3266                            // into row-major grid coordinates.
3267                            let coords = writer.chunk_coords_from_slot(*index, chunk_idx as u64)?;
3268                            writer.write_compressed_chunk_fixed_array_inner(
3269                                *index,
3270                                &coords,
3271                                data,
3272                                filter_mask,
3273                            )?;
3274                        } else if kind == ChunkIndexKind::BtreeV1 {
3275                            // Same for the classic index, whose key carries a
3276                            // stored size and a filter mask of its own.
3277                            let coords = writer.chunk_coords_from_slot(*index, chunk_idx as u64)?;
3278                            writer.write_compressed_chunk_btree_v1_inner(
3279                                *index,
3280                                &coords,
3281                                data,
3282                                filter_mask,
3283                            )?;
3284                        } else if kind == ChunkIndexKind::SingleChunk {
3285                            // Same again for the single-chunk index, whose
3286                            // layout message carries the stored size and mask
3287                            // inline.
3288                            let coords = writer.chunk_coords_from_slot(*index, chunk_idx as u64)?;
3289                            writer.write_compressed_chunk_single_chunk_inner(
3290                                *index,
3291                                &coords,
3292                                data,
3293                                filter_mask,
3294                            )?;
3295                        } else {
3296                            writer.write_compressed_chunk_inner(
3297                                *index,
3298                                chunk_idx as u64,
3299                                data,
3300                                filter_mask,
3301                            )?;
3302                        }
3303                        Ok(())
3304                    }
3305                    _ => Err(Hdf5Error::InvalidState(
3306                        "file is no longer in write mode".into(),
3307                    )),
3308                }
3309            }
3310            DatasetInfo::Reader { .. } => {
3311                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
3312            }
3313        }
3314    }
3315
3316    /// Write a single chunk to a v2-B-tree-indexed dataset, addressed by its
3317    /// chunk-grid coordinates (one per dimension).
3318    ///
3319    /// This is the entry point for datasets with two or more unlimited
3320    /// dimensions. The dataset's logical dimensions are extended to cover
3321    /// the written chunk. `data` is the raw bytes of one full chunk.
3322    ///
3323    /// ```no_run
3324    /// # use rust_hdf5::H5File;
3325    /// let file = H5File::create("bt2.h5").unwrap();
3326    /// let ds = file.new_dataset::<i32>()
3327    ///     .shape(&[0, 0])
3328    ///     .chunk(&[2, 2])
3329    ///     .max_shape(&[None, None])
3330    ///     .create("grid")
3331    ///     .unwrap();
3332    /// let chunk = [0i32, 1, 2, 3];
3333    /// let bytes: Vec<u8> = chunk.iter().flat_map(|v| v.to_le_bytes()).collect();
3334    /// ds.write_chunk_at(&[0, 0], &bytes).unwrap();
3335    /// ```
3336    pub fn write_chunk_at(&self, chunk_coords: &[usize], data: &[u8]) -> Result<()> {
3337        self.write_chunk_at_inner(chunk_coords, ChunkBytes::Unfiltered(data), "write_chunk_at")
3338    }
3339
3340    /// Write an already-filtered chunk **verbatim** to a chunked dataset,
3341    /// addressed by its chunk-grid coordinates.
3342    ///
3343    /// The coordinate-addressed twin of
3344    /// [`write_chunk_raw`](Self::write_chunk_raw), and the form a
3345    /// v2-B-tree-indexed dataset needs: with two or more unlimited dimensions
3346    /// there is no fixed chunk grid for a linear index to mean anything against.
3347    /// As with `write_chunk_at`, the dataset's logical dimensions are extended
3348    /// to cover the written chunk.
3349    ///
3350    /// `data` is the already-filtered bytes of one chunk — its length is the
3351    /// *stored* size — and `filter_mask` bit *i* set means filter *i* of the
3352    /// pipeline was **not** applied and must be skipped on read. Pass 0 when the
3353    /// full pipeline already ran upstream.
3354    ///
3355    /// The dataset must be chunked **and** filtered; an unfiltered chunk index
3356    /// has no slot to record a stored size or mask.
3357    pub fn write_chunk_raw_at(
3358        &self,
3359        chunk_coords: &[usize],
3360        data: &[u8],
3361        filter_mask: u32,
3362    ) -> Result<()> {
3363        self.write_chunk_at_inner(
3364            chunk_coords,
3365            ChunkBytes::Prefiltered { data, filter_mask },
3366            "write_chunk_raw_at",
3367        )
3368    }
3369
3370    /// The single owner of coordinate-addressed chunk writes: validates the
3371    /// coordinates, grows the dataspace to cover them, and routes the bytes to
3372    /// whichever chunk index the dataset uses. Whether the filter pipeline runs
3373    /// here or already ran upstream is carried by `bytes`, not by a second copy
3374    /// of this dispatch.
3375    fn write_chunk_at_inner(
3376        &self,
3377        chunk_coords: &[usize],
3378        bytes: ChunkBytes<'_>,
3379        what: &str,
3380    ) -> Result<()> {
3381        match &self.info {
3382            DatasetInfo::Writer {
3383                index, chunk_index, ..
3384            } => {
3385                let Some(kind) = *chunk_index else {
3386                    return Err(Hdf5Error::InvalidState(format!(
3387                        "{what} is only for chunked datasets"
3388                    )));
3389                };
3390                let coords: Vec<u64> = chunk_coords.iter().map(|&c| c as u64).collect();
3391                let inner = borrow_inner(&self.file_inner);
3392                let writer = match &*inner {
3393                    H5FileInner::Writer(w) => w,
3394                    _ => {
3395                        return Err(Hdf5Error::InvalidState(
3396                            "file is no longer in write mode".into(),
3397                        ))
3398                    }
3399                };
3400                // Whole-operation guard: the dims snapshot, the chunk write
3401                // and the extend below must not interleave with a concurrent
3402                // same-dataset operation.
3403                let cell = writer.ds(*index);
3404                let _op = cell.op.lock();
3405                let chunk_dims = writer
3406                    .dataset_chunk_dims(*index)
3407                    .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
3408                    .to_vec();
3409                let dims = writer.dataset_dims(*index).to_vec();
3410                if coords.len() != dims.len() {
3411                    return Err(Hdf5Error::InvalidState(format!(
3412                        "chunk_coords has {} entries but the dataset has {} dimensions",
3413                        coords.len(),
3414                        dims.len()
3415                    )));
3416                }
3417                if chunk_dims.len() != dims.len() {
3418                    return Err(Hdf5Error::InvalidState(format!(
3419                        "dataset chunk shape has {} dimensions but the dataspace has {}",
3420                        chunk_dims.len(),
3421                        dims.len()
3422                    )));
3423                }
3424
3425                if kind == ChunkIndexKind::FixedArray {
3426                    // Fixed-array (fixed-shape) dataset: no dimension growth.
3427                    match bytes {
3428                        ChunkBytes::Unfiltered(data) => {
3429                            writer.write_chunk_fixed_array_inner(*index, &coords, data)?
3430                        }
3431                        ChunkBytes::Prefiltered { data, filter_mask } => writer
3432                            .write_compressed_chunk_fixed_array_inner(
3433                                *index,
3434                                &coords,
3435                                data,
3436                                filter_mask,
3437                            )?,
3438                    }
3439                    return Ok(());
3440                }
3441
3442                if kind == ChunkIndexKind::Implicit {
3443                    // Implicit index: fixed shape, so no dimension growth
3444                    // either, and no slot to record a stored size in.
3445                    match bytes {
3446                        ChunkBytes::Unfiltered(data) => {
3447                            writer.write_chunk_implicit_inner(*index, &coords, data)?
3448                        }
3449                        ChunkBytes::Prefiltered { .. } => {
3450                            return Err(Hdf5Error::InvalidState(
3451                                "this dataset uses the implicit chunk index, which stores \
3452                                 every chunk at its full unfiltered size and has nowhere \
3453                                 to record a stored size or a filter mask"
3454                                    .into(),
3455                            ))
3456                        }
3457                    }
3458                    return Ok(());
3459                }
3460
3461                if kind == ChunkIndexKind::SingleChunk {
3462                    // Single-chunk index: fixed shape covered by exactly one
3463                    // chunk, so no dimension growth either.
3464                    match bytes {
3465                        ChunkBytes::Unfiltered(data) => {
3466                            writer.write_chunk_single_chunk_inner(*index, &coords, data)?
3467                        }
3468                        ChunkBytes::Prefiltered { data, filter_mask } => writer
3469                            .write_compressed_chunk_single_chunk_inner(
3470                                *index,
3471                                &coords,
3472                                data,
3473                                filter_mask,
3474                            )?,
3475                    }
3476                    return Ok(());
3477                }
3478
3479                // The remaining indexes (v2 B-tree, v1 B-tree, extensible
3480                // array) can all grow: validate the coordinates and compute
3481                // the grown dimensions up-front, before any chunk is
3482                // written, so an overflowing coordinate cannot leave an
3483                // orphaned chunk in the file.
3484                //
3485                // The last chunk of a dimension usually hangs past the extent
3486                // — a length of 10 in chunks of 4 ends at 12 — so the growth
3487                // is capped at the declared maximum, which is what the chunk
3488                // still covers. Without the cap a legal edge chunk would be
3489                // written and then rejected by the extend below.
3490                let max_dims = writer.dataset_max_dims(*index);
3491                let mut new_dims = dims.clone();
3492                for d in 0..dims.len() {
3493                    let needed = coords[d]
3494                        .checked_add(1)
3495                        .and_then(|c| c.checked_mul(chunk_dims[d]))
3496                        .ok_or_else(|| {
3497                            Hdf5Error::InvalidState(format!(
3498                                "chunk coordinate {} in dimension {} is too large",
3499                                coords[d], d
3500                            ))
3501                        })?;
3502                    let needed = needed.min(max_dims[d]);
3503                    if needed > new_dims[d] {
3504                        new_dims[d] = needed;
3505                    }
3506                }
3507
3508                if kind == ChunkIndexKind::BtreeV2 {
3509                    match bytes {
3510                        ChunkBytes::Unfiltered(data) => {
3511                            writer.write_chunk_btree_v2_inner(*index, &coords, data)?
3512                        }
3513                        ChunkBytes::Prefiltered { data, filter_mask } => writer
3514                            .write_compressed_chunk_btree_v2_inner(
3515                                *index,
3516                                &coords,
3517                                data,
3518                                filter_mask,
3519                            )?,
3520                    }
3521                } else if kind == ChunkIndexKind::BtreeV1 {
3522                    // The classic index takes any shape, fixed or unlimited,
3523                    // so it grows the dataspace with the chunk the way the v2
3524                    // B-tree does — bounded below by the maximum extent.
3525                    match bytes {
3526                        ChunkBytes::Unfiltered(data) => {
3527                            writer.write_chunk_btree_v1_inner(*index, &coords, data)?
3528                        }
3529                        ChunkBytes::Prefiltered { data, filter_mask } => writer
3530                            .write_compressed_chunk_btree_v1_inner(
3531                                *index,
3532                                &coords,
3533                                data,
3534                                filter_mask,
3535                            )?,
3536                    }
3537                } else {
3538                    // Extensible array: the chunk's index-grid slot (row-major
3539                    // against the maximum extent).
3540                    let linear = writer.chunk_slot(*index, &coords)?;
3541                    match bytes {
3542                        ChunkBytes::Unfiltered(data) => {
3543                            writer.write_chunk_inner(*index, linear, data)?
3544                        }
3545                        ChunkBytes::Prefiltered { data, filter_mask } => writer
3546                            .write_compressed_chunk_inner(*index, linear, data, filter_mask)?,
3547                    }
3548                }
3549
3550                if new_dims != dims {
3551                    writer.extend_dataset_inner(*index, &new_dims)?;
3552                }
3553                Ok(())
3554            }
3555            DatasetInfo::Reader { .. } => {
3556                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
3557            }
3558        }
3559    }
3560
3561    /// Write multiple chunks in a batch, optionally compressing in parallel.
3562    ///
3563    /// `chunks` is a slice of `(chunk_index, raw_data)` pairs. When a filter
3564    /// pipeline is configured and the `parallel` feature is enabled, all
3565    /// chunks are compressed concurrently via rayon.
3566    pub fn write_chunks_batch(&self, chunks: &[(usize, &[u8])]) -> Result<()> {
3567        match &self.info {
3568            DatasetInfo::Writer {
3569                index, chunk_index, ..
3570            } => {
3571                if chunk_index.is_none() {
3572                    return Err(Hdf5Error::InvalidState(
3573                        "write_chunks_batch is only for chunked datasets".into(),
3574                    ));
3575                }
3576                let pairs: Vec<(u64, &[u8])> = chunks
3577                    .iter()
3578                    .map(|(idx, data)| (*idx as u64, *data))
3579                    .collect();
3580                let inner = borrow_inner(&self.file_inner);
3581                match &*inner {
3582                    H5FileInner::Writer(writer) => {
3583                        writer.write_chunks_batch(*index, &pairs)?;
3584                        Ok(())
3585                    }
3586                    _ => Err(Hdf5Error::InvalidState(
3587                        "file is no longer in write mode".into(),
3588                    )),
3589                }
3590            }
3591            DatasetInfo::Reader { .. } => {
3592                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
3593            }
3594        }
3595    }
3596
3597    /// Append data along the first dimension of a chunked dataset.
3598    ///
3599    /// `data` must contain a whole number of "frames" — slices along
3600    /// dimension 0. For example, if the dataset has shape `[N, H, W]`
3601    /// and `chunk_dims = [1, H, W]`, then `data.len()` must be a
3602    /// multiple of `H * W`.
3603    ///
3604    /// This method writes the necessary chunks and extends the dataset
3605    /// shape automatically.
3606    ///
3607    /// ```no_run
3608    /// # use rust_hdf5::H5File;
3609    /// let file = H5File::create("append.h5").unwrap();
3610    /// let ds = file.new_dataset::<f64>()
3611    ///     .shape(&[0, 3])
3612    ///     .chunk(&[1, 3])
3613    ///     .max_shape(&[None, Some(3)])
3614    ///     .create("data")
3615    ///     .unwrap();
3616    /// ds.append(&[1.0, 2.0, 3.0]).unwrap();       // shape becomes [1, 3]
3617    /// ds.append(&[4.0, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap(); // shape becomes [3, 3]
3618    /// ```
3619    pub fn append<T: H5Type>(&self, data: &[T]) -> Result<()> {
3620        match &self.info {
3621            DatasetInfo::Writer {
3622                index,
3623                element_size,
3624                chunk_index,
3625                ..
3626            } => {
3627                if chunk_index.is_none() {
3628                    return Err(Hdf5Error::InvalidState(
3629                        "append is only for chunked datasets".into(),
3630                    ));
3631                }
3632                if T::element_size() != *element_size {
3633                    return Err(Hdf5Error::TypeMismatch(format!(
3634                        "append type has element size {} but dataset expects {}",
3635                        T::element_size(),
3636                        element_size,
3637                    )));
3638                }
3639
3640                let ds_index = *index;
3641                let es = *element_size;
3642
3643                let inner = borrow_inner(&self.file_inner);
3644                let writer = match &*inner {
3645                    H5FileInner::Writer(w) => w,
3646                    _ => {
3647                        return Err(Hdf5Error::InvalidState(
3648                            "file is no longer in write mode".into(),
3649                        ))
3650                    }
3651                };
3652
3653                // Whole-operation guard: the buffer take, the frame writes,
3654                // the re-buffer and the extend below are separate slot
3655                // acquisitions that a concurrent same-dataset append must not
3656                // interleave with.
3657                let cell = writer.ds(ds_index);
3658                let _op = cell.op.lock();
3659
3660                let chunk_dims = writer
3661                    .dataset_chunk_dims(ds_index)
3662                    .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
3663                    .to_vec();
3664                let dims = writer.dataset_dims(ds_index).to_vec();
3665
3666                // Frame size = product of dims[1..]
3667                let frame_elems: usize = if dims.len() > 1 {
3668                    dims[1..].iter().map(|&d| d as usize).product()
3669                } else {
3670                    1
3671                };
3672
3673                if frame_elems == 0 {
3674                    return Err(Hdf5Error::InvalidState(
3675                        "cannot append to dataset with zero-size trailing dimensions".into(),
3676                    ));
3677                }
3678
3679                if !data.len().is_multiple_of(frame_elems) {
3680                    return Err(Hdf5Error::InvalidState(format!(
3681                        "data length {} is not a multiple of frame size {}",
3682                        data.len(),
3683                        frame_elems,
3684                    )));
3685                }
3686
3687                let n_new_frames = data.len() / frame_elems;
3688                let current_dim0 = dims[0] as usize;
3689
3690                // Chunk size along first dimension
3691                let chunk_dim0 = chunk_dims[0] as usize;
3692                let frame_bytes = frame_elems * es;
3693
3694                let host = unsafe {
3695                    std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * es)
3696                };
3697                let datatype = writer.dataset_datatype(ds_index);
3698                let raw = to_stored_byte_order(host, &datatype, es)?;
3699
3700                // Merge the buffer with the new frames when it is the
3701                // dataset's tail; a buffer left mid-extent (the extent moved
3702                // past it) keeps its recorded place — flush it and start
3703                // fresh at the current end.
3704                let taken = { writer.ds(ds_index).lock().append.take() };
3705                let (base_dim0, buffered_frames, mut combined) = match taken {
3706                    Some(b) if b.base + b.frames == current_dim0 as u64 => {
3707                        (b.base as usize, b.frames as usize, b.bytes)
3708                    }
3709                    Some(b) => {
3710                        writer.write_append_frames(ds_index, b.base, b.frames, &b.bytes)?;
3711                        (current_dim0, 0, Vec::new())
3712                    }
3713                    None => (current_dim0, 0, Vec::new()),
3714                };
3715                combined.extend_from_slice(&raw);
3716
3717                let total_frames = buffered_frames + n_new_frames;
3718
3719                // Rows up to the last chunk boundary are written now; the
3720                // tail that does not complete a chunk goes back in the
3721                // buffer for the next append (or the flush at close). The
3722                // boundary can precede `base_dim0` — a reopened file's
3723                // flushed partial chunk leaves the base mid-chunk — in
3724                // which case everything is tail.
3725                let last_boundary = ((base_dim0 + total_frames) / chunk_dim0) * chunk_dim0;
3726                let write_frames = last_boundary.saturating_sub(base_dim0);
3727                let tail_frames = total_frames - write_frames;
3728                if write_frames > 0 {
3729                    writer.write_append_frames(
3730                        ds_index,
3731                        base_dim0 as u64,
3732                        write_frames as u64,
3733                        &combined[..write_frames * frame_bytes],
3734                    )?;
3735                }
3736                if tail_frames > 0 {
3737                    let ds = writer.ds(ds_index);
3738                    let mut m = ds.lock();
3739                    m.append = Some(crate::io::writer::AppendBuffer {
3740                        base: (base_dim0 + write_frames) as u64,
3741                        frames: tail_frames as u64,
3742                        bytes: combined[write_frames * frame_bytes..].to_vec(),
3743                    });
3744                }
3745
3746                // Extend dims to include all frames (buffered + new)
3747                let logical_dim0 = base_dim0 + total_frames;
3748                let mut new_dims: Vec<u64> = dims;
3749                new_dims[0] = logical_dim0 as u64;
3750                writer.extend_dataset_inner(ds_index, &new_dims)?;
3751
3752                Ok(())
3753            }
3754            DatasetInfo::Reader { .. } => {
3755                Err(Hdf5Error::InvalidState("cannot append in read mode".into()))
3756            }
3757        }
3758    }
3759
3760    /// Extend the dimensions of a chunked dataset.
3761    pub fn extend(&self, new_dims: &[usize]) -> Result<()> {
3762        match &self.info {
3763            DatasetInfo::Writer {
3764                index, chunk_index, ..
3765            } => {
3766                if chunk_index.is_none() {
3767                    return Err(Hdf5Error::InvalidState(
3768                        "extend is only for chunked datasets".into(),
3769                    ));
3770                }
3771
3772                let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
3773                let inner = borrow_inner(&self.file_inner);
3774                match &*inner {
3775                    H5FileInner::Writer(writer) => {
3776                        writer.extend_dataset(*index, &dims_u64)?;
3777                        Ok(())
3778                    }
3779                    _ => Err(Hdf5Error::InvalidState(
3780                        "file is no longer in write mode".into(),
3781                    )),
3782                }
3783            }
3784            DatasetInfo::Reader { .. } => {
3785                Err(Hdf5Error::InvalidState("cannot extend in read mode".into()))
3786            }
3787        }
3788    }
3789
3790    /// Set the logical extent of a chunked dataset, growing **or
3791    /// shrinking** any dimension.
3792    ///
3793    /// Unlike [`extend`](Self::extend), which only grows, this can reduce a
3794    /// dimension — for example to correct an over-extended frame count
3795    /// after writing a partial multi-frame chunk. Shrinking prunes the
3796    /// stored chunks the way libhdf5's `H5Dset_extent` does: a chunk
3797    /// entirely beyond the new extent is removed from the chunk index and
3798    /// its storage freed for reuse, and a chunk the new extent cuts
3799    /// through has its out-of-extent region overwritten with the fill
3800    /// value — so growing the extent back exposes fill values, not the
3801    /// old data. The new extent must not exceed the dataset's maximum
3802    /// dimensions.
3803    pub fn set_extent(&self, new_dims: &[usize]) -> Result<()> {
3804        match &self.info {
3805            DatasetInfo::Writer { index, .. } => {
3806                let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
3807                let inner = borrow_inner(&self.file_inner);
3808                match &*inner {
3809                    H5FileInner::Writer(writer) => {
3810                        writer.set_dataset_extent(*index, &dims_u64)?;
3811                        Ok(())
3812                    }
3813                    _ => Err(Hdf5Error::InvalidState(
3814                        "file is no longer in write mode".into(),
3815                    )),
3816                }
3817            }
3818            DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
3819                "cannot set extent in read mode".into(),
3820            )),
3821        }
3822    }
3823
3824    /// Flush a chunked dataset's index structures to disk.
3825    pub fn flush(&self) -> Result<()> {
3826        match &self.info {
3827            DatasetInfo::Writer { index, .. } => {
3828                let inner = borrow_inner(&self.file_inner);
3829                match &*inner {
3830                    H5FileInner::Writer(writer) => {
3831                        writer.flush_dataset(*index)?;
3832                        Ok(())
3833                    }
3834                    _ => Ok(()),
3835                }
3836            }
3837            DatasetInfo::Reader { .. } => Ok(()),
3838        }
3839    }
3840
3841    /// Read a slice (hyperslab) of the dataset as a typed vector.
3842    ///
3843    /// `starts` and `counts` define the N-dimensional selection:
3844    /// `starts[d]` = first index along dim d, `counts[d]` = how many elements.
3845    pub fn read_slice<T: H5Type>(&self, starts: &[usize], counts: &[usize]) -> Result<Vec<T>> {
3846        match &self.info {
3847            DatasetInfo::Reader {
3848                name,
3849                shape,
3850                element_size,
3851            } => {
3852                if T::element_size() != *element_size {
3853                    return Err(Hdf5Error::TypeMismatch(format!(
3854                        "read type has element size {} but dataset has element size {}",
3855                        T::element_size(),
3856                        element_size,
3857                    )));
3858                }
3859                let datatype = self.datatype()?;
3860                let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
3861                let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
3862
3863                // Bounds before sizing: the destination is allocated here,
3864                // ahead of the reader's own check, so a selection the extent
3865                // does not admit must be refused before its size is computed.
3866                let dims: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
3867                check_hyperslab(&dims, &starts_u64, &counts_u64)?;
3868                let count = element_count(&counts_u64)?;
3869                let mut inner = borrow_inner_mut(&self.file_inner);
3870                let H5FileInner::Reader(reader) = &mut *inner else {
3871                    return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
3872                };
3873                // The selection lands in the vector this returns, so its bytes
3874                // are touched once instead of being read into a byte buffer and
3875                // copied into a second one of the same size.
3876                read_image_into_new(count, |image| {
3877                    reader.read_slice_into_dst(
3878                        name,
3879                        &starts_u64,
3880                        &counts_u64,
3881                        image,
3882                        ReadDst::Fresh,
3883                    )?;
3884                    to_host_byte_order(image, &datatype, T::element_size())
3885                })
3886            }
3887            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
3888                "cannot read_slice from a dataset in write mode".into(),
3889            )),
3890        }
3891    }
3892
3893    /// Read a strided hyperslab as a typed vector — h5py's stepped slicing
3894    /// (`ds[a:b:s]`) or the general `start`/`stride`/`count`/`block` form of
3895    /// `H5Sselect_hyperslab`.
3896    ///
3897    /// One entry per dimension: `start[d]` is the first index, `stride[d]`
3898    /// the spacing between selected blocks (all-`1` is the same selection
3899    /// [`read_slice`](Self::read_slice) reads), `count[d]` how many blocks,
3900    /// and `block[d]` how many contiguous elements each block covers. The
3901    /// returned vector is row-major over `count[d] * block[d]` per
3902    /// dimension — exactly the shape h5py's stepped slicing produces.
3903    ///
3904    /// ```no_run
3905    /// # use rust_hdf5::H5File;
3906    /// let file = H5File::open("data.h5").unwrap();
3907    /// let ds = file.dataset("series").unwrap(); // shape [100]
3908    /// // Python: ds[0:100:2] — every other element.
3909    /// let evens: Vec<f64> = ds.read_hyperslab(&[0], &[2], &[50], &[1]).unwrap();
3910    /// ```
3911    pub fn read_hyperslab<T: H5Type>(
3912        &self,
3913        start: &[usize],
3914        stride: &[usize],
3915        count: &[usize],
3916        block: &[usize],
3917    ) -> Result<Vec<T>> {
3918        match &self.info {
3919            DatasetInfo::Reader {
3920                name, element_size, ..
3921            } => {
3922                if T::element_size() != *element_size {
3923                    return Err(Hdf5Error::TypeMismatch(format!(
3924                        "read type has element size {} but dataset has element size {}",
3925                        T::element_size(),
3926                        element_size,
3927                    )));
3928                }
3929                let datatype = self.datatype()?;
3930                let start_u64: Vec<u64> = start.iter().map(|&s| s as u64).collect();
3931                let stride_u64: Vec<u64> = stride.iter().map(|&s| s as u64).collect();
3932                let count_u64: Vec<u64> = count.iter().map(|&c| c as u64).collect();
3933                let block_u64: Vec<u64> = block.iter().map(|&b| b as u64).collect();
3934
3935                let selected: Vec<u64> = count_u64
3936                    .iter()
3937                    .zip(&block_u64)
3938                    .map(|(&c, &b)| c.saturating_mul(b))
3939                    .collect();
3940                let n = element_count(&selected)?;
3941                let mut inner = borrow_inner_mut(&self.file_inner);
3942                let H5FileInner::Reader(reader) = &mut *inner else {
3943                    return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
3944                };
3945                read_image_into_new(n, |image| {
3946                    reader.read_hyperslab_into(
3947                        name,
3948                        &start_u64,
3949                        &stride_u64,
3950                        &count_u64,
3951                        &block_u64,
3952                        image,
3953                    )?;
3954                    to_host_byte_order(image, &datatype, T::element_size())
3955                })
3956            }
3957            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
3958                "cannot read_hyperslab from a dataset in write mode".into(),
3959            )),
3960        }
3961    }
3962
3963    /// Read a list of coordinates in one call, as a typed vector — h5py
3964    /// fancy indexing with a coordinate list.
3965    ///
3966    /// `points[i]` is a coordinate with one entry per dimension. The
3967    /// returned vector holds one element per point, in the same order as
3968    /// `points`, regardless of the dataset's rank.
3969    ///
3970    /// ```no_run
3971    /// # use rust_hdf5::H5File;
3972    /// let file = H5File::open("data.h5").unwrap();
3973    /// let ds = file.dataset("grid").unwrap(); // shape [10, 10]
3974    /// // Python: ds[np.array([[0, 0], [3, 4], [9, 9]])]
3975    /// let picked: Vec<f64> = ds.read_points(&[vec![0, 0], vec![3, 4], vec![9, 9]]).unwrap();
3976    /// ```
3977    pub fn read_points<T: H5Type>(&self, points: &[Vec<usize>]) -> Result<Vec<T>> {
3978        match &self.info {
3979            DatasetInfo::Reader {
3980                name, element_size, ..
3981            } => {
3982                if T::element_size() != *element_size {
3983                    return Err(Hdf5Error::TypeMismatch(format!(
3984                        "read type has element size {} but dataset has element size {}",
3985                        T::element_size(),
3986                        element_size,
3987                    )));
3988                }
3989                let datatype = self.datatype()?;
3990                let points_u64: Vec<Vec<u64>> = points
3991                    .iter()
3992                    .map(|p| p.iter().map(|&c| c as u64).collect())
3993                    .collect();
3994
3995                let mut inner = borrow_inner_mut(&self.file_inner);
3996                let H5FileInner::Reader(reader) = &mut *inner else {
3997                    return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
3998                };
3999                read_image_into_new(points_u64.len(), |image| {
4000                    reader.read_points_into(name, &points_u64, image, ReadDst::Fresh)?;
4001                    to_host_byte_order(image, &datatype, T::element_size())
4002                })
4003            }
4004            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4005                "cannot read_points from a dataset in write mode".into(),
4006            )),
4007        }
4008    }
4009
4010    /// Read one chunk's raw (still-filtered) bytes and its filter mask,
4011    /// addressed by chunk-grid coordinates — the read half of
4012    /// [`write_chunk_raw_at`](Self::write_chunk_raw_at) and the HDF5 "direct
4013    /// chunk read" (`H5Dread_chunk`, formerly `H5DOread_chunk`; h5py's
4014    /// `Dataset.id.read_direct_chunk`).
4015    ///
4016    /// The bytes are exactly what is stored on disk: filtered/compressed if
4017    /// the dataset has a filter pipeline, with no decompression applied. The
4018    /// returned `u32` is the chunk's filter mask: bit *i* set means filter
4019    /// *i* of the pipeline was **not** applied to this particular chunk and
4020    /// must be skipped when reversing it.
4021    ///
4022    /// `Err` if the dataset is not chunked, `chunk_coords` has the wrong
4023    /// rank, or the chunk at those coordinates has never been written.
4024    ///
4025    /// ```no_run
4026    /// # use rust_hdf5::H5File;
4027    /// let file = H5File::open("data.h5").unwrap();
4028    /// let ds = file.dataset("frames").unwrap();
4029    /// let (raw, filter_mask) = ds.read_chunk_raw_at(&[0, 0]).unwrap();
4030    /// ```
4031    pub fn read_chunk_raw_at(&self, chunk_coords: &[usize]) -> Result<(Vec<u8>, u32)> {
4032        match &self.info {
4033            DatasetInfo::Reader { name, .. } => {
4034                let coords_u64: Vec<u64> = chunk_coords.iter().map(|&c| c as u64).collect();
4035                let mut inner = borrow_inner_mut(&self.file_inner);
4036                match &mut *inner {
4037                    H5FileInner::Reader(reader) => Ok(reader.read_chunk_raw_at(name, &coords_u64)?),
4038                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
4039                }
4040            }
4041            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4042                "cannot read_chunk_raw_at from a dataset in write mode".into(),
4043            )),
4044        }
4045    }
4046
4047    /// Write a typed slice to a sub-region of the dataset.
4048    ///
4049    /// `starts` and `counts` define the N-dimensional selection, which must lie
4050    /// inside the dataset's current extent.
4051    ///
4052    /// Works for both contiguous and chunked datasets. For a chunked dataset
4053    /// only the chunks the selection touches are rewritten — a partially
4054    /// covered chunk is read back, patched, and written again, so updating one
4055    /// row of an appendable dataset costs the chunks that row crosses rather
4056    /// than the whole dataset. Elements of a touched chunk that the selection
4057    /// does not cover keep their stored value, or the dataset's fill value if
4058    /// the chunk did not exist yet.
4059    pub fn write_slice<T: H5Type>(
4060        &self,
4061        starts: &[usize],
4062        counts: &[usize],
4063        data: &[T],
4064    ) -> Result<()> {
4065        match &self.info {
4066            DatasetInfo::Writer {
4067                index,
4068                element_size,
4069                ..
4070            } => {
4071                if T::element_size() != *element_size {
4072                    return Err(Hdf5Error::TypeMismatch(format!(
4073                        "write type has element size {} but dataset expects {}",
4074                        T::element_size(),
4075                        element_size,
4076                    )));
4077                }
4078
4079                let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
4080                let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
4081
4082                let inner = borrow_inner(&self.file_inner);
4083                let H5FileInner::Writer(writer) = &*inner else {
4084                    return Err(Hdf5Error::InvalidState(
4085                        "file is no longer in write mode".into(),
4086                    ));
4087                };
4088
4089                // Bounds before sizing, against the extent the writer holds
4090                // now (an extend since this handle was taken counts): a
4091                // selection the extent does not admit is refused for that
4092                // reason, not for the size it would have had.
4093                check_hyperslab(&writer.dataset_dims(*index), &starts_u64, &counts_u64)?;
4094                let expected = element_count(&counts_u64)?;
4095                if data.len() != expected {
4096                    return Err(Hdf5Error::InvalidState(format!(
4097                        "data length {} does not match slice size {}",
4098                        data.len(),
4099                        expected,
4100                    )));
4101                }
4102
4103                let byte_len = data.len() * T::element_size();
4104                let host =
4105                    unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
4106
4107                let datatype = writer.dataset_datatype(*index);
4108                let stored = to_stored_byte_order(host, &datatype, T::element_size())?;
4109                writer.write_slice(*index, &starts_u64, &counts_u64, &stored)?;
4110                Ok(())
4111            }
4112            DatasetInfo::Reader { .. } => {
4113                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
4114            }
4115        }
4116    }
4117
4118    /// Replace elements `start .. start + strings.len()` of a 1-D
4119    /// variable-length string dataset.
4120    ///
4121    /// The extent and every element outside the range are left alone, and the
4122    /// cost is the new strings plus the chunks holding their references — not
4123    /// the column. The dataset's character set is enforced: a non-ASCII
4124    /// replacement in a dataset that declares ASCII is rejected rather than
4125    /// stored under a datatype that misdescribes it.
4126    ///
4127    /// The global heap objects the replaced references pointed at are freed —
4128    /// the same reclaim libhdf5 performs on an overwrite — so updating one
4129    /// element repeatedly reuses space rather than growing the file. A
4130    /// collection emptied by the update returns its block to the allocator.
4131    /// Under SWMR nothing is freed, because a reader may still be following
4132    /// those references.
4133    ///
4134    /// ```no_run
4135    /// # use rust_hdf5::H5File;
4136    /// let file = H5File::open_rw("meta.h5").unwrap();
4137    /// let ds = file.dataset_writer("notes").unwrap();
4138    /// ds.write_vlen_strings_slice(42, &["replacement"]).unwrap();
4139    /// file.close().unwrap();
4140    /// ```
4141    pub fn write_vlen_strings_slice(&self, start: usize, strings: &[&str]) -> Result<()> {
4142        match &self.info {
4143            DatasetInfo::Writer { index, .. } => {
4144                let inner = borrow_inner(&self.file_inner);
4145                match &*inner {
4146                    H5FileInner::Writer(writer) => {
4147                        writer.write_vlen_strings_slice(*index, start as u64, strings)?;
4148                        Ok(())
4149                    }
4150                    _ => Err(Hdf5Error::InvalidState(
4151                        "file is no longer in write mode".into(),
4152                    )),
4153                }
4154            }
4155            DatasetInfo::Reader { .. } => {
4156                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
4157            }
4158        }
4159    }
4160
4161    /// Read variable-length strings from a dataset.
4162    ///
4163    /// This handles h5py-style vlen string datasets that store strings
4164    /// as global heap references. Returns one String per element.
4165    pub fn read_vlen_strings(&self) -> Result<Vec<String>> {
4166        match &self.info {
4167            DatasetInfo::Reader { name, .. } => {
4168                let mut inner = borrow_inner_mut(&self.file_inner);
4169                match &mut *inner {
4170                    H5FileInner::Reader(reader) => Ok(reader.read_vlen_strings(name)?),
4171                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
4172                }
4173            }
4174            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4175                "cannot read vlen strings from a dataset in write mode".into(),
4176            )),
4177        }
4178    }
4179
4180    /// Read variable-length byte arrays from a dataset.
4181    ///
4182    /// This handles vlen byte-array datasets (a vlen sequence of `u8`, e.g.
4183    /// those written by [`write_vlen_bytes`](crate::H5File::write_vlen_bytes))
4184    /// that store each element as a global heap reference. Returns one
4185    /// `Vec<u8>` per element.
4186    pub fn read_vlen_bytes(&self) -> Result<Vec<Vec<u8>>> {
4187        match &self.info {
4188            DatasetInfo::Reader { name, .. } => {
4189                let mut inner = borrow_inner_mut(&self.file_inner);
4190                match &mut *inner {
4191                    H5FileInner::Reader(reader) => Ok(reader.read_vlen_bytes(name)?),
4192                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
4193                }
4194            }
4195            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4196                "cannot read vlen bytes from a dataset in write mode".into(),
4197            )),
4198        }
4199    }
4200
4201    /// Write object references naming `paths` into elements `0..paths.len()`
4202    /// — h5py's `refs[i] = f['/target'].ref`.
4203    ///
4204    /// The dataset must have been created with
4205    /// [`object_references`](DatasetBuilder::object_references). A path names
4206    /// a dataset or a group (`/` is the root group) and must already exist;
4207    /// what reaches the file is the target's object header address, which is
4208    /// assigned when the file is finalized. Elements left unwritten read back
4209    /// as null references.
4210    pub fn write_object_references(&self, paths: &[&str]) -> Result<()> {
4211        match &self.info {
4212            DatasetInfo::Writer { index, .. } => {
4213                let inner = borrow_inner(&self.file_inner);
4214                match &*inner {
4215                    H5FileInner::Writer(writer) => {
4216                        writer.write_object_references(*index, 0, paths)?;
4217                        Ok(())
4218                    }
4219                    _ => Err(Hdf5Error::InvalidState(
4220                        "file is no longer in write mode".into(),
4221                    )),
4222                }
4223            }
4224            DatasetInfo::Reader { .. } => {
4225                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
4226            }
4227        }
4228    }
4229
4230    /// Mark this dataset as a dimension scale — `H5DSset_scale`, h5py's
4231    /// `ds.make_scale(name)`.
4232    ///
4233    /// Writes the `CLASS` attribute as the fixed-length null-terminated
4234    /// string `DIMENSION_SCALE` (16 bytes, the width `H5DSis_scale` checks
4235    /// for) and, when `name` is given, `NAME` the same way. A dataset that
4236    /// has scales attached to it cannot become one. Write mode only.
4237    ///
4238    /// ```no_run
4239    /// # use rust_hdf5::H5File;
4240    /// let file = H5File::create("scales.h5").unwrap();
4241    /// let x = file.new_dataset::<f64>().shape([4]).create("x").unwrap();
4242    /// x.write_raw(&[0.0, 0.5, 1.0, 1.5]).unwrap();
4243    /// x.set_scale(Some("x")).unwrap();
4244    /// ```
4245    pub fn set_scale(&self, name: Option<&str>) -> Result<()> {
4246        let index = self.writer_index("set_scale")?;
4247        let inner = borrow_inner(&self.file_inner);
4248        match &*inner {
4249            H5FileInner::Writer(writer) => Ok(writer.set_dimension_scale(index, name)?),
4250            _ => Err(Hdf5Error::InvalidState(
4251                "file is no longer in write mode".into(),
4252            )),
4253        }
4254    }
4255
4256    /// Attach `scale` as a dimension scale of this dataset's `axis` —
4257    /// `H5DSattach_scale`, h5py's `ds.dims[axis].attach_scale(scale)`.
4258    ///
4259    /// Records the attachment in this dataset's `DIMENSION_LIST` and the
4260    /// scale's `REFERENCE_LIST`, and marks `scale` as a dimension scale if it
4261    /// is not one yet. An axis may carry several scales: each attach appends.
4262    /// Attaching a scale already on that axis changes nothing. Both datasets
4263    /// must belong to this file, in write mode; a scale cannot have scales of
4264    /// its own, a dataset that is a scale cannot have scales attached, and
4265    /// `axis` must be below the rank (a scalar dataset counts as rank 1).
4266    ///
4267    /// ```no_run
4268    /// # use rust_hdf5::H5File;
4269    /// let file = H5File::create("scales.h5").unwrap();
4270    /// let data = file.new_dataset::<i32>().shape([2, 3]).create("data").unwrap();
4271    /// let x = file.new_dataset::<f64>().shape([3]).create("x").unwrap();
4272    /// x.set_scale(Some("x")).unwrap();
4273    /// data.attach_scale(1, &x).unwrap();
4274    /// ```
4275    pub fn attach_scale(&self, axis: usize, scale: &H5Dataset) -> Result<()> {
4276        let did = self.writer_index("attach_scale")?;
4277        let dsid = scale.writer_index("attach_scale")?;
4278        if !same_inner(&self.file_inner, &scale.file_inner) {
4279            return Err(Hdf5Error::InvalidState(
4280                "the dimension scale belongs to another file".into(),
4281            ));
4282        }
4283        let inner = borrow_inner(&self.file_inner);
4284        match &*inner {
4285            H5FileInner::Writer(writer) => Ok(writer.attach_dimension_scale(did, dsid, axis)?),
4286            _ => Err(Hdf5Error::InvalidState(
4287                "file is no longer in write mode".into(),
4288            )),
4289        }
4290    }
4291
4292    /// This dataset's index in the writer's registry, or the error a
4293    /// write-only operation `what` reports on a read-mode handle.
4294    fn writer_index(&self, what: &str) -> Result<usize> {
4295        match &self.info {
4296            DatasetInfo::Writer { index, .. } => Ok(*index),
4297            DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(format!(
4298                "{what} is only available in write mode"
4299            ))),
4300        }
4301    }
4302
4303    /// Write region references over `targets` into elements
4304    /// `0..targets.len()` — h5py's `refs[i] = f['/target'].regionref[0:3]`.
4305    ///
4306    /// The dataset must have been created with
4307    /// [`region_references`](DatasetBuilder::region_references). Each target is
4308    /// the path of an existing *dataset* and a [`Selection`] over it, which
4309    /// must fit that dataset's extent — the rule `H5Rcreate` applies. What
4310    /// reaches the file is a global-heap object holding the target's object
4311    /// header address (assigned when the file is finalized) and the serialized
4312    /// selection. Elements left unwritten read back as null references.
4313    ///
4314    /// ```no_run
4315    /// # use rust_hdf5::{H5File, PointSelection, Selection};
4316    /// let file = H5File::create("regions.h5").unwrap();
4317    /// file.new_dataset::<i32>().shape([4, 6]).create("m").unwrap();
4318    /// let refs = file.new_dataset::<u64>()
4319    ///     .region_references()
4320    ///     .shape([1])
4321    ///     .create("refs")
4322    ///     .unwrap();
4323    /// let points = Selection::Points(PointSelection {
4324    ///     rank: 2,
4325    ///     points: vec![vec![0, 1], vec![3, 5]],
4326    /// });
4327    /// refs.write_region_references(&[("/m", points)]).unwrap();
4328    /// file.close().unwrap();
4329    /// ```
4330    pub fn write_region_references(&self, targets: &[(&str, Selection)]) -> Result<()> {
4331        match &self.info {
4332            DatasetInfo::Writer { index, .. } => {
4333                let inner = borrow_inner(&self.file_inner);
4334                match &*inner {
4335                    H5FileInner::Writer(writer) => {
4336                        writer.write_region_references(*index, 0, targets)?;
4337                        Ok(())
4338                    }
4339                    _ => Err(Hdf5Error::InvalidState(
4340                        "file is no longer in write mode".into(),
4341                    )),
4342                }
4343            }
4344            DatasetInfo::Reader { .. } => {
4345                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
4346            }
4347        }
4348    }
4349
4350    /// Write revised region references over `targets` into elements
4351    /// `0..targets.len()` — `H5Rcreate_region` plus `H5Dwrite` of an
4352    /// `H5T_STD_REF` dataset.
4353    ///
4354    /// The dataset must have been created with
4355    /// [`std_region_references`](DatasetBuilder::std_region_references) or one
4356    /// of its two siblings, which make the same datatype. Each target is the
4357    /// path of an existing *dataset* and a [`Selection`] over it, which must
4358    /// fit that dataset's extent. What reaches the file is a global-heap blob
4359    /// holding the target's object header address (assigned when the file is
4360    /// finalized) and the serialized selection, and an element carrying the
4361    /// blob's id and its byte count. Elements left unwritten read back as null
4362    /// references.
4363    pub fn write_std_region_references(&self, targets: &[(&str, Selection)]) -> Result<()> {
4364        let targets: Vec<(&str, ReferenceTarget)> = targets
4365            .iter()
4366            .map(|(path, selection)| (*path, ReferenceTarget::Region(selection.clone())))
4367            .collect();
4368        self.write_revised_references(&targets)
4369    }
4370
4371    /// Write attribute references naming `targets` into elements
4372    /// `0..targets.len()` — `H5Rcreate_attr` plus `H5Dwrite` of an
4373    /// `H5T_STD_REF` dataset.
4374    ///
4375    /// Each target is the path of an existing object — a dataset, a group, or
4376    /// `/` for the root group — and the name of an attribute it already
4377    /// carries. There is no pre-1.12 form of this reference kind, so the
4378    /// dataset must have been created with
4379    /// [`attribute_references`](DatasetBuilder::attribute_references) or one of
4380    /// its two siblings. Elements left unwritten read back as null references.
4381    pub fn write_attribute_references(&self, targets: &[(&str, &str)]) -> Result<()> {
4382        let targets: Vec<(&str, ReferenceTarget)> = targets
4383            .iter()
4384            .map(|(path, name)| (*path, ReferenceTarget::Attribute((*name).to_string())))
4385            .collect();
4386        self.write_revised_references(&targets)
4387    }
4388
4389    /// Store `targets` as 1.12 reference elements, whatever mix of kinds they
4390    /// are: the one path both revised-reference writers take.
4391    fn write_revised_references(&self, targets: &[(&str, ReferenceTarget)]) -> Result<()> {
4392        match &self.info {
4393            DatasetInfo::Writer { index, .. } => {
4394                let inner = borrow_inner(&self.file_inner);
4395                match &*inner {
4396                    H5FileInner::Writer(writer) => {
4397                        writer.write_revised_references(*index, 0, targets)?;
4398                        Ok(())
4399                    }
4400                    _ => Err(Hdf5Error::InvalidState(
4401                        "file is no longer in write mode".into(),
4402                    )),
4403                }
4404            }
4405            DatasetInfo::Reader { .. } => {
4406                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
4407            }
4408        }
4409    }
4410
4411    /// Read a reference dataset's elements, each resolved to the object it
4412    /// names.
4413    ///
4414    /// Every reference kind is read: the pre-1.12 pair h5py writes —
4415    /// `Reference` (an object header address) and `RegionReference` (a heap id
4416    /// whose heap object holds the target plus a serialized selection) — and
4417    /// the 1.12 `H5T_STD_REF` trio, `H5R_OBJECT2`, `H5R_DATASET_REGION2` and
4418    /// `H5R_ATTR`. An object reference comes back as [`Reference::Object`]
4419    /// carrying the target's path, a region reference as
4420    /// [`Reference::Region`], whose [`bounds`](Reference::bounds) is the
4421    /// selection's bounding box — libhdf5's `H5Sget_select_bounds` — and an
4422    /// attribute reference as [`Reference::Attr`], which adds the attribute's
4423    /// name.
4424    ///
4425    /// A 1.12 reference written into a file other than its target's carries
4426    /// that file's name, and [`Reference::file`] reports it; the path is then
4427    /// a path inside that file, resolved by opening it under the name the
4428    /// reference carries, and `None` when nothing is there.
4429    ///
4430    /// ```no_run
4431    /// # use rust_hdf5::H5File;
4432    /// let file = H5File::open("refs.h5").unwrap();
4433    /// for r in file.dataset("refs").unwrap().read_references().unwrap() {
4434    ///     println!("{:?} {:?}", r.path(), r.bounds());
4435    /// }
4436    /// ```
4437    pub fn read_references(&self) -> Result<Vec<Reference>> {
4438        match &self.info {
4439            DatasetInfo::Reader { name, .. } => {
4440                let mut inner = borrow_inner_mut(&self.file_inner);
4441                match &mut *inner {
4442                    H5FileInner::Reader(reader) => Ok(reader.read_references(name)?),
4443                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
4444                }
4445            }
4446            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4447                "cannot read references from a dataset in write mode".into(),
4448            )),
4449        }
4450    }
4451
4452    /// Read a string dataset, fixed-width or variable-length, as one `String`
4453    /// per element.
4454    ///
4455    /// The width of a `FixedString` dataset is whatever the file says, so a
4456    /// 24-byte label column and a 100-byte one are read by the same call. The
4457    /// padding rule the datatype declares decides where each element ends —
4458    /// null-terminated (0), null-padded (1) or space-padded (2) — and its
4459    /// character set decides how the remaining bytes are decoded: ASCII (0)
4460    /// requires 7-bit bytes, UTF-8 (1) requires valid UTF-8. An element that
4461    /// violates either is an error naming the element, not a silent
4462    /// substitution; [`read_strings_lossy`](Self::read_strings_lossy) is the
4463    /// call that accepts such a file, replacing what it cannot decode.
4464    ///
4465    /// ```no_run
4466    /// # use rust_hdf5::H5File;
4467    /// let file = H5File::open("labels.h5").unwrap();
4468    /// let labels = file.dataset("names").unwrap().read_strings().unwrap();
4469    /// ```
4470    pub fn read_strings(&self) -> Result<Vec<String>> {
4471        self.read_strings_inner(false)
4472    }
4473
4474    /// [`read_strings`](Self::read_strings), but bytes that do not decode
4475    /// under the dataset's character set become U+FFFD instead of an error.
4476    ///
4477    /// Producers do mislabel the character set — a file that declares ASCII
4478    /// while storing Latin-1 or UTF-8 bytes reads here and not there.
4479    pub fn read_strings_lossy(&self) -> Result<Vec<String>> {
4480        self.read_strings_inner(true)
4481    }
4482
4483    /// The single owner of string decoding for both string datatypes: the
4484    /// element bytes are found differently, the padding and character-set
4485    /// rules that turn them into a `String` are the same.
4486    fn read_strings_inner(&self, lossy: bool) -> Result<Vec<String>> {
4487        if matches!(self.info, DatasetInfo::Writer { .. }) {
4488            return Err(Hdf5Error::InvalidState(
4489                "cannot read strings from a dataset in write mode".into(),
4490            ));
4491        }
4492        match self.datatype()? {
4493            DatatypeMessage::VarLenString { charset, .. } => self
4494                .read_vlen_bytes()?
4495                .iter()
4496                .enumerate()
4497                .map(|(i, bytes)| decode_string(bytes, charset, lossy, i))
4498                .collect(),
4499            DatatypeMessage::FixedString {
4500                size,
4501                padding,
4502                charset,
4503            } => {
4504                let width = size as usize;
4505                if width == 0 {
4506                    // A corrupt file can declare it; `chunks_exact(0)` panics.
4507                    return Err(Hdf5Error::InvalidState(
4508                        "fixed-string datatype has zero width".into(),
4509                    ));
4510                }
4511                // `read_raw_bytes` returns `product(dims) * width` bytes, so
4512                // `chunks_exact` leaves no remainder.
4513                let raw = self.read_raw_bytes()?;
4514                raw.chunks_exact(width)
4515                    .enumerate()
4516                    .map(|(i, elem)| {
4517                        decode_string(trim_fixed_string(elem, padding, i)?, charset, lossy, i)
4518                    })
4519                    .collect()
4520            }
4521            other => Err(Hdf5Error::InvalidState(format!(
4522                "read_strings is only for string datasets, this one is {other:?}"
4523            ))),
4524        }
4525    }
4526
4527    /// Read the entire dataset as a typed vector.
4528    ///
4529    /// The raw bytes are read from the file and reinterpreted as `T`. The
4530    /// caller must ensure that `T` matches the datatype used when the dataset
4531    /// was written.
4532    ///
4533    /// # Errors
4534    ///
4535    /// Returns an error if:
4536    /// - The file is in write mode.
4537    /// - The raw data size is not a multiple of `T::element_size()`.
4538    pub fn read_raw<T: H5Type>(&self) -> Result<Vec<T>> {
4539        match &self.info {
4540            DatasetInfo::Reader {
4541                name, element_size, ..
4542            } => {
4543                if T::element_size() != *element_size {
4544                    return Err(Hdf5Error::TypeMismatch(format!(
4545                        "read type has element size {} but dataset has element size {}",
4546                        T::element_size(),
4547                        element_size,
4548                    )));
4549                }
4550
4551                let datatype = self.datatype()?;
4552                let mut inner = borrow_inner_mut(&self.file_inner);
4553                let H5FileInner::Reader(reader) = &mut *inner else {
4554                    return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
4555                };
4556                let total = reader.dataset_raw_size(name)? as usize;
4557                if !total.is_multiple_of(T::element_size()) {
4558                    return Err(Hdf5Error::TypeMismatch(format!(
4559                        "raw data size {total} is not a multiple of element size {}",
4560                        T::element_size(),
4561                    )));
4562                }
4563
4564                // The image is read into the vector this returns, so the
4565                // bytes are touched once rather than being zeroed, read, and
4566                // then copied into a second buffer of the same size.
4567                read_image_into_new(total / T::element_size(), |image| {
4568                    reader.read_dataset_raw_into_dst(name, image, ReadDst::Fresh)?;
4569                    to_host_byte_order(image, &datatype, T::element_size())
4570                })
4571            }
4572            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4573                "cannot read from a dataset in write mode".into(),
4574            )),
4575        }
4576    }
4577
4578    /// View the entire dataset as `&[T]` pointing straight into the file's
4579    /// memory map — no read, no copy, no allocation.
4580    ///
4581    /// The returned [`MappedView<T>`](crate::MappedView) dereferences to
4582    /// `&[T]` holding exactly what [`read_raw`](Self::read_raw) would have
4583    /// returned, bit for bit.
4584    ///
4585    /// # When it works
4586    ///
4587    /// The file must be open read-only and mapped (which a read-only open
4588    /// does whenever the OS allows), the dataset's raw data must be one
4589    /// contiguous stretch of that file, and its stored elements must already
4590    /// be the host image of a `T` — same width, host byte order, significant
4591    /// bits filling the element. That is the ordinary case for an
4592    /// uncompressed, non-chunked numeric dataset written by either this crate
4593    /// or libhdf5.
4594    ///
4595    /// # When it refuses
4596    ///
4597    /// Zero-copy is a contract, not an optimization: when the bytes cannot be
4598    /// handed over as they lie, this returns
4599    /// [`Hdf5Error::NotViewable`] naming the
4600    /// reason and never quietly falls back to copying. Every
4601    /// [`ViewRefusal`](crate::ViewRefusal) is a case where
4602    /// [`read_raw`](Self::read_raw) still works: the file is not mapped (an
4603    /// open that holds no shared lock never maps), the layout is chunked,
4604    /// compact, virtual, or external, no storage is
4605    /// allocated (the dataset reads as its fill value), `T` is the wrong
4606    /// width, the stored elements need a byte-order swap or bit unpacking,
4607    /// the data lands at an offset `T`'s alignment does not permit, or the
4608    /// image runs past the end of the map.
4609    ///
4610    /// # Snapshot semantics
4611    ///
4612    /// The view owns a share of the map rather than borrowing the file
4613    /// handle, so it stays readable after the dataset and the file are
4614    /// dropped, and after a SWMR refresh has retaken the map — a live view
4615    /// keeps showing the file as it was when *its* map was taken, while the
4616    /// refreshed handle reads the new one. Nothing about a view is
4617    /// invalidated by anything this process does. The share carries the
4618    /// shared file lock the map was taken under, so for as long as any view
4619    /// is alive a writer that honours locks cannot open the file, whether or
4620    /// not the reader that took the map is still open.
4621    ///
4622    /// # Truncation
4623    ///
4624    /// The pages are the file's own. Another process writing the file in
4625    /// place is seen through the view, and one *truncating* it under the map
4626    /// faults with `SIGBUS` on the pages that went away. The shared lock the
4627    /// view keeps is what stands between the map and such a writer; one that
4628    /// waives locks ([`FileLocking::Disabled`](crate::FileLocking::Disabled),
4629    /// or a filesystem without them) is outside what any guard inside this
4630    /// process can see.
4631    ///
4632    /// ```no_run
4633    /// # use rust_hdf5::H5File;
4634    /// let file = H5File::open("data.h5")?;
4635    /// let ds = file.dataset("matrix")?;
4636    /// let view = ds.read_mapped::<f64>()?;
4637    /// let total: f64 = view.iter().sum();
4638    /// # Ok::<(), rust_hdf5::Hdf5Error>(())
4639    /// ```
4640    #[cfg(feature = "mmap")]
4641    pub fn read_mapped<T: H5Type>(&self) -> Result<crate::mapped::MappedView<T>> {
4642        self.mapped_view(crate::mapped::ViewRange::Whole)
4643    }
4644
4645    /// View a contiguous sub-range of the dataset as `&[T]` pointing straight
4646    /// into the file's memory map.
4647    ///
4648    /// `starts` and `counts` name the same N-dimensional selection
4649    /// [`read_slice`](Self::read_slice) takes, and the view holds exactly what
4650    /// that call would have returned — but only when the selection is one
4651    /// contiguous run of the stored image: a trailing group of dimensions
4652    /// taken whole, the dimension before it taken as one span, and a single
4653    /// index along every dimension before that. Anything else steps over
4654    /// elements a single slice cannot skip, and is refused with
4655    /// [`ViewRefusal::Range`](crate::ViewRefusal::Range) rather than gathered
4656    /// into a copy.
4657    ///
4658    /// Everything [`read_mapped`](Self::read_mapped) documents about when a
4659    /// dataset can be viewed, snapshot semantics, and truncation applies here
4660    /// unchanged.
4661    #[cfg(feature = "mmap")]
4662    pub fn read_mapped_slice<T: H5Type>(
4663        &self,
4664        starts: &[usize],
4665        counts: &[usize],
4666    ) -> Result<crate::mapped::MappedView<T>> {
4667        self.mapped_view(crate::mapped::ViewRange::Slab { starts, counts })
4668    }
4669
4670    /// The one route from a dataset handle to the file's map: ask the reader
4671    /// that owns the dataset for the facts, and hand them to
4672    /// [`crate::mapped::view`], which is the only thing that can turn them
4673    /// into a view.
4674    #[cfg(feature = "mmap")]
4675    fn mapped_view<T: H5Type>(
4676        &self,
4677        range: crate::mapped::ViewRange<'_>,
4678    ) -> Result<crate::mapped::MappedView<T>> {
4679        let DatasetInfo::Reader { name, .. } = &self.info else {
4680            return Err(Hdf5Error::InvalidState(
4681                "cannot read from a dataset in write mode".into(),
4682            ));
4683        };
4684        let mut inner = borrow_inner_mut(&self.file_inner);
4685        let H5FileInner::Reader(reader) = &mut *inner else {
4686            return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
4687        };
4688        let src = reader.dataset_view_source(name)?;
4689        crate::mapped::view::<T>(&src, range).map_err(Hdf5Error::NotViewable)
4690    }
4691
4692    /// Read the raw byte image of a dataset without an `H5Type` carrier.
4693    ///
4694    /// The counterpart to [`write_raw_bytes`](Self::write_raw_bytes): returns
4695    /// the element bytes verbatim regardless of the on-disk element type, so a
4696    /// runtime [`CompoundType`](crate::types::CompoundType) whose records have
4697    /// no matching Rust primitive can be read back and decoded by the caller.
4698    pub fn read_raw_bytes(&self) -> Result<Vec<u8>> {
4699        match &self.info {
4700            DatasetInfo::Reader { name, .. } => {
4701                let mut inner = borrow_inner_mut(&self.file_inner);
4702                match &mut *inner {
4703                    H5FileInner::Reader(reader) => Ok(reader.read_dataset_raw(name)?),
4704                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
4705                }
4706            }
4707            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4708                "cannot read from a dataset in write mode".into(),
4709            )),
4710        }
4711    }
4712
4713    /// Read a numeric dataset as `T`, converting each element from the
4714    /// on-disk datatype.
4715    ///
4716    /// Unlike [`read_raw`](Self::read_raw), which requires `T`'s size to match
4717    /// the stored element size exactly, this inspects the dataset's datatype
4718    /// message — class, signedness, byte order, width — and converts per
4719    /// element:
4720    ///
4721    /// - integer → integer: checked; a stored value that does not fit in `T`
4722    ///   is an error naming the element index and value, never a silent wrap.
4723    /// - `f32` source → `f64`: exact widening.
4724    /// - `f64` source → `f32`, float → integer, and integer → float are
4725    ///   rejected as [`TypeMismatch`](Hdf5Error::TypeMismatch).
4726    ///
4727    /// Big-endian sources are decoded according to the datatype's byte order,
4728    /// which [`read_raw`](Self::read_raw)'s size-only check would misread.
4729    ///
4730    /// ```no_run
4731    /// # use rust_hdf5::H5File;
4732    /// let file = H5File::open("data.h5").unwrap();
4733    /// let ds = file.dataset("counts").unwrap(); // stored as e.g. i16
4734    /// let counts = ds.read_numeric_as::<i64>().unwrap();
4735    /// ```
4736    pub fn read_numeric_as<T: ReadNumeric>(&self) -> Result<Vec<T>> {
4737        match &self.info {
4738            DatasetInfo::Reader { name, .. } => {
4739                let (kind, raw) = {
4740                    let mut inner = borrow_inner_mut(&self.file_inner);
4741                    match &mut *inner {
4742                        H5FileInner::Reader(reader) => {
4743                            let info = reader
4744                                .dataset_info(name)
4745                                .ok_or_else(|| Hdf5Error::NotFound(name.clone()))?;
4746                            let kind = numeric::classify(&info.datatype)?;
4747                            (kind, reader.read_dataset_raw(name)?)
4748                        }
4749                        _ => {
4750                            return Err(Hdf5Error::InvalidState("file is not in read mode".into()))
4751                        }
4752                    }
4753                };
4754                numeric::convert(kind, &raw)
4755            }
4756            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4757                "cannot read from a dataset in write mode".into(),
4758            )),
4759        }
4760    }
4761
4762    /// Read a slice (hyperslab) of a numeric dataset as `T`, with the same
4763    /// per-element datatype conversion as
4764    /// [`read_numeric_as`](Self::read_numeric_as).
4765    ///
4766    /// `starts` and `counts` define the N-dimensional selection exactly as in
4767    /// [`read_slice`](Self::read_slice).
4768    pub fn read_numeric_slice_as<T: ReadNumeric>(
4769        &self,
4770        starts: &[usize],
4771        counts: &[usize],
4772    ) -> Result<Vec<T>> {
4773        match &self.info {
4774            DatasetInfo::Reader { name, .. } => {
4775                let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
4776                let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
4777                let (kind, raw) = {
4778                    let mut inner = borrow_inner_mut(&self.file_inner);
4779                    match &mut *inner {
4780                        H5FileInner::Reader(reader) => {
4781                            let info = reader
4782                                .dataset_info(name)
4783                                .ok_or_else(|| Hdf5Error::NotFound(name.clone()))?;
4784                            let kind = numeric::classify(&info.datatype)?;
4785                            (kind, reader.read_slice(name, &starts_u64, &counts_u64)?)
4786                        }
4787                        _ => {
4788                            return Err(Hdf5Error::InvalidState("file is not in read mode".into()))
4789                        }
4790                    }
4791                };
4792                numeric::convert(kind, &raw)
4793            }
4794            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4795                "cannot read_slice from a dataset in write mode".into(),
4796            )),
4797        }
4798    }
4799
4800    /// Read the whole dataset into a caller-provided buffer, with no allocation.
4801    ///
4802    /// `out` must have exactly `product(dims)` elements (the dataset's element
4803    /// count) and `T::element_size()` must match the dataset's on-disk element
4804    /// size, otherwise an error is returned and `out` is left unspecified. The
4805    /// zero-copy counterpart of [`read_raw`](Self::read_raw): the bytes are read
4806    /// straight into `out` rather than into a fresh `Vec`, so a pinned /
4807    /// page-locked host buffer can be filled in one pass and DMA'd to a GPU
4808    /// without the extra staging copy a `read_raw` + copy-into-pinned would
4809    /// incur. Works for every layout (contiguous, compact, and chunked under
4810    /// any index); for chunked data each decoded chunk is scattered directly
4811    /// into `out`.
4812    ///
4813    /// ```no_run
4814    /// # use rust_hdf5::H5File;
4815    /// let file = H5File::open("data.h5").unwrap();
4816    /// let ds = file.dataset("frames").unwrap();
4817    /// let n: usize = ds.shape().iter().product();
4818    /// let mut buf = vec![0u16; n];           // or a pinned host allocation
4819    /// ds.read_raw_into(&mut buf).unwrap();
4820    /// ```
4821    pub fn read_raw_into<T: H5Type>(&self, out: &mut [T]) -> Result<()> {
4822        match &self.info {
4823            DatasetInfo::Reader {
4824                name, element_size, ..
4825            } => {
4826                if T::element_size() != *element_size {
4827                    return Err(Hdf5Error::TypeMismatch(format!(
4828                        "read type has element size {} but dataset has element size {}",
4829                        T::element_size(),
4830                        element_size,
4831                    )));
4832                }
4833                let datatype = self.datatype()?;
4834                // Safety: `T: H5Type` is a `Copy` POD numeric with a defined
4835                // byte representation; every bit pattern the read writes is a
4836                // valid `T`. The byte view borrows `out` exclusively for this
4837                // call, and `out.len() * element_size` cannot overflow because
4838                // it is the byte length of an existing slice (<= isize::MAX).
4839                let byte_len = out.len() * T::element_size();
4840                let bytes = unsafe {
4841                    std::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut u8, byte_len)
4842                };
4843                {
4844                    let mut inner = borrow_inner_mut(&self.file_inner);
4845                    match &mut *inner {
4846                        H5FileInner::Reader(reader) => reader.read_dataset_raw_into(name, bytes)?,
4847                        _ => {
4848                            return Err(Hdf5Error::InvalidState("file is not in read mode".into()))
4849                        }
4850                    }
4851                }
4852                to_host_byte_order(bytes, &datatype, T::element_size())
4853            }
4854            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4855                "cannot read from a dataset in write mode".into(),
4856            )),
4857        }
4858    }
4859
4860    /// Read a hyperslab into a caller-provided buffer, with no allocation.
4861    ///
4862    /// `out` must have exactly `product(counts)` elements and
4863    /// `T::element_size()` must match the dataset's element size. The zero-copy
4864    /// counterpart of [`read_slice`](Self::read_slice) and the slice analogue of
4865    /// [`read_raw_into`](Self::read_raw_into): only chunks overlapping the
4866    /// selection are read, and the selected bytes land directly in `out` — the
4867    /// entry point for reading one frame / block straight into a pinned host
4868    /// buffer for an H2D transfer.
4869    ///
4870    /// ```no_run
4871    /// # use rust_hdf5::H5File;
4872    /// let file = H5File::open("vol.h5").unwrap();
4873    /// let ds = file.dataset("vol").unwrap();   // shape [nz, ny, nx]
4874    /// let (ny, nx) = (ds.shape()[1], ds.shape()[2]);
4875    /// let mut frame = vec![0f32; ny * nx];     // or a pinned host allocation
4876    /// ds.read_slice_into(&mut frame, &[5, 0, 0], &[1, ny, nx]).unwrap();
4877    /// ```
4878    pub fn read_slice_into<T: H5Type>(
4879        &self,
4880        out: &mut [T],
4881        starts: &[usize],
4882        counts: &[usize],
4883    ) -> Result<()> {
4884        match &self.info {
4885            DatasetInfo::Reader {
4886                name, element_size, ..
4887            } => {
4888                if T::element_size() != *element_size {
4889                    return Err(Hdf5Error::TypeMismatch(format!(
4890                        "read type has element size {} but dataset has element size {}",
4891                        T::element_size(),
4892                        element_size,
4893                    )));
4894                }
4895                let datatype = self.datatype()?;
4896                let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
4897                let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
4898                // Safety: see `read_raw_into` — `T: H5Type` POD, exclusive
4899                // borrow of `out`, byte length within bounds.
4900                let byte_len = out.len() * T::element_size();
4901                let bytes = unsafe {
4902                    std::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut u8, byte_len)
4903                };
4904                {
4905                    let mut inner = borrow_inner_mut(&self.file_inner);
4906                    match &mut *inner {
4907                        H5FileInner::Reader(reader) => {
4908                            reader.read_slice_into(name, &starts_u64, &counts_u64, bytes)?
4909                        }
4910                        _ => {
4911                            return Err(Hdf5Error::InvalidState("file is not in read mode".into()))
4912                        }
4913                    }
4914                }
4915                to_host_byte_order(bytes, &datatype, T::element_size())
4916            }
4917            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
4918                "cannot read from a dataset in write mode".into(),
4919            )),
4920        }
4921    }
4922}
4923
4924// ---------------------------------------------------------------------------
4925// Datatype-aware numeric conversion (read_numeric_as)
4926// ---------------------------------------------------------------------------
4927
4928/// Marker trait for the Rust types [`H5Dataset::read_numeric_as`] can convert
4929/// into: the integer primitives (checked, never wrapping) plus `f32`/`f64`
4930/// (widening only).
4931///
4932/// Sealed — the conversion policy is part of the library contract, so the
4933/// trait cannot be implemented outside this crate.
4934pub trait ReadNumeric: numeric::Sealed {}
4935impl<T: numeric::Sealed> ReadNumeric for T {}
4936
4937pub(crate) mod numeric {
4938    //! Per-element decode + checked conversion for `read_numeric_as` (and the
4939    //! attribute counterpart `H5Attribute::read_numeric_as`).
4940
4941    use crate::error::{Hdf5Error, Result};
4942    use crate::format::messages::datatype::{ByteOrder, DatatypeMessage, IeeeFormat};
4943
4944    /// The on-disk element shape `classify` accepted.
4945    #[derive(Clone, Copy)]
4946    pub enum SourceKind {
4947        Int {
4948            size: usize,
4949            signed: bool,
4950            byte_order: ByteOrder,
4951        },
4952        F16(ByteOrder),
4953        F32(ByteOrder),
4954        F64(ByteOrder),
4955    }
4956
4957    impl SourceKind {
4958        fn element_size(self) -> usize {
4959            match self {
4960                SourceKind::Int { size, .. } => size,
4961                SourceKind::F16(_) => 2,
4962                SourceKind::F32(_) => 4,
4963                SourceKind::F64(_) => 8,
4964            }
4965        }
4966    }
4967
4968    /// Widen an IEEE 754 binary16 bit pattern to `f32`, which represents every
4969    /// half — including subnormals, infinities and NaN payloads — exactly.
4970    ///
4971    /// Rust has no stable `f16` to convert through.
4972    fn f16_bits_to_f32(bits: u16) -> f32 {
4973        let sign = u32::from(bits >> 15);
4974        let exponent = u32::from((bits >> 10) & 0x1f);
4975        let mantissa = u32::from(bits & 0x03ff);
4976        if exponent == 0 {
4977            // Zero and subnormals: the value is mantissa * 2^-24, which is a
4978            // normal f32 for every mantissa, so the multiply is exact. Going
4979            // through the sign separately keeps -0.0.
4980            let magnitude = mantissa as f32 * (1.0 / 16_777_216.0);
4981            return if sign == 1 { -magnitude } else { magnitude };
4982        }
4983        let out = if exponent == 0x1f {
4984            // Infinity and NaN; shifting the mantissa maps the quiet bit onto
4985            // f32's quiet bit and preserves the rest of the payload.
4986            (sign << 31) | 0x7f80_0000 | (mantissa << 13)
4987        } else {
4988            // Normal: rebias the exponent (127 - 15) and left-align the
4989            // mantissa.
4990            (sign << 31) | ((exponent + 112) << 23) | (mantissa << 13)
4991        };
4992        f32::from_bits(out)
4993    }
4994
4995    /// Map a datatype message to a supported numeric source shape.
4996    ///
4997    /// Accepts standard-width integers (1/2/4/8 bytes, full precision, zero
4998    /// bit offset) and IEEE binary32/binary64 floats; everything else is a
4999    /// `TypeMismatch` naming what was found.
5000    pub fn classify(dt: &DatatypeMessage) -> Result<SourceKind> {
5001        match *dt {
5002            DatatypeMessage::FixedPoint {
5003                size,
5004                byte_order,
5005                signed,
5006                bit_offset,
5007                bit_precision,
5008            } => {
5009                if !matches!(size, 1 | 2 | 4 | 8)
5010                    || bit_offset != 0
5011                    || u32::from(bit_precision) != size * 8
5012                {
5013                    return Err(Hdf5Error::TypeMismatch(format!(
5014                        "fixed-point datatype (size {size}, bit offset {bit_offset}, \
5015                         precision {bit_precision}) is not a standard-width integer",
5016                    )));
5017                }
5018                Ok(SourceKind::Int {
5019                    size: size as usize,
5020                    signed,
5021                    byte_order,
5022                })
5023            }
5024            DatatypeMessage::BitField {
5025                size,
5026                byte_order,
5027                bit_offset,
5028                bit_precision,
5029            } => {
5030                // A bit field has no signed form; a full-width one is the
5031                // unsigned integer of the stored width. A narrower one would
5032                // need a shift-and-mask conversion this path does not model.
5033                if !matches!(size, 1 | 2 | 4 | 8)
5034                    || bit_offset != 0
5035                    || u32::from(bit_precision) != size * 8
5036                {
5037                    return Err(Hdf5Error::TypeMismatch(format!(
5038                        "bit-field datatype (size {size}, bit offset {bit_offset}, \
5039                         precision {bit_precision}) is not a whole-width bit field",
5040                    )));
5041                }
5042                Ok(SourceKind::Int {
5043                    size: size as usize,
5044                    signed: false,
5045                    byte_order,
5046                })
5047            }
5048            DatatypeMessage::FloatingPoint {
5049                size,
5050                byte_order,
5051                exponent_size,
5052                mantissa_size,
5053                ..
5054            } => match dt.ieee_format() {
5055                Some(IeeeFormat::Binary16) => Ok(SourceKind::F16(byte_order)),
5056                Some(IeeeFormat::Binary32) => Ok(SourceKind::F32(byte_order)),
5057                Some(IeeeFormat::Binary64) => Ok(SourceKind::F64(byte_order)),
5058                None => Err(Hdf5Error::TypeMismatch(format!(
5059                    "floating-point datatype (size {size}, exponent {exponent_size} bits, \
5060                     mantissa {mantissa_size} bits) is not an IEEE 754 interchange format",
5061                ))),
5062            },
5063            ref other => Err(Hdf5Error::TypeMismatch(format!(
5064                "dataset datatype '{other}' is not numeric",
5065            ))),
5066        }
5067    }
5068
5069    /// Decode and convert every element of `raw` into `T`.
5070    ///
5071    /// One loop per source width and lane type, so an element is a
5072    /// fixed-size load, a sign extension and a range check, and the
5073    /// `TypeMismatch` for an element that does not fit is built only when
5074    /// one is found. An empty input converts to an empty vector whatever
5075    /// the classes.
5076    pub fn convert<T: Sealed>(kind: SourceKind, raw: &[u8]) -> Result<Vec<T>> {
5077        let size = kind.element_size();
5078        if !raw.len().is_multiple_of(size) {
5079            return Err(Hdf5Error::TypeMismatch(format!(
5080                "raw data size {} is not a multiple of element size {size}",
5081                raw.len(),
5082            )));
5083        }
5084        let mut out = vec![T::default(); raw.len() / size];
5085        match kind {
5086            SourceKind::Int {
5087                size,
5088                signed,
5089                byte_order,
5090            } => {
5091                let le = byte_order == ByteOrder::LittleEndian;
5092                match (size, signed) {
5093                    (1, true) => ints::<1, i64, T>(raw, le, &mut out),
5094                    (1, false) => ints::<1, u64, T>(raw, le, &mut out),
5095                    (2, true) => ints::<2, i64, T>(raw, le, &mut out),
5096                    (2, false) => ints::<2, u64, T>(raw, le, &mut out),
5097                    (4, true) => ints::<4, i64, T>(raw, le, &mut out),
5098                    (4, false) => ints::<4, u64, T>(raw, le, &mut out),
5099                    (8, true) => ints::<8, i64, T>(raw, le, &mut out),
5100                    (8, false) => ints::<8, u64, T>(raw, le, &mut out),
5101                    _ => unreachable!("classify admits 1-, 2-, 4- and 8-byte integers only"),
5102                }
5103            }
5104            SourceKind::F16(order) => floats::<2, T>(raw, order, &mut out, |b| {
5105                T::from_f32(f16_bits_to_f32(u16::from_le_bytes(b)))
5106            }),
5107            SourceKind::F32(order) => {
5108                floats::<4, T>(raw, order, &mut out, |b| T::from_f32(f32::from_le_bytes(b)))
5109            }
5110            SourceKind::F64(order) => {
5111                floats::<8, T>(raw, order, &mut out, |b| T::from_f64(f64::from_le_bytes(b)))
5112            }
5113        }?;
5114        Ok(out)
5115    }
5116
5117    /// The integer elements of `raw`, `N` bytes each, through lane `L`.
5118    fn ints<const N: usize, L: IntLane, T: Sealed>(
5119        raw: &[u8],
5120        le: bool,
5121        out: &mut [T],
5122    ) -> Result<()> {
5123        let (chunks, _) = raw.as_chunks::<N>();
5124        for (index, (o, &chunk)) in out.iter_mut().zip(chunks).enumerate() {
5125            let mut bytes = chunk;
5126            if !le {
5127                bytes.reverse();
5128            }
5129            *o = L::load(bytes).into_target(index)?;
5130        }
5131        Ok(())
5132    }
5133
5134    /// The float elements of `raw`, `N` bytes each, `decode` taking each
5135    /// one's little-endian bytes.
5136    fn floats<const N: usize, T: Sealed>(
5137        raw: &[u8],
5138        order: ByteOrder,
5139        out: &mut [T],
5140        decode: impl Fn([u8; N]) -> Result<T>,
5141    ) -> Result<()> {
5142        let (chunks, _) = raw.as_chunks::<N>();
5143        for (o, &chunk) in out.iter_mut().zip(chunks) {
5144            let mut bytes = chunk;
5145            if order == ByteOrder::BigEndian {
5146                bytes.reverse();
5147            }
5148            *o = decode(bytes)?;
5149        }
5150        Ok(())
5151    }
5152
5153    /// The lane an integer source decodes to: `i64` for a signed source,
5154    /// `u64` for an unsigned one, so every standard width — u64::MAX
5155    /// included — is held without loss.
5156    pub trait IntLane: Copy {
5157        /// The value of an element's little-endian `bytes`, `N` in 1..=8.
5158        fn load<const N: usize>(bytes: [u8; N]) -> Self;
5159        fn into_target<T: Sealed>(self, index: usize) -> Result<T>;
5160    }
5161
5162    impl IntLane for u64 {
5163        fn load<const N: usize>(bytes: [u8; N]) -> Self {
5164            let mut padded = [0u8; 8];
5165            padded[..N].copy_from_slice(&bytes);
5166            u64::from_le_bytes(padded)
5167        }
5168        fn into_target<T: Sealed>(self, index: usize) -> Result<T> {
5169            T::from_u64(self, index)
5170        }
5171    }
5172
5173    impl IntLane for i64 {
5174        fn load<const N: usize>(bytes: [u8; N]) -> Self {
5175            // Arithmetic right shift sign-extends the low `N` bytes.
5176            let shift = 64 - 8 * N as u32;
5177            ((u64::load(bytes) << shift) as i64) >> shift
5178        }
5179        fn into_target<T: Sealed>(self, index: usize) -> Result<T> {
5180            T::from_i64(self, index)
5181        }
5182    }
5183
5184    /// The sealed half of `ReadNumeric`: how one source element becomes a
5185    /// `Self`, or a `TypeMismatch` explaining why it cannot. `index` is the
5186    /// element's position, for the message.
5187    pub trait Sealed: Copy + Default {
5188        fn from_i64(v: i64, index: usize) -> Result<Self>;
5189        fn from_u64(v: u64, index: usize) -> Result<Self>;
5190        fn from_f32(v: f32) -> Result<Self>;
5191        fn from_f64(v: f64) -> Result<Self>;
5192    }
5193
5194    macro_rules! int_targets {
5195        ($($t:ty),* $(,)?) => {$(
5196            impl Sealed for $t {
5197                fn from_i64(v: i64, index: usize) -> Result<Self> {
5198                    <$t>::try_from(v).map_err(|_| int_overflow::<$t>(v, index))
5199                }
5200                fn from_u64(v: u64, index: usize) -> Result<Self> {
5201                    <$t>::try_from(v).map_err(|_| int_overflow::<$t>(v, index))
5202                }
5203                fn from_f32(_: f32) -> Result<Self> {
5204                    Err(float_as_int::<$t>())
5205                }
5206                fn from_f64(_: f64) -> Result<Self> {
5207                    Err(float_as_int::<$t>())
5208                }
5209            }
5210        )*};
5211    }
5212    int_targets!(i8, i16, i32, i64, u8, u16, u32, u64);
5213
5214    // Not in the macro: `i128::try_from` and `u128::try_from(u64)` are
5215    // infallible, which trips clippy::unnecessary_fallible_conversions.
5216    impl Sealed for i128 {
5217        fn from_i64(v: i64, _index: usize) -> Result<Self> {
5218            Ok(Self::from(v))
5219        }
5220        fn from_u64(v: u64, _index: usize) -> Result<Self> {
5221            Ok(Self::from(v))
5222        }
5223        fn from_f32(_: f32) -> Result<Self> {
5224            Err(float_as_int::<i128>())
5225        }
5226        fn from_f64(_: f64) -> Result<Self> {
5227            Err(float_as_int::<i128>())
5228        }
5229    }
5230
5231    impl Sealed for u128 {
5232        fn from_i64(v: i64, index: usize) -> Result<Self> {
5233            Self::try_from(v).map_err(|_| int_overflow::<u128>(v, index))
5234        }
5235        fn from_u64(v: u64, _index: usize) -> Result<Self> {
5236            Ok(Self::from(v))
5237        }
5238        fn from_f32(_: f32) -> Result<Self> {
5239            Err(float_as_int::<u128>())
5240        }
5241        fn from_f64(_: f64) -> Result<Self> {
5242            Err(float_as_int::<u128>())
5243        }
5244    }
5245
5246    fn int_overflow<T>(v: impl std::fmt::Display, index: usize) -> Hdf5Error {
5247        Hdf5Error::TypeMismatch(format!(
5248            "value {v} at element {index} does not fit in {}",
5249            std::any::type_name::<T>()
5250        ))
5251    }
5252
5253    fn float_as_int<T>() -> Hdf5Error {
5254        Hdf5Error::TypeMismatch(format!(
5255            "cannot read a floating-point dataset as {}; read as f64 and convert explicitly",
5256            std::any::type_name::<T>()
5257        ))
5258    }
5259
5260    impl Sealed for f32 {
5261        fn from_i64(_: i64, _index: usize) -> Result<Self> {
5262            Err(int_as_f32())
5263        }
5264        fn from_u64(_: u64, _index: usize) -> Result<Self> {
5265            Err(int_as_f32())
5266        }
5267        fn from_f32(v: f32) -> Result<Self> {
5268            Ok(v)
5269        }
5270        fn from_f64(_: f64) -> Result<Self> {
5271            Err(Hdf5Error::TypeMismatch(
5272                "narrowing an f64 dataset to f32 loses precision; read as f64".into(),
5273            ))
5274        }
5275    }
5276
5277    fn int_as_f32() -> Hdf5Error {
5278        Hdf5Error::TypeMismatch(
5279            "cannot read an integer dataset as f32; read as an integer type and convert \
5280             explicitly"
5281                .into(),
5282        )
5283    }
5284
5285    impl Sealed for f64 {
5286        fn from_i64(_: i64, _index: usize) -> Result<Self> {
5287            Err(int_as_f64())
5288        }
5289        fn from_u64(_: u64, _index: usize) -> Result<Self> {
5290            Err(int_as_f64())
5291        }
5292        // Every f32 is exactly representable as f64.
5293        fn from_f32(v: f32) -> Result<Self> {
5294            Ok(f64::from(v))
5295        }
5296        fn from_f64(v: f64) -> Result<Self> {
5297            Ok(v)
5298        }
5299    }
5300
5301    fn int_as_f64() -> Hdf5Error {
5302        Hdf5Error::TypeMismatch(
5303            "cannot read an integer dataset as f64; integers above 2^53 lose precision — \
5304             read as an integer type and convert explicitly"
5305                .into(),
5306        )
5307    }
5308
5309    #[cfg(test)]
5310    mod tests {
5311        use super::*;
5312
5313        /// Every binary16 bit pattern class widens to the f32 with the same
5314        /// value: zeros keep their sign, subnormals stay exact, infinities and
5315        /// NaN payloads survive.
5316        #[test]
5317        fn f16_widening_is_exact() {
5318            let cases: [(u16, f32); 10] = [
5319                (0x0000, 0.0),
5320                (0x3c00, 1.0),
5321                (0xc000, -2.0),
5322                (0x3555, 0.333_251_95),   // nearest half to 1/3
5323                (0x0001, 5.960_464_5e-8), // smallest subnormal, 2^-24
5324                (0x03ff, 6.097_555e-5),   // largest subnormal
5325                (0x0400, 6.103_515_6e-5), // smallest normal
5326                (0x7bff, 65504.0),        // largest finite
5327                (0x7c00, f32::INFINITY),
5328                (0xfc00, f32::NEG_INFINITY),
5329            ];
5330            for (bits, expected) in cases {
5331                let got = f16_bits_to_f32(bits);
5332                assert_eq!(got, expected, "0x{bits:04x} widened to {got}");
5333            }
5334
5335            let neg_zero = f16_bits_to_f32(0x8000);
5336            assert_eq!(neg_zero, 0.0);
5337            assert!(neg_zero.is_sign_negative(), "-0.0 lost its sign");
5338
5339            let nan = f16_bits_to_f32(0x7e01);
5340            assert!(nan.is_nan());
5341            // The quiet bit and the payload land in f32's mantissa.
5342            assert_eq!(nan.to_bits(), 0x7fc0_2000);
5343        }
5344
5345        #[test]
5346        fn f16_source_converts_and_honors_byte_order() {
5347            let kind = classify(&DatatypeMessage::f16_type()).unwrap();
5348            // 1.0, -2.0, 0.333..., 65504
5349            let raw = [0x00, 0x3c, 0x00, 0xc0, 0x55, 0x35, 0xff, 0x7b];
5350            assert_eq!(
5351                convert::<f32>(kind, &raw).unwrap(),
5352                vec![1.0, -2.0, 0.333_251_95, 65504.0]
5353            );
5354            assert_eq!(
5355                convert::<f64>(kind, &raw).unwrap(),
5356                vec![1.0, -2.0, 0.333_251_953_125, 65504.0]
5357            );
5358
5359            let DatatypeMessage::FloatingPoint { .. } = DatatypeMessage::f16_type() else {
5360                unreachable!()
5361            };
5362            let mut be = DatatypeMessage::f16_type();
5363            if let DatatypeMessage::FloatingPoint { byte_order, .. } = &mut be {
5364                *byte_order = ByteOrder::BigEndian;
5365            }
5366            let be_kind = classify(&be).unwrap();
5367            assert_eq!(convert::<f32>(be_kind, &[0x3c, 0x00]).unwrap(), vec![1.0]);
5368        }
5369
5370        /// A float whose layout is not an interchange format is refused, not
5371        /// reinterpreted.
5372        #[test]
5373        fn non_ieee_float_is_refused() {
5374            let mut odd = DatatypeMessage::f32_type();
5375            if let DatatypeMessage::FloatingPoint { exponent_bias, .. } = &mut odd {
5376                *exponent_bias = 63;
5377            }
5378            assert!(odd.ieee_format().is_none());
5379            let err = classify(&odd).err().expect("non-IEEE float was accepted");
5380            assert!(
5381                err.to_string().contains("IEEE 754 interchange format"),
5382                "unexpected error: {err}"
5383            );
5384        }
5385    }
5386}
5387
5388#[cfg(test)]
5389mod tests {
5390    use crate::H5File;
5391    use std::path::PathBuf;
5392
5393    fn temp_path(name: &str) -> PathBuf {
5394        // Include PID + a per-call atomic counter so that concurrent
5395        // cargo invocations and any kernel-level "lock not yet
5396        // released" races between sequential opens cannot collide.
5397        use std::sync::atomic::{AtomicU64, Ordering};
5398        static COUNTER: AtomicU64 = AtomicU64::new(0);
5399        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
5400        std::env::temp_dir().join(format!(
5401            "hdf5_dataset_test_{}_{}_{}.h5",
5402            name,
5403            std::process::id(),
5404            n
5405        ))
5406    }
5407
5408    #[test]
5409    fn runtime_compound_via_datatype_override_and_raw_bytes() {
5410        use crate::format::messages::datatype::DatatypeMessage;
5411        use crate::types::{CompoundType, H5Type};
5412
5413        let path = temp_path("compound_raw");
5414        // A 12-byte packed compound with NO matching Rust primitive carrier,
5415        // so it can only be written through the datatype() override +
5416        // write_raw_bytes path (the runtime-CompoundType use case).
5417        let ct = CompoundType {
5418            members: vec![
5419                ("id".to_string(), i32::hdf5_type(), 0),
5420                ("val".to_string(), f64::hdf5_type(), 4),
5421            ],
5422            total_size: 12,
5423        };
5424        let recs: [(i32, f64); 3] = [(1, 2.5), (2, 3.5), (3, -4.0)];
5425        let mut bytes = Vec::new();
5426        for (id, val) in recs {
5427            bytes.extend_from_slice(&id.to_le_bytes());
5428            bytes.extend_from_slice(&val.to_le_bytes());
5429        }
5430
5431        {
5432            let file = H5File::create(&path).unwrap();
5433            let ds = file
5434                .new_dataset::<u8>()
5435                .datatype(ct.to_datatype())
5436                .shape([recs.len()])
5437                .create("records")
5438                .unwrap();
5439            ds.write_raw_bytes(&bytes).unwrap();
5440            file.close().unwrap();
5441        }
5442        {
5443            let file = H5File::open(&path).unwrap();
5444            let ds = file.dataset("records").unwrap();
5445            // The on-disk element type is the compound we specified (size 12),
5446            // not the u8 carrier.
5447            match ds.datatype().unwrap() {
5448                DatatypeMessage::Compound { size, members } => {
5449                    assert_eq!(size, 12);
5450                    assert_eq!(members.len(), 2);
5451                    assert_eq!(members[0].name, "id");
5452                    assert_eq!(members[0].offset, 0);
5453                    assert_eq!(members[1].name, "val");
5454                    assert_eq!(members[1].offset, 4);
5455                }
5456                other => panic!("expected compound datatype, got {other:?}"),
5457            }
5458            assert_eq!(ds.read_raw_bytes().unwrap(), bytes);
5459        }
5460        std::fs::remove_file(&path).ok();
5461    }
5462
5463    #[test]
5464    fn builder_requires_shape() {
5465        let path = temp_path("no_shape");
5466        let file = H5File::create(&path).unwrap();
5467        let result = file.new_dataset::<u8>().create("data");
5468        assert!(result.is_err());
5469        std::fs::remove_file(&path).ok();
5470    }
5471
5472    // The last chunk along a *fixed* dimension covers more elements than the
5473    // extent has, so growing the dataspace to the chunk's far edge asks for
5474    // more than the declared maximum. Before the clamp the chunk was written
5475    // and then the call failed on that extend, leaving the bytes in the file
5476    // and the caller an error.
5477    #[test]
5478    fn a_partial_edge_chunk_does_not_grow_past_the_declared_maximum() {
5479        let path = temp_path("edge_chunk_extent");
5480        // Extensible array: dimension 1 is unlimited, dimension 0 is fixed at
5481        // 10 and not a multiple of the chunk's 4.
5482        let file = H5File::create(&path).unwrap();
5483        let ds = file
5484            .new_dataset::<i32>()
5485            .shape([10usize, 4])
5486            .max_shape(&[Some(10), None])
5487            .chunk(&[4, 4])
5488            .create("grid")
5489            .unwrap();
5490        let chunk: Vec<u8> = (0i32..16).flat_map(|v| v.to_le_bytes()).collect();
5491        // Chunk row 2 spans elements 8..12 of a dimension that stops at 10.
5492        ds.write_chunk_at(&[2, 0], &chunk).unwrap();
5493        assert_eq!(ds.shape(), vec![10, 4]);
5494        file.close().unwrap();
5495
5496        let file = H5File::open(&path).unwrap();
5497        let back = file.dataset("grid").unwrap().read_raw::<i32>().unwrap();
5498        assert_eq!(back.len(), 40);
5499        assert_eq!(&back[32..40], &[0, 1, 2, 3, 4, 5, 6, 7]);
5500        drop(file);
5501        std::fs::remove_file(&path).ok();
5502    }
5503
5504    // The cap is in `write_chunk_at_inner`, so it belongs to every chunk index
5505    // whose write reaches the extend below it — the v2 B-tree as much as the
5506    // extensible array. A rank-3 dataset with two unlimited dimensions gets
5507    // that index, and its third, fixed dimension is where the last chunk
5508    // overhangs. (The fixed array and the implicit index return before the
5509    // extend: their shape cannot grow at all. The version-1 B-tree does reach
5510    // it — `tests/legacy_append.rs` carries that case, which needs a classic
5511    // file.)
5512    #[test]
5513    fn the_edge_write_cap_holds_for_the_v2_btree_index() {
5514        let path = temp_path("edge_chunk_bt2");
5515        let file = H5File::create(&path).unwrap();
5516        let ds = file
5517            .new_dataset::<i32>()
5518            .shape([10usize, 4, 4])
5519            .max_shape(&[Some(10), None, None])
5520            .chunk(&[4, 4, 4])
5521            .create("cube")
5522            .unwrap();
5523        let chunk: Vec<u8> = (0i32..64).flat_map(|v| v.to_le_bytes()).collect();
5524        // Chunk plane 2 spans elements 8..12 of a dimension that stops at 10.
5525        ds.write_chunk_at(&[2, 0, 0], &chunk).unwrap();
5526        assert_eq!(ds.shape(), vec![10, 4, 4]);
5527        file.close().unwrap();
5528
5529        let file = H5File::open(&path).unwrap();
5530        let back = file.dataset("cube").unwrap().read_raw::<i32>().unwrap();
5531        assert_eq!(back.len(), 160);
5532        // Rows 8 and 9 of the written plane, 16 elements each.
5533        assert_eq!(&back[128..160], &(0i32..32).collect::<Vec<_>>()[..]);
5534        drop(file);
5535        std::fs::remove_file(&path).ok();
5536    }
5537
5538    // The cap guards a chunk-coordinate write, and neither an externally
5539    // stored nor a virtual dataset has chunk coordinates to guard: both are
5540    // contiguous storage classes, refused at build together with chunked
5541    // storage, and `write_chunk_at` refuses what is not chunked. So the path
5542    // the case above exercises cannot be entered for either — asserted here
5543    // rather than left to inspection, since both classes route their raw bytes
5544    // through the same writer as the chunk grid does.
5545    #[test]
5546    fn an_external_or_virtual_dataset_never_reaches_the_edge_write_cap() {
5547        use crate::Selection;
5548        let dir = std::env::temp_dir().join(format!(
5549            "rust_hdf5_edge_cap_{}_{}",
5550            std::process::id(),
5551            temp_path("x").file_name().unwrap().to_string_lossy()
5552        ));
5553        std::fs::create_dir_all(&dir).unwrap();
5554        let path = dir.join("edge_cap.h5");
5555        let file = H5File::create(&path).unwrap();
5556        let payload = dir.join("payload.raw");
5557
5558        // Chunked storage and these two are mutually exclusive at build.
5559        for (which, res) in [
5560            (
5561                "external",
5562                file.new_dataset::<i32>()
5563                    .shape([10usize])
5564                    .external(&[(payload.to_str().unwrap(), 0, 40)])
5565                    .chunk(&[4])
5566                    .create("a"),
5567            ),
5568            (
5569                "virtual",
5570                file.new_dataset::<i32>()
5571                    .shape([10usize])
5572                    .virtual_mapping(Selection::All, "src.h5", "src", Selection::All)
5573                    .chunk(&[4])
5574                    .create("b"),
5575            ),
5576        ] {
5577            match res {
5578                Ok(_) => panic!("a {which} dataset cannot also be chunked"),
5579                Err(e) => assert!(e.to_string().contains("chunked"), "{which}: {e}"),
5580            }
5581        }
5582
5583        // And the coordinate write itself is refused on both, with the extent
5584        // left exactly where it was.
5585        let ext = file
5586            .new_dataset::<i32>()
5587            .shape([10usize])
5588            .external(&[(payload.to_str().unwrap(), 0, 40)])
5589            .create("outside")
5590            .unwrap();
5591        let vds = file
5592            .new_dataset::<i32>()
5593            .shape([10usize])
5594            .virtual_mapping(Selection::All, "src.h5", "src", Selection::All)
5595            .create("elsewhere")
5596            .unwrap();
5597        let chunk: Vec<u8> = (0i32..4).flat_map(|v| v.to_le_bytes()).collect();
5598        for (which, ds) in [("external", &ext), ("virtual", &vds)] {
5599            let err = ds.write_chunk_at(&[2], &chunk).unwrap_err().to_string();
5600            assert!(err.contains("only for chunked datasets"), "{which}: {err}");
5601            let err = ds
5602                .write_chunk_raw_at(&[2], &chunk, 0)
5603                .unwrap_err()
5604                .to_string();
5605            assert!(err.contains("only for chunked datasets"), "{which}: {err}");
5606            assert_eq!(ds.shape(), vec![10], "{which}");
5607        }
5608        file.close().unwrap();
5609        std::fs::remove_dir_all(&dir).ok();
5610    }
5611
5612    #[test]
5613    fn write_raw_size_mismatch() {
5614        let path = temp_path("size_mismatch");
5615        let file = H5File::create(&path).unwrap();
5616        let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
5617        // Provide 3 elements instead of 4
5618        let result = ds.write_raw(&[1u8, 2, 3]);
5619        assert!(result.is_err());
5620        std::fs::remove_file(&path).ok();
5621    }
5622
5623    // A: a filter set without explicit chunk dimensions must auto-chunk (whole
5624    // dataset = one chunk) rather than silently drop the filter on the
5625    // contiguous path. write_raw then populates that single chunk.
5626    #[cfg(feature = "deflate")]
5627    #[test]
5628    fn filter_without_chunk_autochunks_and_roundtrips() {
5629        let path = temp_path("autochunk_filter");
5630        let data: Vec<i32> = (0..8).collect();
5631        {
5632            let file = H5File::create(&path).unwrap();
5633            let ds = file
5634                .new_dataset::<i32>()
5635                .deflate(6)
5636                .shape([8])
5637                .create("seq")
5638                .unwrap();
5639            ds.write_raw(&data).unwrap();
5640            file.close().unwrap();
5641        }
5642        {
5643            let file = H5File::open(&path).unwrap();
5644            let ds = file.dataset("seq").unwrap();
5645            // The filter forced chunked storage: a single whole-dataset chunk.
5646            assert!(
5647                ds.is_chunked(),
5648                "auto-chunk did not produce chunked storage"
5649            );
5650            assert_eq!(ds.chunk_dims(), Some(vec![8]));
5651            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
5652        }
5653        std::fs::remove_file(&path).ok();
5654    }
5655
5656    // B: write_raw on an explicitly chunked + compressed dataset scatters the
5657    // full row-major image across a multi-chunk grid, including edge chunks
5658    // (7/3 -> 3,3,1 along dim0; 5/2 -> 2,2,1 along dim1).
5659    #[cfg(feature = "deflate")]
5660    #[test]
5661    fn an_edge_chunk_pads_with_zero_not_the_chunk_written_before_it() {
5662        // 4x5 over a 2x3 chunk: the second chunk of each row covers only two
5663        // of its three columns, so a third of it is padding. The image write
5664        // stages every chunk of a shape like this through one reused buffer,
5665        // and the padding has to reach the file as zero rather than as
5666        // whatever the chunk before it left in that buffer.
5667        let path = temp_path("edge_chunk_padding");
5668        let data: Vec<i32> = (1..=20).collect(); // no zeros of its own
5669        {
5670            let file = H5File::create(&path).unwrap();
5671            let ds = file
5672                .new_dataset::<i32>()
5673                .shape([4, 5])
5674                .chunk(&[2, 3])
5675                .create("grid")
5676                .unwrap();
5677            ds.write_raw(&data).unwrap();
5678            file.close().unwrap();
5679        }
5680        {
5681            let file = H5File::open(&path).unwrap();
5682            let ds = file.dataset("grid").unwrap();
5683            let as_i32 = |bytes: Vec<u8>| -> Vec<i32> {
5684                bytes
5685                    .as_chunks::<4>()
5686                    .0
5687                    .iter()
5688                    .map(|b| i32::from_le_bytes(*b))
5689                    .collect()
5690            };
5691            // The chunk that precedes each edge chunk is full, so a leak would
5692            // show as its 3rd and 6th elements (3 and 8, then 13 and 18).
5693            assert_eq!(
5694                as_i32(ds.read_chunk_raw_at(&[0, 0]).unwrap().0),
5695                [1, 2, 3, 6, 7, 8]
5696            );
5697            assert_eq!(
5698                as_i32(ds.read_chunk_raw_at(&[0, 1]).unwrap().0),
5699                [4, 5, 0, 9, 10, 0]
5700            );
5701            assert_eq!(
5702                as_i32(ds.read_chunk_raw_at(&[1, 1]).unwrap().0),
5703                [14, 15, 0, 19, 20, 0]
5704            );
5705            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
5706        }
5707        std::fs::remove_file(&path).ok();
5708    }
5709
5710    #[test]
5711    #[cfg(feature = "deflate")]
5712    fn write_raw_multichunk_edge_roundtrips() {
5713        let path = temp_path("multichunk_edge");
5714        let data: Vec<i32> = (0..35).collect(); // 7 x 5 row-major
5715        {
5716            let file = H5File::create(&path).unwrap();
5717            let ds = file
5718                .new_dataset::<i32>()
5719                .shape([7, 5])
5720                .chunk(&[3, 2])
5721                .deflate(4)
5722                .create("grid")
5723                .unwrap();
5724            ds.write_raw(&data).unwrap();
5725            file.close().unwrap();
5726        }
5727        {
5728            let file = H5File::open(&path).unwrap();
5729            let ds = file.dataset("grid").unwrap();
5730            assert_eq!(ds.shape(), vec![7, 5]);
5731            assert_eq!(ds.chunk_dims(), Some(vec![3, 2]));
5732            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
5733        }
5734        std::fs::remove_file(&path).ok();
5735    }
5736
5737    // B: write_raw on an unfiltered chunked dataset (previously rejected with
5738    // "use write_chunk for chunked datasets") now gathers and round-trips.
5739    #[test]
5740    fn write_raw_unfiltered_chunked_roundtrips() {
5741        let path = temp_path("chunked_unfiltered");
5742        let data: Vec<f64> = (0..12).map(|i| i as f64 * 1.5).collect(); // 4 x 3
5743        {
5744            let file = H5File::create(&path).unwrap();
5745            let ds = file
5746                .new_dataset::<f64>()
5747                .shape([4, 3])
5748                .chunk(&[2, 2])
5749                .create("m")
5750                .unwrap();
5751            ds.write_raw(&data).unwrap();
5752            file.close().unwrap();
5753        }
5754        {
5755            let file = H5File::open(&path).unwrap();
5756            let ds = file.dataset("m").unwrap();
5757            assert_eq!(ds.chunk_dims(), Some(vec![2, 2]));
5758            assert_eq!(ds.read_raw::<f64>().unwrap(), data);
5759        }
5760        std::fs::remove_file(&path).ok();
5761    }
5762
5763    // write_raw on an extensible-array (unlimited first dim) compressed dataset
5764    // drives write_full_image_chunked's EA branch, which gathers chunks and
5765    // compresses them through the windowed batch path. Round-trips the full
5766    // image, including a partial edge chunk along the unlimited dimension.
5767    #[cfg(feature = "deflate")]
5768    #[test]
5769    fn write_raw_ea_compressed_roundtrips() {
5770        let path = temp_path("write_raw_ea_deflate");
5771        let data: Vec<i32> = (0..20).collect(); // 5 x 4 row-major
5772        {
5773            let file = H5File::create(&path).unwrap();
5774            let ds = file
5775                .new_dataset::<i32>()
5776                .shape([5, 4])
5777                .chunk(&[2, 4])
5778                .max_shape(&[None, Some(4)]) // unlimited dim 0 -> extensible array
5779                .deflate(5)
5780                .create("stream")
5781                .unwrap();
5782            ds.write_raw(&data).unwrap();
5783            file.close().unwrap();
5784        }
5785        {
5786            let file = H5File::open(&path).unwrap();
5787            let ds = file.dataset("stream").unwrap();
5788            assert_eq!(ds.shape(), vec![5, 4]);
5789            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
5790        }
5791        std::fs::remove_file(&path).ok();
5792    }
5793
5794    // 3D chunked Full read with partial-edge chunks in every dimension. This
5795    // drives copy_chunk_to_output's multi-dim run-memcpy path with two outer
5796    // dimensions, exercising the nested outer-coordinate carry and the
5797    // last-axis edge clamp (chunks hang off the high edge in all three axes).
5798    #[cfg(feature = "deflate")]
5799    #[test]
5800    fn read_full_3d_chunked_edge_roundtrips() {
5801        let path = temp_path("full_3d_chunked_edge");
5802        // shape 5x4x3, chunk 2x3x2 -> ceil gives 3x2x2 chunks; the last chunk
5803        // along each axis is partial (1, 1, and 1 element respectively).
5804        let total: usize = 5 * 4 * 3;
5805        let data: Vec<i32> = (0..total as i32).collect();
5806        {
5807            let file = H5File::create(&path).unwrap();
5808            let ds = file
5809                .new_dataset::<i32>()
5810                .shape([5, 4, 3])
5811                .chunk(&[2, 3, 2])
5812                .deflate(4)
5813                .create("vol")
5814                .unwrap();
5815            ds.write_raw(&data).unwrap();
5816            file.close().unwrap();
5817        }
5818        {
5819            let file = H5File::open(&path).unwrap();
5820            let ds = file.dataset("vol").unwrap();
5821            assert_eq!(ds.shape(), vec![5, 4, 3]);
5822            assert_eq!(ds.chunk_dims(), Some(vec![2, 3, 2]));
5823            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
5824        }
5825        std::fs::remove_file(&path).ok();
5826    }
5827
5828    #[test]
5829    fn roundtrip_u8_1d() {
5830        let path = temp_path("rt_u8_1d");
5831        let data: Vec<u8> = (0..10).collect();
5832
5833        {
5834            let file = H5File::create(&path).unwrap();
5835            let ds = file.new_dataset::<u8>().shape([10]).create("seq").unwrap();
5836            ds.write_raw(&data).unwrap();
5837            file.close().unwrap();
5838        }
5839
5840        {
5841            let file = H5File::open(&path).unwrap();
5842            let ds = file.dataset("seq").unwrap();
5843            assert_eq!(ds.shape(), vec![10]);
5844            let readback = ds.read_raw::<u8>().unwrap();
5845            assert_eq!(readback, data);
5846        }
5847
5848        std::fs::remove_file(&path).ok();
5849    }
5850
5851    #[test]
5852    fn roundtrip_i32_2d() {
5853        let path = temp_path("rt_i32_2d");
5854        let data: Vec<i32> = vec![-1, 0, 1, 2, 3, 4];
5855
5856        {
5857            let file = H5File::create(&path).unwrap();
5858            let ds = file
5859                .new_dataset::<i32>()
5860                .shape([2, 3])
5861                .create("matrix")
5862                .unwrap();
5863            ds.write_raw(&data).unwrap();
5864            file.close().unwrap();
5865        }
5866
5867        {
5868            let file = H5File::open(&path).unwrap();
5869            let ds = file.dataset("matrix").unwrap();
5870            assert_eq!(ds.shape(), vec![2, 3]);
5871            let readback = ds.read_raw::<i32>().unwrap();
5872            assert_eq!(readback, data);
5873        }
5874
5875        std::fs::remove_file(&path).ok();
5876    }
5877
5878    #[test]
5879    fn roundtrip_f64_3d() {
5880        let path = temp_path("rt_f64_3d");
5881        let data: Vec<f64> = (0..24).map(|i| i as f64 * 0.5).collect();
5882
5883        {
5884            let file = H5File::create(&path).unwrap();
5885            let ds = file
5886                .new_dataset::<f64>()
5887                .shape([2, 3, 4])
5888                .create("cube")
5889                .unwrap();
5890            ds.write_raw(&data).unwrap();
5891            file.close().unwrap();
5892        }
5893
5894        {
5895            let file = H5File::open(&path).unwrap();
5896            let ds = file.dataset("cube").unwrap();
5897            assert_eq!(ds.shape(), vec![2, 3, 4]);
5898            let readback = ds.read_raw::<f64>().unwrap();
5899            assert_eq!(readback, data);
5900        }
5901
5902        std::fs::remove_file(&path).ok();
5903    }
5904
5905    #[test]
5906    fn cannot_read_in_write_mode() {
5907        let path = temp_path("no_read_write");
5908        let file = H5File::create(&path).unwrap();
5909        let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
5910        ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
5911        let result = ds.read_raw::<u8>();
5912        assert!(result.is_err());
5913        std::fs::remove_file(&path).ok();
5914    }
5915
5916    #[test]
5917    fn cannot_write_in_read_mode() {
5918        let path = temp_path("no_write_read");
5919
5920        {
5921            let file = H5File::create(&path).unwrap();
5922            let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
5923            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
5924            file.close().unwrap();
5925        }
5926
5927        {
5928            let file = H5File::open(&path).unwrap();
5929            let ds = file.dataset("x").unwrap();
5930            let result = ds.write_raw(&[5u8, 6, 7, 8]);
5931            assert!(result.is_err());
5932        }
5933
5934        std::fs::remove_file(&path).ok();
5935    }
5936
5937    #[test]
5938    fn numeric_attr_roundtrip() {
5939        let path = temp_path("num_attr");
5940        {
5941            let file = H5File::create(&path).unwrap();
5942            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
5943            ds.write_raw(&[1.0f32; 4]).unwrap();
5944
5945            let a1 = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
5946            a1.write_numeric(&1.2345f64).unwrap();
5947
5948            let a2 = ds.new_attr::<i32>().shape(()).create("count").unwrap();
5949            a2.write_numeric(&42i32).unwrap();
5950
5951            file.close().unwrap();
5952        }
5953        {
5954            let file = H5File::open(&path).unwrap();
5955            let ds = file.dataset("data").unwrap();
5956
5957            let scale = ds.attr("scale").unwrap();
5958            let val: f64 = scale.read_numeric().unwrap();
5959            assert!((val - 1.2345).abs() < 1e-10);
5960
5961            let count = ds.attr("count").unwrap();
5962            let val: i32 = count.read_numeric().unwrap();
5963            assert_eq!(val, 42);
5964        }
5965        std::fs::remove_file(&path).ok();
5966    }
5967
5968    #[test]
5969    fn array_attr_roundtrip() {
5970        let path = temp_path("array_attr");
5971        let offsets = [10i32, -20, 30];
5972        {
5973            let file = H5File::create(&path).unwrap();
5974            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
5975            ds.write_raw(&[1.0f32; 4]).unwrap();
5976
5977            // 1-D int32 array attribute (NDArrayDimOffset-style).
5978            let a = ds
5979                .new_attr::<i32>()
5980                .shape([3])
5981                .create("dim_offset")
5982                .unwrap();
5983            a.write_array(&offsets).unwrap();
5984
5985            // Wrong element count is rejected.
5986            let bad = ds.new_attr::<i32>().shape([3]).create("bad").unwrap();
5987            assert!(bad.write_array(&[1i32, 2]).is_err());
5988
5989            file.close().unwrap();
5990        }
5991        {
5992            let file = H5File::open(&path).unwrap();
5993            let ds = file.dataset("data").unwrap();
5994            let a = ds.attr("dim_offset").unwrap();
5995            let raw = a.read_raw().unwrap();
5996            assert_eq!(raw.len(), 3 * 4);
5997            let got: Vec<i32> = raw
5998                .as_chunks::<4>()
5999                .0
6000                .iter()
6001                .map(|b| i32::from_le_bytes(*b))
6002                .collect();
6003            assert_eq!(got, offsets);
6004        }
6005        std::fs::remove_file(&path).ok();
6006    }
6007
6008    #[test]
6009    fn attr_datatype_exposes_class_and_sign() {
6010        // H5Attribute::datatype() must report the stored datatype class and
6011        // signedness so a generic attr->metadata mapper need not infer it from
6012        // the byte width (the HDF5-L1 adapter blocker this accessor unblocks).
6013        use crate::format::messages::datatype::DatatypeMessage;
6014
6015        let path = temp_path("attr_datatype");
6016        {
6017            let file = H5File::create(&path).unwrap();
6018            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
6019            ds.new_attr::<f64>()
6020                .shape(())
6021                .create("scale")
6022                .unwrap()
6023                .write_numeric(&1.5f64)
6024                .unwrap();
6025            ds.new_attr::<i32>()
6026                .shape(())
6027                .create("count")
6028                .unwrap()
6029                .write_numeric(&7i32)
6030                .unwrap();
6031            file.close().unwrap();
6032        }
6033        {
6034            let file = H5File::open(&path).unwrap();
6035            let ds = file.dataset("data").unwrap();
6036
6037            match ds.attr("scale").unwrap().datatype().unwrap() {
6038                DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 8),
6039                other => panic!("expected FloatingPoint for f64 attr, got {other:?}"),
6040            }
6041
6042            match ds.attr("count").unwrap().datatype().unwrap() {
6043                DatatypeMessage::FixedPoint { size, signed, .. } => {
6044                    assert_eq!(size, 4);
6045                    assert!(signed, "i32 attr must be signed");
6046                }
6047                other => panic!("expected FixedPoint for i32 attr, got {other:?}"),
6048            }
6049        }
6050        std::fs::remove_file(&path).ok();
6051    }
6052
6053    #[test]
6054    fn attr_datatype_in_write_mode_errors() {
6055        let path = temp_path("attr_datatype_write_mode");
6056        let file = H5File::create(&path).unwrap();
6057        let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
6058        let attr = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
6059        assert!(attr.datatype().is_err());
6060        std::fs::remove_file(&path).ok();
6061    }
6062
6063    #[test]
6064    fn cannot_create_dataset_in_read_mode() {
6065        let path = temp_path("no_create_read");
6066
6067        {
6068            let _file = H5File::create(&path).unwrap();
6069        }
6070
6071        {
6072            let file = H5File::open(&path).unwrap();
6073            let result = file.new_dataset::<u8>().shape([4]).create("x");
6074            assert!(result.is_err());
6075        }
6076
6077        std::fs::remove_file(&path).ok();
6078    }
6079
6080    #[test]
6081    fn shape_accessor() {
6082        let path = temp_path("shape_acc");
6083
6084        let file = H5File::create(&path).unwrap();
6085        let ds = file
6086            .new_dataset::<f32>()
6087            .shape([5, 10, 3])
6088            .create("tensor")
6089            .unwrap();
6090        assert_eq!(ds.shape(), vec![5, 10, 3]);
6091
6092        std::fs::remove_file(&path).ok();
6093    }
6094
6095    #[test]
6096    fn slice_roundtrip_2d() {
6097        let path = temp_path("slice_2d");
6098
6099        // Create a 4x5 dataset, write full, then read a slice
6100        let data: Vec<i32> = (0..20).collect();
6101        {
6102            let file = H5File::create(&path).unwrap();
6103            let ds = file
6104                .new_dataset::<i32>()
6105                .shape([4, 5])
6106                .create("mat")
6107                .unwrap();
6108            ds.write_raw(&data).unwrap();
6109            file.close().unwrap();
6110        }
6111        {
6112            let file = H5File::open(&path).unwrap();
6113            let ds = file.dataset("mat").unwrap();
6114            // Read rows 1..3, cols 2..4 (2x2 slice)
6115            let slice = ds.read_slice::<i32>(&[1, 2], &[2, 2]).unwrap();
6116            // Row 1: [5,6,7,8,9] -> cols 2..4 = [7,8]
6117            // Row 2: [10,11,12,13,14] -> cols 2..4 = [12,13]
6118            assert_eq!(slice, vec![7, 8, 12, 13]);
6119        }
6120
6121        std::fs::remove_file(&path).ok();
6122    }
6123
6124    // H2D zero-alloc reads. `read_raw_into` / `read_slice_into` fill a
6125    // caller-provided buffer and MUST produce byte-for-byte the same data as
6126    // their Vec-returning counterparts (`read_raw` / `read_slice`) on every
6127    // creatable layout, since both now share one buffer-filling core.
6128    fn assert_into_matches<T>(ds: &super::H5Dataset, starts: &[usize], counts: &[usize])
6129    where
6130        T: crate::types::H5Type + Copy + std::fmt::Debug + PartialEq + Default,
6131    {
6132        let n: usize = ds.shape().iter().product();
6133        let want_full = ds.read_raw::<T>().unwrap();
6134        let mut got_full = vec![T::default(); n];
6135        ds.read_raw_into::<T>(&mut got_full).unwrap();
6136        assert_eq!(got_full, want_full, "read_raw_into != read_raw");
6137
6138        let want_slice = ds.read_slice::<T>(starts, counts).unwrap();
6139        let sn: usize = counts.iter().product();
6140        let mut got_slice = vec![T::default(); sn];
6141        ds.read_slice_into::<T>(&mut got_slice, starts, counts)
6142            .unwrap();
6143        assert_eq!(got_slice, want_slice, "read_slice_into != read_slice");
6144    }
6145
6146    #[test]
6147    fn read_into_matches_vec_contiguous() {
6148        let path = temp_path("into_contig");
6149        let data: Vec<i32> = (0..20).collect(); // 4 x 5 contiguous
6150        {
6151            let file = H5File::create(&path).unwrap();
6152            let ds = file
6153                .new_dataset::<i32>()
6154                .shape([4, 5])
6155                .create("mat")
6156                .unwrap();
6157            ds.write_raw(&data).unwrap();
6158            file.close().unwrap();
6159        }
6160        {
6161            let file = H5File::open(&path).unwrap();
6162            let ds = file.dataset("mat").unwrap();
6163            assert_eq!(ds.chunk_dims(), None);
6164            assert_into_matches::<i32>(&ds, &[1, 2], &[2, 2]);
6165        }
6166        std::fs::remove_file(&path).ok();
6167    }
6168
6169    #[test]
6170    fn read_into_matches_vec_chunked_unfiltered() {
6171        let path = temp_path("into_chunk");
6172        let data: Vec<f64> = (0..35).map(|i| i as f64 * 1.5).collect(); // 7 x 5
6173        {
6174            let file = H5File::create(&path).unwrap();
6175            let ds = file
6176                .new_dataset::<f64>()
6177                .shape([7, 5])
6178                .chunk(&[3, 2]) // multi-chunk grid with edge chunks
6179                .create("grid")
6180                .unwrap();
6181            ds.write_raw(&data).unwrap();
6182            file.close().unwrap();
6183        }
6184        {
6185            let file = H5File::open(&path).unwrap();
6186            let ds = file.dataset("grid").unwrap();
6187            assert_eq!(ds.chunk_dims(), Some(vec![3, 2]));
6188            // Slice spans multiple chunks (rows 2..5, cols 1..4).
6189            assert_into_matches::<f64>(&ds, &[2, 1], &[3, 3]);
6190        }
6191        std::fs::remove_file(&path).ok();
6192    }
6193
6194    #[test]
6195    fn read_into_matches_vec_single_chunk() {
6196        let path = temp_path("into_single_chunk");
6197        let data: Vec<i32> = (0..12).collect(); // 3 x 4, one chunk covers all
6198        {
6199            let file = H5File::create(&path).unwrap();
6200            let ds = file
6201                .new_dataset::<i32>()
6202                .shape([3, 4])
6203                .chunk(&[3, 4]) // chunk == shape -> SingleChunk index
6204                .create("g")
6205                .unwrap();
6206            ds.write_raw(&data).unwrap();
6207            file.close().unwrap();
6208        }
6209        {
6210            let file = H5File::open(&path).unwrap();
6211            let ds = file.dataset("g").unwrap();
6212            assert_eq!(ds.chunk_dims(), Some(vec![3, 4]));
6213            assert_into_matches::<i32>(&ds, &[1, 1], &[2, 2]);
6214        }
6215        std::fs::remove_file(&path).ok();
6216    }
6217
6218    #[cfg(feature = "deflate")]
6219    #[test]
6220    fn read_into_matches_vec_chunked_deflate() {
6221        let path = temp_path("into_chunk_deflate");
6222        let data: Vec<i32> = (0..35).collect(); // 7 x 5
6223        {
6224            let file = H5File::create(&path).unwrap();
6225            let ds = file
6226                .new_dataset::<i32>()
6227                .shape([7, 5])
6228                .chunk(&[3, 2])
6229                .deflate(4)
6230                .create("grid")
6231                .unwrap();
6232            ds.write_raw(&data).unwrap();
6233            file.close().unwrap();
6234        }
6235        {
6236            let file = H5File::open(&path).unwrap();
6237            let ds = file.dataset("grid").unwrap();
6238            assert_eq!(ds.chunk_dims(), Some(vec![3, 2]));
6239            assert_into_matches::<i32>(&ds, &[2, 1], &[3, 3]);
6240        }
6241        std::fs::remove_file(&path).ok();
6242    }
6243
6244    #[test]
6245    fn read_into_wrong_buffer_size_rejected() {
6246        let path = temp_path("into_badlen");
6247        let data: Vec<i32> = (0..20).collect(); // 4 x 5
6248        {
6249            let file = H5File::create(&path).unwrap();
6250            let ds = file
6251                .new_dataset::<i32>()
6252                .shape([4, 5])
6253                .create("mat")
6254                .unwrap();
6255            ds.write_raw(&data).unwrap();
6256            file.close().unwrap();
6257        }
6258        {
6259            let file = H5File::open(&path).unwrap();
6260            let ds = file.dataset("mat").unwrap();
6261
6262            // Too small / too large full-read buffers are both rejected.
6263            let mut small = vec![0i32; 19];
6264            assert!(ds.read_raw_into::<i32>(&mut small).is_err());
6265            let mut large = vec![0i32; 21];
6266            assert!(ds.read_raw_into::<i32>(&mut large).is_err());
6267
6268            // Slice buffer must be exactly product(counts) = 4.
6269            let mut bad_slice = vec![0i32; 3];
6270            assert!(ds
6271                .read_slice_into::<i32>(&mut bad_slice, &[1, 2], &[2, 2])
6272                .is_err());
6273            // The correctly sized slice buffer succeeds.
6274            let mut ok_slice = vec![0i32; 4];
6275            assert!(ds
6276                .read_slice_into::<i32>(&mut ok_slice, &[1, 2], &[2, 2])
6277                .is_ok());
6278        }
6279        std::fs::remove_file(&path).ok();
6280    }
6281
6282    #[test]
6283    fn read_into_wrong_element_size_rejected() {
6284        let path = temp_path("into_badtype");
6285        let data: Vec<i32> = (0..20).collect(); // element size 4
6286        {
6287            let file = H5File::create(&path).unwrap();
6288            let ds = file
6289                .new_dataset::<i32>()
6290                .shape([4, 5])
6291                .create("mat")
6292                .unwrap();
6293            ds.write_raw(&data).unwrap();
6294            file.close().unwrap();
6295        }
6296        {
6297            let file = H5File::open(&path).unwrap();
6298            let ds = file.dataset("mat").unwrap();
6299            // u8 (size 1) and i64 (size 8) mismatch the dataset's 4-byte
6300            // element size -> TypeMismatch, even with a "correctly sized" Vec.
6301            let mut as_u8 = vec![0u8; 20];
6302            assert!(matches!(
6303                ds.read_raw_into::<u8>(&mut as_u8),
6304                Err(crate::Hdf5Error::TypeMismatch(_))
6305            ));
6306            let mut as_i64 = vec![0i64; 20];
6307            assert!(matches!(
6308                ds.read_slice_into::<i64>(&mut as_i64, &[0, 0], &[4, 5]),
6309                Err(crate::Hdf5Error::TypeMismatch(_))
6310            ));
6311        }
6312        std::fs::remove_file(&path).ok();
6313    }
6314
6315    #[test]
6316    fn write_slice_2d() {
6317        let path = temp_path("write_slice_2d");
6318
6319        {
6320            let file = H5File::create(&path).unwrap();
6321            let ds = file
6322                .new_dataset::<f32>()
6323                .shape([3, 4])
6324                .create("data")
6325                .unwrap();
6326            ds.write_raw(&[0.0f32; 12]).unwrap();
6327            // Overwrite a 2x2 sub-region
6328            ds.write_slice(&[1, 1], &[2, 2], &[10.0f32, 20.0, 30.0, 40.0])
6329                .unwrap();
6330            file.close().unwrap();
6331        }
6332        {
6333            let file = H5File::open(&path).unwrap();
6334            let ds = file.dataset("data").unwrap();
6335            let full = ds.read_raw::<f32>().unwrap();
6336            // Row 0: [0,0,0,0]
6337            // Row 1: [0,10,20,0]
6338            // Row 2: [0,30,40,0]
6339            assert_eq!(
6340                full,
6341                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,]
6342            );
6343        }
6344
6345        std::fs::remove_file(&path).ok();
6346    }
6347
6348    /// One 2x4 i32 chunk whose every element is `v`.
6349    fn chunk_of(v: i32) -> Vec<u8> {
6350        (0..8).flat_map(|_| v.to_le_bytes()).collect()
6351    }
6352
6353    /// Write chunk (0,0) `rewrites` times — each time with a different value,
6354    /// so no write can be skipped — and return the closed file's size along
6355    /// with what the chunk reads back as.
6356    fn rewrite_chunk(
6357        tag: &str,
6358        rewrites: i32,
6359        build: impl Fn(&H5File) -> crate::H5Dataset,
6360    ) -> (u64, i32) {
6361        let path = temp_path(tag);
6362        {
6363            let file = H5File::create(&path).unwrap();
6364            let ds = build(&file);
6365            for v in 1..=rewrites {
6366                ds.write_chunk_at(&[0, 0], &chunk_of(v)).unwrap();
6367            }
6368            file.close().unwrap();
6369        }
6370        let size = std::fs::metadata(&path).unwrap().len();
6371        let first = {
6372            let file = H5File::open(&path).unwrap();
6373            file.dataset("d").unwrap().read_raw::<i32>().unwrap()[0]
6374        };
6375        std::fs::remove_file(&path).ok();
6376        (size, first)
6377    }
6378
6379    // An unfiltered chunk's stored size is fixed by the chunk shape, so
6380    // rewriting it must overwrite the block it already occupies rather than
6381    // abandoning it and appending a new one (libhdf5 H5D__chunk_flush_entry
6382    // leaves must_alloc false for exactly this case). The file must therefore
6383    // be byte-identical in size no matter how many times the chunk is written.
6384    #[test]
6385    fn rewriting_an_unfiltered_extensible_array_chunk_stays_in_place() {
6386        let build = |f: &H5File| {
6387            f.new_dataset::<i32>()
6388                .shape([2, 4])
6389                .chunk(&[2, 4])
6390                .max_shape(&[None, Some(4)])
6391                .create("d")
6392                .unwrap()
6393        };
6394        let (once, _) = rewrite_chunk("rewrite_ea_1", 1, build);
6395        let (many, last) = rewrite_chunk("rewrite_ea_8", 8, build);
6396        assert_eq!(many, once, "8 rewrites grew the file past a single write");
6397        assert_eq!(last, 8, "the last write must be the one that survives");
6398    }
6399
6400    #[test]
6401    fn rewriting_an_unfiltered_fixed_array_chunk_stays_in_place() {
6402        let build = |f: &H5File| {
6403            f.new_dataset::<i32>()
6404                .shape([2, 4])
6405                .chunk(&[2, 4])
6406                .create("d")
6407                .unwrap()
6408        };
6409        let (once, _) = rewrite_chunk("rewrite_fa_1", 1, build);
6410        let (many, last) = rewrite_chunk("rewrite_fa_8", 8, build);
6411        assert_eq!(many, once, "8 rewrites grew the file past a single write");
6412        assert_eq!(last, 8);
6413    }
6414
6415    #[test]
6416    fn rewriting_an_unfiltered_btree_v2_chunk_stays_in_place() {
6417        let build = |f: &H5File| {
6418            f.new_dataset::<i32>()
6419                .shape([2, 4])
6420                .chunk(&[2, 4])
6421                .max_shape(&[None, None])
6422                .create("d")
6423                .unwrap()
6424        };
6425        let (once, _) = rewrite_chunk("rewrite_bt2_1", 1, build);
6426        let (many, last) = rewrite_chunk("rewrite_bt2_8", 8, build);
6427        assert_eq!(many, once, "8 rewrites grew the file past a single write");
6428        assert_eq!(last, 8);
6429    }
6430
6431    // A flush re-serializes the whole v2 B-tree over the dataset's node-block
6432    // pool. Every node is the same size, so the blocks already on disk are
6433    // reused and repeated flushes cost nothing; sizing the root to its record
6434    // count instead would relocate it each time and orphan the block it left.
6435    #[test]
6436    fn repeated_flushes_do_not_grow_a_btree_v2_index() {
6437        let flush_n = |label: &str, flushes: usize| -> u64 {
6438            let path = temp_path(label);
6439            {
6440                let file = H5File::create(&path).unwrap();
6441                let ds = file
6442                    .new_dataset::<i32>()
6443                    .shape([2, 4])
6444                    .chunk(&[2, 4])
6445                    .max_shape(&[None, None])
6446                    .create("d")
6447                    .unwrap();
6448                let bytes: Vec<u8> = (0..8i32).flat_map(|v| v.to_le_bytes()).collect();
6449                ds.write_chunk_at(&[0, 0], &bytes).unwrap();
6450                for _ in 0..flushes {
6451                    ds.flush().unwrap();
6452                }
6453                file.close().unwrap();
6454            }
6455            let size = std::fs::metadata(&path).unwrap().len();
6456            // The data must survive every rewrite of the index.
6457            {
6458                let file = H5File::open(&path).unwrap();
6459                let ds = file.dataset("d").unwrap();
6460                assert_eq!(ds.read_raw::<i32>().unwrap(), (0..8).collect::<Vec<i32>>());
6461            }
6462            std::fs::remove_file(&path).ok();
6463            size
6464        };
6465        assert_eq!(
6466            flush_n("bt2_flush_8", 8),
6467            flush_n("bt2_flush_1", 1),
6468            "8 index flushes grew the file past a single one"
6469        );
6470    }
6471
6472    // A filtered chunk whose compressed size changes cannot stay put, so it
6473    // moves and releases its old block (libhdf5 H5D__chunk_file_alloc calls
6474    // H5MF_xfree). Alternating between two payloads of different compressed
6475    // size must therefore keep reusing the same two blocks instead of
6476    // appending a fresh one each time.
6477    #[cfg(feature = "deflate")]
6478    #[test]
6479    fn rewriting_a_filtered_chunk_recycles_the_released_block() {
6480        // All-equal elements deflate to far fewer bytes than a varied payload,
6481        // so the two writes below land at different stored sizes.
6482        let flat: Vec<u8> = (0..8).flat_map(|_| 7i32.to_le_bytes()).collect();
6483        let varied: Vec<u8> = (0..8i32)
6484            .flat_map(|i| i.wrapping_mul(0x5bd1_e995).to_le_bytes())
6485            .collect();
6486
6487        let sizes: Vec<u64> = [1usize, 8]
6488            .iter()
6489            .map(|&rounds| {
6490                let path = temp_path(&format!("rewrite_filtered_{rounds}"));
6491                {
6492                    let file = H5File::create(&path).unwrap();
6493                    let ds = file
6494                        .new_dataset::<i32>()
6495                        .shape([2, 4])
6496                        .chunk(&[2, 4])
6497                        .max_shape(&[None, Some(4)])
6498                        .deflate(6)
6499                        .create("d")
6500                        .unwrap();
6501                    for _ in 0..rounds {
6502                        ds.write_chunk_at(&[0, 0], &flat).unwrap();
6503                        ds.write_chunk_at(&[0, 0], &varied).unwrap();
6504                    }
6505                    file.close().unwrap();
6506                }
6507                let size = std::fs::metadata(&path).unwrap().len();
6508                {
6509                    let file = H5File::open(&path).unwrap();
6510                    let got = file.dataset("d").unwrap().read_raw::<i32>().unwrap();
6511                    let want: Vec<i32> = (0..8i32).map(|i| i.wrapping_mul(0x5bd1_e995)).collect();
6512                    assert_eq!(got, want, "the last write must survive the round trip");
6513                }
6514                std::fs::remove_file(&path).ok();
6515                size
6516            })
6517            .collect();
6518
6519        assert_eq!(
6520            sizes[1], sizes[0],
6521            "8 alternating rewrites grew the file past a single pair"
6522        );
6523    }
6524
6525    #[test]
6526    fn write_slice_out_of_bounds_rejected() {
6527        let path = temp_path("write_slice_oob");
6528        let file = H5File::create(&path).unwrap();
6529        let ds = file.new_dataset::<i32>().shape([4]).create("d").unwrap();
6530        ds.write_raw(&[0i32; 4]).unwrap();
6531        // start 2 + count 6 = 8 > extent 4 -> must error, not corrupt.
6532        assert!(ds.write_slice(&[2], &[6], &[9i32; 6]).is_err());
6533        // An in-bounds slice still works.
6534        assert!(ds.write_slice(&[1], &[2], &[7i32, 8]).is_ok());
6535        std::fs::remove_file(&path).ok();
6536    }
6537
6538    #[test]
6539    fn duplicate_dataset_name_rejected() {
6540        let path = temp_path("dup_name");
6541        let file = H5File::create(&path).unwrap();
6542        let _ = file.new_dataset::<i32>().shape([2]).create("d").unwrap();
6543        assert!(file.new_dataset::<i32>().shape([2]).create("d").is_err());
6544        std::fs::remove_file(&path).ok();
6545    }
6546
6547    #[test]
6548    fn extend_cannot_shrink() {
6549        let path = temp_path("extend_shrink");
6550        let file = H5File::create(&path).unwrap();
6551        let ds = file
6552            .new_dataset::<i32>()
6553            .shape([0])
6554            .chunk(&[2])
6555            .max_shape(&[None])
6556            .create("d")
6557            .unwrap();
6558        ds.append(&[1i32, 2, 3, 4]).unwrap();
6559        // Shrinking below the written extent must be rejected.
6560        assert!(ds.extend(&[2]).is_err());
6561        // Growing is fine.
6562        assert!(ds.extend(&[6]).is_ok());
6563        std::fs::remove_file(&path).ok();
6564    }
6565
6566    #[test]
6567    fn attr_read_roundtrip() {
6568        use crate::types::VarLenUnicode;
6569        let path = temp_path("attr_read");
6570
6571        {
6572            let file = H5File::create(&path).unwrap();
6573            let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
6574            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
6575            let a1 = ds
6576                .new_attr::<VarLenUnicode>()
6577                .shape(())
6578                .create("units")
6579                .unwrap();
6580            a1.write_string("meters").unwrap();
6581            let a2 = ds
6582                .new_attr::<VarLenUnicode>()
6583                .shape(())
6584                .create("desc")
6585                .unwrap();
6586            a2.write_string("test data").unwrap();
6587            file.close().unwrap();
6588        }
6589        {
6590            let file = H5File::open(&path).unwrap();
6591            let ds = file.dataset("data").unwrap();
6592
6593            let names = ds.attr_names().unwrap();
6594            assert!(names.contains(&"units".to_string()));
6595            assert!(names.contains(&"desc".to_string()));
6596
6597            let units = ds.attr("units").unwrap();
6598            assert_eq!(units.read_string().unwrap(), "meters");
6599
6600            let desc = ds.attr("desc").unwrap();
6601            assert_eq!(desc.read_string().unwrap(), "test data");
6602        }
6603
6604        std::fs::remove_file(&path).ok();
6605    }
6606
6607    #[test]
6608    fn type_mismatch_element_size() {
6609        let path = temp_path("type_mismatch");
6610
6611        {
6612            let file = H5File::create(&path).unwrap();
6613            let ds = file.new_dataset::<f64>().shape([4]).create("data").unwrap();
6614            ds.write_raw(&[1.0f64, 2.0, 3.0, 4.0]).unwrap();
6615            file.close().unwrap();
6616        }
6617
6618        {
6619            let file = H5File::open(&path).unwrap();
6620            let ds = file.dataset("data").unwrap();
6621            // Try to read as u8 (element_size = 1) from a f64 dataset (element_size = 8)
6622            let result = ds.read_raw::<u8>();
6623            assert!(result.is_err());
6624        }
6625
6626        std::fs::remove_file(&path).ok();
6627    }
6628
6629    #[test]
6630    fn dataset_survives_file_move() {
6631        let path = temp_path("ds_survives");
6632
6633        let ds = {
6634            let file = H5File::create(&path).unwrap();
6635            file.new_dataset::<u8>().shape([4]).create("x").unwrap()
6636        };
6637        // file is dropped here, but ds still holds Rc to the inner state
6638        ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
6639        // The writer will finalize on drop of the last Rc
6640
6641        std::fs::remove_file(&path).ok();
6642    }
6643
6644    #[test]
6645    fn new_attr_scalar_string() {
6646        use crate::types::VarLenUnicode;
6647
6648        let path = temp_path("attr_scalar_string");
6649        {
6650            let file = H5File::create(&path).unwrap();
6651            let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
6652            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
6653
6654            let attr = ds
6655                .new_attr::<VarLenUnicode>()
6656                .shape(())
6657                .create("name")
6658                .unwrap();
6659            attr.write_scalar(&VarLenUnicode("test_value".to_string()))
6660                .unwrap();
6661
6662            file.close().unwrap();
6663        }
6664
6665        // Verify the file is still valid and readable
6666        {
6667            use crate::format::messages::datatype::DatatypeMessage;
6668            let file = H5File::open(&path).unwrap();
6669            let ds = file.dataset("data").unwrap();
6670            assert_eq!(ds.shape(), vec![4]);
6671            let readback = ds.read_raw::<u8>().unwrap();
6672            assert_eq!(readback, vec![1u8, 2, 3, 4]);
6673
6674            // The string attribute is stored as a true variable-length string
6675            // (not fixed-length) and round-trips its value.
6676            let attr = ds.attr("name").unwrap();
6677            assert!(
6678                matches!(
6679                    attr.datatype().unwrap(),
6680                    DatatypeMessage::VarLenString { .. }
6681                ),
6682                "string attribute should have a variable-length string datatype"
6683            );
6684            assert_eq!(attr.read_string().unwrap(), "test_value");
6685        }
6686
6687        std::fs::remove_file(&path).ok();
6688    }
6689
6690    #[test]
6691    fn all_numeric_types_roundtrip() {
6692        let path = temp_path("all_types");
6693
6694        {
6695            let file = H5File::create(&path).unwrap();
6696
6697            let ds = file.new_dataset::<u8>().shape([2]).create("u8").unwrap();
6698            ds.write_raw(&[1u8, 2]).unwrap();
6699
6700            let ds = file.new_dataset::<i8>().shape([2]).create("i8").unwrap();
6701            ds.write_raw(&[-1i8, 1]).unwrap();
6702
6703            let ds = file.new_dataset::<u16>().shape([2]).create("u16").unwrap();
6704            ds.write_raw(&[100u16, 200]).unwrap();
6705
6706            let ds = file.new_dataset::<i16>().shape([2]).create("i16").unwrap();
6707            ds.write_raw(&[-100i16, 100]).unwrap();
6708
6709            let ds = file.new_dataset::<u32>().shape([2]).create("u32").unwrap();
6710            ds.write_raw(&[1000u32, 2000]).unwrap();
6711
6712            let ds = file.new_dataset::<i32>().shape([2]).create("i32").unwrap();
6713            ds.write_raw(&[-1000i32, 1000]).unwrap();
6714
6715            let ds = file.new_dataset::<u64>().shape([2]).create("u64").unwrap();
6716            ds.write_raw(&[10000u64, 20000]).unwrap();
6717
6718            let ds = file.new_dataset::<i64>().shape([2]).create("i64").unwrap();
6719            ds.write_raw(&[-10000i64, 10000]).unwrap();
6720
6721            let ds = file.new_dataset::<f32>().shape([2]).create("f32").unwrap();
6722            ds.write_raw(&[1.5f32, 2.5]).unwrap();
6723
6724            let ds = file.new_dataset::<f64>().shape([2]).create("f64").unwrap();
6725            ds.write_raw(&[1.23456f64, 7.89012]).unwrap();
6726
6727            file.close().unwrap();
6728        }
6729
6730        {
6731            let file = H5File::open(&path).unwrap();
6732
6733            assert_eq!(
6734                file.dataset("u8").unwrap().read_raw::<u8>().unwrap(),
6735                vec![1u8, 2]
6736            );
6737            assert_eq!(
6738                file.dataset("i8").unwrap().read_raw::<i8>().unwrap(),
6739                vec![-1i8, 1]
6740            );
6741            assert_eq!(
6742                file.dataset("u16").unwrap().read_raw::<u16>().unwrap(),
6743                vec![100u16, 200]
6744            );
6745            assert_eq!(
6746                file.dataset("i16").unwrap().read_raw::<i16>().unwrap(),
6747                vec![-100i16, 100]
6748            );
6749            assert_eq!(
6750                file.dataset("u32").unwrap().read_raw::<u32>().unwrap(),
6751                vec![1000u32, 2000]
6752            );
6753            assert_eq!(
6754                file.dataset("i32").unwrap().read_raw::<i32>().unwrap(),
6755                vec![-1000i32, 1000]
6756            );
6757            assert_eq!(
6758                file.dataset("u64").unwrap().read_raw::<u64>().unwrap(),
6759                vec![10000u64, 20000]
6760            );
6761            assert_eq!(
6762                file.dataset("i64").unwrap().read_raw::<i64>().unwrap(),
6763                vec![-10000i64, 10000]
6764            );
6765            assert_eq!(
6766                file.dataset("f32").unwrap().read_raw::<f32>().unwrap(),
6767                vec![1.5f32, 2.5]
6768            );
6769            assert_eq!(
6770                file.dataset("f64").unwrap().read_raw::<f64>().unwrap(),
6771                vec![1.23456f64, 7.89012]
6772            );
6773        }
6774
6775        std::fs::remove_file(&path).ok();
6776    }
6777
6778    #[test]
6779    fn append_chunked_roundtrip() {
6780        let path = temp_path("append_chunked");
6781
6782        {
6783            let file = H5File::create(&path).unwrap();
6784            let ds = file
6785                .new_dataset::<f64>()
6786                .shape([0, 3])
6787                .chunk(&[1, 3])
6788                .max_shape(&[None, Some(3)])
6789                .create("data")
6790                .unwrap();
6791
6792            // Append one frame
6793            ds.append(&[1.0f64, 2.0, 3.0]).unwrap();
6794            // Append two frames at once
6795            ds.append(&[4.0f64, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap();
6796
6797            file.close().unwrap();
6798        }
6799
6800        {
6801            let file = H5File::open(&path).unwrap();
6802            let ds = file.dataset("data").unwrap();
6803            assert_eq!(ds.shape(), vec![3, 3]);
6804            let all = ds.read_raw::<f64>().unwrap();
6805            assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
6806        }
6807
6808        std::fs::remove_file(&path).ok();
6809    }
6810
6811    #[test]
6812    fn append_1d_chunked() {
6813        let path = temp_path("append_1d");
6814
6815        {
6816            let file = H5File::create(&path).unwrap();
6817            let ds = file
6818                .new_dataset::<i32>()
6819                .shape([0])
6820                .chunk(&[4])
6821                .max_shape(&[None])
6822                .create("values")
6823                .unwrap();
6824
6825            ds.append(&[10i32, 20, 30]).unwrap(); // partial chunk
6826            ds.append(&[40i32]).unwrap(); // fills chunk boundary
6827            ds.append(&[50i32, 60, 70, 80]).unwrap(); // full chunk
6828
6829            file.close().unwrap();
6830        }
6831
6832        {
6833            let file = H5File::open(&path).unwrap();
6834            let ds = file.dataset("values").unwrap();
6835            assert_eq!(ds.shape(), vec![8]);
6836            let all = ds.read_raw::<i32>().unwrap();
6837            assert_eq!(all, vec![10, 20, 30, 40, 50, 60, 70, 80]);
6838        }
6839
6840        std::fs::remove_file(&path).ok();
6841    }
6842
6843    #[test]
6844    fn append_partial_chunk_flushed_on_close() {
6845        let path = temp_path("append_partial_close");
6846
6847        {
6848            let file = H5File::create(&path).unwrap();
6849            let ds = file
6850                .new_dataset::<f64>()
6851                .shape([0])
6852                .chunk(&[4])
6853                .max_shape(&[None])
6854                .create("vals")
6855                .unwrap();
6856
6857            // Append 5 elements: chunk 0 = full [1,2,3,4], chunk 1 = partial [5,0,0,0]
6858            ds.append(&[1.0f64, 2.0, 3.0, 4.0, 5.0]).unwrap();
6859            file.close().unwrap();
6860        }
6861
6862        {
6863            let file = H5File::open(&path).unwrap();
6864            let ds = file.dataset("vals").unwrap();
6865            assert_eq!(ds.shape(), vec![5]);
6866            let all = ds.read_raw::<f64>().unwrap();
6867            // The full dataset is 2 chunks * 4 = 8 elements; shape says 5
6868            // read_raw reads total shape elements
6869            assert_eq!(all.len(), 5);
6870            assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
6871        }
6872
6873        std::fs::remove_file(&path).ok();
6874    }
6875
6876    /// An append that leaves its chunk partial is buffered until close, and
6877    /// the flush has to keep the frames that chunk already holds. It built a
6878    /// fresh fill-value chunk around the buffered frame instead, so reopening
6879    /// a file and appending one row erased every earlier row of that chunk
6880    /// (issue #3). Four sessions: the second lands beside an existing row, the
6881    /// third closes chunk 0 and opens chunk 1, the fourth lands beside the row
6882    /// the third left in chunk 1.
6883    #[test]
6884    fn append_after_reopen_keeps_the_partial_chunk_it_lands_in() {
6885        let path = temp_path("append_reopen_partial");
6886
6887        {
6888            let file = H5File::create(&path).unwrap();
6889            let ds = file
6890                .new_dataset::<i32>()
6891                .shape([0, 3])
6892                .chunk(&[4, 3])
6893                .max_shape(&[None, Some(3)])
6894                .create("values")
6895                .unwrap();
6896            ds.append(&[1, 2, 3]).unwrap();
6897            file.close().unwrap();
6898        }
6899        for rows in [
6900            vec![4, 5, 6],
6901            vec![7, 8, 9, 10, 11, 12, 13, 14, 15],
6902            vec![16, 17, 18],
6903        ] {
6904            let file = H5File::open_rw(&path).unwrap();
6905            file.dataset_writer("values")
6906                .unwrap()
6907                .append(&rows)
6908                .unwrap();
6909            file.close().unwrap();
6910        }
6911
6912        let file = H5File::open(&path).unwrap();
6913        let ds = file.dataset("values").unwrap();
6914        assert_eq!(ds.shape(), vec![6, 3]);
6915        assert_eq!(
6916            ds.read_raw::<i32>().unwrap(),
6917            (1..=18).collect::<Vec<i32>>()
6918        );
6919        std::fs::remove_file(&path).ok();
6920    }
6921
6922    #[cfg(feature = "deflate")]
6923    #[test]
6924    fn vlen_append_after_reopen_filtered() {
6925        // Reopen + append into a partially-written *compressed* vlen chunk
6926        // (index-block chunk). Exercises filtered-index-block reconstruction
6927        // in open_append plus filtered read-modify-write.
6928        let path = temp_path("vlen_reopen_filtered");
6929        {
6930            let file = H5File::create(&path).unwrap();
6931            file.create_appendable_vlen_dataset(
6932                "strs",
6933                4,
6934                Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
6935            )
6936            .unwrap();
6937            file.append_vlen_strings("strs", &["alpha", "beta", "gamma"])
6938                .unwrap();
6939            file.close().unwrap();
6940        }
6941        {
6942            let file = H5File::open_rw(&path).unwrap();
6943            file.append_vlen_strings("strs", &["delta"]).unwrap();
6944            file.close().unwrap();
6945        }
6946        {
6947            let file = H5File::open(&path).unwrap();
6948            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
6949            assert_eq!(
6950                got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
6951                vec!["alpha", "beta", "gamma", "delta"]
6952            );
6953        }
6954        std::fs::remove_file(&path).ok();
6955    }
6956
6957    #[test]
6958    fn vlen_append_after_reopen_data_block() {
6959        // Reopen + append into a partial chunk that lives in an extensible-
6960        // array *data block* (chunk index >= idx_blk_elmts). Exercises
6961        // data-block resolution in read_chunk_if_present and write_chunk.
6962        let path = temp_path("vlen_reopen_datablk");
6963        let labels: Vec<String> = (0..9).map(|i| format!("s{i}")).collect();
6964        {
6965            let file = H5File::create(&path).unwrap();
6966            file.create_appendable_vlen_dataset("strs", 2, None)
6967                .unwrap();
6968            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
6969            file.append_vlen_strings("strs", &refs).unwrap();
6970            file.close().unwrap();
6971        }
6972        {
6973            let file = H5File::open_rw(&path).unwrap();
6974            file.append_vlen_strings("strs", &["s9"]).unwrap();
6975            file.close().unwrap();
6976        }
6977        {
6978            let file = H5File::open(&path).unwrap();
6979            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
6980            let want: Vec<String> = (0..10).map(|i| format!("s{i}")).collect();
6981            assert_eq!(got, want);
6982        }
6983        std::fs::remove_file(&path).ok();
6984    }
6985
6986    #[test]
6987    fn vlen_append_after_reopen_super_block() {
6988        // Reopen + append into a partial chunk whose index falls in an
6989        // extensible-array *super block* (chunk index 244 with the default
6990        // EA geometry: idx_blk_elmts=4, data_blk_min_elmts=16,
6991        // sup_blk_min_data_ptrs=4 -> chunks 0..=243 are reached via the
6992        // index block or its direct data blocks, so chunk 244 is reached
6993        // via a super block read from disk). Exercises the ViaSblk branch
6994        // of read_chunk_if_present.
6995        let path = temp_path("vlen_reopen_super");
6996        // 489 strings, chunk size 2 -> chunk 244 holds one string only
6997        // (partially filled) and is flushed to disk on close.
6998        let labels: Vec<String> = (0..489).map(|i| format!("v{i}")).collect();
6999        {
7000            let file = H5File::create(&path).unwrap();
7001            file.create_appendable_vlen_dataset("strs", 2, None)
7002                .unwrap();
7003            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
7004            file.append_vlen_strings("strs", &refs).unwrap();
7005            file.close().unwrap();
7006        }
7007        {
7008            let file = H5File::open_rw(&path).unwrap();
7009            file.append_vlen_strings("strs", &["v489"]).unwrap();
7010            file.close().unwrap();
7011        }
7012        {
7013            let file = H5File::open(&path).unwrap();
7014            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
7015            let want: Vec<String> = (0..490).map(|i| format!("v{i}")).collect();
7016            assert_eq!(got, want);
7017        }
7018        std::fs::remove_file(&path).ok();
7019    }
7020
7021    #[cfg(feature = "deflate")]
7022    #[test]
7023    fn vlen_append_after_reopen_filtered_data_block() {
7024        // The hardest path: compressed + chunk in a data block + partial
7025        // read-modify-write across a reopen.
7026        let path = temp_path("vlen_reopen_filt_datablk");
7027        let labels: Vec<String> = (0..9).map(|i| format!("item{i:02}")).collect();
7028        {
7029            let file = H5File::create(&path).unwrap();
7030            file.create_appendable_vlen_dataset(
7031                "strs",
7032                2,
7033                Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
7034            )
7035            .unwrap();
7036            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
7037            file.append_vlen_strings("strs", &refs).unwrap();
7038            file.close().unwrap();
7039        }
7040        {
7041            let file = H5File::open_rw(&path).unwrap();
7042            file.append_vlen_strings("strs", &["item09"]).unwrap();
7043            file.close().unwrap();
7044        }
7045        {
7046            let file = H5File::open(&path).unwrap();
7047            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
7048            let want: Vec<String> = (0..10).map(|i| format!("item{i:02}")).collect();
7049            assert_eq!(got, want);
7050        }
7051        std::fs::remove_file(&path).ok();
7052    }
7053
7054    #[test]
7055    fn group_nx_class_attribute_roundtrip() {
7056        // Non-root groups carry attributes (NeXus `NX_class`) in their
7057        // own object header, and the reader reads them back by path.
7058        let path = temp_path("group_nx_class");
7059        {
7060            let file = H5File::create(&path).unwrap();
7061            let entry = file.create_group("entry").unwrap();
7062            entry.set_attr_string("NX_class", "NXentry").unwrap();
7063            let det = entry.create_group("detector").unwrap();
7064            det.set_attr_string("NX_class", "NXdetector").unwrap();
7065            det.set_attr_numeric("frame_count", &7i32).unwrap();
7066            det.new_dataset::<f32>()
7067                .shape([4])
7068                .create("data")
7069                .unwrap()
7070                .write_raw(&[1.0f32; 4])
7071                .unwrap();
7072            file.close().unwrap();
7073        }
7074        {
7075            let file = H5File::open(&path).unwrap();
7076            let entry = file.root_group().group("entry").unwrap();
7077            assert_eq!(entry.attr_string("NX_class").unwrap(), "NXentry");
7078            let det = entry.group("detector").unwrap();
7079            assert_eq!(det.attr_string("NX_class").unwrap(), "NXdetector");
7080            let names = det.attr_names().unwrap();
7081            assert!(names.contains(&"NX_class".to_string()));
7082            assert!(names.contains(&"frame_count".to_string()));
7083        }
7084        std::fs::remove_file(&path).ok();
7085    }
7086
7087    #[test]
7088    fn ea_super_block_roundtrip() {
7089        // 2000 chunks span several extensible-array super blocks. Before
7090        // super-block support the writer errored at chunk index 228.
7091        let path = temp_path("ea_super_rt");
7092        {
7093            let file = H5File::create(&path).unwrap();
7094            let ds = file
7095                .new_dataset::<i32>()
7096                .shape([0])
7097                .chunk(&[1])
7098                .max_shape(&[None])
7099                .create("v")
7100                .unwrap();
7101            ds.append(&(0..2000).collect::<Vec<i32>>()).unwrap();
7102            file.close().unwrap();
7103        }
7104        {
7105            let file = H5File::open(&path).unwrap();
7106            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
7107            assert_eq!(v.len(), 2000);
7108            assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
7109        }
7110        std::fs::remove_file(&path).ok();
7111    }
7112
7113    #[cfg(feature = "deflate")]
7114    #[test]
7115    fn ea_filtered_super_block_roundtrip() {
7116        // Compressed chunks across super blocks.
7117        let path = temp_path("ea_filt_super");
7118        {
7119            let file = H5File::create(&path).unwrap();
7120            let ds = file
7121                .new_dataset::<i32>()
7122                .shape([0])
7123                .chunk(&[1])
7124                .max_shape(&[None])
7125                .deflate(4)
7126                .create("v")
7127                .unwrap();
7128            ds.append(&(0..600).collect::<Vec<i32>>()).unwrap();
7129            file.close().unwrap();
7130        }
7131        {
7132            let file = H5File::open(&path).unwrap();
7133            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
7134            assert_eq!(v, (0..600).collect::<Vec<i32>>());
7135        }
7136        std::fs::remove_file(&path).ok();
7137    }
7138
7139    #[test]
7140    fn ea_super_block_open_append() {
7141        // Reopen a dataset and append chunks that fall in super blocks.
7142        let path = temp_path("ea_super_append");
7143        {
7144            let file = H5File::create(&path).unwrap();
7145            let ds = file
7146                .new_dataset::<i32>()
7147                .shape([0])
7148                .chunk(&[1])
7149                .max_shape(&[None])
7150                .create("v")
7151                .unwrap();
7152            ds.append(&(0..300).collect::<Vec<i32>>()).unwrap();
7153            file.close().unwrap();
7154        }
7155        {
7156            let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
7157            let idx = w.dataset_index("v").unwrap();
7158            for c in 300..900u64 {
7159                w.write_chunk(idx, c, &(c as i32).to_le_bytes()).unwrap();
7160            }
7161            w.extend_dataset(idx, &[900]).unwrap();
7162            w.close().unwrap();
7163        }
7164        {
7165            let file = H5File::open(&path).unwrap();
7166            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
7167            assert_eq!(v.len(), 900);
7168            assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
7169        }
7170        std::fs::remove_file(&path).ok();
7171    }
7172
7173    // Two or more unlimited dimensions select the v2 B-tree index; with a
7174    // filter its records become type 11, carrying each chunk's stored size and
7175    // mask. The payload is highly compressible, so the chunks really are
7176    // stored smaller than the extent — the file would be at least
7177    // 6*8*4 = 192 bytes of raw chunk data otherwise.
7178    #[cfg(feature = "deflate")]
7179    #[test]
7180    fn compressed_multi_unlimited_dataset_roundtrips() {
7181        let path = temp_path("bt2_filtered");
7182        {
7183            let file = H5File::create(&path).unwrap();
7184            let ds = file
7185                .new_dataset::<i32>()
7186                .shape([6, 8])
7187                .chunk(&[2, 4])
7188                .max_shape(&[None, None])
7189                .deflate(6)
7190                .create("d")
7191                .unwrap();
7192            ds.write_slice(&[0, 0], &[6, 8], &[7i32; 48]).unwrap();
7193            // A partial write forces a decompress-patch-recompress of one
7194            // chunk, whose new compressed size may not fit its old block.
7195            ds.write_slice(&[1, 1], &[2, 2], &[1i32, 2, 3, 4]).unwrap();
7196            file.close().unwrap();
7197        }
7198        {
7199            let file = H5File::open(&path).unwrap();
7200            let ds = file.dataset("d").unwrap();
7201            assert_eq!(ds.shape(), vec![6, 8]);
7202            let mut want = vec![7i32; 48];
7203            want[9] = 1;
7204            want[10] = 2;
7205            want[17] = 3;
7206            want[18] = 4;
7207            assert_eq!(ds.read_raw::<i32>().unwrap(), want);
7208        }
7209        std::fs::remove_file(&path).ok();
7210    }
7211
7212    #[test]
7213    fn btree_v2_multi_unlimited_roundtrip() {
7214        // A dataset with two unlimited dimensions uses the v2 B-tree chunk
7215        // index; chunks are written by grid coordinates with write_chunk_at.
7216        let path = temp_path("bt2_multi");
7217        {
7218            let file = H5File::create(&path).unwrap();
7219            let ds = file
7220                .new_dataset::<i32>()
7221                .shape([0, 0])
7222                .chunk(&[2, 2])
7223                .max_shape(&[None, None])
7224                .create("grid")
7225                .unwrap();
7226            assert!(ds.is_chunked());
7227            // 4x4 logical grid, value[r][c] = r*4 + c, in 2x2 chunks.
7228            for cr in 0..2usize {
7229                for cc in 0..2usize {
7230                    let mut bytes = Vec::new();
7231                    for i in 0..2usize {
7232                        for j in 0..2usize {
7233                            let v = ((cr * 2 + i) * 4 + (cc * 2 + j)) as i32;
7234                            bytes.extend_from_slice(&v.to_le_bytes());
7235                        }
7236                    }
7237                    ds.write_chunk_at(&[cr, cc], &bytes).unwrap();
7238                }
7239            }
7240            file.close().unwrap();
7241        }
7242        {
7243            let file = H5File::open(&path).unwrap();
7244            let ds = file.dataset("grid").unwrap();
7245            assert_eq!(ds.shape(), vec![4, 4]);
7246            assert_eq!(ds.read_raw::<i32>().unwrap(), (0..16).collect::<Vec<i32>>());
7247        }
7248        std::fs::remove_file(&path).ok();
7249    }
7250
7251    #[test]
7252    fn subframe_chunking_roundtrip() {
7253        // A chunk smaller than a frame: shape [N,8,8], chunk [1,4,4], so each
7254        // frame is tiled into a 2x2 grid of 4x4 chunks. write_chunk_at takes
7255        // the chunk-grid coordinates.
7256        let path = temp_path("subframe");
7257        {
7258            let file = H5File::create(&path).unwrap();
7259            let ds = file
7260                .new_dataset::<i32>()
7261                .shape([0, 8, 8])
7262                .chunk(&[1, 4, 4])
7263                .max_shape(&[None, Some(8), Some(8)])
7264                .create("v")
7265                .unwrap();
7266            for f in 0..3usize {
7267                for cr in 0..2usize {
7268                    for cc in 0..2usize {
7269                        let mut bytes = Vec::new();
7270                        for i in 0..4usize {
7271                            for j in 0..4usize {
7272                                let v = (f * 64 + (cr * 4 + i) * 8 + (cc * 4 + j)) as i32;
7273                                bytes.extend_from_slice(&v.to_le_bytes());
7274                            }
7275                        }
7276                        ds.write_chunk_at(&[f, cr, cc], &bytes).unwrap();
7277                    }
7278                }
7279            }
7280            file.close().unwrap();
7281        }
7282        {
7283            let file = H5File::open(&path).unwrap();
7284            let ds = file.dataset("v").unwrap();
7285            assert_eq!(ds.shape(), vec![3, 8, 8]);
7286            assert_eq!(
7287                ds.read_raw::<i32>().unwrap(),
7288                (0..192).collect::<Vec<i32>>()
7289            );
7290        }
7291        std::fs::remove_file(&path).ok();
7292    }
7293
7294    #[test]
7295    fn fill_value_contiguous_roundtrip() {
7296        let path = temp_path("fill_value_contig");
7297        {
7298            let file = H5File::create(&path).unwrap();
7299            let ds = file
7300                .new_dataset::<f32>()
7301                .shape([4])
7302                .fill_value(2.5f32)
7303                .create("data")
7304                .unwrap();
7305            ds.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
7306            file.close().unwrap();
7307        }
7308        // open_append decodes the fill-value message back from the header.
7309        {
7310            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
7311            let idx = writer.dataset_index("data").unwrap();
7312            assert_eq!(
7313                writer.ds(idx).lock().fill_value,
7314                Some(2.5f32.to_le_bytes().to_vec())
7315            );
7316        }
7317        // Data still reads back correctly.
7318        {
7319            let file = H5File::open(&path).unwrap();
7320            let ds = file.dataset("data").unwrap();
7321            assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
7322        }
7323        std::fs::remove_file(&path).ok();
7324    }
7325
7326    /// Early allocation on a fixed unfiltered shape selects the implicit
7327    /// index, and "implicit" is literal: the file holds no index structure
7328    /// at all, only a version-4 layout message of index type 2 pointing at
7329    /// the run of chunk space the create allocated.
7330    #[test]
7331    fn early_allocation_writes_the_implicit_index() {
7332        let path = temp_path("implicit_index");
7333        {
7334            let file = H5File::create(&path).unwrap();
7335            let ds = file
7336                .new_dataset::<i32>()
7337                .shape([16])
7338                .chunk(&[4])
7339                .early_allocation()
7340                .create("data")
7341                .unwrap();
7342            ds.write_raw(&(0..16i32).collect::<Vec<_>>()).unwrap();
7343            file.close().unwrap();
7344        }
7345        let bytes = std::fs::read(&path).unwrap();
7346        for magic in [b"EAHD", b"EAIB", b"FAHD", b"FADB", b"BTHD", b"TREE"] {
7347            assert!(
7348                !bytes.windows(4).any(|w| w == magic),
7349                "{} appears in a file whose chunk index is supposed to be no \
7350                 structure at all",
7351                String::from_utf8_lossy(magic)
7352            );
7353        }
7354        {
7355            let file = H5File::open(&path).unwrap();
7356            let ds = file.dataset("data").unwrap();
7357            assert_eq!(
7358                ds.read_raw::<i32>().unwrap(),
7359                (0..16i32).collect::<Vec<_>>()
7360            );
7361        }
7362        std::fs::remove_file(&path).ok();
7363    }
7364
7365    /// Two of the conditions are conditions: an unlimited dimension or a
7366    /// filter each send the dataset to the index libhdf5 would pick
7367    /// instead, early allocation or not. (The third — one whole-dataset
7368    /// chunk — sends it to the single-chunk index instead of Fixed Array;
7369    /// see `one_whole_dataset_chunk_writes_the_single_chunk_index`.)
7370    #[test]
7371    #[cfg(feature = "deflate")]
7372    fn early_allocation_only_picks_implicit_where_libhdf5_does() {
7373        // Every case writes and reads back its data, so a mis-selected index
7374        // shows up as wrong bytes and not just as a different structure.
7375        for (which, magic) in [("unlimited", b"EAHD"), ("filtered", b"FAHD")] {
7376            let path = temp_path("implicit_not");
7377            {
7378                let file = H5File::create(&path).unwrap();
7379                let builder = file.new_dataset::<i32>().shape([16]);
7380                let builder = match which {
7381                    "unlimited" => builder.chunk(&[4]).max_shape(&[None]),
7382                    _ => builder.chunk(&[4]).deflate(6),
7383                };
7384                let ds = builder.early_allocation().create("data").unwrap();
7385                ds.write_raw(&(0..16i32).collect::<Vec<_>>()).unwrap();
7386                file.close().unwrap();
7387            }
7388            let bytes = std::fs::read(&path).unwrap();
7389            assert!(
7390                bytes.windows(4).any(|w| w == magic),
7391                "{which}: expected a {} index",
7392                String::from_utf8_lossy(magic)
7393            );
7394            let file = H5File::open(&path).unwrap();
7395            assert_eq!(
7396                file.dataset("data").unwrap().read_raw::<i32>().unwrap(),
7397                (0..16i32).collect::<Vec<_>>(),
7398                "{which}"
7399            );
7400            std::fs::remove_file(&path).ok();
7401        }
7402    }
7403
7404    /// One whole-dataset chunk always selects the single-chunk index —
7405    /// ahead of Fixed Array, and ahead of Implicit too, whether or not early
7406    /// allocation was requested (`H5D__layout_set_latest_indexing` checks it
7407    /// unconditionally). Like Implicit, "single chunk" is literal: no index
7408    /// structure at all, just the one chunk's address — and, unfiltered and
7409    /// early-allocated, that address exists before anything is written — in
7410    /// the layout message directly.
7411    #[test]
7412    fn one_whole_dataset_chunk_writes_the_single_chunk_index() {
7413        for early in [false, true] {
7414            let path = temp_path("single_chunk_index");
7415            {
7416                let file = H5File::create(&path).unwrap();
7417                let builder = file.new_dataset::<i32>().shape([16]).chunk(&[16]);
7418                let builder = if early {
7419                    builder.early_allocation()
7420                } else {
7421                    builder
7422                };
7423                let ds = builder.create("data").unwrap();
7424                ds.write_raw(&(0..16i32).collect::<Vec<_>>()).unwrap();
7425                file.close().unwrap();
7426            }
7427            let bytes = std::fs::read(&path).unwrap();
7428            for magic in [b"EAHD", b"EAIB", b"FAHD", b"FADB", b"BTHD", b"TREE"] {
7429                assert!(
7430                    !bytes.windows(4).any(|w| w == magic),
7431                    "early={early}: {} appears in a file whose chunk index is \
7432                     supposed to be no structure at all",
7433                    String::from_utf8_lossy(magic)
7434                );
7435            }
7436            let file = H5File::open(&path).unwrap();
7437            assert_eq!(
7438                file.dataset("data").unwrap().read_raw::<i32>().unwrap(),
7439                (0..16i32).collect::<Vec<_>>(),
7440                "early={early}"
7441            );
7442            std::fs::remove_file(&path).ok();
7443        }
7444    }
7445
7446    /// An implicitly indexed dataset's chunks all exist from create, so an
7447    /// unwritten one reads back as the fill value — and the fill value is
7448    /// tiled over the whole run at create, not per chunk on demand.
7449    #[test]
7450    fn implicit_index_fills_every_chunk_at_create() {
7451        let path = temp_path("implicit_fill");
7452        {
7453            let file = H5File::create(&path).unwrap();
7454            let ds = file
7455                .new_dataset::<i32>()
7456                .shape([8])
7457                .chunk(&[4])
7458                .early_allocation()
7459                .fill_value(-3i32)
7460                .create("data")
7461                .unwrap();
7462            // Only chunk 0.
7463            let chunk: Vec<u8> = [1i32, 2, 3, 4]
7464                .iter()
7465                .flat_map(|v| v.to_le_bytes())
7466                .collect();
7467            ds.write_chunk(0, &chunk).unwrap();
7468            file.close().unwrap();
7469        }
7470        let file = H5File::open(&path).unwrap();
7471        let ds = file.dataset("data").unwrap();
7472        assert_eq!(
7473            ds.read_raw::<i32>().unwrap(),
7474            vec![1, 2, 3, 4, -3, -3, -3, -3]
7475        );
7476        std::fs::remove_file(&path).ok();
7477    }
7478
7479    /// A reopen has to reconstruct the run's address *and* its length from
7480    /// the layout message alone — there is no index structure to read it
7481    /// back from — or the close would rewrite the dataset as unallocated
7482    /// contiguous storage and drop every byte.
7483    #[test]
7484    fn implicit_index_survives_a_reopen() {
7485        let path = temp_path("implicit_reopen");
7486        {
7487            let file = H5File::create(&path).unwrap();
7488            file.new_dataset::<i32>()
7489                .shape([8])
7490                .chunk(&[4])
7491                .early_allocation()
7492                .create("data")
7493                .unwrap()
7494                .write_raw(&[0i32, 1, 2, 3, 4, 5, 6, 7])
7495                .unwrap();
7496            file.close().unwrap();
7497        }
7498        {
7499            let file = H5File::open_rw(&path).unwrap();
7500            let ds = file.dataset_writer("data").unwrap();
7501            let chunk: Vec<u8> = [10i32, 11, 12, 13]
7502                .iter()
7503                .flat_map(|v| v.to_le_bytes())
7504                .collect();
7505            ds.write_chunk(1, &chunk).unwrap();
7506            file.close().unwrap();
7507        }
7508        let file = H5File::open(&path).unwrap();
7509        assert_eq!(
7510            file.dataset("data").unwrap().read_raw::<i32>().unwrap(),
7511            vec![0, 1, 2, 3, 10, 11, 12, 13]
7512        );
7513        std::fs::remove_file(&path).ok();
7514    }
7515
7516    #[test]
7517    fn fill_value_chunked_roundtrip() {
7518        let path = temp_path("fill_value_chunked");
7519        {
7520            let file = H5File::create(&path).unwrap();
7521            let ds = file
7522                .new_dataset::<i32>()
7523                .shape([0])
7524                .chunk(&[4])
7525                .max_shape(&[None])
7526                .fill_value(-7i32)
7527                .create("vals")
7528                .unwrap();
7529            ds.append(&[1i32, 2, 3, 4]).unwrap();
7530            file.close().unwrap();
7531        }
7532        {
7533            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
7534            let idx = writer.dataset_index("vals").unwrap();
7535            assert_eq!(
7536                writer.ds(idx).lock().fill_value,
7537                Some((-7i32).to_le_bytes().to_vec())
7538            );
7539        }
7540        std::fs::remove_file(&path).ok();
7541    }
7542
7543    #[test]
7544    fn fill_value_bytes_takes_the_stored_type_in_its_declared_order() {
7545        use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
7546        let be12 = DatatypeMessage::FixedPoint {
7547            size: 2,
7548            byte_order: ByteOrder::BigEndian,
7549            signed: false,
7550            bit_offset: 0,
7551            bit_precision: 12,
7552        };
7553        let path = temp_path("fill_value_bytes");
7554        {
7555            let file = H5File::create(&path).unwrap();
7556            file.new_dataset::<u16>()
7557                .shape([0])
7558                .chunk(&[4])
7559                .max_shape(&[None])
7560                .datatype(be12.clone())
7561                .fill_value_bytes(&0x0abcu16.to_ne_bytes())
7562                .create("vals")
7563                .unwrap();
7564            let err = file
7565                .new_dataset::<u16>()
7566                .shape([4])
7567                .datatype(be12)
7568                .fill_value_bytes(&[1, 2, 3])
7569                .create("short");
7570            assert!(matches!(err, Err(crate::Hdf5Error::TypeMismatch(_))));
7571            file.close().unwrap();
7572        }
7573        {
7574            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
7575            let idx = writer.dataset_index("vals").unwrap();
7576            assert_eq!(writer.ds(idx).lock().fill_value, Some(vec![0x0a, 0xbc]));
7577            assert!(writer.dataset_index("short").is_none());
7578        }
7579        std::fs::remove_file(&path).ok();
7580    }
7581
7582    #[test]
7583    fn fill_value_read_missing_chunks() {
7584        // A chunked dataset with chunk 1 left unwritten must read that
7585        // gap back as the user-defined fill value, not zero.
7586        fn i32_bytes(vals: &[i32]) -> Vec<u8> {
7587            vals.iter().flat_map(|v| v.to_le_bytes()).collect()
7588        }
7589        let path = temp_path("fill_value_read_missing");
7590        {
7591            let file = H5File::create(&path).unwrap();
7592            let ds = file
7593                .new_dataset::<i32>()
7594                .shape([0])
7595                .chunk(&[2])
7596                .max_shape(&[None])
7597                .fill_value(-1i32)
7598                .create("vals")
7599                .unwrap();
7600            // chunk 0 = [10,20]; chunk 1 unwritten; chunk 2 = [50,60].
7601            ds.write_chunk(0, &i32_bytes(&[10, 20])).unwrap();
7602            ds.write_chunk(2, &i32_bytes(&[50, 60])).unwrap();
7603            ds.extend(&[6]).unwrap();
7604            file.close().unwrap();
7605        }
7606        {
7607            let file = H5File::open(&path).unwrap();
7608            let ds = file.dataset("vals").unwrap();
7609            let all = ds.read_raw::<i32>().unwrap();
7610            assert_eq!(all, vec![10, 20, -1, -1, 50, 60]);
7611        }
7612        std::fs::remove_file(&path).ok();
7613    }
7614
7615    #[test]
7616    fn fill_value_partial_chunk_padded_with_fill() {
7617        // A partial trailing chunk flushed at close must pad its unwritten
7618        // tail with the fill value. That pad sits beyond the logical shape,
7619        // so it is verified by scanning the on-disk chunk bytes directly.
7620        let path = temp_path("fill_value_partial_pad");
7621        {
7622            let file = H5File::create(&path).unwrap();
7623            let ds = file
7624                .new_dataset::<i32>()
7625                .shape([0])
7626                .chunk(&[4])
7627                .max_shape(&[None])
7628                .fill_value(-9i32)
7629                .create("vals")
7630                .unwrap();
7631            // 3 of 4 frames -> flushed as a partial chunk on close.
7632            ds.append(&[1i32, 2, 3]).unwrap();
7633            file.close().unwrap();
7634        }
7635        let bytes = std::fs::read(&path).unwrap();
7636        // Locate the chunk: i32 LE of [1, 2, 3] written contiguously.
7637        let needle: Vec<u8> = [1i32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
7638        let pos = bytes
7639            .windows(needle.len())
7640            .position(|w| w == needle)
7641            .expect("chunk data [1,2,3] not found in file");
7642        let pad = &bytes[pos + needle.len()..pos + needle.len() + 4];
7643        assert_eq!(
7644            pad,
7645            &(-9i32).to_le_bytes(),
7646            "partial chunk tail must be padded with fill value -9, got {:?}",
7647            pad
7648        );
7649        std::fs::remove_file(&path).ok();
7650    }
7651
7652    #[test]
7653    fn vlen_append_after_reopen_preserves_existing() {
7654        // Reopening and appending into a partially-written vlen chunk must
7655        // read-modify-write: the strings already on disk must survive.
7656        let path = temp_path("vlen_append_reopen");
7657        {
7658            let file = H5File::create(&path).unwrap();
7659            file.create_appendable_vlen_dataset("strs", 4, None)
7660                .unwrap();
7661            // 3 of 4 frames -> flushed as a partial chunk on close.
7662            file.append_vlen_strings("strs", &["a", "b", "c"]).unwrap();
7663            file.close().unwrap();
7664        }
7665        {
7666            // Append a 4th string -> partial-chunk write into chunk 0.
7667            let file = H5File::open_rw(&path).unwrap();
7668            file.append_vlen_strings("strs", &["d"]).unwrap();
7669            file.close().unwrap();
7670        }
7671        {
7672            let file = H5File::open(&path).unwrap();
7673            let ds = file.dataset("strs").unwrap();
7674            let got = ds.read_vlen_strings().unwrap();
7675            assert_eq!(
7676                got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
7677                vec!["a", "b", "c", "d"]
7678            );
7679        }
7680        std::fs::remove_file(&path).ok();
7681    }
7682
7683    #[test]
7684    fn fill_value_size_mismatch_errors() {
7685        let path = temp_path("fill_value_mismatch");
7686        let writer = crate::io::writer::Hdf5Writer::create(&path).unwrap();
7687        let dt = <f64 as crate::types::H5Type>::hdf5_type();
7688        let idx = writer.create_dataset("d", dt, &[4u64]).unwrap();
7689        // f64 element size is 8; a 4-byte fill value must be rejected.
7690        assert!(writer.set_dataset_fill_value(idx, vec![0u8; 4]).is_err());
7691        // The correct width succeeds.
7692        writer.set_dataset_fill_value(idx, vec![0u8; 8]).unwrap();
7693        writer.close().unwrap();
7694        std::fs::remove_file(&path).ok();
7695    }
7696
7697    #[test]
7698    fn datatype_exposes_class_sign_and_byteorder() {
7699        // The byte width alone cannot tell u8 from i8 (both 1 byte) or i32
7700        // from f32 (both 4 bytes). datatype() must report the real class and
7701        // signedness so a reader does not have to guess from element_size.
7702        use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
7703
7704        let path = temp_path("datatype_accessor");
7705        {
7706            let file = H5File::create(&path).unwrap();
7707            file.new_dataset::<u8>().shape([3]).create("u8d").unwrap();
7708            file.new_dataset::<i8>().shape([3]).create("i8d").unwrap();
7709            file.new_dataset::<i32>().shape([3]).create("i32d").unwrap();
7710            file.new_dataset::<f32>().shape([3]).create("f32d").unwrap();
7711            file.close().unwrap();
7712        }
7713
7714        let file = H5File::open(&path).unwrap();
7715
7716        match file.dataset("u8d").unwrap().datatype().unwrap() {
7717            DatatypeMessage::FixedPoint {
7718                size,
7719                signed,
7720                byte_order,
7721                ..
7722            } => {
7723                assert_eq!(size, 1);
7724                assert!(!signed, "u8 must be unsigned");
7725                assert_eq!(byte_order, ByteOrder::LittleEndian);
7726            }
7727            other => panic!("expected FixedPoint for u8, got {other:?}"),
7728        }
7729
7730        match file.dataset("i8d").unwrap().datatype().unwrap() {
7731            DatatypeMessage::FixedPoint { size, signed, .. } => {
7732                assert_eq!(size, 1);
7733                assert!(signed, "i8 must be signed");
7734            }
7735            other => panic!("expected FixedPoint for i8, got {other:?}"),
7736        }
7737
7738        match file.dataset("i32d").unwrap().datatype().unwrap() {
7739            DatatypeMessage::FixedPoint { size, signed, .. } => {
7740                assert_eq!(size, 4);
7741                assert!(signed, "i32 must be signed");
7742            }
7743            other => panic!("expected FixedPoint for i32, got {other:?}"),
7744        }
7745
7746        match file.dataset("f32d").unwrap().datatype().unwrap() {
7747            DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 4),
7748            other => panic!("expected FloatingPoint for f32, got {other:?}"),
7749        }
7750
7751        std::fs::remove_file(&path).ok();
7752    }
7753
7754    #[test]
7755    fn datatype_in_write_mode_errors() {
7756        let path = temp_path("datatype_write_mode");
7757        let file = H5File::create(&path).unwrap();
7758        let ds = file.new_dataset::<f32>().shape([4]).create("d").unwrap();
7759        assert!(ds.datatype().is_err());
7760        std::fs::remove_file(&path).ok();
7761    }
7762
7763    // --- write_chunk_raw (HDF5 direct chunk write) ---------------------------
7764
7765    /// Extensible-array path: pre-compress with the dataset's pipeline, write
7766    /// the bytes verbatim via write_chunk_raw (filter_mask = 0), and confirm
7767    /// the data round-trips through the reader unchanged.
7768    #[cfg(feature = "deflate")]
7769    #[test]
7770    fn write_chunk_raw_ea_roundtrip_mask0() {
7771        use crate::format::messages::filter::{apply_filters, FilterPipeline};
7772        let path = temp_path("wcr_ea_mask0");
7773        let original: Vec<i32> = (0..12).collect();
7774        {
7775            let file = H5File::create(&path).unwrap();
7776            let ds = file
7777                .new_dataset::<i32>()
7778                .shape([0])
7779                .chunk(&[4])
7780                .max_shape(&[None])
7781                .deflate(4)
7782                .create("v")
7783                .unwrap();
7784            assert!(ds.is_chunked());
7785            let pipeline = FilterPipeline::deflate(4);
7786            for c in 0..3usize {
7787                let raw: Vec<u8> = original[c * 4..c * 4 + 4]
7788                    .iter()
7789                    .flat_map(|v| v.to_le_bytes())
7790                    .collect();
7791                let compressed = apply_filters(&pipeline, &raw).unwrap();
7792                ds.write_chunk_raw(c, &compressed, 0).unwrap();
7793            }
7794            ds.set_extent(&[12]).unwrap();
7795            file.close().unwrap();
7796        }
7797        {
7798            let file = H5File::open(&path).unwrap();
7799            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
7800            assert_eq!(v, original);
7801        }
7802        std::fs::remove_file(&path).ok();
7803    }
7804
7805    /// Fixed-array path (all dimensions bounded): same verbatim write through
7806    /// the linear-index dispatch, round-tripped through the reader.
7807    #[cfg(feature = "deflate")]
7808    #[test]
7809    fn write_chunk_raw_fixed_array_roundtrip_mask0() {
7810        use crate::format::messages::filter::{apply_filters, FilterPipeline};
7811        let path = temp_path("wcr_fa_mask0");
7812        let original: Vec<i32> = (0..12).collect();
7813        {
7814            let file = H5File::create(&path).unwrap();
7815            let ds = file
7816                .new_dataset::<i32>()
7817                .shape([12])
7818                .chunk(&[4])
7819                .deflate(4)
7820                .create("v")
7821                .unwrap();
7822            assert!(ds.is_chunked());
7823            let pipeline = FilterPipeline::deflate(4);
7824            for c in 0..3usize {
7825                let raw: Vec<u8> = original[c * 4..c * 4 + 4]
7826                    .iter()
7827                    .flat_map(|v| v.to_le_bytes())
7828                    .collect();
7829                let compressed = apply_filters(&pipeline, &raw).unwrap();
7830                ds.write_chunk_raw(c, &compressed, 0).unwrap();
7831            }
7832            file.close().unwrap();
7833        }
7834        {
7835            let file = H5File::open(&path).unwrap();
7836            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
7837            assert_eq!(v, original);
7838        }
7839        std::fs::remove_file(&path).ok();
7840    }
7841
7842    /// The caller-supplied filter_mask must reach the on-disk filtered index
7843    /// entry (not be hardcoded to 0). Store one chunk uncompressed in a
7844    /// filtered dataset with mask = 1 (deflate skipped), then reopen and decode
7845    /// the extensible-array filtered entry to read the mask back at the format
7846    /// level (independent of the data reader's mask handling).
7847    #[cfg(feature = "deflate")]
7848    #[test]
7849    fn write_chunk_raw_records_filter_mask() {
7850        let path = temp_path("wcr_records_mask");
7851        let raw: Vec<u8> = [10i32, 20, 30, 40]
7852            .iter()
7853            .flat_map(|v| v.to_le_bytes())
7854            .collect();
7855        assert_eq!(raw.len(), 16);
7856        {
7857            let file = H5File::create(&path).unwrap();
7858            let ds = file
7859                .new_dataset::<i32>()
7860                .shape([0])
7861                .chunk(&[4])
7862                .max_shape(&[None])
7863                .deflate(4)
7864                .create("v")
7865                .unwrap();
7866            // mask = 1: bit 0 set => filter 0 (deflate) was skipped, so the
7867            // chunk is stored uncompressed (its raw bytes).
7868            ds.write_chunk_raw(0, &raw, 1).unwrap();
7869            ds.set_extent(&[4]).unwrap();
7870            file.close().unwrap();
7871        }
7872        // Reopen the writer; open_append decodes the filtered index block from
7873        // disk, so the entry reflects exactly what was committed.
7874        {
7875            let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
7876            let idx = w.dataset_index("v").unwrap();
7877            let ds = w.ds(idx);
7878            let m = ds.lock();
7879            let entry = &m
7880                .chunked
7881                .as_ref()
7882                .unwrap()
7883                .filt_iblk
7884                .as_ref()
7885                .unwrap()
7886                .elements[0];
7887            assert_eq!(entry.filter_mask, 1, "filter_mask must round-trip to disk");
7888            assert_eq!(entry.nbytes, 16, "uncompressed chunk stored verbatim");
7889        }
7890        std::fs::remove_file(&path).ok();
7891    }
7892
7893    /// Reader honors a per-chunk filter_mask (EA): one chunk is stored
7894    /// compressed (mask 0), the next stored raw with deflate skipped (mask 1),
7895    /// in the same dataset. A correct reader skips deflate for chunk 1 only;
7896    /// ignoring the mask would feed raw bytes through inflate and corrupt them.
7897    /// A chunk whose stored stream decodes to less than its image places no
7898    /// run at all: the whole chunk reads as the fill value, whether the read
7899    /// laid the fill down first (a plan that leaves output uncovered — here the
7900    /// unallocated middle chunk) or fills only what nothing wrote. The decode
7901    /// writes into the output image itself, so the bytes a short image leaves
7902    /// behind are the ones this covers.
7903    #[cfg(feature = "deflate")]
7904    #[test]
7905    fn a_chunk_that_decodes_short_of_its_image_reads_as_fill() {
7906        use crate::format::messages::filter::{apply_filters, FilterPipeline};
7907        let path = temp_path("short_chunk_image_is_fill");
7908        let pipeline = FilterPipeline::deflate(4);
7909        {
7910            let file = H5File::create(&path).unwrap();
7911            let ds = file
7912                .new_dataset::<i32>()
7913                .shape([0])
7914                .chunk(&[4])
7915                .max_shape(&[None])
7916                .fill_value(-1i32)
7917                .deflate(4)
7918                .create("v")
7919                .unwrap();
7920            // Chunk 0 carries two elements where its image wants four.
7921            let short: Vec<u8> = [7i32, 8].iter().flat_map(|v| v.to_le_bytes()).collect();
7922            ds.write_chunk_raw(0, &apply_filters(&pipeline, &short).unwrap(), 0)
7923                .unwrap();
7924            // Chunk 1 is never written; chunk 2 is whole.
7925            let whole: Vec<u8> = [9i32, 10, 11, 12]
7926                .iter()
7927                .flat_map(|v| v.to_le_bytes())
7928                .collect();
7929            ds.write_chunk_raw(2, &apply_filters(&pipeline, &whole).unwrap(), 0)
7930                .unwrap();
7931            ds.set_extent(&[12]).unwrap();
7932            file.close().unwrap();
7933        }
7934        {
7935            let file = H5File::open(&path).unwrap();
7936            let ds = file.dataset("v").unwrap();
7937            assert_eq!(
7938                ds.read_raw::<i32>().unwrap(),
7939                vec![-1, -1, -1, -1, -1, -1, -1, -1, 9, 10, 11, 12]
7940            );
7941            // The same verdict when the plan covers every output byte, so no
7942            // fill goes down first: chunks 0 and 2 alone.
7943            assert_eq!(ds.read_slice::<i32>(&[0], &[4]).unwrap(), vec![-1; 4]);
7944            assert_eq!(
7945                ds.read_slice::<i32>(&[8], &[4]).unwrap(),
7946                vec![9, 10, 11, 12]
7947            );
7948        }
7949        std::fs::remove_file(&path).ok();
7950    }
7951
7952    /// The staged spelling of the case above: a selection that takes only part
7953    /// of the short chunk decodes it into a buffer sized from the layout, and
7954    /// what that buffer holds past the stream is cut off rather than kept — a
7955    /// run inside the decoded bytes is real data, a run reaching past them is
7956    /// fill.
7957    #[cfg(feature = "deflate")]
7958    #[test]
7959    fn a_staged_chunk_carries_only_what_its_stream_decoded() {
7960        use crate::format::messages::filter::{apply_filters, FilterPipeline};
7961        let path = temp_path("short_chunk_image_staged");
7962        let pipeline = FilterPipeline::deflate(4);
7963        {
7964            let file = H5File::create(&path).unwrap();
7965            let ds = file
7966                .new_dataset::<i32>()
7967                .shape([0])
7968                .chunk(&[4])
7969                .max_shape(&[None])
7970                .fill_value(-1i32)
7971                .deflate(4)
7972                .create("v")
7973                .unwrap();
7974            let short: Vec<u8> = [7i32, 8].iter().flat_map(|v| v.to_le_bytes()).collect();
7975            ds.write_chunk_raw(0, &apply_filters(&pipeline, &short).unwrap(), 0)
7976                .unwrap();
7977            ds.set_extent(&[4]).unwrap();
7978            file.close().unwrap();
7979        }
7980        {
7981            let file = H5File::open(&path).unwrap();
7982            let ds = file.dataset("v").unwrap();
7983            // Inside the decoded bytes.
7984            assert_eq!(ds.read_slice::<i32>(&[0], &[2]).unwrap(), vec![7, 8]);
7985            // Straddling their end: the run is not placed at all.
7986            assert_eq!(ds.read_slice::<i32>(&[1], &[2]).unwrap(), vec![-1, -1]);
7987        }
7988        std::fs::remove_file(&path).ok();
7989    }
7990
7991    #[cfg(feature = "deflate")]
7992    #[test]
7993    fn write_chunk_raw_ea_per_chunk_mask_roundtrip() {
7994        use crate::format::messages::filter::{apply_filters, FilterPipeline};
7995        let path = temp_path("wcr_ea_per_chunk_mask");
7996        let original: Vec<i32> = (0..8).collect();
7997        let pipeline = FilterPipeline::deflate(4);
7998        {
7999            let file = H5File::create(&path).unwrap();
8000            let ds = file
8001                .new_dataset::<i32>()
8002                .shape([0])
8003                .chunk(&[4])
8004                .max_shape(&[None])
8005                .deflate(4)
8006                .create("v")
8007                .unwrap();
8008            let raw0: Vec<u8> = original[0..4]
8009                .iter()
8010                .flat_map(|v| v.to_le_bytes())
8011                .collect();
8012            // chunk 0: compressed through the pipeline, mask 0.
8013            ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
8014                .unwrap();
8015            let raw1: Vec<u8> = original[4..8]
8016                .iter()
8017                .flat_map(|v| v.to_le_bytes())
8018                .collect();
8019            // chunk 1: stored uncompressed, mask 1 (deflate skipped).
8020            ds.write_chunk_raw(1, &raw1, 1).unwrap();
8021            ds.set_extent(&[8]).unwrap();
8022            file.close().unwrap();
8023        }
8024        {
8025            let file = H5File::open(&path).unwrap();
8026            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
8027            assert_eq!(v, original);
8028        }
8029        std::fs::remove_file(&path).ok();
8030    }
8031
8032    /// Reader honors a per-chunk filter_mask (fixed array): same mixed
8033    /// compressed/raw chunks as the EA case, through the fixed-array index.
8034    #[cfg(feature = "deflate")]
8035    #[test]
8036    fn write_chunk_raw_fixed_array_per_chunk_mask_roundtrip() {
8037        use crate::format::messages::filter::{apply_filters, FilterPipeline};
8038        let path = temp_path("wcr_fa_per_chunk_mask");
8039        let original: Vec<i32> = (0..8).collect();
8040        let pipeline = FilterPipeline::deflate(4);
8041        {
8042            let file = H5File::create(&path).unwrap();
8043            let ds = file
8044                .new_dataset::<i32>()
8045                .shape([8])
8046                .chunk(&[4])
8047                .deflate(4)
8048                .create("v")
8049                .unwrap();
8050            let raw0: Vec<u8> = original[0..4]
8051                .iter()
8052                .flat_map(|v| v.to_le_bytes())
8053                .collect();
8054            ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
8055                .unwrap();
8056            let raw1: Vec<u8> = original[4..8]
8057                .iter()
8058                .flat_map(|v| v.to_le_bytes())
8059                .collect();
8060            ds.write_chunk_raw(1, &raw1, 1).unwrap();
8061            file.close().unwrap();
8062        }
8063        {
8064            let file = H5File::open(&path).unwrap();
8065            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
8066            assert_eq!(v, original);
8067        }
8068        std::fs::remove_file(&path).ok();
8069    }
8070
8071    /// An unfiltered chunk index has no slot for a stored size or mask, so a
8072    /// direct chunk write must be rejected rather than silently dropping them.
8073    #[test]
8074    fn write_chunk_raw_rejects_unfiltered() {
8075        let path = temp_path("wcr_unfiltered");
8076        let file = H5File::create(&path).unwrap();
8077        let ds = file
8078            .new_dataset::<i32>()
8079            .shape([0])
8080            .chunk(&[4])
8081            .max_shape(&[None])
8082            .create("v")
8083            .unwrap();
8084        let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
8085        assert!(
8086            err.to_string().contains("filtered dataset"),
8087            "expected a filtered-dataset error, got: {err}"
8088        );
8089        std::fs::remove_file(&path).ok();
8090    }
8091
8092    /// Two or more unlimited dimensions leave no fixed chunk grid for a linear
8093    /// index to mean anything against, so the linear entry point points the
8094    /// caller at the coordinate-addressed one rather than guessing a grid.
8095    #[test]
8096    fn write_chunk_raw_sends_btree_v2_to_the_coordinate_form() {
8097        let path = temp_path("wcr_btree2");
8098        let file = H5File::create(&path).unwrap();
8099        let ds = file
8100            .new_dataset::<i32>()
8101            .shape([0, 0])
8102            .chunk(&[2, 2])
8103            .max_shape(&[None, None])
8104            .create("grid")
8105            .unwrap();
8106        let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
8107        assert!(
8108            err.to_string().contains("write_chunk_raw_at"),
8109            "expected a pointer to the coordinate form, got: {err}"
8110        );
8111        std::fs::remove_file(&path).ok();
8112    }
8113
8114    /// Direct chunk writes on a v2-B-tree index: the bytes are stored verbatim
8115    /// and the type-11 record carries their size and the caller's mask, so a
8116    /// chunk written with the pipeline skipped (mask 1) reads back as the raw
8117    /// bytes while one written compressed (mask 0) is decompressed.
8118    #[cfg(feature = "deflate")]
8119    #[test]
8120    fn write_chunk_raw_at_round_trips_on_btree_v2() {
8121        use crate::format::messages::filter::{apply_filters, FilterPipeline};
8122
8123        let path = temp_path("wcr_at_btree2");
8124        let raw0: Vec<u8> = (0..4i32).flat_map(|v| v.to_le_bytes()).collect();
8125        let raw1: Vec<u8> = (100..104i32).flat_map(|v| v.to_le_bytes()).collect();
8126        {
8127            let file = H5File::create(&path).unwrap();
8128            let ds = file
8129                .new_dataset::<i32>()
8130                .shape([0, 0])
8131                .chunk(&[2, 2])
8132                .max_shape(&[None, None])
8133                .deflate(6)
8134                .create("grid")
8135                .unwrap();
8136            let pipeline = FilterPipeline::deflate(6);
8137            // Chunk (0,0): pipeline already applied upstream, mask 0.
8138            ds.write_chunk_raw_at(&[0, 0], &apply_filters(&pipeline, &raw0).unwrap(), 0)
8139                .unwrap();
8140            // Chunk (1,1): stored uncompressed, mask 1 says filter 0 was skipped.
8141            ds.write_chunk_raw_at(&[1, 1], &raw1, 1).unwrap();
8142            file.close().unwrap();
8143        }
8144        let file = H5File::open(&path).unwrap();
8145        let ds = file.dataset("grid").unwrap();
8146        assert_eq!(ds.shape(), vec![4, 4]);
8147        let all = ds.read_raw::<i32>().unwrap();
8148        // Chunk (0,0) occupies rows 0..2, columns 0..2.
8149        assert_eq!([all[0], all[1], all[4], all[5]], [0, 1, 2, 3]);
8150        // Chunk (1,1) occupies rows 2..4, columns 2..4.
8151        assert_eq!([all[10], all[11], all[14], all[15]], [100, 101, 102, 103]);
8152        drop(file);
8153        std::fs::remove_file(&path).ok();
8154    }
8155
8156    /// The coordinate form is not BT2-only: it addresses an extensible- or
8157    /// fixed-array dataset's grid just as well, and records the same mask.
8158    #[cfg(feature = "deflate")]
8159    #[test]
8160    fn write_chunk_raw_at_round_trips_on_the_array_indexes() {
8161        for (label, max_shape) in [
8162            ("wcr_at_ea", Some(vec![None, Some(4usize)])),
8163            ("wcr_at_fa", None),
8164        ] {
8165            let path = temp_path(label);
8166            let raw: Vec<u8> = (0..8i32).flat_map(|v| v.to_le_bytes()).collect();
8167            {
8168                let file = H5File::create(&path).unwrap();
8169                let mut b = file
8170                    .new_dataset::<i32>()
8171                    .shape([4usize, 4])
8172                    .chunk(&[2, 4])
8173                    .deflate(6);
8174                if let Some(ref ms) = max_shape {
8175                    b = b.max_shape(ms);
8176                }
8177                let ds = b.create("grid").unwrap();
8178                // Row-of-chunks 1, stored uncompressed with filter 0 skipped.
8179                ds.write_chunk_raw_at(&[1, 0], &raw, 1).unwrap();
8180                file.close().unwrap();
8181            }
8182            let file = H5File::open(&path).unwrap();
8183            let ds = file.dataset("grid").unwrap();
8184            let all = ds.read_raw::<i32>().unwrap();
8185            assert_eq!(&all[8..16], &(0..8).collect::<Vec<i32>>()[..], "{label}");
8186            drop(file);
8187            std::fs::remove_file(&path).ok();
8188        }
8189    }
8190
8191    /// A direct write hands over caller-supplied bytes, so the v2 B-tree's
8192    /// chunk-size field can overflow just as the array indexes' can. A 4-byte
8193    /// chunk gives chunk_size_len = 2 (max 65535).
8194    #[cfg(feature = "deflate")]
8195    #[test]
8196    fn write_chunk_raw_at_rejects_an_oversized_btree_v2_chunk() {
8197        let path = temp_path("wcr_at_oversized");
8198        let file = H5File::create(&path).unwrap();
8199        let ds = file
8200            .new_dataset::<i32>()
8201            .shape([0, 0])
8202            .chunk(&[1, 1])
8203            .max_shape(&[None, None])
8204            .deflate(4)
8205            .create("grid")
8206            .unwrap();
8207        let err = ds
8208            .write_chunk_raw_at(&[0, 0], &vec![0u8; 70000], 0)
8209            .unwrap_err();
8210        assert!(
8211            err.to_string().contains("does not fit"),
8212            "expected a chunk-size-field overflow error, got: {err}"
8213        );
8214        std::fs::remove_file(&path).ok();
8215    }
8216
8217    /// An unfiltered v2 B-tree record has no slot for a stored size or mask,
8218    /// the same reason the array indexes reject a direct write.
8219    #[test]
8220    fn write_chunk_raw_at_rejects_an_unfiltered_btree_v2() {
8221        let path = temp_path("wcr_at_unfiltered");
8222        let file = H5File::create(&path).unwrap();
8223        let ds = file
8224            .new_dataset::<i32>()
8225            .shape([0, 0])
8226            .chunk(&[2, 2])
8227            .max_shape(&[None, None])
8228            .create("grid")
8229            .unwrap();
8230        let err = ds.write_chunk_raw_at(&[0, 0], &[0u8; 16], 0).unwrap_err();
8231        assert!(
8232            err.to_string().contains("filtered dataset"),
8233            "expected a filtered-dataset error, got: {err}"
8234        );
8235        std::fs::remove_file(&path).ok();
8236    }
8237
8238    /// A stored size that does not fit the index's chunk-size field must error
8239    /// (libhdf5 H5D_CHUNK_ENCODE_SIZE_CHECK) instead of truncating silently.
8240    /// A 4-byte chunk (chunk[1] of i32) has chunk_size_len = 2 (max 65535), so
8241    /// a 70000-byte stored chunk overflows it.
8242    #[cfg(feature = "deflate")]
8243    #[test]
8244    fn write_chunk_raw_rejects_oversized_chunk() {
8245        let path = temp_path("wcr_oversized");
8246        let file = H5File::create(&path).unwrap();
8247        let ds = file
8248            .new_dataset::<i32>()
8249            .shape([0])
8250            .chunk(&[1])
8251            .max_shape(&[None])
8252            .deflate(4)
8253            .create("v")
8254            .unwrap();
8255        let err = ds.write_chunk_raw(0, &vec![0u8; 70000], 0).unwrap_err();
8256        assert!(
8257            err.to_string().contains("does not fit"),
8258            "expected a chunk-size-field overflow error, got: {err}"
8259        );
8260        std::fs::remove_file(&path).ok();
8261    }
8262
8263    // ---- issue #5: runtime-width fixed-string reading ----------------------
8264
8265    use crate::format::messages::datatype::{CompoundMember, DatatypeMessage};
8266
8267    /// Build a 1-D fixed-string dataset of `width` bytes per element from raw
8268    /// element images, optionally chunked and deflated.
8269    fn write_fixed_string_dataset(
8270        path: &std::path::Path,
8271        dt: DatatypeMessage,
8272        width: usize,
8273        elems: &[&[u8]],
8274        compressed: bool,
8275    ) {
8276        let mut raw = Vec::with_capacity(elems.len() * width);
8277        for e in elems {
8278            assert!(e.len() <= width);
8279            raw.extend_from_slice(e);
8280            raw.resize(raw.len() + (width - e.len()), 0);
8281        }
8282        let file = H5File::create(path).unwrap();
8283        let mut b = file.new_dataset::<u8>().datatype(dt).shape([elems.len()]);
8284        if compressed {
8285            b = b.chunk(&[2]).deflate(6);
8286        }
8287        let ds = b.create("labels").unwrap();
8288        ds.write_raw_bytes(&raw).unwrap();
8289        file.close().unwrap();
8290    }
8291
8292    /// The width is whatever the file says, so one call reads a 24-byte label
8293    /// column and a 100-byte one. Producers like VASP pick it per dataset.
8294    #[test]
8295    fn read_strings_handles_any_fixed_width() {
8296        for width in [4usize, 24, 100] {
8297            let path = temp_path(&format!("fixed_str_{width}"));
8298            write_fixed_string_dataset(
8299                &path,
8300                DatatypeMessage::fixed_string(width as u32),
8301                width,
8302                &[b"ab", b"cde", b""],
8303                false,
8304            );
8305            let file = H5File::open(&path).unwrap();
8306            let got = file.dataset("labels").unwrap().read_strings().unwrap();
8307            assert_eq!(got, vec!["ab", "cde", ""], "width {width}");
8308            std::fs::remove_file(&path).ok();
8309        }
8310    }
8311
8312    /// Each padding rule decides where the value ends. Null-terminated and
8313    /// null-padded both stop at the first NUL and ignore the bytes after it;
8314    /// space-padded strips only a tail of spaces, so an embedded NUL is
8315    /// content there.
8316    ///
8317    /// Checked against libhdf5 1.14.6: reading this same `"ab\0X\0\0"`
8318    /// null-padded element into a wider null-terminated destination gives
8319    /// `"ab"`, and reading a space-padded `"a\0b     "` gives `"a\0b"` —
8320    /// `H5T__conv_s_s` runs the same `!s[nchars]` loop for both null rules.
8321    #[test]
8322    fn read_strings_honors_every_padding_rule() {
8323        // "ab" then a NUL then trailing junk that both null rules must drop.
8324        let elem: &[u8] = b"ab\0X\0\0";
8325        for (padding, want) in [(0u8, "ab"), (1, "ab")] {
8326            let path = temp_path(&format!("fixed_pad_{padding}"));
8327            write_fixed_string_dataset(
8328                &path,
8329                DatatypeMessage::FixedString {
8330                    size: 6,
8331                    padding,
8332                    charset: 0,
8333                },
8334                6,
8335                &[elem],
8336                false,
8337            );
8338            let file = H5File::open(&path).unwrap();
8339            let got = file.dataset("labels").unwrap().read_strings().unwrap();
8340            assert_eq!(got, vec![want.to_string()], "padding {padding}");
8341            std::fs::remove_file(&path).ok();
8342        }
8343        // Space-padded keeps interior spaces and strips only the tail.
8344        let path = temp_path("fixed_pad_2");
8345        write_fixed_string_dataset(
8346            &path,
8347            DatatypeMessage::FixedString {
8348                size: 8,
8349                padding: 2,
8350                charset: 0,
8351            },
8352            8,
8353            &[b"a b     "],
8354            false,
8355        );
8356        let file = H5File::open(&path).unwrap();
8357        assert_eq!(
8358            file.dataset("labels").unwrap().read_strings().unwrap(),
8359            vec!["a b".to_string()]
8360        );
8361        std::fs::remove_file(&path).ok();
8362
8363        // ... and an embedded NUL, which no space rule marks as an end.
8364        let path = temp_path("fixed_pad_2_nul");
8365        write_fixed_string_dataset(
8366            &path,
8367            DatatypeMessage::FixedString {
8368                size: 8,
8369                padding: 2,
8370                charset: 0,
8371            },
8372            8,
8373            &[b"a\0b     "],
8374            false,
8375        );
8376        let file = H5File::open(&path).unwrap();
8377        assert_eq!(
8378            file.dataset("labels").unwrap().read_strings().unwrap(),
8379            vec!["a\0b".to_string()]
8380        );
8381        std::fs::remove_file(&path).ok();
8382    }
8383
8384    /// A reserved padding or character-set code is an error naming the element,
8385    /// not a guess.
8386    #[test]
8387    fn read_strings_rejects_reserved_datatype_codes() {
8388        for (padding, charset, want) in [(3u8, 0u8, "padding rule 3"), (0, 7, "character set 7")] {
8389            let path = temp_path(&format!("fixed_reserved_{padding}_{charset}"));
8390            write_fixed_string_dataset(
8391                &path,
8392                DatatypeMessage::FixedString {
8393                    size: 4,
8394                    padding,
8395                    charset,
8396                },
8397                4,
8398                &[b"ab"],
8399                false,
8400            );
8401            let file = H5File::open(&path).unwrap();
8402            let err = file
8403                .dataset("labels")
8404                .unwrap()
8405                .read_strings()
8406                .unwrap_err()
8407                .to_string();
8408            assert!(err.contains(want), "got: {err}");
8409            std::fs::remove_file(&path).ok();
8410        }
8411    }
8412
8413    /// The typed read paths reinterpret the element image, so the stored order
8414    /// has to be the host's first. A scalar is swapped; a composite cannot be
8415    /// (its members have their own orders and offsets) and is refused.
8416    #[test]
8417    fn to_host_byte_order_converts_scalars_and_refuses_composites() {
8418        use crate::dataset::{to_host_byte_order, HOST_BYTE_ORDER};
8419        use crate::format::messages::datatype::ByteOrder;
8420
8421        let foreign = match HOST_BYTE_ORDER {
8422            ByteOrder::LittleEndian => ByteOrder::BigEndian,
8423            ByteOrder::BigEndian => ByteOrder::LittleEndian,
8424        };
8425        let int = |order, size| DatatypeMessage::FixedPoint {
8426            size,
8427            byte_order: order,
8428            signed: false,
8429            bit_offset: 0,
8430            bit_precision: (size * 8) as u16,
8431        };
8432
8433        // Foreign order: each element is reversed, elementwise.
8434        let mut buf = [1u8, 2, 3, 4, 5, 6, 7, 8];
8435        to_host_byte_order(&mut buf, &int(foreign, 4), 4).unwrap();
8436        assert_eq!(buf, [4, 3, 2, 1, 8, 7, 6, 5]);
8437
8438        // Host order: untouched.
8439        let mut buf = [1u8, 2, 3, 4];
8440        to_host_byte_order(&mut buf, &int(HOST_BYTE_ORDER, 4), 4).unwrap();
8441        assert_eq!(buf, [1, 2, 3, 4]);
8442
8443        // One byte wide: no order to convert.
8444        let mut buf = [1u8, 2, 3, 4];
8445        to_host_byte_order(&mut buf, &int(foreign, 1), 1).unwrap();
8446        assert_eq!(buf, [1, 2, 3, 4]);
8447
8448        // An enum stores its values in its base type's order.
8449        let mut buf = [1u8, 2];
8450        let enumeration = DatatypeMessage::Enum {
8451            base: Box::new(int(foreign, 2)),
8452            members: Vec::new(),
8453        };
8454        to_host_byte_order(&mut buf, &enumeration, 2).unwrap();
8455        assert_eq!(buf, [2, 1]);
8456
8457        // A string has no byte order at all.
8458        let mut buf = *b"abcd";
8459        to_host_byte_order(&mut buf, &DatatypeMessage::fixed_string(4), 4).unwrap();
8460        assert_eq!(&buf, b"abcd");
8461
8462        // A compound whose members are all host-order is reinterpretable.
8463        let compound = |order| DatatypeMessage::Compound {
8464            size: 4,
8465            members: vec![CompoundMember {
8466                name: "x".into(),
8467                offset: 0,
8468                datatype: int(order, 4),
8469            }],
8470        };
8471        let mut buf = [1u8, 2, 3, 4];
8472        to_host_byte_order(&mut buf, &compound(HOST_BYTE_ORDER), 4).unwrap();
8473        assert_eq!(buf, [1, 2, 3, 4]);
8474
8475        // One that is not says so, rather than handing back the raw bytes.
8476        let mut buf = [1u8, 2, 3, 4];
8477        let err = to_host_byte_order(&mut buf, &compound(foreign), 4)
8478            .expect_err("a foreign-order compound was reinterpreted")
8479            .to_string();
8480        assert!(err.contains("read_raw_bytes"), "got: {err}");
8481        assert_eq!(buf, [1, 2, 3, 4], "the refused image is left alone");
8482    }
8483
8484    /// The write direction answers for exactly the types the read direction
8485    /// does — same classifier — and borrows the caller's bytes whenever the
8486    /// declared order is already the host's.
8487    #[test]
8488    fn to_stored_byte_order_converts_scalars_and_refuses_composites() {
8489        use crate::dataset::{to_stored_byte_order, FOREIGN_BYTE_ORDER, HOST_BYTE_ORDER};
8490        use std::borrow::Cow;
8491
8492        let int = |order, size| DatatypeMessage::FixedPoint {
8493            size,
8494            byte_order: order,
8495            signed: false,
8496            bit_offset: 0,
8497            bit_precision: (size * 8) as u16,
8498        };
8499
8500        // Declared foreign: each element is reversed on the way out.
8501        let host = [1u8, 2, 3, 4, 5, 6, 7, 8];
8502        let stored = to_stored_byte_order(&host, &int(FOREIGN_BYTE_ORDER, 4), 4).unwrap();
8503        assert_eq!(&*stored, &[4, 3, 2, 1, 8, 7, 6, 5]);
8504        assert!(matches!(stored, Cow::Owned(_)), "a swap needs its own copy");
8505
8506        // Declared host order: handed through without a copy.
8507        let stored = to_stored_byte_order(&host, &int(HOST_BYTE_ORDER, 4), 4).unwrap();
8508        assert!(matches!(stored, Cow::Borrowed(_)), "no copy without a swap");
8509        assert_eq!(&*stored, &host);
8510
8511        // One byte wide: no order to lay out.
8512        let stored = to_stored_byte_order(&host, &int(FOREIGN_BYTE_ORDER, 1), 1).unwrap();
8513        assert_eq!(&*stored, &host);
8514
8515        // An enum stores its values in its base type's order.
8516        let enumeration = DatatypeMessage::Enum {
8517            base: Box::new(int(FOREIGN_BYTE_ORDER, 2)),
8518            members: Vec::new(),
8519        };
8520        let stored = to_stored_byte_order(&[1u8, 2], &enumeration, 2).unwrap();
8521        assert_eq!(&*stored, &[2, 1]);
8522
8523        // A compound cannot be laid out as a unit; one that declares the
8524        // foreign order for a member is refused, not written host-order.
8525        let compound = |order| DatatypeMessage::Compound {
8526            size: 4,
8527            members: vec![CompoundMember {
8528                name: "x".into(),
8529                offset: 0,
8530                datatype: int(order, 4),
8531            }],
8532        };
8533        let stored = to_stored_byte_order(&[1u8, 2, 3, 4], &compound(HOST_BYTE_ORDER), 4).unwrap();
8534        assert_eq!(&*stored, &[1, 2, 3, 4]);
8535        let err = to_stored_byte_order(&[1u8, 2, 3, 4], &compound(FOREIGN_BYTE_ORDER), 4)
8536            .expect_err("a foreign-order compound was written from host bytes")
8537            .to_string();
8538        assert!(err.contains("write_raw_bytes"), "got: {err}");
8539    }
8540
8541    /// The declared character set is enforced: a byte that cannot be decoded is
8542    /// an error naming the element, and the lossy call is what accepts the file
8543    /// instead of a silent substitution here.
8544    #[test]
8545    fn read_strings_enforces_the_character_set_and_lossy_does_not() {
8546        // Latin-1 "é" (0xE9) in a dataset that declares ASCII, and a lone 0xFF
8547        // in one that declares UTF-8.
8548        for (charset, bytes, want) in [
8549            (0u8, b"caf\xe9".as_slice(), "ASCII character set"),
8550            (1, b"a\xff".as_slice(), "not valid UTF-8"),
8551        ] {
8552            let path = temp_path(&format!("fixed_charset_{charset}"));
8553            write_fixed_string_dataset(
8554                &path,
8555                DatatypeMessage::FixedString {
8556                    size: 6,
8557                    padding: 1,
8558                    charset,
8559                },
8560                6,
8561                &[b"ok", bytes],
8562                false,
8563            );
8564            let file = H5File::open(&path).unwrap();
8565            let ds = file.dataset("labels").unwrap();
8566            let err = ds.read_strings().unwrap_err().to_string();
8567            assert!(err.contains(want) && err.contains("string 1"), "got: {err}");
8568            let lossy = ds.read_strings_lossy().unwrap();
8569            assert_eq!(lossy[0], "ok");
8570            assert_eq!(
8571                lossy[1].chars().next().unwrap(),
8572                if charset == 0 { 'c' } else { 'a' }
8573            );
8574            std::fs::remove_file(&path).ok();
8575        }
8576    }
8577
8578    /// Valid multi-byte UTF-8 survives, and the trailing NUL padding does not
8579    /// split a character.
8580    #[test]
8581    fn read_strings_reads_utf8_fixed_strings() {
8582        let path = temp_path("fixed_utf8");
8583        write_fixed_string_dataset(
8584            &path,
8585            DatatypeMessage::fixed_string_utf8(12),
8586            12,
8587            &["héllo".as_bytes(), "안녕".as_bytes()],
8588            false,
8589        );
8590        let file = H5File::open(&path).unwrap();
8591        assert_eq!(
8592            file.dataset("labels").unwrap().read_strings().unwrap(),
8593            vec!["héllo".to_string(), "안녕".to_string()]
8594        );
8595        std::fs::remove_file(&path).ok();
8596    }
8597
8598    /// The decode sits on the decoded raw-data path, so a chunked and deflated
8599    /// dataset reads the same as a contiguous one.
8600    #[cfg(feature = "deflate")]
8601    #[test]
8602    fn read_strings_reads_a_compressed_fixed_string_dataset() {
8603        let path = temp_path("fixed_str_deflate");
8604        write_fixed_string_dataset(
8605            &path,
8606            DatatypeMessage::fixed_string(16),
8607            16,
8608            &[b"alpha", b"beta", b"gamma", b"delta", b"epsilon"],
8609            true,
8610        );
8611        let file = H5File::open(&path).unwrap();
8612        assert_eq!(
8613            file.dataset("labels").unwrap().read_strings().unwrap(),
8614            vec!["alpha", "beta", "gamma", "delta", "epsilon"]
8615        );
8616        std::fs::remove_file(&path).ok();
8617    }
8618
8619    /// One call covers both string datatypes, so a caller need not branch on
8620    /// which one the file used.
8621    #[test]
8622    fn read_strings_also_reads_variable_length_strings() {
8623        let path = temp_path("read_strings_vlen");
8624        {
8625            let file = H5File::create(&path).unwrap();
8626            file.write_vlen_strings("names", &["alpha", "", "안녕"])
8627                .unwrap();
8628            file.close().unwrap();
8629        }
8630        let file = H5File::open(&path).unwrap();
8631        assert_eq!(
8632            file.dataset("names").unwrap().read_strings().unwrap(),
8633            vec!["alpha".to_string(), String::new(), "안녕".to_string()]
8634        );
8635        std::fs::remove_file(&path).ok();
8636    }
8637
8638    /// A file declaring a zero-width fixed string is an error, not the panic
8639    /// `chunks_exact(0)` would raise. Nothing in this crate writes one, so the
8640    /// test patches the width in the encoded datatype message down to zero and
8641    /// re-stamps the object header's checksum over the result.
8642    #[test]
8643    fn read_strings_rejects_a_zero_width_fixed_string_dataset() {
8644        use crate::format::checksum::checksum_metadata;
8645        use crate::format::object_header::OHDR_SIGNATURE;
8646
8647        let path = temp_path("fixed_str_zero_width");
8648        write_fixed_string_dataset(
8649            &path,
8650            DatatypeMessage::fixed_string(37),
8651            37,
8652            &[b"ab", b"cd"],
8653            false,
8654        );
8655
8656        // Version 1 string datatype: class|version, padding|charset, two
8657        // reserved bytes, then the width as a little-endian u32. The width is
8658        // 37 so the eight bytes occur once in the file.
8659        let mut bytes = std::fs::read(&path).unwrap();
8660        let needle = [0x13u8, 0, 0, 0, 37, 0, 0, 0];
8661        let at = bytes
8662            .windows(needle.len())
8663            .position(|w| w == needle)
8664            .expect("encoded fixed-string datatype message");
8665        assert!(
8666            !bytes[at + 1..].windows(needle.len()).any(|w| w == needle),
8667            "the datatype message pattern is not unique in the file"
8668        );
8669
8670        // The enclosing v2 object header ends in a checksum over everything
8671        // from its signature onwards; find the offset where the stored value
8672        // still agrees, so the patched header can be re-stamped there.
8673        let ohdr = bytes[..at]
8674            .windows(4)
8675            .rposition(|w| w == OHDR_SIGNATURE)
8676            .expect("enclosing object header");
8677        let cksum_at = (at + needle.len()..bytes.len() - 4)
8678            .find(|&e| {
8679                u32::from_le_bytes(bytes[e..e + 4].try_into().unwrap())
8680                    == checksum_metadata(&bytes[ohdr..e])
8681            })
8682            .expect("object header checksum");
8683
8684        bytes[at + 4..at + 8].copy_from_slice(&0u32.to_le_bytes());
8685        let fixed = checksum_metadata(&bytes[ohdr..cksum_at]);
8686        bytes[cksum_at..cksum_at + 4].copy_from_slice(&fixed.to_le_bytes());
8687        std::fs::write(&path, &bytes).unwrap();
8688
8689        let file = H5File::open(&path).unwrap();
8690        let err = file
8691            .dataset("labels")
8692            .unwrap()
8693            .read_strings()
8694            .unwrap_err()
8695            .to_string();
8696        assert!(err.contains("zero width"), "got: {err}");
8697        std::fs::remove_file(&path).ok();
8698    }
8699
8700    /// A non-string dataset is an error, not an attempt to reinterpret bytes.
8701    #[test]
8702    fn read_strings_rejects_a_non_string_dataset() {
8703        let path = temp_path("read_strings_numeric");
8704        {
8705            let file = H5File::create(&path).unwrap();
8706            let ds = file.new_dataset::<i32>().shape([3]).create("nums").unwrap();
8707            ds.write_raw(&[1i32, 2, 3]).unwrap();
8708            file.close().unwrap();
8709        }
8710        let file = H5File::open(&path).unwrap();
8711        let err = file
8712            .dataset("nums")
8713            .unwrap()
8714            .read_strings()
8715            .unwrap_err()
8716            .to_string();
8717        assert!(err.contains("only for string datasets"), "got: {err}");
8718        std::fs::remove_file(&path).ok();
8719    }
8720
8721    // ---- issue #6: random updates to vlen string datasets ------------------
8722
8723    /// One element changes; the extent and every other element stay as they
8724    /// were, on a contiguous vlen dataset.
8725    #[test]
8726    fn write_vlen_strings_slice_replaces_one_element() {
8727        let path = temp_path("vlen_slice_contig");
8728        {
8729            let file = H5File::create(&path).unwrap();
8730            file.write_vlen_strings("notes", &["a", "b", "c", "d"])
8731                .unwrap();
8732            file.close().unwrap();
8733        }
8734        {
8735            let file = H5File::open_rw(&path).unwrap();
8736            file.dataset_writer("notes")
8737                .unwrap()
8738                .write_vlen_strings_slice(1, &["replacement"])
8739                .unwrap();
8740            file.close().unwrap();
8741        }
8742        let file = H5File::open(&path).unwrap();
8743        let ds = file.dataset("notes").unwrap();
8744        assert_eq!(ds.shape(), vec![4]);
8745        assert_eq!(
8746            ds.read_vlen_strings().unwrap(),
8747            vec!["a", "replacement", "c", "d"]
8748        );
8749        std::fs::remove_file(&path).ok();
8750    }
8751
8752    /// The same on an appendable chunked dataset, across a reopen, over a range
8753    /// that spans a chunk boundary.
8754    #[test]
8755    fn write_vlen_strings_slice_spans_chunks_after_reopen() {
8756        let path = temp_path("vlen_slice_chunked");
8757        {
8758            let file = H5File::create(&path).unwrap();
8759            file.create_appendable_vlen_dataset("notes", 2, None)
8760                .unwrap();
8761            let all: Vec<String> = (0..6).map(|i| format!("v{i}")).collect();
8762            let refs: Vec<&str> = all.iter().map(|s| s.as_str()).collect();
8763            file.append_vlen_strings("notes", &refs).unwrap();
8764            file.close().unwrap();
8765        }
8766        {
8767            // Elements 1..4 cross the 2-element chunk boundary twice.
8768            let file = H5File::open_rw(&path).unwrap();
8769            file.dataset_writer("notes")
8770                .unwrap()
8771                .write_vlen_strings_slice(1, &["x", "y", "z"])
8772                .unwrap();
8773            file.close().unwrap();
8774        }
8775        let file = H5File::open(&path).unwrap();
8776        let ds = file.dataset("notes").unwrap();
8777        assert_eq!(ds.shape(), vec![6]);
8778        assert_eq!(
8779            ds.read_vlen_strings().unwrap(),
8780            vec!["v0", "x", "y", "z", "v4", "v5"]
8781        );
8782        std::fs::remove_file(&path).ok();
8783    }
8784
8785    /// Elements the append buffer still holds are not on disk yet; the
8786    /// update flushes them to their chunks first, so the flush at close has
8787    /// nothing left to write the pre-update reference over.
8788    #[test]
8789    fn write_vlen_strings_slice_updates_buffered_elements() {
8790        let path = temp_path("vlen_slice_buffered");
8791        {
8792            let file = H5File::create(&path).unwrap();
8793            file.create_appendable_vlen_dataset("notes", 4, None)
8794                .unwrap();
8795            // 3 of a 4-element chunk: all three stay in the append buffer.
8796            file.append_vlen_strings("notes", &["a", "b", "c"]).unwrap();
8797            file.dataset_writer("notes")
8798                .unwrap()
8799                .write_vlen_strings_slice(1, &["patched"])
8800                .unwrap();
8801            file.close().unwrap();
8802        }
8803        let file = H5File::open(&path).unwrap();
8804        assert_eq!(
8805            file.dataset("notes").unwrap().read_vlen_strings().unwrap(),
8806            vec!["a", "patched", "c"]
8807        );
8808        std::fs::remove_file(&path).ok();
8809    }
8810
8811    /// A range past the end is rejected before anything is written, and an
8812    /// empty batch costs the file nothing — without the early return it would
8813    /// still allocate and write an empty global-heap collection.
8814    #[test]
8815    fn write_vlen_strings_slice_checks_its_range() {
8816        let build = |name: &str, empty_call: bool| {
8817            let path = temp_path(name);
8818            let file = H5File::create(&path).unwrap();
8819            file.write_vlen_strings("notes", &["a", "b"]).unwrap();
8820            let ds = file.dataset_writer("notes").unwrap();
8821            let err = ds
8822                .write_vlen_strings_slice(1, &["x", "y"])
8823                .unwrap_err()
8824                .to_string();
8825            assert!(
8826                err.contains("outside the dataset's 2 elements"),
8827                "got: {err}"
8828            );
8829            if empty_call {
8830                ds.write_vlen_strings_slice(0, &[]).unwrap();
8831            }
8832            file.close().unwrap();
8833            path
8834        };
8835
8836        let with_empty = build("vlen_slice_range", true);
8837        let control = build("vlen_slice_range_control", false);
8838        assert_eq!(
8839            std::fs::metadata(&with_empty).unwrap().len(),
8840            std::fs::metadata(&control).unwrap().len(),
8841            "the rejected and empty calls must leave the file untouched"
8842        );
8843
8844        let file = H5File::open(&with_empty).unwrap();
8845        assert_eq!(
8846            file.dataset("notes").unwrap().read_vlen_strings().unwrap(),
8847            vec!["a", "b"]
8848        );
8849        std::fs::remove_file(&with_empty).ok();
8850        std::fs::remove_file(&control).ok();
8851    }
8852
8853    /// The element offset is one-dimensional, so a multi-dimensional dataset is
8854    /// rejected rather than silently indexed along the first axis.
8855    #[test]
8856    fn write_vlen_strings_slice_rejects_a_multidimensional_dataset() {
8857        let path = temp_path("vlen_slice_2d");
8858        let file = H5File::create(&path).unwrap();
8859        let ds = file
8860            .new_dataset::<u8>()
8861            .datatype(DatatypeMessage::vlen_string_utf8())
8862            .shape([2, 3])
8863            .create("grid")
8864            .unwrap();
8865        let err = ds
8866            .write_vlen_strings_slice(0, &["x"])
8867            .unwrap_err()
8868            .to_string();
8869        assert!(err.contains("1-dimension datasets"), "got: {err}");
8870        file.close().unwrap();
8871        std::fs::remove_file(&path).ok();
8872    }
8873
8874    /// A `&str` is UTF-8, so writing a non-ASCII one into a dataset that
8875    /// declares the ASCII character set would mislabel the bytes.
8876    #[test]
8877    fn write_vlen_strings_slice_enforces_the_ascii_character_set() {
8878        let path = temp_path("vlen_slice_ascii");
8879        let file = H5File::create(&path).unwrap();
8880        let ds = file
8881            .new_dataset::<u8>()
8882            .datatype(DatatypeMessage::vlen_string_ascii())
8883            .shape([3])
8884            .create("notes")
8885            .unwrap();
8886        let err = ds
8887            .write_vlen_strings_slice(0, &["ok", "안녕"])
8888            .unwrap_err()
8889            .to_string();
8890        assert!(
8891            err.contains("string 1") && err.contains("is not ASCII"),
8892            "got: {err}"
8893        );
8894        ds.write_vlen_strings_slice(0, &["ok", "fine"]).unwrap();
8895        file.close().unwrap();
8896        std::fs::remove_file(&path).ok();
8897    }
8898
8899    /// A numeric dataset is rejected: its elements are not vlen references and
8900    /// writing one would corrupt the column.
8901    #[test]
8902    fn write_vlen_strings_slice_rejects_a_non_vlen_dataset() {
8903        let path = temp_path("vlen_slice_numeric");
8904        let file = H5File::create(&path).unwrap();
8905        let ds = file.new_dataset::<i32>().shape([3]).create("nums").unwrap();
8906        ds.write_raw(&[1i32, 2, 3]).unwrap();
8907        let err = ds
8908            .write_vlen_strings_slice(0, &["x"])
8909            .unwrap_err()
8910            .to_string();
8911        assert!(
8912            err.contains("only for variable-length string datasets"),
8913            "got: {err}"
8914        );
8915        file.close().unwrap();
8916        std::fs::remove_file(&path).ok();
8917    }
8918
8919    // ---- superseded global heap objects (libhdf5 H5HG_remove parity) -------
8920
8921    /// Repeatedly replacing the same element must not grow the file per
8922    /// update: the collection each update supersedes is freed and the next
8923    /// update's collection lands in that block. Without the release every
8924    /// update costs another `H5HG_MINALLOC` (4096) bytes.
8925    #[test]
8926    fn write_vlen_strings_slice_reuses_the_freed_heap_block() {
8927        let size_after = |updates: usize| {
8928            let path = temp_path(&format!("vlen_slice_heap_reuse_{updates}"));
8929            let file = H5File::create(&path).unwrap();
8930            file.write_vlen_strings("notes", &["a", "b"]).unwrap();
8931            let ds = file.dataset_writer("notes").unwrap();
8932            for i in 0..updates {
8933                ds.write_vlen_strings_slice(0, &[&format!("update {i}")])
8934                    .unwrap();
8935            }
8936            file.close().unwrap();
8937            let n = std::fs::metadata(&path).unwrap().len();
8938            let read = H5File::open(&path).unwrap();
8939            assert_eq!(
8940                read.dataset("notes").unwrap().read_vlen_strings().unwrap(),
8941                vec![format!("update {}", updates - 1), "b".to_string()]
8942            );
8943            drop(read);
8944            std::fs::remove_file(&path).ok();
8945            n
8946        };
8947
8948        // The allocator settles once a freed block is available to reuse, so
8949        // every count past that produces the same file.
8950        let settled = size_after(3);
8951        assert_eq!(size_after(20), settled, "20 updates against 3");
8952        assert_eq!(size_after(50), settled, "50 updates against 3");
8953    }
8954
8955    /// An empty string is stored as a real heap object under a reference whose
8956    /// sequence length is zero, so the release must go by the address, not the
8957    /// length — a length test strands the object and its collection forever.
8958    #[test]
8959    fn write_vlen_strings_slice_frees_an_empty_strings_object() {
8960        let size_after = |updates: usize| {
8961            let path = temp_path(&format!("vlen_slice_empty_reuse_{updates}"));
8962            let file = H5File::create(&path).unwrap();
8963            file.write_vlen_strings("notes", &["", "b"]).unwrap();
8964            let ds = file.dataset_writer("notes").unwrap();
8965            for _ in 0..updates {
8966                ds.write_vlen_strings_slice(0, &[""]).unwrap();
8967            }
8968            file.close().unwrap();
8969            let n = std::fs::metadata(&path).unwrap().len();
8970            let read = H5File::open(&path).unwrap();
8971            assert_eq!(
8972                read.dataset("notes").unwrap().read_vlen_strings().unwrap(),
8973                vec!["".to_string(), "b".to_string()]
8974            );
8975            drop(read);
8976            std::fs::remove_file(&path).ok();
8977            n
8978        };
8979
8980        let settled = size_after(3);
8981        assert_eq!(size_after(20), settled, "20 empty updates against 3");
8982        assert_eq!(size_after(50), settled, "50 empty updates against 3");
8983    }
8984
8985    /// The elements the update does not name keep their strings, so freeing
8986    /// the superseded objects must not disturb the collection's survivors.
8987    #[test]
8988    fn write_vlen_strings_slice_keeps_the_untouched_strings_readable() {
8989        let path = temp_path("vlen_slice_heap_survivors");
8990        {
8991            let file = H5File::create(&path).unwrap();
8992            file.write_vlen_strings("notes", &["a", "b", "c", "d"])
8993                .unwrap();
8994            let ds = file.dataset_writer("notes").unwrap();
8995            // Two updates inside the one collection the create wrote, so the
8996            // second reads a collection the first already rewrote.
8997            ds.write_vlen_strings_slice(1, &["B"]).unwrap();
8998            ds.write_vlen_strings_slice(3, &["D"]).unwrap();
8999            file.close().unwrap();
9000        }
9001        let file = H5File::open(&path).unwrap();
9002        assert_eq!(
9003            file.dataset("notes").unwrap().read_vlen_strings().unwrap(),
9004            vec!["a", "B", "c", "D"]
9005        );
9006        std::fs::remove_file(&path).ok();
9007    }
9008
9009    /// Replacing every element of a chunked dataset empties the collection the
9010    /// append wrote, and the file must still read back correctly after its
9011    /// block goes to the allocator.
9012    #[test]
9013    fn write_vlen_strings_slice_frees_an_emptied_collection() {
9014        let path = temp_path("vlen_slice_heap_emptied");
9015        {
9016            let file = H5File::create(&path).unwrap();
9017            file.create_appendable_vlen_dataset("notes", 2, None)
9018                .unwrap();
9019            file.append_vlen_strings("notes", &["p", "q", "r", "s"])
9020                .unwrap();
9021            file.close().unwrap();
9022        }
9023        {
9024            let file = H5File::open_rw(&path).unwrap();
9025            file.dataset_writer("notes")
9026                .unwrap()
9027                .write_vlen_strings_slice(0, &["w", "x", "y", "z"])
9028                .unwrap();
9029            file.close().unwrap();
9030        }
9031        let file = H5File::open(&path).unwrap();
9032        assert_eq!(
9033            file.dataset("notes").unwrap().read_vlen_strings().unwrap(),
9034            vec!["w", "x", "y", "z"]
9035        );
9036        std::fs::remove_file(&path).ok();
9037    }
9038
9039    /// A collection larger than the 4096-byte minimum must keep its size when
9040    /// an object leaves it. Re-encoding at the natural size instead shrinks
9041    /// what the header declares, so the block's tail stops being part of the
9042    /// collection and the eventual free returns less than was allocated —
9043    /// stranding the difference on every cycle.
9044    #[test]
9045    fn write_vlen_strings_slice_keeps_an_oversized_collections_block_whole() {
9046        let big = |tag: char| std::iter::repeat_n(tag, 2000).collect::<String>();
9047        let size_after = |cycles: usize| {
9048            let path = temp_path(&format!("vlen_slice_heap_big_{cycles}"));
9049            let file = H5File::create(&path).unwrap();
9050            let seed: Vec<String> = "abcd".chars().map(big).collect();
9051            let refs: Vec<&str> = seed.iter().map(|s| s.as_str()).collect();
9052            // Four 2000-byte strings do not fit the 4096-byte minimum, so this
9053            // is one collection well above it.
9054            file.write_vlen_strings("notes", &refs).unwrap();
9055            let ds = file.dataset_writer("notes").unwrap();
9056            for _ in 0..cycles {
9057                // Partially empty the collection, then finish it off: the
9058                // block is freed only after it has been rewritten once.
9059                let head = big('x');
9060                ds.write_vlen_strings_slice(0, &[&head]).unwrap();
9061                let tail: Vec<String> = "yzw".chars().map(big).collect();
9062                let tail_refs: Vec<&str> = tail.iter().map(|s| s.as_str()).collect();
9063                ds.write_vlen_strings_slice(1, &tail_refs).unwrap();
9064            }
9065            file.close().unwrap();
9066            let n = std::fs::metadata(&path).unwrap().len();
9067            let read = H5File::open(&path).unwrap();
9068            assert_eq!(
9069                read.dataset("notes").unwrap().read_vlen_strings().unwrap(),
9070                vec![big('x'), big('y'), big('z'), big('w')]
9071            );
9072            drop(read);
9073            std::fs::remove_file(&path).ok();
9074            n
9075        };
9076
9077        let settled = size_after(4);
9078        assert_eq!(size_after(30), settled, "30 cycles against 4");
9079    }
9080
9081    /// An element still in the append buffer has never been on disk, so its
9082    /// superseded object has to be found in the buffer or it is stranded.
9083    #[test]
9084    fn write_vlen_strings_slice_releases_a_buffered_elements_object() {
9085        let path = temp_path("vlen_slice_heap_buffered");
9086        let file = H5File::create(&path).unwrap();
9087        file.create_appendable_vlen_dataset("notes", 4, None)
9088            .unwrap();
9089        file.append_vlen_strings("notes", &["a", "b", "c"]).unwrap();
9090        let ds = file.dataset_writer("notes").unwrap();
9091        for i in 0..20 {
9092            ds.write_vlen_strings_slice(1, &[&format!("patch {i}")])
9093                .unwrap();
9094        }
9095        file.close().unwrap();
9096
9097        let file = H5File::open(&path).unwrap();
9098        assert_eq!(
9099            file.dataset("notes").unwrap().read_vlen_strings().unwrap(),
9100            vec!["a", "patch 19", "c"]
9101        );
9102        let size = std::fs::metadata(&path).unwrap().len();
9103        std::fs::remove_file(&path).ok();
9104        assert!(
9105            size < 20 * 4096,
9106            "20 buffered updates left {size} bytes, one collection per update"
9107        );
9108    }
9109
9110    /// Regression: a typed `write_slice` into rows the append buffer still
9111    /// held wrote the chunks, and the flush at close wrote the stale buffered
9112    /// rows back over it — write 99, read 50. The slice now flushes the
9113    /// buffer first, making the chunks the single authority for those rows.
9114    #[test]
9115    fn write_slice_into_the_buffered_tail_survives_close() {
9116        let path = temp_path("slice_into_buffered_tail");
9117        {
9118            let file = H5File::create(&path).unwrap();
9119            let ds = file
9120                .new_dataset::<i32>()
9121                .shape([0])
9122                .chunk(&[4])
9123                .max_shape(&[None])
9124                .create("d")
9125                .unwrap();
9126            // 6 rows: 4 land in chunk 0, rows 4 and 5 stay buffered.
9127            ds.append(&[10, 11, 12, 13, 50, 51]).unwrap();
9128            ds.write_slice(&[4], &[1], &[99]).unwrap();
9129            file.close().unwrap();
9130        }
9131        {
9132            let file = H5File::open(&path).unwrap();
9133            let ds = file.dataset("d").unwrap();
9134            assert_eq!(ds.read_raw::<i32>().unwrap(), vec![10, 11, 12, 13, 99, 51]);
9135        }
9136        std::fs::remove_file(&path).ok();
9137    }
9138
9139    /// Extending a dataset while appends sit in the buffer must not move
9140    /// them: the buffer records the absolute row its frames belong to, so
9141    /// the flush at close lands them there, and the grown region reads as
9142    /// fill.
9143    #[test]
9144    fn extend_does_not_move_buffered_appends() {
9145        let path = temp_path("extend_keeps_buffered_rows");
9146        {
9147            let file = H5File::create(&path).unwrap();
9148            let ds = file
9149                .new_dataset::<i32>()
9150                .shape([0])
9151                .chunk(&[4])
9152                .max_shape(&[None])
9153                .create("d")
9154                .unwrap();
9155            ds.append(&[10, 11, 12, 13, 50, 51]).unwrap(); // rows 4, 5 buffered
9156            ds.extend(&[10]).unwrap();
9157            file.close().unwrap();
9158        }
9159        {
9160            let file = H5File::open(&path).unwrap();
9161            let ds = file.dataset("d").unwrap();
9162            assert_eq!(
9163                ds.read_raw::<i32>().unwrap(),
9164                vec![10, 11, 12, 13, 50, 51, 0, 0, 0, 0]
9165            );
9166        }
9167        std::fs::remove_file(&path).ok();
9168    }
9169
9170    /// Regression: appends to a v2 B-tree indexed dataset (two unlimited
9171    /// dimensions) buffered fine but close() failed "not a chunked dataset"
9172    /// and lost the buffered rows — the append's chunk writes required the
9173    /// extensible-array index. They now go through the index-generic
9174    /// hyperslab engine.
9175    #[test]
9176    fn append_to_a_btree_v2_dataset_survives_close() {
9177        let path = temp_path("append_bt2_close");
9178        {
9179            let file = H5File::create(&path).unwrap();
9180            let ds = file
9181                .new_dataset::<i32>()
9182                .shape([0, 3])
9183                .chunk(&[4, 3])
9184                .max_shape(&[None, None])
9185                .create("d")
9186                .unwrap();
9187            // One buffered row, then a batch that crosses the chunk
9188            // boundary: 4 rows fill chunk band 0, one row stays buffered
9189            // for the flush at close.
9190            ds.append(&[1, 2, 3]).unwrap();
9191            ds.append(&(4..=15).collect::<Vec<i32>>()).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![5, 3]);
9198            assert_eq!(
9199                ds.read_raw::<i32>().unwrap(),
9200                (1..=15).collect::<Vec<i32>>()
9201            );
9202        }
9203        std::fs::remove_file(&path).ok();
9204    }
9205
9206    /// A chunk row narrower than the frame row is legal geometry (libhdf5
9207    /// creates it); appended frames must be scattered across the row's
9208    /// tiles at the chunk stride, not packed at the frame stride.
9209    #[test]
9210    fn append_scatters_frames_across_narrow_chunk_tiles() {
9211        let path = temp_path("append_narrow_chunks");
9212        {
9213            let file = H5File::create(&path).unwrap();
9214            let ds = file
9215                .new_dataset::<i32>()
9216                .shape([0, 8])
9217                .chunk(&[2, 4])
9218                .max_shape(&[None, Some(8)])
9219                .create("d")
9220                .unwrap();
9221            // 3 rows of 8: rows 0..2 complete chunk band 0 (two tiles),
9222            // row 2 is flushed partial at close.
9223            ds.append(&(0..24).collect::<Vec<i32>>()).unwrap();
9224            file.close().unwrap();
9225        }
9226        {
9227            let file = H5File::open(&path).unwrap();
9228            let ds = file.dataset("d").unwrap();
9229            assert_eq!(ds.shape(), vec![3, 8]);
9230            assert_eq!(ds.read_raw::<i32>().unwrap(), (0..24).collect::<Vec<i32>>());
9231        }
9232        std::fs::remove_file(&path).ok();
9233    }
9234
9235    /// A fixed-array dataset has no room to grow: appending must surface an
9236    /// error naming the chunk grid, not lose rows silently. (Before the
9237    /// index-generic append it failed as "not a chunked dataset".)
9238    #[test]
9239    fn append_to_a_full_fixed_array_dataset_errors() {
9240        let path = temp_path("append_fa_errors");
9241        let file = H5File::create(&path).unwrap();
9242        let ds = file
9243            .new_dataset::<i32>()
9244            .shape([4, 3])
9245            .chunk(&[2, 3])
9246            .create("d")
9247            .unwrap();
9248        let err = ds.append(&(0..6).collect::<Vec<i32>>()).unwrap_err();
9249        assert!(
9250            err.to_string().contains("chunk grid"),
9251            "unexpected error: {err}"
9252        );
9253        file.close().unwrap();
9254        std::fs::remove_file(&path).ok();
9255    }
9256
9257    /// A finite max_shape above the current shape used to be dropped on the
9258    /// fixed-array path: the array was sized from the current dims and the
9259    /// stored dataspace had no maximum, so growth failed. The array is now
9260    /// sized from the maximum's chunk grid (libhdf5 `max_nchunks`), so a
9261    /// fixed-max dataset appends up to its maximum and roundtrips.
9262    #[test]
9263    fn fixed_array_with_a_larger_max_shape_grows_and_survives_close() {
9264        let path = temp_path("fa_growable_dim0");
9265        {
9266            let file = H5File::create(&path).unwrap();
9267            let ds = file
9268                .new_dataset::<i32>()
9269                .shape([4, 3])
9270                .chunk(&[2, 3])
9271                .max_shape(&[Some(10), Some(3)])
9272                .create("d")
9273                .unwrap();
9274            ds.write_raw(&(0..12).collect::<Vec<i32>>()).unwrap();
9275            ds.append(&(12..18).collect::<Vec<i32>>()).unwrap();
9276            file.close().unwrap();
9277        }
9278        {
9279            let file = H5File::open(&path).unwrap();
9280            let ds = file.dataset("d").unwrap();
9281            assert_eq!(ds.shape(), vec![6, 3]);
9282            assert_eq!(ds.read_raw::<i32>().unwrap(), (0..18).collect::<Vec<i32>>());
9283        }
9284        std::fs::remove_file(&path).ok();
9285    }
9286
9287    /// The multiplier-dimension boundary: growing a dimension other than 0
9288    /// changes the current chunk grid but not the index grid. Chunk slots
9289    /// must come from the maximum's grid (libhdf5 `max_down_chunks`), or the
9290    /// chunks written before the extend are looked up under different
9291    /// indices after it.
9292    #[test]
9293    fn fixed_array_growable_inner_dimension_keeps_chunk_slots() {
9294        let path = temp_path("fa_growable_dim1");
9295        {
9296            let file = H5File::create(&path).unwrap();
9297            let ds = file
9298                .new_dataset::<i32>()
9299                .shape([4, 3])
9300                .chunk(&[2, 3])
9301                .max_shape(&[Some(4), Some(9)])
9302                .create("d")
9303                .unwrap();
9304            ds.write_raw(&(0..12).collect::<Vec<i32>>()).unwrap();
9305            ds.extend(&[4, 6]).unwrap();
9306            ds.write_slice(&[0, 3], &[4, 3], &(12..24).collect::<Vec<i32>>())
9307                .unwrap();
9308            file.close().unwrap();
9309        }
9310        {
9311            let file = H5File::open(&path).unwrap();
9312            let ds = file.dataset("d").unwrap();
9313            assert_eq!(ds.shape(), vec![4, 6]);
9314            // Row-major [4,6]: row r is [r*3 .. r*3+3) from the first write
9315            // then [12 + r*3 ..) from the second.
9316            let mut expect = Vec::new();
9317            for r in 0i32..4 {
9318                expect.extend((r * 3)..(r * 3 + 3));
9319                expect.extend((12 + r * 3)..(12 + r * 3 + 3));
9320            }
9321            assert_eq!(ds.read_raw::<i32>().unwrap(), expect);
9322        }
9323        std::fs::remove_file(&path).ok();
9324    }
9325
9326    /// Growth boundaries: past the stored maximum is rejected, and a dataset
9327    /// without a stored maximum is fixed at its extent (libhdf5 defaults
9328    /// maxdims to dims at creation).
9329    #[test]
9330    fn extend_beyond_the_maximum_is_rejected() {
9331        let path = temp_path("extend_beyond_max");
9332        let file = H5File::create(&path).unwrap();
9333        let ds = file
9334            .new_dataset::<i32>()
9335            .shape([4, 3])
9336            .chunk(&[2, 3])
9337            .max_shape(&[Some(6), Some(3)])
9338            .create("d")
9339            .unwrap();
9340        ds.extend(&[6, 3]).unwrap();
9341        let err = ds.extend(&[8, 3]).unwrap_err();
9342        assert!(
9343            err.to_string().contains("exceeds the maximum"),
9344            "unexpected error: {err}"
9345        );
9346        file.close().unwrap();
9347        std::fs::remove_file(&path).ok();
9348    }
9349
9350    /// An unlimited dimension other than 0 has no fixed linear slot without
9351    /// libhdf5's extensible-array swizzling; `chunk_grid::linear_index` now
9352    /// implements that swizzle for any dimension, so this creates cleanly
9353    /// and every extend keeps writing new chunks to new slots, never
9354    /// re-addressing one already on disk.
9355    #[test]
9356    fn builder_accepts_an_unlimited_inner_dimension() {
9357        let path = temp_path("unlimited_inner_dim");
9358        let file = H5File::create(&path).unwrap();
9359        let ds = file
9360            .new_dataset::<i32>()
9361            .shape([4, 0])
9362            .chunk(&[2, 2])
9363            .max_shape(&[Some(4), None])
9364            .create("d")
9365            .unwrap();
9366        assert_eq!(ds.shape(), vec![4, 0]);
9367
9368        // Write, then extend and write again: if the linear index were
9369        // recomputed from the *current* extent instead of the maximum one,
9370        // the second extend would shift every slot number and the first
9371        // write's chunks would decode under the wrong coordinates below.
9372        ds.extend(&[4, 2]).unwrap();
9373        ds.write_slice(&[0, 0], &[4, 2], &[1, 2, 3, 4, 5, 6, 7, 8])
9374            .unwrap();
9375        ds.extend(&[4, 4]).unwrap();
9376        ds.write_slice(&[0, 2], &[4, 2], &[9, 10, 11, 12, 13, 14, 15, 16])
9377            .unwrap();
9378
9379        file.close().unwrap();
9380        let file = H5File::open(&path).unwrap();
9381        let ds = file.dataset("d").unwrap();
9382        assert_eq!(
9383            ds.read_slice::<i32>(&[0, 0], &[4, 4]).unwrap(),
9384            vec![1, 2, 9, 10, 3, 4, 11, 12, 5, 6, 13, 14, 7, 8, 15, 16]
9385        );
9386        std::fs::remove_file(&path).ok();
9387    }
9388
9389    /// Regression: a chunk wider than a fixed max dimension used to be
9390    /// accepted, and appends then packed rows at the chunk stride — writing
9391    /// [1, 2, 3, 4] and reading back [1, 2, 0, 0]. libhdf5 rejects the
9392    /// geometry at create (`H5D__chunk_construct`); so do we now.
9393    #[test]
9394    fn builder_rejects_a_chunk_wider_than_a_fixed_max_dimension() {
9395        let path = temp_path("builder_chunk_wider_than_max");
9396        let file = H5File::create(&path).unwrap();
9397        let err = match file
9398            .new_dataset::<i32>()
9399            .shape([0, 2])
9400            .chunk(&[2, 4])
9401            .max_shape(&[None, Some(2)])
9402            .create("v5")
9403        {
9404            Ok(_) => panic!("create accepted a chunk wider than the fixed max dimension"),
9405            Err(e) => e,
9406        };
9407        assert!(
9408            err.to_string().contains("maximum dimension size"),
9409            "unexpected error: {err}"
9410        );
9411        file.close().unwrap();
9412        std::fs::remove_file(&path).ok();
9413    }
9414
9415    /// Boundary: `fits in T` vs `does not fit in T`, for both the too-large
9416    /// (u64::MAX → i64) and the negative-to-unsigned (−1 → u32) directions.
9417    #[test]
9418    fn numeric_int_checked_conversion_boundaries() {
9419        let path = temp_path("numeric_int_bounds");
9420        {
9421            let file = H5File::create(&path).unwrap();
9422            let ds = file.new_dataset::<u64>().shape([2]).create("u").unwrap();
9423            ds.write_raw(&[1u64, u64::MAX]).unwrap();
9424            let ds = file.new_dataset::<i32>().shape([2]).create("i").unwrap();
9425            ds.write_raw(&[-1i32, 5]).unwrap();
9426            file.close().unwrap();
9427        }
9428        let file = H5File::open(&path).unwrap();
9429
9430        let u = file.dataset("u").unwrap();
9431        assert_eq!(u.read_numeric_as::<u64>().unwrap(), vec![1, u64::MAX]);
9432        assert_eq!(
9433            u.read_numeric_as::<i128>().unwrap(),
9434            vec![1, i128::from(u64::MAX)]
9435        );
9436        let err = u.read_numeric_as::<i64>().unwrap_err();
9437        assert!(
9438            err.to_string()
9439                .contains("value 18446744073709551615 at element 1 does not fit in i64"),
9440            "unexpected error: {err}"
9441        );
9442
9443        let i = file.dataset("i").unwrap();
9444        assert_eq!(i.read_numeric_as::<i64>().unwrap(), vec![-1, 5]);
9445        let err = i.read_numeric_as::<u32>().unwrap_err();
9446        assert!(
9447            err.to_string()
9448                .contains("value -1 at element 0 does not fit in u32"),
9449            "unexpected error: {err}"
9450        );
9451        std::fs::remove_file(&path).ok();
9452    }
9453
9454    /// Boundary: f32 → f64 is exact widening; f64 → f32 is rejected.
9455    #[test]
9456    fn numeric_float_widening_only() {
9457        let path = temp_path("numeric_float");
9458        {
9459            let file = H5File::create(&path).unwrap();
9460            let ds = file.new_dataset::<f32>().shape([2]).create("f4").unwrap();
9461            ds.write_raw(&[1.5f32, -2.25]).unwrap();
9462            let ds = file.new_dataset::<f64>().shape([1]).create("f8").unwrap();
9463            ds.write_raw(&[3.75f64]).unwrap();
9464            file.close().unwrap();
9465        }
9466        let file = H5File::open(&path).unwrap();
9467
9468        let f4 = file.dataset("f4").unwrap();
9469        assert_eq!(f4.read_numeric_as::<f32>().unwrap(), vec![1.5, -2.25]);
9470        assert_eq!(f4.read_numeric_as::<f64>().unwrap(), vec![1.5, -2.25]);
9471
9472        let f8 = file.dataset("f8").unwrap();
9473        assert_eq!(f8.read_numeric_as::<f64>().unwrap(), vec![3.75]);
9474        let err = f8.read_numeric_as::<f32>().unwrap_err();
9475        assert!(
9476            err.to_string().contains("narrowing"),
9477            "unexpected error: {err}"
9478        );
9479        std::fs::remove_file(&path).ok();
9480    }
9481
9482    /// Boundary: cross-class conversions (float ↔ integer) are rejected in
9483    /// both directions, and a non-numeric datatype is rejected at classify.
9484    #[test]
9485    fn numeric_cross_class_and_non_numeric_rejected() {
9486        let path = temp_path("numeric_cross_class");
9487        {
9488            let file = H5File::create(&path).unwrap();
9489            let ds = file.new_dataset::<f64>().shape([1]).create("f8").unwrap();
9490            ds.write_raw(&[1.0f64]).unwrap();
9491            let ds = file.new_dataset::<i32>().shape([1]).create("i4").unwrap();
9492            ds.write_raw(&[7i32]).unwrap();
9493            file.write_vlen_strings("s", &["a", "b"]).unwrap();
9494            file.close().unwrap();
9495        }
9496        let file = H5File::open(&path).unwrap();
9497
9498        let err = file
9499            .dataset("f8")
9500            .unwrap()
9501            .read_numeric_as::<i64>()
9502            .unwrap_err();
9503        assert!(
9504            err.to_string().contains("floating-point dataset as i64"),
9505            "unexpected error: {err}"
9506        );
9507        let err = file
9508            .dataset("i4")
9509            .unwrap()
9510            .read_numeric_as::<f64>()
9511            .unwrap_err();
9512        assert!(
9513            err.to_string().contains("integer dataset as f64"),
9514            "unexpected error: {err}"
9515        );
9516        let err = file
9517            .dataset("s")
9518            .unwrap()
9519            .read_numeric_as::<i64>()
9520            .unwrap_err();
9521        assert!(
9522            err.to_string().contains("is not numeric"),
9523            "unexpected error: {err}"
9524        );
9525        std::fs::remove_file(&path).ok();
9526    }
9527
9528    /// Boundary: big-endian sources decode per the datatype's byte order.
9529    /// Unit-level (the writer only emits little-endian): feed `convert` a
9530    /// big-endian datatype plus big-endian bytes directly.
9531    #[test]
9532    fn numeric_big_endian_decode() {
9533        use super::numeric;
9534        use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
9535
9536        let dt = DatatypeMessage::FixedPoint {
9537            size: 4,
9538            byte_order: ByteOrder::BigEndian,
9539            signed: true,
9540            bit_offset: 0,
9541            bit_precision: 32,
9542        };
9543        let mut raw = Vec::new();
9544        raw.extend_from_slice(&(-2i32).to_be_bytes());
9545        raw.extend_from_slice(&(100_000i32).to_be_bytes());
9546        let kind = numeric::classify(&dt).unwrap();
9547        assert_eq!(
9548            numeric::convert::<i64>(kind, &raw).unwrap(),
9549            vec![-2, 100_000]
9550        );
9551
9552        let dt = DatatypeMessage::FloatingPoint {
9553            size: 8,
9554            byte_order: ByteOrder::BigEndian,
9555            sign_location: 63,
9556            bit_offset: 0,
9557            bit_precision: 64,
9558            exponent_location: 52,
9559            exponent_size: 11,
9560            mantissa_location: 0,
9561            mantissa_size: 52,
9562            exponent_bias: 1023,
9563        };
9564        let raw = (-2.25f64).to_be_bytes();
9565        let kind = numeric::classify(&dt).unwrap();
9566        assert_eq!(numeric::convert::<f64>(kind, &raw).unwrap(), vec![-2.25]);
9567    }
9568
9569    /// `H5Attribute::read_numeric` validates the stored datatype before
9570    /// reinterpreting bytes: cross-width, cross-class, and non-numeric
9571    /// attributes error instead of returning bit-garbage, while the exact
9572    /// type and the HBool / complex-compound paths keep working.
9573    #[test]
9574    fn attr_read_numeric_validates_datatype() {
9575        use crate::types::{Complex64, HBool, VarLenUnicode};
9576        let path = temp_path("attr_read_numeric_validate");
9577        {
9578            let file = H5File::create(&path).unwrap();
9579            let ds = file.new_dataset::<f32>().shape([2]).create("d").unwrap();
9580            ds.write_raw(&[1.0f32; 2]).unwrap();
9581            let a = ds.new_attr::<f64>().shape(()).create("f8").unwrap();
9582            a.write_numeric(&1.5f64).unwrap();
9583            let a = ds.new_attr::<i32>().shape(()).create("i4").unwrap();
9584            a.write_numeric(&-7i32).unwrap();
9585            let a = ds.new_attr::<HBool>().shape(()).create("b").unwrap();
9586            a.write_numeric(&HBool::from(true)).unwrap();
9587            let a = ds.new_attr::<Complex64>().shape(()).create("z").unwrap();
9588            a.write_numeric(&Complex64 { re: 1.0, im: -2.0 }).unwrap();
9589            let a = ds
9590                .new_attr::<VarLenUnicode>()
9591                .shape(())
9592                .create("s")
9593                .unwrap();
9594            a.write_scalar(&VarLenUnicode("text".into())).unwrap();
9595            file.close().unwrap();
9596        }
9597        let file = H5File::open(&path).unwrap();
9598        let ds = file.dataset("d").unwrap();
9599
9600        let f8 = ds.attr("f8").unwrap();
9601        assert_eq!(f8.read_numeric::<f64>().unwrap(), 1.5);
9602        // Previously returned the low half of the f64 image as an f32.
9603        let err = f8.read_numeric::<f32>().unwrap_err();
9604        assert!(
9605            err.to_string().contains("read_numeric_as"),
9606            "unexpected error: {err}"
9607        );
9608        assert!(f8.read_numeric::<i64>().is_err());
9609
9610        let i4 = ds.attr("i4").unwrap();
9611        assert_eq!(i4.read_numeric::<i32>().unwrap(), -7);
9612        assert!(i4.read_numeric::<u32>().is_err());
9613
9614        assert!(bool::from(
9615            ds.attr("b").unwrap().read_numeric::<HBool>().unwrap()
9616        ));
9617        let z = ds.attr("z").unwrap().read_numeric::<Complex64>().unwrap();
9618        assert_eq!((z.re, z.im), (1.0, -2.0));
9619
9620        // A vlen string attribute: read_numeric used to transmute the heap
9621        // reference bytes into the requested type.
9622        let s = ds.attr("s").unwrap();
9623        assert!(s.read_numeric::<f64>().is_err());
9624        assert!(s.read_numeric_as::<f64>().is_err());
9625        std::fs::remove_file(&path).ok();
9626    }
9627
9628    /// The attribute conversion read applies the dataset rules: checked
9629    /// int → int naming index and value on overflow, widening-only floats,
9630    /// cross-class rejected; an array attribute converts every element.
9631    #[test]
9632    fn attr_read_numeric_as_converts() {
9633        let path = temp_path("attr_read_numeric_as");
9634        {
9635            let file = H5File::create(&path).unwrap();
9636            let ds = file.new_dataset::<f32>().shape([2]).create("d").unwrap();
9637            ds.write_raw(&[1.0f32; 2]).unwrap();
9638            let a = ds.new_attr::<i32>().shape(()).create("i4").unwrap();
9639            a.write_numeric(&-7i32).unwrap();
9640            let a = ds.new_attr::<u64>().shape(()).create("u8max").unwrap();
9641            a.write_numeric(&u64::MAX).unwrap();
9642            let a = ds.new_attr::<i16>().shape([3]).create("arr").unwrap();
9643            a.write_array(&[1i16, -2, 3]).unwrap();
9644            file.close().unwrap();
9645        }
9646        let file = H5File::open(&path).unwrap();
9647        let ds = file.dataset("d").unwrap();
9648        assert_eq!(
9649            ds.attr("i4").unwrap().read_numeric_as::<i64>().unwrap(),
9650            vec![-7]
9651        );
9652        let err = ds
9653            .attr("u8max")
9654            .unwrap()
9655            .read_numeric_as::<i64>()
9656            .unwrap_err();
9657        assert!(
9658            err.to_string().contains("does not fit in i64"),
9659            "unexpected error: {err}"
9660        );
9661        assert_eq!(
9662            ds.attr("arr").unwrap().read_numeric_as::<i32>().unwrap(),
9663            vec![1, -2, 3]
9664        );
9665        assert!(ds.attr("i4").unwrap().read_numeric_as::<f64>().is_err());
9666        std::fs::remove_file(&path).ok();
9667    }
9668
9669    /// The hyperslab variant applies the same conversion to a sub-selection.
9670    #[test]
9671    fn numeric_slice_conversion() {
9672        let path = temp_path("numeric_slice");
9673        {
9674            let file = H5File::create(&path).unwrap();
9675            let ds = file.new_dataset::<i16>().shape([2, 3]).create("m").unwrap();
9676            ds.write_raw(&[1i16, 2, 3, 4, 5, 6]).unwrap();
9677            file.close().unwrap();
9678        }
9679        let file = H5File::open(&path).unwrap();
9680        let m = file.dataset("m").unwrap();
9681        assert_eq!(
9682            m.read_numeric_slice_as::<i32>(&[0, 1], &[2, 2]).unwrap(),
9683            vec![2, 3, 5, 6]
9684        );
9685        std::fs::remove_file(&path).ok();
9686    }
9687
9688    /// A NULL dataspace round-trips through the public API as `is_null() ==
9689    /// true`, `shape() == []`, and `read_raw_bytes()` empty — and stays
9690    /// distinguishable from a scalar dataset, which shares the same empty
9691    /// `shape()` but holds exactly one element.
9692    #[test]
9693    fn null_dataspace_distinct_from_scalar() {
9694        let path = temp_path("null_vs_scalar");
9695        {
9696            let file = H5File::create(&path).unwrap();
9697            file.new_dataset::<i32>().null().create("empty").unwrap();
9698            let scalar = file.new_dataset::<i32>().scalar().create("scalar").unwrap();
9699            scalar.write_raw(&[42i32]).unwrap();
9700            file.close().unwrap();
9701        }
9702        let file = H5File::open(&path).unwrap();
9703
9704        let empty = file.dataset("empty").unwrap();
9705        assert!(empty.is_null());
9706        assert_eq!(empty.shape(), Vec::<usize>::new());
9707        assert_eq!(empty.total_elements(), 0);
9708        assert_eq!(empty.read_raw_bytes().unwrap(), Vec::<u8>::new());
9709
9710        let scalar = file.dataset("scalar").unwrap();
9711        assert!(!scalar.is_null());
9712        assert_eq!(scalar.shape(), Vec::<usize>::new());
9713        assert_eq!(scalar.total_elements(), 1);
9714        assert_eq!(scalar.read_raw::<i32>().unwrap(), vec![42]);
9715
9716        std::fs::remove_file(&path).ok();
9717    }
9718
9719    /// A NULL dataspace dataset rejects writes outright — there is nothing
9720    /// to write into — rather than silently accepting a scalar-shaped
9721    /// write against unallocated storage.
9722    #[test]
9723    fn null_dataspace_rejects_writes() {
9724        let path = temp_path("null_write_rejected");
9725        let file = H5File::create(&path).unwrap();
9726        let ds = file.new_dataset::<i32>().null().create("empty").unwrap();
9727        assert!(ds.write_raw(&[1i32]).is_err());
9728        assert!(ds.write_raw_bytes(&[0u8; 4]).is_err());
9729        file.close().unwrap();
9730        std::fs::remove_file(&path).ok();
9731    }
9732
9733    /// `.null()` combined with `.chunk()` or a fill value is rejected at
9734    /// `create()` rather than silently dropping the conflicting option —
9735    /// a NULL dataspace can never be chunked or filtered upstream.
9736    #[test]
9737    fn null_dataspace_rejects_chunking_and_fill_value() {
9738        let path = temp_path("null_chunk_rejected");
9739        let file = H5File::create(&path).unwrap();
9740        assert!(file
9741            .new_dataset::<i32>()
9742            .null()
9743            .chunk(&[4])
9744            .create("a")
9745            .is_err());
9746        assert!(file
9747            .new_dataset::<i32>()
9748            .null()
9749            .fill_value(7i32)
9750            .create("b")
9751            .is_err());
9752        file.close().unwrap();
9753        std::fs::remove_file(&path).ok();
9754    }
9755
9756    /// A committed type is resolved before the dataset is created, so a name
9757    /// that is not one — or one paired with object references, which would
9758    /// make the stored type disagree with the payload — leaves no dataset
9759    /// behind.
9760    #[test]
9761    fn a_committed_type_that_cannot_be_shared_creates_no_dataset() {
9762        use crate::format::messages::datatype::DatatypeMessage;
9763
9764        let path = temp_path("committed_refused");
9765        let file = H5File::create(&path).unwrap();
9766        file.commit_datatype("t", DatatypeMessage::i32_type())
9767            .unwrap();
9768
9769        assert!(file
9770            .new_dataset::<i32>()
9771            .committed_type("absent")
9772            .shape([2usize])
9773            .create("a")
9774            .is_err());
9775        assert!(file
9776            .new_dataset::<u64>()
9777            .committed_type("t")
9778            .object_references()
9779            .shape([2usize])
9780            .create("b")
9781            .is_err());
9782        // A dataset already exists under that name, so the type cannot take
9783        // it either.
9784        file.new_dataset::<i32>()
9785            .shape([2usize])
9786            .create("taken")
9787            .unwrap();
9788        assert!(file
9789            .commit_datatype("taken", DatatypeMessage::i32_type())
9790            .is_err());
9791        assert!(file
9792            .commit_datatype("t", DatatypeMessage::f64_type())
9793            .is_err());
9794
9795        assert_eq!(file.dataset_names(), vec!["taken".to_string()]);
9796        assert_eq!(file.named_datatype_names(), vec!["t".to_string()]);
9797        file.close().unwrap();
9798        std::fs::remove_file(&path).ok();
9799    }
9800
9801    /// Deleting the group that named a committed datatype takes the name with
9802    /// it: nothing in the file reaches the type, so it is not written, the
9803    /// name is free again, and it can no longer be shared by that name.
9804    #[test]
9805    fn deleting_a_group_takes_the_committed_datatypes_it_named() {
9806        use crate::format::messages::datatype::DatatypeMessage;
9807
9808        let path = temp_path("committed_group_deleted");
9809        {
9810            let file = H5File::create(&path).unwrap();
9811            let types = file.create_group("types").unwrap();
9812            types
9813                .commit_datatype("t", DatatypeMessage::i32_type())
9814                .unwrap();
9815            assert_eq!(file.named_datatype_names(), vec!["types/t".to_string()]);
9816
9817            file.delete_group("types").unwrap();
9818            assert!(file.named_datatype_names().is_empty());
9819            assert!(file
9820                .new_dataset::<i32>()
9821                .committed_type("types/t")
9822                .shape([2usize])
9823                .create("d")
9824                .is_err());
9825
9826            // The name is free, so a new group may take it back.
9827            let types = file.create_group("types").unwrap();
9828            types
9829                .commit_datatype("t", DatatypeMessage::f64_type())
9830                .unwrap();
9831            file.close().unwrap();
9832        }
9833        let file = H5File::open(&path).unwrap();
9834        assert_eq!(file.named_datatype_names(), vec!["types/t".to_string()]);
9835        assert_eq!(
9836            file.named_datatype("types/t").unwrap().datatype().unwrap(),
9837            crate::format::messages::datatype::DatatypeMessage::f64_type()
9838        );
9839        drop(file);
9840        std::fs::remove_file(&path).ok();
9841    }
9842
9843    /// The layout class the reader sees for `name`, plus the image a compact
9844    /// layout carries — what distinguishes compact storage from contiguous
9845    /// storage that happens to hold the same bytes.
9846    fn compact_image(path: &std::path::Path, name: &str) -> Option<Vec<u8>> {
9847        use crate::format::messages::data_layout::DataLayoutMessage;
9848        let mut reader = crate::io::reader::Hdf5Reader::open(path).unwrap();
9849        match &reader.dataset_info(name).unwrap().layout {
9850            DataLayoutMessage::Compact { data } => Some(data.clone()),
9851            other => panic!("{name}: expected a compact layout, got {other:?}"),
9852        }
9853    }
9854
9855    /// `.compact()` puts the raw data inside the data layout message: the
9856    /// dataset has no data block of its own, and the image the layout carries
9857    /// is what a read returns.
9858    #[test]
9859    fn a_compact_dataset_stores_its_data_in_the_layout_message() {
9860        let path = temp_path("compact_roundtrip");
9861        let values: Vec<i32> = (0..16).collect();
9862        {
9863            let file = H5File::create(&path).unwrap();
9864            file.new_dataset::<i32>()
9865                .shape([16usize])
9866                .compact()
9867                .create("d")
9868                .unwrap()
9869                .write_raw(&values)
9870                .unwrap();
9871            file.close().unwrap();
9872        }
9873
9874        let image = compact_image(&path, "d").unwrap();
9875        assert_eq!(
9876            image,
9877            values
9878                .iter()
9879                .flat_map(|v| v.to_le_bytes())
9880                .collect::<Vec<_>>()
9881        );
9882
9883        let file = H5File::open(&path).unwrap();
9884        let ds = file.dataset("d").unwrap();
9885        assert_eq!(ds.shape(), vec![16]);
9886        assert_eq!(ds.read_raw::<i32>().unwrap(), values);
9887        std::fs::remove_file(&path).ok();
9888    }
9889
9890    /// A compact dataset created inside a group is linked from that group,
9891    /// not from the root: its create path goes through the same parent
9892    /// resolution every other layout uses.
9893    #[test]
9894    fn a_compact_dataset_lands_in_its_group() {
9895        let path = temp_path("compact_in_group");
9896        {
9897            let file = H5File::create(&path).unwrap();
9898            let g = file.root_group().create_group("g").unwrap();
9899            g.new_dataset::<u8>()
9900                .shape([4usize])
9901                .compact()
9902                .create("d")
9903                .unwrap()
9904                .write_raw(&[1u8, 2, 3, 4])
9905                .unwrap();
9906            file.close().unwrap();
9907        }
9908        assert_eq!(compact_image(&path, "/g/d").unwrap(), vec![1u8, 2, 3, 4]);
9909
9910        let file = H5File::open(&path).unwrap();
9911        assert_eq!(
9912            file.dataset("/g/d").unwrap().read_raw::<u8>().unwrap(),
9913            vec![1u8, 2, 3, 4]
9914        );
9915        std::fs::remove_file(&path).ok();
9916    }
9917
9918    /// A compact dataset's storage is the image itself, so the fill value has
9919    /// to be tiled into it at create — `H5D__compact_fill`'s job. An unwritten
9920    /// element must read back as the fill value, not as zero.
9921    #[test]
9922    fn an_unwritten_compact_dataset_reads_back_as_its_fill_value() {
9923        let path = temp_path("compact_fill");
9924        {
9925            let file = H5File::create(&path).unwrap();
9926            file.new_dataset::<i32>()
9927                .shape([4usize])
9928                .compact()
9929                .fill_value(-7i32)
9930                .create("d")
9931                .unwrap();
9932            file.close().unwrap();
9933        }
9934        let file = H5File::open(&path).unwrap();
9935        assert_eq!(
9936            file.dataset("d").unwrap().read_raw::<i32>().unwrap(),
9937            vec![-7i32; 4]
9938        );
9939        std::fs::remove_file(&path).ok();
9940    }
9941
9942    /// The image is the layout message, so anything that rewrites the header
9943    /// rewrites the data with it. Reopening and attaching an attribute makes
9944    /// the header stale; the rebuilt one must still carry the image rather
9945    /// than fall back to an unallocated contiguous layout.
9946    #[test]
9947    fn a_reopened_compact_dataset_keeps_its_image() {
9948        let path = temp_path("compact_reopen");
9949        let values: Vec<i32> = (100..108).collect();
9950        {
9951            let file = H5File::create(&path).unwrap();
9952            file.new_dataset::<i32>()
9953                .shape([8usize])
9954                .compact()
9955                .create("d")
9956                .unwrap()
9957                .write_raw(&values)
9958                .unwrap();
9959            file.close().unwrap();
9960        }
9961        {
9962            let file = H5File::open_rw(&path).unwrap();
9963            file.dataset_writer("d")
9964                .unwrap()
9965                .new_attr::<i32>()
9966                .shape(())
9967                .create("note")
9968                .unwrap()
9969                .write_numeric(&1i32)
9970                .unwrap();
9971            file.close().unwrap();
9972        }
9973        assert_eq!(
9974            compact_image(&path, "d").unwrap(),
9975            values
9976                .iter()
9977                .flat_map(|v| v.to_le_bytes())
9978                .collect::<Vec<_>>()
9979        );
9980
9981        let file = H5File::open(&path).unwrap();
9982        assert_eq!(
9983            file.dataset("d").unwrap().read_raw::<i32>().unwrap(),
9984            values
9985        );
9986        std::fs::remove_file(&path).ok();
9987    }
9988
9989    /// The ceiling is what a data layout message can hold, so it is checked
9990    /// in bytes and names them: the largest image that fits is accepted and
9991    /// one element more is refused.
9992    #[test]
9993    fn the_compact_ceiling_is_checked_in_bytes() {
9994        let path = temp_path("compact_ceiling");
9995        let file = H5File::create(&path).unwrap();
9996
9997        let fits = crate::MAX_COMPACT_DATA / 4;
9998        file.new_dataset::<i32>()
9999            .shape([fits])
10000            .compact()
10001            .create("fits")
10002            .unwrap();
10003
10004        let over = fits + 1;
10005        let err = match file
10006            .new_dataset::<i32>()
10007            .shape([over])
10008            .compact()
10009            .create("over")
10010        {
10011            Err(e) => e.to_string(),
10012            Ok(_) => panic!("an image {} bytes wide must be refused", over * 4),
10013        };
10014        assert!(
10015            err.contains(&(over * 4).to_string())
10016                && err.contains(&crate::MAX_COMPACT_DATA.to_string()),
10017            "the error must name both sizes: {err}"
10018        );
10019
10020        file.close().unwrap();
10021        std::fs::remove_file(&path).ok();
10022    }
10023
10024    /// Compact storage has no chunk grid to filter and no room to grow, so
10025    /// each conflicting option is refused at `create()` rather than silently
10026    /// overriding the layout the way `H5Pset_chunk` does.
10027    #[test]
10028    fn compact_rejects_chunking_filters_growth_and_a_null_dataspace() {
10029        let path = temp_path("compact_rejects");
10030        let file = H5File::create(&path).unwrap();
10031        assert!(file
10032            .new_dataset::<i32>()
10033            .shape([4usize])
10034            .compact()
10035            .chunk(&[4])
10036            .create("a")
10037            .is_err());
10038        assert!(file
10039            .new_dataset::<i32>()
10040            .shape([4usize])
10041            .compact()
10042            .deflate(4)
10043            .create("b")
10044            .is_err());
10045        assert!(file
10046            .new_dataset::<i32>()
10047            .shape([4usize])
10048            .compact()
10049            .max_shape(&[None])
10050            .create("c")
10051            .is_err());
10052        assert!(file
10053            .new_dataset::<i32>()
10054            .shape([4usize])
10055            .compact()
10056            .max_shape(&[Some(8)])
10057            .create("d")
10058            .is_err());
10059        assert!(file
10060            .new_dataset::<i32>()
10061            .null()
10062            .compact()
10063            .create("e")
10064            .is_err());
10065        file.close().unwrap();
10066        std::fs::remove_file(&path).ok();
10067    }
10068
10069    /// The filter pipeline the reader decodes from `name`'s header.
10070    fn stored_pipeline(
10071        path: &std::path::Path,
10072        name: &str,
10073    ) -> crate::format::messages::filter::FilterPipeline {
10074        let mut reader = crate::io::reader::Hdf5Reader::open(path).unwrap();
10075        reader
10076            .dataset_info(name)
10077            .unwrap()
10078            .filter_pipeline
10079            .clone()
10080            .unwrap_or_else(|| panic!("{name}: no filter pipeline"))
10081    }
10082
10083    /// Shuffle is a permutation, not a compressor, so it is a pipeline on its
10084    /// own — `H5Pset_shuffle` with nothing behind it. Its stage must reach the
10085    /// header, and the reader must unpermute what it wrote.
10086    #[test]
10087    fn shuffle_alone_is_a_filter_pipeline() {
10088        use crate::format::messages::filter::{FilterPipeline, FILTER_SHUFFLE};
10089        let path = temp_path("shuffle_alone");
10090        let values: Vec<i32> = (0..64).collect();
10091        {
10092            let file = H5File::create(&path).unwrap();
10093            file.new_dataset::<i32>()
10094                .shape([64usize])
10095                .chunk(&[16])
10096                .shuffle()
10097                .create("d")
10098                .unwrap()
10099                .write_raw(&values)
10100                .unwrap();
10101            file.close().unwrap();
10102        }
10103        let pipeline = stored_pipeline(&path, "d");
10104        assert_eq!(pipeline, FilterPipeline::shuffle(4));
10105        assert_eq!(pipeline.filters[0].id, FILTER_SHUFFLE);
10106
10107        let file = H5File::open(&path).unwrap();
10108        assert_eq!(
10109            file.dataset("d").unwrap().read_raw::<i32>().unwrap(),
10110            values
10111        );
10112        std::fs::remove_file(&path).ok();
10113    }
10114
10115    /// `.shuffle()` and `.deflate()` are separate stages that compose, and
10116    /// `.shuffle_deflate()` is the shorthand for both — one pipeline, built
10117    /// once, whichever way it was asked for.
10118    #[cfg(feature = "deflate")]
10119    #[test]
10120    fn shuffle_composes_with_deflate() {
10121        let path = temp_path("shuffle_then_deflate");
10122        let values: Vec<i32> = (0..64).collect();
10123        {
10124            let file = H5File::create(&path).unwrap();
10125            for (name, ds) in [
10126                ("split", file.new_dataset::<i32>().shuffle().deflate(6)),
10127                ("combined", file.new_dataset::<i32>().shuffle_deflate(6)),
10128            ] {
10129                ds.shape([64usize])
10130                    .chunk(&[16])
10131                    .create(name)
10132                    .unwrap()
10133                    .write_raw(&values)
10134                    .unwrap();
10135            }
10136            file.close().unwrap();
10137        }
10138        assert_eq!(
10139            stored_pipeline(&path, "split"),
10140            crate::format::messages::filter::FilterPipeline::shuffle_deflate(4, 6)
10141        );
10142        assert_eq!(
10143            stored_pipeline(&path, "split"),
10144            stored_pipeline(&path, "combined")
10145        );
10146
10147        let file = H5File::open(&path).unwrap();
10148        for name in ["split", "combined"] {
10149            assert_eq!(
10150                file.dataset(name).unwrap().read_raw::<i32>().unwrap(),
10151                values,
10152                "{name}"
10153            );
10154        }
10155        std::fs::remove_file(&path).ok();
10156    }
10157
10158    /// The width shuffle permutes by is the stored element's, which a
10159    /// `datatype` override moves away from the carrier type `T`: recording
10160    /// `T`'s width would permute a 4-byte element as four 1-byte ones and
10161    /// hand libhdf5 a chunk it unshuffles into different bytes.
10162    #[test]
10163    fn shuffle_records_the_stored_element_width() {
10164        use crate::format::messages::datatype::DatatypeMessage;
10165        use crate::format::messages::filter::FilterPipeline;
10166        let path = temp_path("shuffle_override_width");
10167        let bytes: Vec<u8> = (0..16u8).collect();
10168        {
10169            let file = H5File::create(&path).unwrap();
10170            file.new_dataset::<u8>()
10171                .datatype(DatatypeMessage::i32_type())
10172                .shape([4usize])
10173                .chunk(&[4])
10174                .shuffle()
10175                .create("d")
10176                .unwrap()
10177                .write_raw_bytes(&bytes)
10178                .unwrap();
10179            file.close().unwrap();
10180        }
10181        assert_eq!(stored_pipeline(&path, "d"), FilterPipeline::shuffle(4));
10182
10183        let file = H5File::open(&path).unwrap();
10184        assert_eq!(
10185            file.dataset("d").unwrap().read_raw::<i32>().unwrap(),
10186            bytes
10187                .chunks(4)
10188                .map(|c| i32::from_le_bytes(c.try_into().unwrap()))
10189                .collect::<Vec<_>>()
10190        );
10191        std::fs::remove_file(&path).ok();
10192    }
10193
10194    /// A write into a virtual dataset is refused by name rather than landing
10195    /// somewhere no reader would look: libhdf5 pushes such a write through
10196    /// the mapping into the source dataset (`H5D__virtual_write`), which this
10197    /// writer does not do.
10198    #[test]
10199    fn a_virtual_dataset_refuses_every_write() {
10200        use crate::Selection;
10201        let path = temp_path("vds_write_refused");
10202        let file = H5File::create(&path).unwrap();
10203        let ds = file
10204            .new_dataset::<i32>()
10205            .shape([16usize])
10206            .virtual_mapping(Selection::All, "src.h5", "src", Selection::All)
10207            .create("vds")
10208            .unwrap();
10209        for err in [
10210            ds.write_raw(&(0..16i32).collect::<Vec<_>>()).unwrap_err(),
10211            ds.write_slice(&[0], &[2], &[1i32, 2]).unwrap_err(),
10212        ] {
10213            let msg = err.to_string();
10214            assert!(msg.contains("virtual dataset"), "{msg}");
10215        }
10216        file.close().unwrap();
10217        std::fs::remove_file(&path).ok();
10218    }
10219
10220    /// A `%b` substitution only means something when the virtual selection is
10221    /// unlimited and the source selection is not: that is the shape where
10222    /// each block draws from a different source dataset. On any other mapping
10223    /// there is only one block, so `H5D_virtual_check_mapping_post` refuses
10224    /// the specifier — and an illegal conversion is refused wherever it
10225    /// appears.
10226    #[test]
10227    fn a_printf_source_name_needs_the_mapping_shape_that_uses_it() {
10228        use crate::Selection;
10229        let path = temp_path("vds_printf");
10230        let file = H5File::create(&path).unwrap();
10231        for (f, d) in [("src_%b.h5", "src"), ("src.h5", "block_%b")] {
10232            let err = match file
10233                .new_dataset::<i32>()
10234                .shape([16usize])
10235                .virtual_mapping(Selection::All, f, d, Selection::All)
10236                .create("vds")
10237            {
10238                Ok(_) => panic!("a bounded mapping has one block, so %b names nothing"),
10239                Err(e) => e.to_string(),
10240            };
10241            assert!(err.contains("printf specifier"), "{err}");
10242        }
10243        // `%z` is not a conversion libhdf5 has, in any mapping shape.
10244        let err = match file
10245            .new_dataset::<i32>()
10246            .shape([1usize, 2])
10247            .max_shape(&[None, Some(2)])
10248            .virtual_mapping(unlimited_rows(), "src_%z.h5", "src", Selection::All)
10249            .create("vds_bad")
10250        {
10251            Ok(_) => panic!("%z is not a legal conversion"),
10252            Err(e) => e.to_string(),
10253        };
10254        assert!(err.contains("invalid format specifier"), "{err}");
10255        file.close().unwrap();
10256        std::fs::remove_file(&path).ok();
10257    }
10258
10259    /// A printf mapping stitches one source dataset per block of its
10260    /// unlimited virtual selection, and the extent stops at the first block
10261    /// with no source (`H5D__virtual_set_extent_unlim`'s printf arm, at the
10262    /// default `H5D_VDS_LAST_AVAILABLE` view and `printf_gap` 0).
10263    #[test]
10264    fn a_printf_mapping_stitches_one_source_per_block() {
10265        use crate::Selection;
10266        let path = temp_path("vds_printf_blocks");
10267        {
10268            let file = H5File::create(&path).unwrap();
10269            for (b, base) in [(0, 0i32), (1, 100), (3, 300)] {
10270                file.new_dataset::<i32>()
10271                    .shape([2usize])
10272                    .create(&format!("block{b}"))
10273                    .unwrap()
10274                    .write_raw(&[base, base + 1])
10275                    .unwrap();
10276            }
10277            file.new_dataset::<i32>()
10278                .shape([1usize, 2])
10279                .max_shape(&[None, Some(2)])
10280                .virtual_mapping(unlimited_rows(), ".", "block%b", Selection::All)
10281                .create("vds")
10282                .unwrap();
10283            file.close().unwrap();
10284        }
10285        let file = H5File::open(&path).unwrap();
10286        let ds = file.dataset("vds").unwrap();
10287        // `block2` is missing, so `block3` is never reached: two rows.
10288        assert_eq!(ds.shape(), vec![2, 2]);
10289        assert_eq!(ds.read_raw::<i32>().unwrap(), vec![0, 1, 100, 101]);
10290        drop(file);
10291        std::fs::remove_file(&path).ok();
10292    }
10293
10294    /// A mapping whose source cannot be opened reads back as the fill value,
10295    /// not as an error: `H5D__virtual_open_source_dset` accepts a null source
10296    /// file and clears the error stack for a missing source dataset
10297    /// (H5Dvirtual.c:877-909), so `H5D__virtual_read_one` finds no projected
10298    /// memory space and reads nothing for it (H5Dvirtual.c:2661-2665).
10299    #[test]
10300    fn a_source_that_cannot_be_opened_reads_as_the_fill_value() {
10301        use crate::{Hyperslab, HyperslabBlock, Selection};
10302        let block = |start: u64, end: u64| Selection::Hyperslab {
10303            rank: 1,
10304            form: Hyperslab::Blocks(vec![HyperslabBlock {
10305                start: vec![start],
10306                end: vec![end],
10307            }]),
10308        };
10309        let path = temp_path("vds_absent_source");
10310        {
10311            let file = H5File::create(&path).unwrap();
10312            file.new_dataset::<i32>()
10313                .shape([4usize])
10314                .create("here")
10315                .unwrap()
10316                .write_raw(&[1i32, 2, 3, 4])
10317                .unwrap();
10318            file.new_dataset::<i32>()
10319                .shape([12usize])
10320                .fill_value(-3i32)
10321                .virtual_mapping(block(0, 3), ".", "here", block(0, 3))
10322                // A dataset that is not in this file.
10323                .virtual_mapping(block(4, 7), ".", "absent", block(0, 3))
10324                // A file that does not exist beside this one.
10325                .virtual_mapping(block(8, 11), "no_such_vds_source.h5", "src", block(0, 3))
10326                .create("vds")
10327                .unwrap();
10328            file.close().unwrap();
10329        }
10330        let file = H5File::open(&path).unwrap();
10331        let ds = file.dataset("vds").unwrap();
10332        assert_eq!(
10333            ds.read_raw::<i32>().unwrap(),
10334            vec![1, 2, 3, 4, -3, -3, -3, -3, -3, -3, -3, -3]
10335        );
10336        // The same rule on the slice path, which stitches the whole image
10337        // before extracting the region.
10338        assert_eq!(
10339            ds.read_slice::<i32>(&[2], &[6]).unwrap(),
10340            vec![3, 4, -3, -3, -3, -3]
10341        );
10342        drop(file);
10343        std::fs::remove_file(&path).ok();
10344    }
10345
10346    /// `%%` is an escaped literal `%`, not a substitution: the mapping is an
10347    /// ordinary bounded one, and the source it resolves against is the name
10348    /// with a single `%` in it.
10349    #[test]
10350    fn an_escaped_percent_is_a_literal_in_a_source_name() {
10351        use crate::Selection;
10352        let path = temp_path("vds_escaped_pct");
10353        {
10354            let file = H5File::create(&path).unwrap();
10355            file.new_dataset::<i32>()
10356                .shape([4usize])
10357                .create("od%d")
10358                .unwrap()
10359                .write_raw(&[5i32, 6, 7, 8])
10360                .unwrap();
10361            file.new_dataset::<i32>()
10362                .shape([4usize])
10363                .virtual_mapping(Selection::All, ".", "od%%d", Selection::All)
10364                .create("vds")
10365                .unwrap();
10366            file.close().unwrap();
10367        }
10368        let file = H5File::open(&path).unwrap();
10369        let ds = file.dataset("vds").unwrap();
10370        assert_eq!(ds.read_raw::<i32>().unwrap(), vec![5, 6, 7, 8]);
10371        // The stored name keeps its escape; only the resolution unescapes.
10372        assert_eq!(ds.virtual_mappings().unwrap()[0].source_dset_name, "od%%d");
10373        drop(file);
10374        std::fs::remove_file(&path).ok();
10375    }
10376
10377    /// An unlimited mapping takes its extent from the source it names, so a
10378    /// virtual dataset written with one reports the source's rows, not the
10379    /// seed extent its dataspace message stores
10380    /// (`H5D__virtual_set_extent_unlim`). Same file, so the resolution runs
10381    /// without opening another one.
10382    #[test]
10383    fn an_unlimited_mapping_takes_its_extent_from_its_source() {
10384        let path = temp_path("vds_unlimited");
10385        {
10386            let file = H5File::create(&path).unwrap();
10387            file.new_dataset::<i32>()
10388                .shape([5usize, 2])
10389                .chunk(&[5, 2])
10390                .max_shape(&[None, Some(2)])
10391                .create("src")
10392                .unwrap()
10393                .write_raw(&(0..10i32).collect::<Vec<_>>())
10394                .unwrap();
10395            file.new_dataset::<i32>()
10396                .shape([1usize, 2])
10397                .max_shape(&[None, Some(2)])
10398                .virtual_mapping(unlimited_rows(), ".", "src", unlimited_rows())
10399                .create("vds")
10400                .unwrap();
10401            file.close().unwrap();
10402        }
10403        let file = H5File::open(&path).unwrap();
10404        let ds = file.dataset("vds").unwrap();
10405        assert_eq!(ds.shape(), vec![5, 2]);
10406        assert_eq!(ds.read_raw::<i32>().unwrap(), (0..10).collect::<Vec<i32>>());
10407        drop(file);
10408        std::fs::remove_file(&path).ok();
10409    }
10410
10411    /// The blocks-0/1/3 printf file every dataset-access test below reads,
10412    /// laid out exactly like `a_printf_mapping_stitches_one_source_per_block`
10413    /// so the gap is the only thing that changes.
10414    fn printf_gap_file(tag: &str) -> std::path::PathBuf {
10415        use crate::Selection;
10416        let path = temp_path(tag);
10417        let file = H5File::create(&path).unwrap();
10418        for (b, base) in [(0, 0i32), (1, 100), (3, 300)] {
10419            file.new_dataset::<i32>()
10420                .shape([2usize])
10421                .create(&format!("block{b}"))
10422                .unwrap()
10423                .write_raw(&[base, base + 1])
10424                .unwrap();
10425        }
10426        file.new_dataset::<i32>()
10427            .shape([1usize, 2])
10428            .max_shape(&[None, Some(2)])
10429            .fill_value(-7i32)
10430            .virtual_mapping(unlimited_rows(), ".", "block%b", Selection::All)
10431            .create("vds")
10432            .unwrap();
10433        file.close().unwrap();
10434        path
10435    }
10436
10437    /// `H5Pset_virtual_printf_gap` lets the block scan look past a missing
10438    /// source, and the blocks it looked past stay inside the extent reading
10439    /// as the fill value (H5Dvirtual.c:1519-1614, :2661-2665). Measured
10440    /// against libhdf5 1.14.6 through h5py's `h5p.PropDAID`: gap 0 gives
10441    /// two rows, gap 1 and gap 2 both give four with row 2 filled.
10442    #[test]
10443    fn a_printf_gap_looks_past_the_missing_block() {
10444        use crate::DatasetAccess;
10445        let path = printf_gap_file("vds_printf_gap");
10446        let file = H5File::open(&path).unwrap();
10447        for (gap, shape, data) in [
10448            (0u64, vec![2usize, 2], vec![0i32, 1, 100, 101]),
10449            (1, vec![4, 2], vec![0, 1, 100, 101, -7, -7, 300, 301]),
10450            (2, vec![4, 2], vec![0, 1, 100, 101, -7, -7, 300, 301]),
10451        ] {
10452            let ds = file
10453                .dataset_with("vds", DatasetAccess::new().virtual_printf_gap(gap))
10454                .unwrap();
10455            assert_eq!(ds.shape(), shape, "gap {gap}");
10456            assert_eq!(ds.read_raw::<i32>().unwrap(), data, "gap {gap}");
10457        }
10458        // Back to the default: the extent follows the properties the open
10459        // names, in both directions.
10460        let ds = file.dataset("vds").unwrap();
10461        assert_eq!(ds.shape(), vec![2, 2]);
10462        assert_eq!(ds.read_raw::<i32>().unwrap(), vec![0, 1, 100, 101]);
10463        drop(file);
10464        std::fs::remove_file(&path).ok();
10465    }
10466
10467    /// A relatively-named source is found next to the virtual dataset even
10468    /// when the process is somewhere else entirely: `H5F_prefix_open_file`
10469    /// tries the primary file's `H5F_EXTPATH` — the directory it was opened
10470    /// from — before the bare relative name against the working directory
10471    /// (H5Fint.c:952-977). Measured against libhdf5 1.14.6 through h5py: a
10472    /// `VirtualSource("src.h5", ...)` beside its VDS reads its data with
10473    /// `HDF5_VDS_PREFIX` unset and the working directory elsewhere; before
10474    /// the reader took that step it read back all fill value.
10475    #[test]
10476    fn a_relative_source_resolves_next_to_the_virtual_dataset() {
10477        use crate::Selection;
10478        let dir = std::env::temp_dir().join(format!(
10479            "rust_hdf5_vds_beside_{}_{:?}",
10480            std::process::id(),
10481            std::thread::current().id()
10482        ));
10483        std::fs::create_dir_all(&dir).unwrap();
10484        {
10485            let file = H5File::create(dir.join("src.h5")).unwrap();
10486            file.new_dataset::<i32>()
10487                .shape([2usize, 4])
10488                .create("data")
10489                .unwrap()
10490                .write_raw(&(0..8i32).collect::<Vec<_>>())
10491                .unwrap();
10492            file.close().unwrap();
10493        }
10494        {
10495            let file = H5File::create(dir.join("v.h5")).unwrap();
10496            file.new_dataset::<i32>()
10497                .shape([2usize, 4])
10498                .fill_value(-9i32)
10499                // Named relatively, as h5py's `VirtualSource("src.h5", ...)`
10500                // stores it — nothing in the file says where it lives.
10501                .virtual_mapping(Selection::All, "src.h5", "data", Selection::All)
10502                .create("v")
10503                .unwrap();
10504            file.close().unwrap();
10505        }
10506        // The working directory is the crate root under `cargo test`, not
10507        // `dir`, so only the extpath step can find `src.h5`.
10508        assert_ne!(std::env::current_dir().unwrap(), dir);
10509        let file = H5File::open(dir.join("v.h5")).unwrap();
10510        let ds = file.dataset("v").unwrap();
10511        assert_eq!(ds.read_raw::<i32>().unwrap(), (0..8i32).collect::<Vec<_>>());
10512        drop(file);
10513        std::fs::remove_dir_all(&dir).ok();
10514    }
10515
10516    /// The first open of a virtual dataset fixes its access properties for
10517    /// every open that overlaps it: only the open that finds no shared info
10518    /// in `H5FO_opened` runs `H5D__open_oid(dataset, dapl_id)`, and a later
10519    /// one just points at that shared info without ever reading its own dapl
10520    /// (H5Dint.c:1496-1500, :1523-1528) — the view and the gap live in the
10521    /// shared layout storage `H5D__virtual_init` filled from that first dapl
10522    /// (H5Dvirtual.c:2178-2188). Measured against libhdf5 1.14.6 and 2.0.0
10523    /// through `h5d.open(..., dapl=...)` on the printf-gap VDS below: opening
10524    /// gap 0 then gap 1 gives both handles two rows; with every handle closed,
10525    /// opening gap 1 then gap 0 gives both four rows and the gap row reads as
10526    /// fill; with every handle closed again, gap 0 alone is back to two rows.
10527    #[test]
10528    fn the_first_open_of_a_virtual_dataset_fixes_the_properties_for_later_opens() {
10529        use crate::DatasetAccess;
10530        let path = printf_gap_file("vds_printf_first_open");
10531        let file = H5File::open(&path).unwrap();
10532        let gap = |g: u64| DatasetAccess::new().virtual_printf_gap(g);
10533        {
10534            let a = file.dataset_with("vds", gap(0)).unwrap();
10535            let b = file.dataset_with("vds", gap(1)).unwrap();
10536            assert_eq!(a.shape(), vec![2, 2]);
10537            assert_eq!(b.shape(), vec![2, 2]);
10538            assert_eq!(b.read_raw::<i32>().unwrap(), vec![0, 1, 100, 101]);
10539        }
10540        {
10541            let c = file.dataset_with("vds", gap(1)).unwrap();
10542            let d = file.dataset_with("vds", gap(0)).unwrap();
10543            assert_eq!(c.shape(), vec![4, 2]);
10544            assert_eq!(d.shape(), vec![4, 2]);
10545            assert_eq!(
10546                d.read_raw::<i32>().unwrap(),
10547                vec![0, 1, 100, 101, -7, -7, 300, 301]
10548            );
10549        }
10550        let e = file.dataset_with("vds", gap(0)).unwrap();
10551        assert_eq!(e.shape(), vec![2, 2]);
10552        assert_eq!(e.read_raw::<i32>().unwrap(), vec![0, 1, 100, 101]);
10553        drop(e);
10554        drop(file);
10555        std::fs::remove_file(&path).ok();
10556    }
10557
10558    /// `H5D_VDS_FIRST_MISSING` ignores the printf gap: `H5D__virtual_init`
10559    /// reads the gap property only under `H5D_VDS_LAST_AVAILABLE` and forces
10560    /// it to 0 otherwise (H5Dvirtual.c:2182-2188). Measured against libhdf5
10561    /// 1.14.6: gap 2 under this view still gives two rows.
10562    #[test]
10563    fn the_first_missing_view_ignores_the_printf_gap() {
10564        use crate::{DatasetAccess, VirtualView};
10565        let path = printf_gap_file("vds_printf_first_missing");
10566        let file = H5File::open(&path).unwrap();
10567        for gap in [0u64, 2] {
10568            let ds = file
10569                .dataset_with(
10570                    "vds",
10571                    DatasetAccess::new()
10572                        .virtual_view(VirtualView::FirstMissing)
10573                        .virtual_printf_gap(gap),
10574                )
10575                .unwrap();
10576            assert_eq!(ds.shape(), vec![2, 2], "gap {gap}");
10577            assert_eq!(
10578                ds.read_raw::<i32>().unwrap(),
10579                vec![0, 1, 100, 101],
10580                "gap {gap}"
10581            );
10582        }
10583        drop(file);
10584        std::fs::remove_file(&path).ok();
10585    }
10586
10587    /// On a mapping unlimited on both sides the view is
10588    /// `H5S_hyper_get_clip_extent_match`'s `incl_trail`
10589    /// (H5Dvirtual.c:1447-1451): with a stride wider than its block, the
10590    /// extent under `H5D_VDS_LAST_AVAILABLE` ends at the last mapped row,
10591    /// and under `H5D_VDS_FIRST_MISSING` it runs on to where the next block
10592    /// would start. Measured against libhdf5 1.14.6 over a three-row source
10593    /// with stride 3 and block 2: two rows and three rows.
10594    #[test]
10595    fn the_view_decides_whether_a_trailing_gap_is_inside_the_extent() {
10596        use crate::format::selection::UNLIMITED;
10597        use crate::{DatasetAccess, Hyperslab, RegularHyperslab, Selection, VirtualView};
10598        let strided = || Selection::Hyperslab {
10599            rank: 2,
10600            form: Hyperslab::Regular(RegularHyperslab {
10601                start: vec![0, 0],
10602                stride: vec![3, 1],
10603                count: vec![UNLIMITED, 1],
10604                block: vec![2, 2],
10605            }),
10606        };
10607        let path = temp_path("vds_view_trail");
10608        {
10609            let file = H5File::create(&path).unwrap();
10610            file.new_dataset::<i32>()
10611                .shape([3usize, 2])
10612                .max_shape(&[None, Some(2)])
10613                .chunk(&[1, 2])
10614                .create("src")
10615                .unwrap()
10616                .write_raw(&(0..6i32).collect::<Vec<_>>())
10617                .unwrap();
10618            file.new_dataset::<i32>()
10619                .shape([1usize, 2])
10620                .max_shape(&[None, Some(2)])
10621                .fill_value(-9i32)
10622                .virtual_mapping(strided(), ".", "src", strided())
10623                .create("vds")
10624                .unwrap();
10625            file.close().unwrap();
10626        }
10627        let file = H5File::open(&path).unwrap();
10628        {
10629            let last = file.dataset("vds").unwrap();
10630            assert_eq!(last.shape(), vec![2, 2]);
10631            assert_eq!(last.read_raw::<i32>().unwrap(), vec![0, 1, 2, 3]);
10632        }
10633        // The handle above is gone, so this open is the one that resolves.
10634        let first = file
10635            .dataset_with(
10636                "vds",
10637                DatasetAccess::new().virtual_view(VirtualView::FirstMissing),
10638            )
10639            .unwrap();
10640        assert_eq!(first.shape(), vec![3, 2]);
10641        assert_eq!(first.read_raw::<i32>().unwrap(), vec![0, 1, 2, 3, -9, -9]);
10642        drop(file);
10643        std::fs::remove_file(&path).ok();
10644    }
10645
10646    /// The property list reads back what was set (`H5Pget_virtual_view`,
10647    /// `H5Pget_virtual_printf_gap`), and the gap `HSIZE_UNDEF` that
10648    /// `H5Pset_virtual_printf_gap` refuses is refused by the open that would
10649    /// have used it.
10650    #[test]
10651    fn the_access_property_list_reads_back_and_refuses_hsize_undef() {
10652        use crate::{DatasetAccess, VirtualView};
10653        let plist = DatasetAccess::new();
10654        assert_eq!(plist.view(), VirtualView::LastAvailable);
10655        assert_eq!(plist.printf_gap(), 0);
10656        let set = plist
10657            .virtual_view(VirtualView::FirstMissing)
10658            .virtual_printf_gap(4);
10659        assert_eq!(set.view(), VirtualView::FirstMissing);
10660        // The *property* keeps what was set even though the resolution under
10661        // this view scans with 0.
10662        assert_eq!(set.printf_gap(), 4);
10663
10664        let path = printf_gap_file("vds_printf_gap_undef");
10665        let file = H5File::open(&path).unwrap();
10666        let err = match file.dataset_with("vds", DatasetAccess::new().virtual_printf_gap(u64::MAX))
10667        {
10668            Ok(_) => panic!("HSIZE_UNDEF is not a valid printf gap size"),
10669            Err(e) => e.to_string(),
10670        };
10671        assert!(err.contains("HSIZE_UNDEF"), "{err}");
10672        drop(file);
10673        std::fs::remove_file(&path).ok();
10674    }
10675
10676    /// The rank-2 `count = (H5S_UNLIMITED, 1)`, `block = (1, 2)` selection
10677    /// both sides of an unlimited row-wise mapping use.
10678    fn unlimited_rows() -> crate::Selection {
10679        use crate::format::selection::UNLIMITED;
10680        use crate::{Hyperslab, RegularHyperslab, Selection};
10681        Selection::Hyperslab {
10682            rank: 2,
10683            form: Hyperslab::Regular(RegularHyperslab {
10684                start: vec![0, 0],
10685                stride: vec![1, 1],
10686                count: vec![UNLIMITED, 1],
10687                block: vec![1, 2],
10688            }),
10689        }
10690    }
10691
10692    /// An unlimited virtual selection over a *limited* source selection is
10693    /// the printf shape, and without a `%b` in a source name there is no
10694    /// second dataset to fill the second block —
10695    /// `H5D_virtual_check_mapping_post` refuses it, and so does this.
10696    #[test]
10697    fn an_unlimited_virtual_selection_over_a_limited_source_is_refused() {
10698        use crate::Selection;
10699        let path = temp_path("vds_unlim_limited_src");
10700        let file = H5File::create(&path).unwrap();
10701        let err = match file
10702            .new_dataset::<i32>()
10703            .shape([1usize, 2])
10704            .max_shape(&[None, Some(2)])
10705            .virtual_mapping(unlimited_rows(), "src.h5", "src", Selection::All)
10706            .create("vds")
10707        {
10708            Ok(_) => panic!("no printf substitution names the mapping's later blocks"),
10709            Err(e) => e.to_string(),
10710        };
10711        assert!(err.contains("printf"), "{err}");
10712        file.close().unwrap();
10713        std::fs::remove_file(&path).ok();
10714    }
10715
10716    /// A virtual dataset stores nothing of its own, so it cannot also be one
10717    /// of the storage classes that do.
10718    #[test]
10719    fn a_virtual_dataset_cannot_also_be_chunked_or_external() {
10720        use crate::Selection;
10721        let path = temp_path("vds_exclusive");
10722        let file = H5File::create(&path).unwrap();
10723        let builder = || {
10724            file.new_dataset::<i32>().shape([16usize]).virtual_mapping(
10725                Selection::All,
10726                "src.h5",
10727                "src",
10728                Selection::All,
10729            )
10730        };
10731        for (which, res) in [
10732            ("chunked", builder().chunk(&[4]).create("a")),
10733            ("compact", builder().compact().create("b")),
10734            (
10735                "external",
10736                builder().external(&[("x.raw", 0, 64)]).create("c"),
10737            ),
10738            ("references", builder().object_references().create("d")),
10739        ] {
10740            match res {
10741                Ok(_) => panic!("a virtual dataset cannot also be {which}"),
10742                Err(e) => assert!(e.to_string().contains("virtual dataset"), "{which}: {e}"),
10743            }
10744        }
10745        file.close().unwrap();
10746        std::fs::remove_file(&path).ok();
10747    }
10748
10749    /// Deleting a virtual dataset frees the global heap object its mapping
10750    /// list lived in — `H5D__virtual_delete` reaches `H5HG_remove` — so the
10751    /// next dataset's own heap object reuses that space instead of the file
10752    /// growing by a whole collection per deleted virtual dataset.
10753    #[test]
10754    fn deleting_a_virtual_dataset_frees_its_mapping_list() {
10755        use crate::Selection;
10756        let path = temp_path("vds_delete");
10757        let baseline = {
10758            let file = H5File::create(&path).unwrap();
10759            file.new_dataset::<i32>()
10760                .shape([16usize])
10761                .virtual_mapping(Selection::All, "src.h5", "src", Selection::All)
10762                .create("vds")
10763                .unwrap();
10764            file.delete_dataset("vds").unwrap();
10765            file.close().unwrap();
10766            std::fs::metadata(&path).unwrap().len()
10767        };
10768        std::fs::remove_file(&path).ok();
10769
10770        // Ten more create-then-delete rounds must land on the same file size:
10771        // each round's heap object is removed, its collection becomes empty
10772        // and returns to the allocator, and the next round takes it back.
10773        let file = H5File::create(&path).unwrap();
10774        for i in 0..10 {
10775            let name = format!("vds{i}");
10776            file.new_dataset::<i32>()
10777                .shape([16usize])
10778                .virtual_mapping(Selection::All, "src.h5", "src", Selection::All)
10779                .create(&name)
10780                .unwrap();
10781            file.delete_dataset(&name).unwrap();
10782        }
10783        file.close().unwrap();
10784        assert_eq!(std::fs::metadata(&path).unwrap().len(), baseline);
10785        std::fs::remove_file(&path).ok();
10786    }
10787
10788    /// [`H5Dataset::storage_layout`] tells the four classes apart — the
10789    /// negative case for any one class is simply that it is not another.
10790    #[test]
10791    fn storage_layout_reports_each_class() {
10792        use crate::StorageLayout;
10793        let path = temp_path("storage_layout");
10794        {
10795            let file = H5File::create(&path).unwrap();
10796            file.new_dataset::<i32>()
10797                .shape([4usize])
10798                .create("contig")
10799                .unwrap();
10800            file.new_dataset::<i32>()
10801                .shape([4usize])
10802                .compact()
10803                .create("compact")
10804                .unwrap();
10805            file.new_dataset::<i32>()
10806                .shape([8usize])
10807                .chunk(&[4])
10808                .create("chunked")
10809                .unwrap();
10810            file.close().unwrap();
10811        }
10812        let file = H5File::open(&path).unwrap();
10813        assert_eq!(
10814            file.dataset("contig").unwrap().storage_layout().unwrap(),
10815            StorageLayout::Contiguous
10816        );
10817        assert_eq!(
10818            file.dataset("compact").unwrap().storage_layout().unwrap(),
10819            StorageLayout::Compact
10820        );
10821        assert_eq!(
10822            file.dataset("chunked").unwrap().storage_layout().unwrap(),
10823            StorageLayout::Chunked
10824        );
10825    }
10826
10827    /// Read-mode-only accessor, matching `datatype()`'s own contract.
10828    #[test]
10829    fn storage_layout_errors_in_write_mode() {
10830        let path = temp_path("storage_layout_write_mode");
10831        let file = H5File::create(&path).unwrap();
10832        let ds = file
10833            .new_dataset::<i32>()
10834            .shape([4usize])
10835            .create("data")
10836            .unwrap();
10837        assert!(ds.storage_layout().is_err());
10838        file.close().unwrap();
10839    }
10840
10841    /// [`H5Dataset::chunk_index`] reports the real on-disk index kind
10842    /// (extensible array for one unlimited dimension, version-1 B-tree
10843    /// under a legacy libver bound) and `None` for an unchunked dataset —
10844    /// the negative case.
10845    #[test]
10846    fn chunk_index_reports_the_stored_kind() {
10847        use crate::ChunkIndex;
10848        let path = temp_path("chunk_index");
10849        {
10850            let file = H5File::create(&path).unwrap();
10851            file.new_dataset::<i32>()
10852                .shape([4usize])
10853                .create("contig")
10854                .unwrap();
10855            file.new_dataset::<i32>()
10856                .shape([16usize])
10857                .chunk(&[4])
10858                .max_shape(&[None])
10859                .create("earray")
10860                .unwrap();
10861            file.set_libver_latest(false).unwrap();
10862            file.new_dataset::<i32>()
10863                .shape([8usize])
10864                .chunk(&[4])
10865                .max_shape(&[None])
10866                .create("btree1")
10867                .unwrap();
10868            file.close().unwrap();
10869        }
10870        let file = H5File::open(&path).unwrap();
10871        assert_eq!(file.dataset("contig").unwrap().chunk_index().unwrap(), None);
10872        assert_eq!(
10873            file.dataset("earray").unwrap().chunk_index().unwrap(),
10874            Some(ChunkIndex::ExtensibleArray)
10875        );
10876        assert_eq!(
10877            file.dataset("btree1").unwrap().chunk_index().unwrap(),
10878            Some(ChunkIndex::BtreeV1)
10879        );
10880    }
10881
10882    /// [`H5Dataset::filters`] reports the stored pipeline in order — and
10883    /// the negative case: an unfiltered dataset reports an empty pipeline,
10884    /// not an error.
10885    #[test]
10886    fn filters_reports_the_stored_pipeline() {
10887        use crate::format::messages::filter::{FILTER_DEFLATE, FILTER_SHUFFLE, FLAG_OPTIONAL};
10888        let path = temp_path("filters");
10889        {
10890            let file = H5File::create(&path).unwrap();
10891            file.new_dataset::<i32>()
10892                .shape([16usize])
10893                .create("unfiltered")
10894                .unwrap();
10895            file.new_dataset::<i32>()
10896                .shape([16usize])
10897                .chunk(&[4])
10898                .shuffle()
10899                .deflate(6)
10900                .create("filtered")
10901                .unwrap();
10902            file.close().unwrap();
10903        }
10904        let file = H5File::open(&path).unwrap();
10905        assert_eq!(
10906            file.dataset("unfiltered").unwrap().filters().unwrap(),
10907            Vec::new()
10908        );
10909        let filters = file.dataset("filtered").unwrap().filters().unwrap();
10910        assert_eq!(filters.len(), 2);
10911        assert_eq!(filters[0].id, FILTER_SHUFFLE);
10912        assert_eq!(filters[0].flags, FLAG_OPTIONAL);
10913        assert_eq!(filters[1].id, FILTER_DEFLATE);
10914        assert_eq!(filters[1].cd_values, vec![6]);
10915    }
10916
10917    /// [`H5Dataset::fill_value`] reports the explicit bytes for a dataset
10918    /// created with `.fill_value(...)`, and the negative case: a dataset
10919    /// with no fill value set reports [`FillValue::Default`], not an error.
10920    /// `FillValue::Undefined` has no constructor on either this crate's
10921    /// writer or h5py's public API, so it is not exercised here.
10922    #[test]
10923    fn fill_value_reports_the_stored_value() {
10924        use crate::FillValue;
10925        let path = temp_path("fill_value");
10926        {
10927            let file = H5File::create(&path).unwrap();
10928            file.new_dataset::<i32>()
10929                .shape([4usize])
10930                .create("unset")
10931                .unwrap();
10932            file.new_dataset::<i32>()
10933                .shape([4usize])
10934                .fill_value(-7i32)
10935                .create("set")
10936                .unwrap();
10937            file.close().unwrap();
10938        }
10939        let file = H5File::open(&path).unwrap();
10940        assert_eq!(
10941            file.dataset("unset").unwrap().fill_value().unwrap(),
10942            FillValue::Default
10943        );
10944        assert_eq!(
10945            file.dataset("set").unwrap().fill_value().unwrap(),
10946            FillValue::UserDefined((-7i32).to_le_bytes().to_vec())
10947        );
10948    }
10949
10950    /// The three `H5D__efl_construct` / `H5Pset_external` rules an
10951    /// `H5O_EFL_UNLIMITED` slot lives inside: it may only be the last slot,
10952    /// an unlimited dataspace must have one, and only the first dimension
10953    /// may be extendible.
10954    #[test]
10955    fn the_unlimited_external_slot_keeps_its_three_rules() {
10956        use crate::format::messages::external_file_list::UNLIMITED;
10957        let path = temp_path("efl_unlim_rules");
10958        let file = H5File::create(&path).unwrap();
10959
10960        // "previous file size is unlimited": nothing behind an unlimited slot
10961        // could ever be reached.
10962        let err = match file
10963            .new_dataset::<i32>()
10964            .shape([8usize])
10965            .external(&[("a.raw", 0, UNLIMITED), ("b.raw", 0, 32)])
10966            .create("mid")
10967        {
10968            Ok(_) => panic!("an unlimited slot absorbs everything behind it"),
10969            Err(e) => e.to_string(),
10970        };
10971        assert!(err.contains("only be the last"), "{err}");
10972
10973        // "unlimited dataspace but finite storage".
10974        let err = match file
10975            .new_dataset::<i32>()
10976            .shape([8usize])
10977            .max_shape(&[None])
10978            .external(&[("a.raw", 0, 32)])
10979            .create("finite")
10980        {
10981            Ok(_) => panic!("no finite reservation covers an unlimited extent"),
10982            Err(e) => e.to_string(),
10983        };
10984        assert!(err.contains("unlimited dataspace"), "{err}");
10985
10986        // "only the first dimension can be extendible".
10987        let err = match file
10988            .new_dataset::<i32>()
10989            .shape([2usize, 4])
10990            .max_shape(&[Some(2), None])
10991            .external(&[("a.raw", 0, UNLIMITED)])
10992            .create("dim1")
10993        {
10994            Ok(_) => panic!("only the slowest-varying dimension may be extendible"),
10995            Err(e) => e.to_string(),
10996        };
10997        assert!(err.contains("only the first dimension"), "{err}");
10998
10999        // And the legal shape: an unlimited last slot under an unlimited
11000        // first dimension.
11001        file.new_dataset::<i32>()
11002            .shape([8usize])
11003            .max_shape(&[None])
11004            .external(&[("a.raw", 0, 16), ("b.raw", 0, UNLIMITED)])
11005            .create("ok")
11006            .unwrap()
11007            .write_raw(&(0..8i32).collect::<Vec<_>>())
11008            .unwrap();
11009        file.close().unwrap();
11010        std::fs::remove_file(&path).ok();
11011        for raw in ["a.raw", "b.raw"] {
11012            std::fs::remove_file(raw).ok();
11013        }
11014    }
11015
11016    /// An unlimited slot reserves nothing, so a read of it is bounded by the
11017    /// dataset's extent and by what the file physically holds: the tail past
11018    /// the end of a short raw file reads back as zero, exactly as
11019    /// `H5D__efl_read` fills it.
11020    #[test]
11021    fn an_unlimited_external_slot_reads_zero_past_the_end_of_its_file() {
11022        use crate::format::messages::external_file_list::UNLIMITED;
11023        let dir = std::env::temp_dir().join(format!("rh5_efl_short_{}", std::process::id()));
11024        std::fs::create_dir_all(&dir).unwrap();
11025        let path = dir.join("f.h5");
11026        let raw = dir.join("short.raw");
11027        // Four elements' worth of bytes for an eight-element dataset.
11028        std::fs::write(&raw, [0u8; 16]).unwrap();
11029        {
11030            let file = H5File::create(&path).unwrap();
11031            file.new_dataset::<i32>()
11032                .shape([8usize])
11033                .max_shape(&[None])
11034                .external(&[(raw.to_str().unwrap(), 0, UNLIMITED)])
11035                .create("data")
11036                .unwrap();
11037            file.close().unwrap();
11038        }
11039        let file = H5File::open(&path).unwrap();
11040        assert_eq!(
11041            file.dataset("data").unwrap().read_raw::<i32>().unwrap(),
11042            vec![0i32; 8]
11043        );
11044        drop(file);
11045        std::fs::remove_dir_all(&dir).ok();
11046    }
11047
11048    /// [`H5Dataset::external_files`] reports the stored segment list in
11049    /// order, and the negative case: a dataset whose data lives in this
11050    /// file reports an empty list, not an error.
11051    #[test]
11052    fn external_files_reports_the_stored_segments() {
11053        let path = temp_path("external_files");
11054        {
11055            let file = H5File::create(&path).unwrap();
11056            file.new_dataset::<i32>()
11057                .shape([4usize])
11058                .create("contig")
11059                .unwrap();
11060            file.new_dataset::<i32>()
11061                .shape([16usize])
11062                .external(&[("a.raw", 0, 32), ("b.raw", 8, 32)])
11063                .create("external")
11064                .unwrap();
11065            file.close().unwrap();
11066        }
11067        let file = H5File::open(&path).unwrap();
11068        assert_eq!(
11069            file.dataset("contig").unwrap().external_files().unwrap(),
11070            Vec::new()
11071        );
11072        let segments = file.dataset("external").unwrap().external_files().unwrap();
11073        assert_eq!(segments.len(), 2);
11074        assert_eq!(segments[0].name, "a.raw");
11075        assert_eq!(segments[0].offset, 0);
11076        assert_eq!(segments[0].size, 32);
11077        assert_eq!(segments[1].name, "b.raw");
11078        assert_eq!(segments[1].offset, 8);
11079        assert_eq!(segments[1].size, 32);
11080    }
11081
11082    /// [`H5Dataset::max_shape`] reports an unlimited axis as `None` and a
11083    /// fixed one as its current size — and the negative case: a dataset
11084    /// with no maximum-dimensions message reports max == current, not an
11085    /// error.
11086    #[test]
11087    fn max_shape_reports_unlimited_and_fixed_axes() {
11088        let path = temp_path("max_shape");
11089        {
11090            let file = H5File::create(&path).unwrap();
11091            file.new_dataset::<i32>()
11092                .shape([4usize, 8])
11093                .create("fixed")
11094                .unwrap();
11095            file.new_dataset::<i32>()
11096                .shape([4usize, 8])
11097                .chunk(&[2, 8])
11098                .max_shape(&[None, Some(8)])
11099                .create("unlimited")
11100                .unwrap();
11101            file.close().unwrap();
11102        }
11103        let file = H5File::open(&path).unwrap();
11104        assert_eq!(
11105            file.dataset("fixed").unwrap().max_shape().unwrap(),
11106            vec![Some(4), Some(8)]
11107        );
11108        assert_eq!(
11109            file.dataset("unlimited").unwrap().max_shape().unwrap(),
11110            vec![None, Some(8)]
11111        );
11112    }
11113
11114    /// [`H5Dataset::virtual_mappings`] reports the stored source/virtual
11115    /// mapping list in order, and the negative case: a dataset with no
11116    /// virtual layout reports an empty list, not an error.
11117    #[test]
11118    fn virtual_mappings_reports_the_stored_mappings() {
11119        use crate::Selection;
11120        let path = temp_path("virtual_mappings");
11121        {
11122            let file = H5File::create(&path).unwrap();
11123            file.new_dataset::<i32>()
11124                .shape([4usize])
11125                .create("plain")
11126                .unwrap();
11127            file.new_dataset::<i32>()
11128                .shape([16usize])
11129                .virtual_mapping(Selection::All, "src.h5", "src", Selection::All)
11130                .create("vds")
11131                .unwrap();
11132            file.close().unwrap();
11133        }
11134        let file = H5File::open(&path).unwrap();
11135        assert_eq!(
11136            file.dataset("plain").unwrap().virtual_mappings().unwrap(),
11137            Vec::new()
11138        );
11139        let mappings = file.dataset("vds").unwrap().virtual_mappings().unwrap();
11140        assert_eq!(mappings.len(), 1);
11141        assert_eq!(mappings[0].source_file_name, "src.h5");
11142        assert_eq!(mappings[0].source_dset_name, "src");
11143        assert_eq!(mappings[0].source_selection, Selection::All);
11144        assert_eq!(mappings[0].virtual_selection, Selection::All);
11145    }
11146}