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 crate::attribute::AttrBuilder;
8use crate::error::{Hdf5Error, Result};
9use crate::file::{borrow_inner, borrow_inner_mut, clone_inner, H5FileInner, SharedInner};
10use crate::format::messages::datatype::DatatypeMessage;
11use crate::types::H5Type;
12
13// ---------------------------------------------------------------------------
14// DatasetBuilder
15// ---------------------------------------------------------------------------
16
17/// A fluent builder for creating datasets.
18///
19/// Obtained from [`H5File::new_dataset::<T>()`](crate::file::H5File::new_dataset).
20///
21/// ```no_run
22/// # use rust_hdf5::H5File;
23/// let file = H5File::create("builder.h5").unwrap();
24/// let ds = file.new_dataset::<f32>()
25///     .shape(&[10, 20])
26///     .create("temperatures")
27///     .unwrap();
28/// ```
29pub struct DatasetBuilder<T: H5Type> {
30    file_inner: SharedInner,
31    shape: Option<Vec<usize>>,
32    chunk_dims: Option<Vec<usize>>,
33    max_shape: Option<Vec<Option<usize>>>,
34    deflate_level: Option<u32>,
35    shuffle_deflate_level: Option<u32>,
36    custom_pipeline: Option<crate::format::messages::filter::FilterPipeline>,
37    group_path: Option<String>,
38    fill_value: Option<Vec<u8>>,
39    datatype_override: Option<crate::format::messages::datatype::DatatypeMessage>,
40    _marker: std::marker::PhantomData<T>,
41}
42
43impl<T: H5Type> DatasetBuilder<T> {
44    pub(crate) fn new(file_inner: SharedInner) -> Self {
45        Self {
46            file_inner,
47            shape: None,
48            chunk_dims: None,
49            max_shape: None,
50            deflate_level: None,
51            shuffle_deflate_level: None,
52            custom_pipeline: None,
53            group_path: None,
54            fill_value: None,
55            datatype_override: None,
56            _marker: std::marker::PhantomData,
57        }
58    }
59
60    pub(crate) fn new_in_group(file_inner: SharedInner, group_path: String) -> Self {
61        Self {
62            file_inner,
63            shape: None,
64            chunk_dims: None,
65            max_shape: None,
66            deflate_level: None,
67            shuffle_deflate_level: None,
68            custom_pipeline: None,
69            group_path: Some(group_path),
70            fill_value: None,
71            datatype_override: None,
72            _marker: std::marker::PhantomData,
73        }
74    }
75
76    /// Set the dataset dimensions.
77    ///
78    /// This is required before calling [`create`](Self::create).
79    /// Use an empty slice `&[]` for a scalar (0-dimensional) dataset.
80    #[must_use]
81    pub fn shape<S: AsRef<[usize]>>(mut self, dims: S) -> Self {
82        self.shape = Some(dims.as_ref().to_vec());
83        self
84    }
85
86    /// Create a scalar (0-dimensional) dataset holding a single value.
87    #[must_use]
88    pub fn scalar(mut self) -> Self {
89        self.shape = Some(vec![]);
90        self
91    }
92
93    /// Set chunk dimensions for chunked storage.
94    ///
95    /// When set, the dataset uses chunked storage with the extensible array
96    /// index. You should also call [`max_shape`](Self::max_shape) or
97    /// [`resizable`](Self::resizable) to allow extending.
98    #[must_use]
99    pub fn chunk(mut self, chunk_dims: &[usize]) -> Self {
100        self.chunk_dims = Some(chunk_dims.to_vec());
101        self
102    }
103
104    /// Make all dimensions unlimited (resizable).
105    ///
106    /// This sets max_dims to u64::MAX for all dimensions.
107    #[must_use]
108    pub fn resizable(mut self) -> Self {
109        self.max_shape = Some(vec![None; self.shape.as_ref().map_or(0, |s| s.len())]);
110        self
111    }
112
113    /// Set maximum dimensions. `None` means unlimited for that dimension.
114    #[must_use]
115    pub fn max_shape(mut self, max: &[Option<usize>]) -> Self {
116        self.max_shape = Some(max.to_vec());
117        self
118    }
119
120    /// Enable deflate (gzip) compression with the given level (0-9).
121    ///
122    /// Requires chunked storage (call `.chunk()` before `.create()`).
123    /// Level 0 = no compression, 9 = maximum compression. Default is 6.
124    #[must_use]
125    pub fn deflate(mut self, level: u32) -> Self {
126        self.deflate_level = Some(level);
127        self
128    }
129
130    /// Enable shuffle + deflate compression.
131    ///
132    /// Shuffle reorders bytes by position within elements before compression,
133    /// which typically improves compression ratios for numeric data.
134    /// Requires chunked storage.
135    #[must_use]
136    pub fn shuffle_deflate(mut self, level: u32) -> Self {
137        self.shuffle_deflate_level = Some(level);
138        self
139    }
140
141    /// Enable Zstandard compression with the given level (1-22, default 3).
142    ///
143    /// Requires chunked storage (call `.chunk()` before `.create()`).
144    #[must_use]
145    pub fn zstd(mut self, level: u32) -> Self {
146        self.custom_pipeline = Some(crate::format::messages::filter::FilterPipeline::zstd(level));
147        self
148    }
149
150    /// Set a custom filter pipeline for compression.
151    ///
152    /// This takes precedence over [`deflate`](Self::deflate) and
153    /// [`shuffle_deflate`](Self::shuffle_deflate). Requires chunked storage.
154    #[must_use]
155    pub fn filter_pipeline(
156        mut self,
157        pipeline: crate::format::messages::filter::FilterPipeline,
158    ) -> Self {
159        self.custom_pipeline = Some(pipeline);
160        self
161    }
162
163    /// Override the stored element datatype.
164    ///
165    /// By default the dataset is created with the datatype derived from the
166    /// Rust type parameter `T` ([`H5Type::hdf5_type`]). Use this to store a
167    /// different on-disk datatype than the in-memory element type — for
168    /// example a reduced-precision fixed-point type that matches an N-bit
169    /// filter (see [`FilterPipeline::nbit`]). The element *byte* size of the
170    /// override must equal `T::element_size()`; the N-bit filter packs the
171    /// significant bits within that fixed footprint.
172    ///
173    /// [`H5Type::hdf5_type`]: crate::H5Type::hdf5_type
174    /// [`FilterPipeline::nbit`]: crate::FilterPipeline::nbit
175    #[must_use]
176    pub fn datatype(mut self, dt: crate::format::messages::datatype::DatatypeMessage) -> Self {
177        self.datatype_override = Some(dt);
178        self
179    }
180
181    /// Set a user-defined fill value for unwritten elements.
182    ///
183    /// Without this, datasets use the HDF5 default zero-fill. When set,
184    /// the value is written into the dataset's fill-value message
185    /// (`fill_defined = 2`), so HDF5 readers treat unallocated chunks and
186    /// unwritten regions as this value rather than zero.
187    ///
188    /// ```no_run
189    /// # use rust_hdf5::H5File;
190    /// let file = H5File::create("fv.h5").unwrap();
191    /// let ds = file.new_dataset::<f32>()
192    ///     .shape(&[100])
193    ///     .fill_value(f32::NAN)
194    ///     .create("data")
195    ///     .unwrap();
196    /// ```
197    #[must_use]
198    pub fn fill_value(mut self, value: T) -> Self {
199        let es = T::element_size();
200        // Safety: `T: H5Type` is a `Copy` numeric primitive with a
201        // well-defined byte representation; `element_size()` matches
202        // `size_of::<T>()`. The slice borrows `value` only for this call.
203        let raw = unsafe { std::slice::from_raw_parts(&value as *const T as *const u8, es) };
204        self.fill_value = Some(raw.to_vec());
205        self
206    }
207
208    /// Finalize and create the dataset with the given `name`.
209    ///
210    /// The name is the link name within the root group (e.g. `"data"` or
211    /// `"group1/data"` once nested groups are supported).
212    pub fn create(self, name: &str) -> Result<H5Dataset> {
213        let shape = self.shape.ok_or_else(|| {
214            Hdf5Error::InvalidState("shape must be set before calling create()".into())
215        })?;
216
217        // Build the full name: if created within a group, prefix with group path
218        let full_name = if let Some(ref gp) = self.group_path {
219            if gp == "/" {
220                name.to_string()
221            } else {
222                let trimmed = gp.trim_start_matches('/');
223                format!("{}/{}", trimmed, name)
224            }
225        } else {
226            name.to_string()
227        };
228        let group_path = self.group_path.clone();
229        let fill_value = self.fill_value.clone();
230
231        let dims_u64: Vec<u64> = shape.iter().map(|&d| d as u64).collect();
232        let datatype = self.datatype_override.clone().unwrap_or_else(T::hdf5_type);
233        // Size one element from the on-disk datatype, not the carrier `T`. For
234        // the default path this equals `T::element_size()`; when a `datatype()`
235        // override is set (N-bit, or a runtime `CompoundType`), the stored type
236        // — not `T` — defines the element width, so the dataspace, the raw
237        // allocation, and the `write_raw` length check all agree with the bytes
238        // libhdf5/h5py will read.
239        let element_size = datatype.element_size() as usize;
240
241        // A filter pipeline requires chunked storage. When a filter is
242        // requested without explicit chunk dimensions, store the whole
243        // dataset as a single chunk instead of silently dropping the filter
244        // on the contiguous path. (This is one whole-dataset chunk, not
245        // h5py's ~1 MiB chunk-size heuristic; pass explicit chunk dimensions
246        // for large datasets.)
247        let wants_filter = self.custom_pipeline.is_some()
248            || self.shuffle_deflate_level.is_some()
249            || self.deflate_level.is_some();
250        let auto_chunk: Option<Vec<usize>> =
251            if self.chunk_dims.is_none() && wants_filter && !shape.is_empty() {
252                Some(shape.iter().map(|&d| d.max(1)).collect())
253            } else {
254                None
255            };
256
257        if let Some(chunk_dims) = self.chunk_dims.as_ref().or(auto_chunk.as_ref()) {
258            // Chunked dataset
259            let chunk_u64: Vec<u64> = chunk_dims.iter().map(|&d| d as u64).collect();
260            let max_u64: Vec<u64> = if let Some(ref max) = self.max_shape {
261                max.iter()
262                    .map(|m| m.map_or(u64::MAX, |v| v as u64))
263                    .collect()
264            } else {
265                // Default: max = current
266                dims_u64.clone()
267            };
268
269            // libhdf5 selects the chunk index from the dataspace: a v2
270            // B-tree for two or more unlimited dimensions, an extensible
271            // array for exactly one, and a fixed array when there are none.
272            let n_unlimited = max_u64.iter().filter(|&&m| m == u64::MAX).count();
273            let is_btree2 = n_unlimited >= 2;
274            let is_fixed_array = n_unlimited == 0;
275
276            let index = {
277                let inner = borrow_inner(&self.file_inner);
278                match &*inner {
279                    H5FileInner::Writer(writer) => {
280                        let idx = if is_btree2 {
281                            if wants_filter {
282                                return Err(Hdf5Error::InvalidState(
283                                    "compression of v2 B-tree (multi-unlimited-dimension) \
284                                     datasets is not yet supported"
285                                        .into(),
286                                ));
287                            }
288                            writer.create_btree_v2_dataset(
289                                &full_name, datatype, &dims_u64, &max_u64, &chunk_u64,
290                            )?
291                        } else if is_fixed_array {
292                            // A chunked dataset with no unlimited dimension
293                            // must use the fixed-array index — libhdf5
294                            // rejects an extensible-array index here. A
295                            // compressed fixed-shape dataset uses a *filtered*
296                            // fixed array (FA client id 1).
297                            if wants_filter {
298                                let pipeline = if let Some(p) = self.custom_pipeline {
299                                    p
300                                } else if let Some(level) = self.shuffle_deflate_level {
301                                    crate::format::messages::filter::FilterPipeline::shuffle_deflate(
302                                        T::element_size() as u32,
303                                        level,
304                                    )
305                                } else {
306                                    // deflate_level (checked by wants_filter).
307                                    crate::format::messages::filter::FilterPipeline::deflate(
308                                        self.deflate_level.unwrap(),
309                                    )
310                                };
311                                writer.create_fixed_array_dataset_with_pipeline(
312                                    &full_name, datatype, &dims_u64, &chunk_u64, pipeline,
313                                )?
314                            } else {
315                                writer.create_fixed_array_dataset(
316                                    &full_name, datatype, &dims_u64, &chunk_u64,
317                                )?
318                            }
319                        } else if let Some(pipeline) = self.custom_pipeline {
320                            writer.create_chunked_dataset_with_pipeline(
321                                &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, pipeline,
322                            )?
323                        } else if let Some(level) = self.shuffle_deflate_level {
324                            let pipeline =
325                                crate::format::messages::filter::FilterPipeline::shuffle_deflate(
326                                    T::element_size() as u32,
327                                    level,
328                                );
329                            writer.create_chunked_dataset_with_pipeline(
330                                &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, pipeline,
331                            )?
332                        } else if let Some(level) = self.deflate_level {
333                            writer.create_chunked_dataset_compressed(
334                                &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, level,
335                            )?
336                        } else {
337                            writer.create_chunked_dataset(
338                                &full_name, datatype, &dims_u64, &max_u64, &chunk_u64,
339                            )?
340                        };
341                        if let Some(ref gp) = group_path {
342                            if gp != "/" {
343                                writer.assign_dataset_to_group(gp, idx)?;
344                            }
345                        }
346                        if let Some(ref fv) = fill_value {
347                            writer.set_dataset_fill_value(idx, fv.clone())?;
348                        }
349                        idx
350                    }
351                    H5FileInner::Reader(_) => {
352                        return Err(Hdf5Error::InvalidState(
353                            "cannot create a dataset in read mode".into(),
354                        ));
355                    }
356                    H5FileInner::Closed => {
357                        return Err(Hdf5Error::InvalidState("file is closed".into()));
358                    }
359                }
360            };
361
362            Ok(H5Dataset {
363                file_inner: clone_inner(&self.file_inner),
364                info: DatasetInfo::Writer {
365                    index,
366                    shape,
367                    element_size,
368                    chunked: true,
369                    btree2: is_btree2,
370                    fixed_array: is_fixed_array,
371                },
372            })
373        } else {
374            // Contiguous dataset (original path)
375            let index = {
376                let inner = borrow_inner(&self.file_inner);
377                match &*inner {
378                    H5FileInner::Writer(writer) => {
379                        let idx = writer.create_dataset(&full_name, datatype, &dims_u64)?;
380                        if let Some(ref gp) = group_path {
381                            if gp != "/" {
382                                writer.assign_dataset_to_group(gp, idx)?;
383                            }
384                        }
385                        if let Some(ref fv) = fill_value {
386                            writer.set_dataset_fill_value(idx, fv.clone())?;
387                        }
388                        idx
389                    }
390                    H5FileInner::Reader(_) => {
391                        return Err(Hdf5Error::InvalidState(
392                            "cannot create a dataset in read mode".into(),
393                        ));
394                    }
395                    H5FileInner::Closed => {
396                        return Err(Hdf5Error::InvalidState("file is closed".into()));
397                    }
398                }
399            };
400
401            Ok(H5Dataset {
402                file_inner: clone_inner(&self.file_inner),
403                info: DatasetInfo::Writer {
404                    index,
405                    shape,
406                    element_size,
407                    chunked: false,
408                    btree2: false,
409                    fixed_array: false,
410                },
411            })
412        }
413    }
414}
415
416// ---------------------------------------------------------------------------
417// DatasetInfo
418// ---------------------------------------------------------------------------
419
420/// Internal metadata about a dataset handle.
421enum DatasetInfo {
422    /// A dataset created via `new_dataset().create()` in write mode.
423    Writer {
424        /// Index into the writer's dataset list.
425        index: usize,
426        /// Shape (current dimensions).
427        shape: Vec<usize>,
428        /// Size of one element in bytes.
429        element_size: usize,
430        /// Whether this is a chunked dataset.
431        chunked: bool,
432        /// Whether the chunk index is a v2 B-tree (multiple unlimited dims).
433        btree2: bool,
434        /// Whether the chunk index is a Fixed Array (no unlimited dims).
435        fixed_array: bool,
436    },
437    /// A dataset opened by name in read mode.
438    Reader {
439        /// The link name of the dataset.
440        name: String,
441        /// Shape (current dimensions).
442        shape: Vec<usize>,
443        /// Size of one element in bytes.
444        element_size: usize,
445    },
446}
447
448// ---------------------------------------------------------------------------
449// H5Dataset
450// ---------------------------------------------------------------------------
451
452/// A handle to an HDF5 dataset, supporting typed read and write operations.
453///
454/// The dataset holds a shared reference to the file's I/O backend, so it
455/// remains valid even if the originating [`H5File`](crate::file::H5File) is
456/// moved or dropped (they share ownership via `Rc`).
457pub struct H5Dataset {
458    file_inner: SharedInner,
459    info: DatasetInfo,
460}
461
462impl H5Dataset {
463    /// Create a reader-mode dataset handle (called internally by `H5File::dataset`).
464    pub(crate) fn new_reader(
465        file_inner: SharedInner,
466        name: String,
467        shape: Vec<usize>,
468        element_size: usize,
469    ) -> Self {
470        Self {
471            file_inner,
472            info: DatasetInfo::Reader {
473                name,
474                shape,
475                element_size,
476            },
477        }
478    }
479
480    /// Create a writer-mode dataset handle for an already-created dataset
481    /// (called internally by [`H5File::dataset_writer`](crate::file::H5File::dataset_writer)).
482    ///
483    /// Reconstructs the same handle `new_dataset().create()` returns, so the
484    /// reopened dataset supports attribute writes and chunk appends.
485    pub(crate) fn new_writer(
486        file_inner: SharedInner,
487        index: usize,
488        shape: Vec<usize>,
489        element_size: usize,
490        chunked: bool,
491        btree2: bool,
492        fixed_array: bool,
493    ) -> Self {
494        Self {
495            file_inner,
496            info: DatasetInfo::Writer {
497                index,
498                shape,
499                element_size,
500                chunked,
501                btree2,
502                fixed_array,
503            },
504        }
505    }
506
507    /// Return the dataset dimensions.
508    pub fn shape(&self) -> Vec<usize> {
509        match &self.info {
510            DatasetInfo::Writer { shape, .. } => shape.clone(),
511            DatasetInfo::Reader { shape, .. } => shape.clone(),
512        }
513    }
514
515    /// Return the number of dimensions (rank) of the dataset.
516    pub fn ndims(&self) -> usize {
517        match &self.info {
518            DatasetInfo::Writer { shape, .. } => shape.len(),
519            DatasetInfo::Reader { shape, .. } => shape.len(),
520        }
521    }
522
523    /// Return the total number of elements in the dataset.
524    pub fn total_elements(&self) -> usize {
525        match &self.info {
526            DatasetInfo::Writer { shape, .. } => shape.iter().product(),
527            DatasetInfo::Reader { shape, .. } => shape.iter().product(),
528        }
529    }
530
531    /// Return the size of one element in bytes.
532    pub fn element_size(&self) -> usize {
533        match &self.info {
534            DatasetInfo::Writer { element_size, .. } => *element_size,
535            DatasetInfo::Reader { element_size, .. } => *element_size,
536        }
537    }
538
539    /// Return the element datatype as parsed from the file (read mode only).
540    ///
541    /// Unlike [`element_size`](Self::element_size), which reports only the
542    /// byte width, this exposes the full datatype: its class (integer vs
543    /// floating-point vs string vs compound …), signedness, byte order and
544    /// bit precision. Callers that must reconstruct the exact stored type —
545    /// for example to map it to a NumPy / Arrow dtype — should use this
546    /// instead of inferring a type from the byte width, which cannot
547    /// distinguish `u8` from `i8` (both 1 byte) or `i32` from `f32` (both 4
548    /// bytes).
549    ///
550    /// # Errors
551    ///
552    /// Returns an error if the file is in write mode, or if the dataset can
553    /// no longer be found in the reader's metadata.
554    ///
555    /// ```no_run
556    /// # use rust_hdf5::{H5File, DatatypeMessage};
557    /// let file = H5File::open("data.h5").unwrap();
558    /// let ds = file.dataset("image").unwrap();
559    /// match ds.datatype().unwrap() {
560    ///     DatatypeMessage::FixedPoint { size, signed, .. } => {
561    ///         println!("integer: {} bytes, signed={}", size, signed);
562    ///     }
563    ///     DatatypeMessage::FloatingPoint { size, .. } => {
564    ///         println!("float: {} bytes", size);
565    ///     }
566    ///     other => println!("other type: {other}"),
567    /// }
568    /// ```
569    pub fn datatype(&self) -> Result<DatatypeMessage> {
570        match &self.info {
571            DatasetInfo::Reader { name, .. } => {
572                let inner = borrow_inner(&self.file_inner);
573                match &*inner {
574                    H5FileInner::Reader(reader) => reader
575                        .dataset_info(name)
576                        .map(|info| info.datatype.clone())
577                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
578                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
579                }
580            }
581            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
582                "datatype() is only available in read mode".into(),
583            )),
584        }
585    }
586
587    /// Return the chunk dimensions, if this is a chunked dataset.
588    pub fn chunk_dims(&self) -> Option<Vec<usize>> {
589        match &self.info {
590            DatasetInfo::Reader { name, .. } => {
591                let inner = borrow_inner(&self.file_inner);
592                if let H5FileInner::Reader(reader) = &*inner {
593                    if let Some(info) = reader.dataset_info(name) {
594                        use crate::format::messages::data_layout::DataLayoutMessage;
595                        let chunk_dims = match &info.layout {
596                            DataLayoutMessage::ChunkedV4 { chunk_dims, .. }
597                            | DataLayoutMessage::ChunkedV3 { chunk_dims, .. } => Some(chunk_dims),
598                            _ => None,
599                        };
600                        if let Some(chunk_dims) = chunk_dims {
601                            // Strip trailing element-size dimension
602                            return Some(
603                                chunk_dims[..chunk_dims.len() - 1]
604                                    .iter()
605                                    .map(|&d| d as usize)
606                                    .collect(),
607                            );
608                        }
609                    }
610                }
611                None
612            }
613            DatasetInfo::Writer { .. } => None,
614        }
615    }
616
617    /// Return whether this is a chunked dataset.
618    pub fn is_chunked(&self) -> bool {
619        match &self.info {
620            DatasetInfo::Writer { chunked, .. } => *chunked,
621            DatasetInfo::Reader { name, .. } => {
622                let inner = borrow_inner(&self.file_inner);
623                match &*inner {
624                    H5FileInner::Reader(reader) => {
625                        if let Some(info) = reader.dataset_info(name) {
626                            use crate::format::messages::data_layout::DataLayoutMessage;
627                            matches!(
628                                info.layout,
629                                DataLayoutMessage::ChunkedV4 { .. }
630                                    | DataLayoutMessage::ChunkedV3 { .. }
631                            )
632                        } else {
633                            false
634                        }
635                    }
636                    _ => false,
637                }
638            }
639        }
640    }
641
642    /// Return the names of all attributes on this dataset (read mode only).
643    pub fn attr_names(&self) -> Result<Vec<String>> {
644        match &self.info {
645            DatasetInfo::Reader { name, .. } => {
646                let inner = borrow_inner(&self.file_inner);
647                match &*inner {
648                    H5FileInner::Reader(reader) => Ok(reader.dataset_attr_names(name)?),
649                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
650                }
651            }
652            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
653                "attr_names not available in write mode".into(),
654            )),
655        }
656    }
657
658    /// Open an attribute by name (read mode only).
659    pub fn attr(&self, attr_name: &str) -> Result<crate::attribute::H5Attribute> {
660        match &self.info {
661            DatasetInfo::Reader { name, .. } => {
662                let inner = borrow_inner(&self.file_inner);
663                match &*inner {
664                    H5FileInner::Reader(reader) => {
665                        let attr_msg = reader.dataset_attr(name, attr_name)?.clone();
666                        Ok(crate::attribute::H5Attribute::new_reader(
667                            clone_inner(&self.file_inner),
668                            attr_msg,
669                        ))
670                    }
671                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
672                }
673            }
674            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
675                "attr() not available in write mode".into(),
676            )),
677        }
678    }
679
680    /// Start building a new attribute on this dataset.
681    ///
682    /// Returns a fluent builder. Call `.shape(())` for a scalar attribute
683    /// and `.create("name")` to finalize.
684    ///
685    /// # Example
686    ///
687    /// ```no_run
688    /// # use rust_hdf5::H5File;
689    /// # use rust_hdf5::types::VarLenUnicode;
690    /// let file = H5File::create("attr.h5").unwrap();
691    /// let ds = file.new_dataset::<f32>().shape(&[10]).create("data").unwrap();
692    /// let attr = ds.new_attr::<VarLenUnicode>().shape(()).create("units").unwrap();
693    /// attr.write_scalar(&VarLenUnicode("meters".to_string())).unwrap();
694    /// ```
695    pub fn new_attr<T: 'static>(&self) -> AttrBuilder<'_, T> {
696        let ds_index = match &self.info {
697            DatasetInfo::Writer { index, .. } => *index,
698            DatasetInfo::Reader { .. } => {
699                // Reader mode: we'll return a builder that will error on create.
700                // Using usize::MAX as sentinel.
701                usize::MAX
702            }
703        };
704        AttrBuilder::new(&self.file_inner, ds_index)
705    }
706
707    /// Write a typed slice to the dataset (contiguous datasets only).
708    ///
709    /// The slice length must match the total number of elements declared by
710    /// the dataset shape. The data is reinterpreted as raw bytes and written
711    /// to the file.
712    ///
713    /// # Errors
714    ///
715    /// Returns an error if:
716    /// - The file is in read mode.
717    /// - The data length does not match the declared shape.
718    pub fn write_raw<T: H5Type>(&self, data: &[T]) -> Result<()> {
719        match &self.info {
720            DatasetInfo::Writer {
721                index,
722                shape,
723                element_size,
724                chunked,
725                btree2,
726                fixed_array,
727            } => {
728                let total_elements: usize = shape.iter().product();
729                if data.len() != total_elements {
730                    return Err(Hdf5Error::InvalidState(format!(
731                        "data length {} does not match dataset size {}",
732                        data.len(),
733                        total_elements,
734                    )));
735                }
736
737                // Verify element size matches
738                if T::element_size() != *element_size {
739                    return Err(Hdf5Error::TypeMismatch(format!(
740                        "write type has element size {} but dataset expects {}",
741                        T::element_size(),
742                        element_size,
743                    )));
744                }
745
746                // Safety: T: Copy + 'static (numeric primitive) with well-defined
747                // byte representation. The resulting slice borrows `data` and
748                // lives only as long as this block.
749                let byte_len = data.len() * T::element_size();
750                let raw =
751                    unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
752
753                if *chunked {
754                    // A chunked dataset has no contiguous data block; scatter
755                    // the full row-major image into its chunk grid and write
756                    // each chunk through the dataset's filter pipeline.
757                    return self.write_full_image_chunked(
758                        *index,
759                        *btree2,
760                        *fixed_array,
761                        raw,
762                        *element_size,
763                    );
764                }
765
766                let inner = borrow_inner(&self.file_inner);
767                match &*inner {
768                    H5FileInner::Writer(writer) => {
769                        writer.write_dataset_raw(*index, raw)?;
770                        Ok(())
771                    }
772                    _ => Err(Hdf5Error::InvalidState(
773                        "file is no longer in write mode".into(),
774                    )),
775                }
776            }
777            DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
778                "cannot write to a dataset opened in read mode".into(),
779            )),
780        }
781    }
782
783    /// Write the raw byte image of a contiguous dataset directly.
784    ///
785    /// Unlike [`write_raw`](Self::write_raw), this is not generic over an
786    /// `H5Type` carrier, so it works for element types that have no matching
787    /// Rust primitive — in particular a runtime
788    /// [`CompoundType`](crate::types::CompoundType) of arbitrary size set via
789    /// [`DatasetBuilder::datatype`]. `bytes.len()` must equal
790    /// `product(shape) * element_size`, where `element_size` is taken from the
791    /// dataset's on-disk datatype.
792    ///
793    /// ```no_run
794    /// # use rust_hdf5::H5File;
795    /// # use rust_hdf5::types::{CompoundType, H5Type};
796    /// let file = H5File::create("c.h5").unwrap();
797    /// let ct = CompoundType {
798    ///     members: vec![
799    ///         ("id".to_string(), i32::hdf5_type(), 0),
800    ///         ("val".to_string(), f64::hdf5_type(), 4),
801    ///     ],
802    ///     total_size: 12,
803    /// };
804    /// let ds = file
805    ///     .new_dataset::<u8>()
806    ///     .datatype(ct.to_datatype())
807    ///     .shape(&[2])
808    ///     .create("records")
809    ///     .unwrap();
810    /// let mut bytes = Vec::new();
811    /// bytes.extend_from_slice(&1i32.to_le_bytes());
812    /// bytes.extend_from_slice(&2.5f64.to_le_bytes());
813    /// bytes.extend_from_slice(&2i32.to_le_bytes());
814    /// bytes.extend_from_slice(&3.5f64.to_le_bytes());
815    /// ds.write_raw_bytes(&bytes).unwrap();
816    /// ```
817    pub fn write_raw_bytes(&self, bytes: &[u8]) -> Result<()> {
818        match &self.info {
819            DatasetInfo::Writer {
820                index,
821                shape,
822                element_size,
823                chunked,
824                btree2,
825                fixed_array,
826            } => {
827                let expected: usize = shape.iter().product::<usize>() * *element_size;
828                if bytes.len() != expected {
829                    return Err(Hdf5Error::InvalidState(format!(
830                        "raw byte length {} does not match dataset size {} \
831                         (product(shape) * element_size {})",
832                        bytes.len(),
833                        expected,
834                        element_size,
835                    )));
836                }
837                if *chunked {
838                    // Scatter the full row-major image into the chunk grid
839                    // (same path as write_raw, carrier-agnostic bytes).
840                    return self.write_full_image_chunked(
841                        *index,
842                        *btree2,
843                        *fixed_array,
844                        bytes,
845                        *element_size,
846                    );
847                }
848                let inner = borrow_inner(&self.file_inner);
849                match &*inner {
850                    H5FileInner::Writer(writer) => {
851                        writer.write_dataset_raw(*index, bytes)?;
852                        Ok(())
853                    }
854                    _ => Err(Hdf5Error::InvalidState(
855                        "file is no longer in write mode".into(),
856                    )),
857                }
858            }
859            DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
860                "cannot write to a dataset opened in read mode".into(),
861            )),
862        }
863    }
864
865    /// Scatter a full row-major dataset image into its chunk grid, writing
866    /// every chunk through the dataset's filter pipeline.
867    ///
868    /// This is the chunked counterpart of a single contiguous `write_dataset_raw`
869    /// — it is how [`write_raw`](Self::write_raw) and
870    /// [`write_raw_bytes`](Self::write_raw_bytes) populate a chunked dataset
871    /// (including the single auto-chunk created when a filter is set without
872    /// explicit chunk dimensions). Edge chunks are zero-padded to the full
873    /// chunk footprint, exactly as libhdf5 stores them.
874    fn write_full_image_chunked(
875        &self,
876        index: usize,
877        btree2: bool,
878        fixed_array: bool,
879        bytes: &[u8],
880        element_size: usize,
881    ) -> Result<()> {
882        let inner = borrow_inner(&self.file_inner);
883        let writer = match &*inner {
884            H5FileInner::Writer(w) => w,
885            _ => {
886                return Err(Hdf5Error::InvalidState(
887                    "file is no longer in write mode".into(),
888                ))
889            }
890        };
891        let chunk_dims = writer
892            .dataset_chunk_dims(index)
893            .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
894            .to_vec();
895        let dims = writer.dataset_dims(index).to_vec();
896        let rank = dims.len();
897
898        // Chunk grid: number of chunks along each dimension (row-major).
899        let mut grid = vec![0u64; rank];
900        for d in 0..rank {
901            grid[d] = if chunk_dims[d] > 0 {
902                dims[d].div_ceil(chunk_dims[d])
903            } else {
904                0
905            };
906        }
907        let total_chunks: u64 = grid.iter().product();
908
909        for linear in 0..total_chunks {
910            // Decode the linear chunk index into row-major grid coordinates.
911            let mut rem = linear;
912            let mut coords = vec![0u64; rank];
913            for d in (0..rank).rev() {
914                coords[d] = rem % grid[d];
915                rem /= grid[d];
916            }
917            let chunk_buf = Self::gather_chunk(bytes, &dims, &chunk_dims, &coords, element_size);
918            if fixed_array {
919                writer.write_chunk_fixed_array(index, &coords, &chunk_buf)?;
920            } else if btree2 {
921                writer.write_chunk_btree_v2(index, &coords, &chunk_buf)?;
922            } else {
923                // Extensible array (single unlimited dimension): the linear
924                // index over the grid is the array's chunk index.
925                writer.write_chunk(index, linear, &chunk_buf)?;
926            }
927        }
928        Ok(())
929    }
930
931    /// Gather one chunk's bytes from a row-major full-dataset image.
932    ///
933    /// `coords` are the chunk's grid coordinates. The returned buffer is
934    /// exactly `product(chunk_dims) * element_size` bytes, zero-padded where
935    /// the chunk extends past the dataset edge.
936    fn gather_chunk(
937        source: &[u8],
938        dims: &[u64],
939        chunk_dims: &[u64],
940        coords: &[u64],
941        element_size: usize,
942    ) -> Vec<u8> {
943        let rank = dims.len();
944        let chunk_elems: u64 = chunk_dims.iter().product();
945        let mut out = vec![0u8; chunk_elems as usize * element_size];
946        if rank == 0 {
947            // Scalar dataset: a single element, no chunking dimension.
948            if source.len() >= element_size {
949                out[..element_size].copy_from_slice(&source[..element_size]);
950            }
951            return out;
952        }
953
954        // Actual extent of this chunk along each dimension (edge chunks are
955        // smaller than the nominal chunk shape).
956        let mut extent = vec![0u64; rank];
957        for d in 0..rank {
958            let start = coords[d] * chunk_dims[d];
959            let end = ((coords[d] + 1) * chunk_dims[d]).min(dims[d]);
960            extent[d] = end.saturating_sub(start);
961        }
962        if extent.contains(&0) {
963            return out; // nothing of the dataset falls in this chunk
964        }
965
966        // Row-major strides (in elements) for the source (over `dims`) and the
967        // destination chunk buffer (over `chunk_dims`).
968        let mut src_stride = vec![1u64; rank];
969        let mut dst_stride = vec![1u64; rank];
970        for d in (0..rank - 1).rev() {
971            src_stride[d] = src_stride[d + 1] * dims[d + 1];
972            dst_stride[d] = dst_stride[d + 1] * chunk_dims[d + 1];
973        }
974
975        // Copy one contiguous run along the last axis per outer multi-index.
976        let last = rank - 1;
977        let run = extent[last] as usize * element_size;
978        let outer: u64 = extent[..last].iter().product::<u64>().max(1);
979        let mut idx = vec![0u64; rank]; // local indices within the chunk extent
980        for _ in 0..outer {
981            let mut src_off = 0u64;
982            let mut dst_off = 0u64;
983            for d in 0..rank {
984                let global = coords[d] * chunk_dims[d] + idx[d];
985                src_off += global * src_stride[d];
986                dst_off += idx[d] * dst_stride[d];
987            }
988            let s = src_off as usize * element_size;
989            let dpos = dst_off as usize * element_size;
990            out[dpos..dpos + run].copy_from_slice(&source[s..s + run]);
991
992            // Advance the multi-index over axes [0..last); the last axis is the
993            // contiguous run handled above.
994            let mut d = last;
995            while d > 0 {
996                d -= 1;
997                idx[d] += 1;
998                if idx[d] < extent[d] {
999                    break;
1000                }
1001                idx[d] = 0;
1002            }
1003        }
1004        out
1005    }
1006
1007    /// Write a single chunk to a chunked dataset.
1008    ///
1009    /// `chunk_idx` is the linear chunk index (typically the frame number for
1010    /// streaming datasets). `data` is the raw byte data for one chunk.
1011    ///
1012    /// For datasets with two or more unlimited dimensions (v2 B-tree index),
1013    /// use [`write_chunk_at`](Self::write_chunk_at) instead.
1014    pub fn write_chunk(&self, chunk_idx: usize, data: &[u8]) -> Result<()> {
1015        match &self.info {
1016            DatasetInfo::Writer {
1017                index,
1018                chunked,
1019                btree2,
1020                fixed_array,
1021                ..
1022            } => {
1023                if !*chunked {
1024                    return Err(Hdf5Error::InvalidState(
1025                        "write_chunk is only for chunked datasets".into(),
1026                    ));
1027                }
1028                if *btree2 {
1029                    return Err(Hdf5Error::InvalidState(
1030                        "this dataset uses a v2 B-tree chunk index; use write_chunk_at \
1031                         with the chunk's grid coordinates"
1032                            .into(),
1033                    ));
1034                }
1035
1036                let inner = borrow_inner(&self.file_inner);
1037                match &*inner {
1038                    H5FileInner::Writer(writer) => {
1039                        if *fixed_array {
1040                            // Fixed-array dataset: convert the linear chunk
1041                            // index into row-major grid coordinates.
1042                            let chunk_dims = writer
1043                                .dataset_chunk_dims(*index)
1044                                .ok_or_else(|| {
1045                                    Hdf5Error::InvalidState("dataset has no chunk info".into())
1046                                })?
1047                                .to_vec();
1048                            let dims = writer.dataset_dims(*index).to_vec();
1049                            let mut grid = vec![0u64; dims.len()];
1050                            for d in 0..dims.len() {
1051                                grid[d] = if chunk_dims[d] > 0 {
1052                                    dims[d].div_ceil(chunk_dims[d])
1053                                } else {
1054                                    1
1055                                };
1056                            }
1057                            // A zero-extent dimension yields a grid of 0
1058                            // chunks — there is no chunk to write.
1059                            if grid.contains(&0) {
1060                                return Err(Hdf5Error::InvalidState(
1061                                    "dataset has a zero-extent dimension and no chunks".into(),
1062                                ));
1063                            }
1064                            let mut rem = chunk_idx as u64;
1065                            let mut coords = vec![0u64; dims.len()];
1066                            for d in (0..dims.len()).rev() {
1067                                coords[d] = rem % grid[d];
1068                                rem /= grid[d];
1069                            }
1070                            // A leftover means chunk_idx exceeded the grid.
1071                            if rem != 0 {
1072                                return Err(Hdf5Error::InvalidState(format!(
1073                                    "chunk index {chunk_idx} is out of range for this dataset"
1074                                )));
1075                            }
1076                            writer.write_chunk_fixed_array(*index, &coords, data)?;
1077                        } else {
1078                            writer.write_chunk(*index, chunk_idx as u64, data)?;
1079                        }
1080                        Ok(())
1081                    }
1082                    _ => Err(Hdf5Error::InvalidState(
1083                        "file is no longer in write mode".into(),
1084                    )),
1085                }
1086            }
1087            DatasetInfo::Reader { .. } => {
1088                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1089            }
1090        }
1091    }
1092
1093    /// Write an already-filtered (pre-compressed) chunk **verbatim**, recording
1094    /// the caller-supplied `filter_mask`. The bytes are stored as-is without
1095    /// running the dataset's filter pipeline — the HDF5 "direct chunk write"
1096    /// (`H5Dwrite_chunk`, formerly `H5DOwrite_chunk`) operation.
1097    ///
1098    /// `chunk_idx` is the linear chunk index (the frame number for streaming
1099    /// datasets), exactly as for [`write_chunk`](Self::write_chunk). `data` is
1100    /// the already-filtered bytes of one chunk — its length is the *stored*
1101    /// (compressed) size, not the uncompressed chunk size.
1102    ///
1103    /// `filter_mask` is a bitfield: bit *i* set means filter *i* of the
1104    /// dataset's pipeline was **not** applied to this chunk and must be skipped
1105    /// on read. Pass 0 when the full pipeline was already applied upstream (the
1106    /// common case: a codec plugin handed you compressed frames).
1107    ///
1108    /// The dataset must be chunked **and** filtered; an unfiltered chunk index
1109    /// has no slot to record a stored size or mask. For a v2-B-tree-indexed
1110    /// dataset (two or more unlimited dimensions) direct chunk writes are not
1111    /// supported.
1112    ///
1113    /// # Reading back
1114    ///
1115    /// Both this crate's reader and libhdf5/h5py honor the per-chunk
1116    /// `filter_mask`: a chunk written with any mask round-trips correctly, with
1117    /// the reader skipping exactly the filters the mask marks as not applied.
1118    pub fn write_chunk_raw(&self, chunk_idx: usize, data: &[u8], filter_mask: u32) -> Result<()> {
1119        match &self.info {
1120            DatasetInfo::Writer {
1121                index,
1122                chunked,
1123                btree2,
1124                fixed_array,
1125                ..
1126            } => {
1127                if !*chunked {
1128                    return Err(Hdf5Error::InvalidState(
1129                        "write_chunk_raw is only for chunked datasets".into(),
1130                    ));
1131                }
1132                if *btree2 {
1133                    return Err(Hdf5Error::InvalidState(
1134                        "direct chunk writes are not supported for v2-B-tree-indexed \
1135                         datasets (two or more unlimited dimensions)"
1136                            .into(),
1137                    ));
1138                }
1139
1140                let inner = borrow_inner(&self.file_inner);
1141                match &*inner {
1142                    H5FileInner::Writer(writer) => {
1143                        if *fixed_array {
1144                            // Fixed-array dataset: convert the linear chunk
1145                            // index into row-major grid coordinates.
1146                            let chunk_dims = writer
1147                                .dataset_chunk_dims(*index)
1148                                .ok_or_else(|| {
1149                                    Hdf5Error::InvalidState("dataset has no chunk info".into())
1150                                })?
1151                                .to_vec();
1152                            let dims = writer.dataset_dims(*index).to_vec();
1153                            let mut grid = vec![0u64; dims.len()];
1154                            for d in 0..dims.len() {
1155                                grid[d] = if chunk_dims[d] > 0 {
1156                                    dims[d].div_ceil(chunk_dims[d])
1157                                } else {
1158                                    1
1159                                };
1160                            }
1161                            // A zero-extent dimension yields a grid of 0
1162                            // chunks — there is no chunk to write.
1163                            if grid.contains(&0) {
1164                                return Err(Hdf5Error::InvalidState(
1165                                    "dataset has a zero-extent dimension and no chunks".into(),
1166                                ));
1167                            }
1168                            let mut rem = chunk_idx as u64;
1169                            let mut coords = vec![0u64; dims.len()];
1170                            for d in (0..dims.len()).rev() {
1171                                coords[d] = rem % grid[d];
1172                                rem /= grid[d];
1173                            }
1174                            // A leftover means chunk_idx exceeded the grid.
1175                            if rem != 0 {
1176                                return Err(Hdf5Error::InvalidState(format!(
1177                                    "chunk index {chunk_idx} is out of range for this dataset"
1178                                )));
1179                            }
1180                            writer.write_compressed_chunk_fixed_array(
1181                                *index,
1182                                &coords,
1183                                data,
1184                                filter_mask,
1185                            )?;
1186                        } else {
1187                            writer.write_compressed_chunk(
1188                                *index,
1189                                chunk_idx as u64,
1190                                data,
1191                                filter_mask,
1192                            )?;
1193                        }
1194                        Ok(())
1195                    }
1196                    _ => Err(Hdf5Error::InvalidState(
1197                        "file is no longer in write mode".into(),
1198                    )),
1199                }
1200            }
1201            DatasetInfo::Reader { .. } => {
1202                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1203            }
1204        }
1205    }
1206
1207    /// Write a single chunk to a v2-B-tree-indexed dataset, addressed by its
1208    /// chunk-grid coordinates (one per dimension).
1209    ///
1210    /// This is the entry point for datasets with two or more unlimited
1211    /// dimensions. The dataset's logical dimensions are extended to cover
1212    /// the written chunk. `data` is the raw bytes of one full chunk.
1213    ///
1214    /// ```no_run
1215    /// # use rust_hdf5::H5File;
1216    /// let file = H5File::create("bt2.h5").unwrap();
1217    /// let ds = file.new_dataset::<i32>()
1218    ///     .shape(&[0, 0])
1219    ///     .chunk(&[2, 2])
1220    ///     .max_shape(&[None, None])
1221    ///     .create("grid")
1222    ///     .unwrap();
1223    /// let chunk = [0i32, 1, 2, 3];
1224    /// let bytes: Vec<u8> = chunk.iter().flat_map(|v| v.to_le_bytes()).collect();
1225    /// ds.write_chunk_at(&[0, 0], &bytes).unwrap();
1226    /// ```
1227    pub fn write_chunk_at(&self, chunk_coords: &[usize], data: &[u8]) -> Result<()> {
1228        match &self.info {
1229            DatasetInfo::Writer {
1230                index,
1231                chunked,
1232                btree2,
1233                fixed_array,
1234                ..
1235            } => {
1236                if !*chunked {
1237                    return Err(Hdf5Error::InvalidState(
1238                        "write_chunk_at is only for chunked datasets".into(),
1239                    ));
1240                }
1241                let coords: Vec<u64> = chunk_coords.iter().map(|&c| c as u64).collect();
1242                let btree2 = *btree2;
1243                let fixed_array = *fixed_array;
1244                let inner = borrow_inner(&self.file_inner);
1245                let writer = match &*inner {
1246                    H5FileInner::Writer(w) => w,
1247                    _ => {
1248                        return Err(Hdf5Error::InvalidState(
1249                            "file is no longer in write mode".into(),
1250                        ))
1251                    }
1252                };
1253                let chunk_dims = writer
1254                    .dataset_chunk_dims(*index)
1255                    .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
1256                    .to_vec();
1257                let dims = writer.dataset_dims(*index).to_vec();
1258                if coords.len() != dims.len() {
1259                    return Err(Hdf5Error::InvalidState(format!(
1260                        "chunk_coords has {} entries but the dataset has {} dimensions",
1261                        coords.len(),
1262                        dims.len()
1263                    )));
1264                }
1265                if chunk_dims.len() != dims.len() {
1266                    return Err(Hdf5Error::InvalidState(format!(
1267                        "dataset chunk shape has {} dimensions but the dataspace has {}",
1268                        chunk_dims.len(),
1269                        dims.len()
1270                    )));
1271                }
1272
1273                // Validate coordinates and compute the grown dimensions
1274                // up-front, before any chunk is written, so an overflowing
1275                // coordinate cannot leave an orphaned chunk in the file.
1276                let mut new_dims = dims.clone();
1277                for d in 0..dims.len() {
1278                    let needed = coords[d]
1279                        .checked_add(1)
1280                        .and_then(|c| c.checked_mul(chunk_dims[d]))
1281                        .ok_or_else(|| {
1282                            Hdf5Error::InvalidState(format!(
1283                                "chunk coordinate {} in dimension {} is too large",
1284                                coords[d], d
1285                            ))
1286                        })?;
1287                    if needed > new_dims[d] {
1288                        new_dims[d] = needed;
1289                    }
1290                }
1291
1292                if fixed_array {
1293                    // Fixed-array (fixed-shape) dataset: no dimension growth.
1294                    writer.write_chunk_fixed_array(*index, &coords, data)?;
1295                    return Ok(());
1296                }
1297
1298                if btree2 {
1299                    writer.write_chunk_btree_v2(*index, &coords, data)?;
1300                } else {
1301                    // Extensible array: linearize the chunk-grid coordinates
1302                    // (row-major) into the array's chunk index.
1303                    let mut linear = 0u64;
1304                    for d in 0..dims.len() {
1305                        let grid = if chunk_dims[d] > 0 {
1306                            dims[d].div_ceil(chunk_dims[d])
1307                        } else {
1308                            1
1309                        };
1310                        linear = linear
1311                            .checked_mul(grid)
1312                            .and_then(|l| l.checked_add(coords[d]))
1313                            .ok_or_else(|| {
1314                                Hdf5Error::InvalidState(
1315                                    "chunk coordinates overflow the array index".into(),
1316                                )
1317                            })?;
1318                    }
1319                    writer.write_chunk(*index, linear, data)?;
1320                }
1321
1322                if new_dims != dims {
1323                    writer.extend_dataset(*index, &new_dims)?;
1324                }
1325                Ok(())
1326            }
1327            DatasetInfo::Reader { .. } => {
1328                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1329            }
1330        }
1331    }
1332
1333    /// Write multiple chunks in a batch, optionally compressing in parallel.
1334    ///
1335    /// `chunks` is a slice of `(chunk_index, raw_data)` pairs. When a filter
1336    /// pipeline is configured and the `parallel` feature is enabled, all
1337    /// chunks are compressed concurrently via rayon.
1338    pub fn write_chunks_batch(&self, chunks: &[(usize, &[u8])]) -> Result<()> {
1339        match &self.info {
1340            DatasetInfo::Writer { index, chunked, .. } => {
1341                if !*chunked {
1342                    return Err(Hdf5Error::InvalidState(
1343                        "write_chunks_batch is only for chunked datasets".into(),
1344                    ));
1345                }
1346                let pairs: Vec<(u64, &[u8])> = chunks
1347                    .iter()
1348                    .map(|(idx, data)| (*idx as u64, *data))
1349                    .collect();
1350                let inner = borrow_inner(&self.file_inner);
1351                match &*inner {
1352                    H5FileInner::Writer(writer) => {
1353                        writer.write_chunks_batch(*index, &pairs)?;
1354                        Ok(())
1355                    }
1356                    _ => Err(Hdf5Error::InvalidState(
1357                        "file is no longer in write mode".into(),
1358                    )),
1359                }
1360            }
1361            DatasetInfo::Reader { .. } => {
1362                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1363            }
1364        }
1365    }
1366
1367    /// Append data along the first dimension of a chunked dataset.
1368    ///
1369    /// `data` must contain a whole number of "frames" — slices along
1370    /// dimension 0. For example, if the dataset has shape `[N, H, W]`
1371    /// and `chunk_dims = [1, H, W]`, then `data.len()` must be a
1372    /// multiple of `H * W`.
1373    ///
1374    /// This method writes the necessary chunks and extends the dataset
1375    /// shape automatically.
1376    ///
1377    /// ```no_run
1378    /// # use rust_hdf5::H5File;
1379    /// let file = H5File::create("append.h5").unwrap();
1380    /// let ds = file.new_dataset::<f64>()
1381    ///     .shape(&[0, 3])
1382    ///     .chunk(&[1, 3])
1383    ///     .max_shape(&[None, Some(3)])
1384    ///     .create("data")
1385    ///     .unwrap();
1386    /// ds.append(&[1.0, 2.0, 3.0]).unwrap();       // shape becomes [1, 3]
1387    /// ds.append(&[4.0, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap(); // shape becomes [3, 3]
1388    /// ```
1389    pub fn append<T: H5Type>(&self, data: &[T]) -> Result<()> {
1390        match &self.info {
1391            DatasetInfo::Writer {
1392                index,
1393                element_size,
1394                chunked,
1395                ..
1396            } => {
1397                if !*chunked {
1398                    return Err(Hdf5Error::InvalidState(
1399                        "append is only for chunked datasets".into(),
1400                    ));
1401                }
1402                if T::element_size() != *element_size {
1403                    return Err(Hdf5Error::TypeMismatch(format!(
1404                        "append type has element size {} but dataset expects {}",
1405                        T::element_size(),
1406                        element_size,
1407                    )));
1408                }
1409
1410                let ds_index = *index;
1411                let es = *element_size;
1412
1413                let inner = borrow_inner(&self.file_inner);
1414                let writer = match &*inner {
1415                    H5FileInner::Writer(w) => w,
1416                    _ => {
1417                        return Err(Hdf5Error::InvalidState(
1418                            "file is no longer in write mode".into(),
1419                        ))
1420                    }
1421                };
1422
1423                let chunk_dims = writer
1424                    .dataset_chunk_dims(ds_index)
1425                    .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
1426                    .to_vec();
1427                let dims = writer.dataset_dims(ds_index).to_vec();
1428
1429                // Frame size = product of dims[1..]
1430                let frame_elems: usize = if dims.len() > 1 {
1431                    dims[1..].iter().map(|&d| d as usize).product()
1432                } else {
1433                    1
1434                };
1435
1436                if frame_elems == 0 {
1437                    return Err(Hdf5Error::InvalidState(
1438                        "cannot append to dataset with zero-size trailing dimensions".into(),
1439                    ));
1440                }
1441
1442                if !data.len().is_multiple_of(frame_elems) {
1443                    return Err(Hdf5Error::InvalidState(format!(
1444                        "data length {} is not a multiple of frame size {}",
1445                        data.len(),
1446                        frame_elems,
1447                    )));
1448                }
1449
1450                let n_new_frames = data.len() / frame_elems;
1451                let current_dim0 = dims[0] as usize;
1452
1453                // Chunk size along first dimension
1454                let chunk_dim0 = chunk_dims[0] as usize;
1455                let frame_bytes = frame_elems * es;
1456
1457                let raw = unsafe {
1458                    std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * es)
1459                };
1460
1461                // Merge buffered data with new data. Scope the slot guard: the
1462                // loop below calls `write_chunk`, which re-locks the same slot.
1463                let (buffered_frames, mut combined) = {
1464                    let ds = writer.ds(ds_index);
1465                    let mut m = ds.lock();
1466                    let buffered_frames = m.append_buffered_frames as usize;
1467                    let combined = std::mem::take(&mut m.append_buffer);
1468                    m.append_buffered_frames = 0;
1469                    (buffered_frames, combined)
1470                };
1471                combined.extend_from_slice(raw);
1472
1473                let total_frames = buffered_frames + n_new_frames;
1474                let total_bytes = combined.len();
1475
1476                // Base chunk index: account for buffered frames
1477                let base_dim0 = current_dim0 - buffered_frames;
1478                let mut byte_pos = 0usize;
1479                let mut frame_pos = 0usize;
1480
1481                while frame_pos < total_frames {
1482                    let abs_frame = base_dim0 + frame_pos;
1483                    let chunk_idx = abs_frame / chunk_dim0;
1484                    let remaining_frames = total_frames - frame_pos;
1485                    let frames_to_fill = chunk_dim0 - (abs_frame % chunk_dim0);
1486
1487                    if remaining_frames >= frames_to_fill {
1488                        // Full chunk — write
1489                        let end = byte_pos + frames_to_fill * frame_bytes;
1490                        if frames_to_fill == chunk_dim0 {
1491                            writer.write_chunk(
1492                                ds_index,
1493                                chunk_idx as u64,
1494                                &combined[byte_pos..end],
1495                            )?;
1496                        } else {
1497                            // Partial-chunk write: this branch only runs with
1498                            // offset_in_chunk > 0, meaning the chunk already
1499                            // holds earlier frames on disk. Read-modify-write
1500                            // so those frames survive — a fresh fill buffer
1501                            // would erase them.
1502                            let offset_in_chunk = (abs_frame % chunk_dim0) * frame_bytes;
1503                            let mut chunk_buf =
1504                                match writer.read_chunk_if_present(ds_index, chunk_idx as u64)? {
1505                                    Some(existing) => existing,
1506                                    None => {
1507                                        return Err(Hdf5Error::InvalidState(format!(
1508                                            "cannot append into partially-written chunk {}: \
1509                                         its existing content was not found in the chunk \
1510                                         index (the file may be inconsistent)",
1511                                            chunk_idx
1512                                        )));
1513                                    }
1514                                };
1515                            chunk_buf
1516                                [offset_in_chunk..offset_in_chunk + frames_to_fill * frame_bytes]
1517                                .copy_from_slice(&combined[byte_pos..end]);
1518                            writer.write_chunk(ds_index, chunk_idx as u64, &chunk_buf)?;
1519                        }
1520                        byte_pos = end;
1521                        frame_pos += frames_to_fill;
1522                    } else {
1523                        // Partial chunk — buffer for next append
1524                        let ds = writer.ds(ds_index);
1525                        let mut m = ds.lock();
1526                        m.append_buffer = combined[byte_pos..total_bytes].to_vec();
1527                        m.append_buffered_frames = remaining_frames as u64;
1528                        frame_pos = total_frames;
1529                    }
1530                }
1531
1532                // Extend dims to include all frames (buffered + new)
1533                let logical_dim0 = base_dim0 + total_frames;
1534                let mut new_dims: Vec<u64> = dims;
1535                new_dims[0] = logical_dim0 as u64;
1536                writer.extend_dataset(ds_index, &new_dims)?;
1537
1538                Ok(())
1539            }
1540            DatasetInfo::Reader { .. } => {
1541                Err(Hdf5Error::InvalidState("cannot append in read mode".into()))
1542            }
1543        }
1544    }
1545
1546    /// Extend the dimensions of a chunked dataset.
1547    pub fn extend(&self, new_dims: &[usize]) -> Result<()> {
1548        match &self.info {
1549            DatasetInfo::Writer { index, chunked, .. } => {
1550                if !*chunked {
1551                    return Err(Hdf5Error::InvalidState(
1552                        "extend is only for chunked datasets".into(),
1553                    ));
1554                }
1555
1556                let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
1557                let inner = borrow_inner(&self.file_inner);
1558                match &*inner {
1559                    H5FileInner::Writer(writer) => {
1560                        writer.extend_dataset(*index, &dims_u64)?;
1561                        Ok(())
1562                    }
1563                    _ => Err(Hdf5Error::InvalidState(
1564                        "file is no longer in write mode".into(),
1565                    )),
1566                }
1567            }
1568            DatasetInfo::Reader { .. } => {
1569                Err(Hdf5Error::InvalidState("cannot extend in read mode".into()))
1570            }
1571        }
1572    }
1573
1574    /// Set the logical extent of a chunked dataset, growing **or
1575    /// shrinking** any dimension.
1576    ///
1577    /// Unlike [`extend`](Self::extend), which only grows, this can reduce a
1578    /// dimension — for example to correct an over-extended frame count
1579    /// after writing a partial multi-frame chunk. Shrinking changes the
1580    /// logical dataspace only: data in chunks beyond the new extent stays
1581    /// in the file but is no longer visible on read, exactly as libhdf5's
1582    /// `H5Dset_extent` behaves. The new extent must not exceed the
1583    /// dataset's maximum dimensions.
1584    pub fn set_extent(&self, new_dims: &[usize]) -> Result<()> {
1585        match &self.info {
1586            DatasetInfo::Writer { index, .. } => {
1587                let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
1588                let inner = borrow_inner(&self.file_inner);
1589                match &*inner {
1590                    H5FileInner::Writer(writer) => {
1591                        writer.set_dataset_extent(*index, &dims_u64)?;
1592                        Ok(())
1593                    }
1594                    _ => Err(Hdf5Error::InvalidState(
1595                        "file is no longer in write mode".into(),
1596                    )),
1597                }
1598            }
1599            DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
1600                "cannot set extent in read mode".into(),
1601            )),
1602        }
1603    }
1604
1605    /// Flush a chunked dataset's index structures to disk.
1606    pub fn flush(&self) -> Result<()> {
1607        match &self.info {
1608            DatasetInfo::Writer { index, .. } => {
1609                let inner = borrow_inner(&self.file_inner);
1610                match &*inner {
1611                    H5FileInner::Writer(writer) => {
1612                        writer.flush_dataset(*index)?;
1613                        Ok(())
1614                    }
1615                    _ => Ok(()),
1616                }
1617            }
1618            DatasetInfo::Reader { .. } => Ok(()),
1619        }
1620    }
1621
1622    /// Read a slice (hyperslab) of the dataset as a typed vector.
1623    ///
1624    /// `starts` and `counts` define the N-dimensional selection:
1625    /// `starts[d]` = first index along dim d, `counts[d]` = how many elements.
1626    pub fn read_slice<T: H5Type>(&self, starts: &[usize], counts: &[usize]) -> Result<Vec<T>> {
1627        match &self.info {
1628            DatasetInfo::Reader {
1629                name, element_size, ..
1630            } => {
1631                if T::element_size() != *element_size {
1632                    return Err(Hdf5Error::TypeMismatch(format!(
1633                        "read type has element size {} but dataset has element size {}",
1634                        T::element_size(),
1635                        element_size,
1636                    )));
1637                }
1638                let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
1639                let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
1640
1641                let raw = {
1642                    let mut inner = borrow_inner_mut(&self.file_inner);
1643                    match &mut *inner {
1644                        H5FileInner::Reader(reader) => {
1645                            reader.read_slice(name, &starts_u64, &counts_u64)?
1646                        }
1647                        _ => {
1648                            return Err(Hdf5Error::InvalidState("file is not in read mode".into()))
1649                        }
1650                    }
1651                };
1652
1653                if raw.len() % T::element_size() != 0 {
1654                    return Err(Hdf5Error::TypeMismatch(format!(
1655                        "raw data size {} is not a multiple of element size {}",
1656                        raw.len(),
1657                        T::element_size(),
1658                    )));
1659                }
1660
1661                let count = raw.len() / T::element_size();
1662                let mut result = Vec::<T>::with_capacity(count);
1663                unsafe {
1664                    std::ptr::copy_nonoverlapping(
1665                        raw.as_ptr(),
1666                        result.as_mut_ptr() as *mut u8,
1667                        raw.len(),
1668                    );
1669                    result.set_len(count);
1670                }
1671                Ok(result)
1672            }
1673            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1674                "cannot read_slice from a dataset in write mode".into(),
1675            )),
1676        }
1677    }
1678
1679    /// Write a typed slice to a sub-region of a contiguous dataset.
1680    ///
1681    /// `starts` and `counts` define the N-dimensional selection.
1682    pub fn write_slice<T: H5Type>(
1683        &self,
1684        starts: &[usize],
1685        counts: &[usize],
1686        data: &[T],
1687    ) -> Result<()> {
1688        match &self.info {
1689            DatasetInfo::Writer {
1690                index,
1691                element_size,
1692                chunked,
1693                ..
1694            } => {
1695                if *chunked {
1696                    return Err(Hdf5Error::InvalidState(
1697                        "write_slice is only for contiguous datasets".into(),
1698                    ));
1699                }
1700                if T::element_size() != *element_size {
1701                    return Err(Hdf5Error::TypeMismatch(format!(
1702                        "write type has element size {} but dataset expects {}",
1703                        T::element_size(),
1704                        element_size,
1705                    )));
1706                }
1707
1708                let expected: usize = counts.iter().product();
1709                if data.len() != expected {
1710                    return Err(Hdf5Error::InvalidState(format!(
1711                        "data length {} does not match slice size {}",
1712                        data.len(),
1713                        expected,
1714                    )));
1715                }
1716
1717                let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
1718                let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
1719
1720                let byte_len = data.len() * T::element_size();
1721                let raw =
1722                    unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
1723
1724                let inner = borrow_inner(&self.file_inner);
1725                match &*inner {
1726                    H5FileInner::Writer(writer) => {
1727                        writer.write_slice(*index, &starts_u64, &counts_u64, raw)?;
1728                        Ok(())
1729                    }
1730                    _ => Err(Hdf5Error::InvalidState(
1731                        "file is no longer in write mode".into(),
1732                    )),
1733                }
1734            }
1735            DatasetInfo::Reader { .. } => {
1736                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1737            }
1738        }
1739    }
1740
1741    /// Read variable-length strings from a dataset.
1742    ///
1743    /// This handles h5py-style vlen string datasets that store strings
1744    /// as global heap references. Returns one String per element.
1745    pub fn read_vlen_strings(&self) -> Result<Vec<String>> {
1746        match &self.info {
1747            DatasetInfo::Reader { name, .. } => {
1748                let mut inner = borrow_inner_mut(&self.file_inner);
1749                match &mut *inner {
1750                    H5FileInner::Reader(reader) => Ok(reader.read_vlen_strings(name)?),
1751                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
1752                }
1753            }
1754            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1755                "cannot read vlen strings from a dataset in write mode".into(),
1756            )),
1757        }
1758    }
1759
1760    /// Read variable-length byte arrays from a dataset.
1761    ///
1762    /// This handles vlen byte-array datasets (a vlen sequence of `u8`, e.g.
1763    /// those written by [`write_vlen_bytes`](crate::H5File::write_vlen_bytes))
1764    /// that store each element as a global heap reference. Returns one
1765    /// `Vec<u8>` per element.
1766    pub fn read_vlen_bytes(&self) -> Result<Vec<Vec<u8>>> {
1767        match &self.info {
1768            DatasetInfo::Reader { name, .. } => {
1769                let mut inner = borrow_inner_mut(&self.file_inner);
1770                match &mut *inner {
1771                    H5FileInner::Reader(reader) => Ok(reader.read_vlen_bytes(name)?),
1772                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
1773                }
1774            }
1775            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1776                "cannot read vlen bytes from a dataset in write mode".into(),
1777            )),
1778        }
1779    }
1780
1781    /// Read the entire dataset as a typed vector.
1782    ///
1783    /// The raw bytes are read from the file and reinterpreted as `T`. The
1784    /// caller must ensure that `T` matches the datatype used when the dataset
1785    /// was written.
1786    ///
1787    /// # Errors
1788    ///
1789    /// Returns an error if:
1790    /// - The file is in write mode.
1791    /// - The raw data size is not a multiple of `T::element_size()`.
1792    pub fn read_raw<T: H5Type>(&self) -> Result<Vec<T>> {
1793        match &self.info {
1794            DatasetInfo::Reader {
1795                name, element_size, ..
1796            } => {
1797                if T::element_size() != *element_size {
1798                    return Err(Hdf5Error::TypeMismatch(format!(
1799                        "read type has element size {} but dataset has element size {}",
1800                        T::element_size(),
1801                        element_size,
1802                    )));
1803                }
1804
1805                let raw = {
1806                    let mut inner = borrow_inner_mut(&self.file_inner);
1807                    match &mut *inner {
1808                        H5FileInner::Reader(reader) => reader.read_dataset_raw(name)?,
1809                        _ => {
1810                            return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
1811                        }
1812                    }
1813                };
1814
1815                if raw.len() % T::element_size() != 0 {
1816                    return Err(Hdf5Error::TypeMismatch(format!(
1817                        "raw data size {} is not a multiple of element size {}",
1818                        raw.len(),
1819                        T::element_size(),
1820                    )));
1821                }
1822
1823                let count = raw.len() / T::element_size();
1824                let mut result = Vec::<T>::with_capacity(count);
1825
1826                // Safety: T is Copy + 'static (required by H5Type). We verified
1827                // the byte count matches count * size_of::<T>() above.
1828                // copy_nonoverlapping fills the memory with valid bit patterns
1829                // for all H5Type implementors (numeric primitives).
1830                // We call set_len AFTER the copy so that if an unexpected panic
1831                // occurs, uninitialized memory is never exposed.
1832                unsafe {
1833                    std::ptr::copy_nonoverlapping(
1834                        raw.as_ptr(),
1835                        result.as_mut_ptr() as *mut u8,
1836                        raw.len(),
1837                    );
1838                    result.set_len(count);
1839                }
1840
1841                Ok(result)
1842            }
1843            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1844                "cannot read from a dataset in write mode".into(),
1845            )),
1846        }
1847    }
1848
1849    /// Read the raw byte image of a dataset without an `H5Type` carrier.
1850    ///
1851    /// The counterpart to [`write_raw_bytes`](Self::write_raw_bytes): returns
1852    /// the element bytes verbatim regardless of the on-disk element type, so a
1853    /// runtime [`CompoundType`](crate::types::CompoundType) whose records have
1854    /// no matching Rust primitive can be read back and decoded by the caller.
1855    pub fn read_raw_bytes(&self) -> Result<Vec<u8>> {
1856        match &self.info {
1857            DatasetInfo::Reader { name, .. } => {
1858                let mut inner = borrow_inner_mut(&self.file_inner);
1859                match &mut *inner {
1860                    H5FileInner::Reader(reader) => Ok(reader.read_dataset_raw(name)?),
1861                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
1862                }
1863            }
1864            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1865                "cannot read from a dataset in write mode".into(),
1866            )),
1867        }
1868    }
1869
1870    /// Read the whole dataset into a caller-provided buffer, with no allocation.
1871    ///
1872    /// `out` must have exactly `product(dims)` elements (the dataset's element
1873    /// count) and `T::element_size()` must match the dataset's on-disk element
1874    /// size, otherwise an error is returned and `out` is left unspecified. The
1875    /// zero-copy counterpart of [`read_raw`](Self::read_raw): the bytes are read
1876    /// straight into `out` rather than into a fresh `Vec`, so a pinned /
1877    /// page-locked host buffer can be filled in one pass and DMA'd to a GPU
1878    /// without the extra staging copy a `read_raw` + copy-into-pinned would
1879    /// incur. Works for every layout (contiguous, compact, and chunked under
1880    /// any index); for chunked data each decoded chunk is scattered directly
1881    /// into `out`.
1882    ///
1883    /// ```no_run
1884    /// # use rust_hdf5::H5File;
1885    /// let file = H5File::open("data.h5").unwrap();
1886    /// let ds = file.dataset("frames").unwrap();
1887    /// let n: usize = ds.shape().iter().product();
1888    /// let mut buf = vec![0u16; n];           // or a pinned host allocation
1889    /// ds.read_raw_into(&mut buf).unwrap();
1890    /// ```
1891    pub fn read_raw_into<T: H5Type>(&self, out: &mut [T]) -> Result<()> {
1892        match &self.info {
1893            DatasetInfo::Reader {
1894                name, element_size, ..
1895            } => {
1896                if T::element_size() != *element_size {
1897                    return Err(Hdf5Error::TypeMismatch(format!(
1898                        "read type has element size {} but dataset has element size {}",
1899                        T::element_size(),
1900                        element_size,
1901                    )));
1902                }
1903                // Safety: `T: H5Type` is a `Copy` POD numeric with a defined
1904                // byte representation; every bit pattern the read writes is a
1905                // valid `T`. The byte view borrows `out` exclusively for this
1906                // call, and `out.len() * element_size` cannot overflow because
1907                // it is the byte length of an existing slice (<= isize::MAX).
1908                let byte_len = out.len() * T::element_size();
1909                let bytes = unsafe {
1910                    std::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut u8, byte_len)
1911                };
1912                let mut inner = borrow_inner_mut(&self.file_inner);
1913                match &mut *inner {
1914                    H5FileInner::Reader(reader) => Ok(reader.read_dataset_raw_into(name, bytes)?),
1915                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
1916                }
1917            }
1918            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1919                "cannot read from a dataset in write mode".into(),
1920            )),
1921        }
1922    }
1923
1924    /// Read a hyperslab into a caller-provided buffer, with no allocation.
1925    ///
1926    /// `out` must have exactly `product(counts)` elements and
1927    /// `T::element_size()` must match the dataset's element size. The zero-copy
1928    /// counterpart of [`read_slice`](Self::read_slice) and the slice analogue of
1929    /// [`read_raw_into`](Self::read_raw_into): only chunks overlapping the
1930    /// selection are read, and the selected bytes land directly in `out` — the
1931    /// entry point for reading one frame / block straight into a pinned host
1932    /// buffer for an H2D transfer.
1933    ///
1934    /// ```no_run
1935    /// # use rust_hdf5::H5File;
1936    /// let file = H5File::open("vol.h5").unwrap();
1937    /// let ds = file.dataset("vol").unwrap();   // shape [nz, ny, nx]
1938    /// let (ny, nx) = (ds.shape()[1], ds.shape()[2]);
1939    /// let mut frame = vec![0f32; ny * nx];     // or a pinned host allocation
1940    /// ds.read_slice_into(&mut frame, &[5, 0, 0], &[1, ny, nx]).unwrap();
1941    /// ```
1942    pub fn read_slice_into<T: H5Type>(
1943        &self,
1944        out: &mut [T],
1945        starts: &[usize],
1946        counts: &[usize],
1947    ) -> Result<()> {
1948        match &self.info {
1949            DatasetInfo::Reader {
1950                name, element_size, ..
1951            } => {
1952                if T::element_size() != *element_size {
1953                    return Err(Hdf5Error::TypeMismatch(format!(
1954                        "read type has element size {} but dataset has element size {}",
1955                        T::element_size(),
1956                        element_size,
1957                    )));
1958                }
1959                let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
1960                let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
1961                // Safety: see `read_raw_into` — `T: H5Type` POD, exclusive
1962                // borrow of `out`, byte length within bounds.
1963                let byte_len = out.len() * T::element_size();
1964                let bytes = unsafe {
1965                    std::slice::from_raw_parts_mut(out.as_mut_ptr() as *mut u8, byte_len)
1966                };
1967                let mut inner = borrow_inner_mut(&self.file_inner);
1968                match &mut *inner {
1969                    H5FileInner::Reader(reader) => {
1970                        Ok(reader.read_slice_into(name, &starts_u64, &counts_u64, bytes)?)
1971                    }
1972                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
1973                }
1974            }
1975            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1976                "cannot read from a dataset in write mode".into(),
1977            )),
1978        }
1979    }
1980}
1981
1982#[cfg(test)]
1983mod tests {
1984    use crate::H5File;
1985    use std::path::PathBuf;
1986
1987    fn temp_path(name: &str) -> PathBuf {
1988        // Include PID + a per-call atomic counter so that concurrent
1989        // cargo invocations and any kernel-level "lock not yet
1990        // released" races between sequential opens cannot collide.
1991        use std::sync::atomic::{AtomicU64, Ordering};
1992        static COUNTER: AtomicU64 = AtomicU64::new(0);
1993        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1994        std::env::temp_dir().join(format!(
1995            "hdf5_dataset_test_{}_{}_{}.h5",
1996            name,
1997            std::process::id(),
1998            n
1999        ))
2000    }
2001
2002    #[test]
2003    fn runtime_compound_via_datatype_override_and_raw_bytes() {
2004        use crate::format::messages::datatype::DatatypeMessage;
2005        use crate::types::{CompoundType, H5Type};
2006
2007        let path = temp_path("compound_raw");
2008        // A 12-byte packed compound with NO matching Rust primitive carrier,
2009        // so it can only be written through the datatype() override +
2010        // write_raw_bytes path (the runtime-CompoundType use case).
2011        let ct = CompoundType {
2012            members: vec![
2013                ("id".to_string(), i32::hdf5_type(), 0),
2014                ("val".to_string(), f64::hdf5_type(), 4),
2015            ],
2016            total_size: 12,
2017        };
2018        let recs: [(i32, f64); 3] = [(1, 2.5), (2, 3.5), (3, -4.0)];
2019        let mut bytes = Vec::new();
2020        for (id, val) in recs {
2021            bytes.extend_from_slice(&id.to_le_bytes());
2022            bytes.extend_from_slice(&val.to_le_bytes());
2023        }
2024
2025        {
2026            let file = H5File::create(&path).unwrap();
2027            let ds = file
2028                .new_dataset::<u8>()
2029                .datatype(ct.to_datatype())
2030                .shape([recs.len()])
2031                .create("records")
2032                .unwrap();
2033            ds.write_raw_bytes(&bytes).unwrap();
2034            file.close().unwrap();
2035        }
2036        {
2037            let file = H5File::open(&path).unwrap();
2038            let ds = file.dataset("records").unwrap();
2039            // The on-disk element type is the compound we specified (size 12),
2040            // not the u8 carrier.
2041            match ds.datatype().unwrap() {
2042                DatatypeMessage::Compound { size, members } => {
2043                    assert_eq!(size, 12);
2044                    assert_eq!(members.len(), 2);
2045                    assert_eq!(members[0].name, "id");
2046                    assert_eq!(members[0].offset, 0);
2047                    assert_eq!(members[1].name, "val");
2048                    assert_eq!(members[1].offset, 4);
2049                }
2050                other => panic!("expected compound datatype, got {other:?}"),
2051            }
2052            assert_eq!(ds.read_raw_bytes().unwrap(), bytes);
2053        }
2054        std::fs::remove_file(&path).ok();
2055    }
2056
2057    #[test]
2058    fn builder_requires_shape() {
2059        let path = temp_path("no_shape");
2060        let file = H5File::create(&path).unwrap();
2061        let result = file.new_dataset::<u8>().create("data");
2062        assert!(result.is_err());
2063        std::fs::remove_file(&path).ok();
2064    }
2065
2066    #[test]
2067    fn write_raw_size_mismatch() {
2068        let path = temp_path("size_mismatch");
2069        let file = H5File::create(&path).unwrap();
2070        let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
2071        // Provide 3 elements instead of 4
2072        let result = ds.write_raw(&[1u8, 2, 3]);
2073        assert!(result.is_err());
2074        std::fs::remove_file(&path).ok();
2075    }
2076
2077    // A: a filter set without explicit chunk dimensions must auto-chunk (whole
2078    // dataset = one chunk) rather than silently drop the filter on the
2079    // contiguous path. write_raw then populates that single chunk.
2080    #[cfg(feature = "deflate")]
2081    #[test]
2082    fn filter_without_chunk_autochunks_and_roundtrips() {
2083        let path = temp_path("autochunk_filter");
2084        let data: Vec<i32> = (0..8).collect();
2085        {
2086            let file = H5File::create(&path).unwrap();
2087            let ds = file
2088                .new_dataset::<i32>()
2089                .deflate(6)
2090                .shape([8])
2091                .create("seq")
2092                .unwrap();
2093            ds.write_raw(&data).unwrap();
2094            file.close().unwrap();
2095        }
2096        {
2097            let file = H5File::open(&path).unwrap();
2098            let ds = file.dataset("seq").unwrap();
2099            // The filter forced chunked storage: a single whole-dataset chunk.
2100            assert!(
2101                ds.is_chunked(),
2102                "auto-chunk did not produce chunked storage"
2103            );
2104            assert_eq!(ds.chunk_dims(), Some(vec![8]));
2105            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
2106        }
2107        std::fs::remove_file(&path).ok();
2108    }
2109
2110    // B: write_raw on an explicitly chunked + compressed dataset scatters the
2111    // full row-major image across a multi-chunk grid, including edge chunks
2112    // (7/3 -> 3,3,1 along dim0; 5/2 -> 2,2,1 along dim1).
2113    #[cfg(feature = "deflate")]
2114    #[test]
2115    fn write_raw_multichunk_edge_roundtrips() {
2116        let path = temp_path("multichunk_edge");
2117        let data: Vec<i32> = (0..35).collect(); // 7 x 5 row-major
2118        {
2119            let file = H5File::create(&path).unwrap();
2120            let ds = file
2121                .new_dataset::<i32>()
2122                .shape([7, 5])
2123                .chunk(&[3, 2])
2124                .deflate(4)
2125                .create("grid")
2126                .unwrap();
2127            ds.write_raw(&data).unwrap();
2128            file.close().unwrap();
2129        }
2130        {
2131            let file = H5File::open(&path).unwrap();
2132            let ds = file.dataset("grid").unwrap();
2133            assert_eq!(ds.shape(), vec![7, 5]);
2134            assert_eq!(ds.chunk_dims(), Some(vec![3, 2]));
2135            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
2136        }
2137        std::fs::remove_file(&path).ok();
2138    }
2139
2140    // B: write_raw on an unfiltered chunked dataset (previously rejected with
2141    // "use write_chunk for chunked datasets") now gathers and round-trips.
2142    #[test]
2143    fn write_raw_unfiltered_chunked_roundtrips() {
2144        let path = temp_path("chunked_unfiltered");
2145        let data: Vec<f64> = (0..12).map(|i| i as f64 * 1.5).collect(); // 4 x 3
2146        {
2147            let file = H5File::create(&path).unwrap();
2148            let ds = file
2149                .new_dataset::<f64>()
2150                .shape([4, 3])
2151                .chunk(&[2, 2])
2152                .create("m")
2153                .unwrap();
2154            ds.write_raw(&data).unwrap();
2155            file.close().unwrap();
2156        }
2157        {
2158            let file = H5File::open(&path).unwrap();
2159            let ds = file.dataset("m").unwrap();
2160            assert_eq!(ds.chunk_dims(), Some(vec![2, 2]));
2161            assert_eq!(ds.read_raw::<f64>().unwrap(), data);
2162        }
2163        std::fs::remove_file(&path).ok();
2164    }
2165
2166    #[test]
2167    fn roundtrip_u8_1d() {
2168        let path = temp_path("rt_u8_1d");
2169        let data: Vec<u8> = (0..10).collect();
2170
2171        {
2172            let file = H5File::create(&path).unwrap();
2173            let ds = file.new_dataset::<u8>().shape([10]).create("seq").unwrap();
2174            ds.write_raw(&data).unwrap();
2175            file.close().unwrap();
2176        }
2177
2178        {
2179            let file = H5File::open(&path).unwrap();
2180            let ds = file.dataset("seq").unwrap();
2181            assert_eq!(ds.shape(), vec![10]);
2182            let readback = ds.read_raw::<u8>().unwrap();
2183            assert_eq!(readback, data);
2184        }
2185
2186        std::fs::remove_file(&path).ok();
2187    }
2188
2189    #[test]
2190    fn roundtrip_i32_2d() {
2191        let path = temp_path("rt_i32_2d");
2192        let data: Vec<i32> = vec![-1, 0, 1, 2, 3, 4];
2193
2194        {
2195            let file = H5File::create(&path).unwrap();
2196            let ds = file
2197                .new_dataset::<i32>()
2198                .shape([2, 3])
2199                .create("matrix")
2200                .unwrap();
2201            ds.write_raw(&data).unwrap();
2202            file.close().unwrap();
2203        }
2204
2205        {
2206            let file = H5File::open(&path).unwrap();
2207            let ds = file.dataset("matrix").unwrap();
2208            assert_eq!(ds.shape(), vec![2, 3]);
2209            let readback = ds.read_raw::<i32>().unwrap();
2210            assert_eq!(readback, data);
2211        }
2212
2213        std::fs::remove_file(&path).ok();
2214    }
2215
2216    #[test]
2217    fn roundtrip_f64_3d() {
2218        let path = temp_path("rt_f64_3d");
2219        let data: Vec<f64> = (0..24).map(|i| i as f64 * 0.5).collect();
2220
2221        {
2222            let file = H5File::create(&path).unwrap();
2223            let ds = file
2224                .new_dataset::<f64>()
2225                .shape([2, 3, 4])
2226                .create("cube")
2227                .unwrap();
2228            ds.write_raw(&data).unwrap();
2229            file.close().unwrap();
2230        }
2231
2232        {
2233            let file = H5File::open(&path).unwrap();
2234            let ds = file.dataset("cube").unwrap();
2235            assert_eq!(ds.shape(), vec![2, 3, 4]);
2236            let readback = ds.read_raw::<f64>().unwrap();
2237            assert_eq!(readback, data);
2238        }
2239
2240        std::fs::remove_file(&path).ok();
2241    }
2242
2243    #[test]
2244    fn cannot_read_in_write_mode() {
2245        let path = temp_path("no_read_write");
2246        let file = H5File::create(&path).unwrap();
2247        let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
2248        ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2249        let result = ds.read_raw::<u8>();
2250        assert!(result.is_err());
2251        std::fs::remove_file(&path).ok();
2252    }
2253
2254    #[test]
2255    fn cannot_write_in_read_mode() {
2256        let path = temp_path("no_write_read");
2257
2258        {
2259            let file = H5File::create(&path).unwrap();
2260            let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
2261            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2262            file.close().unwrap();
2263        }
2264
2265        {
2266            let file = H5File::open(&path).unwrap();
2267            let ds = file.dataset("x").unwrap();
2268            let result = ds.write_raw(&[5u8, 6, 7, 8]);
2269            assert!(result.is_err());
2270        }
2271
2272        std::fs::remove_file(&path).ok();
2273    }
2274
2275    #[test]
2276    fn numeric_attr_roundtrip() {
2277        let path = temp_path("num_attr");
2278        {
2279            let file = H5File::create(&path).unwrap();
2280            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
2281            ds.write_raw(&[1.0f32; 4]).unwrap();
2282
2283            let a1 = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
2284            a1.write_numeric(&1.2345f64).unwrap();
2285
2286            let a2 = ds.new_attr::<i32>().shape(()).create("count").unwrap();
2287            a2.write_numeric(&42i32).unwrap();
2288
2289            file.close().unwrap();
2290        }
2291        {
2292            let file = H5File::open(&path).unwrap();
2293            let ds = file.dataset("data").unwrap();
2294
2295            let scale = ds.attr("scale").unwrap();
2296            let val: f64 = scale.read_numeric().unwrap();
2297            assert!((val - 1.2345).abs() < 1e-10);
2298
2299            let count = ds.attr("count").unwrap();
2300            let val: i32 = count.read_numeric().unwrap();
2301            assert_eq!(val, 42);
2302        }
2303        std::fs::remove_file(&path).ok();
2304    }
2305
2306    #[test]
2307    fn array_attr_roundtrip() {
2308        let path = temp_path("array_attr");
2309        let offsets = [10i32, -20, 30];
2310        {
2311            let file = H5File::create(&path).unwrap();
2312            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
2313            ds.write_raw(&[1.0f32; 4]).unwrap();
2314
2315            // 1-D int32 array attribute (NDArrayDimOffset-style).
2316            let a = ds
2317                .new_attr::<i32>()
2318                .shape([3])
2319                .create("dim_offset")
2320                .unwrap();
2321            a.write_array(&offsets).unwrap();
2322
2323            // Wrong element count is rejected.
2324            let bad = ds.new_attr::<i32>().shape([3]).create("bad").unwrap();
2325            assert!(bad.write_array(&[1i32, 2]).is_err());
2326
2327            file.close().unwrap();
2328        }
2329        {
2330            let file = H5File::open(&path).unwrap();
2331            let ds = file.dataset("data").unwrap();
2332            let a = ds.attr("dim_offset").unwrap();
2333            let raw = a.read_raw().unwrap();
2334            assert_eq!(raw.len(), 3 * 4);
2335            let got: Vec<i32> = raw
2336                .chunks_exact(4)
2337                .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
2338                .collect();
2339            assert_eq!(got, offsets);
2340        }
2341        std::fs::remove_file(&path).ok();
2342    }
2343
2344    #[test]
2345    fn attr_datatype_exposes_class_and_sign() {
2346        // H5Attribute::datatype() must report the stored datatype class and
2347        // signedness so a generic attr->metadata mapper need not infer it from
2348        // the byte width (the HDF5-L1 adapter blocker this accessor unblocks).
2349        use crate::format::messages::datatype::DatatypeMessage;
2350
2351        let path = temp_path("attr_datatype");
2352        {
2353            let file = H5File::create(&path).unwrap();
2354            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
2355            ds.new_attr::<f64>()
2356                .shape(())
2357                .create("scale")
2358                .unwrap()
2359                .write_numeric(&1.5f64)
2360                .unwrap();
2361            ds.new_attr::<i32>()
2362                .shape(())
2363                .create("count")
2364                .unwrap()
2365                .write_numeric(&7i32)
2366                .unwrap();
2367            file.close().unwrap();
2368        }
2369        {
2370            let file = H5File::open(&path).unwrap();
2371            let ds = file.dataset("data").unwrap();
2372
2373            match ds.attr("scale").unwrap().datatype().unwrap() {
2374                DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 8),
2375                other => panic!("expected FloatingPoint for f64 attr, got {other:?}"),
2376            }
2377
2378            match ds.attr("count").unwrap().datatype().unwrap() {
2379                DatatypeMessage::FixedPoint { size, signed, .. } => {
2380                    assert_eq!(size, 4);
2381                    assert!(signed, "i32 attr must be signed");
2382                }
2383                other => panic!("expected FixedPoint for i32 attr, got {other:?}"),
2384            }
2385        }
2386        std::fs::remove_file(&path).ok();
2387    }
2388
2389    #[test]
2390    fn attr_datatype_in_write_mode_errors() {
2391        let path = temp_path("attr_datatype_write_mode");
2392        let file = H5File::create(&path).unwrap();
2393        let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
2394        let attr = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
2395        assert!(attr.datatype().is_err());
2396        std::fs::remove_file(&path).ok();
2397    }
2398
2399    #[test]
2400    fn cannot_create_dataset_in_read_mode() {
2401        let path = temp_path("no_create_read");
2402
2403        {
2404            let _file = H5File::create(&path).unwrap();
2405        }
2406
2407        {
2408            let file = H5File::open(&path).unwrap();
2409            let result = file.new_dataset::<u8>().shape([4]).create("x");
2410            assert!(result.is_err());
2411        }
2412
2413        std::fs::remove_file(&path).ok();
2414    }
2415
2416    #[test]
2417    fn shape_accessor() {
2418        let path = temp_path("shape_acc");
2419
2420        let file = H5File::create(&path).unwrap();
2421        let ds = file
2422            .new_dataset::<f32>()
2423            .shape([5, 10, 3])
2424            .create("tensor")
2425            .unwrap();
2426        assert_eq!(ds.shape(), vec![5, 10, 3]);
2427
2428        std::fs::remove_file(&path).ok();
2429    }
2430
2431    #[test]
2432    fn slice_roundtrip_2d() {
2433        let path = temp_path("slice_2d");
2434
2435        // Create a 4x5 dataset, write full, then read a slice
2436        let data: Vec<i32> = (0..20).collect();
2437        {
2438            let file = H5File::create(&path).unwrap();
2439            let ds = file
2440                .new_dataset::<i32>()
2441                .shape([4, 5])
2442                .create("mat")
2443                .unwrap();
2444            ds.write_raw(&data).unwrap();
2445            file.close().unwrap();
2446        }
2447        {
2448            let file = H5File::open(&path).unwrap();
2449            let ds = file.dataset("mat").unwrap();
2450            // Read rows 1..3, cols 2..4 (2x2 slice)
2451            let slice = ds.read_slice::<i32>(&[1, 2], &[2, 2]).unwrap();
2452            // Row 1: [5,6,7,8,9] -> cols 2..4 = [7,8]
2453            // Row 2: [10,11,12,13,14] -> cols 2..4 = [12,13]
2454            assert_eq!(slice, vec![7, 8, 12, 13]);
2455        }
2456
2457        std::fs::remove_file(&path).ok();
2458    }
2459
2460    // H2D zero-alloc reads. `read_raw_into` / `read_slice_into` fill a
2461    // caller-provided buffer and MUST produce byte-for-byte the same data as
2462    // their Vec-returning counterparts (`read_raw` / `read_slice`) on every
2463    // creatable layout, since both now share one buffer-filling core.
2464    fn assert_into_matches<T>(ds: &super::H5Dataset, starts: &[usize], counts: &[usize])
2465    where
2466        T: crate::types::H5Type + Copy + std::fmt::Debug + PartialEq + Default,
2467    {
2468        let n: usize = ds.shape().iter().product();
2469        let want_full = ds.read_raw::<T>().unwrap();
2470        let mut got_full = vec![T::default(); n];
2471        ds.read_raw_into::<T>(&mut got_full).unwrap();
2472        assert_eq!(got_full, want_full, "read_raw_into != read_raw");
2473
2474        let want_slice = ds.read_slice::<T>(starts, counts).unwrap();
2475        let sn: usize = counts.iter().product();
2476        let mut got_slice = vec![T::default(); sn];
2477        ds.read_slice_into::<T>(&mut got_slice, starts, counts)
2478            .unwrap();
2479        assert_eq!(got_slice, want_slice, "read_slice_into != read_slice");
2480    }
2481
2482    #[test]
2483    fn read_into_matches_vec_contiguous() {
2484        let path = temp_path("into_contig");
2485        let data: Vec<i32> = (0..20).collect(); // 4 x 5 contiguous
2486        {
2487            let file = H5File::create(&path).unwrap();
2488            let ds = file
2489                .new_dataset::<i32>()
2490                .shape([4, 5])
2491                .create("mat")
2492                .unwrap();
2493            ds.write_raw(&data).unwrap();
2494            file.close().unwrap();
2495        }
2496        {
2497            let file = H5File::open(&path).unwrap();
2498            let ds = file.dataset("mat").unwrap();
2499            assert_eq!(ds.chunk_dims(), None);
2500            assert_into_matches::<i32>(&ds, &[1, 2], &[2, 2]);
2501        }
2502        std::fs::remove_file(&path).ok();
2503    }
2504
2505    #[test]
2506    fn read_into_matches_vec_chunked_unfiltered() {
2507        let path = temp_path("into_chunk");
2508        let data: Vec<f64> = (0..35).map(|i| i as f64 * 1.5).collect(); // 7 x 5
2509        {
2510            let file = H5File::create(&path).unwrap();
2511            let ds = file
2512                .new_dataset::<f64>()
2513                .shape([7, 5])
2514                .chunk(&[3, 2]) // multi-chunk grid with edge chunks
2515                .create("grid")
2516                .unwrap();
2517            ds.write_raw(&data).unwrap();
2518            file.close().unwrap();
2519        }
2520        {
2521            let file = H5File::open(&path).unwrap();
2522            let ds = file.dataset("grid").unwrap();
2523            assert_eq!(ds.chunk_dims(), Some(vec![3, 2]));
2524            // Slice spans multiple chunks (rows 2..5, cols 1..4).
2525            assert_into_matches::<f64>(&ds, &[2, 1], &[3, 3]);
2526        }
2527        std::fs::remove_file(&path).ok();
2528    }
2529
2530    #[test]
2531    fn read_into_matches_vec_single_chunk() {
2532        let path = temp_path("into_single_chunk");
2533        let data: Vec<i32> = (0..12).collect(); // 3 x 4, one chunk covers all
2534        {
2535            let file = H5File::create(&path).unwrap();
2536            let ds = file
2537                .new_dataset::<i32>()
2538                .shape([3, 4])
2539                .chunk(&[3, 4]) // chunk == shape -> SingleChunk index
2540                .create("g")
2541                .unwrap();
2542            ds.write_raw(&data).unwrap();
2543            file.close().unwrap();
2544        }
2545        {
2546            let file = H5File::open(&path).unwrap();
2547            let ds = file.dataset("g").unwrap();
2548            assert_eq!(ds.chunk_dims(), Some(vec![3, 4]));
2549            assert_into_matches::<i32>(&ds, &[1, 1], &[2, 2]);
2550        }
2551        std::fs::remove_file(&path).ok();
2552    }
2553
2554    #[cfg(feature = "deflate")]
2555    #[test]
2556    fn read_into_matches_vec_chunked_deflate() {
2557        let path = temp_path("into_chunk_deflate");
2558        let data: Vec<i32> = (0..35).collect(); // 7 x 5
2559        {
2560            let file = H5File::create(&path).unwrap();
2561            let ds = file
2562                .new_dataset::<i32>()
2563                .shape([7, 5])
2564                .chunk(&[3, 2])
2565                .deflate(4)
2566                .create("grid")
2567                .unwrap();
2568            ds.write_raw(&data).unwrap();
2569            file.close().unwrap();
2570        }
2571        {
2572            let file = H5File::open(&path).unwrap();
2573            let ds = file.dataset("grid").unwrap();
2574            assert_eq!(ds.chunk_dims(), Some(vec![3, 2]));
2575            assert_into_matches::<i32>(&ds, &[2, 1], &[3, 3]);
2576        }
2577        std::fs::remove_file(&path).ok();
2578    }
2579
2580    #[test]
2581    fn read_into_wrong_buffer_size_rejected() {
2582        let path = temp_path("into_badlen");
2583        let data: Vec<i32> = (0..20).collect(); // 4 x 5
2584        {
2585            let file = H5File::create(&path).unwrap();
2586            let ds = file
2587                .new_dataset::<i32>()
2588                .shape([4, 5])
2589                .create("mat")
2590                .unwrap();
2591            ds.write_raw(&data).unwrap();
2592            file.close().unwrap();
2593        }
2594        {
2595            let file = H5File::open(&path).unwrap();
2596            let ds = file.dataset("mat").unwrap();
2597
2598            // Too small / too large full-read buffers are both rejected.
2599            let mut small = vec![0i32; 19];
2600            assert!(ds.read_raw_into::<i32>(&mut small).is_err());
2601            let mut large = vec![0i32; 21];
2602            assert!(ds.read_raw_into::<i32>(&mut large).is_err());
2603
2604            // Slice buffer must be exactly product(counts) = 4.
2605            let mut bad_slice = vec![0i32; 3];
2606            assert!(ds
2607                .read_slice_into::<i32>(&mut bad_slice, &[1, 2], &[2, 2])
2608                .is_err());
2609            // The correctly sized slice buffer succeeds.
2610            let mut ok_slice = vec![0i32; 4];
2611            assert!(ds
2612                .read_slice_into::<i32>(&mut ok_slice, &[1, 2], &[2, 2])
2613                .is_ok());
2614        }
2615        std::fs::remove_file(&path).ok();
2616    }
2617
2618    #[test]
2619    fn read_into_wrong_element_size_rejected() {
2620        let path = temp_path("into_badtype");
2621        let data: Vec<i32> = (0..20).collect(); // element size 4
2622        {
2623            let file = H5File::create(&path).unwrap();
2624            let ds = file
2625                .new_dataset::<i32>()
2626                .shape([4, 5])
2627                .create("mat")
2628                .unwrap();
2629            ds.write_raw(&data).unwrap();
2630            file.close().unwrap();
2631        }
2632        {
2633            let file = H5File::open(&path).unwrap();
2634            let ds = file.dataset("mat").unwrap();
2635            // u8 (size 1) and i64 (size 8) mismatch the dataset's 4-byte
2636            // element size -> TypeMismatch, even with a "correctly sized" Vec.
2637            let mut as_u8 = vec![0u8; 20];
2638            assert!(matches!(
2639                ds.read_raw_into::<u8>(&mut as_u8),
2640                Err(crate::Hdf5Error::TypeMismatch(_))
2641            ));
2642            let mut as_i64 = vec![0i64; 20];
2643            assert!(matches!(
2644                ds.read_slice_into::<i64>(&mut as_i64, &[0, 0], &[4, 5]),
2645                Err(crate::Hdf5Error::TypeMismatch(_))
2646            ));
2647        }
2648        std::fs::remove_file(&path).ok();
2649    }
2650
2651    #[test]
2652    fn write_slice_2d() {
2653        let path = temp_path("write_slice_2d");
2654
2655        {
2656            let file = H5File::create(&path).unwrap();
2657            let ds = file
2658                .new_dataset::<f32>()
2659                .shape([3, 4])
2660                .create("data")
2661                .unwrap();
2662            ds.write_raw(&[0.0f32; 12]).unwrap();
2663            // Overwrite a 2x2 sub-region
2664            ds.write_slice(&[1, 1], &[2, 2], &[10.0f32, 20.0, 30.0, 40.0])
2665                .unwrap();
2666            file.close().unwrap();
2667        }
2668        {
2669            let file = H5File::open(&path).unwrap();
2670            let ds = file.dataset("data").unwrap();
2671            let full = ds.read_raw::<f32>().unwrap();
2672            // Row 0: [0,0,0,0]
2673            // Row 1: [0,10,20,0]
2674            // Row 2: [0,30,40,0]
2675            assert_eq!(
2676                full,
2677                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,]
2678            );
2679        }
2680
2681        std::fs::remove_file(&path).ok();
2682    }
2683
2684    #[test]
2685    fn write_slice_out_of_bounds_rejected() {
2686        let path = temp_path("write_slice_oob");
2687        let file = H5File::create(&path).unwrap();
2688        let ds = file.new_dataset::<i32>().shape([4]).create("d").unwrap();
2689        ds.write_raw(&[0i32; 4]).unwrap();
2690        // start 2 + count 6 = 8 > extent 4 -> must error, not corrupt.
2691        assert!(ds.write_slice(&[2], &[6], &[9i32; 6]).is_err());
2692        // An in-bounds slice still works.
2693        assert!(ds.write_slice(&[1], &[2], &[7i32, 8]).is_ok());
2694        std::fs::remove_file(&path).ok();
2695    }
2696
2697    #[test]
2698    fn duplicate_dataset_name_rejected() {
2699        let path = temp_path("dup_name");
2700        let file = H5File::create(&path).unwrap();
2701        let _ = file.new_dataset::<i32>().shape([2]).create("d").unwrap();
2702        assert!(file.new_dataset::<i32>().shape([2]).create("d").is_err());
2703        std::fs::remove_file(&path).ok();
2704    }
2705
2706    #[test]
2707    fn extend_cannot_shrink() {
2708        let path = temp_path("extend_shrink");
2709        let file = H5File::create(&path).unwrap();
2710        let ds = file
2711            .new_dataset::<i32>()
2712            .shape([0])
2713            .chunk(&[2])
2714            .max_shape(&[None])
2715            .create("d")
2716            .unwrap();
2717        ds.append(&[1i32, 2, 3, 4]).unwrap();
2718        // Shrinking below the written extent must be rejected.
2719        assert!(ds.extend(&[2]).is_err());
2720        // Growing is fine.
2721        assert!(ds.extend(&[6]).is_ok());
2722        std::fs::remove_file(&path).ok();
2723    }
2724
2725    #[test]
2726    fn attr_read_roundtrip() {
2727        use crate::types::VarLenUnicode;
2728        let path = temp_path("attr_read");
2729
2730        {
2731            let file = H5File::create(&path).unwrap();
2732            let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
2733            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2734            let a1 = ds
2735                .new_attr::<VarLenUnicode>()
2736                .shape(())
2737                .create("units")
2738                .unwrap();
2739            a1.write_string("meters").unwrap();
2740            let a2 = ds
2741                .new_attr::<VarLenUnicode>()
2742                .shape(())
2743                .create("desc")
2744                .unwrap();
2745            a2.write_string("test data").unwrap();
2746            file.close().unwrap();
2747        }
2748        {
2749            let file = H5File::open(&path).unwrap();
2750            let ds = file.dataset("data").unwrap();
2751
2752            let names = ds.attr_names().unwrap();
2753            assert!(names.contains(&"units".to_string()));
2754            assert!(names.contains(&"desc".to_string()));
2755
2756            let units = ds.attr("units").unwrap();
2757            assert_eq!(units.read_string().unwrap(), "meters");
2758
2759            let desc = ds.attr("desc").unwrap();
2760            assert_eq!(desc.read_string().unwrap(), "test data");
2761        }
2762
2763        std::fs::remove_file(&path).ok();
2764    }
2765
2766    #[test]
2767    fn type_mismatch_element_size() {
2768        let path = temp_path("type_mismatch");
2769
2770        {
2771            let file = H5File::create(&path).unwrap();
2772            let ds = file.new_dataset::<f64>().shape([4]).create("data").unwrap();
2773            ds.write_raw(&[1.0f64, 2.0, 3.0, 4.0]).unwrap();
2774            file.close().unwrap();
2775        }
2776
2777        {
2778            let file = H5File::open(&path).unwrap();
2779            let ds = file.dataset("data").unwrap();
2780            // Try to read as u8 (element_size = 1) from a f64 dataset (element_size = 8)
2781            let result = ds.read_raw::<u8>();
2782            assert!(result.is_err());
2783        }
2784
2785        std::fs::remove_file(&path).ok();
2786    }
2787
2788    #[test]
2789    fn dataset_survives_file_move() {
2790        let path = temp_path("ds_survives");
2791
2792        let ds = {
2793            let file = H5File::create(&path).unwrap();
2794            file.new_dataset::<u8>().shape([4]).create("x").unwrap()
2795        };
2796        // file is dropped here, but ds still holds Rc to the inner state
2797        ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2798        // The writer will finalize on drop of the last Rc
2799
2800        std::fs::remove_file(&path).ok();
2801    }
2802
2803    #[test]
2804    fn new_attr_scalar_string() {
2805        use crate::types::VarLenUnicode;
2806
2807        let path = temp_path("attr_scalar_string");
2808        {
2809            let file = H5File::create(&path).unwrap();
2810            let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
2811            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2812
2813            let attr = ds
2814                .new_attr::<VarLenUnicode>()
2815                .shape(())
2816                .create("name")
2817                .unwrap();
2818            attr.write_scalar(&VarLenUnicode("test_value".to_string()))
2819                .unwrap();
2820
2821            file.close().unwrap();
2822        }
2823
2824        // Verify the file is still valid and readable
2825        {
2826            use crate::format::messages::datatype::DatatypeMessage;
2827            let file = H5File::open(&path).unwrap();
2828            let ds = file.dataset("data").unwrap();
2829            assert_eq!(ds.shape(), vec![4]);
2830            let readback = ds.read_raw::<u8>().unwrap();
2831            assert_eq!(readback, vec![1u8, 2, 3, 4]);
2832
2833            // The string attribute is stored as a true variable-length string
2834            // (not fixed-length) and round-trips its value.
2835            let attr = ds.attr("name").unwrap();
2836            assert!(
2837                matches!(
2838                    attr.datatype().unwrap(),
2839                    DatatypeMessage::VarLenString { .. }
2840                ),
2841                "string attribute should have a variable-length string datatype"
2842            );
2843            assert_eq!(attr.read_string().unwrap(), "test_value");
2844        }
2845
2846        std::fs::remove_file(&path).ok();
2847    }
2848
2849    #[test]
2850    fn all_numeric_types_roundtrip() {
2851        let path = temp_path("all_types");
2852
2853        {
2854            let file = H5File::create(&path).unwrap();
2855
2856            let ds = file.new_dataset::<u8>().shape([2]).create("u8").unwrap();
2857            ds.write_raw(&[1u8, 2]).unwrap();
2858
2859            let ds = file.new_dataset::<i8>().shape([2]).create("i8").unwrap();
2860            ds.write_raw(&[-1i8, 1]).unwrap();
2861
2862            let ds = file.new_dataset::<u16>().shape([2]).create("u16").unwrap();
2863            ds.write_raw(&[100u16, 200]).unwrap();
2864
2865            let ds = file.new_dataset::<i16>().shape([2]).create("i16").unwrap();
2866            ds.write_raw(&[-100i16, 100]).unwrap();
2867
2868            let ds = file.new_dataset::<u32>().shape([2]).create("u32").unwrap();
2869            ds.write_raw(&[1000u32, 2000]).unwrap();
2870
2871            let ds = file.new_dataset::<i32>().shape([2]).create("i32").unwrap();
2872            ds.write_raw(&[-1000i32, 1000]).unwrap();
2873
2874            let ds = file.new_dataset::<u64>().shape([2]).create("u64").unwrap();
2875            ds.write_raw(&[10000u64, 20000]).unwrap();
2876
2877            let ds = file.new_dataset::<i64>().shape([2]).create("i64").unwrap();
2878            ds.write_raw(&[-10000i64, 10000]).unwrap();
2879
2880            let ds = file.new_dataset::<f32>().shape([2]).create("f32").unwrap();
2881            ds.write_raw(&[1.5f32, 2.5]).unwrap();
2882
2883            let ds = file.new_dataset::<f64>().shape([2]).create("f64").unwrap();
2884            ds.write_raw(&[1.23456f64, 7.89012]).unwrap();
2885
2886            file.close().unwrap();
2887        }
2888
2889        {
2890            let file = H5File::open(&path).unwrap();
2891
2892            assert_eq!(
2893                file.dataset("u8").unwrap().read_raw::<u8>().unwrap(),
2894                vec![1u8, 2]
2895            );
2896            assert_eq!(
2897                file.dataset("i8").unwrap().read_raw::<i8>().unwrap(),
2898                vec![-1i8, 1]
2899            );
2900            assert_eq!(
2901                file.dataset("u16").unwrap().read_raw::<u16>().unwrap(),
2902                vec![100u16, 200]
2903            );
2904            assert_eq!(
2905                file.dataset("i16").unwrap().read_raw::<i16>().unwrap(),
2906                vec![-100i16, 100]
2907            );
2908            assert_eq!(
2909                file.dataset("u32").unwrap().read_raw::<u32>().unwrap(),
2910                vec![1000u32, 2000]
2911            );
2912            assert_eq!(
2913                file.dataset("i32").unwrap().read_raw::<i32>().unwrap(),
2914                vec![-1000i32, 1000]
2915            );
2916            assert_eq!(
2917                file.dataset("u64").unwrap().read_raw::<u64>().unwrap(),
2918                vec![10000u64, 20000]
2919            );
2920            assert_eq!(
2921                file.dataset("i64").unwrap().read_raw::<i64>().unwrap(),
2922                vec![-10000i64, 10000]
2923            );
2924            assert_eq!(
2925                file.dataset("f32").unwrap().read_raw::<f32>().unwrap(),
2926                vec![1.5f32, 2.5]
2927            );
2928            assert_eq!(
2929                file.dataset("f64").unwrap().read_raw::<f64>().unwrap(),
2930                vec![1.23456f64, 7.89012]
2931            );
2932        }
2933
2934        std::fs::remove_file(&path).ok();
2935    }
2936
2937    #[test]
2938    fn append_chunked_roundtrip() {
2939        let path = temp_path("append_chunked");
2940
2941        {
2942            let file = H5File::create(&path).unwrap();
2943            let ds = file
2944                .new_dataset::<f64>()
2945                .shape([0, 3])
2946                .chunk(&[1, 3])
2947                .max_shape(&[None, Some(3)])
2948                .create("data")
2949                .unwrap();
2950
2951            // Append one frame
2952            ds.append(&[1.0f64, 2.0, 3.0]).unwrap();
2953            // Append two frames at once
2954            ds.append(&[4.0f64, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap();
2955
2956            file.close().unwrap();
2957        }
2958
2959        {
2960            let file = H5File::open(&path).unwrap();
2961            let ds = file.dataset("data").unwrap();
2962            assert_eq!(ds.shape(), vec![3, 3]);
2963            let all = ds.read_raw::<f64>().unwrap();
2964            assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
2965        }
2966
2967        std::fs::remove_file(&path).ok();
2968    }
2969
2970    #[test]
2971    fn append_1d_chunked() {
2972        let path = temp_path("append_1d");
2973
2974        {
2975            let file = H5File::create(&path).unwrap();
2976            let ds = file
2977                .new_dataset::<i32>()
2978                .shape([0])
2979                .chunk(&[4])
2980                .max_shape(&[None])
2981                .create("values")
2982                .unwrap();
2983
2984            ds.append(&[10i32, 20, 30]).unwrap(); // partial chunk
2985            ds.append(&[40i32]).unwrap(); // fills chunk boundary
2986            ds.append(&[50i32, 60, 70, 80]).unwrap(); // full chunk
2987
2988            file.close().unwrap();
2989        }
2990
2991        {
2992            let file = H5File::open(&path).unwrap();
2993            let ds = file.dataset("values").unwrap();
2994            assert_eq!(ds.shape(), vec![8]);
2995            let all = ds.read_raw::<i32>().unwrap();
2996            assert_eq!(all, vec![10, 20, 30, 40, 50, 60, 70, 80]);
2997        }
2998
2999        std::fs::remove_file(&path).ok();
3000    }
3001
3002    #[test]
3003    fn append_partial_chunk_flushed_on_close() {
3004        let path = temp_path("append_partial_close");
3005
3006        {
3007            let file = H5File::create(&path).unwrap();
3008            let ds = file
3009                .new_dataset::<f64>()
3010                .shape([0])
3011                .chunk(&[4])
3012                .max_shape(&[None])
3013                .create("vals")
3014                .unwrap();
3015
3016            // Append 5 elements: chunk 0 = full [1,2,3,4], chunk 1 = partial [5,0,0,0]
3017            ds.append(&[1.0f64, 2.0, 3.0, 4.0, 5.0]).unwrap();
3018            file.close().unwrap();
3019        }
3020
3021        {
3022            let file = H5File::open(&path).unwrap();
3023            let ds = file.dataset("vals").unwrap();
3024            assert_eq!(ds.shape(), vec![5]);
3025            let all = ds.read_raw::<f64>().unwrap();
3026            // The full dataset is 2 chunks * 4 = 8 elements; shape says 5
3027            // read_raw reads total shape elements
3028            assert_eq!(all.len(), 5);
3029            assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
3030        }
3031
3032        std::fs::remove_file(&path).ok();
3033    }
3034
3035    #[cfg(feature = "deflate")]
3036    #[test]
3037    fn vlen_append_after_reopen_filtered() {
3038        // Reopen + append into a partially-written *compressed* vlen chunk
3039        // (index-block chunk). Exercises filtered-index-block reconstruction
3040        // in open_append plus filtered read-modify-write.
3041        let path = temp_path("vlen_reopen_filtered");
3042        {
3043            let file = H5File::create(&path).unwrap();
3044            file.create_appendable_vlen_dataset(
3045                "strs",
3046                4,
3047                Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
3048            )
3049            .unwrap();
3050            file.append_vlen_strings("strs", &["alpha", "beta", "gamma"])
3051                .unwrap();
3052            file.close().unwrap();
3053        }
3054        {
3055            let file = H5File::open_rw(&path).unwrap();
3056            file.append_vlen_strings("strs", &["delta"]).unwrap();
3057            file.close().unwrap();
3058        }
3059        {
3060            let file = H5File::open(&path).unwrap();
3061            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
3062            assert_eq!(
3063                got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
3064                vec!["alpha", "beta", "gamma", "delta"]
3065            );
3066        }
3067        std::fs::remove_file(&path).ok();
3068    }
3069
3070    #[test]
3071    fn vlen_append_after_reopen_data_block() {
3072        // Reopen + append into a partial chunk that lives in an extensible-
3073        // array *data block* (chunk index >= idx_blk_elmts). Exercises
3074        // data-block resolution in read_chunk_if_present and write_chunk.
3075        let path = temp_path("vlen_reopen_datablk");
3076        let labels: Vec<String> = (0..9).map(|i| format!("s{i}")).collect();
3077        {
3078            let file = H5File::create(&path).unwrap();
3079            file.create_appendable_vlen_dataset("strs", 2, None)
3080                .unwrap();
3081            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
3082            file.append_vlen_strings("strs", &refs).unwrap();
3083            file.close().unwrap();
3084        }
3085        {
3086            let file = H5File::open_rw(&path).unwrap();
3087            file.append_vlen_strings("strs", &["s9"]).unwrap();
3088            file.close().unwrap();
3089        }
3090        {
3091            let file = H5File::open(&path).unwrap();
3092            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
3093            let want: Vec<String> = (0..10).map(|i| format!("s{i}")).collect();
3094            assert_eq!(got, want);
3095        }
3096        std::fs::remove_file(&path).ok();
3097    }
3098
3099    #[test]
3100    fn vlen_append_after_reopen_super_block() {
3101        // Reopen + append into a partial chunk whose index falls in an
3102        // extensible-array *super block* (chunk index 244 with the default
3103        // EA geometry: idx_blk_elmts=4, data_blk_min_elmts=16,
3104        // sup_blk_min_data_ptrs=4 -> chunks 0..=243 are reached via the
3105        // index block or its direct data blocks, so chunk 244 is reached
3106        // via a super block read from disk). Exercises the ViaSblk branch
3107        // of read_chunk_if_present.
3108        let path = temp_path("vlen_reopen_super");
3109        // 489 strings, chunk size 2 -> chunk 244 holds one string only
3110        // (partially filled) and is flushed to disk on close.
3111        let labels: Vec<String> = (0..489).map(|i| format!("v{i}")).collect();
3112        {
3113            let file = H5File::create(&path).unwrap();
3114            file.create_appendable_vlen_dataset("strs", 2, None)
3115                .unwrap();
3116            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
3117            file.append_vlen_strings("strs", &refs).unwrap();
3118            file.close().unwrap();
3119        }
3120        {
3121            let file = H5File::open_rw(&path).unwrap();
3122            file.append_vlen_strings("strs", &["v489"]).unwrap();
3123            file.close().unwrap();
3124        }
3125        {
3126            let file = H5File::open(&path).unwrap();
3127            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
3128            let want: Vec<String> = (0..490).map(|i| format!("v{i}")).collect();
3129            assert_eq!(got, want);
3130        }
3131        std::fs::remove_file(&path).ok();
3132    }
3133
3134    #[cfg(feature = "deflate")]
3135    #[test]
3136    fn vlen_append_after_reopen_filtered_data_block() {
3137        // The hardest path: compressed + chunk in a data block + partial
3138        // read-modify-write across a reopen.
3139        let path = temp_path("vlen_reopen_filt_datablk");
3140        let labels: Vec<String> = (0..9).map(|i| format!("item{i:02}")).collect();
3141        {
3142            let file = H5File::create(&path).unwrap();
3143            file.create_appendable_vlen_dataset(
3144                "strs",
3145                2,
3146                Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
3147            )
3148            .unwrap();
3149            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
3150            file.append_vlen_strings("strs", &refs).unwrap();
3151            file.close().unwrap();
3152        }
3153        {
3154            let file = H5File::open_rw(&path).unwrap();
3155            file.append_vlen_strings("strs", &["item09"]).unwrap();
3156            file.close().unwrap();
3157        }
3158        {
3159            let file = H5File::open(&path).unwrap();
3160            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
3161            let want: Vec<String> = (0..10).map(|i| format!("item{i:02}")).collect();
3162            assert_eq!(got, want);
3163        }
3164        std::fs::remove_file(&path).ok();
3165    }
3166
3167    #[test]
3168    fn group_nx_class_attribute_roundtrip() {
3169        // Non-root groups carry attributes (NeXus `NX_class`) in their
3170        // own object header, and the reader reads them back by path.
3171        let path = temp_path("group_nx_class");
3172        {
3173            let file = H5File::create(&path).unwrap();
3174            let entry = file.create_group("entry").unwrap();
3175            entry.set_attr_string("NX_class", "NXentry").unwrap();
3176            let det = entry.create_group("detector").unwrap();
3177            det.set_attr_string("NX_class", "NXdetector").unwrap();
3178            det.set_attr_numeric("frame_count", &7i32).unwrap();
3179            det.new_dataset::<f32>()
3180                .shape([4])
3181                .create("data")
3182                .unwrap()
3183                .write_raw(&[1.0f32; 4])
3184                .unwrap();
3185            file.close().unwrap();
3186        }
3187        {
3188            let file = H5File::open(&path).unwrap();
3189            let entry = file.root_group().group("entry").unwrap();
3190            assert_eq!(entry.attr_string("NX_class").unwrap(), "NXentry");
3191            let det = entry.group("detector").unwrap();
3192            assert_eq!(det.attr_string("NX_class").unwrap(), "NXdetector");
3193            let names = det.attr_names().unwrap();
3194            assert!(names.contains(&"NX_class".to_string()));
3195            assert!(names.contains(&"frame_count".to_string()));
3196        }
3197        std::fs::remove_file(&path).ok();
3198    }
3199
3200    #[test]
3201    fn ea_super_block_roundtrip() {
3202        // 2000 chunks span several extensible-array super blocks. Before
3203        // super-block support the writer errored at chunk index 228.
3204        let path = temp_path("ea_super_rt");
3205        {
3206            let file = H5File::create(&path).unwrap();
3207            let ds = file
3208                .new_dataset::<i32>()
3209                .shape([0])
3210                .chunk(&[1])
3211                .max_shape(&[None])
3212                .create("v")
3213                .unwrap();
3214            ds.append(&(0..2000).collect::<Vec<i32>>()).unwrap();
3215            file.close().unwrap();
3216        }
3217        {
3218            let file = H5File::open(&path).unwrap();
3219            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3220            assert_eq!(v.len(), 2000);
3221            assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
3222        }
3223        std::fs::remove_file(&path).ok();
3224    }
3225
3226    #[cfg(feature = "deflate")]
3227    #[test]
3228    fn ea_filtered_super_block_roundtrip() {
3229        // Compressed chunks across super blocks.
3230        let path = temp_path("ea_filt_super");
3231        {
3232            let file = H5File::create(&path).unwrap();
3233            let ds = file
3234                .new_dataset::<i32>()
3235                .shape([0])
3236                .chunk(&[1])
3237                .max_shape(&[None])
3238                .deflate(4)
3239                .create("v")
3240                .unwrap();
3241            ds.append(&(0..600).collect::<Vec<i32>>()).unwrap();
3242            file.close().unwrap();
3243        }
3244        {
3245            let file = H5File::open(&path).unwrap();
3246            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3247            assert_eq!(v, (0..600).collect::<Vec<i32>>());
3248        }
3249        std::fs::remove_file(&path).ok();
3250    }
3251
3252    #[test]
3253    fn ea_super_block_open_append() {
3254        // Reopen a dataset and append chunks that fall in super blocks.
3255        let path = temp_path("ea_super_append");
3256        {
3257            let file = H5File::create(&path).unwrap();
3258            let ds = file
3259                .new_dataset::<i32>()
3260                .shape([0])
3261                .chunk(&[1])
3262                .max_shape(&[None])
3263                .create("v")
3264                .unwrap();
3265            ds.append(&(0..300).collect::<Vec<i32>>()).unwrap();
3266            file.close().unwrap();
3267        }
3268        {
3269            let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
3270            let idx = w.dataset_index("v").unwrap();
3271            for c in 300..900u64 {
3272                w.write_chunk(idx, c, &(c as i32).to_le_bytes()).unwrap();
3273            }
3274            w.extend_dataset(idx, &[900]).unwrap();
3275            w.close().unwrap();
3276        }
3277        {
3278            let file = H5File::open(&path).unwrap();
3279            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3280            assert_eq!(v.len(), 900);
3281            assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
3282        }
3283        std::fs::remove_file(&path).ok();
3284    }
3285
3286    #[test]
3287    fn btree_v2_multi_unlimited_roundtrip() {
3288        // A dataset with two unlimited dimensions uses the v2 B-tree chunk
3289        // index; chunks are written by grid coordinates with write_chunk_at.
3290        let path = temp_path("bt2_multi");
3291        {
3292            let file = H5File::create(&path).unwrap();
3293            let ds = file
3294                .new_dataset::<i32>()
3295                .shape([0, 0])
3296                .chunk(&[2, 2])
3297                .max_shape(&[None, None])
3298                .create("grid")
3299                .unwrap();
3300            assert!(ds.is_chunked());
3301            // 4x4 logical grid, value[r][c] = r*4 + c, in 2x2 chunks.
3302            for cr in 0..2usize {
3303                for cc in 0..2usize {
3304                    let mut bytes = Vec::new();
3305                    for i in 0..2usize {
3306                        for j in 0..2usize {
3307                            let v = ((cr * 2 + i) * 4 + (cc * 2 + j)) as i32;
3308                            bytes.extend_from_slice(&v.to_le_bytes());
3309                        }
3310                    }
3311                    ds.write_chunk_at(&[cr, cc], &bytes).unwrap();
3312                }
3313            }
3314            file.close().unwrap();
3315        }
3316        {
3317            let file = H5File::open(&path).unwrap();
3318            let ds = file.dataset("grid").unwrap();
3319            assert_eq!(ds.shape(), vec![4, 4]);
3320            assert_eq!(ds.read_raw::<i32>().unwrap(), (0..16).collect::<Vec<i32>>());
3321        }
3322        std::fs::remove_file(&path).ok();
3323    }
3324
3325    #[test]
3326    fn subframe_chunking_roundtrip() {
3327        // A chunk smaller than a frame: shape [N,8,8], chunk [1,4,4], so each
3328        // frame is tiled into a 2x2 grid of 4x4 chunks. write_chunk_at takes
3329        // the chunk-grid coordinates.
3330        let path = temp_path("subframe");
3331        {
3332            let file = H5File::create(&path).unwrap();
3333            let ds = file
3334                .new_dataset::<i32>()
3335                .shape([0, 8, 8])
3336                .chunk(&[1, 4, 4])
3337                .max_shape(&[None, Some(8), Some(8)])
3338                .create("v")
3339                .unwrap();
3340            for f in 0..3usize {
3341                for cr in 0..2usize {
3342                    for cc in 0..2usize {
3343                        let mut bytes = Vec::new();
3344                        for i in 0..4usize {
3345                            for j in 0..4usize {
3346                                let v = (f * 64 + (cr * 4 + i) * 8 + (cc * 4 + j)) as i32;
3347                                bytes.extend_from_slice(&v.to_le_bytes());
3348                            }
3349                        }
3350                        ds.write_chunk_at(&[f, cr, cc], &bytes).unwrap();
3351                    }
3352                }
3353            }
3354            file.close().unwrap();
3355        }
3356        {
3357            let file = H5File::open(&path).unwrap();
3358            let ds = file.dataset("v").unwrap();
3359            assert_eq!(ds.shape(), vec![3, 8, 8]);
3360            assert_eq!(
3361                ds.read_raw::<i32>().unwrap(),
3362                (0..192).collect::<Vec<i32>>()
3363            );
3364        }
3365        std::fs::remove_file(&path).ok();
3366    }
3367
3368    #[test]
3369    fn fill_value_contiguous_roundtrip() {
3370        let path = temp_path("fill_value_contig");
3371        {
3372            let file = H5File::create(&path).unwrap();
3373            let ds = file
3374                .new_dataset::<f32>()
3375                .shape([4])
3376                .fill_value(2.5f32)
3377                .create("data")
3378                .unwrap();
3379            ds.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
3380            file.close().unwrap();
3381        }
3382        // open_append decodes the fill-value message back from the header.
3383        {
3384            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
3385            let idx = writer.dataset_index("data").unwrap();
3386            assert_eq!(
3387                writer.ds(idx).lock().fill_value,
3388                Some(2.5f32.to_le_bytes().to_vec())
3389            );
3390        }
3391        // Data still reads back correctly.
3392        {
3393            let file = H5File::open(&path).unwrap();
3394            let ds = file.dataset("data").unwrap();
3395            assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
3396        }
3397        std::fs::remove_file(&path).ok();
3398    }
3399
3400    #[test]
3401    fn fill_value_chunked_roundtrip() {
3402        let path = temp_path("fill_value_chunked");
3403        {
3404            let file = H5File::create(&path).unwrap();
3405            let ds = file
3406                .new_dataset::<i32>()
3407                .shape([0])
3408                .chunk(&[4])
3409                .max_shape(&[None])
3410                .fill_value(-7i32)
3411                .create("vals")
3412                .unwrap();
3413            ds.append(&[1i32, 2, 3, 4]).unwrap();
3414            file.close().unwrap();
3415        }
3416        {
3417            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
3418            let idx = writer.dataset_index("vals").unwrap();
3419            assert_eq!(
3420                writer.ds(idx).lock().fill_value,
3421                Some((-7i32).to_le_bytes().to_vec())
3422            );
3423        }
3424        std::fs::remove_file(&path).ok();
3425    }
3426
3427    #[test]
3428    fn fill_value_read_missing_chunks() {
3429        // A chunked dataset with chunk 1 left unwritten must read that
3430        // gap back as the user-defined fill value, not zero.
3431        fn i32_bytes(vals: &[i32]) -> Vec<u8> {
3432            vals.iter().flat_map(|v| v.to_le_bytes()).collect()
3433        }
3434        let path = temp_path("fill_value_read_missing");
3435        {
3436            let file = H5File::create(&path).unwrap();
3437            let ds = file
3438                .new_dataset::<i32>()
3439                .shape([0])
3440                .chunk(&[2])
3441                .max_shape(&[None])
3442                .fill_value(-1i32)
3443                .create("vals")
3444                .unwrap();
3445            // chunk 0 = [10,20]; chunk 1 unwritten; chunk 2 = [50,60].
3446            ds.write_chunk(0, &i32_bytes(&[10, 20])).unwrap();
3447            ds.write_chunk(2, &i32_bytes(&[50, 60])).unwrap();
3448            ds.extend(&[6]).unwrap();
3449            file.close().unwrap();
3450        }
3451        {
3452            let file = H5File::open(&path).unwrap();
3453            let ds = file.dataset("vals").unwrap();
3454            let all = ds.read_raw::<i32>().unwrap();
3455            assert_eq!(all, vec![10, 20, -1, -1, 50, 60]);
3456        }
3457        std::fs::remove_file(&path).ok();
3458    }
3459
3460    #[test]
3461    fn fill_value_partial_chunk_padded_with_fill() {
3462        // A partial trailing chunk flushed at close must pad its unwritten
3463        // tail with the fill value. That pad sits beyond the logical shape,
3464        // so it is verified by scanning the on-disk chunk bytes directly.
3465        let path = temp_path("fill_value_partial_pad");
3466        {
3467            let file = H5File::create(&path).unwrap();
3468            let ds = file
3469                .new_dataset::<i32>()
3470                .shape([0])
3471                .chunk(&[4])
3472                .max_shape(&[None])
3473                .fill_value(-9i32)
3474                .create("vals")
3475                .unwrap();
3476            // 3 of 4 frames -> flushed as a partial chunk on close.
3477            ds.append(&[1i32, 2, 3]).unwrap();
3478            file.close().unwrap();
3479        }
3480        let bytes = std::fs::read(&path).unwrap();
3481        // Locate the chunk: i32 LE of [1, 2, 3] written contiguously.
3482        let needle: Vec<u8> = [1i32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
3483        let pos = bytes
3484            .windows(needle.len())
3485            .position(|w| w == needle)
3486            .expect("chunk data [1,2,3] not found in file");
3487        let pad = &bytes[pos + needle.len()..pos + needle.len() + 4];
3488        assert_eq!(
3489            pad,
3490            &(-9i32).to_le_bytes(),
3491            "partial chunk tail must be padded with fill value -9, got {:?}",
3492            pad
3493        );
3494        std::fs::remove_file(&path).ok();
3495    }
3496
3497    #[test]
3498    fn vlen_append_after_reopen_preserves_existing() {
3499        // Reopening and appending into a partially-written vlen chunk must
3500        // read-modify-write: the strings already on disk must survive.
3501        let path = temp_path("vlen_append_reopen");
3502        {
3503            let file = H5File::create(&path).unwrap();
3504            file.create_appendable_vlen_dataset("strs", 4, None)
3505                .unwrap();
3506            // 3 of 4 frames -> flushed as a partial chunk on close.
3507            file.append_vlen_strings("strs", &["a", "b", "c"]).unwrap();
3508            file.close().unwrap();
3509        }
3510        {
3511            // Append a 4th string -> partial-chunk write into chunk 0.
3512            let file = H5File::open_rw(&path).unwrap();
3513            file.append_vlen_strings("strs", &["d"]).unwrap();
3514            file.close().unwrap();
3515        }
3516        {
3517            let file = H5File::open(&path).unwrap();
3518            let ds = file.dataset("strs").unwrap();
3519            let got = ds.read_vlen_strings().unwrap();
3520            assert_eq!(
3521                got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
3522                vec!["a", "b", "c", "d"]
3523            );
3524        }
3525        std::fs::remove_file(&path).ok();
3526    }
3527
3528    #[test]
3529    fn fill_value_size_mismatch_errors() {
3530        let path = temp_path("fill_value_mismatch");
3531        let writer = crate::io::writer::Hdf5Writer::create(&path).unwrap();
3532        let dt = <f64 as crate::types::H5Type>::hdf5_type();
3533        let idx = writer.create_dataset("d", dt, &[4u64]).unwrap();
3534        // f64 element size is 8; a 4-byte fill value must be rejected.
3535        assert!(writer.set_dataset_fill_value(idx, vec![0u8; 4]).is_err());
3536        // The correct width succeeds.
3537        writer.set_dataset_fill_value(idx, vec![0u8; 8]).unwrap();
3538        writer.close().unwrap();
3539        std::fs::remove_file(&path).ok();
3540    }
3541
3542    #[test]
3543    fn datatype_exposes_class_sign_and_byteorder() {
3544        // The byte width alone cannot tell u8 from i8 (both 1 byte) or i32
3545        // from f32 (both 4 bytes). datatype() must report the real class and
3546        // signedness so a reader does not have to guess from element_size.
3547        use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
3548
3549        let path = temp_path("datatype_accessor");
3550        {
3551            let file = H5File::create(&path).unwrap();
3552            file.new_dataset::<u8>().shape([3]).create("u8d").unwrap();
3553            file.new_dataset::<i8>().shape([3]).create("i8d").unwrap();
3554            file.new_dataset::<i32>().shape([3]).create("i32d").unwrap();
3555            file.new_dataset::<f32>().shape([3]).create("f32d").unwrap();
3556            file.close().unwrap();
3557        }
3558
3559        let file = H5File::open(&path).unwrap();
3560
3561        match file.dataset("u8d").unwrap().datatype().unwrap() {
3562            DatatypeMessage::FixedPoint {
3563                size,
3564                signed,
3565                byte_order,
3566                ..
3567            } => {
3568                assert_eq!(size, 1);
3569                assert!(!signed, "u8 must be unsigned");
3570                assert_eq!(byte_order, ByteOrder::LittleEndian);
3571            }
3572            other => panic!("expected FixedPoint for u8, got {other:?}"),
3573        }
3574
3575        match file.dataset("i8d").unwrap().datatype().unwrap() {
3576            DatatypeMessage::FixedPoint { size, signed, .. } => {
3577                assert_eq!(size, 1);
3578                assert!(signed, "i8 must be signed");
3579            }
3580            other => panic!("expected FixedPoint for i8, got {other:?}"),
3581        }
3582
3583        match file.dataset("i32d").unwrap().datatype().unwrap() {
3584            DatatypeMessage::FixedPoint { size, signed, .. } => {
3585                assert_eq!(size, 4);
3586                assert!(signed, "i32 must be signed");
3587            }
3588            other => panic!("expected FixedPoint for i32, got {other:?}"),
3589        }
3590
3591        match file.dataset("f32d").unwrap().datatype().unwrap() {
3592            DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 4),
3593            other => panic!("expected FloatingPoint for f32, got {other:?}"),
3594        }
3595
3596        std::fs::remove_file(&path).ok();
3597    }
3598
3599    #[test]
3600    fn datatype_in_write_mode_errors() {
3601        let path = temp_path("datatype_write_mode");
3602        let file = H5File::create(&path).unwrap();
3603        let ds = file.new_dataset::<f32>().shape([4]).create("d").unwrap();
3604        assert!(ds.datatype().is_err());
3605        std::fs::remove_file(&path).ok();
3606    }
3607
3608    // --- write_chunk_raw (HDF5 direct chunk write) ---------------------------
3609
3610    /// Extensible-array path: pre-compress with the dataset's pipeline, write
3611    /// the bytes verbatim via write_chunk_raw (filter_mask = 0), and confirm
3612    /// the data round-trips through the reader unchanged.
3613    #[cfg(feature = "deflate")]
3614    #[test]
3615    fn write_chunk_raw_ea_roundtrip_mask0() {
3616        use crate::format::messages::filter::{apply_filters, FilterPipeline};
3617        let path = temp_path("wcr_ea_mask0");
3618        let original: Vec<i32> = (0..12).collect();
3619        {
3620            let file = H5File::create(&path).unwrap();
3621            let ds = file
3622                .new_dataset::<i32>()
3623                .shape([0])
3624                .chunk(&[4])
3625                .max_shape(&[None])
3626                .deflate(4)
3627                .create("v")
3628                .unwrap();
3629            assert!(ds.is_chunked());
3630            let pipeline = FilterPipeline::deflate(4);
3631            for c in 0..3usize {
3632                let raw: Vec<u8> = original[c * 4..c * 4 + 4]
3633                    .iter()
3634                    .flat_map(|v| v.to_le_bytes())
3635                    .collect();
3636                let compressed = apply_filters(&pipeline, &raw).unwrap();
3637                ds.write_chunk_raw(c, &compressed, 0).unwrap();
3638            }
3639            ds.set_extent(&[12]).unwrap();
3640            file.close().unwrap();
3641        }
3642        {
3643            let file = H5File::open(&path).unwrap();
3644            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3645            assert_eq!(v, original);
3646        }
3647        std::fs::remove_file(&path).ok();
3648    }
3649
3650    /// Fixed-array path (all dimensions bounded): same verbatim write through
3651    /// the linear-index dispatch, round-tripped through the reader.
3652    #[cfg(feature = "deflate")]
3653    #[test]
3654    fn write_chunk_raw_fixed_array_roundtrip_mask0() {
3655        use crate::format::messages::filter::{apply_filters, FilterPipeline};
3656        let path = temp_path("wcr_fa_mask0");
3657        let original: Vec<i32> = (0..12).collect();
3658        {
3659            let file = H5File::create(&path).unwrap();
3660            let ds = file
3661                .new_dataset::<i32>()
3662                .shape([12])
3663                .chunk(&[4])
3664                .deflate(4)
3665                .create("v")
3666                .unwrap();
3667            assert!(ds.is_chunked());
3668            let pipeline = FilterPipeline::deflate(4);
3669            for c in 0..3usize {
3670                let raw: Vec<u8> = original[c * 4..c * 4 + 4]
3671                    .iter()
3672                    .flat_map(|v| v.to_le_bytes())
3673                    .collect();
3674                let compressed = apply_filters(&pipeline, &raw).unwrap();
3675                ds.write_chunk_raw(c, &compressed, 0).unwrap();
3676            }
3677            file.close().unwrap();
3678        }
3679        {
3680            let file = H5File::open(&path).unwrap();
3681            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3682            assert_eq!(v, original);
3683        }
3684        std::fs::remove_file(&path).ok();
3685    }
3686
3687    /// The caller-supplied filter_mask must reach the on-disk filtered index
3688    /// entry (not be hardcoded to 0). Store one chunk uncompressed in a
3689    /// filtered dataset with mask = 1 (deflate skipped), then reopen and decode
3690    /// the extensible-array filtered entry to read the mask back at the format
3691    /// level (independent of the data reader's mask handling).
3692    #[cfg(feature = "deflate")]
3693    #[test]
3694    fn write_chunk_raw_records_filter_mask() {
3695        let path = temp_path("wcr_records_mask");
3696        let raw: Vec<u8> = [10i32, 20, 30, 40]
3697            .iter()
3698            .flat_map(|v| v.to_le_bytes())
3699            .collect();
3700        assert_eq!(raw.len(), 16);
3701        {
3702            let file = H5File::create(&path).unwrap();
3703            let ds = file
3704                .new_dataset::<i32>()
3705                .shape([0])
3706                .chunk(&[4])
3707                .max_shape(&[None])
3708                .deflate(4)
3709                .create("v")
3710                .unwrap();
3711            // mask = 1: bit 0 set => filter 0 (deflate) was skipped, so the
3712            // chunk is stored uncompressed (its raw bytes).
3713            ds.write_chunk_raw(0, &raw, 1).unwrap();
3714            ds.set_extent(&[4]).unwrap();
3715            file.close().unwrap();
3716        }
3717        // Reopen the writer; open_append decodes the filtered index block from
3718        // disk, so the entry reflects exactly what was committed.
3719        {
3720            let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
3721            let idx = w.dataset_index("v").unwrap();
3722            let ds = w.ds(idx);
3723            let m = ds.lock();
3724            let entry = &m
3725                .chunked
3726                .as_ref()
3727                .unwrap()
3728                .filt_iblk
3729                .as_ref()
3730                .unwrap()
3731                .elements[0];
3732            assert_eq!(entry.filter_mask, 1, "filter_mask must round-trip to disk");
3733            assert_eq!(entry.nbytes, 16, "uncompressed chunk stored verbatim");
3734        }
3735        std::fs::remove_file(&path).ok();
3736    }
3737
3738    /// Reader honors a per-chunk filter_mask (EA): one chunk is stored
3739    /// compressed (mask 0), the next stored raw with deflate skipped (mask 1),
3740    /// in the same dataset. A correct reader skips deflate for chunk 1 only;
3741    /// ignoring the mask would feed raw bytes through inflate and corrupt them.
3742    #[cfg(feature = "deflate")]
3743    #[test]
3744    fn write_chunk_raw_ea_per_chunk_mask_roundtrip() {
3745        use crate::format::messages::filter::{apply_filters, FilterPipeline};
3746        let path = temp_path("wcr_ea_per_chunk_mask");
3747        let original: Vec<i32> = (0..8).collect();
3748        let pipeline = FilterPipeline::deflate(4);
3749        {
3750            let file = H5File::create(&path).unwrap();
3751            let ds = file
3752                .new_dataset::<i32>()
3753                .shape([0])
3754                .chunk(&[4])
3755                .max_shape(&[None])
3756                .deflate(4)
3757                .create("v")
3758                .unwrap();
3759            let raw0: Vec<u8> = original[0..4]
3760                .iter()
3761                .flat_map(|v| v.to_le_bytes())
3762                .collect();
3763            // chunk 0: compressed through the pipeline, mask 0.
3764            ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
3765                .unwrap();
3766            let raw1: Vec<u8> = original[4..8]
3767                .iter()
3768                .flat_map(|v| v.to_le_bytes())
3769                .collect();
3770            // chunk 1: stored uncompressed, mask 1 (deflate skipped).
3771            ds.write_chunk_raw(1, &raw1, 1).unwrap();
3772            ds.set_extent(&[8]).unwrap();
3773            file.close().unwrap();
3774        }
3775        {
3776            let file = H5File::open(&path).unwrap();
3777            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3778            assert_eq!(v, original);
3779        }
3780        std::fs::remove_file(&path).ok();
3781    }
3782
3783    /// Reader honors a per-chunk filter_mask (fixed array): same mixed
3784    /// compressed/raw chunks as the EA case, through the fixed-array index.
3785    #[cfg(feature = "deflate")]
3786    #[test]
3787    fn write_chunk_raw_fixed_array_per_chunk_mask_roundtrip() {
3788        use crate::format::messages::filter::{apply_filters, FilterPipeline};
3789        let path = temp_path("wcr_fa_per_chunk_mask");
3790        let original: Vec<i32> = (0..8).collect();
3791        let pipeline = FilterPipeline::deflate(4);
3792        {
3793            let file = H5File::create(&path).unwrap();
3794            let ds = file
3795                .new_dataset::<i32>()
3796                .shape([8])
3797                .chunk(&[4])
3798                .deflate(4)
3799                .create("v")
3800                .unwrap();
3801            let raw0: Vec<u8> = original[0..4]
3802                .iter()
3803                .flat_map(|v| v.to_le_bytes())
3804                .collect();
3805            ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
3806                .unwrap();
3807            let raw1: Vec<u8> = original[4..8]
3808                .iter()
3809                .flat_map(|v| v.to_le_bytes())
3810                .collect();
3811            ds.write_chunk_raw(1, &raw1, 1).unwrap();
3812            file.close().unwrap();
3813        }
3814        {
3815            let file = H5File::open(&path).unwrap();
3816            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3817            assert_eq!(v, original);
3818        }
3819        std::fs::remove_file(&path).ok();
3820    }
3821
3822    /// An unfiltered chunk index has no slot for a stored size or mask, so a
3823    /// direct chunk write must be rejected rather than silently dropping them.
3824    #[test]
3825    fn write_chunk_raw_rejects_unfiltered() {
3826        let path = temp_path("wcr_unfiltered");
3827        let file = H5File::create(&path).unwrap();
3828        let ds = file
3829            .new_dataset::<i32>()
3830            .shape([0])
3831            .chunk(&[4])
3832            .max_shape(&[None])
3833            .create("v")
3834            .unwrap();
3835        let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
3836        assert!(
3837            err.to_string().contains("filtered dataset"),
3838            "expected a filtered-dataset error, got: {err}"
3839        );
3840        std::fs::remove_file(&path).ok();
3841    }
3842
3843    /// v2-B-tree-indexed datasets (two or more unlimited dimensions) do not
3844    /// support direct chunk writes.
3845    #[test]
3846    fn write_chunk_raw_rejects_btree_v2() {
3847        let path = temp_path("wcr_btree2");
3848        let file = H5File::create(&path).unwrap();
3849        let ds = file
3850            .new_dataset::<i32>()
3851            .shape([0, 0])
3852            .chunk(&[2, 2])
3853            .max_shape(&[None, None])
3854            .create("grid")
3855            .unwrap();
3856        let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
3857        assert!(
3858            err.to_string().contains("v2-B-tree"),
3859            "expected a v2-B-tree rejection, got: {err}"
3860        );
3861        std::fs::remove_file(&path).ok();
3862    }
3863
3864    /// A stored size that does not fit the index's chunk-size field must error
3865    /// (libhdf5 H5D_CHUNK_ENCODE_SIZE_CHECK) instead of truncating silently.
3866    /// A 4-byte chunk (chunk[1] of i32) has chunk_size_len = 2 (max 65535), so
3867    /// a 70000-byte stored chunk overflows it.
3868    #[cfg(feature = "deflate")]
3869    #[test]
3870    fn write_chunk_raw_rejects_oversized_chunk() {
3871        let path = temp_path("wcr_oversized");
3872        let file = H5File::create(&path).unwrap();
3873        let ds = file
3874            .new_dataset::<i32>()
3875            .shape([0])
3876            .chunk(&[1])
3877            .max_shape(&[None])
3878            .deflate(4)
3879            .create("v")
3880            .unwrap();
3881        let err = ds.write_chunk_raw(0, &vec![0u8; 70000], 0).unwrap_err();
3882        assert!(
3883            err.to_string().contains("does not fit"),
3884            "expected a chunk-size-field overflow error, got: {err}"
3885        );
3886        std::fs::remove_file(&path).ok();
3887    }
3888}