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 cannot_create_dataset_in_read_mode() {
1707        let path = temp_path("no_create_read");
1708
1709        {
1710            let _file = H5File::create(&path).unwrap();
1711        }
1712
1713        {
1714            let file = H5File::open(&path).unwrap();
1715            let result = file.new_dataset::<u8>().shape([4]).create("x");
1716            assert!(result.is_err());
1717        }
1718
1719        std::fs::remove_file(&path).ok();
1720    }
1721
1722    #[test]
1723    fn shape_accessor() {
1724        let path = temp_path("shape_acc");
1725
1726        let file = H5File::create(&path).unwrap();
1727        let ds = file
1728            .new_dataset::<f32>()
1729            .shape([5, 10, 3])
1730            .create("tensor")
1731            .unwrap();
1732        assert_eq!(ds.shape(), vec![5, 10, 3]);
1733
1734        std::fs::remove_file(&path).ok();
1735    }
1736
1737    #[test]
1738    fn slice_roundtrip_2d() {
1739        let path = temp_path("slice_2d");
1740
1741        // Create a 4x5 dataset, write full, then read a slice
1742        let data: Vec<i32> = (0..20).collect();
1743        {
1744            let file = H5File::create(&path).unwrap();
1745            let ds = file
1746                .new_dataset::<i32>()
1747                .shape([4, 5])
1748                .create("mat")
1749                .unwrap();
1750            ds.write_raw(&data).unwrap();
1751            file.close().unwrap();
1752        }
1753        {
1754            let file = H5File::open(&path).unwrap();
1755            let ds = file.dataset("mat").unwrap();
1756            // Read rows 1..3, cols 2..4 (2x2 slice)
1757            let slice = ds.read_slice::<i32>(&[1, 2], &[2, 2]).unwrap();
1758            // Row 1: [5,6,7,8,9] -> cols 2..4 = [7,8]
1759            // Row 2: [10,11,12,13,14] -> cols 2..4 = [12,13]
1760            assert_eq!(slice, vec![7, 8, 12, 13]);
1761        }
1762
1763        std::fs::remove_file(&path).ok();
1764    }
1765
1766    #[test]
1767    fn write_slice_2d() {
1768        let path = temp_path("write_slice_2d");
1769
1770        {
1771            let file = H5File::create(&path).unwrap();
1772            let ds = file
1773                .new_dataset::<f32>()
1774                .shape([3, 4])
1775                .create("data")
1776                .unwrap();
1777            ds.write_raw(&[0.0f32; 12]).unwrap();
1778            // Overwrite a 2x2 sub-region
1779            ds.write_slice(&[1, 1], &[2, 2], &[10.0f32, 20.0, 30.0, 40.0])
1780                .unwrap();
1781            file.close().unwrap();
1782        }
1783        {
1784            let file = H5File::open(&path).unwrap();
1785            let ds = file.dataset("data").unwrap();
1786            let full = ds.read_raw::<f32>().unwrap();
1787            // Row 0: [0,0,0,0]
1788            // Row 1: [0,10,20,0]
1789            // Row 2: [0,30,40,0]
1790            assert_eq!(
1791                full,
1792                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,]
1793            );
1794        }
1795
1796        std::fs::remove_file(&path).ok();
1797    }
1798
1799    #[test]
1800    fn write_slice_out_of_bounds_rejected() {
1801        let path = temp_path("write_slice_oob");
1802        let file = H5File::create(&path).unwrap();
1803        let ds = file.new_dataset::<i32>().shape([4]).create("d").unwrap();
1804        ds.write_raw(&[0i32; 4]).unwrap();
1805        // start 2 + count 6 = 8 > extent 4 -> must error, not corrupt.
1806        assert!(ds.write_slice(&[2], &[6], &[9i32; 6]).is_err());
1807        // An in-bounds slice still works.
1808        assert!(ds.write_slice(&[1], &[2], &[7i32, 8]).is_ok());
1809        std::fs::remove_file(&path).ok();
1810    }
1811
1812    #[test]
1813    fn duplicate_dataset_name_rejected() {
1814        let path = temp_path("dup_name");
1815        let file = H5File::create(&path).unwrap();
1816        let _ = file.new_dataset::<i32>().shape([2]).create("d").unwrap();
1817        assert!(file.new_dataset::<i32>().shape([2]).create("d").is_err());
1818        std::fs::remove_file(&path).ok();
1819    }
1820
1821    #[test]
1822    fn extend_cannot_shrink() {
1823        let path = temp_path("extend_shrink");
1824        let file = H5File::create(&path).unwrap();
1825        let ds = file
1826            .new_dataset::<i32>()
1827            .shape([0])
1828            .chunk(&[2])
1829            .max_shape(&[None])
1830            .create("d")
1831            .unwrap();
1832        ds.append(&[1i32, 2, 3, 4]).unwrap();
1833        // Shrinking below the written extent must be rejected.
1834        assert!(ds.extend(&[2]).is_err());
1835        // Growing is fine.
1836        assert!(ds.extend(&[6]).is_ok());
1837        std::fs::remove_file(&path).ok();
1838    }
1839
1840    #[test]
1841    fn attr_read_roundtrip() {
1842        use crate::types::VarLenUnicode;
1843        let path = temp_path("attr_read");
1844
1845        {
1846            let file = H5File::create(&path).unwrap();
1847            let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
1848            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1849            let a1 = ds
1850                .new_attr::<VarLenUnicode>()
1851                .shape(())
1852                .create("units")
1853                .unwrap();
1854            a1.write_string("meters").unwrap();
1855            let a2 = ds
1856                .new_attr::<VarLenUnicode>()
1857                .shape(())
1858                .create("desc")
1859                .unwrap();
1860            a2.write_string("test data").unwrap();
1861            file.close().unwrap();
1862        }
1863        {
1864            let file = H5File::open(&path).unwrap();
1865            let ds = file.dataset("data").unwrap();
1866
1867            let names = ds.attr_names().unwrap();
1868            assert!(names.contains(&"units".to_string()));
1869            assert!(names.contains(&"desc".to_string()));
1870
1871            let units = ds.attr("units").unwrap();
1872            assert_eq!(units.read_string().unwrap(), "meters");
1873
1874            let desc = ds.attr("desc").unwrap();
1875            assert_eq!(desc.read_string().unwrap(), "test data");
1876        }
1877
1878        std::fs::remove_file(&path).ok();
1879    }
1880
1881    #[test]
1882    fn type_mismatch_element_size() {
1883        let path = temp_path("type_mismatch");
1884
1885        {
1886            let file = H5File::create(&path).unwrap();
1887            let ds = file.new_dataset::<f64>().shape([4]).create("data").unwrap();
1888            ds.write_raw(&[1.0f64, 2.0, 3.0, 4.0]).unwrap();
1889            file.close().unwrap();
1890        }
1891
1892        {
1893            let file = H5File::open(&path).unwrap();
1894            let ds = file.dataset("data").unwrap();
1895            // Try to read as u8 (element_size = 1) from a f64 dataset (element_size = 8)
1896            let result = ds.read_raw::<u8>();
1897            assert!(result.is_err());
1898        }
1899
1900        std::fs::remove_file(&path).ok();
1901    }
1902
1903    #[test]
1904    fn dataset_survives_file_move() {
1905        let path = temp_path("ds_survives");
1906
1907        let ds = {
1908            let file = H5File::create(&path).unwrap();
1909            file.new_dataset::<u8>().shape([4]).create("x").unwrap()
1910        };
1911        // file is dropped here, but ds still holds Rc to the inner state
1912        ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1913        // The writer will finalize on drop of the last Rc
1914
1915        std::fs::remove_file(&path).ok();
1916    }
1917
1918    #[test]
1919    fn new_attr_scalar_string() {
1920        use crate::types::VarLenUnicode;
1921
1922        let path = temp_path("attr_scalar_string");
1923        {
1924            let file = H5File::create(&path).unwrap();
1925            let ds = file.new_dataset::<u8>().shape([4]).create("data").unwrap();
1926            ds.write_raw(&[1u8, 2, 3, 4]).unwrap();
1927
1928            let attr = ds
1929                .new_attr::<VarLenUnicode>()
1930                .shape(())
1931                .create("name")
1932                .unwrap();
1933            attr.write_scalar(&VarLenUnicode("test_value".to_string()))
1934                .unwrap();
1935
1936            file.close().unwrap();
1937        }
1938
1939        // Verify the file is still valid and readable
1940        {
1941            let file = H5File::open(&path).unwrap();
1942            let ds = file.dataset("data").unwrap();
1943            assert_eq!(ds.shape(), vec![4]);
1944            let readback = ds.read_raw::<u8>().unwrap();
1945            assert_eq!(readback, vec![1u8, 2, 3, 4]);
1946        }
1947
1948        std::fs::remove_file(&path).ok();
1949    }
1950
1951    #[test]
1952    fn all_numeric_types_roundtrip() {
1953        let path = temp_path("all_types");
1954
1955        {
1956            let file = H5File::create(&path).unwrap();
1957
1958            let ds = file.new_dataset::<u8>().shape([2]).create("u8").unwrap();
1959            ds.write_raw(&[1u8, 2]).unwrap();
1960
1961            let ds = file.new_dataset::<i8>().shape([2]).create("i8").unwrap();
1962            ds.write_raw(&[-1i8, 1]).unwrap();
1963
1964            let ds = file.new_dataset::<u16>().shape([2]).create("u16").unwrap();
1965            ds.write_raw(&[100u16, 200]).unwrap();
1966
1967            let ds = file.new_dataset::<i16>().shape([2]).create("i16").unwrap();
1968            ds.write_raw(&[-100i16, 100]).unwrap();
1969
1970            let ds = file.new_dataset::<u32>().shape([2]).create("u32").unwrap();
1971            ds.write_raw(&[1000u32, 2000]).unwrap();
1972
1973            let ds = file.new_dataset::<i32>().shape([2]).create("i32").unwrap();
1974            ds.write_raw(&[-1000i32, 1000]).unwrap();
1975
1976            let ds = file.new_dataset::<u64>().shape([2]).create("u64").unwrap();
1977            ds.write_raw(&[10000u64, 20000]).unwrap();
1978
1979            let ds = file.new_dataset::<i64>().shape([2]).create("i64").unwrap();
1980            ds.write_raw(&[-10000i64, 10000]).unwrap();
1981
1982            let ds = file.new_dataset::<f32>().shape([2]).create("f32").unwrap();
1983            ds.write_raw(&[1.5f32, 2.5]).unwrap();
1984
1985            let ds = file.new_dataset::<f64>().shape([2]).create("f64").unwrap();
1986            ds.write_raw(&[1.23456f64, 7.89012]).unwrap();
1987
1988            file.close().unwrap();
1989        }
1990
1991        {
1992            let file = H5File::open(&path).unwrap();
1993
1994            assert_eq!(
1995                file.dataset("u8").unwrap().read_raw::<u8>().unwrap(),
1996                vec![1u8, 2]
1997            );
1998            assert_eq!(
1999                file.dataset("i8").unwrap().read_raw::<i8>().unwrap(),
2000                vec![-1i8, 1]
2001            );
2002            assert_eq!(
2003                file.dataset("u16").unwrap().read_raw::<u16>().unwrap(),
2004                vec![100u16, 200]
2005            );
2006            assert_eq!(
2007                file.dataset("i16").unwrap().read_raw::<i16>().unwrap(),
2008                vec![-100i16, 100]
2009            );
2010            assert_eq!(
2011                file.dataset("u32").unwrap().read_raw::<u32>().unwrap(),
2012                vec![1000u32, 2000]
2013            );
2014            assert_eq!(
2015                file.dataset("i32").unwrap().read_raw::<i32>().unwrap(),
2016                vec![-1000i32, 1000]
2017            );
2018            assert_eq!(
2019                file.dataset("u64").unwrap().read_raw::<u64>().unwrap(),
2020                vec![10000u64, 20000]
2021            );
2022            assert_eq!(
2023                file.dataset("i64").unwrap().read_raw::<i64>().unwrap(),
2024                vec![-10000i64, 10000]
2025            );
2026            assert_eq!(
2027                file.dataset("f32").unwrap().read_raw::<f32>().unwrap(),
2028                vec![1.5f32, 2.5]
2029            );
2030            assert_eq!(
2031                file.dataset("f64").unwrap().read_raw::<f64>().unwrap(),
2032                vec![1.23456f64, 7.89012]
2033            );
2034        }
2035
2036        std::fs::remove_file(&path).ok();
2037    }
2038
2039    #[test]
2040    fn append_chunked_roundtrip() {
2041        let path = temp_path("append_chunked");
2042
2043        {
2044            let file = H5File::create(&path).unwrap();
2045            let ds = file
2046                .new_dataset::<f64>()
2047                .shape([0, 3])
2048                .chunk(&[1, 3])
2049                .max_shape(&[None, Some(3)])
2050                .create("data")
2051                .unwrap();
2052
2053            // Append one frame
2054            ds.append(&[1.0f64, 2.0, 3.0]).unwrap();
2055            // Append two frames at once
2056            ds.append(&[4.0f64, 5.0, 6.0, 7.0, 8.0, 9.0]).unwrap();
2057
2058            file.close().unwrap();
2059        }
2060
2061        {
2062            let file = H5File::open(&path).unwrap();
2063            let ds = file.dataset("data").unwrap();
2064            assert_eq!(ds.shape(), vec![3, 3]);
2065            let all = ds.read_raw::<f64>().unwrap();
2066            assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
2067        }
2068
2069        std::fs::remove_file(&path).ok();
2070    }
2071
2072    #[test]
2073    fn append_1d_chunked() {
2074        let path = temp_path("append_1d");
2075
2076        {
2077            let file = H5File::create(&path).unwrap();
2078            let ds = file
2079                .new_dataset::<i32>()
2080                .shape([0])
2081                .chunk(&[4])
2082                .max_shape(&[None])
2083                .create("values")
2084                .unwrap();
2085
2086            ds.append(&[10i32, 20, 30]).unwrap(); // partial chunk
2087            ds.append(&[40i32]).unwrap(); // fills chunk boundary
2088            ds.append(&[50i32, 60, 70, 80]).unwrap(); // full chunk
2089
2090            file.close().unwrap();
2091        }
2092
2093        {
2094            let file = H5File::open(&path).unwrap();
2095            let ds = file.dataset("values").unwrap();
2096            assert_eq!(ds.shape(), vec![8]);
2097            let all = ds.read_raw::<i32>().unwrap();
2098            assert_eq!(all, vec![10, 20, 30, 40, 50, 60, 70, 80]);
2099        }
2100
2101        std::fs::remove_file(&path).ok();
2102    }
2103
2104    #[test]
2105    fn append_partial_chunk_flushed_on_close() {
2106        let path = temp_path("append_partial_close");
2107
2108        {
2109            let file = H5File::create(&path).unwrap();
2110            let ds = file
2111                .new_dataset::<f64>()
2112                .shape([0])
2113                .chunk(&[4])
2114                .max_shape(&[None])
2115                .create("vals")
2116                .unwrap();
2117
2118            // Append 5 elements: chunk 0 = full [1,2,3,4], chunk 1 = partial [5,0,0,0]
2119            ds.append(&[1.0f64, 2.0, 3.0, 4.0, 5.0]).unwrap();
2120            file.close().unwrap();
2121        }
2122
2123        {
2124            let file = H5File::open(&path).unwrap();
2125            let ds = file.dataset("vals").unwrap();
2126            assert_eq!(ds.shape(), vec![5]);
2127            let all = ds.read_raw::<f64>().unwrap();
2128            // The full dataset is 2 chunks * 4 = 8 elements; shape says 5
2129            // read_raw reads total shape elements
2130            assert_eq!(all.len(), 5);
2131            assert_eq!(all, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
2132        }
2133
2134        std::fs::remove_file(&path).ok();
2135    }
2136
2137    #[cfg(feature = "deflate")]
2138    #[test]
2139    fn vlen_append_after_reopen_filtered() {
2140        // Reopen + append into a partially-written *compressed* vlen chunk
2141        // (index-block chunk). Exercises filtered-index-block reconstruction
2142        // in open_append plus filtered read-modify-write.
2143        let path = temp_path("vlen_reopen_filtered");
2144        {
2145            let file = H5File::create(&path).unwrap();
2146            file.create_appendable_vlen_dataset(
2147                "strs",
2148                4,
2149                Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2150            )
2151            .unwrap();
2152            file.append_vlen_strings("strs", &["alpha", "beta", "gamma"])
2153                .unwrap();
2154            file.close().unwrap();
2155        }
2156        {
2157            let file = H5File::open_rw(&path).unwrap();
2158            file.append_vlen_strings("strs", &["delta"]).unwrap();
2159            file.close().unwrap();
2160        }
2161        {
2162            let file = H5File::open(&path).unwrap();
2163            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2164            assert_eq!(
2165                got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
2166                vec!["alpha", "beta", "gamma", "delta"]
2167            );
2168        }
2169        std::fs::remove_file(&path).ok();
2170    }
2171
2172    #[test]
2173    fn vlen_append_after_reopen_data_block() {
2174        // Reopen + append into a partial chunk that lives in an extensible-
2175        // array *data block* (chunk index >= idx_blk_elmts). Exercises
2176        // data-block resolution in read_chunk_if_present and write_chunk.
2177        let path = temp_path("vlen_reopen_datablk");
2178        let labels: Vec<String> = (0..9).map(|i| format!("s{i}")).collect();
2179        {
2180            let file = H5File::create(&path).unwrap();
2181            file.create_appendable_vlen_dataset("strs", 2, None)
2182                .unwrap();
2183            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2184            file.append_vlen_strings("strs", &refs).unwrap();
2185            file.close().unwrap();
2186        }
2187        {
2188            let file = H5File::open_rw(&path).unwrap();
2189            file.append_vlen_strings("strs", &["s9"]).unwrap();
2190            file.close().unwrap();
2191        }
2192        {
2193            let file = H5File::open(&path).unwrap();
2194            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2195            let want: Vec<String> = (0..10).map(|i| format!("s{i}")).collect();
2196            assert_eq!(got, want);
2197        }
2198        std::fs::remove_file(&path).ok();
2199    }
2200
2201    #[test]
2202    fn vlen_append_after_reopen_super_block() {
2203        // Reopen + append into a partial chunk whose index falls in an
2204        // extensible-array *super block* (chunk index 244 with the default
2205        // EA geometry: idx_blk_elmts=4, data_blk_min_elmts=16,
2206        // sup_blk_min_data_ptrs=4 -> chunks 0..=243 are reached via the
2207        // index block or its direct data blocks, so chunk 244 is reached
2208        // via a super block read from disk). Exercises the ViaSblk branch
2209        // of read_chunk_if_present.
2210        let path = temp_path("vlen_reopen_super");
2211        // 489 strings, chunk size 2 -> chunk 244 holds one string only
2212        // (partially filled) and is flushed to disk on close.
2213        let labels: Vec<String> = (0..489).map(|i| format!("v{i}")).collect();
2214        {
2215            let file = H5File::create(&path).unwrap();
2216            file.create_appendable_vlen_dataset("strs", 2, None)
2217                .unwrap();
2218            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2219            file.append_vlen_strings("strs", &refs).unwrap();
2220            file.close().unwrap();
2221        }
2222        {
2223            let file = H5File::open_rw(&path).unwrap();
2224            file.append_vlen_strings("strs", &["v489"]).unwrap();
2225            file.close().unwrap();
2226        }
2227        {
2228            let file = H5File::open(&path).unwrap();
2229            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2230            let want: Vec<String> = (0..490).map(|i| format!("v{i}")).collect();
2231            assert_eq!(got, want);
2232        }
2233        std::fs::remove_file(&path).ok();
2234    }
2235
2236    #[cfg(feature = "deflate")]
2237    #[test]
2238    fn vlen_append_after_reopen_filtered_data_block() {
2239        // The hardest path: compressed + chunk in a data block + partial
2240        // read-modify-write across a reopen.
2241        let path = temp_path("vlen_reopen_filt_datablk");
2242        let labels: Vec<String> = (0..9).map(|i| format!("item{i:02}")).collect();
2243        {
2244            let file = H5File::create(&path).unwrap();
2245            file.create_appendable_vlen_dataset(
2246                "strs",
2247                2,
2248                Some(crate::format::messages::filter::FilterPipeline::deflate(6)),
2249            )
2250            .unwrap();
2251            let refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
2252            file.append_vlen_strings("strs", &refs).unwrap();
2253            file.close().unwrap();
2254        }
2255        {
2256            let file = H5File::open_rw(&path).unwrap();
2257            file.append_vlen_strings("strs", &["item09"]).unwrap();
2258            file.close().unwrap();
2259        }
2260        {
2261            let file = H5File::open(&path).unwrap();
2262            let got = file.dataset("strs").unwrap().read_vlen_strings().unwrap();
2263            let want: Vec<String> = (0..10).map(|i| format!("item{i:02}")).collect();
2264            assert_eq!(got, want);
2265        }
2266        std::fs::remove_file(&path).ok();
2267    }
2268
2269    #[test]
2270    fn group_nx_class_attribute_roundtrip() {
2271        // Non-root groups carry attributes (NeXus `NX_class`) in their
2272        // own object header, and the reader reads them back by path.
2273        let path = temp_path("group_nx_class");
2274        {
2275            let file = H5File::create(&path).unwrap();
2276            let entry = file.create_group("entry").unwrap();
2277            entry.set_attr_string("NX_class", "NXentry").unwrap();
2278            let det = entry.create_group("detector").unwrap();
2279            det.set_attr_string("NX_class", "NXdetector").unwrap();
2280            det.set_attr_numeric("frame_count", &7i32).unwrap();
2281            det.new_dataset::<f32>()
2282                .shape([4])
2283                .create("data")
2284                .unwrap()
2285                .write_raw(&[1.0f32; 4])
2286                .unwrap();
2287            file.close().unwrap();
2288        }
2289        {
2290            let file = H5File::open(&path).unwrap();
2291            let entry = file.root_group().group("entry").unwrap();
2292            assert_eq!(entry.attr_string("NX_class").unwrap(), "NXentry");
2293            let det = entry.group("detector").unwrap();
2294            assert_eq!(det.attr_string("NX_class").unwrap(), "NXdetector");
2295            let names = det.attr_names().unwrap();
2296            assert!(names.contains(&"NX_class".to_string()));
2297            assert!(names.contains(&"frame_count".to_string()));
2298        }
2299        std::fs::remove_file(&path).ok();
2300    }
2301
2302    #[test]
2303    fn ea_super_block_roundtrip() {
2304        // 2000 chunks span several extensible-array super blocks. Before
2305        // super-block support the writer errored at chunk index 228.
2306        let path = temp_path("ea_super_rt");
2307        {
2308            let file = H5File::create(&path).unwrap();
2309            let ds = file
2310                .new_dataset::<i32>()
2311                .shape([0])
2312                .chunk(&[1])
2313                .max_shape(&[None])
2314                .create("v")
2315                .unwrap();
2316            ds.append(&(0..2000).collect::<Vec<i32>>()).unwrap();
2317            file.close().unwrap();
2318        }
2319        {
2320            let file = H5File::open(&path).unwrap();
2321            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2322            assert_eq!(v.len(), 2000);
2323            assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2324        }
2325        std::fs::remove_file(&path).ok();
2326    }
2327
2328    #[cfg(feature = "deflate")]
2329    #[test]
2330    fn ea_filtered_super_block_roundtrip() {
2331        // Compressed chunks across super blocks.
2332        let path = temp_path("ea_filt_super");
2333        {
2334            let file = H5File::create(&path).unwrap();
2335            let ds = file
2336                .new_dataset::<i32>()
2337                .shape([0])
2338                .chunk(&[1])
2339                .max_shape(&[None])
2340                .deflate(4)
2341                .create("v")
2342                .unwrap();
2343            ds.append(&(0..600).collect::<Vec<i32>>()).unwrap();
2344            file.close().unwrap();
2345        }
2346        {
2347            let file = H5File::open(&path).unwrap();
2348            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2349            assert_eq!(v, (0..600).collect::<Vec<i32>>());
2350        }
2351        std::fs::remove_file(&path).ok();
2352    }
2353
2354    #[test]
2355    fn ea_super_block_open_append() {
2356        // Reopen a dataset and append chunks that fall in super blocks.
2357        let path = temp_path("ea_super_append");
2358        {
2359            let file = H5File::create(&path).unwrap();
2360            let ds = file
2361                .new_dataset::<i32>()
2362                .shape([0])
2363                .chunk(&[1])
2364                .max_shape(&[None])
2365                .create("v")
2366                .unwrap();
2367            ds.append(&(0..300).collect::<Vec<i32>>()).unwrap();
2368            file.close().unwrap();
2369        }
2370        {
2371            let mut w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2372            let idx = w.dataset_index("v").unwrap();
2373            for c in 300..900u64 {
2374                w.write_chunk(idx, c, &(c as i32).to_le_bytes()).unwrap();
2375            }
2376            w.extend_dataset(idx, &[900]).unwrap();
2377            w.close().unwrap();
2378        }
2379        {
2380            let file = H5File::open(&path).unwrap();
2381            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2382            assert_eq!(v.len(), 900);
2383            assert!(v.iter().enumerate().all(|(i, &x)| x == i as i32));
2384        }
2385        std::fs::remove_file(&path).ok();
2386    }
2387
2388    #[test]
2389    fn btree_v2_multi_unlimited_roundtrip() {
2390        // A dataset with two unlimited dimensions uses the v2 B-tree chunk
2391        // index; chunks are written by grid coordinates with write_chunk_at.
2392        let path = temp_path("bt2_multi");
2393        {
2394            let file = H5File::create(&path).unwrap();
2395            let ds = file
2396                .new_dataset::<i32>()
2397                .shape([0, 0])
2398                .chunk(&[2, 2])
2399                .max_shape(&[None, None])
2400                .create("grid")
2401                .unwrap();
2402            assert!(ds.is_chunked());
2403            // 4x4 logical grid, value[r][c] = r*4 + c, in 2x2 chunks.
2404            for cr in 0..2usize {
2405                for cc in 0..2usize {
2406                    let mut bytes = Vec::new();
2407                    for i in 0..2usize {
2408                        for j in 0..2usize {
2409                            let v = ((cr * 2 + i) * 4 + (cc * 2 + j)) as i32;
2410                            bytes.extend_from_slice(&v.to_le_bytes());
2411                        }
2412                    }
2413                    ds.write_chunk_at(&[cr, cc], &bytes).unwrap();
2414                }
2415            }
2416            file.close().unwrap();
2417        }
2418        {
2419            let file = H5File::open(&path).unwrap();
2420            let ds = file.dataset("grid").unwrap();
2421            assert_eq!(ds.shape(), vec![4, 4]);
2422            assert_eq!(ds.read_raw::<i32>().unwrap(), (0..16).collect::<Vec<i32>>());
2423        }
2424        std::fs::remove_file(&path).ok();
2425    }
2426
2427    #[test]
2428    fn subframe_chunking_roundtrip() {
2429        // A chunk smaller than a frame: shape [N,8,8], chunk [1,4,4], so each
2430        // frame is tiled into a 2x2 grid of 4x4 chunks. write_chunk_at takes
2431        // the chunk-grid coordinates.
2432        let path = temp_path("subframe");
2433        {
2434            let file = H5File::create(&path).unwrap();
2435            let ds = file
2436                .new_dataset::<i32>()
2437                .shape([0, 8, 8])
2438                .chunk(&[1, 4, 4])
2439                .max_shape(&[None, Some(8), Some(8)])
2440                .create("v")
2441                .unwrap();
2442            for f in 0..3usize {
2443                for cr in 0..2usize {
2444                    for cc in 0..2usize {
2445                        let mut bytes = Vec::new();
2446                        for i in 0..4usize {
2447                            for j in 0..4usize {
2448                                let v = (f * 64 + (cr * 4 + i) * 8 + (cc * 4 + j)) as i32;
2449                                bytes.extend_from_slice(&v.to_le_bytes());
2450                            }
2451                        }
2452                        ds.write_chunk_at(&[f, cr, cc], &bytes).unwrap();
2453                    }
2454                }
2455            }
2456            file.close().unwrap();
2457        }
2458        {
2459            let file = H5File::open(&path).unwrap();
2460            let ds = file.dataset("v").unwrap();
2461            assert_eq!(ds.shape(), vec![3, 8, 8]);
2462            assert_eq!(
2463                ds.read_raw::<i32>().unwrap(),
2464                (0..192).collect::<Vec<i32>>()
2465            );
2466        }
2467        std::fs::remove_file(&path).ok();
2468    }
2469
2470    #[test]
2471    fn fill_value_contiguous_roundtrip() {
2472        let path = temp_path("fill_value_contig");
2473        {
2474            let file = H5File::create(&path).unwrap();
2475            let ds = file
2476                .new_dataset::<f32>()
2477                .shape([4])
2478                .fill_value(2.5f32)
2479                .create("data")
2480                .unwrap();
2481            ds.write_raw(&[1.0f32, 2.0, 3.0, 4.0]).unwrap();
2482            file.close().unwrap();
2483        }
2484        // open_append decodes the fill-value message back from the header.
2485        {
2486            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2487            let idx = writer.dataset_index("data").unwrap();
2488            assert_eq!(
2489                writer.datasets[idx].fill_value,
2490                Some(2.5f32.to_le_bytes().to_vec())
2491            );
2492        }
2493        // Data still reads back correctly.
2494        {
2495            let file = H5File::open(&path).unwrap();
2496            let ds = file.dataset("data").unwrap();
2497            assert_eq!(ds.read_raw::<f32>().unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
2498        }
2499        std::fs::remove_file(&path).ok();
2500    }
2501
2502    #[test]
2503    fn fill_value_chunked_roundtrip() {
2504        let path = temp_path("fill_value_chunked");
2505        {
2506            let file = H5File::create(&path).unwrap();
2507            let ds = file
2508                .new_dataset::<i32>()
2509                .shape([0])
2510                .chunk(&[4])
2511                .max_shape(&[None])
2512                .fill_value(-7i32)
2513                .create("vals")
2514                .unwrap();
2515            ds.append(&[1i32, 2, 3, 4]).unwrap();
2516            file.close().unwrap();
2517        }
2518        {
2519            let writer = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2520            let idx = writer.dataset_index("vals").unwrap();
2521            assert_eq!(
2522                writer.datasets[idx].fill_value,
2523                Some((-7i32).to_le_bytes().to_vec())
2524            );
2525        }
2526        std::fs::remove_file(&path).ok();
2527    }
2528
2529    #[test]
2530    fn fill_value_read_missing_chunks() {
2531        // A chunked dataset with chunk 1 left unwritten must read that
2532        // gap back as the user-defined fill value, not zero.
2533        fn i32_bytes(vals: &[i32]) -> Vec<u8> {
2534            vals.iter().flat_map(|v| v.to_le_bytes()).collect()
2535        }
2536        let path = temp_path("fill_value_read_missing");
2537        {
2538            let file = H5File::create(&path).unwrap();
2539            let ds = file
2540                .new_dataset::<i32>()
2541                .shape([0])
2542                .chunk(&[2])
2543                .max_shape(&[None])
2544                .fill_value(-1i32)
2545                .create("vals")
2546                .unwrap();
2547            // chunk 0 = [10,20]; chunk 1 unwritten; chunk 2 = [50,60].
2548            ds.write_chunk(0, &i32_bytes(&[10, 20])).unwrap();
2549            ds.write_chunk(2, &i32_bytes(&[50, 60])).unwrap();
2550            ds.extend(&[6]).unwrap();
2551            file.close().unwrap();
2552        }
2553        {
2554            let file = H5File::open(&path).unwrap();
2555            let ds = file.dataset("vals").unwrap();
2556            let all = ds.read_raw::<i32>().unwrap();
2557            assert_eq!(all, vec![10, 20, -1, -1, 50, 60]);
2558        }
2559        std::fs::remove_file(&path).ok();
2560    }
2561
2562    #[test]
2563    fn fill_value_partial_chunk_padded_with_fill() {
2564        // A partial trailing chunk flushed at close must pad its unwritten
2565        // tail with the fill value. That pad sits beyond the logical shape,
2566        // so it is verified by scanning the on-disk chunk bytes directly.
2567        let path = temp_path("fill_value_partial_pad");
2568        {
2569            let file = H5File::create(&path).unwrap();
2570            let ds = file
2571                .new_dataset::<i32>()
2572                .shape([0])
2573                .chunk(&[4])
2574                .max_shape(&[None])
2575                .fill_value(-9i32)
2576                .create("vals")
2577                .unwrap();
2578            // 3 of 4 frames -> flushed as a partial chunk on close.
2579            ds.append(&[1i32, 2, 3]).unwrap();
2580            file.close().unwrap();
2581        }
2582        let bytes = std::fs::read(&path).unwrap();
2583        // Locate the chunk: i32 LE of [1, 2, 3] written contiguously.
2584        let needle: Vec<u8> = [1i32, 2, 3].iter().flat_map(|v| v.to_le_bytes()).collect();
2585        let pos = bytes
2586            .windows(needle.len())
2587            .position(|w| w == needle)
2588            .expect("chunk data [1,2,3] not found in file");
2589        let pad = &bytes[pos + needle.len()..pos + needle.len() + 4];
2590        assert_eq!(
2591            pad,
2592            &(-9i32).to_le_bytes(),
2593            "partial chunk tail must be padded with fill value -9, got {:?}",
2594            pad
2595        );
2596        std::fs::remove_file(&path).ok();
2597    }
2598
2599    #[test]
2600    fn vlen_append_after_reopen_preserves_existing() {
2601        // Reopening and appending into a partially-written vlen chunk must
2602        // read-modify-write: the strings already on disk must survive.
2603        let path = temp_path("vlen_append_reopen");
2604        {
2605            let file = H5File::create(&path).unwrap();
2606            file.create_appendable_vlen_dataset("strs", 4, None)
2607                .unwrap();
2608            // 3 of 4 frames -> flushed as a partial chunk on close.
2609            file.append_vlen_strings("strs", &["a", "b", "c"]).unwrap();
2610            file.close().unwrap();
2611        }
2612        {
2613            // Append a 4th string -> partial-chunk write into chunk 0.
2614            let file = H5File::open_rw(&path).unwrap();
2615            file.append_vlen_strings("strs", &["d"]).unwrap();
2616            file.close().unwrap();
2617        }
2618        {
2619            let file = H5File::open(&path).unwrap();
2620            let ds = file.dataset("strs").unwrap();
2621            let got = ds.read_vlen_strings().unwrap();
2622            assert_eq!(
2623                got.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
2624                vec!["a", "b", "c", "d"]
2625            );
2626        }
2627        std::fs::remove_file(&path).ok();
2628    }
2629
2630    #[test]
2631    fn fill_value_size_mismatch_errors() {
2632        let path = temp_path("fill_value_mismatch");
2633        let mut writer = crate::io::writer::Hdf5Writer::create(&path).unwrap();
2634        let dt = <f64 as crate::types::H5Type>::hdf5_type();
2635        let idx = writer.create_dataset("d", dt, &[4u64]).unwrap();
2636        // f64 element size is 8; a 4-byte fill value must be rejected.
2637        assert!(writer.set_dataset_fill_value(idx, vec![0u8; 4]).is_err());
2638        // The correct width succeeds.
2639        writer.set_dataset_fill_value(idx, vec![0u8; 8]).unwrap();
2640        writer.close().unwrap();
2641        std::fs::remove_file(&path).ok();
2642    }
2643
2644    #[test]
2645    fn datatype_exposes_class_sign_and_byteorder() {
2646        // The byte width alone cannot tell u8 from i8 (both 1 byte) or i32
2647        // from f32 (both 4 bytes). datatype() must report the real class and
2648        // signedness so a reader does not have to guess from element_size.
2649        use crate::format::messages::datatype::{ByteOrder, DatatypeMessage};
2650
2651        let path = temp_path("datatype_accessor");
2652        {
2653            let file = H5File::create(&path).unwrap();
2654            file.new_dataset::<u8>().shape([3]).create("u8d").unwrap();
2655            file.new_dataset::<i8>().shape([3]).create("i8d").unwrap();
2656            file.new_dataset::<i32>().shape([3]).create("i32d").unwrap();
2657            file.new_dataset::<f32>().shape([3]).create("f32d").unwrap();
2658            file.close().unwrap();
2659        }
2660
2661        let file = H5File::open(&path).unwrap();
2662
2663        match file.dataset("u8d").unwrap().datatype().unwrap() {
2664            DatatypeMessage::FixedPoint {
2665                size,
2666                signed,
2667                byte_order,
2668                ..
2669            } => {
2670                assert_eq!(size, 1);
2671                assert!(!signed, "u8 must be unsigned");
2672                assert_eq!(byte_order, ByteOrder::LittleEndian);
2673            }
2674            other => panic!("expected FixedPoint for u8, got {other:?}"),
2675        }
2676
2677        match file.dataset("i8d").unwrap().datatype().unwrap() {
2678            DatatypeMessage::FixedPoint { size, signed, .. } => {
2679                assert_eq!(size, 1);
2680                assert!(signed, "i8 must be signed");
2681            }
2682            other => panic!("expected FixedPoint for i8, got {other:?}"),
2683        }
2684
2685        match file.dataset("i32d").unwrap().datatype().unwrap() {
2686            DatatypeMessage::FixedPoint { size, signed, .. } => {
2687                assert_eq!(size, 4);
2688                assert!(signed, "i32 must be signed");
2689            }
2690            other => panic!("expected FixedPoint for i32, got {other:?}"),
2691        }
2692
2693        match file.dataset("f32d").unwrap().datatype().unwrap() {
2694            DatatypeMessage::FloatingPoint { size, .. } => assert_eq!(size, 4),
2695            other => panic!("expected FloatingPoint for f32, got {other:?}"),
2696        }
2697
2698        std::fs::remove_file(&path).ok();
2699    }
2700
2701    #[test]
2702    fn datatype_in_write_mode_errors() {
2703        let path = temp_path("datatype_write_mode");
2704        let file = H5File::create(&path).unwrap();
2705        let ds = file.new_dataset::<f32>().shape([4]).create("d").unwrap();
2706        assert!(ds.datatype().is_err());
2707        std::fs::remove_file(&path).ok();
2708    }
2709
2710    // --- write_chunk_raw (HDF5 direct chunk write) ---------------------------
2711
2712    /// Extensible-array path: pre-compress with the dataset's pipeline, write
2713    /// the bytes verbatim via write_chunk_raw (filter_mask = 0), and confirm
2714    /// the data round-trips through the reader unchanged.
2715    #[cfg(feature = "deflate")]
2716    #[test]
2717    fn write_chunk_raw_ea_roundtrip_mask0() {
2718        use crate::format::messages::filter::{apply_filters, FilterPipeline};
2719        let path = temp_path("wcr_ea_mask0");
2720        let original: Vec<i32> = (0..12).collect();
2721        {
2722            let file = H5File::create(&path).unwrap();
2723            let ds = file
2724                .new_dataset::<i32>()
2725                .shape([0])
2726                .chunk(&[4])
2727                .max_shape(&[None])
2728                .deflate(4)
2729                .create("v")
2730                .unwrap();
2731            assert!(ds.is_chunked());
2732            let pipeline = FilterPipeline::deflate(4);
2733            for c in 0..3usize {
2734                let raw: Vec<u8> = original[c * 4..c * 4 + 4]
2735                    .iter()
2736                    .flat_map(|v| v.to_le_bytes())
2737                    .collect();
2738                let compressed = apply_filters(&pipeline, &raw).unwrap();
2739                ds.write_chunk_raw(c, &compressed, 0).unwrap();
2740            }
2741            ds.set_extent(&[12]).unwrap();
2742            file.close().unwrap();
2743        }
2744        {
2745            let file = H5File::open(&path).unwrap();
2746            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2747            assert_eq!(v, original);
2748        }
2749        std::fs::remove_file(&path).ok();
2750    }
2751
2752    /// Fixed-array path (all dimensions bounded): same verbatim write through
2753    /// the linear-index dispatch, round-tripped through the reader.
2754    #[cfg(feature = "deflate")]
2755    #[test]
2756    fn write_chunk_raw_fixed_array_roundtrip_mask0() {
2757        use crate::format::messages::filter::{apply_filters, FilterPipeline};
2758        let path = temp_path("wcr_fa_mask0");
2759        let original: Vec<i32> = (0..12).collect();
2760        {
2761            let file = H5File::create(&path).unwrap();
2762            let ds = file
2763                .new_dataset::<i32>()
2764                .shape([12])
2765                .chunk(&[4])
2766                .deflate(4)
2767                .create("v")
2768                .unwrap();
2769            assert!(ds.is_chunked());
2770            let pipeline = FilterPipeline::deflate(4);
2771            for c in 0..3usize {
2772                let raw: Vec<u8> = original[c * 4..c * 4 + 4]
2773                    .iter()
2774                    .flat_map(|v| v.to_le_bytes())
2775                    .collect();
2776                let compressed = apply_filters(&pipeline, &raw).unwrap();
2777                ds.write_chunk_raw(c, &compressed, 0).unwrap();
2778            }
2779            file.close().unwrap();
2780        }
2781        {
2782            let file = H5File::open(&path).unwrap();
2783            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2784            assert_eq!(v, original);
2785        }
2786        std::fs::remove_file(&path).ok();
2787    }
2788
2789    /// The caller-supplied filter_mask must reach the on-disk filtered index
2790    /// entry (not be hardcoded to 0). Store one chunk uncompressed in a
2791    /// filtered dataset with mask = 1 (deflate skipped), then reopen and decode
2792    /// the extensible-array filtered entry to read the mask back at the format
2793    /// level (independent of the data reader's mask handling).
2794    #[cfg(feature = "deflate")]
2795    #[test]
2796    fn write_chunk_raw_records_filter_mask() {
2797        let path = temp_path("wcr_records_mask");
2798        let raw: Vec<u8> = [10i32, 20, 30, 40]
2799            .iter()
2800            .flat_map(|v| v.to_le_bytes())
2801            .collect();
2802        assert_eq!(raw.len(), 16);
2803        {
2804            let file = H5File::create(&path).unwrap();
2805            let ds = file
2806                .new_dataset::<i32>()
2807                .shape([0])
2808                .chunk(&[4])
2809                .max_shape(&[None])
2810                .deflate(4)
2811                .create("v")
2812                .unwrap();
2813            // mask = 1: bit 0 set => filter 0 (deflate) was skipped, so the
2814            // chunk is stored uncompressed (its raw bytes).
2815            ds.write_chunk_raw(0, &raw, 1).unwrap();
2816            ds.set_extent(&[4]).unwrap();
2817            file.close().unwrap();
2818        }
2819        // Reopen the writer; open_append decodes the filtered index block from
2820        // disk, so the entry reflects exactly what was committed.
2821        {
2822            let w = crate::io::writer::Hdf5Writer::open_append(&path).unwrap();
2823            let idx = w.dataset_index("v").unwrap();
2824            let entry = &w.datasets[idx]
2825                .chunked
2826                .as_ref()
2827                .unwrap()
2828                .filt_iblk
2829                .as_ref()
2830                .unwrap()
2831                .elements[0];
2832            assert_eq!(entry.filter_mask, 1, "filter_mask must round-trip to disk");
2833            assert_eq!(entry.nbytes, 16, "uncompressed chunk stored verbatim");
2834        }
2835        std::fs::remove_file(&path).ok();
2836    }
2837
2838    /// Reader honors a per-chunk filter_mask (EA): one chunk is stored
2839    /// compressed (mask 0), the next stored raw with deflate skipped (mask 1),
2840    /// in the same dataset. A correct reader skips deflate for chunk 1 only;
2841    /// ignoring the mask would feed raw bytes through inflate and corrupt them.
2842    #[cfg(feature = "deflate")]
2843    #[test]
2844    fn write_chunk_raw_ea_per_chunk_mask_roundtrip() {
2845        use crate::format::messages::filter::{apply_filters, FilterPipeline};
2846        let path = temp_path("wcr_ea_per_chunk_mask");
2847        let original: Vec<i32> = (0..8).collect();
2848        let pipeline = FilterPipeline::deflate(4);
2849        {
2850            let file = H5File::create(&path).unwrap();
2851            let ds = file
2852                .new_dataset::<i32>()
2853                .shape([0])
2854                .chunk(&[4])
2855                .max_shape(&[None])
2856                .deflate(4)
2857                .create("v")
2858                .unwrap();
2859            let raw0: Vec<u8> = original[0..4]
2860                .iter()
2861                .flat_map(|v| v.to_le_bytes())
2862                .collect();
2863            // chunk 0: compressed through the pipeline, mask 0.
2864            ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
2865                .unwrap();
2866            let raw1: Vec<u8> = original[4..8]
2867                .iter()
2868                .flat_map(|v| v.to_le_bytes())
2869                .collect();
2870            // chunk 1: stored uncompressed, mask 1 (deflate skipped).
2871            ds.write_chunk_raw(1, &raw1, 1).unwrap();
2872            ds.set_extent(&[8]).unwrap();
2873            file.close().unwrap();
2874        }
2875        {
2876            let file = H5File::open(&path).unwrap();
2877            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2878            assert_eq!(v, original);
2879        }
2880        std::fs::remove_file(&path).ok();
2881    }
2882
2883    /// Reader honors a per-chunk filter_mask (fixed array): same mixed
2884    /// compressed/raw chunks as the EA case, through the fixed-array index.
2885    #[cfg(feature = "deflate")]
2886    #[test]
2887    fn write_chunk_raw_fixed_array_per_chunk_mask_roundtrip() {
2888        use crate::format::messages::filter::{apply_filters, FilterPipeline};
2889        let path = temp_path("wcr_fa_per_chunk_mask");
2890        let original: Vec<i32> = (0..8).collect();
2891        let pipeline = FilterPipeline::deflate(4);
2892        {
2893            let file = H5File::create(&path).unwrap();
2894            let ds = file
2895                .new_dataset::<i32>()
2896                .shape([8])
2897                .chunk(&[4])
2898                .deflate(4)
2899                .create("v")
2900                .unwrap();
2901            let raw0: Vec<u8> = original[0..4]
2902                .iter()
2903                .flat_map(|v| v.to_le_bytes())
2904                .collect();
2905            ds.write_chunk_raw(0, &apply_filters(&pipeline, &raw0).unwrap(), 0)
2906                .unwrap();
2907            let raw1: Vec<u8> = original[4..8]
2908                .iter()
2909                .flat_map(|v| v.to_le_bytes())
2910                .collect();
2911            ds.write_chunk_raw(1, &raw1, 1).unwrap();
2912            file.close().unwrap();
2913        }
2914        {
2915            let file = H5File::open(&path).unwrap();
2916            let v = file.dataset("v").unwrap().read_raw::<i32>().unwrap();
2917            assert_eq!(v, original);
2918        }
2919        std::fs::remove_file(&path).ok();
2920    }
2921
2922    /// An unfiltered chunk index has no slot for a stored size or mask, so a
2923    /// direct chunk write must be rejected rather than silently dropping them.
2924    #[test]
2925    fn write_chunk_raw_rejects_unfiltered() {
2926        let path = temp_path("wcr_unfiltered");
2927        let file = H5File::create(&path).unwrap();
2928        let ds = file
2929            .new_dataset::<i32>()
2930            .shape([0])
2931            .chunk(&[4])
2932            .max_shape(&[None])
2933            .create("v")
2934            .unwrap();
2935        let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
2936        assert!(
2937            err.to_string().contains("filtered dataset"),
2938            "expected a filtered-dataset error, got: {err}"
2939        );
2940        std::fs::remove_file(&path).ok();
2941    }
2942
2943    /// v2-B-tree-indexed datasets (two or more unlimited dimensions) do not
2944    /// support direct chunk writes.
2945    #[test]
2946    fn write_chunk_raw_rejects_btree_v2() {
2947        let path = temp_path("wcr_btree2");
2948        let file = H5File::create(&path).unwrap();
2949        let ds = file
2950            .new_dataset::<i32>()
2951            .shape([0, 0])
2952            .chunk(&[2, 2])
2953            .max_shape(&[None, None])
2954            .create("grid")
2955            .unwrap();
2956        let err = ds.write_chunk_raw(0, &[0u8; 16], 0).unwrap_err();
2957        assert!(
2958            err.to_string().contains("v2-B-tree"),
2959            "expected a v2-B-tree rejection, got: {err}"
2960        );
2961        std::fs::remove_file(&path).ok();
2962    }
2963
2964    /// A stored size that does not fit the index's chunk-size field must error
2965    /// (libhdf5 H5D_CHUNK_ENCODE_SIZE_CHECK) instead of truncating silently.
2966    /// A 4-byte chunk (chunk[1] of i32) has chunk_size_len = 2 (max 65535), so
2967    /// a 70000-byte stored chunk overflows it.
2968    #[cfg(feature = "deflate")]
2969    #[test]
2970    fn write_chunk_raw_rejects_oversized_chunk() {
2971        let path = temp_path("wcr_oversized");
2972        let file = H5File::create(&path).unwrap();
2973        let ds = file
2974            .new_dataset::<i32>()
2975            .shape([0])
2976            .chunk(&[1])
2977            .max_shape(&[None])
2978            .deflate(4)
2979            .create("v")
2980            .unwrap();
2981        let err = ds.write_chunk_raw(0, &vec![0u8; 70000], 0).unwrap_err();
2982        assert!(
2983            err.to_string().contains("does not fit"),
2984            "expected a chunk-size-field overflow error, got: {err}"
2985        );
2986        std::fs::remove_file(&path).ok();
2987    }
2988}