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        let element_size = T::element_size();
234
235        if let Some(ref chunk_dims) = self.chunk_dims {
236            // Chunked dataset
237            let chunk_u64: Vec<u64> = chunk_dims.iter().map(|&d| d as u64).collect();
238            let max_u64: Vec<u64> = if let Some(ref max) = self.max_shape {
239                max.iter()
240                    .map(|m| m.map_or(u64::MAX, |v| v as u64))
241                    .collect()
242            } else {
243                // Default: max = current
244                dims_u64.clone()
245            };
246
247            // libhdf5 selects the chunk index from the dataspace: a v2
248            // B-tree for two or more unlimited dimensions, an extensible
249            // array for exactly one, and a fixed array when there are none.
250            let n_unlimited = max_u64.iter().filter(|&&m| m == u64::MAX).count();
251            let is_btree2 = n_unlimited >= 2;
252            let is_fixed_array = n_unlimited == 0;
253            let wants_filter = self.custom_pipeline.is_some()
254                || self.shuffle_deflate_level.is_some()
255                || self.deflate_level.is_some();
256
257            let index = {
258                let mut inner = borrow_inner_mut(&self.file_inner);
259                match &mut *inner {
260                    H5FileInner::Writer(writer) => {
261                        let idx = if is_btree2 {
262                            if wants_filter {
263                                return Err(Hdf5Error::InvalidState(
264                                    "compression of v2 B-tree (multi-unlimited-dimension) \
265                                     datasets is not yet supported"
266                                        .into(),
267                                ));
268                            }
269                            writer.create_btree_v2_dataset(
270                                &full_name, datatype, &dims_u64, &max_u64, &chunk_u64,
271                            )?
272                        } else if is_fixed_array {
273                            // A chunked dataset with no unlimited dimension
274                            // must use the fixed-array index — libhdf5
275                            // rejects an extensible-array index here. A
276                            // compressed fixed-shape dataset uses a *filtered*
277                            // fixed array (FA client id 1).
278                            if wants_filter {
279                                let pipeline = if let Some(p) = self.custom_pipeline {
280                                    p
281                                } else if let Some(level) = self.shuffle_deflate_level {
282                                    crate::format::messages::filter::FilterPipeline::shuffle_deflate(
283                                        T::element_size() as u32,
284                                        level,
285                                    )
286                                } else {
287                                    // deflate_level (checked by wants_filter).
288                                    crate::format::messages::filter::FilterPipeline::deflate(
289                                        self.deflate_level.unwrap(),
290                                    )
291                                };
292                                writer.create_fixed_array_dataset_with_pipeline(
293                                    &full_name, datatype, &dims_u64, &chunk_u64, pipeline,
294                                )?
295                            } else {
296                                writer.create_fixed_array_dataset(
297                                    &full_name, datatype, &dims_u64, &chunk_u64,
298                                )?
299                            }
300                        } else if let Some(pipeline) = self.custom_pipeline {
301                            writer.create_chunked_dataset_with_pipeline(
302                                &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, pipeline,
303                            )?
304                        } else if let Some(level) = self.shuffle_deflate_level {
305                            let pipeline =
306                                crate::format::messages::filter::FilterPipeline::shuffle_deflate(
307                                    T::element_size() as u32,
308                                    level,
309                                );
310                            writer.create_chunked_dataset_with_pipeline(
311                                &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, pipeline,
312                            )?
313                        } else if let Some(level) = self.deflate_level {
314                            writer.create_chunked_dataset_compressed(
315                                &full_name, datatype, &dims_u64, &max_u64, &chunk_u64, level,
316                            )?
317                        } else {
318                            writer.create_chunked_dataset(
319                                &full_name, datatype, &dims_u64, &max_u64, &chunk_u64,
320                            )?
321                        };
322                        if let Some(ref gp) = group_path {
323                            if gp != "/" {
324                                writer.assign_dataset_to_group(gp, idx)?;
325                            }
326                        }
327                        if let Some(ref fv) = fill_value {
328                            writer.set_dataset_fill_value(idx, fv.clone())?;
329                        }
330                        idx
331                    }
332                    H5FileInner::Reader(_) => {
333                        return Err(Hdf5Error::InvalidState(
334                            "cannot create a dataset in read mode".into(),
335                        ));
336                    }
337                    H5FileInner::Closed => {
338                        return Err(Hdf5Error::InvalidState("file is closed".into()));
339                    }
340                }
341            };
342
343            Ok(H5Dataset {
344                file_inner: clone_inner(&self.file_inner),
345                info: DatasetInfo::Writer {
346                    index,
347                    shape,
348                    element_size,
349                    chunked: true,
350                    btree2: is_btree2,
351                    fixed_array: is_fixed_array,
352                },
353            })
354        } else {
355            // Contiguous dataset (original path)
356            let index = {
357                let mut inner = borrow_inner_mut(&self.file_inner);
358                match &mut *inner {
359                    H5FileInner::Writer(writer) => {
360                        let idx = writer.create_dataset(&full_name, datatype, &dims_u64)?;
361                        if let Some(ref gp) = group_path {
362                            if gp != "/" {
363                                writer.assign_dataset_to_group(gp, idx)?;
364                            }
365                        }
366                        if let Some(ref fv) = fill_value {
367                            writer.set_dataset_fill_value(idx, fv.clone())?;
368                        }
369                        idx
370                    }
371                    H5FileInner::Reader(_) => {
372                        return Err(Hdf5Error::InvalidState(
373                            "cannot create a dataset in read mode".into(),
374                        ));
375                    }
376                    H5FileInner::Closed => {
377                        return Err(Hdf5Error::InvalidState("file is closed".into()));
378                    }
379                }
380            };
381
382            Ok(H5Dataset {
383                file_inner: clone_inner(&self.file_inner),
384                info: DatasetInfo::Writer {
385                    index,
386                    shape,
387                    element_size,
388                    chunked: false,
389                    btree2: false,
390                    fixed_array: false,
391                },
392            })
393        }
394    }
395}
396
397// ---------------------------------------------------------------------------
398// DatasetInfo
399// ---------------------------------------------------------------------------
400
401/// Internal metadata about a dataset handle.
402enum DatasetInfo {
403    /// A dataset created via `new_dataset().create()` in write mode.
404    Writer {
405        /// Index into the writer's dataset list.
406        index: usize,
407        /// Shape (current dimensions).
408        shape: Vec<usize>,
409        /// Size of one element in bytes.
410        element_size: usize,
411        /// Whether this is a chunked dataset.
412        chunked: bool,
413        /// Whether the chunk index is a v2 B-tree (multiple unlimited dims).
414        btree2: bool,
415        /// Whether the chunk index is a Fixed Array (no unlimited dims).
416        fixed_array: bool,
417    },
418    /// A dataset opened by name in read mode.
419    Reader {
420        /// The link name of the dataset.
421        name: String,
422        /// Shape (current dimensions).
423        shape: Vec<usize>,
424        /// Size of one element in bytes.
425        element_size: usize,
426    },
427}
428
429// ---------------------------------------------------------------------------
430// H5Dataset
431// ---------------------------------------------------------------------------
432
433/// A handle to an HDF5 dataset, supporting typed read and write operations.
434///
435/// The dataset holds a shared reference to the file's I/O backend, so it
436/// remains valid even if the originating [`H5File`](crate::file::H5File) is
437/// moved or dropped (they share ownership via `Rc`).
438pub struct H5Dataset {
439    file_inner: SharedInner,
440    info: DatasetInfo,
441}
442
443impl H5Dataset {
444    /// Create a reader-mode dataset handle (called internally by `H5File::dataset`).
445    pub(crate) fn new_reader(
446        file_inner: SharedInner,
447        name: String,
448        shape: Vec<usize>,
449        element_size: usize,
450    ) -> Self {
451        Self {
452            file_inner,
453            info: DatasetInfo::Reader {
454                name,
455                shape,
456                element_size,
457            },
458        }
459    }
460
461    /// Create a writer-mode dataset handle for an already-created dataset
462    /// (called internally by [`H5File::dataset_writer`](crate::file::H5File::dataset_writer)).
463    ///
464    /// Reconstructs the same handle `new_dataset().create()` returns, so the
465    /// reopened dataset supports attribute writes and chunk appends.
466    pub(crate) fn new_writer(
467        file_inner: SharedInner,
468        index: usize,
469        shape: Vec<usize>,
470        element_size: usize,
471        chunked: bool,
472        btree2: bool,
473        fixed_array: bool,
474    ) -> Self {
475        Self {
476            file_inner,
477            info: DatasetInfo::Writer {
478                index,
479                shape,
480                element_size,
481                chunked,
482                btree2,
483                fixed_array,
484            },
485        }
486    }
487
488    /// Return the dataset dimensions.
489    pub fn shape(&self) -> Vec<usize> {
490        match &self.info {
491            DatasetInfo::Writer { shape, .. } => shape.clone(),
492            DatasetInfo::Reader { shape, .. } => shape.clone(),
493        }
494    }
495
496    /// Return the number of dimensions (rank) of the dataset.
497    pub fn ndims(&self) -> usize {
498        match &self.info {
499            DatasetInfo::Writer { shape, .. } => shape.len(),
500            DatasetInfo::Reader { shape, .. } => shape.len(),
501        }
502    }
503
504    /// Return the total number of elements in the dataset.
505    pub fn total_elements(&self) -> usize {
506        match &self.info {
507            DatasetInfo::Writer { shape, .. } => shape.iter().product(),
508            DatasetInfo::Reader { shape, .. } => shape.iter().product(),
509        }
510    }
511
512    /// Return the size of one element in bytes.
513    pub fn element_size(&self) -> usize {
514        match &self.info {
515            DatasetInfo::Writer { element_size, .. } => *element_size,
516            DatasetInfo::Reader { element_size, .. } => *element_size,
517        }
518    }
519
520    /// Return the element datatype as parsed from the file (read mode only).
521    ///
522    /// Unlike [`element_size`](Self::element_size), which reports only the
523    /// byte width, this exposes the full datatype: its class (integer vs
524    /// floating-point vs string vs compound …), signedness, byte order and
525    /// bit precision. Callers that must reconstruct the exact stored type —
526    /// for example to map it to a NumPy / Arrow dtype — should use this
527    /// instead of inferring a type from the byte width, which cannot
528    /// distinguish `u8` from `i8` (both 1 byte) or `i32` from `f32` (both 4
529    /// bytes).
530    ///
531    /// # Errors
532    ///
533    /// Returns an error if the file is in write mode, or if the dataset can
534    /// no longer be found in the reader's metadata.
535    ///
536    /// ```no_run
537    /// # use rust_hdf5::{H5File, DatatypeMessage};
538    /// let file = H5File::open("data.h5").unwrap();
539    /// let ds = file.dataset("image").unwrap();
540    /// match ds.datatype().unwrap() {
541    ///     DatatypeMessage::FixedPoint { size, signed, .. } => {
542    ///         println!("integer: {} bytes, signed={}", size, signed);
543    ///     }
544    ///     DatatypeMessage::FloatingPoint { size, .. } => {
545    ///         println!("float: {} bytes", size);
546    ///     }
547    ///     other => println!("other type: {other}"),
548    /// }
549    /// ```
550    pub fn datatype(&self) -> Result<DatatypeMessage> {
551        match &self.info {
552            DatasetInfo::Reader { name, .. } => {
553                let inner = borrow_inner(&self.file_inner);
554                match &*inner {
555                    H5FileInner::Reader(reader) => reader
556                        .dataset_info(name)
557                        .map(|info| info.datatype.clone())
558                        .ok_or_else(|| Hdf5Error::NotFound(name.clone())),
559                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
560                }
561            }
562            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
563                "datatype() is only available in read mode".into(),
564            )),
565        }
566    }
567
568    /// Return the chunk dimensions, if this is a chunked dataset.
569    pub fn chunk_dims(&self) -> Option<Vec<usize>> {
570        match &self.info {
571            DatasetInfo::Reader { name, .. } => {
572                let inner = borrow_inner(&self.file_inner);
573                if let H5FileInner::Reader(reader) = &*inner {
574                    if let Some(info) = reader.dataset_info(name) {
575                        use crate::format::messages::data_layout::DataLayoutMessage;
576                        let chunk_dims = match &info.layout {
577                            DataLayoutMessage::ChunkedV4 { chunk_dims, .. }
578                            | DataLayoutMessage::ChunkedV3 { chunk_dims, .. } => Some(chunk_dims),
579                            _ => None,
580                        };
581                        if let Some(chunk_dims) = chunk_dims {
582                            // Strip trailing element-size dimension
583                            return Some(
584                                chunk_dims[..chunk_dims.len() - 1]
585                                    .iter()
586                                    .map(|&d| d as usize)
587                                    .collect(),
588                            );
589                        }
590                    }
591                }
592                None
593            }
594            DatasetInfo::Writer { .. } => None,
595        }
596    }
597
598    /// Return whether this is a chunked dataset.
599    pub fn is_chunked(&self) -> bool {
600        match &self.info {
601            DatasetInfo::Writer { chunked, .. } => *chunked,
602            DatasetInfo::Reader { name, .. } => {
603                let inner = borrow_inner(&self.file_inner);
604                match &*inner {
605                    H5FileInner::Reader(reader) => {
606                        if let Some(info) = reader.dataset_info(name) {
607                            use crate::format::messages::data_layout::DataLayoutMessage;
608                            matches!(
609                                info.layout,
610                                DataLayoutMessage::ChunkedV4 { .. }
611                                    | DataLayoutMessage::ChunkedV3 { .. }
612                            )
613                        } else {
614                            false
615                        }
616                    }
617                    _ => false,
618                }
619            }
620        }
621    }
622
623    /// Return the names of all attributes on this dataset (read mode only).
624    pub fn attr_names(&self) -> Result<Vec<String>> {
625        match &self.info {
626            DatasetInfo::Reader { name, .. } => {
627                let inner = borrow_inner(&self.file_inner);
628                match &*inner {
629                    H5FileInner::Reader(reader) => Ok(reader.dataset_attr_names(name)?),
630                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
631                }
632            }
633            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
634                "attr_names not available in write mode".into(),
635            )),
636        }
637    }
638
639    /// Open an attribute by name (read mode only).
640    pub fn attr(&self, attr_name: &str) -> Result<crate::attribute::H5Attribute> {
641        match &self.info {
642            DatasetInfo::Reader { name, .. } => {
643                let inner = borrow_inner(&self.file_inner);
644                match &*inner {
645                    H5FileInner::Reader(reader) => {
646                        let attr_msg = reader.dataset_attr(name, attr_name)?.clone();
647                        Ok(crate::attribute::H5Attribute::new_reader(
648                            clone_inner(&self.file_inner),
649                            attr_msg,
650                        ))
651                    }
652                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
653                }
654            }
655            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
656                "attr() not available in write mode".into(),
657            )),
658        }
659    }
660
661    /// Start building a new attribute on this dataset.
662    ///
663    /// Returns a fluent builder. Call `.shape(())` for a scalar attribute
664    /// and `.create("name")` to finalize.
665    ///
666    /// # Example
667    ///
668    /// ```no_run
669    /// # use rust_hdf5::H5File;
670    /// # use rust_hdf5::types::VarLenUnicode;
671    /// let file = H5File::create("attr.h5").unwrap();
672    /// let ds = file.new_dataset::<f32>().shape(&[10]).create("data").unwrap();
673    /// let attr = ds.new_attr::<VarLenUnicode>().shape(()).create("units").unwrap();
674    /// attr.write_scalar(&VarLenUnicode("meters".to_string())).unwrap();
675    /// ```
676    pub fn new_attr<T: 'static>(&self) -> AttrBuilder<'_, T> {
677        let ds_index = match &self.info {
678            DatasetInfo::Writer { index, .. } => *index,
679            DatasetInfo::Reader { .. } => {
680                // Reader mode: we'll return a builder that will error on create.
681                // Using usize::MAX as sentinel.
682                usize::MAX
683            }
684        };
685        AttrBuilder::new(&self.file_inner, ds_index)
686    }
687
688    /// Write a typed slice to the dataset (contiguous datasets only).
689    ///
690    /// The slice length must match the total number of elements declared by
691    /// the dataset shape. The data is reinterpreted as raw bytes and written
692    /// to the file.
693    ///
694    /// # Errors
695    ///
696    /// Returns an error if:
697    /// - The file is in read mode.
698    /// - The data length does not match the declared shape.
699    pub fn write_raw<T: H5Type>(&self, data: &[T]) -> Result<()> {
700        match &self.info {
701            DatasetInfo::Writer {
702                index,
703                shape,
704                element_size,
705                chunked,
706                btree2: _,
707                fixed_array: _,
708            } => {
709                if *chunked {
710                    return Err(Hdf5Error::InvalidState(
711                        "use write_chunk for chunked datasets".into(),
712                    ));
713                }
714
715                let total_elements: usize = shape.iter().product();
716                if data.len() != total_elements {
717                    return Err(Hdf5Error::InvalidState(format!(
718                        "data length {} does not match dataset size {}",
719                        data.len(),
720                        total_elements,
721                    )));
722                }
723
724                // Verify element size matches
725                if T::element_size() != *element_size {
726                    return Err(Hdf5Error::TypeMismatch(format!(
727                        "write type has element size {} but dataset expects {}",
728                        T::element_size(),
729                        element_size,
730                    )));
731                }
732
733                // Safety: T: Copy + 'static (numeric primitive) with well-defined
734                // byte representation. The resulting slice borrows `data` and
735                // lives only as long as this block.
736                let byte_len = data.len() * T::element_size();
737                let raw =
738                    unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
739
740                let mut inner = borrow_inner_mut(&self.file_inner);
741                match &mut *inner {
742                    H5FileInner::Writer(writer) => {
743                        writer.write_dataset_raw(*index, raw)?;
744                        Ok(())
745                    }
746                    _ => Err(Hdf5Error::InvalidState(
747                        "file is no longer in write mode".into(),
748                    )),
749                }
750            }
751            DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
752                "cannot write to a dataset opened in read mode".into(),
753            )),
754        }
755    }
756
757    /// Write a single chunk to a chunked dataset.
758    ///
759    /// `chunk_idx` is the linear chunk index (typically the frame number for
760    /// streaming datasets). `data` is the raw byte data for one chunk.
761    ///
762    /// For datasets with two or more unlimited dimensions (v2 B-tree index),
763    /// use [`write_chunk_at`](Self::write_chunk_at) instead.
764    pub fn write_chunk(&self, chunk_idx: usize, data: &[u8]) -> Result<()> {
765        match &self.info {
766            DatasetInfo::Writer {
767                index,
768                chunked,
769                btree2,
770                fixed_array,
771                ..
772            } => {
773                if !*chunked {
774                    return Err(Hdf5Error::InvalidState(
775                        "write_chunk is only for chunked datasets".into(),
776                    ));
777                }
778                if *btree2 {
779                    return Err(Hdf5Error::InvalidState(
780                        "this dataset uses a v2 B-tree chunk index; use write_chunk_at \
781                         with the chunk's grid coordinates"
782                            .into(),
783                    ));
784                }
785
786                let mut inner = borrow_inner_mut(&self.file_inner);
787                match &mut *inner {
788                    H5FileInner::Writer(writer) => {
789                        if *fixed_array {
790                            // Fixed-array dataset: convert the linear chunk
791                            // index into row-major grid coordinates.
792                            let chunk_dims = writer
793                                .dataset_chunk_dims(*index)
794                                .ok_or_else(|| {
795                                    Hdf5Error::InvalidState("dataset has no chunk info".into())
796                                })?
797                                .to_vec();
798                            let dims = writer.dataset_dims(*index).to_vec();
799                            let mut grid = vec![0u64; dims.len()];
800                            for d in 0..dims.len() {
801                                grid[d] = if chunk_dims[d] > 0 {
802                                    dims[d].div_ceil(chunk_dims[d])
803                                } else {
804                                    1
805                                };
806                            }
807                            // A zero-extent dimension yields a grid of 0
808                            // chunks — there is no chunk to write.
809                            if grid.contains(&0) {
810                                return Err(Hdf5Error::InvalidState(
811                                    "dataset has a zero-extent dimension and no chunks".into(),
812                                ));
813                            }
814                            let mut rem = chunk_idx as u64;
815                            let mut coords = vec![0u64; dims.len()];
816                            for d in (0..dims.len()).rev() {
817                                coords[d] = rem % grid[d];
818                                rem /= grid[d];
819                            }
820                            // A leftover means chunk_idx exceeded the grid.
821                            if rem != 0 {
822                                return Err(Hdf5Error::InvalidState(format!(
823                                    "chunk index {chunk_idx} is out of range for this dataset"
824                                )));
825                            }
826                            writer.write_chunk_fixed_array(*index, &coords, data)?;
827                        } else {
828                            writer.write_chunk(*index, chunk_idx as u64, data)?;
829                        }
830                        Ok(())
831                    }
832                    _ => Err(Hdf5Error::InvalidState(
833                        "file is no longer in write mode".into(),
834                    )),
835                }
836            }
837            DatasetInfo::Reader { .. } => {
838                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
839            }
840        }
841    }
842
843    /// Write an already-filtered (pre-compressed) chunk **verbatim**, recording
844    /// the caller-supplied `filter_mask`. The bytes are stored as-is without
845    /// running the dataset's filter pipeline — the HDF5 "direct chunk write"
846    /// (`H5Dwrite_chunk`, formerly `H5DOwrite_chunk`) operation.
847    ///
848    /// `chunk_idx` is the linear chunk index (the frame number for streaming
849    /// datasets), exactly as for [`write_chunk`](Self::write_chunk). `data` is
850    /// the already-filtered bytes of one chunk — its length is the *stored*
851    /// (compressed) size, not the uncompressed chunk size.
852    ///
853    /// `filter_mask` is a bitfield: bit *i* set means filter *i* of the
854    /// dataset's pipeline was **not** applied to this chunk and must be skipped
855    /// on read. Pass 0 when the full pipeline was already applied upstream (the
856    /// common case: a codec plugin handed you compressed frames).
857    ///
858    /// The dataset must be chunked **and** filtered; an unfiltered chunk index
859    /// has no slot to record a stored size or mask. For a v2-B-tree-indexed
860    /// dataset (two or more unlimited dimensions) direct chunk writes are not
861    /// supported.
862    ///
863    /// # Reading back
864    ///
865    /// Both this crate's reader and libhdf5/h5py honor the per-chunk
866    /// `filter_mask`: a chunk written with any mask round-trips correctly, with
867    /// the reader skipping exactly the filters the mask marks as not applied.
868    pub fn write_chunk_raw(&self, chunk_idx: usize, data: &[u8], filter_mask: u32) -> Result<()> {
869        match &self.info {
870            DatasetInfo::Writer {
871                index,
872                chunked,
873                btree2,
874                fixed_array,
875                ..
876            } => {
877                if !*chunked {
878                    return Err(Hdf5Error::InvalidState(
879                        "write_chunk_raw is only for chunked datasets".into(),
880                    ));
881                }
882                if *btree2 {
883                    return Err(Hdf5Error::InvalidState(
884                        "direct chunk writes are not supported for v2-B-tree-indexed \
885                         datasets (two or more unlimited dimensions)"
886                            .into(),
887                    ));
888                }
889
890                let mut inner = borrow_inner_mut(&self.file_inner);
891                match &mut *inner {
892                    H5FileInner::Writer(writer) => {
893                        if *fixed_array {
894                            // Fixed-array dataset: convert the linear chunk
895                            // index into row-major grid coordinates.
896                            let chunk_dims = writer
897                                .dataset_chunk_dims(*index)
898                                .ok_or_else(|| {
899                                    Hdf5Error::InvalidState("dataset has no chunk info".into())
900                                })?
901                                .to_vec();
902                            let dims = writer.dataset_dims(*index).to_vec();
903                            let mut grid = vec![0u64; dims.len()];
904                            for d in 0..dims.len() {
905                                grid[d] = if chunk_dims[d] > 0 {
906                                    dims[d].div_ceil(chunk_dims[d])
907                                } else {
908                                    1
909                                };
910                            }
911                            // A zero-extent dimension yields a grid of 0
912                            // chunks — there is no chunk to write.
913                            if grid.contains(&0) {
914                                return Err(Hdf5Error::InvalidState(
915                                    "dataset has a zero-extent dimension and no chunks".into(),
916                                ));
917                            }
918                            let mut rem = chunk_idx as u64;
919                            let mut coords = vec![0u64; dims.len()];
920                            for d in (0..dims.len()).rev() {
921                                coords[d] = rem % grid[d];
922                                rem /= grid[d];
923                            }
924                            // A leftover means chunk_idx exceeded the grid.
925                            if rem != 0 {
926                                return Err(Hdf5Error::InvalidState(format!(
927                                    "chunk index {chunk_idx} is out of range for this dataset"
928                                )));
929                            }
930                            writer.write_compressed_chunk_fixed_array(
931                                *index,
932                                &coords,
933                                data,
934                                filter_mask,
935                            )?;
936                        } else {
937                            writer.write_compressed_chunk(
938                                *index,
939                                chunk_idx as u64,
940                                data,
941                                filter_mask,
942                            )?;
943                        }
944                        Ok(())
945                    }
946                    _ => Err(Hdf5Error::InvalidState(
947                        "file is no longer in write mode".into(),
948                    )),
949                }
950            }
951            DatasetInfo::Reader { .. } => {
952                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
953            }
954        }
955    }
956
957    /// Write a single chunk to a v2-B-tree-indexed dataset, addressed by its
958    /// chunk-grid coordinates (one per dimension).
959    ///
960    /// This is the entry point for datasets with two or more unlimited
961    /// dimensions. The dataset's logical dimensions are extended to cover
962    /// the written chunk. `data` is the raw bytes of one full chunk.
963    ///
964    /// ```no_run
965    /// # use rust_hdf5::H5File;
966    /// let file = H5File::create("bt2.h5").unwrap();
967    /// let ds = file.new_dataset::<i32>()
968    ///     .shape(&[0, 0])
969    ///     .chunk(&[2, 2])
970    ///     .max_shape(&[None, None])
971    ///     .create("grid")
972    ///     .unwrap();
973    /// let chunk = [0i32, 1, 2, 3];
974    /// let bytes: Vec<u8> = chunk.iter().flat_map(|v| v.to_le_bytes()).collect();
975    /// ds.write_chunk_at(&[0, 0], &bytes).unwrap();
976    /// ```
977    pub fn write_chunk_at(&self, chunk_coords: &[usize], data: &[u8]) -> Result<()> {
978        match &self.info {
979            DatasetInfo::Writer {
980                index,
981                chunked,
982                btree2,
983                fixed_array,
984                ..
985            } => {
986                if !*chunked {
987                    return Err(Hdf5Error::InvalidState(
988                        "write_chunk_at is only for chunked datasets".into(),
989                    ));
990                }
991                let coords: Vec<u64> = chunk_coords.iter().map(|&c| c as u64).collect();
992                let btree2 = *btree2;
993                let fixed_array = *fixed_array;
994                let mut inner = borrow_inner_mut(&self.file_inner);
995                let writer = match &mut *inner {
996                    H5FileInner::Writer(w) => w,
997                    _ => {
998                        return Err(Hdf5Error::InvalidState(
999                            "file is no longer in write mode".into(),
1000                        ))
1001                    }
1002                };
1003                let chunk_dims = writer
1004                    .dataset_chunk_dims(*index)
1005                    .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
1006                    .to_vec();
1007                let dims = writer.dataset_dims(*index).to_vec();
1008                if coords.len() != dims.len() {
1009                    return Err(Hdf5Error::InvalidState(format!(
1010                        "chunk_coords has {} entries but the dataset has {} dimensions",
1011                        coords.len(),
1012                        dims.len()
1013                    )));
1014                }
1015                if chunk_dims.len() != dims.len() {
1016                    return Err(Hdf5Error::InvalidState(format!(
1017                        "dataset chunk shape has {} dimensions but the dataspace has {}",
1018                        chunk_dims.len(),
1019                        dims.len()
1020                    )));
1021                }
1022
1023                // Validate coordinates and compute the grown dimensions
1024                // up-front, before any chunk is written, so an overflowing
1025                // coordinate cannot leave an orphaned chunk in the file.
1026                let mut new_dims = dims.clone();
1027                for d in 0..dims.len() {
1028                    let needed = coords[d]
1029                        .checked_add(1)
1030                        .and_then(|c| c.checked_mul(chunk_dims[d]))
1031                        .ok_or_else(|| {
1032                            Hdf5Error::InvalidState(format!(
1033                                "chunk coordinate {} in dimension {} is too large",
1034                                coords[d], d
1035                            ))
1036                        })?;
1037                    if needed > new_dims[d] {
1038                        new_dims[d] = needed;
1039                    }
1040                }
1041
1042                if fixed_array {
1043                    // Fixed-array (fixed-shape) dataset: no dimension growth.
1044                    writer.write_chunk_fixed_array(*index, &coords, data)?;
1045                    return Ok(());
1046                }
1047
1048                if btree2 {
1049                    writer.write_chunk_btree_v2(*index, &coords, data)?;
1050                } else {
1051                    // Extensible array: linearize the chunk-grid coordinates
1052                    // (row-major) into the array's chunk index.
1053                    let mut linear = 0u64;
1054                    for d in 0..dims.len() {
1055                        let grid = if chunk_dims[d] > 0 {
1056                            dims[d].div_ceil(chunk_dims[d])
1057                        } else {
1058                            1
1059                        };
1060                        linear = linear
1061                            .checked_mul(grid)
1062                            .and_then(|l| l.checked_add(coords[d]))
1063                            .ok_or_else(|| {
1064                                Hdf5Error::InvalidState(
1065                                    "chunk coordinates overflow the array index".into(),
1066                                )
1067                            })?;
1068                    }
1069                    writer.write_chunk(*index, linear, data)?;
1070                }
1071
1072                if new_dims != dims {
1073                    writer.extend_dataset(*index, &new_dims)?;
1074                }
1075                Ok(())
1076            }
1077            DatasetInfo::Reader { .. } => {
1078                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1079            }
1080        }
1081    }
1082
1083    /// Write multiple chunks in a batch, optionally compressing in parallel.
1084    ///
1085    /// `chunks` is a slice of `(chunk_index, raw_data)` pairs. When a filter
1086    /// pipeline is configured and the `parallel` feature is enabled, all
1087    /// chunks are compressed concurrently via rayon.
1088    pub fn write_chunks_batch(&self, chunks: &[(usize, &[u8])]) -> Result<()> {
1089        match &self.info {
1090            DatasetInfo::Writer { index, chunked, .. } => {
1091                if !*chunked {
1092                    return Err(Hdf5Error::InvalidState(
1093                        "write_chunks_batch is only for chunked datasets".into(),
1094                    ));
1095                }
1096                let pairs: Vec<(u64, &[u8])> = chunks
1097                    .iter()
1098                    .map(|(idx, data)| (*idx as u64, *data))
1099                    .collect();
1100                let mut inner = borrow_inner_mut(&self.file_inner);
1101                match &mut *inner {
1102                    H5FileInner::Writer(writer) => {
1103                        writer.write_chunks_batch(*index, &pairs)?;
1104                        Ok(())
1105                    }
1106                    _ => Err(Hdf5Error::InvalidState(
1107                        "file is no longer in write mode".into(),
1108                    )),
1109                }
1110            }
1111            DatasetInfo::Reader { .. } => {
1112                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1113            }
1114        }
1115    }
1116
1117    /// Append data along the first dimension of a chunked dataset.
1118    ///
1119    /// `data` must contain a whole number of "frames" — slices along
1120    /// dimension 0. For example, if the dataset has shape `[N, H, W]`
1121    /// and `chunk_dims = [1, H, W]`, then `data.len()` must be a
1122    /// multiple of `H * W`.
1123    ///
1124    /// This method writes the necessary chunks and extends the dataset
1125    /// shape automatically.
1126    ///
1127    /// ```no_run
1128    /// # use rust_hdf5::H5File;
1129    /// let file = H5File::create("append.h5").unwrap();
1130    /// let ds = file.new_dataset::<f64>()
1131    ///     .shape(&[0, 3])
1132    ///     .chunk(&[1, 3])
1133    ///     .max_shape(&[None, Some(3)])
1134    ///     .create("data")
1135    ///     .unwrap();
1136    /// ds.append(&[1.0, 2.0, 3.0]).unwrap();       // shape becomes [1, 3]
1137    /// ds.append(&[4.0, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap(); // shape becomes [3, 3]
1138    /// ```
1139    pub fn append<T: H5Type>(&self, data: &[T]) -> Result<()> {
1140        match &self.info {
1141            DatasetInfo::Writer {
1142                index,
1143                element_size,
1144                chunked,
1145                ..
1146            } => {
1147                if !*chunked {
1148                    return Err(Hdf5Error::InvalidState(
1149                        "append is only for chunked datasets".into(),
1150                    ));
1151                }
1152                if T::element_size() != *element_size {
1153                    return Err(Hdf5Error::TypeMismatch(format!(
1154                        "append type has element size {} but dataset expects {}",
1155                        T::element_size(),
1156                        element_size,
1157                    )));
1158                }
1159
1160                let ds_index = *index;
1161                let es = *element_size;
1162
1163                let mut inner = borrow_inner_mut(&self.file_inner);
1164                let writer = match &mut *inner {
1165                    H5FileInner::Writer(w) => w,
1166                    _ => {
1167                        return Err(Hdf5Error::InvalidState(
1168                            "file is no longer in write mode".into(),
1169                        ))
1170                    }
1171                };
1172
1173                let chunk_dims = writer
1174                    .dataset_chunk_dims(ds_index)
1175                    .ok_or_else(|| Hdf5Error::InvalidState("dataset has no chunk info".into()))?
1176                    .to_vec();
1177                let dims = writer.dataset_dims(ds_index).to_vec();
1178
1179                // Frame size = product of dims[1..]
1180                let frame_elems: usize = if dims.len() > 1 {
1181                    dims[1..].iter().map(|&d| d as usize).product()
1182                } else {
1183                    1
1184                };
1185
1186                if frame_elems == 0 {
1187                    return Err(Hdf5Error::InvalidState(
1188                        "cannot append to dataset with zero-size trailing dimensions".into(),
1189                    ));
1190                }
1191
1192                if !data.len().is_multiple_of(frame_elems) {
1193                    return Err(Hdf5Error::InvalidState(format!(
1194                        "data length {} is not a multiple of frame size {}",
1195                        data.len(),
1196                        frame_elems,
1197                    )));
1198                }
1199
1200                let n_new_frames = data.len() / frame_elems;
1201                let current_dim0 = dims[0] as usize;
1202
1203                // Chunk size along first dimension
1204                let chunk_dim0 = chunk_dims[0] as usize;
1205                let frame_bytes = frame_elems * es;
1206
1207                let raw = unsafe {
1208                    std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * es)
1209                };
1210
1211                // Merge buffered data with new data
1212                let ds = &mut writer.datasets[ds_index];
1213                let buffered_frames = ds.append_buffered_frames as usize;
1214                let mut combined = std::mem::take(&mut ds.append_buffer);
1215                combined.extend_from_slice(raw);
1216                ds.append_buffered_frames = 0;
1217
1218                let total_frames = buffered_frames + n_new_frames;
1219                let total_bytes = combined.len();
1220
1221                // Base chunk index: account for buffered frames
1222                let base_dim0 = current_dim0 - buffered_frames;
1223                let mut byte_pos = 0usize;
1224                let mut frame_pos = 0usize;
1225
1226                while frame_pos < total_frames {
1227                    let abs_frame = base_dim0 + frame_pos;
1228                    let chunk_idx = abs_frame / chunk_dim0;
1229                    let remaining_frames = total_frames - frame_pos;
1230                    let frames_to_fill = chunk_dim0 - (abs_frame % chunk_dim0);
1231
1232                    if remaining_frames >= frames_to_fill {
1233                        // Full chunk — write
1234                        let end = byte_pos + frames_to_fill * frame_bytes;
1235                        if frames_to_fill == chunk_dim0 {
1236                            writer.write_chunk(
1237                                ds_index,
1238                                chunk_idx as u64,
1239                                &combined[byte_pos..end],
1240                            )?;
1241                        } else {
1242                            // Partial-chunk write: this branch only runs with
1243                            // offset_in_chunk > 0, meaning the chunk already
1244                            // holds earlier frames on disk. Read-modify-write
1245                            // so those frames survive — a fresh fill buffer
1246                            // would erase them.
1247                            let offset_in_chunk = (abs_frame % chunk_dim0) * frame_bytes;
1248                            let mut chunk_buf =
1249                                match writer.read_chunk_if_present(ds_index, chunk_idx as u64)? {
1250                                    Some(existing) => existing,
1251                                    None => {
1252                                        return Err(Hdf5Error::InvalidState(format!(
1253                                            "cannot append into partially-written chunk {}: \
1254                                         its existing content was not found in the chunk \
1255                                         index (the file may be inconsistent)",
1256                                            chunk_idx
1257                                        )));
1258                                    }
1259                                };
1260                            chunk_buf
1261                                [offset_in_chunk..offset_in_chunk + frames_to_fill * frame_bytes]
1262                                .copy_from_slice(&combined[byte_pos..end]);
1263                            writer.write_chunk(ds_index, chunk_idx as u64, &chunk_buf)?;
1264                        }
1265                        byte_pos = end;
1266                        frame_pos += frames_to_fill;
1267                    } else {
1268                        // Partial chunk — buffer for next append
1269                        let ds = &mut writer.datasets[ds_index];
1270                        ds.append_buffer = combined[byte_pos..total_bytes].to_vec();
1271                        ds.append_buffered_frames = remaining_frames as u64;
1272                        frame_pos = total_frames;
1273                    }
1274                }
1275
1276                // Extend dims to include all frames (buffered + new)
1277                let logical_dim0 = base_dim0 + total_frames;
1278                let mut new_dims: Vec<u64> = dims;
1279                new_dims[0] = logical_dim0 as u64;
1280                writer.extend_dataset(ds_index, &new_dims)?;
1281
1282                Ok(())
1283            }
1284            DatasetInfo::Reader { .. } => {
1285                Err(Hdf5Error::InvalidState("cannot append in read mode".into()))
1286            }
1287        }
1288    }
1289
1290    /// Extend the dimensions of a chunked dataset.
1291    pub fn extend(&self, new_dims: &[usize]) -> Result<()> {
1292        match &self.info {
1293            DatasetInfo::Writer { index, chunked, .. } => {
1294                if !*chunked {
1295                    return Err(Hdf5Error::InvalidState(
1296                        "extend is only for chunked datasets".into(),
1297                    ));
1298                }
1299
1300                let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
1301                let mut inner = borrow_inner_mut(&self.file_inner);
1302                match &mut *inner {
1303                    H5FileInner::Writer(writer) => {
1304                        writer.extend_dataset(*index, &dims_u64)?;
1305                        Ok(())
1306                    }
1307                    _ => Err(Hdf5Error::InvalidState(
1308                        "file is no longer in write mode".into(),
1309                    )),
1310                }
1311            }
1312            DatasetInfo::Reader { .. } => {
1313                Err(Hdf5Error::InvalidState("cannot extend in read mode".into()))
1314            }
1315        }
1316    }
1317
1318    /// Set the logical extent of a chunked dataset, growing **or
1319    /// shrinking** any dimension.
1320    ///
1321    /// Unlike [`extend`](Self::extend), which only grows, this can reduce a
1322    /// dimension — for example to correct an over-extended frame count
1323    /// after writing a partial multi-frame chunk. Shrinking changes the
1324    /// logical dataspace only: data in chunks beyond the new extent stays
1325    /// in the file but is no longer visible on read, exactly as libhdf5's
1326    /// `H5Dset_extent` behaves. The new extent must not exceed the
1327    /// dataset's maximum dimensions.
1328    pub fn set_extent(&self, new_dims: &[usize]) -> Result<()> {
1329        match &self.info {
1330            DatasetInfo::Writer { index, .. } => {
1331                let dims_u64: Vec<u64> = new_dims.iter().map(|&d| d as u64).collect();
1332                let mut inner = borrow_inner_mut(&self.file_inner);
1333                match &mut *inner {
1334                    H5FileInner::Writer(writer) => {
1335                        writer.set_dataset_extent(*index, &dims_u64)?;
1336                        Ok(())
1337                    }
1338                    _ => Err(Hdf5Error::InvalidState(
1339                        "file is no longer in write mode".into(),
1340                    )),
1341                }
1342            }
1343            DatasetInfo::Reader { .. } => Err(Hdf5Error::InvalidState(
1344                "cannot set extent in read mode".into(),
1345            )),
1346        }
1347    }
1348
1349    /// Flush a chunked dataset's index structures to disk.
1350    pub fn flush(&self) -> Result<()> {
1351        match &self.info {
1352            DatasetInfo::Writer { index, .. } => {
1353                let mut inner = borrow_inner_mut(&self.file_inner);
1354                match &mut *inner {
1355                    H5FileInner::Writer(writer) => {
1356                        writer.flush_dataset(*index)?;
1357                        Ok(())
1358                    }
1359                    _ => Ok(()),
1360                }
1361            }
1362            DatasetInfo::Reader { .. } => Ok(()),
1363        }
1364    }
1365
1366    /// Read a slice (hyperslab) of the dataset as a typed vector.
1367    ///
1368    /// `starts` and `counts` define the N-dimensional selection:
1369    /// `starts[d]` = first index along dim d, `counts[d]` = how many elements.
1370    pub fn read_slice<T: H5Type>(&self, starts: &[usize], counts: &[usize]) -> Result<Vec<T>> {
1371        match &self.info {
1372            DatasetInfo::Reader {
1373                name, element_size, ..
1374            } => {
1375                if T::element_size() != *element_size {
1376                    return Err(Hdf5Error::TypeMismatch(format!(
1377                        "read type has element size {} but dataset has element size {}",
1378                        T::element_size(),
1379                        element_size,
1380                    )));
1381                }
1382                let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
1383                let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
1384
1385                let raw = {
1386                    let mut inner = borrow_inner_mut(&self.file_inner);
1387                    match &mut *inner {
1388                        H5FileInner::Reader(reader) => {
1389                            reader.read_slice(name, &starts_u64, &counts_u64)?
1390                        }
1391                        _ => {
1392                            return Err(Hdf5Error::InvalidState("file is not in read mode".into()))
1393                        }
1394                    }
1395                };
1396
1397                if raw.len() % T::element_size() != 0 {
1398                    return Err(Hdf5Error::TypeMismatch(format!(
1399                        "raw data size {} is not a multiple of element size {}",
1400                        raw.len(),
1401                        T::element_size(),
1402                    )));
1403                }
1404
1405                let count = raw.len() / T::element_size();
1406                let mut result = Vec::<T>::with_capacity(count);
1407                unsafe {
1408                    std::ptr::copy_nonoverlapping(
1409                        raw.as_ptr(),
1410                        result.as_mut_ptr() as *mut u8,
1411                        raw.len(),
1412                    );
1413                    result.set_len(count);
1414                }
1415                Ok(result)
1416            }
1417            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1418                "cannot read_slice from a dataset in write mode".into(),
1419            )),
1420        }
1421    }
1422
1423    /// Write a typed slice to a sub-region of a contiguous dataset.
1424    ///
1425    /// `starts` and `counts` define the N-dimensional selection.
1426    pub fn write_slice<T: H5Type>(
1427        &self,
1428        starts: &[usize],
1429        counts: &[usize],
1430        data: &[T],
1431    ) -> Result<()> {
1432        match &self.info {
1433            DatasetInfo::Writer {
1434                index,
1435                element_size,
1436                chunked,
1437                ..
1438            } => {
1439                if *chunked {
1440                    return Err(Hdf5Error::InvalidState(
1441                        "write_slice is only for contiguous datasets".into(),
1442                    ));
1443                }
1444                if T::element_size() != *element_size {
1445                    return Err(Hdf5Error::TypeMismatch(format!(
1446                        "write type has element size {} but dataset expects {}",
1447                        T::element_size(),
1448                        element_size,
1449                    )));
1450                }
1451
1452                let expected: usize = counts.iter().product();
1453                if data.len() != expected {
1454                    return Err(Hdf5Error::InvalidState(format!(
1455                        "data length {} does not match slice size {}",
1456                        data.len(),
1457                        expected,
1458                    )));
1459                }
1460
1461                let starts_u64: Vec<u64> = starts.iter().map(|&s| s as u64).collect();
1462                let counts_u64: Vec<u64> = counts.iter().map(|&c| c as u64).collect();
1463
1464                let byte_len = data.len() * T::element_size();
1465                let raw =
1466                    unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) };
1467
1468                let mut inner = borrow_inner_mut(&self.file_inner);
1469                match &mut *inner {
1470                    H5FileInner::Writer(writer) => {
1471                        writer.write_slice(*index, &starts_u64, &counts_u64, raw)?;
1472                        Ok(())
1473                    }
1474                    _ => Err(Hdf5Error::InvalidState(
1475                        "file is no longer in write mode".into(),
1476                    )),
1477                }
1478            }
1479            DatasetInfo::Reader { .. } => {
1480                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
1481            }
1482        }
1483    }
1484
1485    /// Read variable-length strings from a dataset.
1486    ///
1487    /// This handles h5py-style vlen string datasets that store strings
1488    /// as global heap references. Returns one String per element.
1489    pub fn read_vlen_strings(&self) -> Result<Vec<String>> {
1490        match &self.info {
1491            DatasetInfo::Reader { name, .. } => {
1492                let mut inner = borrow_inner_mut(&self.file_inner);
1493                match &mut *inner {
1494                    H5FileInner::Reader(reader) => Ok(reader.read_vlen_strings(name)?),
1495                    _ => Err(Hdf5Error::InvalidState("file is not in read mode".into())),
1496                }
1497            }
1498            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1499                "cannot read vlen strings from a dataset in write mode".into(),
1500            )),
1501        }
1502    }
1503
1504    /// Read the entire dataset as a typed vector.
1505    ///
1506    /// The raw bytes are read from the file and reinterpreted as `T`. The
1507    /// caller must ensure that `T` matches the datatype used when the dataset
1508    /// was written.
1509    ///
1510    /// # Errors
1511    ///
1512    /// Returns an error if:
1513    /// - The file is in write mode.
1514    /// - The raw data size is not a multiple of `T::element_size()`.
1515    pub fn read_raw<T: H5Type>(&self) -> Result<Vec<T>> {
1516        match &self.info {
1517            DatasetInfo::Reader {
1518                name, element_size, ..
1519            } => {
1520                if T::element_size() != *element_size {
1521                    return Err(Hdf5Error::TypeMismatch(format!(
1522                        "read type has element size {} but dataset has element size {}",
1523                        T::element_size(),
1524                        element_size,
1525                    )));
1526                }
1527
1528                let raw = {
1529                    let mut inner = borrow_inner_mut(&self.file_inner);
1530                    match &mut *inner {
1531                        H5FileInner::Reader(reader) => reader.read_dataset_raw(name)?,
1532                        _ => {
1533                            return Err(Hdf5Error::InvalidState("file is not in read mode".into()));
1534                        }
1535                    }
1536                };
1537
1538                if raw.len() % T::element_size() != 0 {
1539                    return Err(Hdf5Error::TypeMismatch(format!(
1540                        "raw data size {} is not a multiple of element size {}",
1541                        raw.len(),
1542                        T::element_size(),
1543                    )));
1544                }
1545
1546                let count = raw.len() / T::element_size();
1547                let mut result = Vec::<T>::with_capacity(count);
1548
1549                // Safety: T is Copy + 'static (required by H5Type). We verified
1550                // the byte count matches count * size_of::<T>() above.
1551                // copy_nonoverlapping fills the memory with valid bit patterns
1552                // for all H5Type implementors (numeric primitives).
1553                // We call set_len AFTER the copy so that if an unexpected panic
1554                // occurs, uninitialized memory is never exposed.
1555                unsafe {
1556                    std::ptr::copy_nonoverlapping(
1557                        raw.as_ptr(),
1558                        result.as_mut_ptr() as *mut u8,
1559                        raw.len(),
1560                    );
1561                    result.set_len(count);
1562                }
1563
1564                Ok(result)
1565            }
1566            DatasetInfo::Writer { .. } => Err(Hdf5Error::InvalidState(
1567                "cannot read from a dataset in write mode".into(),
1568            )),
1569        }
1570    }
1571}
1572
1573#[cfg(test)]
1574mod tests {
1575    use crate::H5File;
1576    use std::path::PathBuf;
1577
1578    fn temp_path(name: &str) -> PathBuf {
1579        // Include PID + a per-call atomic counter so that concurrent
1580        // cargo invocations and any kernel-level "lock not yet
1581        // released" races between sequential opens cannot collide.
1582        use std::sync::atomic::{AtomicU64, Ordering};
1583        static COUNTER: AtomicU64 = AtomicU64::new(0);
1584        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1585        std::env::temp_dir().join(format!(
1586            "hdf5_dataset_test_{}_{}_{}.h5",
1587            name,
1588            std::process::id(),
1589            n
1590        ))
1591    }
1592
1593    #[test]
1594    fn builder_requires_shape() {
1595        let path = temp_path("no_shape");
1596        let file = H5File::create(&path).unwrap();
1597        let result = file.new_dataset::<u8>().create("data");
1598        assert!(result.is_err());
1599        std::fs::remove_file(&path).ok();
1600    }
1601
1602    #[test]
1603    fn write_raw_size_mismatch() {
1604        let path = temp_path("size_mismatch");
1605        let file = H5File::create(&path).unwrap();
1606        let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
1607        // Provide 3 elements instead of 4
1608        let result = ds.write_raw(&[1u8, 2, 3]);
1609        assert!(result.is_err());
1610        std::fs::remove_file(&path).ok();
1611    }
1612
1613    #[test]
1614    fn roundtrip_u8_1d() {
1615        let path = temp_path("rt_u8_1d");
1616        let data: Vec<u8> = (0..10).collect();
1617
1618        {
1619            let file = H5File::create(&path).unwrap();
1620            let ds = file.new_dataset::<u8>().shape([10]).create("seq").unwrap();
1621            ds.write_raw(&data).unwrap();
1622            file.close().unwrap();
1623        }
1624
1625        {
1626            let file = H5File::open(&path).unwrap();
1627            let ds = file.dataset("seq").unwrap();
1628            assert_eq!(ds.shape(), vec![10]);
1629            let readback = ds.read_raw::<u8>().unwrap();
1630            assert_eq!(readback, data);
1631        }
1632
1633        std::fs::remove_file(&path).ok();
1634    }
1635
1636    #[test]
1637    fn roundtrip_i32_2d() {
1638        let path = temp_path("rt_i32_2d");
1639        let data: Vec<i32> = vec![-1, 0, 1, 2, 3, 4];
1640
1641        {
1642            let file = H5File::create(&path).unwrap();
1643            let ds = file
1644                .new_dataset::<i32>()
1645                .shape([2, 3])
1646                .create("matrix")
1647                .unwrap();
1648            ds.write_raw(&data).unwrap();
1649            file.close().unwrap();
1650        }
1651
1652        {
1653            let file = H5File::open(&path).unwrap();
1654            let ds = file.dataset("matrix").unwrap();
1655            assert_eq!(ds.shape(), vec![2, 3]);
1656            let readback = ds.read_raw::<i32>().unwrap();
1657            assert_eq!(readback, data);
1658        }
1659
1660        std::fs::remove_file(&path).ok();
1661    }
1662
1663    #[test]
1664    fn roundtrip_f64_3d() {
1665        let path = temp_path("rt_f64_3d");
1666        let data: Vec<f64> = (0..24).map(|i| i as f64 * 0.5).collect();
1667
1668        {
1669            let file = H5File::create(&path).unwrap();
1670            let ds = file
1671                .new_dataset::<f64>()
1672                .shape([2, 3, 4])
1673                .create("cube")
1674                .unwrap();
1675            ds.write_raw(&data).unwrap();
1676            file.close().unwrap();
1677        }
1678
1679        {
1680            let file = H5File::open(&path).unwrap();
1681            let ds = file.dataset("cube").unwrap();
1682            assert_eq!(ds.shape(), vec![2, 3, 4]);
1683            let readback = ds.read_raw::<f64>().unwrap();
1684            assert_eq!(readback, data);
1685        }
1686
1687        std::fs::remove_file(&path).ok();
1688    }
1689
1690    #[test]
1691    fn cannot_read_in_write_mode() {
1692        let path = temp_path("no_read_write");
1693        let file = H5File::create(&path).unwrap();
1694        let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
1695        ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1696        let result = ds.read_raw::<u8>();
1697        assert!(result.is_err());
1698        std::fs::remove_file(&path).ok();
1699    }
1700
1701    #[test]
1702    fn cannot_write_in_read_mode() {
1703        let path = temp_path("no_write_read");
1704
1705        {
1706            let file = H5File::create(&path).unwrap();
1707            let ds = file.new_dataset::<u8>().shape([4]).create("x").unwrap();
1708            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1709            file.close().unwrap();
1710        }
1711
1712        {
1713            let file = H5File::open(&path).unwrap();
1714            let ds = file.dataset("x").unwrap();
1715            let result = ds.write_raw(&[5u8, 6, 7, 8]);
1716            assert!(result.is_err());
1717        }
1718
1719        std::fs::remove_file(&path).ok();
1720    }
1721
1722    #[test]
1723    fn numeric_attr_roundtrip() {
1724        let path = temp_path("num_attr");
1725        {
1726            let file = H5File::create(&path).unwrap();
1727            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
1728            ds.write_raw(&[1.0f32; 4]).unwrap();
1729
1730            let a1 = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
1731            a1.write_numeric(&1.2345f64).unwrap();
1732
1733            let a2 = ds.new_attr::<i32>().shape(()).create("count").unwrap();
1734            a2.write_numeric(&42i32).unwrap();
1735
1736            file.close().unwrap();
1737        }
1738        {
1739            let file = H5File::open(&path).unwrap();
1740            let ds = file.dataset("data").unwrap();
1741
1742            let scale = ds.attr("scale").unwrap();
1743            let val: f64 = scale.read_numeric().unwrap();
1744            assert!((val - 1.2345).abs() < 1e-10);
1745
1746            let count = ds.attr("count").unwrap();
1747            let val: i32 = count.read_numeric().unwrap();
1748            assert_eq!(val, 42);
1749        }
1750        std::fs::remove_file(&path).ok();
1751    }
1752
1753    #[test]
1754    fn array_attr_roundtrip() {
1755        let path = temp_path("array_attr");
1756        let offsets = [10i32, -20, 30];
1757        {
1758            let file = H5File::create(&path).unwrap();
1759            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
1760            ds.write_raw(&[1.0f32; 4]).unwrap();
1761
1762            // 1-D int32 array attribute (NDArrayDimOffset-style).
1763            let a = ds
1764                .new_attr::<i32>()
1765                .shape([3])
1766                .create("dim_offset")
1767                .unwrap();
1768            a.write_array(&offsets).unwrap();
1769
1770            // Wrong element count is rejected.
1771            let bad = ds.new_attr::<i32>().shape([3]).create("bad").unwrap();
1772            assert!(bad.write_array(&[1i32, 2]).is_err());
1773
1774            file.close().unwrap();
1775        }
1776        {
1777            let file = H5File::open(&path).unwrap();
1778            let ds = file.dataset("data").unwrap();
1779            let a = ds.attr("dim_offset").unwrap();
1780            let raw = a.read_raw().unwrap();
1781            assert_eq!(raw.len(), 3 * 4);
1782            let got: Vec<i32> = raw
1783                .chunks_exact(4)
1784                .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
1785                .collect();
1786            assert_eq!(got, offsets);
1787        }
1788        std::fs::remove_file(&path).ok();
1789    }
1790
1791    #[test]
1792    fn attr_datatype_exposes_class_and_sign() {
1793        // H5Attribute::datatype() must report the stored datatype class and
1794        // signedness so a generic attr->metadata mapper need not infer it from
1795        // the byte width (the HDF5-L1 adapter blocker this accessor unblocks).
1796        use crate::format::messages::datatype::DatatypeMessage;
1797
1798        let path = temp_path("attr_datatype");
1799        {
1800            let file = H5File::create(&path).unwrap();
1801            let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
1802            ds.new_attr::<f64>()
1803                .shape(())
1804                .create("scale")
1805                .unwrap()
1806                .write_numeric(&1.5f64)
1807                .unwrap();
1808            ds.new_attr::<i32>()
1809                .shape(())
1810                .create("count")
1811                .unwrap()
1812                .write_numeric(&7i32)
1813                .unwrap();
1814            file.close().unwrap();
1815        }
1816        {
1817            let file = H5File::open(&path).unwrap();
1818            let ds = file.dataset("data").unwrap();
1819
1820            match ds.attr("scale").unwrap().datatype().unwrap() {
1821                DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 8),
1822                other => panic!("expected FloatingPoint for f64 attr, got {other:?}"),
1823            }
1824
1825            match ds.attr("count").unwrap().datatype().unwrap() {
1826                DatatypeMessage::FixedPoint { size, signed, .. } => {
1827                    assert_eq!(size, 4);
1828                    assert!(signed, "i32 attr must be signed");
1829                }
1830                other => panic!("expected FixedPoint for i32 attr, got {other:?}"),
1831            }
1832        }
1833        std::fs::remove_file(&path).ok();
1834    }
1835
1836    #[test]
1837    fn attr_datatype_in_write_mode_errors() {
1838        let path = temp_path("attr_datatype_write_mode");
1839        let file = H5File::create(&path).unwrap();
1840        let ds = file.new_dataset::<f32>().shape([4]).create("data").unwrap();
1841        let attr = ds.new_attr::<f64>().shape(()).create("scale").unwrap();
1842        assert!(attr.datatype().is_err());
1843        std::fs::remove_file(&path).ok();
1844    }
1845
1846    #[test]
1847    fn cannot_create_dataset_in_read_mode() {
1848        let path = temp_path("no_create_read");
1849
1850        {
1851            let _file = H5File::create(&path).unwrap();
1852        }
1853
1854        {
1855            let file = H5File::open(&path).unwrap();
1856            let result = file.new_dataset::<u8>().shape([4]).create("x");
1857            assert!(result.is_err());
1858        }
1859
1860        std::fs::remove_file(&path).ok();
1861    }
1862
1863    #[test]
1864    fn shape_accessor() {
1865        let path = temp_path("shape_acc");
1866
1867        let file = H5File::create(&path).unwrap();
1868        let ds = file
1869            .new_dataset::<f32>()
1870            .shape([5, 10, 3])
1871            .create("tensor")
1872            .unwrap();
1873        assert_eq!(ds.shape(), vec![5, 10, 3]);
1874
1875        std::fs::remove_file(&path).ok();
1876    }
1877
1878    #[test]
1879    fn slice_roundtrip_2d() {
1880        let path = temp_path("slice_2d");
1881
1882        // Create a 4x5 dataset, write full, then read a slice
1883        let data: Vec<i32> = (0..20).collect();
1884        {
1885            let file = H5File::create(&path).unwrap();
1886            let ds = file
1887                .new_dataset::<i32>()
1888                .shape([4, 5])
1889                .create("mat")
1890                .unwrap();
1891            ds.write_raw(&data).unwrap();
1892            file.close().unwrap();
1893        }
1894        {
1895            let file = H5File::open(&path).unwrap();
1896            let ds = file.dataset("mat").unwrap();
1897            // Read rows 1..3, cols 2..4 (2x2 slice)
1898            let slice = ds.read_slice::<i32>(&[1, 2], &[2, 2]).unwrap();
1899            // Row 1: [5,6,7,8,9] -> cols 2..4 = [7,8]
1900            // Row 2: [10,11,12,13,14] -> cols 2..4 = [12,13]
1901            assert_eq!(slice, vec![7, 8, 12, 13]);
1902        }
1903
1904        std::fs::remove_file(&path).ok();
1905    }
1906
1907    #[test]
1908    fn write_slice_2d() {
1909        let path = temp_path("write_slice_2d");
1910
1911        {
1912            let file = H5File::create(&path).unwrap();
1913            let ds = file
1914                .new_dataset::<f32>()
1915                .shape([3, 4])
1916                .create("data")
1917                .unwrap();
1918            ds.write_raw(&[0.0f32; 12]).unwrap();
1919            // Overwrite a 2x2 sub-region
1920            ds.write_slice(&[1, 1], &[2, 2], &[10.0f32, 20.0, 30.0, 40.0])
1921                .unwrap();
1922            file.close().unwrap();
1923        }
1924        {
1925            let file = H5File::open(&path).unwrap();
1926            let ds = file.dataset("data").unwrap();
1927            let full = ds.read_raw::<f32>().unwrap();
1928            // Row 0: [0,0,0,0]
1929            // Row 1: [0,10,20,0]
1930            // Row 2: [0,30,40,0]
1931            assert_eq!(
1932                full,
1933                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,]
1934            );
1935        }
1936
1937        std::fs::remove_file(&path).ok();
1938    }
1939
1940    #[test]
1941    fn write_slice_out_of_bounds_rejected() {
1942        let path = temp_path("write_slice_oob");
1943        let file = H5File::create(&path).unwrap();
1944        let ds = file.new_dataset::<i32>().shape([4]).create("d").unwrap();
1945        ds.write_raw(&[0i32; 4]).unwrap();
1946        // start 2 + count 6 = 8 > extent 4 -> must error, not corrupt.
1947        assert!(ds.write_slice(&[2], &[6], &[9i32; 6]).is_err());
1948        // An in-bounds slice still works.
1949        assert!(ds.write_slice(&[1], &[2], &[7i32, 8]).is_ok());
1950        std::fs::remove_file(&path).ok();
1951    }
1952
1953    #[test]
1954    fn duplicate_dataset_name_rejected() {
1955        let path = temp_path("dup_name");
1956        let file = H5File::create(&path).unwrap();
1957        let _ = file.new_dataset::<i32>().shape([2]).create("d").unwrap();
1958        assert!(file.new_dataset::<i32>().shape([2]).create("d").is_err());
1959        std::fs::remove_file(&path).ok();
1960    }
1961
1962    #[test]
1963    fn extend_cannot_shrink() {
1964        let path = temp_path("extend_shrink");
1965        let file = H5File::create(&path).unwrap();
1966        let ds = file
1967            .new_dataset::<i32>()
1968            .shape([0])
1969            .chunk(&[2])
1970            .max_shape(&[None])
1971            .create("d")
1972            .unwrap();
1973        ds.append(&[1i32, 2, 3, 4]).unwrap();
1974        // Shrinking below the written extent must be rejected.
1975        assert!(ds.extend(&[2]).is_err());
1976        // Growing is fine.
1977        assert!(ds.extend(&[6]).is_ok());
1978        std::fs::remove_file(&path).ok();
1979    }
1980
1981    #[test]
1982    fn attr_read_roundtrip() {
1983        use crate::types::VarLenUnicode;
1984        let path = temp_path("attr_read");
1985
1986        {
1987            let file = H5File::create(&path).unwrap();
1988            let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
1989            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1990            let a1 = ds
1991                .new_attr::<VarLenUnicode>()
1992                .shape(())
1993                .create("units")
1994                .unwrap();
1995            a1.write_string("meters").unwrap();
1996            let a2 = ds
1997                .new_attr::<VarLenUnicode>()
1998                .shape(())
1999                .create("desc")
2000                .unwrap();
2001            a2.write_string("test data").unwrap();
2002            file.close().unwrap();
2003        }
2004        {
2005            let file = H5File::open(&path).unwrap();
2006            let ds = file.dataset("data").unwrap();
2007
2008            let names = ds.attr_names().unwrap();
2009            assert!(names.contains(&"units".to_string()));
2010            assert!(names.contains(&"desc".to_string()));
2011
2012            let units = ds.attr("units").unwrap();
2013            assert_eq!(units.read_string().unwrap(), "meters");
2014
2015            let desc = ds.attr("desc").unwrap();
2016            assert_eq!(desc.read_string().unwrap(), "test data");
2017        }
2018
2019        std::fs::remove_file(&path).ok();
2020    }
2021
2022    #[test]
2023    fn type_mismatch_element_size() {
2024        let path = temp_path("type_mismatch");
2025
2026        {
2027            let file = H5File::create(&path).unwrap();
2028            let ds = file.new_dataset::<f64>().shape([4]).create("data").unwrap();
2029            ds.write_raw(&[1.0f64, 2.0, 3.0, 4.0]).unwrap();
2030            file.close().unwrap();
2031        }
2032
2033        {
2034            let file = H5File::open(&path).unwrap();
2035            let ds = file.dataset("data").unwrap();
2036            // Try to read as u8 (element_size = 1) from a f64 dataset (element_size = 8)
2037            let result = ds.read_raw::<u8>();
2038            assert!(result.is_err());
2039        }
2040
2041        std::fs::remove_file(&path).ok();
2042    }
2043
2044    #[test]
2045    fn dataset_survives_file_move() {
2046        let path = temp_path("ds_survives");
2047
2048        let ds = {
2049            let file = H5File::create(&path).unwrap();
2050            file.new_dataset::<u8>().shape([4]).create("x").unwrap()
2051        };
2052        // file is dropped here, but ds still holds Rc to the inner state
2053        ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2054        // The writer will finalize on drop of the last Rc
2055
2056        std::fs::remove_file(&path).ok();
2057    }
2058
2059    #[test]
2060    fn new_attr_scalar_string() {
2061        use crate::types::VarLenUnicode;
2062
2063        let path = temp_path("attr_scalar_string");
2064        {
2065            let file = H5File::create(&path).unwrap();
2066            let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
2067            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
2068
2069            let attr = ds
2070                .new_attr::<VarLenUnicode>()
2071                .shape(())
2072                .create("name")
2073                .unwrap();
2074            attr.write_scalar(&VarLenUnicode("test_value".to_string()))
2075                .unwrap();
2076
2077            file.close().unwrap();
2078        }
2079
2080        // Verify the file is still valid and readable
2081        {
2082            let file = H5File::open(&path).unwrap();
2083            let ds = file.dataset("data").unwrap();
2084            assert_eq!(ds.shape(), vec![4]);
2085            let readback = ds.read_raw::<u8>().unwrap();
2086            assert_eq!(readback, vec![1u8, 2, 3, 4]);
2087        }
2088
2089        std::fs::remove_file(&path).ok();
2090    }
2091
2092    #[test]
2093    fn all_numeric_types_roundtrip() {
2094        let path = temp_path("all_types");
2095
2096        {
2097            let file = H5File::create(&path).unwrap();
2098
2099            let ds = file.new_dataset::<u8>().shape([2]).create("u8").unwrap();
2100            ds.write_raw(&[1u8, 2]).unwrap();
2101
2102            let ds = file.new_dataset::<i8>().shape([2]).create("i8").unwrap();
2103            ds.write_raw(&[-1i8, 1]).unwrap();
2104
2105            let ds = file.new_dataset::<u16>().shape([2]).create("u16").unwrap();
2106            ds.write_raw(&[100u16, 200]).unwrap();
2107
2108            let ds = file.new_dataset::<i16>().shape([2]).create("i16").unwrap();
2109            ds.write_raw(&[-100i16, 100]).unwrap();
2110
2111            let ds = file.new_dataset::<u32>().shape([2]).create("u32").unwrap();
2112            ds.write_raw(&[1000u32, 2000]).unwrap();
2113
2114            let ds = file.new_dataset::<i32>().shape([2]).create("i32").unwrap();
2115            ds.write_raw(&[-1000i32, 1000]).unwrap();
2116
2117            let ds = file.new_dataset::<u64>().shape([2]).create("u64").unwrap();
2118            ds.write_raw(&[10000u64, 20000]).unwrap();
2119
2120            let ds = file.new_dataset::<i64>().shape([2]).create("i64").unwrap();
2121            ds.write_raw(&[-10000i64, 10000]).unwrap();
2122
2123            let ds = file.new_dataset::<f32>().shape([2]).create("f32").unwrap();
2124            ds.write_raw(&[1.5f32, 2.5]).unwrap();
2125
2126            let ds = file.new_dataset::<f64>().shape([2]).create("f64").unwrap();
2127            ds.write_raw(&[1.23456f64, 7.89012]).unwrap();
2128
2129            file.close().unwrap();
2130        }
2131
2132        {
2133            let file = H5File::open(&path).unwrap();
2134
2135            assert_eq!(
2136                file.dataset("u8").unwrap().read_raw::<u8>().unwrap(),
2137                vec![1u8, 2]
2138            );
2139            assert_eq!(
2140                file.dataset("i8").unwrap().read_raw::<i8>().unwrap(),
2141                vec![-1i8, 1]
2142            );
2143            assert_eq!(
2144                file.dataset("u16").unwrap().read_raw::<u16>().unwrap(),
2145                vec![100u16, 200]
2146            );
2147            assert_eq!(
2148                file.dataset("i16").unwrap().read_raw::<i16>().unwrap(),
2149                vec![-100i16, 100]
2150            );
2151            assert_eq!(
2152                file.dataset("u32").unwrap().read_raw::<u32>().unwrap(),
2153                vec![1000u32, 2000]
2154            );
2155            assert_eq!(
2156                file.dataset("i32").unwrap().read_raw::<i32>().unwrap(),
2157                vec![-1000i32, 1000]
2158            );
2159            assert_eq!(
2160                file.dataset("u64").unwrap().read_raw::<u64>().unwrap(),
2161                vec![10000u64, 20000]
2162            );
2163            assert_eq!(
2164                file.dataset("i64").unwrap().read_raw::<i64>().unwrap(),
2165                vec![-10000i64, 10000]
2166            );
2167            assert_eq!(
2168                file.dataset("f32").unwrap().read_raw::<f32>().unwrap(),
2169                vec![1.5f32, 2.5]
2170            );
2171            assert_eq!(
2172                file.dataset("f64").unwrap().read_raw::<f64>().unwrap(),
2173                vec![1.23456f64, 7.89012]
2174            );
2175        }
2176
2177        std::fs::remove_file(&path).ok();
2178    }
2179
2180    #[test]
2181    fn append_chunked_roundtrip() {
2182        let path = temp_path("append_chunked");
2183
2184        {
2185            let file = H5File::create(&path).unwrap();
2186            let ds = file
2187                .new_dataset::<f64>()
2188                .shape([0, 3])
2189                .chunk(&[1, 3])
2190                .max_shape(&[None, Some(3)])
2191                .create("data")
2192                .unwrap();
2193
2194            // Append one frame
2195            ds.append(&[1.0f64, 2.0, 3.0]).unwrap();
2196            // Append two frames at once
2197            ds.append(&[4.0f64, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap();
2198
2199            file.close().unwrap();
2200        }
2201
2202        {
2203            let file = H5File::open(&path).unwrap();
2204            let ds = file.dataset("data").unwrap();
2205            assert_eq!(ds.shape(), vec![3, 3]);
2206            let all = ds.read_raw::<f64>().unwrap();
2207            assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
2208        }
2209
2210        std::fs::remove_file(&path).ok();
2211    }
2212
2213    #[test]
2214    fn append_1d_chunked() {
2215        let path = temp_path("append_1d");
2216
2217        {
2218            let file = H5File::create(&path).unwrap();
2219            let ds = file
2220                .new_dataset::<i32>()
2221                .shape([0])
2222                .chunk(&[4])
2223                .max_shape(&[None])
2224                .create("values")
2225                .unwrap();
2226
2227            ds.append(&[10i32, 20, 30]).unwrap(); // partial chunk
2228            ds.append(&[40i32]).unwrap(); // fills chunk boundary
2229            ds.append(&[50i32, 60, 70, 80]).unwrap(); // full chunk
2230
2231            file.close().unwrap();
2232        }
2233
2234        {
2235            let file = H5File::open(&path).unwrap();
2236            let ds = file.dataset("values").unwrap();
2237            assert_eq!(ds.shape(), vec![8]);
2238            let all = ds.read_raw::<i32>().unwrap();
2239            assert_eq!(all, vec![10, 20, 30, 40, 50, 60, 70, 80]);
2240        }
2241
2242        std::fs::remove_file(&path).ok();
2243    }
2244
2245    #[test]
2246    fn append_partial_chunk_flushed_on_close() {
2247        let path = temp_path("append_partial_close");
2248
2249        {
2250            let file = H5File::create(&path).unwrap();
2251            let ds = file
2252                .new_dataset::<f64>()
2253                .shape([0])
2254                .chunk(&[4])
2255                .max_shape(&[None])
2256                .create("vals")
2257                .unwrap();
2258
2259            // Append 5 elements: chunk 0 = full [1,2,3,4], chunk 1 = partial [5,0,0,0]
2260            ds.append(&[1.0f64, 2.0, 3.0, 4.0, 5.0]).unwrap();
2261            file.close().unwrap();
2262        }
2263
2264        {
2265            let file = H5File::open(&path).unwrap();
2266            let ds = file.dataset("vals").unwrap();
2267            assert_eq!(ds.shape(), vec![5]);
2268            let all = ds.read_raw::<f64>().unwrap();
2269            // The full dataset is 2 chunks * 4 = 8 elements; shape says 5
2270            // read_raw reads total shape elements
2271            assert_eq!(all.len(), 5);
2272            assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
2273        }
2274
2275        std::fs::remove_file(&path).ok();
2276    }
2277
2278    #[cfg(feature = "deflate")]
2279    #[test]
2280    fn vlen_append_after_reopen_filtered() {
2281        // Reopen + append into a partially-written *compressed* vlen chunk
2282        // (index-block chunk). Exercises filtered-index-block reconstruction
2283        // in open_append plus filtered read-modify-write.
2284        let path = temp_path("vlen_reopen_filtered");
2285        {
2286            let file = H5File::create(&path).unwrap();
2287            file.create_appendable_vlen_dataset(
2288                "strs",
2289                4,
2290                Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2291            )
2292            .unwrap();
2293            file.append_vlen_strings("strs", &["alpha", "beta", "gamma"])
2294                .unwrap();
2295            file.close().unwrap();
2296        }
2297        {
2298            let file = H5File::open_rw(&path).unwrap();
2299            file.append_vlen_strings("strs", &["delta"]).unwrap();
2300            file.close().unwrap();
2301        }
2302        {
2303            let file = H5File::open(&path).unwrap();
2304            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2305            assert_eq!(
2306                got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
2307                vec!["alpha", "beta", "gamma", "delta"]
2308            );
2309        }
2310        std::fs::remove_file(&path).ok();
2311    }
2312
2313    #[test]
2314    fn vlen_append_after_reopen_data_block() {
2315        // Reopen + append into a partial chunk that lives in an extensible-
2316        // array *data block* (chunk index >= idx_blk_elmts). Exercises
2317        // data-block resolution in read_chunk_if_present and write_chunk.
2318        let path = temp_path("vlen_reopen_datablk");
2319        let labels: Vec<String> = (0..9).map(|i| format!("s{i}")).collect();
2320        {
2321            let file = H5File::create(&path).unwrap();
2322            file.create_appendable_vlen_dataset("strs", 2, None)
2323                .unwrap();
2324            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2325            file.append_vlen_strings("strs", &refs).unwrap();
2326            file.close().unwrap();
2327        }
2328        {
2329            let file = H5File::open_rw(&path).unwrap();
2330            file.append_vlen_strings("strs", &["s9"]).unwrap();
2331            file.close().unwrap();
2332        }
2333        {
2334            let file = H5File::open(&path).unwrap();
2335            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2336            let want: Vec<String> = (0..10).map(|i| format!("s{i}")).collect();
2337            assert_eq!(got, want);
2338        }
2339        std::fs::remove_file(&path).ok();
2340    }
2341
2342    #[test]
2343    fn vlen_append_after_reopen_super_block() {
2344        // Reopen + append into a partial chunk whose index falls in an
2345        // extensible-array *super block* (chunk index 244 with the default
2346        // EA geometry: idx_blk_elmts=4, data_blk_min_elmts=16,
2347        // sup_blk_min_data_ptrs=4 -> chunks 0..=243 are reached via the
2348        // index block or its direct data blocks, so chunk 244 is reached
2349        // via a super block read from disk). Exercises the ViaSblk branch
2350        // of read_chunk_if_present.
2351        let path = temp_path("vlen_reopen_super");
2352        // 489 strings, chunk size 2 -> chunk 244 holds one string only
2353        // (partially filled) and is flushed to disk on close.
2354        let labels: Vec<String> = (0..489).map(|i| format!("v{i}")).collect();
2355        {
2356            let file = H5File::create(&path).unwrap();
2357            file.create_appendable_vlen_dataset("strs", 2, None)
2358                .unwrap();
2359            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2360            file.append_vlen_strings("strs", &refs).unwrap();
2361            file.close().unwrap();
2362        }
2363        {
2364            let file = H5File::open_rw(&path).unwrap();
2365            file.append_vlen_strings("strs", &["v489"]).unwrap();
2366            file.close().unwrap();
2367        }
2368        {
2369            let file = H5File::open(&path).unwrap();
2370            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2371            let want: Vec<String> = (0..490).map(|i| format!("v{i}")).collect();
2372            assert_eq!(got, want);
2373        }
2374        std::fs::remove_file(&path).ok();
2375    }
2376
2377    #[cfg(feature = "deflate")]
2378    #[test]
2379    fn vlen_append_after_reopen_filtered_data_block() {
2380        // The hardest path: compressed + chunk in a data block + partial
2381        // read-modify-write across a reopen.
2382        let path = temp_path("vlen_reopen_filt_datablk");
2383        let labels: Vec<String> = (0..9).map(|i| format!("item{i:02}")).collect();
2384        {
2385            let file = H5File::create(&path).unwrap();
2386            file.create_appendable_vlen_dataset(
2387                "strs",
2388                2,
2389                Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2390            )
2391            .unwrap();
2392            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2393            file.append_vlen_strings("strs", &refs).unwrap();
2394            file.close().unwrap();
2395        }
2396        {
2397            let file = H5File::open_rw(&path).unwrap();
2398            file.append_vlen_strings("strs", &["item09"]).unwrap();
2399            file.close().unwrap();
2400        }
2401        {
2402            let file = H5File::open(&path).unwrap();
2403            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2404            let want: Vec<String> = (0..10).map(|i| format!("item{i:02}")).collect();
2405            assert_eq!(got, want);
2406        }
2407        std::fs::remove_file(&path).ok();
2408    }
2409
2410    #[test]
2411    fn group_nx_class_attribute_roundtrip() {
2412        // Non-root groups carry attributes (NeXus `NX_class`) in their
2413        // own object header, and the reader reads them back by path.
2414        let path = temp_path("group_nx_class");
2415        {
2416            let file = H5File::create(&path).unwrap();
2417            let entry = file.create_group("entry").unwrap();
2418            entry.set_attr_string("NX_class", "NXentry").unwrap();
2419            let det = entry.create_group("detector").unwrap();
2420            det.set_attr_string("NX_class", "NXdetector").unwrap();
2421            det.set_attr_numeric("frame_count", &7i32).unwrap();
2422            det.new_dataset::<f32>()
2423                .shape([4])
2424                .create("data")
2425                .unwrap()
2426                .write_raw(&[1.0f32; 4])
2427                .unwrap();
2428            file.close().unwrap();
2429        }
2430        {
2431            let file = H5File::open(&path).unwrap();
2432            let entry = file.root_group().group("entry").unwrap();
2433            assert_eq!(entry.attr_string("NX_class").unwrap(), "NXentry");
2434            let det = entry.group("detector").unwrap();
2435            assert_eq!(det.attr_string("NX_class").unwrap(), "NXdetector");
2436            let names = det.attr_names().unwrap();
2437            assert!(names.contains(&"NX_class".to_string()));
2438            assert!(names.contains(&"frame_count".to_string()));
2439        }
2440        std::fs::remove_file(&path).ok();
2441    }
2442
2443    #[test]
2444    fn ea_super_block_roundtrip() {
2445        // 2000 chunks span several extensible-array super blocks. Before
2446        // super-block support the writer errored at chunk index 228.
2447        let path = temp_path("ea_super_rt");
2448        {
2449            let file = H5File::create(&path).unwrap();
2450            let ds = file
2451                .new_dataset::<i32>()
2452                .shape([0])
2453                .chunk(&[1])
2454                .max_shape(&[None])
2455                .create("v")
2456                .unwrap();
2457            ds.append(&(0..2000).collect::<Vec<i32>>()).unwrap();
2458            file.close().unwrap();
2459        }
2460        {
2461            let file = H5File::open(&path).unwrap();
2462            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2463            assert_eq!(v.len(), 2000);
2464            assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2465        }
2466        std::fs::remove_file(&path).ok();
2467    }
2468
2469    #[cfg(feature = "deflate")]
2470    #[test]
2471    fn ea_filtered_super_block_roundtrip() {
2472        // Compressed chunks across super blocks.
2473        let path = temp_path("ea_filt_super");
2474        {
2475            let file = H5File::create(&path).unwrap();
2476            let ds = file
2477                .new_dataset::<i32>()
2478                .shape([0])
2479                .chunk(&[1])
2480                .max_shape(&[None])
2481                .deflate(4)
2482                .create("v")
2483                .unwrap();
2484            ds.append(&(0..600).collect::<Vec<i32>>()).unwrap();
2485            file.close().unwrap();
2486        }
2487        {
2488            let file = H5File::open(&path).unwrap();
2489            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2490            assert_eq!(v, (0..600).collect::<Vec<i32>>());
2491        }
2492        std::fs::remove_file(&path).ok();
2493    }
2494
2495    #[test]
2496    fn ea_super_block_open_append() {
2497        // Reopen a dataset and append chunks that fall in super blocks.
2498        let path = temp_path("ea_super_append");
2499        {
2500            let file = H5File::create(&path).unwrap();
2501            let ds = file
2502                .new_dataset::<i32>()
2503                .shape([0])
2504                .chunk(&[1])
2505                .max_shape(&[None])
2506                .create("v")
2507                .unwrap();
2508            ds.append(&(0..300).collect::<Vec<i32>>()).unwrap();
2509            file.close().unwrap();
2510        }
2511        {
2512            let mut w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2513            let idx = w.dataset_index("v").unwrap();
2514            for c in 300..900u64 {
2515                w.write_chunk(idx, c, &(c as i32).to_le_bytes()).unwrap();
2516            }
2517            w.extend_dataset(idx, &[900]).unwrap();
2518            w.close().unwrap();
2519        }
2520        {
2521            let file = H5File::open(&path).unwrap();
2522            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2523            assert_eq!(v.len(), 900);
2524            assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2525        }
2526        std::fs::remove_file(&path).ok();
2527    }
2528
2529    #[test]
2530    fn btree_v2_multi_unlimited_roundtrip() {
2531        // A dataset with two unlimited dimensions uses the v2 B-tree chunk
2532        // index; chunks are written by grid coordinates with write_chunk_at.
2533        let path = temp_path("bt2_multi");
2534        {
2535            let file = H5File::create(&path).unwrap();
2536            let ds = file
2537                .new_dataset::<i32>()
2538                .shape([0, 0])
2539                .chunk(&[2, 2])
2540                .max_shape(&[None, None])
2541                .create("grid")
2542                .unwrap();
2543            assert!(ds.is_chunked());
2544            // 4x4 logical grid, value[r][c] = r*4 + c, in 2x2 chunks.
2545            for cr in 0..2usize {
2546                for cc in 0..2usize {
2547                    let mut bytes = Vec::new();
2548                    for i in 0..2usize {
2549                        for j in 0..2usize {
2550                            let v = ((cr * 2 + i) * 4 + (cc * 2 + j)) as i32;
2551                            bytes.extend_from_slice(&v.to_le_bytes());
2552                        }
2553                    }
2554                    ds.write_chunk_at(&[cr, cc], &bytes).unwrap();
2555                }
2556            }
2557            file.close().unwrap();
2558        }
2559        {
2560            let file = H5File::open(&path).unwrap();
2561            let ds = file.dataset("grid").unwrap();
2562            assert_eq!(ds.shape(), vec![4, 4]);
2563            assert_eq!(ds.read_raw::<i32>().unwrap(), (0..16).collect::<Vec<i32>>());
2564        }
2565        std::fs::remove_file(&path).ok();
2566    }
2567
2568    #[test]
2569    fn subframe_chunking_roundtrip() {
2570        // A chunk smaller than a frame: shape [N,8,8], chunk [1,4,4], so each
2571        // frame is tiled into a 2x2 grid of 4x4 chunks. write_chunk_at takes
2572        // the chunk-grid coordinates.
2573        let path = temp_path("subframe");
2574        {
2575            let file = H5File::create(&path).unwrap();
2576            let ds = file
2577                .new_dataset::<i32>()
2578                .shape([0, 8, 8])
2579                .chunk(&[1, 4, 4])
2580                .max_shape(&[None, Some(8), Some(8)])
2581                .create("v")
2582                .unwrap();
2583            for f in 0..3usize {
2584                for cr in 0..2usize {
2585                    for cc in 0..2usize {
2586                        let mut bytes = Vec::new();
2587                        for i in 0..4usize {
2588                            for j in 0..4usize {
2589                                let v = (f * 64 + (cr * 4 + i) * 8 + (cc * 4 + j)) as i32;
2590                                bytes.extend_from_slice(&v.to_le_bytes());
2591                            }
2592                        }
2593                        ds.write_chunk_at(&[f, cr, cc], &bytes).unwrap();
2594                    }
2595                }
2596            }
2597            file.close().unwrap();
2598        }
2599        {
2600            let file = H5File::open(&path).unwrap();
2601            let ds = file.dataset("v").unwrap();
2602            assert_eq!(ds.shape(), vec![3, 8, 8]);
2603            assert_eq!(
2604                ds.read_raw::<i32>().unwrap(),
2605                (0..192).collect::<Vec<i32>>()
2606            );
2607        }
2608        std::fs::remove_file(&path).ok();
2609    }
2610
2611    #[test]
2612    fn fill_value_contiguous_roundtrip() {
2613        let path = temp_path("fill_value_contig");
2614        {
2615            let file = H5File::create(&path).unwrap();
2616            let ds = file
2617                .new_dataset::<f32>()
2618                .shape([4])
2619                .fill_value(2.5f32)
2620                .create("data")
2621                .unwrap();
2622            ds.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
2623            file.close().unwrap();
2624        }
2625        // open_append decodes the fill-value message back from the header.
2626        {
2627            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2628            let idx = writer.dataset_index("data").unwrap();
2629            assert_eq!(
2630                writer.datasets[idx].fill_value,
2631                Some(2.5f32.to_le_bytes().to_vec())
2632            );
2633        }
2634        // Data still reads back correctly.
2635        {
2636            let file = H5File::open(&path).unwrap();
2637            let ds = file.dataset("data").unwrap();
2638            assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
2639        }
2640        std::fs::remove_file(&path).ok();
2641    }
2642
2643    #[test]
2644    fn fill_value_chunked_roundtrip() {
2645        let path = temp_path("fill_value_chunked");
2646        {
2647            let file = H5File::create(&path).unwrap();
2648            let ds = file
2649                .new_dataset::<i32>()
2650                .shape([0])
2651                .chunk(&[4])
2652                .max_shape(&[None])
2653                .fill_value(-7i32)
2654                .create("vals")
2655                .unwrap();
2656            ds.append(&[1i32, 2, 3, 4]).unwrap();
2657            file.close().unwrap();
2658        }
2659        {
2660            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2661            let idx = writer.dataset_index("vals").unwrap();
2662            assert_eq!(
2663                writer.datasets[idx].fill_value,
2664                Some((-7i32).to_le_bytes().to_vec())
2665            );
2666        }
2667        std::fs::remove_file(&path).ok();
2668    }
2669
2670    #[test]
2671    fn fill_value_read_missing_chunks() {
2672        // A chunked dataset with chunk 1 left unwritten must read that
2673        // gap back as the user-defined fill value, not zero.
2674        fn i32_bytes(vals: &[i32]) -> Vec<u8> {
2675            vals.iter().flat_map(|v| v.to_le_bytes()).collect()
2676        }
2677        let path = temp_path("fill_value_read_missing");
2678        {
2679            let file = H5File::create(&path).unwrap();
2680            let ds = file
2681                .new_dataset::<i32>()
2682                .shape([0])
2683                .chunk(&[2])
2684                .max_shape(&[None])
2685                .fill_value(-1i32)
2686                .create("vals")
2687                .unwrap();
2688            // chunk 0 = [10,20]; chunk 1 unwritten; chunk 2 = [50,60].
2689            ds.write_chunk(0, &i32_bytes(&[10, 20])).unwrap();
2690            ds.write_chunk(2, &i32_bytes(&[50, 60])).unwrap();
2691            ds.extend(&[6]).unwrap();
2692            file.close().unwrap();
2693        }
2694        {
2695            let file = H5File::open(&path).unwrap();
2696            let ds = file.dataset("vals").unwrap();
2697            let all = ds.read_raw::<i32>().unwrap();
2698            assert_eq!(all, vec![10, 20, -1, -1, 50, 60]);
2699        }
2700        std::fs::remove_file(&path).ok();
2701    }
2702
2703    #[test]
2704    fn fill_value_partial_chunk_padded_with_fill() {
2705        // A partial trailing chunk flushed at close must pad its unwritten
2706        // tail with the fill value. That pad sits beyond the logical shape,
2707        // so it is verified by scanning the on-disk chunk bytes directly.
2708        let path = temp_path("fill_value_partial_pad");
2709        {
2710            let file = H5File::create(&path).unwrap();
2711            let ds = file
2712                .new_dataset::<i32>()
2713                .shape([0])
2714                .chunk(&[4])
2715                .max_shape(&[None])
2716                .fill_value(-9i32)
2717                .create("vals")
2718                .unwrap();
2719            // 3 of 4 frames -> flushed as a partial chunk on close.
2720            ds.append(&[1i32, 2, 3]).unwrap();
2721            file.close().unwrap();
2722        }
2723        let bytes = std::fs::read(&path).unwrap();
2724        // Locate the chunk: i32 LE of [1, 2, 3] written contiguously.
2725        let needle: Vec<u8> = [1i32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
2726        let pos = bytes
2727            .windows(needle.len())
2728            .position(|w| w == needle)
2729            .expect("chunk data [1,2,3] not found in file");
2730        let pad = &bytes[pos + needle.len()..pos + needle.len() + 4];
2731        assert_eq!(
2732            pad,
2733            &(-9i32).to_le_bytes(),
2734            "partial chunk tail must be padded with fill value -9, got {:?}",
2735            pad
2736        );
2737        std::fs::remove_file(&path).ok();
2738    }
2739
2740    #[test]
2741    fn vlen_append_after_reopen_preserves_existing() {
2742        // Reopening and appending into a partially-written vlen chunk must
2743        // read-modify-write: the strings already on disk must survive.
2744        let path = temp_path("vlen_append_reopen");
2745        {
2746            let file = H5File::create(&path).unwrap();
2747            file.create_appendable_vlen_dataset("strs", 4, None)
2748                .unwrap();
2749            // 3 of 4 frames -> flushed as a partial chunk on close.
2750            file.append_vlen_strings("strs", &["a", "b", "c"]).unwrap();
2751            file.close().unwrap();
2752        }
2753        {
2754            // Append a 4th string -> partial-chunk write into chunk 0.
2755            let file = H5File::open_rw(&path).unwrap();
2756            file.append_vlen_strings("strs", &["d"]).unwrap();
2757            file.close().unwrap();
2758        }
2759        {
2760            let file = H5File::open(&path).unwrap();
2761            let ds = file.dataset("strs").unwrap();
2762            let got = ds.read_vlen_strings().unwrap();
2763            assert_eq!(
2764                got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
2765                vec!["a", "b", "c", "d"]
2766            );
2767        }
2768        std::fs::remove_file(&path).ok();
2769    }
2770
2771    #[test]
2772    fn fill_value_size_mismatch_errors() {
2773        let path = temp_path("fill_value_mismatch");
2774        let mut writer = crate::io::writer::Hdf5Writer::create(&path).unwrap();
2775        let dt = <f64 as crate::types::H5Type>::hdf5_type();
2776        let idx = writer.create_dataset("d", dt, &[4u64]).unwrap();
2777        // f64 element size is 8; a 4-byte fill value must be rejected.
2778        assert!(writer.set_dataset_fill_value(idx, vec![0u8; 4]).is_err());
2779        // The correct width succeeds.
2780        writer.set_dataset_fill_value(idx, vec![0u8; 8]).unwrap();
2781        writer.close().unwrap();
2782        std::fs::remove_file(&path).ok();
2783    }
2784
2785    #[test]
2786    fn datatype_exposes_class_sign_and_byteorder() {
2787        // The byte width alone cannot tell u8 from i8 (both 1 byte) or i32
2788        // from f32 (both 4 bytes). datatype() must report the real class and
2789        // signedness so a reader does not have to guess from element_size.
2790        use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
2791
2792        let path = temp_path("datatype_accessor");
2793        {
2794            let file = H5File::create(&path).unwrap();
2795            file.new_dataset::<u8>().shape([3]).create("u8d").unwrap();
2796            file.new_dataset::<i8>().shape([3]).create("i8d").unwrap();
2797            file.new_dataset::<i32>().shape([3]).create("i32d").unwrap();
2798            file.new_dataset::<f32>().shape([3]).create("f32d").unwrap();
2799            file.close().unwrap();
2800        }
2801
2802        let file = H5File::open(&path).unwrap();
2803
2804        match file.dataset("u8d").unwrap().datatype().unwrap() {
2805            DatatypeMessage::FixedPoint {
2806                size,
2807                signed,
2808                byte_order,
2809                ..
2810            } => {
2811                assert_eq!(size, 1);
2812                assert!(!signed, "u8 must be unsigned");
2813                assert_eq!(byte_order, ByteOrder::LittleEndian);
2814            }
2815            other => panic!("expected FixedPoint for u8, got {other:?}"),
2816        }
2817
2818        match file.dataset("i8d").unwrap().datatype().unwrap() {
2819            DatatypeMessage::FixedPoint { size, signed, .. } => {
2820                assert_eq!(size, 1);
2821                assert!(signed, "i8 must be signed");
2822            }
2823            other => panic!("expected FixedPoint for i8, got {other:?}"),
2824        }
2825
2826        match file.dataset("i32d").unwrap().datatype().unwrap() {
2827            DatatypeMessage::FixedPoint { size, signed, .. } => {
2828                assert_eq!(size, 4);
2829                assert!(signed, "i32 must be signed");
2830            }
2831            other => panic!("expected FixedPoint for i32, got {other:?}"),
2832        }
2833
2834        match file.dataset("f32d").unwrap().datatype().unwrap() {
2835            DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 4),
2836            other => panic!("expected FloatingPoint for f32, got {other:?}"),
2837        }
2838
2839        std::fs::remove_file(&path).ok();
2840    }
2841
2842    #[test]
2843    fn datatype_in_write_mode_errors() {
2844        let path = temp_path("datatype_write_mode");
2845        let file = H5File::create(&path).unwrap();
2846        let ds = file.new_dataset::<f32>().shape([4]).create("d").unwrap();
2847        assert!(ds.datatype().is_err());
2848        std::fs::remove_file(&path).ok();
2849    }
2850
2851    // --- write_chunk_raw (HDF5 direct chunk write) ---------------------------
2852
2853    /// Extensible-array path: pre-compress with the dataset's pipeline, write
2854    /// the bytes verbatim via write_chunk_raw (filter_mask = 0), and confirm
2855    /// the data round-trips through the reader unchanged.
2856    #[cfg(feature = "deflate")]
2857    #[test]
2858    fn write_chunk_raw_ea_roundtrip_mask0() {
2859        use crate::format::messages::filter::{apply_filters, FilterPipeline};
2860        let path = temp_path("wcr_ea_mask0");
2861        let original: Vec<i32> = (0..12).collect();
2862        {
2863            let file = H5File::create(&path).unwrap();
2864            let ds = file
2865                .new_dataset::<i32>()
2866                .shape([0])
2867                .chunk(&[4])
2868                .max_shape(&[None])
2869                .deflate(4)
2870                .create("v")
2871                .unwrap();
2872            assert!(ds.is_chunked());
2873            let pipeline = FilterPipeline::deflate(4);
2874            for c in 0..3usize {
2875                let raw: Vec<u8> = original[c * 4..c * 4 + 4]
2876                    .iter()
2877                    .flat_map(|v| v.to_le_bytes())
2878                    .collect();
2879                let compressed = apply_filters(&pipeline, &raw).unwrap();
2880                ds.write_chunk_raw(c, &compressed, 0).unwrap();
2881            }
2882            ds.set_extent(&[12]).unwrap();
2883            file.close().unwrap();
2884        }
2885        {
2886            let file = H5File::open(&path).unwrap();
2887            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2888            assert_eq!(v, original);
2889        }
2890        std::fs::remove_file(&path).ok();
2891    }
2892
2893    /// Fixed-array path (all dimensions bounded): same verbatim write through
2894    /// the linear-index dispatch, round-tripped through the reader.
2895    #[cfg(feature = "deflate")]
2896    #[test]
2897    fn write_chunk_raw_fixed_array_roundtrip_mask0() {
2898        use crate::format::messages::filter::{apply_filters, FilterPipeline};
2899        let path = temp_path("wcr_fa_mask0");
2900        let original: Vec<i32> = (0..12).collect();
2901        {
2902            let file = H5File::create(&path).unwrap();
2903            let ds = file
2904                .new_dataset::<i32>()
2905                .shape([12])
2906                .chunk(&[4])
2907                .deflate(4)
2908                .create("v")
2909                .unwrap();
2910            assert!(ds.is_chunked());
2911            let pipeline = FilterPipeline::deflate(4);
2912            for c in 0..3usize {
2913                let raw: Vec<u8> = original[c * 4..c * 4 + 4]
2914                    .iter()
2915                    .flat_map(|v| v.to_le_bytes())
2916                    .collect();
2917                let compressed = apply_filters(&pipeline, &raw).unwrap();
2918                ds.write_chunk_raw(c, &compressed, 0).unwrap();
2919            }
2920            file.close().unwrap();
2921        }
2922        {
2923            let file = H5File::open(&path).unwrap();
2924            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2925            assert_eq!(v, original);
2926        }
2927        std::fs::remove_file(&path).ok();
2928    }
2929
2930    /// The caller-supplied filter_mask must reach the on-disk filtered index
2931    /// entry (not be hardcoded to 0). Store one chunk uncompressed in a
2932    /// filtered dataset with mask = 1 (deflate skipped), then reopen and decode
2933    /// the extensible-array filtered entry to read the mask back at the format
2934    /// level (independent of the data reader's mask handling).
2935    #[cfg(feature = "deflate")]
2936    #[test]
2937    fn write_chunk_raw_records_filter_mask() {
2938        let path = temp_path("wcr_records_mask");
2939        let raw: Vec<u8> = [10i32, 20, 30, 40]
2940            .iter()
2941            .flat_map(|v| v.to_le_bytes())
2942            .collect();
2943        assert_eq!(raw.len(), 16);
2944        {
2945            let file = H5File::create(&path).unwrap();
2946            let ds = file
2947                .new_dataset::<i32>()
2948                .shape([0])
2949                .chunk(&[4])
2950                .max_shape(&[None])
2951                .deflate(4)
2952                .create("v")
2953                .unwrap();
2954            // mask = 1: bit 0 set => filter 0 (deflate) was skipped, so the
2955            // chunk is stored uncompressed (its raw bytes).
2956            ds.write_chunk_raw(0, &raw, 1).unwrap();
2957            ds.set_extent(&[4]).unwrap();
2958            file.close().unwrap();
2959        }
2960        // Reopen the writer; open_append decodes the filtered index block from
2961        // disk, so the entry reflects exactly what was committed.
2962        {
2963            let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2964            let idx = w.dataset_index("v").unwrap();
2965            let entry = &w.datasets[idx]
2966                .chunked
2967                .as_ref()
2968                .unwrap()
2969                .filt_iblk
2970                .as_ref()
2971                .unwrap()
2972                .elements[0];
2973            assert_eq!(entry.filter_mask, 1, "filter_mask must round-trip to disk");
2974            assert_eq!(entry.nbytes, 16, "uncompressed chunk stored verbatim");
2975        }
2976        std::fs::remove_file(&path).ok();
2977    }
2978
2979    /// Reader honors a per-chunk filter_mask (EA): one chunk is stored
2980    /// compressed (mask 0), the next stored raw with deflate skipped (mask 1),
2981    /// in the same dataset. A correct reader skips deflate for chunk 1 only;
2982    /// ignoring the mask would feed raw bytes through inflate and corrupt them.
2983    #[cfg(feature = "deflate")]
2984    #[test]
2985    fn write_chunk_raw_ea_per_chunk_mask_roundtrip() {
2986        use crate::format::messages::filter::{apply_filters, FilterPipeline};
2987        let path = temp_path("wcr_ea_per_chunk_mask");
2988        let original: Vec<i32> = (0..8).collect();
2989        let pipeline = FilterPipeline::deflate(4);
2990        {
2991            let file = H5File::create(&path).unwrap();
2992            let ds = file
2993                .new_dataset::<i32>()
2994                .shape([0])
2995                .chunk(&[4])
2996                .max_shape(&[None])
2997                .deflate(4)
2998                .create("v")
2999                .unwrap();
3000            let raw0: Vec<u8> = original[0..4]
3001                .iter()
3002                .flat_map(|v| v.to_le_bytes())
3003                .collect();
3004            // chunk 0: compressed through the pipeline, mask 0.
3005            ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
3006                .unwrap();
3007            let raw1: Vec<u8> = original[4..8]
3008                .iter()
3009                .flat_map(|v| v.to_le_bytes())
3010                .collect();
3011            // chunk 1: stored uncompressed, mask 1 (deflate skipped).
3012            ds.write_chunk_raw(1, &raw1, 1).unwrap();
3013            ds.set_extent(&[8]).unwrap();
3014            file.close().unwrap();
3015        }
3016        {
3017            let file = H5File::open(&path).unwrap();
3018            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3019            assert_eq!(v, original);
3020        }
3021        std::fs::remove_file(&path).ok();
3022    }
3023
3024    /// Reader honors a per-chunk filter_mask (fixed array): same mixed
3025    /// compressed/raw chunks as the EA case, through the fixed-array index.
3026    #[cfg(feature = "deflate")]
3027    #[test]
3028    fn write_chunk_raw_fixed_array_per_chunk_mask_roundtrip() {
3029        use crate::format::messages::filter::{apply_filters, FilterPipeline};
3030        let path = temp_path("wcr_fa_per_chunk_mask");
3031        let original: Vec<i32> = (0..8).collect();
3032        let pipeline = FilterPipeline::deflate(4);
3033        {
3034            let file = H5File::create(&path).unwrap();
3035            let ds = file
3036                .new_dataset::<i32>()
3037                .shape([8])
3038                .chunk(&[4])
3039                .deflate(4)
3040                .create("v")
3041                .unwrap();
3042            let raw0: Vec<u8> = original[0..4]
3043                .iter()
3044                .flat_map(|v| v.to_le_bytes())
3045                .collect();
3046            ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
3047                .unwrap();
3048            let raw1: Vec<u8> = original[4..8]
3049                .iter()
3050                .flat_map(|v| v.to_le_bytes())
3051                .collect();
3052            ds.write_chunk_raw(1, &raw1, 1).unwrap();
3053            file.close().unwrap();
3054        }
3055        {
3056            let file = H5File::open(&path).unwrap();
3057            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
3058            assert_eq!(v, original);
3059        }
3060        std::fs::remove_file(&path).ok();
3061    }
3062
3063    /// An unfiltered chunk index has no slot for a stored size or mask, so a
3064    /// direct chunk write must be rejected rather than silently dropping them.
3065    #[test]
3066    fn write_chunk_raw_rejects_unfiltered() {
3067        let path = temp_path("wcr_unfiltered");
3068        let file = H5File::create(&path).unwrap();
3069        let ds = file
3070            .new_dataset::<i32>()
3071            .shape([0])
3072            .chunk(&[4])
3073            .max_shape(&[None])
3074            .create("v")
3075            .unwrap();
3076        let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
3077        assert!(
3078            err.to_string().contains("filtered dataset"),
3079            "expected a filtered-dataset error, got: {err}"
3080        );
3081        std::fs::remove_file(&path).ok();
3082    }
3083
3084    /// v2-B-tree-indexed datasets (two or more unlimited dimensions) do not
3085    /// support direct chunk writes.
3086    #[test]
3087    fn write_chunk_raw_rejects_btree_v2() {
3088        let path = temp_path("wcr_btree2");
3089        let file = H5File::create(&path).unwrap();
3090        let ds = file
3091            .new_dataset::<i32>()
3092            .shape([0, 0])
3093            .chunk(&[2, 2])
3094            .max_shape(&[None, None])
3095            .create("grid")
3096            .unwrap();
3097        let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
3098        assert!(
3099            err.to_string().contains("v2-B-tree"),
3100            "expected a v2-B-tree rejection, got: {err}"
3101        );
3102        std::fs::remove_file(&path).ok();
3103    }
3104
3105    /// A stored size that does not fit the index's chunk-size field must error
3106    /// (libhdf5 H5D_CHUNK_ENCODE_SIZE_CHECK) instead of truncating silently.
3107    /// A 4-byte chunk (chunk[1] of i32) has chunk_size_len = 2 (max 65535), so
3108    /// a 70000-byte stored chunk overflows it.
3109    #[cfg(feature = "deflate")]
3110    #[test]
3111    fn write_chunk_raw_rejects_oversized_chunk() {
3112        let path = temp_path("wcr_oversized");
3113        let file = H5File::create(&path).unwrap();
3114        let ds = file
3115            .new_dataset::<i32>()
3116            .shape([0])
3117            .chunk(&[1])
3118            .max_shape(&[None])
3119            .deflate(4)
3120            .create("v")
3121            .unwrap();
3122        let err = ds.write_chunk_raw(0, &vec![0u8; 70000], 0).unwrap_err();
3123        assert!(
3124            err.to_string().contains("does not fit"),
3125            "expected a chunk-size-field overflow error, got: {err}"
3126        );
3127        std::fs::remove_file(&path).ok();
3128    }
3129}