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
1871#[cfg(test)]
1872mod tests {
1873    use crate::H5File;
1874    use std::path::PathBuf;
1875
1876    fn temp_path(name: &str) -> PathBuf {
1877        // Include PID + a per-call atomic counter so that concurrent
1878        // cargo invocations and any kernel-level "lock not yet
1879        // released" races between sequential opens cannot collide.
1880        use std::sync::atomic::{AtomicU64, Ordering};
1881        static COUNTER: AtomicU64 = AtomicU64::new(0);
1882        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1883        std::env::temp_dir().join(format!(
1884            "hdf5_dataset_test_{}_{}_{}.h5",
1885            name,
1886            std::process::id(),
1887            n
1888        ))
1889    }
1890
1891    #[test]
1892    fn runtime_compound_via_datatype_override_and_raw_bytes() {
1893        use crate::format::messages::datatype::DatatypeMessage;
1894        use crate::types::{CompoundType, H5Type};
1895
1896        let path = temp_path("compound_raw");
1897        // A 12-byte packed compound with NO matching Rust primitive carrier,
1898        // so it can only be written through the datatype() override +
1899        // write_raw_bytes path (the runtime-CompoundType use case).
1900        let ct = CompoundType {
1901            members: vec![
1902                ("id".to_string(), i32::hdf5_type(), 0),
1903                ("val".to_string(), f64::hdf5_type(), 4),
1904            ],
1905            total_size: 12,
1906        };
1907        let recs: [(i32, f64); 3] = [(1, 2.5), (2, 3.5), (3, -4.0)];
1908        let mut bytes = Vec::new();
1909        for (id, val) in recs {
1910            bytes.extend_from_slice(&id.to_le_bytes());
1911            bytes.extend_from_slice(&val.to_le_bytes());
1912        }
1913
1914        {
1915            let file = H5File::create(&path).unwrap();
1916            let ds = file
1917                .new_dataset::<u8>()
1918                .datatype(ct.to_datatype())
1919                .shape([recs.len()])
1920                .create("records")
1921                .unwrap();
1922            ds.write_raw_bytes(&bytes).unwrap();
1923            file.close().unwrap();
1924        }
1925        {
1926            let file = H5File::open(&path).unwrap();
1927            let ds = file.dataset("records").unwrap();
1928            // The on-disk element type is the compound we specified (size 12),
1929            // not the u8 carrier.
1930            match ds.datatype().unwrap() {
1931                DatatypeMessage::Compound { size, members } => {
1932                    assert_eq!(size, 12);
1933                    assert_eq!(members.len(), 2);
1934                    assert_eq!(members[0].name, "id");
1935                    assert_eq!(members[0].offset, 0);
1936                    assert_eq!(members[1].name, "val");
1937                    assert_eq!(members[1].offset, 4);
1938                }
1939                other => panic!("expected compound datatype, got {other:?}"),
1940            }
1941            assert_eq!(ds.read_raw_bytes().unwrap(), bytes);
1942        }
1943        std::fs::remove_file(&path).ok();
1944    }
1945
1946    #[test]
1947    fn builder_requires_shape() {
1948        let path = temp_path("no_shape");
1949        let file = H5File::create(&path).unwrap();
1950        let result = file.new_dataset::<u8>().create("data");
1951        assert!(result.is_err());
1952        std::fs::remove_file(&path).ok();
1953    }
1954
1955    #[test]
1956    fn write_raw_size_mismatch() {
1957        let path = temp_path("size_mismatch");
1958        let file = H5File::create(&path).unwrap();
1959        let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
1960        // Provide 3 elements instead of 4
1961        let result = ds.write_raw(&[1u8, 2, 3]);
1962        assert!(result.is_err());
1963        std::fs::remove_file(&path).ok();
1964    }
1965
1966    // A: a filter set without explicit chunk dimensions must auto-chunk (whole
1967    // dataset = one chunk) rather than silently drop the filter on the
1968    // contiguous path. write_raw then populates that single chunk.
1969    #[cfg(feature = "deflate")]
1970    #[test]
1971    fn filter_without_chunk_autochunks_and_roundtrips() {
1972        let path = temp_path("autochunk_filter");
1973        let data: Vec<i32> = (0..8).collect();
1974        {
1975            let file = H5File::create(&path).unwrap();
1976            let ds = file
1977                .new_dataset::<i32>()
1978                .deflate(6)
1979                .shape([8])
1980                .create("seq")
1981                .unwrap();
1982            ds.write_raw(&data).unwrap();
1983            file.close().unwrap();
1984        }
1985        {
1986            let file = H5File::open(&path).unwrap();
1987            let ds = file.dataset("seq").unwrap();
1988            // The filter forced chunked storage: a single whole-dataset chunk.
1989            assert!(
1990                ds.is_chunked(),
1991                "auto-chunk did not produce chunked storage"
1992            );
1993            assert_eq!(ds.chunk_dims(), Some(vec![8]));
1994            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
1995        }
1996        std::fs::remove_file(&path).ok();
1997    }
1998
1999    // B: write_raw on an explicitly chunked + compressed dataset scatters the
2000    // full row-major image across a multi-chunk grid, including edge chunks
2001    // (7/3 -> 3,3,1 along dim0; 5/2 -> 2,2,1 along dim1).
2002    #[cfg(feature = "deflate")]
2003    #[test]
2004    fn write_raw_multichunk_edge_roundtrips() {
2005        let path = temp_path("multichunk_edge");
2006        let data: Vec<i32> = (0..35).collect(); // 7 x 5 row-major
2007        {
2008            let file = H5File::create(&path).unwrap();
2009            let ds = file
2010                .new_dataset::<i32>()
2011                .shape([7, 5])
2012                .chunk(&[3, 2])
2013                .deflate(4)
2014                .create("grid")
2015                .unwrap();
2016            ds.write_raw(&data).unwrap();
2017            file.close().unwrap();
2018        }
2019        {
2020            let file = H5File::open(&path).unwrap();
2021            let ds = file.dataset("grid").unwrap();
2022            assert_eq!(ds.shape(), vec![7, 5]);
2023            assert_eq!(ds.chunk_dims(), Some(vec![3, 2]));
2024            assert_eq!(ds.read_raw::<i32>().unwrap(), data);
2025        }
2026        std::fs::remove_file(&path).ok();
2027    }
2028
2029    // B: write_raw on an unfiltered chunked dataset (previously rejected with
2030    // "use write_chunk for chunked datasets") now gathers and round-trips.
2031    #[test]
2032    fn write_raw_unfiltered_chunked_roundtrips() {
2033        let path = temp_path("chunked_unfiltered");
2034        let data: Vec<f64> = (0..12).map(|i| i as f64 * 1.5).collect(); // 4 x 3
2035        {
2036            let file = H5File::create(&path).unwrap();
2037            let ds = file
2038                .new_dataset::<f64>()
2039                .shape([4, 3])
2040                .chunk(&[2, 2])
2041                .create("m")
2042                .unwrap();
2043            ds.write_raw(&data).unwrap();
2044            file.close().unwrap();
2045        }
2046        {
2047            let file = H5File::open(&path).unwrap();
2048            let ds = file.dataset("m").unwrap();
2049            assert_eq!(ds.chunk_dims(), Some(vec![2, 2]));
2050            assert_eq!(ds.read_raw::<f64>().unwrap(), data);
2051        }
2052        std::fs::remove_file(&path).ok();
2053    }
2054
2055    #[test]
2056    fn roundtrip_u8_1d() {
2057        let path = temp_path("rt_u8_1d");
2058        let data: Vec<u8> = (0..10).collect();
2059
2060        {
2061            let file = H5File::create(&path).unwrap();
2062            let ds = file.new_dataset::<u8>().shape([10]).create("seq").unwrap();
2063            ds.write_raw(&data).unwrap();
2064            file.close().unwrap();
2065        }
2066
2067        {
2068            let file = H5File::open(&path).unwrap();
2069            let ds = file.dataset("seq").unwrap();
2070            assert_eq!(ds.shape(), vec![10]);
2071            let readback = ds.read_raw::<u8>().unwrap();
2072            assert_eq!(readback, data);
2073        }
2074
2075        std::fs::remove_file(&path).ok();
2076    }
2077
2078    #[test]
2079    fn roundtrip_i32_2d() {
2080        let path = temp_path("rt_i32_2d");
2081        let data: Vec<i32> = vec![-1, 0, 1, 2, 3, 4];
2082
2083        {
2084            let file = H5File::create(&path).unwrap();
2085            let ds = file
2086                .new_dataset::<i32>()
2087                .shape([2, 3])
2088                .create("matrix")
2089                .unwrap();
2090            ds.write_raw(&data).unwrap();
2091            file.close().unwrap();
2092        }
2093
2094        {
2095            let file = H5File::open(&path).unwrap();
2096            let ds = file.dataset("matrix").unwrap();
2097            assert_eq!(ds.shape(), vec![2, 3]);
2098            let readback = ds.read_raw::<i32>().unwrap();
2099            assert_eq!(readback, data);
2100        }
2101
2102        std::fs::remove_file(&path).ok();
2103    }
2104
2105    #[test]
2106    fn roundtrip_f64_3d() {
2107        let path = temp_path("rt_f64_3d");
2108        let data: Vec<f64> = (0..24).map(|i| i as f64 * 0.5).collect();
2109
2110        {
2111            let file = H5File::create(&path).unwrap();
2112            let ds = file
2113                .new_dataset::<f64>()
2114                .shape([2, 3, 4])
2115                .create("cube")
2116                .unwrap();
2117            ds.write_raw(&data).unwrap();
2118            file.close().unwrap();
2119        }
2120
2121        {
2122            let file = H5File::open(&path).unwrap();
2123            let ds = file.dataset("cube").unwrap();
2124            assert_eq!(ds.shape(), vec![2, 3, 4]);
2125            let readback = ds.read_raw::<f64>().unwrap();
2126            assert_eq!(readback, data);
2127        }
2128
2129        std::fs::remove_file(&path).ok();
2130    }
2131
2132    #[test]
2133    fn cannot_read_in_write_mode() {
2134        let path = temp_path("no_read_write");
2135        let file = H5File::create(&path).unwrap();
2136        let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
2137        ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2138        let result = ds.read_raw::<u8>();
2139        assert!(result.is_err());
2140        std::fs::remove_file(&path).ok();
2141    }
2142
2143    #[test]
2144    fn cannot_write_in_read_mode() {
2145        let path = temp_path("no_write_read");
2146
2147        {
2148            let file = H5File::create(&path).unwrap();
2149            let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
2150            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2151            file.close().unwrap();
2152        }
2153
2154        {
2155            let file = H5File::open(&path).unwrap();
2156            let ds = file.dataset("x").unwrap();
2157            let result = ds.write_raw(&[5u8, 6, 7, 8]);
2158            assert!(result.is_err());
2159        }
2160
2161        std::fs::remove_file(&path).ok();
2162    }
2163
2164    #[test]
2165    fn numeric_attr_roundtrip() {
2166        let path = temp_path("num_attr");
2167        {
2168            let file = H5File::create(&path).unwrap();
2169            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
2170            ds.write_raw(&[1.0f32; 4]).unwrap();
2171
2172            let a1 = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
2173            a1.write_numeric(&1.2345f64).unwrap();
2174
2175            let a2 = ds.new_attr::<i32>().shape(()).create("count").unwrap();
2176            a2.write_numeric(&42i32).unwrap();
2177
2178            file.close().unwrap();
2179        }
2180        {
2181            let file = H5File::open(&path).unwrap();
2182            let ds = file.dataset("data").unwrap();
2183
2184            let scale = ds.attr("scale").unwrap();
2185            let val: f64 = scale.read_numeric().unwrap();
2186            assert!((val - 1.2345).abs() < 1e-10);
2187
2188            let count = ds.attr("count").unwrap();
2189            let val: i32 = count.read_numeric().unwrap();
2190            assert_eq!(val, 42);
2191        }
2192        std::fs::remove_file(&path).ok();
2193    }
2194
2195    #[test]
2196    fn array_attr_roundtrip() {
2197        let path = temp_path("array_attr");
2198        let offsets = [10i32, -20, 30];
2199        {
2200            let file = H5File::create(&path).unwrap();
2201            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
2202            ds.write_raw(&[1.0f32; 4]).unwrap();
2203
2204            // 1-D int32 array attribute (NDArrayDimOffset-style).
2205            let a = ds
2206                .new_attr::<i32>()
2207                .shape([3])
2208                .create("dim_offset")
2209                .unwrap();
2210            a.write_array(&offsets).unwrap();
2211
2212            // Wrong element count is rejected.
2213            let bad = ds.new_attr::<i32>().shape([3]).create("bad").unwrap();
2214            assert!(bad.write_array(&[1i32, 2]).is_err());
2215
2216            file.close().unwrap();
2217        }
2218        {
2219            let file = H5File::open(&path).unwrap();
2220            let ds = file.dataset("data").unwrap();
2221            let a = ds.attr("dim_offset").unwrap();
2222            let raw = a.read_raw().unwrap();
2223            assert_eq!(raw.len(), 3 * 4);
2224            let got: Vec<i32> = raw
2225                .chunks_exact(4)
2226                .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
2227                .collect();
2228            assert_eq!(got, offsets);
2229        }
2230        std::fs::remove_file(&path).ok();
2231    }
2232
2233    #[test]
2234    fn attr_datatype_exposes_class_and_sign() {
2235        // H5Attribute::datatype() must report the stored datatype class and
2236        // signedness so a generic attr->metadata mapper need not infer it from
2237        // the byte width (the HDF5-L1 adapter blocker this accessor unblocks).
2238        use crate::format::messages::datatype::DatatypeMessage;
2239
2240        let path = temp_path("attr_datatype");
2241        {
2242            let file = H5File::create(&path).unwrap();
2243            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
2244            ds.new_attr::<f64>()
2245                .shape(())
2246                .create("scale")
2247                .unwrap()
2248                .write_numeric(&1.5f64)
2249                .unwrap();
2250            ds.new_attr::<i32>()
2251                .shape(())
2252                .create("count")
2253                .unwrap()
2254                .write_numeric(&7i32)
2255                .unwrap();
2256            file.close().unwrap();
2257        }
2258        {
2259            let file = H5File::open(&path).unwrap();
2260            let ds = file.dataset("data").unwrap();
2261
2262            match ds.attr("scale").unwrap().datatype().unwrap() {
2263                DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 8),
2264                other => panic!("expected FloatingPoint for f64 attr, got {other:?}"),
2265            }
2266
2267            match ds.attr("count").unwrap().datatype().unwrap() {
2268                DatatypeMessage::FixedPoint { size, signed, .. } => {
2269                    assert_eq!(size, 4);
2270                    assert!(signed, "i32 attr must be signed");
2271                }
2272                other => panic!("expected FixedPoint for i32 attr, got {other:?}"),
2273            }
2274        }
2275        std::fs::remove_file(&path).ok();
2276    }
2277
2278    #[test]
2279    fn attr_datatype_in_write_mode_errors() {
2280        let path = temp_path("attr_datatype_write_mode");
2281        let file = H5File::create(&path).unwrap();
2282        let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
2283        let attr = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
2284        assert!(attr.datatype().is_err());
2285        std::fs::remove_file(&path).ok();
2286    }
2287
2288    #[test]
2289    fn cannot_create_dataset_in_read_mode() {
2290        let path = temp_path("no_create_read");
2291
2292        {
2293            let _file = H5File::create(&path).unwrap();
2294        }
2295
2296        {
2297            let file = H5File::open(&path).unwrap();
2298            let result = file.new_dataset::<u8>().shape([4]).create("x");
2299            assert!(result.is_err());
2300        }
2301
2302        std::fs::remove_file(&path).ok();
2303    }
2304
2305    #[test]
2306    fn shape_accessor() {
2307        let path = temp_path("shape_acc");
2308
2309        let file = H5File::create(&path).unwrap();
2310        let ds = file
2311            .new_dataset::<f32>()
2312            .shape([5, 10, 3])
2313            .create("tensor")
2314            .unwrap();
2315        assert_eq!(ds.shape(), vec![5, 10, 3]);
2316
2317        std::fs::remove_file(&path).ok();
2318    }
2319
2320    #[test]
2321    fn slice_roundtrip_2d() {
2322        let path = temp_path("slice_2d");
2323
2324        // Create a 4x5 dataset, write full, then read a slice
2325        let data: Vec<i32> = (0..20).collect();
2326        {
2327            let file = H5File::create(&path).unwrap();
2328            let ds = file
2329                .new_dataset::<i32>()
2330                .shape([4, 5])
2331                .create("mat")
2332                .unwrap();
2333            ds.write_raw(&data).unwrap();
2334            file.close().unwrap();
2335        }
2336        {
2337            let file = H5File::open(&path).unwrap();
2338            let ds = file.dataset("mat").unwrap();
2339            // Read rows 1..3, cols 2..4 (2x2 slice)
2340            let slice = ds.read_slice::<i32>(&[1, 2], &[2, 2]).unwrap();
2341            // Row 1: [5,6,7,8,9] -> cols 2..4 = [7,8]
2342            // Row 2: [10,11,12,13,14] -> cols 2..4 = [12,13]
2343            assert_eq!(slice, vec![7, 8, 12, 13]);
2344        }
2345
2346        std::fs::remove_file(&path).ok();
2347    }
2348
2349    #[test]
2350    fn write_slice_2d() {
2351        let path = temp_path("write_slice_2d");
2352
2353        {
2354            let file = H5File::create(&path).unwrap();
2355            let ds = file
2356                .new_dataset::<f32>()
2357                .shape([3, 4])
2358                .create("data")
2359                .unwrap();
2360            ds.write_raw(&[0.0f32; 12]).unwrap();
2361            // Overwrite a 2x2 sub-region
2362            ds.write_slice(&[1, 1], &[2, 2], &[10.0f32, 20.0, 30.0, 40.0])
2363                .unwrap();
2364            file.close().unwrap();
2365        }
2366        {
2367            let file = H5File::open(&path).unwrap();
2368            let ds = file.dataset("data").unwrap();
2369            let full = ds.read_raw::<f32>().unwrap();
2370            // Row 0: [0,0,0,0]
2371            // Row 1: [0,10,20,0]
2372            // Row 2: [0,30,40,0]
2373            assert_eq!(
2374                full,
2375                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,]
2376            );
2377        }
2378
2379        std::fs::remove_file(&path).ok();
2380    }
2381
2382    #[test]
2383    fn write_slice_out_of_bounds_rejected() {
2384        let path = temp_path("write_slice_oob");
2385        let file = H5File::create(&path).unwrap();
2386        let ds = file.new_dataset::<i32>().shape([4]).create("d").unwrap();
2387        ds.write_raw(&[0i32; 4]).unwrap();
2388        // start 2 + count 6 = 8 > extent 4 -> must error, not corrupt.
2389        assert!(ds.write_slice(&[2], &[6], &[9i32; 6]).is_err());
2390        // An in-bounds slice still works.
2391        assert!(ds.write_slice(&[1], &[2], &[7i32, 8]).is_ok());
2392        std::fs::remove_file(&path).ok();
2393    }
2394
2395    #[test]
2396    fn duplicate_dataset_name_rejected() {
2397        let path = temp_path("dup_name");
2398        let file = H5File::create(&path).unwrap();
2399        let _ = file.new_dataset::<i32>().shape([2]).create("d").unwrap();
2400        assert!(file.new_dataset::<i32>().shape([2]).create("d").is_err());
2401        std::fs::remove_file(&path).ok();
2402    }
2403
2404    #[test]
2405    fn extend_cannot_shrink() {
2406        let path = temp_path("extend_shrink");
2407        let file = H5File::create(&path).unwrap();
2408        let ds = file
2409            .new_dataset::<i32>()
2410            .shape([0])
2411            .chunk(&[2])
2412            .max_shape(&[None])
2413            .create("d")
2414            .unwrap();
2415        ds.append(&[1i32, 2, 3, 4]).unwrap();
2416        // Shrinking below the written extent must be rejected.
2417        assert!(ds.extend(&[2]).is_err());
2418        // Growing is fine.
2419        assert!(ds.extend(&[6]).is_ok());
2420        std::fs::remove_file(&path).ok();
2421    }
2422
2423    #[test]
2424    fn attr_read_roundtrip() {
2425        use crate::types::VarLenUnicode;
2426        let path = temp_path("attr_read");
2427
2428        {
2429            let file = H5File::create(&path).unwrap();
2430            let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
2431            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2432            let a1 = ds
2433                .new_attr::<VarLenUnicode>()
2434                .shape(())
2435                .create("units")
2436                .unwrap();
2437            a1.write_string("meters").unwrap();
2438            let a2 = ds
2439                .new_attr::<VarLenUnicode>()
2440                .shape(())
2441                .create("desc")
2442                .unwrap();
2443            a2.write_string("test data").unwrap();
2444            file.close().unwrap();
2445        }
2446        {
2447            let file = H5File::open(&path).unwrap();
2448            let ds = file.dataset("data").unwrap();
2449
2450            let names = ds.attr_names().unwrap();
2451            assert!(names.contains(&"units".to_string()));
2452            assert!(names.contains(&"desc".to_string()));
2453
2454            let units = ds.attr("units").unwrap();
2455            assert_eq!(units.read_string().unwrap(), "meters");
2456
2457            let desc = ds.attr("desc").unwrap();
2458            assert_eq!(desc.read_string().unwrap(), "test data");
2459        }
2460
2461        std::fs::remove_file(&path).ok();
2462    }
2463
2464    #[test]
2465    fn type_mismatch_element_size() {
2466        let path = temp_path("type_mismatch");
2467
2468        {
2469            let file = H5File::create(&path).unwrap();
2470            let ds = file.new_dataset::<f64>().shape([4]).create("data").unwrap();
2471            ds.write_raw(&[1.0f64, 2.0, 3.0, 4.0]).unwrap();
2472            file.close().unwrap();
2473        }
2474
2475        {
2476            let file = H5File::open(&path).unwrap();
2477            let ds = file.dataset("data").unwrap();
2478            // Try to read as u8 (element_size = 1) from a f64 dataset (element_size = 8)
2479            let result = ds.read_raw::<u8>();
2480            assert!(result.is_err());
2481        }
2482
2483        std::fs::remove_file(&path).ok();
2484    }
2485
2486    #[test]
2487    fn dataset_survives_file_move() {
2488        let path = temp_path("ds_survives");
2489
2490        let ds = {
2491            let file = H5File::create(&path).unwrap();
2492            file.new_dataset::<u8>().shape([4]).create("x").unwrap()
2493        };
2494        // file is dropped here, but ds still holds Rc to the inner state
2495        ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2496        // The writer will finalize on drop of the last Rc
2497
2498        std::fs::remove_file(&path).ok();
2499    }
2500
2501    #[test]
2502    fn new_attr_scalar_string() {
2503        use crate::types::VarLenUnicode;
2504
2505        let path = temp_path("attr_scalar_string");
2506        {
2507            let file = H5File::create(&path).unwrap();
2508            let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
2509            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2510
2511            let attr = ds
2512                .new_attr::<VarLenUnicode>()
2513                .shape(())
2514                .create("name")
2515                .unwrap();
2516            attr.write_scalar(&VarLenUnicode("test_value".to_string()))
2517                .unwrap();
2518
2519            file.close().unwrap();
2520        }
2521
2522        // Verify the file is still valid and readable
2523        {
2524            use crate::format::messages::datatype::DatatypeMessage;
2525            let file = H5File::open(&path).unwrap();
2526            let ds = file.dataset("data").unwrap();
2527            assert_eq!(ds.shape(), vec![4]);
2528            let readback = ds.read_raw::<u8>().unwrap();
2529            assert_eq!(readback, vec![1u8, 2, 3, 4]);
2530
2531            // The string attribute is stored as a true variable-length string
2532            // (not fixed-length) and round-trips its value.
2533            let attr = ds.attr("name").unwrap();
2534            assert!(
2535                matches!(
2536                    attr.datatype().unwrap(),
2537                    DatatypeMessage::VarLenString { .. }
2538                ),
2539                "string attribute should have a variable-length string datatype"
2540            );
2541            assert_eq!(attr.read_string().unwrap(), "test_value");
2542        }
2543
2544        std::fs::remove_file(&path).ok();
2545    }
2546
2547    #[test]
2548    fn all_numeric_types_roundtrip() {
2549        let path = temp_path("all_types");
2550
2551        {
2552            let file = H5File::create(&path).unwrap();
2553
2554            let ds = file.new_dataset::<u8>().shape([2]).create("u8").unwrap();
2555            ds.write_raw(&[1u8, 2]).unwrap();
2556
2557            let ds = file.new_dataset::<i8>().shape([2]).create("i8").unwrap();
2558            ds.write_raw(&[-1i8, 1]).unwrap();
2559
2560            let ds = file.new_dataset::<u16>().shape([2]).create("u16").unwrap();
2561            ds.write_raw(&[100u16, 200]).unwrap();
2562
2563            let ds = file.new_dataset::<i16>().shape([2]).create("i16").unwrap();
2564            ds.write_raw(&[-100i16, 100]).unwrap();
2565
2566            let ds = file.new_dataset::<u32>().shape([2]).create("u32").unwrap();
2567            ds.write_raw(&[1000u32, 2000]).unwrap();
2568
2569            let ds = file.new_dataset::<i32>().shape([2]).create("i32").unwrap();
2570            ds.write_raw(&[-1000i32, 1000]).unwrap();
2571
2572            let ds = file.new_dataset::<u64>().shape([2]).create("u64").unwrap();
2573            ds.write_raw(&[10000u64, 20000]).unwrap();
2574
2575            let ds = file.new_dataset::<i64>().shape([2]).create("i64").unwrap();
2576            ds.write_raw(&[-10000i64, 10000]).unwrap();
2577
2578            let ds = file.new_dataset::<f32>().shape([2]).create("f32").unwrap();
2579            ds.write_raw(&[1.5f32, 2.5]).unwrap();
2580
2581            let ds = file.new_dataset::<f64>().shape([2]).create("f64").unwrap();
2582            ds.write_raw(&[1.23456f64, 7.89012]).unwrap();
2583
2584            file.close().unwrap();
2585        }
2586
2587        {
2588            let file = H5File::open(&path).unwrap();
2589
2590            assert_eq!(
2591                file.dataset("u8").unwrap().read_raw::<u8>().unwrap(),
2592                vec![1u8, 2]
2593            );
2594            assert_eq!(
2595                file.dataset("i8").unwrap().read_raw::<i8>().unwrap(),
2596                vec![-1i8, 1]
2597            );
2598            assert_eq!(
2599                file.dataset("u16").unwrap().read_raw::<u16>().unwrap(),
2600                vec![100u16, 200]
2601            );
2602            assert_eq!(
2603                file.dataset("i16").unwrap().read_raw::<i16>().unwrap(),
2604                vec![-100i16, 100]
2605            );
2606            assert_eq!(
2607                file.dataset("u32").unwrap().read_raw::<u32>().unwrap(),
2608                vec![1000u32, 2000]
2609            );
2610            assert_eq!(
2611                file.dataset("i32").unwrap().read_raw::<i32>().unwrap(),
2612                vec![-1000i32, 1000]
2613            );
2614            assert_eq!(
2615                file.dataset("u64").unwrap().read_raw::<u64>().unwrap(),
2616                vec![10000u64, 20000]
2617            );
2618            assert_eq!(
2619                file.dataset("i64").unwrap().read_raw::<i64>().unwrap(),
2620                vec![-10000i64, 10000]
2621            );
2622            assert_eq!(
2623                file.dataset("f32").unwrap().read_raw::<f32>().unwrap(),
2624                vec![1.5f32, 2.5]
2625            );
2626            assert_eq!(
2627                file.dataset("f64").unwrap().read_raw::<f64>().unwrap(),
2628                vec![1.23456f64, 7.89012]
2629            );
2630        }
2631
2632        std::fs::remove_file(&path).ok();
2633    }
2634
2635    #[test]
2636    fn append_chunked_roundtrip() {
2637        let path = temp_path("append_chunked");
2638
2639        {
2640            let file = H5File::create(&path).unwrap();
2641            let ds = file
2642                .new_dataset::<f64>()
2643                .shape([0, 3])
2644                .chunk(&[1, 3])
2645                .max_shape(&[None, Some(3)])
2646                .create("data")
2647                .unwrap();
2648
2649            // Append one frame
2650            ds.append(&[1.0f64, 2.0, 3.0]).unwrap();
2651            // Append two frames at once
2652            ds.append(&[4.0f64, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap();
2653
2654            file.close().unwrap();
2655        }
2656
2657        {
2658            let file = H5File::open(&path).unwrap();
2659            let ds = file.dataset("data").unwrap();
2660            assert_eq!(ds.shape(), vec![3, 3]);
2661            let all = ds.read_raw::<f64>().unwrap();
2662            assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
2663        }
2664
2665        std::fs::remove_file(&path).ok();
2666    }
2667
2668    #[test]
2669    fn append_1d_chunked() {
2670        let path = temp_path("append_1d");
2671
2672        {
2673            let file = H5File::create(&path).unwrap();
2674            let ds = file
2675                .new_dataset::<i32>()
2676                .shape([0])
2677                .chunk(&[4])
2678                .max_shape(&[None])
2679                .create("values")
2680                .unwrap();
2681
2682            ds.append(&[10i32, 20, 30]).unwrap(); // partial chunk
2683            ds.append(&[40i32]).unwrap(); // fills chunk boundary
2684            ds.append(&[50i32, 60, 70, 80]).unwrap(); // full chunk
2685
2686            file.close().unwrap();
2687        }
2688
2689        {
2690            let file = H5File::open(&path).unwrap();
2691            let ds = file.dataset("values").unwrap();
2692            assert_eq!(ds.shape(), vec![8]);
2693            let all = ds.read_raw::<i32>().unwrap();
2694            assert_eq!(all, vec![10, 20, 30, 40, 50, 60, 70, 80]);
2695        }
2696
2697        std::fs::remove_file(&path).ok();
2698    }
2699
2700    #[test]
2701    fn append_partial_chunk_flushed_on_close() {
2702        let path = temp_path("append_partial_close");
2703
2704        {
2705            let file = H5File::create(&path).unwrap();
2706            let ds = file
2707                .new_dataset::<f64>()
2708                .shape([0])
2709                .chunk(&[4])
2710                .max_shape(&[None])
2711                .create("vals")
2712                .unwrap();
2713
2714            // Append 5 elements: chunk 0 = full [1,2,3,4], chunk 1 = partial [5,0,0,0]
2715            ds.append(&[1.0f64, 2.0, 3.0, 4.0, 5.0]).unwrap();
2716            file.close().unwrap();
2717        }
2718
2719        {
2720            let file = H5File::open(&path).unwrap();
2721            let ds = file.dataset("vals").unwrap();
2722            assert_eq!(ds.shape(), vec![5]);
2723            let all = ds.read_raw::<f64>().unwrap();
2724            // The full dataset is 2 chunks * 4 = 8 elements; shape says 5
2725            // read_raw reads total shape elements
2726            assert_eq!(all.len(), 5);
2727            assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
2728        }
2729
2730        std::fs::remove_file(&path).ok();
2731    }
2732
2733    #[cfg(feature = "deflate")]
2734    #[test]
2735    fn vlen_append_after_reopen_filtered() {
2736        // Reopen + append into a partially-written *compressed* vlen chunk
2737        // (index-block chunk). Exercises filtered-index-block reconstruction
2738        // in open_append plus filtered read-modify-write.
2739        let path = temp_path("vlen_reopen_filtered");
2740        {
2741            let file = H5File::create(&path).unwrap();
2742            file.create_appendable_vlen_dataset(
2743                "strs",
2744                4,
2745                Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2746            )
2747            .unwrap();
2748            file.append_vlen_strings("strs", &["alpha", "beta", "gamma"])
2749                .unwrap();
2750            file.close().unwrap();
2751        }
2752        {
2753            let file = H5File::open_rw(&path).unwrap();
2754            file.append_vlen_strings("strs", &["delta"]).unwrap();
2755            file.close().unwrap();
2756        }
2757        {
2758            let file = H5File::open(&path).unwrap();
2759            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2760            assert_eq!(
2761                got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
2762                vec!["alpha", "beta", "gamma", "delta"]
2763            );
2764        }
2765        std::fs::remove_file(&path).ok();
2766    }
2767
2768    #[test]
2769    fn vlen_append_after_reopen_data_block() {
2770        // Reopen + append into a partial chunk that lives in an extensible-
2771        // array *data block* (chunk index >= idx_blk_elmts). Exercises
2772        // data-block resolution in read_chunk_if_present and write_chunk.
2773        let path = temp_path("vlen_reopen_datablk");
2774        let labels: Vec<String> = (0..9).map(|i| format!("s{i}")).collect();
2775        {
2776            let file = H5File::create(&path).unwrap();
2777            file.create_appendable_vlen_dataset("strs", 2, None)
2778                .unwrap();
2779            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2780            file.append_vlen_strings("strs", &refs).unwrap();
2781            file.close().unwrap();
2782        }
2783        {
2784            let file = H5File::open_rw(&path).unwrap();
2785            file.append_vlen_strings("strs", &["s9"]).unwrap();
2786            file.close().unwrap();
2787        }
2788        {
2789            let file = H5File::open(&path).unwrap();
2790            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2791            let want: Vec<String> = (0..10).map(|i| format!("s{i}")).collect();
2792            assert_eq!(got, want);
2793        }
2794        std::fs::remove_file(&path).ok();
2795    }
2796
2797    #[test]
2798    fn vlen_append_after_reopen_super_block() {
2799        // Reopen + append into a partial chunk whose index falls in an
2800        // extensible-array *super block* (chunk index 244 with the default
2801        // EA geometry: idx_blk_elmts=4, data_blk_min_elmts=16,
2802        // sup_blk_min_data_ptrs=4 -> chunks 0..=243 are reached via the
2803        // index block or its direct data blocks, so chunk 244 is reached
2804        // via a super block read from disk). Exercises the ViaSblk branch
2805        // of read_chunk_if_present.
2806        let path = temp_path("vlen_reopen_super");
2807        // 489 strings, chunk size 2 -> chunk 244 holds one string only
2808        // (partially filled) and is flushed to disk on close.
2809        let labels: Vec<String> = (0..489).map(|i| format!("v{i}")).collect();
2810        {
2811            let file = H5File::create(&path).unwrap();
2812            file.create_appendable_vlen_dataset("strs", 2, None)
2813                .unwrap();
2814            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2815            file.append_vlen_strings("strs", &refs).unwrap();
2816            file.close().unwrap();
2817        }
2818        {
2819            let file = H5File::open_rw(&path).unwrap();
2820            file.append_vlen_strings("strs", &["v489"]).unwrap();
2821            file.close().unwrap();
2822        }
2823        {
2824            let file = H5File::open(&path).unwrap();
2825            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2826            let want: Vec<String> = (0..490).map(|i| format!("v{i}")).collect();
2827            assert_eq!(got, want);
2828        }
2829        std::fs::remove_file(&path).ok();
2830    }
2831
2832    #[cfg(feature = "deflate")]
2833    #[test]
2834    fn vlen_append_after_reopen_filtered_data_block() {
2835        // The hardest path: compressed + chunk in a data block + partial
2836        // read-modify-write across a reopen.
2837        let path = temp_path("vlen_reopen_filt_datablk");
2838        let labels: Vec<String> = (0..9).map(|i| format!("item{i:02}")).collect();
2839        {
2840            let file = H5File::create(&path).unwrap();
2841            file.create_appendable_vlen_dataset(
2842                "strs",
2843                2,
2844                Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2845            )
2846            .unwrap();
2847            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2848            file.append_vlen_strings("strs", &refs).unwrap();
2849            file.close().unwrap();
2850        }
2851        {
2852            let file = H5File::open_rw(&path).unwrap();
2853            file.append_vlen_strings("strs", &["item09"]).unwrap();
2854            file.close().unwrap();
2855        }
2856        {
2857            let file = H5File::open(&path).unwrap();
2858            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2859            let want: Vec<String> = (0..10).map(|i| format!("item{i:02}")).collect();
2860            assert_eq!(got, want);
2861        }
2862        std::fs::remove_file(&path).ok();
2863    }
2864
2865    #[test]
2866    fn group_nx_class_attribute_roundtrip() {
2867        // Non-root groups carry attributes (NeXus `NX_class`) in their
2868        // own object header, and the reader reads them back by path.
2869        let path = temp_path("group_nx_class");
2870        {
2871            let file = H5File::create(&path).unwrap();
2872            let entry = file.create_group("entry").unwrap();
2873            entry.set_attr_string("NX_class", "NXentry").unwrap();
2874            let det = entry.create_group("detector").unwrap();
2875            det.set_attr_string("NX_class", "NXdetector").unwrap();
2876            det.set_attr_numeric("frame_count", &7i32).unwrap();
2877            det.new_dataset::<f32>()
2878                .shape([4])
2879                .create("data")
2880                .unwrap()
2881                .write_raw(&[1.0f32; 4])
2882                .unwrap();
2883            file.close().unwrap();
2884        }
2885        {
2886            let file = H5File::open(&path).unwrap();
2887            let entry = file.root_group().group("entry").unwrap();
2888            assert_eq!(entry.attr_string("NX_class").unwrap(), "NXentry");
2889            let det = entry.group("detector").unwrap();
2890            assert_eq!(det.attr_string("NX_class").unwrap(), "NXdetector");
2891            let names = det.attr_names().unwrap();
2892            assert!(names.contains(&"NX_class".to_string()));
2893            assert!(names.contains(&"frame_count".to_string()));
2894        }
2895        std::fs::remove_file(&path).ok();
2896    }
2897
2898    #[test]
2899    fn ea_super_block_roundtrip() {
2900        // 2000 chunks span several extensible-array super blocks. Before
2901        // super-block support the writer errored at chunk index 228.
2902        let path = temp_path("ea_super_rt");
2903        {
2904            let file = H5File::create(&path).unwrap();
2905            let ds = file
2906                .new_dataset::<i32>()
2907                .shape([0])
2908                .chunk(&[1])
2909                .max_shape(&[None])
2910                .create("v")
2911                .unwrap();
2912            ds.append(&(0..2000).collect::<Vec<i32>>()).unwrap();
2913            file.close().unwrap();
2914        }
2915        {
2916            let file = H5File::open(&path).unwrap();
2917            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2918            assert_eq!(v.len(), 2000);
2919            assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2920        }
2921        std::fs::remove_file(&path).ok();
2922    }
2923
2924    #[cfg(feature = "deflate")]
2925    #[test]
2926    fn ea_filtered_super_block_roundtrip() {
2927        // Compressed chunks across super blocks.
2928        let path = temp_path("ea_filt_super");
2929        {
2930            let file = H5File::create(&path).unwrap();
2931            let ds = file
2932                .new_dataset::<i32>()
2933                .shape([0])
2934                .chunk(&[1])
2935                .max_shape(&[None])
2936                .deflate(4)
2937                .create("v")
2938                .unwrap();
2939            ds.append(&(0..600).collect::<Vec<i32>>()).unwrap();
2940            file.close().unwrap();
2941        }
2942        {
2943            let file = H5File::open(&path).unwrap();
2944            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2945            assert_eq!(v, (0..600).collect::<Vec<i32>>());
2946        }
2947        std::fs::remove_file(&path).ok();
2948    }
2949
2950    #[test]
2951    fn ea_super_block_open_append() {
2952        // Reopen a dataset and append chunks that fall in super blocks.
2953        let path = temp_path("ea_super_append");
2954        {
2955            let file = H5File::create(&path).unwrap();
2956            let ds = file
2957                .new_dataset::<i32>()
2958                .shape([0])
2959                .chunk(&[1])
2960                .max_shape(&[None])
2961                .create("v")
2962                .unwrap();
2963            ds.append(&(0..300).collect::<Vec<i32>>()).unwrap();
2964            file.close().unwrap();
2965        }
2966        {
2967            let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2968            let idx = w.dataset_index("v").unwrap();
2969            for c in 300..900u64 {
2970                w.write_chunk(idx, c, &(c as i32).to_le_bytes()).unwrap();
2971            }
2972            w.extend_dataset(idx, &[900]).unwrap();
2973            w.close().unwrap();
2974        }
2975        {
2976            let file = H5File::open(&path).unwrap();
2977            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2978            assert_eq!(v.len(), 900);
2979            assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2980        }
2981        std::fs::remove_file(&path).ok();
2982    }
2983
2984    #[test]
2985    fn btree_v2_multi_unlimited_roundtrip() {
2986        // A dataset with two unlimited dimensions uses the v2 B-tree chunk
2987        // index; chunks are written by grid coordinates with write_chunk_at.
2988        let path = temp_path("bt2_multi");
2989        {
2990            let file = H5File::create(&path).unwrap();
2991            let ds = file
2992                .new_dataset::<i32>()
2993                .shape([0, 0])
2994                .chunk(&[2, 2])
2995                .max_shape(&[None, None])
2996                .create("grid")
2997                .unwrap();
2998            assert!(ds.is_chunked());
2999            // 4x4 logical grid, value[r][c] = r*4 + c, in 2x2 chunks.
3000            for cr in 0..2usize {
3001                for cc in 0..2usize {
3002                    let mut bytes = Vec::new();
3003                    for i in 0..2usize {
3004                        for j in 0..2usize {
3005                            let v = ((cr * 2 + i) * 4 + (cc * 2 + j)) as i32;
3006                            bytes.extend_from_slice(&v.to_le_bytes());
3007                        }
3008                    }
3009                    ds.write_chunk_at(&[cr, cc], &bytes).unwrap();
3010                }
3011            }
3012            file.close().unwrap();
3013        }
3014        {
3015            let file = H5File::open(&path).unwrap();
3016            let ds = file.dataset("grid").unwrap();
3017            assert_eq!(ds.shape(), vec![4, 4]);
3018            assert_eq!(ds.read_raw::<i32>().unwrap(), (0..16).collect::<Vec<i32>>());
3019        }
3020        std::fs::remove_file(&path).ok();
3021    }
3022
3023    #[test]
3024    fn subframe_chunking_roundtrip() {
3025        // A chunk smaller than a frame: shape [N,8,8], chunk [1,4,4], so each
3026        // frame is tiled into a 2x2 grid of 4x4 chunks. write_chunk_at takes
3027        // the chunk-grid coordinates.
3028        let path = temp_path("subframe");
3029        {
3030            let file = H5File::create(&path).unwrap();
3031            let ds = file
3032                .new_dataset::<i32>()
3033                .shape([0, 8, 8])
3034                .chunk(&[1, 4, 4])
3035                .max_shape(&[None, Some(8), Some(8)])
3036                .create("v")
3037                .unwrap();
3038            for f in 0..3usize {
3039                for cr in 0..2usize {
3040                    for cc in 0..2usize {
3041                        let mut bytes = Vec::new();
3042                        for i in 0..4usize {
3043                            for j in 0..4usize {
3044                                let v = (f * 64 + (cr * 4 + i) * 8 + (cc * 4 + j)) as i32;
3045                                bytes.extend_from_slice(&v.to_le_bytes());
3046                            }
3047                        }
3048                        ds.write_chunk_at(&[f, cr, cc], &bytes).unwrap();
3049                    }
3050                }
3051            }
3052            file.close().unwrap();
3053        }
3054        {
3055            let file = H5File::open(&path).unwrap();
3056            let ds = file.dataset("v").unwrap();
3057            assert_eq!(ds.shape(), vec![3, 8, 8]);
3058            assert_eq!(
3059                ds.read_raw::<i32>().unwrap(),
3060                (0..192).collect::<Vec<i32>>()
3061            );
3062        }
3063        std::fs::remove_file(&path).ok();
3064    }
3065
3066    #[test]
3067    fn fill_value_contiguous_roundtrip() {
3068        let path = temp_path("fill_value_contig");
3069        {
3070            let file = H5File::create(&path).unwrap();
3071            let ds = file
3072                .new_dataset::<f32>()
3073                .shape([4])
3074                .fill_value(2.5f32)
3075                .create("data")
3076                .unwrap();
3077            ds.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
3078            file.close().unwrap();
3079        }
3080        // open_append decodes the fill-value message back from the header.
3081        {
3082            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
3083            let idx = writer.dataset_index("data").unwrap();
3084            assert_eq!(
3085                writer.ds(idx).lock().fill_value,
3086                Some(2.5f32.to_le_bytes().to_vec())
3087            );
3088        }
3089        // Data still reads back correctly.
3090        {
3091            let file = H5File::open(&path).unwrap();
3092            let ds = file.dataset("data").unwrap();
3093            assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
3094        }
3095        std::fs::remove_file(&path).ok();
3096    }
3097
3098    #[test]
3099    fn fill_value_chunked_roundtrip() {
3100        let path = temp_path("fill_value_chunked");
3101        {
3102            let file = H5File::create(&path).unwrap();
3103            let ds = file
3104                .new_dataset::<i32>()
3105                .shape([0])
3106                .chunk(&[4])
3107                .max_shape(&[None])
3108                .fill_value(-7i32)
3109                .create("vals")
3110                .unwrap();
3111            ds.append(&[1i32, 2, 3, 4]).unwrap();
3112            file.close().unwrap();
3113        }
3114        {
3115            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
3116            let idx = writer.dataset_index("vals").unwrap();
3117            assert_eq!(
3118                writer.ds(idx).lock().fill_value,
3119                Some((-7i32).to_le_bytes().to_vec())
3120            );
3121        }
3122        std::fs::remove_file(&path).ok();
3123    }
3124
3125    #[test]
3126    fn fill_value_read_missing_chunks() {
3127        // A chunked dataset with chunk 1 left unwritten must read that
3128        // gap back as the user-defined fill value, not zero.
3129        fn i32_bytes(vals: &[i32]) -> Vec<u8> {
3130            vals.iter().flat_map(|v| v.to_le_bytes()).collect()
3131        }
3132        let path = temp_path("fill_value_read_missing");
3133        {
3134            let file = H5File::create(&path).unwrap();
3135            let ds = file
3136                .new_dataset::<i32>()
3137                .shape([0])
3138                .chunk(&[2])
3139                .max_shape(&[None])
3140                .fill_value(-1i32)
3141                .create("vals")
3142                .unwrap();
3143            // chunk 0 = [10,20]; chunk 1 unwritten; chunk 2 = [50,60].
3144            ds.write_chunk(0, &i32_bytes(&[10, 20])).unwrap();
3145            ds.write_chunk(2, &i32_bytes(&[50, 60])).unwrap();
3146            ds.extend(&[6]).unwrap();
3147            file.close().unwrap();
3148        }
3149        {
3150            let file = H5File::open(&path).unwrap();
3151            let ds = file.dataset("vals").unwrap();
3152            let all = ds.read_raw::<i32>().unwrap();
3153            assert_eq!(all, vec![10, 20, -1, -1, 50, 60]);
3154        }
3155        std::fs::remove_file(&path).ok();
3156    }
3157
3158    #[test]
3159    fn fill_value_partial_chunk_padded_with_fill() {
3160        // A partial trailing chunk flushed at close must pad its unwritten
3161        // tail with the fill value. That pad sits beyond the logical shape,
3162        // so it is verified by scanning the on-disk chunk bytes directly.
3163        let path = temp_path("fill_value_partial_pad");
3164        {
3165            let file = H5File::create(&path).unwrap();
3166            let ds = file
3167                .new_dataset::<i32>()
3168                .shape([0])
3169                .chunk(&[4])
3170                .max_shape(&[None])
3171                .fill_value(-9i32)
3172                .create("vals")
3173                .unwrap();
3174            // 3 of 4 frames -> flushed as a partial chunk on close.
3175            ds.append(&[1i32, 2, 3]).unwrap();
3176            file.close().unwrap();
3177        }
3178        let bytes = std::fs::read(&path).unwrap();
3179        // Locate the chunk: i32 LE of [1, 2, 3] written contiguously.
3180        let needle: Vec<u8> = [1i32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
3181        let pos = bytes
3182            .windows(needle.len())
3183            .position(|w| w == needle)
3184            .expect("chunk data [1,2,3] not found in file");
3185        let pad = &bytes[pos + needle.len()..pos + needle.len() + 4];
3186        assert_eq!(
3187            pad,
3188            &(-9i32).to_le_bytes(),
3189            "partial chunk tail must be padded with fill value -9, got {:?}",
3190            pad
3191        );
3192        std::fs::remove_file(&path).ok();
3193    }
3194
3195    #[test]
3196    fn vlen_append_after_reopen_preserves_existing() {
3197        // Reopening and appending into a partially-written vlen chunk must
3198        // read-modify-write: the strings already on disk must survive.
3199        let path = temp_path("vlen_append_reopen");
3200        {
3201            let file = H5File::create(&path).unwrap();
3202            file.create_appendable_vlen_dataset("strs", 4, None)
3203                .unwrap();
3204            // 3 of 4 frames -> flushed as a partial chunk on close.
3205            file.append_vlen_strings("strs", &["a", "b", "c"]).unwrap();
3206            file.close().unwrap();
3207        }
3208        {
3209            // Append a 4th string -> partial-chunk write into chunk 0.
3210            let file = H5File::open_rw(&path).unwrap();
3211            file.append_vlen_strings("strs", &["d"]).unwrap();
3212            file.close().unwrap();
3213        }
3214        {
3215            let file = H5File::open(&path).unwrap();
3216            let ds = file.dataset("strs").unwrap();
3217            let got = ds.read_vlen_strings().unwrap();
3218            assert_eq!(
3219                got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
3220                vec!["a", "b", "c", "d"]
3221            );
3222        }
3223        std::fs::remove_file(&path).ok();
3224    }
3225
3226    #[test]
3227    fn fill_value_size_mismatch_errors() {
3228        let path = temp_path("fill_value_mismatch");
3229        let writer = crate::io::writer::Hdf5Writer::create(&path).unwrap();
3230        let dt = <f64 as crate::types::H5Type>::hdf5_type();
3231        let idx = writer.create_dataset("d", dt, &[4u64]).unwrap();
3232        // f64 element size is 8; a 4-byte fill value must be rejected.
3233        assert!(writer.set_dataset_fill_value(idx, vec![0u8; 4]).is_err());
3234        // The correct width succeeds.
3235        writer.set_dataset_fill_value(idx, vec![0u8; 8]).unwrap();
3236        writer.close().unwrap();
3237        std::fs::remove_file(&path).ok();
3238    }
3239
3240    #[test]
3241    fn datatype_exposes_class_sign_and_byteorder() {
3242        // The byte width alone cannot tell u8 from i8 (both 1 byte) or i32
3243        // from f32 (both 4 bytes). datatype() must report the real class and
3244        // signedness so a reader does not have to guess from element_size.
3245        use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
3246
3247        let path = temp_path("datatype_accessor");
3248        {
3249            let file = H5File::create(&path).unwrap();
3250            file.new_dataset::<u8>().shape([3]).create("u8d").unwrap();
3251            file.new_dataset::<i8>().shape([3]).create("i8d").unwrap();
3252            file.new_dataset::<i32>().shape([3]).create("i32d").unwrap();
3253            file.new_dataset::<f32>().shape([3]).create("f32d").unwrap();
3254            file.close().unwrap();
3255        }
3256
3257        let file = H5File::open(&path).unwrap();
3258
3259        match file.dataset("u8d").unwrap().datatype().unwrap() {
3260            DatatypeMessage::FixedPoint {
3261                size,
3262                signed,
3263                byte_order,
3264                ..
3265            } => {
3266                assert_eq!(size, 1);
3267                assert!(!signed, "u8 must be unsigned");
3268                assert_eq!(byte_order, ByteOrder::LittleEndian);
3269            }
3270            other => panic!("expected FixedPoint for u8, got {other:?}"),
3271        }
3272
3273        match file.dataset("i8d").unwrap().datatype().unwrap() {
3274            DatatypeMessage::FixedPoint { size, signed, .. } => {
3275                assert_eq!(size, 1);
3276                assert!(signed, "i8 must be signed");
3277            }
3278            other => panic!("expected FixedPoint for i8, got {other:?}"),
3279        }
3280
3281        match file.dataset("i32d").unwrap().datatype().unwrap() {
3282            DatatypeMessage::FixedPoint { size, signed, .. } => {
3283                assert_eq!(size, 4);
3284                assert!(signed, "i32 must be signed");
3285            }
3286            other => panic!("expected FixedPoint for i32, got {other:?}"),
3287        }
3288
3289        match file.dataset("f32d").unwrap().datatype().unwrap() {
3290            DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 4),
3291            other => panic!("expected FloatingPoint for f32, got {other:?}"),
3292        }
3293
3294        std::fs::remove_file(&path).ok();
3295    }
3296
3297    #[test]
3298    fn datatype_in_write_mode_errors() {
3299        let path = temp_path("datatype_write_mode");
3300        let file = H5File::create(&path).unwrap();
3301        let ds = file.new_dataset::<f32>().shape([4]).create("d").unwrap();
3302        assert!(ds.datatype().is_err());
3303        std::fs::remove_file(&path).ok();
3304    }
3305
3306    // --- write_chunk_raw (HDF5 direct chunk write) ---------------------------
3307
3308    /// Extensible-array path: pre-compress with the dataset's pipeline, write
3309    /// the bytes verbatim via write_chunk_raw (filter_mask = 0), and confirm
3310    /// the data round-trips through the reader unchanged.
3311    #[cfg(feature = "deflate")]
3312    #[test]
3313    fn write_chunk_raw_ea_roundtrip_mask0() {
3314        use crate::format::messages::filter::{apply_filters, FilterPipeline};
3315        let path = temp_path("wcr_ea_mask0");
3316        let original: Vec<i32> = (0..12).collect();
3317        {
3318            let file = H5File::create(&path).unwrap();
3319            let ds = file
3320                .new_dataset::<i32>()
3321                .shape([0])
3322                .chunk(&[4])
3323                .max_shape(&[None])
3324                .deflate(4)
3325                .create("v")
3326                .unwrap();
3327            assert!(ds.is_chunked());
3328            let pipeline = FilterPipeline::deflate(4);
3329            for c in 0..3usize {
3330                let raw: Vec<u8> = original[c * 4..c * 4 + 4]
3331                    .iter()
3332                    .flat_map(|v| v.to_le_bytes())
3333                    .collect();
3334                let compressed = apply_filters(&pipeline, &raw).unwrap();
3335                ds.write_chunk_raw(c, &compressed, 0).unwrap();
3336            }
3337            ds.set_extent(&[12]).unwrap();
3338            file.close().unwrap();
3339        }
3340        {
3341            let file = H5File::open(&path).unwrap();
3342            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3343            assert_eq!(v, original);
3344        }
3345        std::fs::remove_file(&path).ok();
3346    }
3347
3348    /// Fixed-array path (all dimensions bounded): same verbatim write through
3349    /// the linear-index dispatch, round-tripped through the reader.
3350    #[cfg(feature = "deflate")]
3351    #[test]
3352    fn write_chunk_raw_fixed_array_roundtrip_mask0() {
3353        use crate::format::messages::filter::{apply_filters, FilterPipeline};
3354        let path = temp_path("wcr_fa_mask0");
3355        let original: Vec<i32> = (0..12).collect();
3356        {
3357            let file = H5File::create(&path).unwrap();
3358            let ds = file
3359                .new_dataset::<i32>()
3360                .shape([12])
3361                .chunk(&[4])
3362                .deflate(4)
3363                .create("v")
3364                .unwrap();
3365            assert!(ds.is_chunked());
3366            let pipeline = FilterPipeline::deflate(4);
3367            for c in 0..3usize {
3368                let raw: Vec<u8> = original[c * 4..c * 4 + 4]
3369                    .iter()
3370                    .flat_map(|v| v.to_le_bytes())
3371                    .collect();
3372                let compressed = apply_filters(&pipeline, &raw).unwrap();
3373                ds.write_chunk_raw(c, &compressed, 0).unwrap();
3374            }
3375            file.close().unwrap();
3376        }
3377        {
3378            let file = H5File::open(&path).unwrap();
3379            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3380            assert_eq!(v, original);
3381        }
3382        std::fs::remove_file(&path).ok();
3383    }
3384
3385    /// The caller-supplied filter_mask must reach the on-disk filtered index
3386    /// entry (not be hardcoded to 0). Store one chunk uncompressed in a
3387    /// filtered dataset with mask = 1 (deflate skipped), then reopen and decode
3388    /// the extensible-array filtered entry to read the mask back at the format
3389    /// level (independent of the data reader's mask handling).
3390    #[cfg(feature = "deflate")]
3391    #[test]
3392    fn write_chunk_raw_records_filter_mask() {
3393        let path = temp_path("wcr_records_mask");
3394        let raw: Vec<u8> = [10i32, 20, 30, 40]
3395            .iter()
3396            .flat_map(|v| v.to_le_bytes())
3397            .collect();
3398        assert_eq!(raw.len(), 16);
3399        {
3400            let file = H5File::create(&path).unwrap();
3401            let ds = file
3402                .new_dataset::<i32>()
3403                .shape([0])
3404                .chunk(&[4])
3405                .max_shape(&[None])
3406                .deflate(4)
3407                .create("v")
3408                .unwrap();
3409            // mask = 1: bit 0 set => filter 0 (deflate) was skipped, so the
3410            // chunk is stored uncompressed (its raw bytes).
3411            ds.write_chunk_raw(0, &raw, 1).unwrap();
3412            ds.set_extent(&[4]).unwrap();
3413            file.close().unwrap();
3414        }
3415        // Reopen the writer; open_append decodes the filtered index block from
3416        // disk, so the entry reflects exactly what was committed.
3417        {
3418            let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
3419            let idx = w.dataset_index("v").unwrap();
3420            let ds = w.ds(idx);
3421            let m = ds.lock();
3422            let entry = &m
3423                .chunked
3424                .as_ref()
3425                .unwrap()
3426                .filt_iblk
3427                .as_ref()
3428                .unwrap()
3429                .elements[0];
3430            assert_eq!(entry.filter_mask, 1, "filter_mask must round-trip to disk");
3431            assert_eq!(entry.nbytes, 16, "uncompressed chunk stored verbatim");
3432        }
3433        std::fs::remove_file(&path).ok();
3434    }
3435
3436    /// Reader honors a per-chunk filter_mask (EA): one chunk is stored
3437    /// compressed (mask 0), the next stored raw with deflate skipped (mask 1),
3438    /// in the same dataset. A correct reader skips deflate for chunk 1 only;
3439    /// ignoring the mask would feed raw bytes through inflate and corrupt them.
3440    #[cfg(feature = "deflate")]
3441    #[test]
3442    fn write_chunk_raw_ea_per_chunk_mask_roundtrip() {
3443        use crate::format::messages::filter::{apply_filters, FilterPipeline};
3444        let path = temp_path("wcr_ea_per_chunk_mask");
3445        let original: Vec<i32> = (0..8).collect();
3446        let pipeline = FilterPipeline::deflate(4);
3447        {
3448            let file = H5File::create(&path).unwrap();
3449            let ds = file
3450                .new_dataset::<i32>()
3451                .shape([0])
3452                .chunk(&[4])
3453                .max_shape(&[None])
3454                .deflate(4)
3455                .create("v")
3456                .unwrap();
3457            let raw0: Vec<u8> = original[0..4]
3458                .iter()
3459                .flat_map(|v| v.to_le_bytes())
3460                .collect();
3461            // chunk 0: compressed through the pipeline, mask 0.
3462            ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
3463                .unwrap();
3464            let raw1: Vec<u8> = original[4..8]
3465                .iter()
3466                .flat_map(|v| v.to_le_bytes())
3467                .collect();
3468            // chunk 1: stored uncompressed, mask 1 (deflate skipped).
3469            ds.write_chunk_raw(1, &raw1, 1).unwrap();
3470            ds.set_extent(&[8]).unwrap();
3471            file.close().unwrap();
3472        }
3473        {
3474            let file = H5File::open(&path).unwrap();
3475            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3476            assert_eq!(v, original);
3477        }
3478        std::fs::remove_file(&path).ok();
3479    }
3480
3481    /// Reader honors a per-chunk filter_mask (fixed array): same mixed
3482    /// compressed/raw chunks as the EA case, through the fixed-array index.
3483    #[cfg(feature = "deflate")]
3484    #[test]
3485    fn write_chunk_raw_fixed_array_per_chunk_mask_roundtrip() {
3486        use crate::format::messages::filter::{apply_filters, FilterPipeline};
3487        let path = temp_path("wcr_fa_per_chunk_mask");
3488        let original: Vec<i32> = (0..8).collect();
3489        let pipeline = FilterPipeline::deflate(4);
3490        {
3491            let file = H5File::create(&path).unwrap();
3492            let ds = file
3493                .new_dataset::<i32>()
3494                .shape([8])
3495                .chunk(&[4])
3496                .deflate(4)
3497                .create("v")
3498                .unwrap();
3499            let raw0: Vec<u8> = original[0..4]
3500                .iter()
3501                .flat_map(|v| v.to_le_bytes())
3502                .collect();
3503            ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
3504                .unwrap();
3505            let raw1: Vec<u8> = original[4..8]
3506                .iter()
3507                .flat_map(|v| v.to_le_bytes())
3508                .collect();
3509            ds.write_chunk_raw(1, &raw1, 1).unwrap();
3510            file.close().unwrap();
3511        }
3512        {
3513            let file = H5File::open(&path).unwrap();
3514            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3515            assert_eq!(v, original);
3516        }
3517        std::fs::remove_file(&path).ok();
3518    }
3519
3520    /// An unfiltered chunk index has no slot for a stored size or mask, so a
3521    /// direct chunk write must be rejected rather than silently dropping them.
3522    #[test]
3523    fn write_chunk_raw_rejects_unfiltered() {
3524        let path = temp_path("wcr_unfiltered");
3525        let file = H5File::create(&path).unwrap();
3526        let ds = file
3527            .new_dataset::<i32>()
3528            .shape([0])
3529            .chunk(&[4])
3530            .max_shape(&[None])
3531            .create("v")
3532            .unwrap();
3533        let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
3534        assert!(
3535            err.to_string().contains("filtered dataset"),
3536            "expected a filtered-dataset error, got: {err}"
3537        );
3538        std::fs::remove_file(&path).ok();
3539    }
3540
3541    /// v2-B-tree-indexed datasets (two or more unlimited dimensions) do not
3542    /// support direct chunk writes.
3543    #[test]
3544    fn write_chunk_raw_rejects_btree_v2() {
3545        let path = temp_path("wcr_btree2");
3546        let file = H5File::create(&path).unwrap();
3547        let ds = file
3548            .new_dataset::<i32>()
3549            .shape([0, 0])
3550            .chunk(&[2, 2])
3551            .max_shape(&[None, None])
3552            .create("grid")
3553            .unwrap();
3554        let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
3555        assert!(
3556            err.to_string().contains("v2-B-tree"),
3557            "expected a v2-B-tree rejection, got: {err}"
3558        );
3559        std::fs::remove_file(&path).ok();
3560    }
3561
3562    /// A stored size that does not fit the index's chunk-size field must error
3563    /// (libhdf5 H5D_CHUNK_ENCODE_SIZE_CHECK) instead of truncating silently.
3564    /// A 4-byte chunk (chunk[1] of i32) has chunk_size_len = 2 (max 65535), so
3565    /// a 70000-byte stored chunk overflows it.
3566    #[cfg(feature = "deflate")]
3567    #[test]
3568    fn write_chunk_raw_rejects_oversized_chunk() {
3569        let path = temp_path("wcr_oversized");
3570        let file = H5File::create(&path).unwrap();
3571        let ds = file
3572            .new_dataset::<i32>()
3573            .shape([0])
3574            .chunk(&[1])
3575            .max_shape(&[None])
3576            .deflate(4)
3577            .create("v")
3578            .unwrap();
3579        let err = ds.write_chunk_raw(0, &vec![0u8; 70000], 0).unwrap_err();
3580        assert!(
3581            err.to_string().contains("does not fit"),
3582            "expected a chunk-size-field overflow error, got: {err}"
3583        );
3584        std::fs::remove_file(&path).ok();
3585    }
3586}