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