Skip to main content

hdf5_writer/
lib.rs

1//! Pure-Rust HDF5 writer.
2//!
3//! Build a file from [`DatasetBuilder`]s and [`AttributeBuilder`]s with
4//! [`Hdf5Builder`], turn it into an [`Hdf5WritePlan`], and serialize it with
5//! [`Hdf5Writer`] (a seekable sink) or [`Hdf5WritePlan::encode`] (bytes):
6//!
7//! ```
8//! use std::io::Cursor;
9//! use hdf5_writer::{DatasetBuilder, Hdf5Builder, Hdf5Writer, WriteOptions};
10//!
11//! let plan = Hdf5Builder::new()
12//!     .dataset(DatasetBuilder::typed_data("grid", vec![2, 3], &[0_i32, 1, 2, 3, 4, 5]).unwrap())
13//!     .into_plan()
14//!     .unwrap();
15//! let bytes = Hdf5Writer::new(Cursor::new(Vec::new()), WriteOptions::default())
16//!     .finish(plan)
17//!     .unwrap()
18//!     .into_inner();
19//! assert_eq!(&bytes[..8], b"\x89HDF\r\n\x1a\n");
20//! ```
21//!
22//! Supported: superblock v2; contiguous, compact, and chunked layouts (implicit,
23//! single-chunk, fixed-array, and version-2 B-tree indices, the last for
24//! unlimited maximum dimensions); groups; attributes; the deflate, shuffle, and
25//! fletcher32 filters; and fixed-point, floating-point, string (fixed and
26//! variable-length), reference, variable-length, bitfield, opaque, compound,
27//! enum, and array datatypes. Output is validated against libhdf5.
28
29use flate2::{write::ZlibEncoder, Compression};
30use std::io::{Seek, SeekFrom, Write};
31
32pub use hdf5_core::{
33    fletcher32, jenkins_lookup3, ByteOrder, ChunkIndexing, CompoundField, DataLayout,
34    DataspaceMessage, DataspaceType, Datatype, EnumMember, FillTime, FillValueMessage,
35    FilterDescription, FilterPipelineMessage, ReferenceType, StringEncoding, StringPadding,
36    StringSize, VarLenKind, FILTER_DEFLATE, FILTER_FLETCHER32, FILTER_LZ4, FILTER_NBIT,
37    FILTER_SCALEOFFSET, FILTER_SHUFFLE, FILTER_SZIP, UNLIMITED,
38};
39
40const HDF5_MAGIC: [u8; 8] = [0x89, b'H', b'D', b'F', 0x0d, 0x0a, 0x1a, 0x0a];
41const OFFSET_SIZE: u8 = 8;
42const LENGTH_SIZE: u8 = 8;
43const UNDEFINED_ADDRESS: u64 = u64::MAX;
44
45const MSG_DATASPACE: u8 = 0x01;
46const MSG_LINK_INFO: u8 = 0x02;
47const MSG_DATATYPE: u8 = 0x03;
48const MSG_FILL_VALUE: u8 = 0x05;
49const MSG_LINK: u8 = 0x06;
50const MSG_DATA_LAYOUT: u8 = 0x08;
51const MSG_GROUP_INFO: u8 = 0x0a;
52const MSG_FILTER_PIPELINE: u8 = 0x0b;
53const MSG_ATTRIBUTE: u8 = 0x0c;
54
55const SUPERBLOCK_V2_SIZE: usize = 48;
56
57/// HDF5 writer errors.
58#[derive(Debug, thiserror::Error)]
59pub enum Error {
60    #[error("I/O error: {0}")]
61    Io(#[from] std::io::Error),
62
63    #[error("HDF5 core error: {0}")]
64    Core(#[from] hdf5_core::Error),
65
66    #[error("invalid definition: {0}")]
67    InvalidDefinition(String),
68
69    /// The supplied data does not match the declared shape/type. `context`
70    /// names what was being written (e.g. `dataset 'grid'`).
71    #[error("{context}: expected {expected} element(s), got {actual}")]
72    DataLengthMismatch {
73        context: String,
74        expected: usize,
75        actual: usize,
76    },
77
78    #[error("unsupported write feature: {0}")]
79    UnsupportedFeature(String),
80}
81
82pub type Result<T> = std::result::Result<T, Error>;
83
84/// HDF5 file variant emitted by the writer.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum Hdf5Variant {
87    /// Modern HDF5 superblock/object-header encoding.
88    Modern,
89}
90
91/// Configuration for HDF5 writes.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct WriteOptions {
94    pub byte_order: ByteOrder,
95    pub variant: Hdf5Variant,
96}
97
98impl Default for WriteOptions {
99    fn default() -> Self {
100        Self {
101            byte_order: ByteOrder::LittleEndian,
102            variant: Hdf5Variant::Modern,
103        }
104    }
105}
106
107/// Trait for Rust types that can describe their HDF5 on-disk datatype.
108pub trait H5WriteType {
109    fn hdf5_type() -> Datatype;
110}
111
112/// Primitive element types that can be encoded into contiguous HDF5 raw data.
113pub trait H5WriteElement: H5WriteType + Copy {
114    fn hdf5_type_with_order(byte_order: ByteOrder) -> Datatype;
115    fn write_one(self, byte_order: ByteOrder, dst: &mut Vec<u8>);
116}
117
118macro_rules! impl_int_type {
119    ($ty:ty, $size:literal, $signed:literal) => {
120        impl H5WriteType for $ty {
121            fn hdf5_type() -> Datatype {
122                <$ty as H5WriteElement>::hdf5_type_with_order(native_order())
123            }
124        }
125
126        impl H5WriteElement for $ty {
127            fn hdf5_type_with_order(byte_order: ByteOrder) -> Datatype {
128                Datatype::FixedPoint {
129                    size: $size,
130                    signed: $signed,
131                    byte_order,
132                }
133            }
134
135            fn write_one(self, byte_order: ByteOrder, dst: &mut Vec<u8>) {
136                match byte_order {
137                    ByteOrder::LittleEndian => dst.extend_from_slice(&self.to_le_bytes()),
138                    ByteOrder::BigEndian => dst.extend_from_slice(&self.to_be_bytes()),
139                }
140            }
141        }
142    };
143}
144
145impl_int_type!(i16, 2, true);
146impl_int_type!(u16, 2, false);
147impl_int_type!(i32, 4, true);
148impl_int_type!(u32, 4, false);
149impl_int_type!(i64, 8, true);
150impl_int_type!(u64, 8, false);
151
152macro_rules! impl_byte_type {
153    ($ty:ty, $signed:literal) => {
154        impl H5WriteType for $ty {
155            fn hdf5_type() -> Datatype {
156                <$ty as H5WriteElement>::hdf5_type_with_order(native_order())
157            }
158        }
159
160        impl H5WriteElement for $ty {
161            fn hdf5_type_with_order(byte_order: ByteOrder) -> Datatype {
162                Datatype::FixedPoint {
163                    size: 1,
164                    signed: $signed,
165                    byte_order,
166                }
167            }
168
169            fn write_one(self, _byte_order: ByteOrder, dst: &mut Vec<u8>) {
170                dst.push(self as u8);
171            }
172        }
173    };
174}
175
176impl_byte_type!(i8, true);
177impl_byte_type!(u8, false);
178
179impl H5WriteType for f32 {
180    fn hdf5_type() -> Datatype {
181        <f32 as H5WriteElement>::hdf5_type_with_order(native_order())
182    }
183}
184
185impl H5WriteElement for f32 {
186    fn hdf5_type_with_order(byte_order: ByteOrder) -> Datatype {
187        Datatype::FloatingPoint {
188            size: 4,
189            byte_order,
190        }
191    }
192
193    fn write_one(self, byte_order: ByteOrder, dst: &mut Vec<u8>) {
194        let bits = self.to_bits();
195        match byte_order {
196            ByteOrder::LittleEndian => dst.extend_from_slice(&bits.to_le_bytes()),
197            ByteOrder::BigEndian => dst.extend_from_slice(&bits.to_be_bytes()),
198        }
199    }
200}
201
202impl H5WriteType for f64 {
203    fn hdf5_type() -> Datatype {
204        <f64 as H5WriteElement>::hdf5_type_with_order(native_order())
205    }
206}
207
208impl H5WriteElement for f64 {
209    fn hdf5_type_with_order(byte_order: ByteOrder) -> Datatype {
210        Datatype::FloatingPoint {
211            size: 8,
212            byte_order,
213        }
214    }
215
216    fn write_one(self, byte_order: ByteOrder, dst: &mut Vec<u8>) {
217        let bits = self.to_bits();
218        match byte_order {
219            ByteOrder::LittleEndian => dst.extend_from_slice(&bits.to_le_bytes()),
220            ByteOrder::BigEndian => dst.extend_from_slice(&bits.to_be_bytes()),
221        }
222    }
223}
224
225fn native_order() -> ByteOrder {
226    if cfg!(target_endian = "little") {
227        ByteOrder::LittleEndian
228    } else {
229        ByteOrder::BigEndian
230    }
231}
232
233/// Dataset definition used by the write planner.
234/// Builds a single HDF5 dataset: its name/path, datatype, shape, layout
235/// (contiguous, compact, or chunked), optional max shape, filters, fill value,
236/// data, and attributes.
237#[derive(Debug, Clone)]
238pub struct DatasetBuilder {
239    name: String,
240    datatype: Datatype,
241    shape: Vec<u64>,
242    max_shape: Option<Vec<u64>>,
243    layout: PlannedLayout,
244    filters: Vec<FilterDescription>,
245    fill_value: Option<Vec<u8>>,
246    raw_data: Option<Vec<u8>>,
247    vlen_string_values: Option<Vec<String>>,
248    vlen_sequence_values: Option<Vec<Vec<u8>>>,
249    attributes: Vec<AttributeBuilder>,
250}
251
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub enum PlannedLayout {
254    Compact,
255    Contiguous,
256    Chunked { chunk_shape: Vec<u64> },
257}
258
259impl DatasetBuilder {
260    pub fn new(name: impl Into<String>, datatype: Datatype, shape: impl Into<Vec<u64>>) -> Self {
261        Self {
262            name: name.into(),
263            datatype,
264            shape: shape.into(),
265            max_shape: None,
266            layout: PlannedLayout::Contiguous,
267            filters: Vec::new(),
268            fill_value: None,
269            raw_data: None,
270            vlen_string_values: None,
271            vlen_sequence_values: None,
272            attributes: Vec::new(),
273        }
274    }
275
276    pub fn typed<T: H5WriteType>(name: impl Into<String>, shape: impl Into<Vec<u64>>) -> Self {
277        Self::new(name, T::hdf5_type(), shape)
278    }
279
280    pub fn typed_with_order<T: H5WriteElement>(
281        name: impl Into<String>,
282        shape: impl Into<Vec<u64>>,
283        byte_order: ByteOrder,
284    ) -> Self {
285        Self::new(name, T::hdf5_type_with_order(byte_order), shape)
286    }
287
288    pub fn typed_data<T: H5WriteElement>(
289        name: impl Into<String>,
290        shape: impl Into<Vec<u64>>,
291        values: &[T],
292    ) -> Result<Self> {
293        Self::typed::<T>(name, shape).data(values)
294    }
295
296    pub fn fixed_string_data<S: AsRef<str>>(
297        name: impl Into<String>,
298        shape: impl Into<Vec<u64>>,
299        values: &[S],
300    ) -> Result<Self> {
301        let name = name.into();
302        let shape = shape.into();
303        let expected = expected_element_count(&shape)?;
304        if values.len() != expected {
305            return Err(Error::DataLengthMismatch {
306                context: format!("dataset '{name}' string data"),
307                expected,
308                actual: values.len(),
309            });
310        }
311        let (datatype, raw_data) = encode_fixed_string_values(values)?;
312        Ok(Self::new(name, datatype, shape).raw_data(raw_data))
313    }
314
315    pub fn vlen_string_data<S: AsRef<str>>(
316        name: impl Into<String>,
317        shape: impl Into<Vec<u64>>,
318        values: &[S],
319    ) -> Result<Self> {
320        let name = name.into();
321        let shape = shape.into();
322        let expected = expected_element_count(&shape)?;
323        if values.len() != expected {
324            return Err(Error::DataLengthMismatch {
325                context: format!("dataset '{name}' string data"),
326                expected,
327                actual: values.len(),
328            });
329        }
330
331        let mut strings = Vec::with_capacity(values.len());
332        let mut encoding = StringEncoding::Ascii;
333        for value in values {
334            let value = value.as_ref();
335            if value.as_bytes().contains(&0) {
336                return Err(Error::InvalidDefinition(
337                    "variable-length string values cannot contain NUL bytes".into(),
338                ));
339            }
340            if !value.is_ascii() {
341                encoding = StringEncoding::Utf8;
342            }
343            strings.push(value.to_string());
344        }
345
346        Ok(Self {
347            name,
348            datatype: Datatype::String {
349                size: StringSize::Variable,
350                encoding,
351                padding: StringPadding::NullTerminate,
352            },
353            shape,
354            max_shape: None,
355            layout: PlannedLayout::Contiguous,
356            filters: Vec::new(),
357            fill_value: None,
358            raw_data: None,
359            vlen_string_values: Some(strings),
360            vlen_sequence_values: None,
361            attributes: Vec::new(),
362        })
363    }
364
365    pub fn vlen_sequence_data(
366        name: impl Into<String>,
367        base: Datatype,
368        shape: impl Into<Vec<u64>>,
369        values: Vec<Vec<u8>>,
370    ) -> Result<Self> {
371        let name = name.into();
372        let shape = shape.into();
373        let expected = expected_element_count(&shape)?;
374        if values.len() != expected {
375            return Err(Error::DataLengthMismatch {
376                context: format!("dataset '{name}' vlen sequence data"),
377                expected,
378                actual: values.len(),
379            });
380        }
381        validate_vlen_sequence_base(&base)?;
382        let base_size = datatype_element_size(&base)?;
383        for value in &values {
384            if value.len() % base_size != 0 {
385                return Err(Error::InvalidDefinition(format!(
386                    "dataset '{}' vlen sequence byte length {} is not a multiple of base element size {base_size}",
387                    name,
388                    value.len()
389                )));
390            }
391        }
392
393        Ok(Self {
394            name,
395            datatype: Datatype::VarLen {
396                base: Box::new(base),
397                kind: VarLenKind::Sequence,
398                encoding: StringEncoding::Ascii,
399                padding: StringPadding::NullTerminate,
400            },
401            shape,
402            max_shape: None,
403            layout: PlannedLayout::Contiguous,
404            filters: Vec::new(),
405            fill_value: None,
406            raw_data: None,
407            vlen_string_values: None,
408            vlen_sequence_values: Some(values),
409            attributes: Vec::new(),
410        })
411    }
412
413    pub fn max_shape(mut self, max_shape: impl Into<Vec<u64>>) -> Self {
414        self.max_shape = Some(max_shape.into());
415        self
416    }
417
418    pub fn compact(mut self) -> Self {
419        self.layout = PlannedLayout::Compact;
420        self
421    }
422
423    pub fn chunked(mut self, chunk_shape: impl Into<Vec<u64>>) -> Self {
424        self.layout = PlannedLayout::Chunked {
425            chunk_shape: chunk_shape.into(),
426        };
427        self
428    }
429
430    pub fn filter(mut self, filter: FilterDescription) -> Self {
431        self.filters.push(filter);
432        self
433    }
434
435    pub fn fill_value(mut self, bytes: impl Into<Vec<u8>>) -> Self {
436        self.fill_value = Some(bytes.into());
437        self
438    }
439
440    pub fn raw_data(mut self, bytes: impl Into<Vec<u8>>) -> Self {
441        self.raw_data = Some(bytes.into());
442        self.vlen_string_values = None;
443        self.vlen_sequence_values = None;
444        self
445    }
446
447    pub fn data<T: H5WriteElement>(mut self, values: &[T]) -> Result<Self> {
448        let byte_order = numeric_datatype_order(&self.datatype)?;
449        ensure_datatype_matches_element::<T>(&self.datatype)?;
450        let expected = expected_data_len(&self.shape, datatype_element_size(&self.datatype)?)?;
451        let actual = std::mem::size_of_val(values);
452        if actual != expected {
453            return Err(Error::DataLengthMismatch {
454                context: format!("dataset '{}' data (bytes)", self.name),
455                expected,
456                actual,
457            });
458        }
459
460        let mut bytes = Vec::with_capacity(actual);
461        for &value in values {
462            value.write_one(byte_order, &mut bytes);
463        }
464        self.raw_data = Some(bytes);
465        self.vlen_string_values = None;
466        self.vlen_sequence_values = None;
467        Ok(self)
468    }
469
470    pub fn attribute(mut self, attribute: AttributeBuilder) -> Self {
471        self.attributes.push(attribute);
472        self
473    }
474
475    pub fn datatype(&self) -> &Datatype {
476        &self.datatype
477    }
478
479    pub fn shape(&self) -> &[u64] {
480        &self.shape
481    }
482
483    pub fn raw_data_bytes(&self) -> Option<&[u8]> {
484        self.raw_data.as_deref()
485    }
486
487    pub fn validate(&self) -> Result<()> {
488        validate_name(&self.name)?;
489        if self.shape.len() > u8::MAX as usize {
490            return Err(Error::InvalidDefinition(
491                "dataset rank exceeds HDF5 rank field capacity".into(),
492            ));
493        }
494        datatype_element_size(&self.datatype)?;
495        if let Some(max_shape) = &self.max_shape {
496            if max_shape.len() != self.shape.len() {
497                return Err(Error::InvalidDefinition(
498                    "dataset max_shape rank must match shape rank".into(),
499                ));
500            }
501            if !matches!(self.layout, PlannedLayout::Chunked { .. }) {
502                return Err(Error::InvalidDefinition(
503                    "resizable HDF5 datasets must use chunked layout".into(),
504                ));
505            }
506            for (&dim, &max_dim) in self.shape.iter().zip(max_shape) {
507                if max_dim != UNLIMITED && max_dim < dim {
508                    return Err(Error::InvalidDefinition(
509                        "dataset max_shape cannot be smaller than current shape".into(),
510                    ));
511                }
512            }
513        }
514        if !self.filters.is_empty() && !matches!(self.layout, PlannedLayout::Chunked { .. }) {
515            return Err(Error::InvalidDefinition(
516                "filtered HDF5 datasets must use chunked layout".into(),
517            ));
518        }
519        if let PlannedLayout::Chunked { chunk_shape } = &self.layout {
520            if chunk_shape.len() != self.shape.len() {
521                return Err(Error::InvalidDefinition(
522                    "chunk shape rank must match dataset rank".into(),
523                ));
524            }
525            if chunk_shape.contains(&0) {
526                return Err(Error::InvalidDefinition(
527                    "chunk dimensions must be non-zero".into(),
528                ));
529            }
530        }
531        if let Some(fill_value) = &self.fill_value {
532            let element_size = datatype_element_size(&self.datatype)?;
533            if fill_value.len() != element_size {
534                return Err(Error::InvalidDefinition(format!(
535                    "dataset '{}' fill value byte length must match datatype element size: expected {element_size}, got {}",
536                    self.name,
537                    fill_value.len()
538                )));
539            }
540        }
541        if let Some(values) = &self.vlen_sequence_values {
542            let Datatype::VarLen {
543                base,
544                kind: VarLenKind::Sequence,
545                ..
546            } = &self.datatype
547            else {
548                return Err(Error::InvalidDefinition(format!(
549                    "dataset '{}' vlen sequence values require a sequence datatype",
550                    self.name
551                )));
552            };
553            let expected = expected_element_count(&self.shape)?;
554            if values.len() != expected {
555                return Err(Error::InvalidDefinition(format!(
556                    "dataset '{}' vlen sequence shape must match value count",
557                    self.name
558                )));
559            }
560            validate_vlen_sequence_base(base)?;
561            let base_size = datatype_element_size(base)?;
562            for value in values {
563                if value.len() % base_size != 0 {
564                    return Err(Error::InvalidDefinition(format!(
565                        "dataset '{}' vlen sequence byte length {} is not a multiple of base element size {base_size}",
566                        self.name,
567                        value.len()
568                    )));
569                }
570            }
571        }
572        validate_attributes(&self.attributes)?;
573        Ok(())
574    }
575}
576
577/// Attribute definition used by HDF5 object headers.
578/// Builds a single HDF5 attribute attached to a dataset, group, or the file
579/// root. Supports scalars, vectors, fixed and variable-length strings, object
580/// references, and reference lists.
581#[derive(Debug, Clone, PartialEq, Eq)]
582pub struct AttributeBuilder {
583    name: String,
584    datatype: Datatype,
585    shape: Vec<u64>,
586    raw_data: Vec<u8>,
587    vlen_object_reference_targets: Option<Vec<Vec<String>>>,
588    vlen_string_values: Option<Vec<String>>,
589    vlen_sequence_values: Option<Vec<Vec<u8>>>,
590    /// Inline compound `{object reference, dimension}` list, resolved to object
591    /// addresses at layout time. Used for HDF5 dimension-scale `REFERENCE_LIST`
592    /// back-references.
593    object_reference_list: Option<Vec<(String, u32)>>,
594}
595
596impl AttributeBuilder {
597    pub fn new(
598        name: impl Into<String>,
599        datatype: Datatype,
600        shape: impl Into<Vec<u64>>,
601        raw_data: impl Into<Vec<u8>>,
602    ) -> Self {
603        Self {
604            name: name.into(),
605            datatype,
606            shape: shape.into(),
607            raw_data: raw_data.into(),
608            vlen_object_reference_targets: None,
609            vlen_string_values: None,
610            vlen_sequence_values: None,
611            object_reference_list: None,
612        }
613    }
614
615    pub fn scalar<T: H5WriteElement>(name: impl Into<String>, value: T) -> Result<Self> {
616        let datatype = T::hdf5_type();
617        let byte_order = numeric_datatype_order(&datatype)?;
618        let mut raw_data = Vec::with_capacity(datatype_element_size(&datatype)?);
619        value.write_one(byte_order, &mut raw_data);
620        Ok(Self::new(name, datatype, Vec::new(), raw_data))
621    }
622
623    pub fn vector<T: H5WriteElement>(name: impl Into<String>, values: &[T]) -> Result<Self> {
624        let datatype = T::hdf5_type();
625        let byte_order = numeric_datatype_order(&datatype)?;
626        let element_size = datatype_element_size(&datatype)?;
627        let mut raw_data = Vec::with_capacity(values.len() * element_size);
628        for &value in values {
629            value.write_one(byte_order, &mut raw_data);
630        }
631        Ok(Self::new(
632            name,
633            datatype,
634            vec![values.len() as u64],
635            raw_data,
636        ))
637    }
638
639    pub fn fixed_string(name: impl Into<String>, value: impl AsRef<str>) -> Self {
640        let value = value.as_ref();
641        let mut raw_data = Vec::with_capacity(value.len() + 1);
642        raw_data.extend_from_slice(value.as_bytes());
643        raw_data.push(0);
644        Self::new(
645            name,
646            Datatype::String {
647                size: StringSize::Fixed(raw_data.len() as u32),
648                encoding: if value.is_ascii() {
649                    StringEncoding::Ascii
650                } else {
651                    StringEncoding::Utf8
652                },
653                padding: StringPadding::NullTerminate,
654            },
655            Vec::new(),
656            raw_data,
657        )
658    }
659
660    pub fn fixed_string_vector<S: AsRef<str>>(
661        name: impl Into<String>,
662        values: &[S],
663    ) -> Result<Self> {
664        let element_count = u64::try_from(values.len()).map_err(|_| {
665            Error::InvalidDefinition("string attribute element count exceeds u64 capacity".into())
666        })?;
667        let (datatype, raw_data) = encode_fixed_string_values(values)?;
668        Ok(Self::new(name, datatype, vec![element_count], raw_data))
669    }
670
671    pub fn vlen_object_references(
672        name: impl Into<String>,
673        target_sequences: Vec<Vec<String>>,
674    ) -> Self {
675        Self {
676            name: name.into(),
677            datatype: Datatype::VarLen {
678                base: Box::new(Datatype::Reference {
679                    ref_type: ReferenceType::Object,
680                    size: OFFSET_SIZE,
681                }),
682                kind: VarLenKind::Sequence,
683                encoding: StringEncoding::Ascii,
684                padding: StringPadding::NullTerminate,
685            },
686            shape: vec![target_sequences.len() as u64],
687            raw_data: Vec::new(),
688            vlen_object_reference_targets: Some(target_sequences),
689            vlen_string_values: None,
690            vlen_sequence_values: None,
691            object_reference_list: None,
692        }
693    }
694
695    /// An inline `{object reference, dimension}` compound list, resolved to
696    /// object-header addresses at write time. This is the HDF5 dimension-scale
697    /// `REFERENCE_LIST` attribute: `entries` pairs each referencing dataset
698    /// (by object path) with the dimension index it attaches the scale to.
699    pub fn object_reference_list(name: impl Into<String>, entries: Vec<(String, u32)>) -> Self {
700        Self {
701            name: name.into(),
702            datatype: reference_list_datatype(),
703            shape: vec![entries.len() as u64],
704            raw_data: Vec::new(),
705            vlen_object_reference_targets: None,
706            vlen_string_values: None,
707            vlen_sequence_values: None,
708            object_reference_list: Some(entries),
709        }
710    }
711
712    pub fn vlen_strings<S: AsRef<str>>(name: impl Into<String>, values: &[S]) -> Result<Self> {
713        let element_count = u64::try_from(values.len()).map_err(|_| {
714            Error::InvalidDefinition("string attribute element count exceeds u64 capacity".into())
715        })?;
716        let mut strings = Vec::with_capacity(values.len());
717        let mut encoding = StringEncoding::Ascii;
718        for value in values {
719            let value = value.as_ref();
720            if value.as_bytes().contains(&0) {
721                return Err(Error::InvalidDefinition(
722                    "variable-length string values cannot contain NUL bytes".into(),
723                ));
724            }
725            if !value.is_ascii() {
726                encoding = StringEncoding::Utf8;
727            }
728            strings.push(value.to_string());
729        }
730
731        Ok(Self {
732            name: name.into(),
733            datatype: Datatype::VarLen {
734                base: Box::new(Datatype::FixedPoint {
735                    size: 1,
736                    signed: false,
737                    byte_order: ByteOrder::LittleEndian,
738                }),
739                kind: VarLenKind::String,
740                encoding,
741                padding: StringPadding::NullTerminate,
742            },
743            shape: vec![element_count],
744            raw_data: Vec::new(),
745            vlen_object_reference_targets: None,
746            vlen_string_values: Some(strings),
747            vlen_sequence_values: None,
748            object_reference_list: None,
749        })
750    }
751
752    pub fn vlen_sequences(
753        name: impl Into<String>,
754        base: Datatype,
755        values: Vec<Vec<u8>>,
756    ) -> Result<Self> {
757        let name = name.into();
758        validate_vlen_sequence_base(&base)?;
759        let base_size = datatype_element_size(&base)?;
760        for value in &values {
761            if value.len() % base_size != 0 {
762                return Err(Error::InvalidDefinition(format!(
763                    "attribute '{}' vlen sequence byte length {} is not a multiple of base element size {base_size}",
764                    name,
765                    value.len()
766                )));
767            }
768        }
769        let element_count = u64::try_from(values.len()).map_err(|_| {
770            Error::InvalidDefinition(
771                "vlen sequence attribute element count exceeds u64 capacity".into(),
772            )
773        })?;
774
775        Ok(Self {
776            name,
777            datatype: Datatype::VarLen {
778                base: Box::new(base),
779                kind: VarLenKind::Sequence,
780                encoding: StringEncoding::Ascii,
781                padding: StringPadding::NullTerminate,
782            },
783            shape: vec![element_count],
784            raw_data: Vec::new(),
785            vlen_object_reference_targets: None,
786            vlen_string_values: None,
787            vlen_sequence_values: Some(values),
788            object_reference_list: None,
789        })
790    }
791
792    pub fn name(&self) -> &str {
793        &self.name
794    }
795
796    pub fn datatype(&self) -> &Datatype {
797        &self.datatype
798    }
799
800    pub fn shape(&self) -> &[u64] {
801        &self.shape
802    }
803
804    pub fn raw_data(&self) -> &[u8] {
805        &self.raw_data
806    }
807
808    pub fn validate(&self) -> Result<()> {
809        validate_name(&self.name)?;
810        if let Some(values) = &self.vlen_string_values {
811            if self.shape != [values.len() as u64] {
812                return Err(Error::InvalidDefinition(format!(
813                    "attribute '{}' vlen string shape must match value count",
814                    self.name
815                )));
816            }
817            for value in values {
818                if value.as_bytes().contains(&0) {
819                    return Err(Error::InvalidDefinition(format!(
820                        "attribute '{}' vlen string values cannot contain NUL bytes",
821                        self.name
822                    )));
823                }
824            }
825            return Ok(());
826        }
827        if let Some(values) = &self.vlen_sequence_values {
828            let Datatype::VarLen {
829                base,
830                kind: VarLenKind::Sequence,
831                ..
832            } = &self.datatype
833            else {
834                return Err(Error::InvalidDefinition(format!(
835                    "attribute '{}' vlen sequence values require a sequence datatype",
836                    self.name
837                )));
838            };
839            if self.shape != [values.len() as u64] {
840                return Err(Error::InvalidDefinition(format!(
841                    "attribute '{}' vlen sequence shape must match value count",
842                    self.name
843                )));
844            }
845            validate_vlen_sequence_base(base)?;
846            let base_size = datatype_element_size(base)?;
847            for value in values {
848                if value.len() % base_size != 0 {
849                    return Err(Error::InvalidDefinition(format!(
850                        "attribute '{}' vlen sequence byte length {} is not a multiple of base element size {base_size}",
851                        self.name,
852                        value.len()
853                    )));
854                }
855            }
856            return Ok(());
857        }
858        if let Some(target_sequences) = &self.vlen_object_reference_targets {
859            if self.shape != [target_sequences.len() as u64] {
860                return Err(Error::InvalidDefinition(format!(
861                    "attribute '{}' vlen reference shape must match sequence count",
862                    self.name
863                )));
864            }
865            for sequence in target_sequences {
866                for target in sequence {
867                    validate_name(target)?;
868                }
869            }
870            return Ok(());
871        }
872        if let Some(entries) = &self.object_reference_list {
873            if self.shape != [entries.len() as u64] {
874                return Err(Error::InvalidDefinition(format!(
875                    "attribute '{}' reference-list shape must match entry count",
876                    self.name
877                )));
878            }
879            for (target, _) in entries {
880                validate_name(target)?;
881            }
882            return Ok(());
883        }
884        let expected = expected_data_len(&self.shape, datatype_element_size(&self.datatype)?)?;
885        if self.raw_data.len() != expected {
886            return Err(Error::DataLengthMismatch {
887                context: format!("attribute '{}' data (bytes)", self.name),
888                expected,
889                actual: self.raw_data.len(),
890            });
891        }
892        Ok(())
893    }
894}
895
896/// Byte size of one `REFERENCE_LIST` compound entry, matching netcdf-c:
897/// object reference (8) at offset 0, dimension `u32` at offset 8, padded to 16.
898const REFERENCE_LIST_ENTRY_SIZE: u32 = 16;
899const REFERENCE_LIST_DIMENSION_OFFSET: usize = 8;
900
901/// The compound datatype netcdf-c uses for dimension-scale `REFERENCE_LIST`
902/// attributes: `{ dataset: object reference, dimension: u32 }`.
903fn reference_list_datatype() -> Datatype {
904    Datatype::Compound {
905        size: REFERENCE_LIST_ENTRY_SIZE,
906        fields: vec![
907            CompoundField {
908                name: "dataset".to_string(),
909                byte_offset: 0,
910                datatype: Datatype::Reference {
911                    ref_type: ReferenceType::Object,
912                    size: OFFSET_SIZE,
913                },
914            },
915            CompoundField {
916                name: "dimension".to_string(),
917                byte_offset: REFERENCE_LIST_DIMENSION_OFFSET as u32,
918                datatype: Datatype::FixedPoint {
919                    size: 4,
920                    signed: false,
921                    byte_order: ByteOrder::LittleEndian,
922                },
923            },
924        ],
925    }
926}
927
928/// Attribute attached to an HDF5 group addressed by relative path.
929#[derive(Debug, Clone, PartialEq, Eq)]
930pub struct GroupAttributeBuilder {
931    path: String,
932    attribute: AttributeBuilder,
933}
934
935impl GroupAttributeBuilder {
936    pub fn new(group_path: impl Into<String>, attribute: AttributeBuilder) -> Self {
937        Self {
938            path: group_path.into(),
939            attribute,
940        }
941    }
942
943    pub fn path(&self) -> &str {
944        &self.path
945    }
946
947    pub fn attribute(&self) -> &AttributeBuilder {
948        &self.attribute
949    }
950
951    pub fn validate(&self) -> Result<()> {
952        validate_name(&self.path)?;
953        self.attribute.validate()
954    }
955}
956
957/// Root HDF5 file builder.
958/// Accumulates datasets, root attributes, and group attributes, then produces
959/// a validated [`Hdf5WritePlan`] via [`Hdf5Builder::into_plan`].
960#[derive(Debug, Clone, Default)]
961pub struct Hdf5Builder {
962    datasets: Vec<DatasetBuilder>,
963    attributes: Vec<AttributeBuilder>,
964    group_attributes: Vec<GroupAttributeBuilder>,
965}
966
967impl Hdf5Builder {
968    pub fn new() -> Self {
969        Self::default()
970    }
971
972    pub fn dataset(mut self, dataset: DatasetBuilder) -> Self {
973        self.datasets.push(dataset);
974        self
975    }
976
977    pub fn attribute(mut self, attribute: AttributeBuilder) -> Self {
978        self.attributes.push(attribute);
979        self
980    }
981
982    pub fn group_attribute(
983        mut self,
984        group_path: impl Into<String>,
985        attribute: AttributeBuilder,
986    ) -> Self {
987        self.group_attributes
988            .push(GroupAttributeBuilder::new(group_path, attribute));
989        self
990    }
991
992    pub fn validate(&self) -> Result<()> {
993        let mut names = std::collections::BTreeSet::new();
994        let mut group_paths = std::collections::BTreeSet::new();
995        for dataset in &self.datasets {
996            dataset.validate()?;
997            if !names.insert(dataset.name.as_str()) {
998                return Err(Error::InvalidDefinition(format!(
999                    "duplicate root dataset '{}'",
1000                    dataset.name
1001                )));
1002            }
1003            let mut parent = parent_path(&dataset.name);
1004            while let Some(path) = parent {
1005                group_paths.insert(path.to_string());
1006                parent = parent_path(path);
1007            }
1008        }
1009        for group_attribute in &self.group_attributes {
1010            group_attribute.validate()?;
1011            let mut path = Some(group_attribute.path.as_str());
1012            while let Some(group_path) = path {
1013                group_paths.insert(group_path.to_string());
1014                path = parent_path(group_path);
1015            }
1016        }
1017        for dataset in &self.datasets {
1018            if group_paths.contains(dataset.name.as_str()) {
1019                return Err(Error::InvalidDefinition(format!(
1020                    "dataset '{}' conflicts with an implicit HDF5 group at the same path",
1021                    dataset.name
1022                )));
1023            }
1024        }
1025        validate_attributes(&self.attributes)?;
1026        validate_group_attributes(&self.group_attributes)?;
1027        Ok(())
1028    }
1029
1030    pub fn into_plan(self) -> Result<Hdf5WritePlan> {
1031        self.validate()?;
1032        Ok(Hdf5WritePlan {
1033            datasets: self.datasets,
1034            attributes: self.attributes,
1035            group_attributes: self.group_attributes,
1036        })
1037    }
1038}
1039
1040/// Validated HDF5 write plan.
1041/// A validated set of datasets and attributes ready to be serialized by
1042/// [`Hdf5Writer`] or [`Hdf5WritePlan::encode`].
1043#[derive(Debug, Clone)]
1044pub struct Hdf5WritePlan {
1045    datasets: Vec<DatasetBuilder>,
1046    attributes: Vec<AttributeBuilder>,
1047    group_attributes: Vec<GroupAttributeBuilder>,
1048}
1049
1050impl Hdf5WritePlan {
1051    pub fn datasets(&self) -> &[DatasetBuilder] {
1052        &self.datasets
1053    }
1054
1055    pub fn attributes(&self) -> &[AttributeBuilder] {
1056        &self.attributes
1057    }
1058
1059    pub fn group_attributes(&self) -> &[GroupAttributeBuilder] {
1060        &self.group_attributes
1061    }
1062
1063    pub fn validate(&self) -> Result<()> {
1064        Hdf5Builder {
1065            datasets: self.datasets.clone(),
1066            attributes: self.attributes.clone(),
1067            group_attributes: self.group_attributes.clone(),
1068        }
1069        .validate()
1070    }
1071
1072    /// Encode the plan to the complete HDF5 file bytes. Callers that already
1073    /// hold a plain [`Write`] sink (and cannot supply a [`Seek`] one) can write
1074    /// these bytes directly, avoiding an intermediate in-memory copy.
1075    pub fn encode(&self, options: WriteOptions) -> Result<Vec<u8>> {
1076        encode_hdf5_file(self, options)
1077    }
1078}
1079
1080/// Writes an [`Hdf5WritePlan`] to a seekable sink. Use
1081/// [`Hdf5WritePlan::encode`] when the sink is a plain [`Write`].
1082pub struct Hdf5Writer<W: Write + Seek> {
1083    sink: W,
1084    options: WriteOptions,
1085}
1086
1087impl<W: Write + Seek> Hdf5Writer<W> {
1088    pub fn new(sink: W, options: WriteOptions) -> Self {
1089        Self { sink, options }
1090    }
1091
1092    /// Encode `plan` and write the file to the sink. Consumes the writer, so a
1093    /// writer can be finished at most once.
1094    pub fn finish(mut self, plan: Hdf5WritePlan) -> Result<W> {
1095        let bytes = encode_hdf5_file(&plan, self.options)?;
1096        self.sink.seek(SeekFrom::Start(0))?;
1097        self.sink.write_all(&bytes)?;
1098        Ok(self.sink)
1099    }
1100
1101    pub fn into_inner(self) -> W {
1102        self.sink
1103    }
1104}
1105
1106fn validate_name(name: &str) -> Result<()> {
1107    if name.is_empty() {
1108        return Err(Error::InvalidDefinition("name must not be empty".into()));
1109    }
1110    if name == "/" || name.split('/').any(str::is_empty) {
1111        return Err(Error::InvalidDefinition(format!(
1112            "invalid HDF5 relative path '{name}'"
1113        )));
1114    }
1115    Ok(())
1116}
1117
1118fn validate_attributes(attributes: &[AttributeBuilder]) -> Result<()> {
1119    let mut names = std::collections::BTreeSet::new();
1120    for attribute in attributes {
1121        attribute.validate()?;
1122        if !names.insert(attribute.name.as_str()) {
1123            return Err(Error::InvalidDefinition(format!(
1124                "duplicate attribute '{}'",
1125                attribute.name
1126            )));
1127        }
1128    }
1129    Ok(())
1130}
1131
1132fn validate_group_attributes(group_attributes: &[GroupAttributeBuilder]) -> Result<()> {
1133    let mut names_by_group =
1134        std::collections::BTreeMap::<&str, std::collections::BTreeSet<&str>>::new();
1135    for group_attribute in group_attributes {
1136        group_attribute.validate()?;
1137        let group_names = names_by_group
1138            .entry(group_attribute.path.as_str())
1139            .or_default();
1140        if !group_names.insert(group_attribute.attribute.name.as_str()) {
1141            return Err(Error::InvalidDefinition(format!(
1142                "duplicate attribute '{}' on HDF5 group '{}'",
1143                group_attribute.attribute.name, group_attribute.path
1144            )));
1145        }
1146    }
1147    Ok(())
1148}
1149
1150#[derive(Debug)]
1151struct DatasetEmission<'a> {
1152    dataset: &'a DatasetBuilder,
1153    raw_data: Vec<u8>,
1154    header_address: u64,
1155    data_address: u64,
1156    header: Vec<u8>,
1157    chunk_index: Option<FixedArrayChunkIndexEmission>,
1158}
1159
1160#[derive(Debug)]
1161struct GroupEmission {
1162    header_address: u64,
1163    header: Vec<u8>,
1164}
1165
1166#[derive(Debug)]
1167struct FixedArrayChunkIndexEmission {
1168    header_address: u64,
1169    data_block_address: u64,
1170    header: Vec<u8>,
1171    data_block: Vec<u8>,
1172}
1173
1174#[derive(Debug)]
1175struct HeaderMessage {
1176    type_id: u8,
1177    payload: Vec<u8>,
1178}
1179
1180#[derive(Debug, Clone)]
1181struct PlannedAttribute {
1182    name: String,
1183    datatype: Datatype,
1184    shape: Vec<u64>,
1185    raw_data: PlannedAttributeRaw,
1186}
1187
1188#[derive(Debug, Clone)]
1189enum PlannedAttributeRaw {
1190    Inline(Vec<u8>),
1191    GlobalHeapVLenReferences(Vec<VLenHeapReference>),
1192}
1193
1194#[derive(Debug, Clone)]
1195struct VLenHeapReference {
1196    sequence_len: u32,
1197    heap_index: u16,
1198}
1199
1200#[derive(Debug, Clone)]
1201struct GlobalHeapObjectPlan {
1202    index: u16,
1203    reference_count: u16,
1204    data: Vec<u8>,
1205}
1206
1207#[derive(Debug, Clone)]
1208struct PlannedAttributes {
1209    root: Vec<PlannedAttribute>,
1210    groups: Vec<Vec<PlannedAttribute>>,
1211    datasets: Vec<Vec<PlannedAttribute>>,
1212    dataset_vlen_refs: Vec<Option<Vec<VLenHeapReference>>>,
1213    heap_objects: Vec<GlobalHeapObjectPlan>,
1214}
1215
1216#[derive(Debug, Clone)]
1217struct GroupHierarchy {
1218    paths: Vec<String>,
1219    root_links: Vec<PlannedLink>,
1220    group_links: Vec<Vec<PlannedLink>>,
1221}
1222
1223#[derive(Debug, Clone, PartialEq, Eq)]
1224struct PlannedLink {
1225    name: String,
1226    target: PlannedLinkTarget,
1227}
1228
1229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1230enum PlannedLinkTarget {
1231    Group(usize),
1232    Dataset(usize),
1233}
1234
1235impl PlannedAttribute {
1236    fn raw_data(&self, heap_address: u64) -> Vec<u8> {
1237        match &self.raw_data {
1238            PlannedAttributeRaw::Inline(raw_data) => raw_data.clone(),
1239            PlannedAttributeRaw::GlobalHeapVLenReferences(refs) => {
1240                encode_vlen_heap_references(refs, heap_address)
1241            }
1242        }
1243    }
1244}
1245
1246fn encode_vlen_heap_references(refs: &[VLenHeapReference], heap_address: u64) -> Vec<u8> {
1247    let mut raw_data = Vec::with_capacity(refs.len() * (4 + usize::from(OFFSET_SIZE) + 4));
1248    for reference in refs {
1249        if reference.heap_index == 0 {
1250            // Empty sequence: an all-zero reference, never dereferenced
1251            // (index 0 is the collection's free-space object).
1252            raw_data.resize(raw_data.len() + 4 + usize::from(OFFSET_SIZE) + 4, 0);
1253            continue;
1254        }
1255        raw_data.extend_from_slice(&reference.sequence_len.to_le_bytes());
1256        raw_data.extend_from_slice(&heap_address.to_le_bytes());
1257        raw_data.extend_from_slice(&(u32::from(reference.heap_index)).to_le_bytes());
1258    }
1259    raw_data
1260}
1261
1262fn encode_hdf5_file(plan: &Hdf5WritePlan, options: WriteOptions) -> Result<Vec<u8>> {
1263    if options.variant != Hdf5Variant::Modern {
1264        return Err(Error::UnsupportedFeature(format!(
1265            "unsupported HDF5 variant {:?}",
1266            options.variant
1267        )));
1268    }
1269
1270    let mut prepared = prepare_datasets(plan, options)?;
1271    let groups = plan_group_hierarchy(&prepared, &plan.group_attributes)?;
1272    let placeholder_group_addresses = vec![0; groups.paths.len()];
1273    let placeholder_dataset_addresses = vec![0; prepared.len()];
1274    let placeholder_targets =
1275        placeholder_target_addresses(&prepared, &groups, &placeholder_group_addresses);
1276    let placeholder_attributes = plan_attributes(plan, &groups, &placeholder_targets)?;
1277    let root_header_size = encode_group_header_from_plan(
1278        &groups.root_links,
1279        &placeholder_group_addresses,
1280        &placeholder_dataset_addresses,
1281        &placeholder_attributes.root,
1282        0,
1283    )?
1284    .len();
1285    let mut next_address = align_u64(SUPERBLOCK_V2_SIZE as u64, 8);
1286    let root_address = next_address;
1287    next_address = align_u64(
1288        checked_add_u64(
1289            next_address,
1290            root_header_size as u64,
1291            "root object header end",
1292        )?,
1293        8,
1294    );
1295
1296    let mut group_addresses = Vec::with_capacity(groups.paths.len());
1297    for (group_links, attributes) in groups
1298        .group_links
1299        .iter()
1300        .zip(&placeholder_attributes.groups)
1301    {
1302        group_addresses.push(next_address);
1303        let placeholder = encode_group_header_from_plan(
1304            group_links,
1305            &placeholder_group_addresses,
1306            &placeholder_dataset_addresses,
1307            attributes,
1308            0,
1309        )?;
1310        next_address = align_u64(
1311            checked_add_u64(
1312                next_address,
1313                placeholder.len() as u64,
1314                "group object header end",
1315            )?,
1316            8,
1317        );
1318    }
1319
1320    let mut header_addresses = Vec::with_capacity(prepared.len());
1321    for (prepared_dataset, attributes) in prepared.iter().zip(&placeholder_attributes.datasets) {
1322        header_addresses.push(next_address);
1323        let placeholder = encode_dataset_header(prepared_dataset, attributes, 0, 0)?;
1324        next_address = align_u64(
1325            checked_add_u64(
1326                next_address,
1327                placeholder.len() as u64,
1328                "dataset object header end",
1329            )?,
1330            8,
1331        );
1332    }
1333
1334    let mut chunk_index_addresses = Vec::with_capacity(prepared.len());
1335    for prepared_dataset in &prepared {
1336        if let Some(chunk_index) = &prepared_dataset.chunk_index {
1337            // Block sizes are address-invariant, so a single sizing pass fixes
1338            // the header and secondary-block addresses.
1339            let (header_len, data_block_len) = chunk_index_block_sizes(chunk_index)?;
1340            let header_address = next_address;
1341            next_address = align_u64(
1342                checked_add_u64(next_address, header_len as u64, "chunk index header end")?,
1343                8,
1344            );
1345            let data_block_address = next_address;
1346            next_address = align_u64(
1347                checked_add_u64(
1348                    next_address,
1349                    data_block_len as u64,
1350                    "chunk index secondary block end",
1351                )?,
1352                8,
1353            );
1354            chunk_index_addresses.push(Some(FixedArrayChunkIndexAddresses {
1355                header: header_address,
1356                data_block: data_block_address,
1357            }));
1358        } else {
1359            chunk_index_addresses.push(None);
1360        }
1361    }
1362
1363    let data_start_address = next_address;
1364
1365    let target_addresses =
1366        target_addresses(&prepared, &header_addresses, &groups, &group_addresses);
1367    let planned_attributes = plan_attributes(plan, &groups, &target_addresses)?;
1368
1369    let heap = if planned_attributes.heap_objects.is_empty() {
1370        Vec::new()
1371    } else {
1372        encode_global_heap_collection(&planned_attributes.heap_objects)?
1373    };
1374    let DataHeapLayout {
1375        data_addresses,
1376        heap_address,
1377        eof_address,
1378    } = stabilize_vlen_dataset_storage(
1379        &mut prepared,
1380        &planned_attributes,
1381        data_start_address,
1382        &heap,
1383    )?;
1384
1385    let root_header = encode_group_header_from_plan(
1386        &groups.root_links,
1387        &group_addresses,
1388        &header_addresses,
1389        &planned_attributes.root,
1390        heap_address,
1391    )?;
1392
1393    let mut group_emissions = Vec::with_capacity(groups.paths.len());
1394    for ((group_links, attributes), header_address) in groups
1395        .group_links
1396        .iter()
1397        .zip(&planned_attributes.groups)
1398        .zip(group_addresses.iter().copied())
1399    {
1400        let header = encode_group_header_from_plan(
1401            group_links,
1402            &group_addresses,
1403            &header_addresses,
1404            attributes,
1405            heap_address,
1406        )?;
1407        group_emissions.push(GroupEmission {
1408            header_address,
1409            header,
1410        });
1411    }
1412
1413    let mut emissions = Vec::with_capacity(prepared.len());
1414    // Consume `prepared` so each dataset's storage bytes move into the emission
1415    // instead of being cloned.
1416    for ((((prepared_dataset, attributes), header_address), data_address), chunk_index_address) in
1417        prepared
1418            .into_iter()
1419            .zip(&planned_attributes.datasets)
1420            .zip(header_addresses.iter().copied())
1421            .zip(data_addresses.iter().copied())
1422            .zip(chunk_index_addresses.iter().copied())
1423    {
1424        let layout_address = chunk_index_address.map_or(data_address, |address| address.header);
1425        let header =
1426            encode_dataset_header(&prepared_dataset, attributes, layout_address, heap_address)?;
1427        let chunk_index = if let (Some(chunk_index), Some(addresses)) =
1428            (&prepared_dataset.chunk_index, chunk_index_address)
1429        {
1430            let (header, data_block) = encode_chunk_index_blocks(
1431                chunk_index,
1432                addresses.header,
1433                addresses.data_block,
1434                data_address,
1435            )?;
1436            Some(FixedArrayChunkIndexEmission {
1437                header_address: addresses.header,
1438                data_block_address: addresses.data_block,
1439                header,
1440                data_block,
1441            })
1442        } else {
1443            None
1444        };
1445        emissions.push(DatasetEmission {
1446            dataset: prepared_dataset.dataset,
1447            raw_data: prepared_dataset.raw_data,
1448            header_address,
1449            data_address,
1450            header,
1451            chunk_index,
1452        });
1453    }
1454
1455    let mut file = Vec::with_capacity(checked_usize(eof_address, "HDF5 file size")?);
1456    file.extend_from_slice(&encode_superblock_v2(root_address, eof_address));
1457    pad_to_address(&mut file, root_address)?;
1458    file.extend_from_slice(&root_header);
1459
1460    for emission in &group_emissions {
1461        pad_to_address(&mut file, emission.header_address)?;
1462        file.extend_from_slice(&emission.header);
1463    }
1464
1465    for emission in &emissions {
1466        pad_to_address(&mut file, emission.header_address)?;
1467        file.extend_from_slice(&emission.header);
1468    }
1469
1470    for emission in &emissions {
1471        if let Some(chunk_index) = &emission.chunk_index {
1472            pad_to_address(&mut file, chunk_index.header_address)?;
1473            file.extend_from_slice(&chunk_index.header);
1474            pad_to_address(&mut file, chunk_index.data_block_address)?;
1475            file.extend_from_slice(&chunk_index.data_block);
1476        }
1477    }
1478
1479    for emission in &emissions {
1480        if !matches!(emission.dataset.layout, PlannedLayout::Compact) {
1481            pad_to_address(&mut file, emission.data_address)?;
1482            file.extend_from_slice(&emission.raw_data);
1483        }
1484    }
1485
1486    if !heap.is_empty() {
1487        pad_to_address(&mut file, heap_address)?;
1488        file.extend_from_slice(&heap);
1489    }
1490
1491    pad_to_address(&mut file, eof_address)?;
1492    Ok(file)
1493}
1494
1495#[derive(Debug)]
1496struct PreparedDataset<'a> {
1497    dataset: &'a DatasetBuilder,
1498    filters: Vec<FilterDescription>,
1499    raw_data: Vec<u8>,
1500    data_size: u64,
1501    chunk_index: Option<PreparedChunkIndex>,
1502}
1503
1504/// On-disk index structure used to locate a chunked dataset's chunks.
1505///
1506/// Datasets with unlimited maximum dimensions require a version-2 B-tree index
1507/// (the fixed array and implicit indices are only valid for fixed max dims);
1508/// everything else with a resolvable chunk index uses the fixed array.
1509#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1510enum ChunkIndexKind {
1511    FixedArray,
1512    BtreeV2,
1513}
1514
1515#[derive(Debug)]
1516struct PreparedChunkIndex {
1517    kind: ChunkIndexKind,
1518    chunks: Vec<PreparedChunkIndexEntry>,
1519    /// Byte width of the encoded chunk-size field in each entry.
1520    chunk_size_len: u8,
1521}
1522
1523#[derive(Debug)]
1524struct PreparedChunkIndexEntry {
1525    relative_address: u64,
1526    size: u64,
1527    filter_mask: u32,
1528    /// Chunk grid coordinates (one per dataset dimension); the B-tree index
1529    /// records these as scaled offsets.
1530    scaled_offsets: Vec<u64>,
1531}
1532
1533#[derive(Debug, Clone)]
1534struct DataHeapLayout {
1535    data_addresses: Vec<u64>,
1536    heap_address: u64,
1537    eof_address: u64,
1538}
1539
1540#[derive(Debug, Clone, Copy)]
1541struct FixedArrayChunkIndexAddresses {
1542    header: u64,
1543    data_block: u64,
1544}
1545
1546fn prepare_datasets(
1547    plan: &Hdf5WritePlan,
1548    options: WriteOptions,
1549) -> Result<Vec<PreparedDataset<'_>>> {
1550    plan.validate()?;
1551    let mut prepared = Vec::with_capacity(plan.datasets.len());
1552    for dataset in &plan.datasets {
1553        let element_size = datatype_element_size(&dataset.datatype)?;
1554        let filters = validate_writer_filters(dataset, element_size)?;
1555        if let Some(datatype_order) = datatype_numeric_order(&dataset.datatype) {
1556            if datatype_order != options.byte_order {
1557                return Err(Error::InvalidDefinition(format!(
1558                    "dataset '{}' datatype byte order {:?} does not match writer byte order {:?}",
1559                    dataset.name, datatype_order, options.byte_order
1560                )));
1561            }
1562        }
1563        let expected = expected_data_len(&dataset.shape, element_size)?;
1564        let (storage_data, chunk_index) = if let Some(values) = &dataset.vlen_string_values {
1565            if dataset.fill_value.is_some() {
1566                return Err(Error::InvalidDefinition(format!(
1567                    "dataset '{}' cannot combine variable-length strings with a fill value",
1568                    dataset.name
1569                )));
1570            }
1571            if matches!(dataset.layout, PlannedLayout::Compact) {
1572                return Err(Error::UnsupportedFeature(format!(
1573                    "compact variable-length string HDF5 datasets are not emitted yet: '{}'",
1574                    dataset.name
1575                )));
1576            }
1577            let refs = vlen_string_heap_references(&dataset.name, values, None)?;
1578            let raw_data = encode_vlen_heap_references(&refs, 0);
1579            if raw_data.len() != expected {
1580                return Err(Error::InvalidDefinition(format!(
1581                    "dataset '{}' expects {expected} vlen reference bytes, got {}",
1582                    dataset.name,
1583                    raw_data.len()
1584                )));
1585            }
1586            prepare_raw_dataset_storage(
1587                &raw_data,
1588                &dataset.shape,
1589                &dataset.layout,
1590                &filters,
1591                element_size,
1592                dataset.max_shape.as_deref(),
1593            )?
1594        } else if let Some(values) = &dataset.vlen_sequence_values {
1595            if dataset.fill_value.is_some() {
1596                return Err(Error::InvalidDefinition(format!(
1597                    "dataset '{}' cannot combine variable-length sequences with a fill value",
1598                    dataset.name
1599                )));
1600            }
1601            if matches!(dataset.layout, PlannedLayout::Compact) {
1602                return Err(Error::UnsupportedFeature(format!(
1603                    "compact variable-length sequence HDF5 datasets are not emitted yet: '{}'",
1604                    dataset.name
1605                )));
1606            }
1607            let Datatype::VarLen {
1608                base,
1609                kind: VarLenKind::Sequence,
1610                ..
1611            } = &dataset.datatype
1612            else {
1613                return Err(Error::InvalidDefinition(format!(
1614                    "dataset '{}' vlen sequence values require a sequence datatype",
1615                    dataset.name
1616                )));
1617            };
1618            validate_vlen_sequence_base(base)?;
1619            let base_size = datatype_element_size(base)?;
1620            let refs = vlen_sequence_heap_references(&dataset.name, values, base_size, None)?;
1621            let raw_data = encode_vlen_heap_references(&refs, 0);
1622            if raw_data.len() != expected {
1623                return Err(Error::InvalidDefinition(format!(
1624                    "dataset '{}' expects {expected} vlen reference bytes, got {}",
1625                    dataset.name,
1626                    raw_data.len()
1627                )));
1628            }
1629            prepare_raw_dataset_storage(
1630                &raw_data,
1631                &dataset.shape,
1632                &dataset.layout,
1633                &filters,
1634                element_size,
1635                dataset.max_shape.as_deref(),
1636            )?
1637        } else if let Some(raw_data) = dataset.raw_data.as_deref() {
1638            if raw_data.len() != expected {
1639                return Err(Error::InvalidDefinition(format!(
1640                    "dataset '{}' expects {expected} data bytes, got {}",
1641                    dataset.name,
1642                    raw_data.len()
1643                )));
1644            }
1645            prepare_raw_dataset_storage(
1646                raw_data,
1647                &dataset.shape,
1648                &dataset.layout,
1649                &filters,
1650                element_size,
1651                dataset.max_shape.as_deref(),
1652            )?
1653        } else if dataset.fill_value.is_some() {
1654            if matches!(dataset.layout, PlannedLayout::Compact) {
1655                return Err(Error::UnsupportedFeature(format!(
1656                    "compact HDF5 datasets are not emitted yet: '{}'",
1657                    dataset.name
1658                )));
1659            }
1660            (Vec::new(), None)
1661        } else {
1662            return Err(Error::InvalidDefinition(format!(
1663                "dataset '{}' has no raw data for binary emission",
1664                dataset.name
1665            )));
1666        };
1667        let data_size = u64::try_from(storage_data.len()).map_err(|_| {
1668            Error::InvalidDefinition(format!(
1669                "dataset '{}' storage byte length exceeds u64 capacity",
1670                dataset.name
1671            ))
1672        })?;
1673
1674        prepared.push(PreparedDataset {
1675            dataset,
1676            filters,
1677            raw_data: storage_data,
1678            data_size,
1679            chunk_index,
1680        });
1681    }
1682    Ok(prepared)
1683}
1684
1685fn prepare_raw_dataset_storage(
1686    raw_data: &[u8],
1687    shape: &[u64],
1688    layout: &PlannedLayout,
1689    filters: &[FilterDescription],
1690    element_size: usize,
1691    max_shape: Option<&[u64]>,
1692) -> Result<(Vec<u8>, Option<PreparedChunkIndex>)> {
1693    let unlimited = has_unlimited_max_shape(max_shape);
1694    match layout {
1695        PlannedLayout::Contiguous | PlannedLayout::Compact => Ok((raw_data.to_vec(), None)),
1696        PlannedLayout::Chunked { chunk_shape } => {
1697            if filters.is_empty() {
1698                let storage = chunked_storage_data(raw_data, shape, chunk_shape, element_size)?;
1699                // Fixed max dims use the compact implicit index; unlimited dims
1700                // require a real (B-tree v2) index.
1701                let index = if unlimited {
1702                    unfiltered_btree_chunk_index(shape, chunk_shape, element_size)?
1703                } else {
1704                    None
1705                };
1706                Ok((storage, index))
1707            } else if chunk_count(shape, chunk_shape)? <= 1 && !unlimited {
1708                let storage_data =
1709                    chunked_storage_data(raw_data, shape, chunk_shape, element_size)?;
1710                if storage_data.is_empty() {
1711                    Ok((storage_data, None))
1712                } else {
1713                    Ok((
1714                        apply_write_filters(&storage_data, filters, element_size)?,
1715                        None,
1716                    ))
1717                }
1718            } else {
1719                let kind = if unlimited {
1720                    ChunkIndexKind::BtreeV2
1721                } else {
1722                    ChunkIndexKind::FixedArray
1723                };
1724                filtered_chunk_storage_data(
1725                    raw_data,
1726                    shape,
1727                    chunk_shape,
1728                    element_size,
1729                    filters,
1730                    kind,
1731                )
1732            }
1733        }
1734    }
1735}
1736
1737fn stabilize_vlen_dataset_storage(
1738    prepared: &mut [PreparedDataset<'_>],
1739    planned_attributes: &PlannedAttributes,
1740    data_start_address: u64,
1741    heap: &[u8],
1742) -> Result<DataHeapLayout> {
1743    let mut layout = compute_data_heap_layout(prepared, data_start_address, heap)?;
1744    for _ in 0..8 {
1745        let mut address_sensitive_size_changed = false;
1746        for (prepared_dataset, refs) in prepared
1747            .iter_mut()
1748            .zip(&planned_attributes.dataset_vlen_refs)
1749        {
1750            let Some(refs) = refs else {
1751                continue;
1752            };
1753            let old_data_size = prepared_dataset.data_size;
1754            let old_chunk_count = prepared_dataset
1755                .chunk_index
1756                .as_ref()
1757                .map(|chunk_index| chunk_index.chunks.len());
1758            let (raw_data, chunk_index) = dataset_vlen_storage_data(
1759                prepared_dataset.dataset,
1760                refs,
1761                layout.heap_address,
1762                &prepared_dataset.filters,
1763            )?;
1764            let data_size = u64::try_from(raw_data.len()).map_err(|_| {
1765                Error::InvalidDefinition(format!(
1766                    "dataset '{}' storage byte length exceeds u64 capacity",
1767                    prepared_dataset.dataset.name
1768                ))
1769            })?;
1770            let new_chunk_count = chunk_index
1771                .as_ref()
1772                .map(|chunk_index| chunk_index.chunks.len());
1773            if old_chunk_count != new_chunk_count {
1774                return Err(Error::InvalidDefinition(format!(
1775                    "dataset '{}' changed filtered chunk index cardinality during VLen address stabilization",
1776                    prepared_dataset.dataset.name
1777                )));
1778            }
1779            prepared_dataset.raw_data = raw_data;
1780            prepared_dataset.data_size = data_size;
1781            prepared_dataset.chunk_index = chunk_index;
1782            if data_size != old_data_size {
1783                address_sensitive_size_changed = true;
1784            }
1785        }
1786        if !address_sensitive_size_changed {
1787            return Ok(layout);
1788        }
1789        layout = compute_data_heap_layout(prepared, data_start_address, heap)?;
1790    }
1791
1792    Err(Error::InvalidDefinition(
1793        "HDF5 VLen dataset storage did not stabilize after repeated address planning".into(),
1794    ))
1795}
1796
1797fn compute_data_heap_layout(
1798    prepared: &[PreparedDataset<'_>],
1799    data_start_address: u64,
1800    heap: &[u8],
1801) -> Result<DataHeapLayout> {
1802    let mut next_address = data_start_address;
1803    let mut data_addresses = Vec::with_capacity(prepared.len());
1804    for prepared_dataset in prepared {
1805        data_addresses.push(next_address);
1806        if !matches!(prepared_dataset.dataset.layout, PlannedLayout::Compact) {
1807            next_address = align_u64(
1808                checked_add_u64(
1809                    next_address,
1810                    prepared_dataset.raw_data.len() as u64,
1811                    "dataset raw data end",
1812                )?,
1813                8,
1814            );
1815        }
1816    }
1817
1818    let heap_address = if heap.is_empty() { 0 } else { next_address };
1819    let eof_address = align_u64(
1820        checked_add_u64(next_address, heap.len() as u64, "global heap end")?,
1821        8,
1822    );
1823    Ok(DataHeapLayout {
1824        data_addresses,
1825        heap_address,
1826        eof_address,
1827    })
1828}
1829
1830fn plan_group_hierarchy(
1831    prepared: &[PreparedDataset<'_>],
1832    group_attributes: &[GroupAttributeBuilder],
1833) -> Result<GroupHierarchy> {
1834    let mut group_paths = std::collections::BTreeSet::new();
1835    for prepared_dataset in prepared {
1836        let mut parent = parent_path(&prepared_dataset.dataset.name);
1837        while let Some(path) = parent {
1838            group_paths.insert(path.to_string());
1839            parent = parent_path(path);
1840        }
1841    }
1842    for group_attribute in group_attributes {
1843        let mut path = Some(group_attribute.path.as_str());
1844        while let Some(group_path) = path {
1845            group_paths.insert(group_path.to_string());
1846            path = parent_path(group_path);
1847        }
1848    }
1849
1850    for prepared_dataset in prepared {
1851        if group_paths.contains(&prepared_dataset.dataset.name) {
1852            return Err(Error::InvalidDefinition(format!(
1853                "dataset '{}' conflicts with an implicit HDF5 group at the same path",
1854                prepared_dataset.dataset.name
1855            )));
1856        }
1857    }
1858
1859    let paths: Vec<_> = group_paths.into_iter().collect();
1860    let group_indices: std::collections::BTreeMap<_, _> = paths
1861        .iter()
1862        .enumerate()
1863        .map(|(index, path)| (path.as_str(), index))
1864        .collect();
1865    let mut root_names = std::collections::BTreeSet::new();
1866    let mut group_names = vec![std::collections::BTreeSet::new(); paths.len()];
1867    let mut root_links = Vec::new();
1868    let mut group_links = vec![Vec::new(); paths.len()];
1869
1870    for (group_index, path) in paths.iter().enumerate() {
1871        let link = PlannedLink {
1872            name: path_basename(path).to_string(),
1873            target: PlannedLinkTarget::Group(group_index),
1874        };
1875        if let Some(parent) = parent_path(path) {
1876            let parent_index = group_indices[parent];
1877            push_group_link(
1878                &mut group_links[parent_index],
1879                &mut group_names[parent_index],
1880                link,
1881            )?;
1882        } else {
1883            push_group_link(&mut root_links, &mut root_names, link)?;
1884        }
1885    }
1886
1887    for (dataset_index, prepared_dataset) in prepared.iter().enumerate() {
1888        let path = prepared_dataset.dataset.name.as_str();
1889        let link = PlannedLink {
1890            name: path_basename(path).to_string(),
1891            target: PlannedLinkTarget::Dataset(dataset_index),
1892        };
1893        if let Some(parent) = parent_path(path) {
1894            let parent_index = group_indices[parent];
1895            push_group_link(
1896                &mut group_links[parent_index],
1897                &mut group_names[parent_index],
1898                link,
1899            )?;
1900        } else {
1901            push_group_link(&mut root_links, &mut root_names, link)?;
1902        }
1903    }
1904
1905    Ok(GroupHierarchy {
1906        paths,
1907        root_links,
1908        group_links,
1909    })
1910}
1911
1912fn push_group_link(
1913    links: &mut Vec<PlannedLink>,
1914    names: &mut std::collections::BTreeSet<String>,
1915    link: PlannedLink,
1916) -> Result<()> {
1917    if !names.insert(link.name.clone()) {
1918        return Err(Error::InvalidDefinition(format!(
1919            "duplicate HDF5 link name '{}'",
1920            link.name
1921        )));
1922    }
1923    links.push(link);
1924    Ok(())
1925}
1926
1927fn parent_path(path: &str) -> Option<&str> {
1928    path.rsplit_once('/').map(|(parent, _)| parent)
1929}
1930
1931fn path_basename(path: &str) -> &str {
1932    path.rsplit_once('/').map_or(path, |(_, name)| name)
1933}
1934
1935fn placeholder_target_addresses(
1936    prepared: &[PreparedDataset<'_>],
1937    groups: &GroupHierarchy,
1938    group_addresses: &[u64],
1939) -> std::collections::BTreeMap<String, u64> {
1940    object_target_addresses(prepared, &vec![0; prepared.len()], groups, group_addresses)
1941}
1942
1943fn target_addresses(
1944    prepared: &[PreparedDataset<'_>],
1945    header_addresses: &[u64],
1946    groups: &GroupHierarchy,
1947    group_addresses: &[u64],
1948) -> std::collections::BTreeMap<String, u64> {
1949    object_target_addresses(prepared, header_addresses, groups, group_addresses)
1950}
1951
1952fn object_target_addresses(
1953    prepared: &[PreparedDataset<'_>],
1954    header_addresses: &[u64],
1955    groups: &GroupHierarchy,
1956    group_addresses: &[u64],
1957) -> std::collections::BTreeMap<String, u64> {
1958    let mut target_addresses: std::collections::BTreeMap<String, u64> = groups
1959        .paths
1960        .iter()
1961        .zip(group_addresses.iter().copied())
1962        .map(|(path, address)| (path.clone(), address))
1963        .collect();
1964    target_addresses.extend(
1965        prepared
1966            .iter()
1967            .zip(header_addresses.iter().copied())
1968            .map(|(prepared_dataset, address)| (prepared_dataset.dataset.name.clone(), address)),
1969    );
1970    target_addresses
1971}
1972
1973fn resolve_planned_links(
1974    links: &[PlannedLink],
1975    group_addresses: &[u64],
1976    dataset_addresses: &[u64],
1977) -> Result<Vec<(String, u64)>> {
1978    links
1979        .iter()
1980        .map(|link| {
1981            let address = match link.target {
1982                PlannedLinkTarget::Group(index) => {
1983                    *group_addresses.get(index).ok_or_else(|| {
1984                        Error::InvalidDefinition(format!(
1985                            "missing planned address for HDF5 group '{}'",
1986                            link.name
1987                        ))
1988                    })?
1989                }
1990                PlannedLinkTarget::Dataset(index) => {
1991                    *dataset_addresses.get(index).ok_or_else(|| {
1992                        Error::InvalidDefinition(format!(
1993                            "missing planned address for HDF5 dataset '{}'",
1994                            link.name
1995                        ))
1996                    })?
1997                }
1998            };
1999            Ok((link.name.clone(), address))
2000        })
2001        .collect()
2002}
2003
2004#[derive(Debug)]
2005struct FixedArrayChunkEntry {
2006    address: u64,
2007    size: u64,
2008    filter_mask: u32,
2009    scaled_offsets: Vec<u64>,
2010}
2011
2012fn fixed_array_entries(
2013    chunk_index: &PreparedChunkIndex,
2014    data_address: u64,
2015) -> Result<Vec<FixedArrayChunkEntry>> {
2016    chunk_index
2017        .chunks
2018        .iter()
2019        .map(|chunk| {
2020            Ok(FixedArrayChunkEntry {
2021                address: checked_add_u64(
2022                    data_address,
2023                    chunk.relative_address,
2024                    "fixed array chunk address",
2025                )?,
2026                size: chunk.size,
2027                filter_mask: chunk.filter_mask,
2028                scaled_offsets: chunk.scaled_offsets.clone(),
2029            })
2030        })
2031        .collect()
2032}
2033
2034fn fixed_array_entry_count(chunk_index: &PreparedChunkIndex) -> Result<u64> {
2035    u64::try_from(chunk_index.chunks.len()).map_err(|_| {
2036        Error::InvalidDefinition("fixed array chunk entry count exceeds u64 capacity".into())
2037    })
2038}
2039
2040/// Byte lengths of a chunk index's two on-disk blocks (header, secondary),
2041/// computed without needing final addresses — used to lay out the file.
2042fn chunk_index_block_sizes(chunk_index: &PreparedChunkIndex) -> Result<(usize, usize)> {
2043    let (header, data_block) = encode_chunk_index_blocks(chunk_index, 0, 0, 0)?;
2044    Ok((header.len(), data_block.len()))
2045}
2046
2047/// Encode a chunk index's header and secondary block for the given addresses.
2048/// The header goes at `header_address`, the secondary block (fixed-array data
2049/// block or B-tree leaf) at `data_block_address`, and chunk data starts at
2050/// `data_address`.
2051fn encode_chunk_index_blocks(
2052    chunk_index: &PreparedChunkIndex,
2053    header_address: u64,
2054    data_block_address: u64,
2055    data_address: u64,
2056) -> Result<(Vec<u8>, Vec<u8>)> {
2057    let entries = fixed_array_entries(chunk_index, data_address)?;
2058    let count = fixed_array_entry_count(chunk_index)?;
2059    match chunk_index.kind {
2060        ChunkIndexKind::FixedArray => Ok((
2061            encode_fixed_array_chunk_index_header(
2062                count,
2063                chunk_index.chunk_size_len,
2064                data_block_address,
2065            )?,
2066            encode_fixed_array_chunk_index_data_block(
2067                header_address,
2068                &entries,
2069                chunk_index.chunk_size_len,
2070            )?,
2071        )),
2072        ChunkIndexKind::BtreeV2 => {
2073            let (record_type, record_size, node_size) = btree_v2_index_params(chunk_index)?;
2074            Ok((
2075                encode_btree_v2_header(
2076                    record_type,
2077                    record_size,
2078                    node_size,
2079                    data_block_address,
2080                    count,
2081                )?,
2082                encode_btree_v2_leaf(
2083                    record_type,
2084                    record_size,
2085                    node_size,
2086                    &entries,
2087                    chunk_index.chunk_size_len,
2088                )?,
2089            ))
2090        }
2091    }
2092}
2093
2094/// Record type, record size, and single-leaf node size for a v2 B-tree chunk
2095/// index.
2096fn btree_v2_index_params(chunk_index: &PreparedChunkIndex) -> Result<(u8, u16, u32)> {
2097    let filtered = chunk_index.chunk_size_len > 0;
2098    let record_type = if filtered {
2099        BTREE_V2_RECORD_TYPE_FILTERED
2100    } else {
2101        BTREE_V2_RECORD_TYPE_UNFILTERED
2102    };
2103    let ndims = chunk_index
2104        .chunks
2105        .first()
2106        .map_or(0, |chunk| chunk.scaled_offsets.len());
2107    let record_size = btree_v2_record_size(ndims, chunk_index.chunk_size_len, filtered)?;
2108    let node_size = btree_v2_node_size(fixed_array_entry_count(chunk_index)?, record_size)?;
2109    Ok((record_type, record_size, node_size))
2110}
2111
2112fn plan_attributes(
2113    plan: &Hdf5WritePlan,
2114    groups: &GroupHierarchy,
2115    target_addresses: &std::collections::BTreeMap<String, u64>,
2116) -> Result<PlannedAttributes> {
2117    let mut heap_objects = Vec::new();
2118    let root = plan_attribute_list(plan.attributes.iter(), target_addresses, &mut heap_objects)?;
2119    let groups = groups
2120        .paths
2121        .iter()
2122        .map(|path| {
2123            plan_attribute_list(
2124                plan.group_attributes.iter().filter_map(|group_attribute| {
2125                    (group_attribute.path == *path).then_some(&group_attribute.attribute)
2126                }),
2127                target_addresses,
2128                &mut heap_objects,
2129            )
2130        })
2131        .collect::<Result<Vec<_>>>()?;
2132    let datasets = plan
2133        .datasets
2134        .iter()
2135        .map(|dataset| {
2136            plan_attribute_list(
2137                dataset.attributes.iter(),
2138                target_addresses,
2139                &mut heap_objects,
2140            )
2141        })
2142        .collect::<Result<Vec<_>>>()?;
2143    let dataset_vlen_refs = plan
2144        .datasets
2145        .iter()
2146        .map(|dataset| plan_dataset_vlen_values(dataset, &mut heap_objects))
2147        .collect::<Result<Vec<_>>>()?;
2148    Ok(PlannedAttributes {
2149        root,
2150        groups,
2151        datasets,
2152        dataset_vlen_refs,
2153        heap_objects,
2154    })
2155}
2156
2157fn plan_attribute_list<'a>(
2158    attributes: impl IntoIterator<Item = &'a AttributeBuilder>,
2159    target_addresses: &std::collections::BTreeMap<String, u64>,
2160    heap_objects: &mut Vec<GlobalHeapObjectPlan>,
2161) -> Result<Vec<PlannedAttribute>> {
2162    attributes
2163        .into_iter()
2164        .map(|attribute| plan_attribute(attribute, target_addresses, heap_objects))
2165        .collect()
2166}
2167
2168fn plan_attribute(
2169    attribute: &AttributeBuilder,
2170    target_addresses: &std::collections::BTreeMap<String, u64>,
2171    heap_objects: &mut Vec<GlobalHeapObjectPlan>,
2172) -> Result<PlannedAttribute> {
2173    attribute.validate()?;
2174    if let Some(values) = &attribute.vlen_string_values {
2175        let refs = vlen_string_heap_references(&attribute.name, values, Some(heap_objects))?;
2176        Ok(PlannedAttribute {
2177            name: attribute.name.clone(),
2178            datatype: attribute.datatype.clone(),
2179            shape: attribute.shape.clone(),
2180            raw_data: PlannedAttributeRaw::GlobalHeapVLenReferences(refs),
2181        })
2182    } else if let Some(values) = &attribute.vlen_sequence_values {
2183        let Datatype::VarLen {
2184            base,
2185            kind: VarLenKind::Sequence,
2186            ..
2187        } = &attribute.datatype
2188        else {
2189            return Err(Error::InvalidDefinition(format!(
2190                "attribute '{}' vlen sequence values require a sequence datatype",
2191                attribute.name
2192            )));
2193        };
2194        validate_vlen_sequence_base(base)?;
2195        let base_size = datatype_element_size(base)?;
2196        let refs =
2197            vlen_sequence_heap_references(&attribute.name, values, base_size, Some(heap_objects))?;
2198        Ok(PlannedAttribute {
2199            name: attribute.name.clone(),
2200            datatype: attribute.datatype.clone(),
2201            shape: attribute.shape.clone(),
2202            raw_data: PlannedAttributeRaw::GlobalHeapVLenReferences(refs),
2203        })
2204    } else if let Some(target_sequences) = &attribute.vlen_object_reference_targets {
2205        let mut refs = Vec::with_capacity(target_sequences.len());
2206        for sequence in target_sequences {
2207            if sequence.is_empty() {
2208                refs.push(VLenHeapReference {
2209                    sequence_len: 0,
2210                    heap_index: 0,
2211                });
2212                continue;
2213            }
2214            let index = checked_heap_index(heap_objects.len() + 1)?;
2215            let mut data = Vec::with_capacity(sequence.len() * usize::from(OFFSET_SIZE));
2216            for target in sequence {
2217                let address = target_addresses.get(target).ok_or_else(|| {
2218                    Error::InvalidDefinition(format!(
2219                        "attribute '{}' references unknown HDF5 object '{}'",
2220                        attribute.name, target
2221                    ))
2222                })?;
2223                data.extend_from_slice(&address.to_le_bytes());
2224            }
2225            let sequence_len = u32::try_from(sequence.len()).map_err(|_| {
2226                Error::UnsupportedFeature(format!(
2227                    "attribute '{}' vlen object-reference sequence exceeds u32 capacity",
2228                    attribute.name
2229                ))
2230            })?;
2231            heap_objects.push(GlobalHeapObjectPlan {
2232                index,
2233                reference_count: 1,
2234                data,
2235            });
2236            refs.push(VLenHeapReference {
2237                sequence_len,
2238                heap_index: index,
2239            });
2240        }
2241        Ok(PlannedAttribute {
2242            name: attribute.name.clone(),
2243            datatype: attribute.datatype.clone(),
2244            shape: attribute.shape.clone(),
2245            raw_data: PlannedAttributeRaw::GlobalHeapVLenReferences(refs),
2246        })
2247    } else if let Some(entries) = &attribute.object_reference_list {
2248        // Inline compound of resolved object references + dimension index,
2249        // one 16-byte entry each (address, u32 dimension, padding).
2250        let mut data = Vec::with_capacity(entries.len() * REFERENCE_LIST_ENTRY_SIZE as usize);
2251        for (target, dimension) in entries {
2252            let address = target_addresses.get(target).ok_or_else(|| {
2253                Error::InvalidDefinition(format!(
2254                    "attribute '{}' references unknown HDF5 object '{}'",
2255                    attribute.name, target
2256                ))
2257            })?;
2258            let start = data.len();
2259            data.extend_from_slice(&address.to_le_bytes());
2260            data.extend_from_slice(&dimension.to_le_bytes());
2261            data.resize(start + REFERENCE_LIST_ENTRY_SIZE as usize, 0);
2262        }
2263        Ok(PlannedAttribute {
2264            name: attribute.name.clone(),
2265            datatype: attribute.datatype.clone(),
2266            shape: attribute.shape.clone(),
2267            raw_data: PlannedAttributeRaw::Inline(data),
2268        })
2269    } else {
2270        Ok(PlannedAttribute {
2271            name: attribute.name.clone(),
2272            datatype: attribute.datatype.clone(),
2273            shape: attribute.shape.clone(),
2274            raw_data: PlannedAttributeRaw::Inline(attribute.raw_data.clone()),
2275        })
2276    }
2277}
2278
2279fn plan_dataset_vlen_values(
2280    dataset: &DatasetBuilder,
2281    heap_objects: &mut Vec<GlobalHeapObjectPlan>,
2282) -> Result<Option<Vec<VLenHeapReference>>> {
2283    if let Some(values) = dataset.vlen_string_values.as_ref() {
2284        return vlen_string_heap_references(&dataset.name, values, Some(heap_objects)).map(Some);
2285    }
2286    if let Some(values) = dataset.vlen_sequence_values.as_ref() {
2287        let Datatype::VarLen {
2288            base,
2289            kind: VarLenKind::Sequence,
2290            ..
2291        } = &dataset.datatype
2292        else {
2293            return Err(Error::InvalidDefinition(format!(
2294                "dataset '{}' vlen sequence values require a sequence datatype",
2295                dataset.name
2296            )));
2297        };
2298        validate_vlen_sequence_base(base)?;
2299        let base_size = datatype_element_size(base)?;
2300        return vlen_sequence_heap_references(&dataset.name, values, base_size, Some(heap_objects))
2301            .map(Some);
2302    }
2303    Ok(None)
2304}
2305
2306/// Plan the global-heap references for variable-length string values.
2307///
2308/// With `heap_objects` present the strings are written to the heap and each
2309/// reference points at its real index; with `None` only the sequence lengths
2310/// are computed (the placeholder pass used for address sizing, heap index 0).
2311fn vlen_string_heap_references(
2312    context: &str,
2313    values: &[String],
2314    mut heap_objects: Option<&mut Vec<GlobalHeapObjectPlan>>,
2315) -> Result<Vec<VLenHeapReference>> {
2316    let mut refs = Vec::with_capacity(values.len());
2317    for value in values {
2318        // The stored object is the string plus a NUL terminator.
2319        let sequence_len = value
2320            .len()
2321            .checked_add(1)
2322            .and_then(|len| u32::try_from(len).ok())
2323            .ok_or_else(|| {
2324                Error::UnsupportedFeature(format!(
2325                    "'{context}' variable-length string exceeds u32 capacity"
2326                ))
2327            })?;
2328        let heap_index = match heap_objects.as_deref_mut() {
2329            Some(heap) => {
2330                let index = checked_heap_index(heap.len() + 1)?;
2331                let mut data = Vec::with_capacity(value.len() + 1);
2332                data.extend_from_slice(value.as_bytes());
2333                data.push(0);
2334                heap.push(GlobalHeapObjectPlan {
2335                    index,
2336                    reference_count: 1,
2337                    data,
2338                });
2339                index
2340            }
2341            None => 0,
2342        };
2343        refs.push(VLenHeapReference {
2344            sequence_len,
2345            heap_index,
2346        });
2347    }
2348    Ok(refs)
2349}
2350
2351/// Plan the global-heap references for variable-length sequence values. As with
2352/// [`vlen_string_heap_references`], `None` computes only sequence lengths.
2353/// Empty sequences never occupy a heap object (index 0).
2354fn vlen_sequence_heap_references(
2355    context: &str,
2356    values: &[Vec<u8>],
2357    base_size: usize,
2358    mut heap_objects: Option<&mut Vec<GlobalHeapObjectPlan>>,
2359) -> Result<Vec<VLenHeapReference>> {
2360    let mut refs = Vec::with_capacity(values.len());
2361    for value in values {
2362        if value.len() % base_size != 0 {
2363            return Err(Error::InvalidDefinition(format!(
2364                "'{context}' variable-length sequence byte length {} is not a multiple of base element size {base_size}",
2365                value.len()
2366            )));
2367        }
2368        let sequence_len = u32::try_from(value.len() / base_size).map_err(|_| {
2369            Error::UnsupportedFeature(format!(
2370                "'{context}' variable-length sequence exceeds u32 element capacity"
2371            ))
2372        })?;
2373        let heap_index = match heap_objects.as_deref_mut() {
2374            Some(heap) if sequence_len != 0 => {
2375                let index = checked_heap_index(heap.len() + 1)?;
2376                heap.push(GlobalHeapObjectPlan {
2377                    index,
2378                    reference_count: 1,
2379                    data: value.clone(),
2380                });
2381                index
2382            }
2383            _ => 0,
2384        };
2385        refs.push(VLenHeapReference {
2386            sequence_len,
2387            heap_index,
2388        });
2389    }
2390    Ok(refs)
2391}
2392
2393fn dataset_vlen_storage_data(
2394    dataset: &DatasetBuilder,
2395    refs: &[VLenHeapReference],
2396    heap_address: u64,
2397    filters: &[FilterDescription],
2398) -> Result<(Vec<u8>, Option<PreparedChunkIndex>)> {
2399    let element_size = datatype_element_size(&dataset.datatype)?;
2400    let raw_data = encode_vlen_heap_references(refs, heap_address);
2401    if matches!(&dataset.layout, PlannedLayout::Compact) {
2402        return Err(Error::UnsupportedFeature(format!(
2403            "compact variable-length HDF5 datasets are not emitted yet: '{}'",
2404            dataset.name
2405        )));
2406    }
2407    prepare_raw_dataset_storage(
2408        &raw_data,
2409        &dataset.shape,
2410        &dataset.layout,
2411        filters,
2412        element_size,
2413        dataset.max_shape.as_deref(),
2414    )
2415}
2416
2417fn checked_heap_index(index: usize) -> Result<u16> {
2418    u16::try_from(index).map_err(|_| {
2419        Error::UnsupportedFeature("global heap object count exceeds u16 capacity".into())
2420    })
2421}
2422
2423/// Smallest global heap collection libhdf5 accepts (H5HG_MINSIZE).
2424const GLOBAL_HEAP_MIN_COLLECTION_SIZE: u64 = 4096;
2425
2426fn encode_global_heap_collection(objects: &[GlobalHeapObjectPlan]) -> Result<Vec<u8>> {
2427    let mut body = Vec::new();
2428    for object in objects {
2429        body.extend_from_slice(&object.index.to_le_bytes());
2430        body.extend_from_slice(&object.reference_count.to_le_bytes());
2431        body.extend_from_slice(&[0; 4]);
2432        body.extend_from_slice(&(object.data.len() as u64).to_le_bytes());
2433        body.extend_from_slice(&object.data);
2434        let padded_len = align_u64(object.data.len() as u64, 8) as usize;
2435        body.resize(body.len() + (padded_len - object.data.len()), 0);
2436    }
2437
2438    // Close the collection with the free-space object (index 0) covering the
2439    // remaining bytes, its declared size including its own 16-byte header.
2440    // libhdf5 also rejects collections below its 4096-byte floor.
2441    let used = checked_add_u64(16, body.len() as u64, "global heap size")?;
2442    let collection_size = align_u64(checked_add_u64(used, 16, "global heap free object end")?, 8)
2443        .max(GLOBAL_HEAP_MIN_COLLECTION_SIZE);
2444    let free_space = collection_size - used;
2445    body.extend_from_slice(&0u16.to_le_bytes());
2446    body.extend_from_slice(&0u16.to_le_bytes());
2447    body.extend_from_slice(&[0; 4]);
2448    body.extend_from_slice(&free_space.to_le_bytes());
2449
2450    let mut bytes = Vec::with_capacity(checked_usize(collection_size, "global heap size")?);
2451    bytes.extend_from_slice(b"GCOL");
2452    bytes.push(1);
2453    bytes.extend_from_slice(&[0, 0, 0]);
2454    bytes.extend_from_slice(&collection_size.to_le_bytes());
2455    bytes.extend_from_slice(&body);
2456    bytes.resize(checked_usize(collection_size, "global heap size")?, 0);
2457    Ok(bytes)
2458}
2459
2460fn encode_superblock_v2(root_address: u64, eof_address: u64) -> Vec<u8> {
2461    let mut bytes = Vec::with_capacity(SUPERBLOCK_V2_SIZE);
2462    bytes.extend_from_slice(&HDF5_MAGIC);
2463    bytes.push(2);
2464    bytes.push(OFFSET_SIZE);
2465    bytes.push(LENGTH_SIZE);
2466    bytes.push(0);
2467    bytes.extend_from_slice(&0u64.to_le_bytes());
2468    bytes.extend_from_slice(&UNDEFINED_ADDRESS.to_le_bytes());
2469    bytes.extend_from_slice(&eof_address.to_le_bytes());
2470    bytes.extend_from_slice(&root_address.to_le_bytes());
2471    let checksum = jenkins_lookup3(&bytes);
2472    bytes.extend_from_slice(&checksum.to_le_bytes());
2473    bytes
2474}
2475
2476fn encode_group_header_from_plan(
2477    links: &[PlannedLink],
2478    group_addresses: &[u64],
2479    dataset_addresses: &[u64],
2480    attributes: &[PlannedAttribute],
2481    heap_address: u64,
2482) -> Result<Vec<u8>> {
2483    let resolved_links = resolve_planned_links(links, group_addresses, dataset_addresses)?;
2484    encode_group_header_from_links(&resolved_links, attributes, heap_address)
2485}
2486
2487fn encode_group_header_from_links(
2488    links: &[(String, u64)],
2489    attributes: &[PlannedAttribute],
2490    heap_address: u64,
2491) -> Result<Vec<u8>> {
2492    // New-style groups must carry a Link Info message so readers can resolve
2493    // the link storage style (libhdf5 rejects group headers without one).
2494    let mut messages = vec![
2495        HeaderMessage {
2496            type_id: MSG_LINK_INFO,
2497            payload: encode_link_info_message(),
2498        },
2499        HeaderMessage {
2500            type_id: MSG_GROUP_INFO,
2501            payload: encode_group_info_message(),
2502        },
2503    ];
2504    messages.extend(encode_attribute_header_messages(attributes, heap_address)?);
2505    let link_messages: Result<Vec<_>> = links
2506        .iter()
2507        .map(|(name, address)| {
2508            Ok(HeaderMessage {
2509                type_id: MSG_LINK,
2510                payload: encode_hard_link_message(name.as_str(), *address)?,
2511            })
2512        })
2513        .collect();
2514    messages.extend(link_messages?);
2515    encode_object_header_v2(&messages)
2516}
2517
2518/// Link Info message: version 0, no creation-order tracking. Links are stored
2519/// compactly in the header, so the fractal heap and name-index B-tree
2520/// addresses are undefined.
2521fn encode_link_info_message() -> Vec<u8> {
2522    let mut bytes = Vec::with_capacity(18);
2523    bytes.push(0);
2524    bytes.push(0);
2525    bytes.extend_from_slice(&UNDEFINED_ADDRESS.to_le_bytes());
2526    bytes.extend_from_slice(&UNDEFINED_ADDRESS.to_le_bytes());
2527    bytes
2528}
2529
2530/// Group Info message: version 0, default phase-change values and estimates.
2531fn encode_group_info_message() -> Vec<u8> {
2532    vec![0, 0]
2533}
2534
2535fn encode_dataset_header(
2536    dataset: &PreparedDataset<'_>,
2537    attributes: &[PlannedAttribute],
2538    data_address: u64,
2539    heap_address: u64,
2540) -> Result<Vec<u8>> {
2541    let mut messages = Vec::new();
2542    messages.push(HeaderMessage {
2543        type_id: MSG_DATASPACE,
2544        payload: encode_dataspace_message(
2545            &dataset.dataset.shape,
2546            dataset.dataset.max_shape.as_deref(),
2547        )?,
2548    });
2549    messages.push(HeaderMessage {
2550        type_id: MSG_DATATYPE,
2551        payload: encode_datatype_message(&dataset.dataset.datatype)?,
2552    });
2553    let alloc_time = space_allocation_time(dataset);
2554    messages.push(HeaderMessage {
2555        type_id: MSG_FILL_VALUE,
2556        payload: match &dataset.dataset.fill_value {
2557            Some(fill_value) => encode_fill_value_message(fill_value, alloc_time)?,
2558            None => encode_default_fill_value_message(alloc_time),
2559        },
2560    });
2561    if !dataset.filters.is_empty() {
2562        messages.push(HeaderMessage {
2563            type_id: MSG_FILTER_PIPELINE,
2564            payload: encode_filter_pipeline_message(&dataset.filters)?,
2565        });
2566    }
2567    messages.push(HeaderMessage {
2568        type_id: MSG_DATA_LAYOUT,
2569        payload: encode_data_layout_message(dataset, data_address)?,
2570    });
2571    messages.extend(encode_attribute_header_messages(attributes, heap_address)?);
2572    encode_object_header_v2(&messages)
2573}
2574
2575fn encode_data_layout_message(dataset: &PreparedDataset<'_>, data_address: u64) -> Result<Vec<u8>> {
2576    let storage_address = if dataset.data_size == 0 {
2577        UNDEFINED_ADDRESS
2578    } else {
2579        data_address
2580    };
2581    match &dataset.dataset.layout {
2582        PlannedLayout::Contiguous => Ok(encode_contiguous_layout_message(
2583            storage_address,
2584            dataset.data_size,
2585        )),
2586        PlannedLayout::Chunked { chunk_shape } => {
2587            let element_size = chunk_element_size(&dataset.dataset.datatype)?;
2588            if let Some(chunk_index) = &dataset.chunk_index {
2589                match chunk_index.kind {
2590                    ChunkIndexKind::FixedArray => {
2591                        let page_bits =
2592                            fixed_array_page_bits(fixed_array_entry_count(chunk_index)?)?;
2593                        encode_fixed_array_chunked_layout_message(
2594                            storage_address,
2595                            chunk_shape,
2596                            element_size,
2597                            page_bits,
2598                        )
2599                    }
2600                    ChunkIndexKind::BtreeV2 => {
2601                        let (_, _, node_size) = btree_v2_index_params(chunk_index)?;
2602                        encode_btree_v2_chunked_layout_message(
2603                            storage_address,
2604                            chunk_shape,
2605                            element_size,
2606                            node_size,
2607                        )
2608                    }
2609                }
2610            } else if dataset.filters.is_empty() {
2611                // Fixed max dims, unfiltered: the compact implicit index.
2612                encode_implicit_chunked_layout_message(storage_address, chunk_shape, element_size)
2613            } else {
2614                // Fixed max dims, filtered, single chunk.
2615                encode_single_chunk_layout_message(
2616                    storage_address,
2617                    chunk_shape,
2618                    dataset.data_size,
2619                    element_size,
2620                )
2621            }
2622        }
2623        PlannedLayout::Compact => encode_compact_layout_message(&dataset.raw_data),
2624    }
2625}
2626
2627/// Byte width of one dataset element as stored in chunked data, appended as
2628/// the trailing chunk dimension in v3/v4 layout messages.
2629fn chunk_element_size(datatype: &Datatype) -> Result<u32> {
2630    let size = datatype_element_size(datatype)?;
2631    u32::try_from(size).map_err(|_| {
2632        Error::UnsupportedFeature("chunked element size exceeds layout message capacity".into())
2633    })
2634}
2635
2636/// Fixed-array data-block page size (log2). Readers reject 0, and any entry
2637/// count above one page would require the paged data-block format the writer
2638/// does not emit, so size the page to hold every entry.
2639fn fixed_array_page_bits(num_entries: u64) -> Result<u8> {
2640    let mut bits = 10u8;
2641    while (1u64 << bits) < num_entries {
2642        bits += 1;
2643        if bits > 25 {
2644            return Err(Error::UnsupportedFeature(
2645                "fixed array chunk count exceeds the single-page data block limit".into(),
2646            ));
2647        }
2648    }
2649    Ok(bits)
2650}
2651
2652fn encode_attribute_header_messages(
2653    attributes: &[PlannedAttribute],
2654    heap_address: u64,
2655) -> Result<Vec<HeaderMessage>> {
2656    attributes
2657        .iter()
2658        .map(|attribute| {
2659            Ok(HeaderMessage {
2660                type_id: MSG_ATTRIBUTE,
2661                payload: encode_attribute_message(attribute, heap_address)?,
2662            })
2663        })
2664        .collect()
2665}
2666
2667fn encode_object_header_v2(messages: &[HeaderMessage]) -> Result<Vec<u8>> {
2668    let mut message_bytes = Vec::new();
2669    for message in messages {
2670        if message.payload.len() > u16::MAX as usize {
2671            return Err(Error::UnsupportedFeature(
2672                "object header continuation messages are not emitted yet".into(),
2673            ));
2674        }
2675        message_bytes.push(message.type_id);
2676        message_bytes.extend_from_slice(&(message.payload.len() as u16).to_le_bytes());
2677        message_bytes.push(0);
2678        message_bytes.extend_from_slice(&message.payload);
2679    }
2680
2681    let (size_flag, size_width) = size_width_for(message_bytes.len() as u64)?;
2682    let mut bytes = Vec::new();
2683    bytes.extend_from_slice(b"OHDR");
2684    bytes.push(2);
2685    bytes.push(size_flag);
2686    write_uvar(message_bytes.len() as u64, size_width, &mut bytes);
2687    bytes.extend_from_slice(&message_bytes);
2688    let checksum = jenkins_lookup3(&bytes);
2689    bytes.extend_from_slice(&checksum.to_le_bytes());
2690    Ok(bytes)
2691}
2692
2693fn encode_hard_link_message(name: &str, address: u64) -> Result<Vec<u8>> {
2694    let name_bytes = name.as_bytes();
2695    let (name_len_flag, name_len_width) = size_width_for(name_bytes.len() as u64)?;
2696    let utf8_flag = if name.is_ascii() { 0 } else { 0x10 };
2697    let mut bytes = Vec::new();
2698    bytes.push(1);
2699    bytes.push(name_len_flag | utf8_flag);
2700    if utf8_flag != 0 {
2701        bytes.push(1);
2702    }
2703    write_uvar(name_bytes.len() as u64, name_len_width, &mut bytes);
2704    bytes.extend_from_slice(name_bytes);
2705    bytes.extend_from_slice(&address.to_le_bytes());
2706    Ok(bytes)
2707}
2708
2709fn encode_attribute_message(attribute: &PlannedAttribute, heap_address: u64) -> Result<Vec<u8>> {
2710    let name_bytes = attribute.name.as_bytes();
2711    let datatype = encode_datatype_message(&attribute.datatype)?;
2712    let dataspace = encode_dataspace_message(&attribute.shape, None)?;
2713    let raw_data = attribute.raw_data(heap_address);
2714    if name_bytes.len() + 1 > u16::MAX as usize
2715        || datatype.len() > u16::MAX as usize
2716        || dataspace.len() > u16::MAX as usize
2717    {
2718        return Err(Error::UnsupportedFeature(format!(
2719            "attribute '{}' is too large for compact attribute message emission",
2720            attribute.name
2721        )));
2722    }
2723
2724    let mut bytes = Vec::new();
2725    bytes.push(3);
2726    bytes.push(0);
2727    bytes.extend_from_slice(&((name_bytes.len() + 1) as u16).to_le_bytes());
2728    bytes.extend_from_slice(&(datatype.len() as u16).to_le_bytes());
2729    bytes.extend_from_slice(&(dataspace.len() as u16).to_le_bytes());
2730    bytes.push(if attribute.name.is_ascii() { 0 } else { 1 });
2731    bytes.extend_from_slice(name_bytes);
2732    bytes.push(0);
2733    bytes.extend_from_slice(&datatype);
2734    bytes.extend_from_slice(&dataspace);
2735    bytes.extend_from_slice(&raw_data);
2736    Ok(bytes)
2737}
2738
2739fn encode_dataspace_message(shape: &[u64], max_shape: Option<&[u64]>) -> Result<Vec<u8>> {
2740    if shape.len() > u8::MAX as usize {
2741        return Err(Error::InvalidDefinition(
2742            "dataset rank exceeds HDF5 rank field capacity".into(),
2743        ));
2744    }
2745    let mut bytes = Vec::new();
2746    bytes.push(2);
2747    bytes.push(shape.len() as u8);
2748    bytes.push(if max_shape.is_some() { 0x01 } else { 0x00 });
2749    bytes.push(if shape.is_empty() { 0 } else { 1 });
2750    for &dim in shape {
2751        bytes.extend_from_slice(&dim.to_le_bytes());
2752    }
2753    if let Some(max_shape) = max_shape {
2754        for &dim in max_shape {
2755            bytes.extend_from_slice(&dim.to_le_bytes());
2756        }
2757    }
2758    Ok(bytes)
2759}
2760
2761fn encode_datatype_message(datatype: &Datatype) -> Result<Vec<u8>> {
2762    match datatype {
2763        Datatype::FixedPoint {
2764            size,
2765            signed,
2766            byte_order,
2767        } => {
2768            let mut flags = byte_order_flag(*byte_order);
2769            if *signed {
2770                flags |= 0x08;
2771            }
2772            let mut bytes = Vec::new();
2773            bytes.extend_from_slice(&class_word(0, 1, flags).to_le_bytes());
2774            bytes.extend_from_slice(&u32::from(*size).to_le_bytes());
2775            bytes.extend_from_slice(&0u16.to_le_bytes());
2776            bytes.extend_from_slice(&(u16::from(*size) * 8).to_le_bytes());
2777            Ok(bytes)
2778        }
2779        Datatype::FloatingPoint { size, byte_order } => {
2780            let (exp_location, exp_size, mantissa_size, exp_bias, sign_location) = match *size {
2781                4 => (23u8, 8u8, 23u8, 127u32, 31u32),
2782                8 => (52u8, 11u8, 52u8, 1023u32, 63u32),
2783                other => {
2784                    return Err(Error::UnsupportedFeature(format!(
2785                        "unsupported floating-point byte width {other}"
2786                    )))
2787                }
2788            };
2789            // Class flags: byte order in bit 0, implied mantissa
2790            // normalization in bits 4-5 (IEEE), and the sign-bit position in
2791            // bits 8-15 (readers reject a sign bit overlapping the mantissa).
2792            let flags = byte_order_flag(*byte_order) | 0x20 | (sign_location << 8);
2793            let mut bytes = Vec::new();
2794            bytes.extend_from_slice(&class_word(1, 1, flags).to_le_bytes());
2795            bytes.extend_from_slice(&u32::from(*size).to_le_bytes());
2796            bytes.extend_from_slice(&0u16.to_le_bytes());
2797            bytes.extend_from_slice(&(u16::from(*size) * 8).to_le_bytes());
2798            bytes.push(exp_location);
2799            bytes.push(exp_size);
2800            bytes.push(0);
2801            bytes.push(mantissa_size);
2802            bytes.extend_from_slice(&exp_bias.to_le_bytes());
2803            Ok(bytes)
2804        }
2805        Datatype::String {
2806            size,
2807            encoding,
2808            padding,
2809        } => match size {
2810            StringSize::Fixed(size) => {
2811                let flags = string_padding_bits(*padding) | (string_encoding_bits(*encoding) << 4);
2812                let mut bytes = Vec::new();
2813                bytes.extend_from_slice(&class_word(3, 1, flags).to_le_bytes());
2814                bytes.extend_from_slice(&size.to_le_bytes());
2815                Ok(bytes)
2816            }
2817            // A variable-length string is a class-9 (variable-length) datatype
2818            // over the 1-byte fixed-point base libhdf5 uses; class 3 has no
2819            // zero-size encoding.
2820            StringSize::Variable => encode_datatype_message(&Datatype::VarLen {
2821                base: Box::new(Datatype::FixedPoint {
2822                    size: 1,
2823                    signed: false,
2824                    byte_order: ByteOrder::LittleEndian,
2825                }),
2826                kind: VarLenKind::String,
2827                encoding: *encoding,
2828                padding: *padding,
2829            }),
2830        },
2831        Datatype::Reference { ref_type, size } => {
2832            let flags = match ref_type {
2833                hdf5_core::ReferenceType::Object => 0,
2834                hdf5_core::ReferenceType::DatasetRegion => 1,
2835            };
2836            let mut bytes = Vec::new();
2837            bytes.extend_from_slice(&class_word(7, 1, flags).to_le_bytes());
2838            bytes.extend_from_slice(&u32::from(*size).to_le_bytes());
2839            Ok(bytes)
2840        }
2841        Datatype::VarLen {
2842            base,
2843            kind,
2844            encoding,
2845            padding,
2846        } => {
2847            let kind_bits = match kind {
2848                VarLenKind::Sequence => 0,
2849                VarLenKind::String => 1,
2850                VarLenKind::Unknown(value) => u32::from(*value),
2851            };
2852            let flags = kind_bits
2853                | (string_padding_bits(*padding) << 4)
2854                | (string_encoding_bits(*encoding) << 8);
2855            let mut bytes = Vec::new();
2856            bytes.extend_from_slice(&class_word(9, 1, flags).to_le_bytes());
2857            bytes.extend_from_slice(&16u32.to_le_bytes());
2858            bytes.extend_from_slice(&encode_datatype_message(base)?);
2859            Ok(bytes)
2860        }
2861        Datatype::Bitfield { size, byte_order } => {
2862            let mut bytes = Vec::new();
2863            bytes.extend_from_slice(&class_word(4, 1, byte_order_flag(*byte_order)).to_le_bytes());
2864            bytes.extend_from_slice(&u32::from(*size).to_le_bytes());
2865            bytes.extend_from_slice(&0u16.to_le_bytes());
2866            bytes.extend_from_slice(&(u16::from(*size) * 8).to_le_bytes());
2867            Ok(bytes)
2868        }
2869        Datatype::Opaque { size, tag } => {
2870            let tag_len = if tag.is_empty() {
2871                0
2872            } else {
2873                checked_add_usize(tag.len(), 1, "opaque datatype tag length")?
2874            };
2875            if tag.as_bytes().contains(&0) {
2876                return Err(Error::InvalidDefinition(
2877                    "opaque datatype tag cannot contain NUL bytes".into(),
2878                ));
2879            }
2880            let flags = u32::try_from(tag_len).map_err(|_| {
2881                Error::UnsupportedFeature("opaque datatype tag exceeds u32 capacity".into())
2882            })?;
2883            if flags > 0xff {
2884                return Err(Error::UnsupportedFeature(
2885                    "opaque datatype tag exceeds HDF5 class flag capacity".into(),
2886                ));
2887            }
2888            let mut bytes = Vec::new();
2889            bytes.extend_from_slice(&class_word(5, 1, flags).to_le_bytes());
2890            bytes.extend_from_slice(&size.to_le_bytes());
2891            if tag_len > 0 {
2892                bytes.extend_from_slice(tag.as_bytes());
2893                bytes.push(0);
2894                align_vec(&mut bytes, 8);
2895            }
2896            Ok(bytes)
2897        }
2898        Datatype::Compound { size, fields } => {
2899            let n_fields = u32::try_from(fields.len()).map_err(|_| {
2900                Error::UnsupportedFeature(
2901                    "compound datatype member count exceeds u32 capacity".into(),
2902                )
2903            })?;
2904            if n_fields > 0xffff {
2905                return Err(Error::UnsupportedFeature(
2906                    "compound datatype member count exceeds HDF5 class flag capacity".into(),
2907                ));
2908            }
2909            let offset_size = compound_member_offset_size(*size);
2910            let mut bytes = Vec::new();
2911            bytes.extend_from_slice(&class_word(6, 3, n_fields).to_le_bytes());
2912            bytes.extend_from_slice(&size.to_le_bytes());
2913            let mut names = std::collections::BTreeSet::new();
2914            for field in fields {
2915                encode_datatype_name(&field.name, "compound field")?;
2916                if !names.insert(field.name.as_str()) {
2917                    return Err(Error::InvalidDefinition(format!(
2918                        "duplicate compound field '{}'",
2919                        field.name
2920                    )));
2921                }
2922                let field_size =
2923                    u32::try_from(datatype_element_size(&field.datatype)?).map_err(|_| {
2924                        Error::UnsupportedFeature(format!(
2925                            "compound field '{}' size exceeds u32 capacity",
2926                            field.name
2927                        ))
2928                    })?;
2929                let field_end = field.byte_offset.checked_add(field_size).ok_or_else(|| {
2930                    Error::InvalidDefinition(format!(
2931                        "compound field '{}' offset overflows datatype size",
2932                        field.name
2933                    ))
2934                })?;
2935                if field_end > *size {
2936                    return Err(Error::InvalidDefinition(format!(
2937                        "compound field '{}' byte range {}..{} is outside datatype size {size}",
2938                        field.name, field.byte_offset, field_end
2939                    )));
2940                }
2941                bytes.extend_from_slice(field.name.as_bytes());
2942                bytes.push(0);
2943                write_uvar(u64::from(field.byte_offset), offset_size, &mut bytes);
2944                bytes.extend_from_slice(&encode_datatype_message(&field.datatype)?);
2945            }
2946            Ok(bytes)
2947        }
2948        Datatype::Enum { base, members } => {
2949            let n_members = u32::try_from(members.len()).map_err(|_| {
2950                Error::UnsupportedFeature("enum datatype member count exceeds u32 capacity".into())
2951            })?;
2952            if n_members > 0xffff {
2953                return Err(Error::UnsupportedFeature(
2954                    "enum datatype member count exceeds HDF5 class flag capacity".into(),
2955                ));
2956            }
2957            if !matches!(base.as_ref(), Datatype::FixedPoint { .. }) {
2958                return Err(Error::InvalidDefinition(
2959                    "enum datatype base must be fixed-point integer".into(),
2960                ));
2961            }
2962            let value_size = datatype_element_size(base)?;
2963            let value_size_u32 = u32::try_from(value_size).map_err(|_| {
2964                Error::UnsupportedFeature("enum datatype value size exceeds u32 capacity".into())
2965            })?;
2966            let mut bytes = Vec::new();
2967            bytes.extend_from_slice(&class_word(8, 3, n_members).to_le_bytes());
2968            bytes.extend_from_slice(&value_size_u32.to_le_bytes());
2969            bytes.extend_from_slice(&encode_datatype_message(base)?);
2970            let mut names = std::collections::BTreeSet::new();
2971            for member in members {
2972                encode_datatype_name(&member.name, "enum member")?;
2973                if !names.insert(member.name.as_str()) {
2974                    return Err(Error::InvalidDefinition(format!(
2975                        "duplicate enum member '{}'",
2976                        member.name
2977                    )));
2978                }
2979                bytes.extend_from_slice(member.name.as_bytes());
2980                bytes.push(0);
2981            }
2982            for member in members {
2983                if member.value.len() != value_size {
2984                    return Err(Error::InvalidDefinition(format!(
2985                        "enum member '{}' value byte length must be {value_size}, got {}",
2986                        member.name,
2987                        member.value.len()
2988                    )));
2989                }
2990                bytes.extend_from_slice(&member.value);
2991            }
2992            Ok(bytes)
2993        }
2994        Datatype::Array { base, dims } => {
2995            let rank = u8::try_from(dims.len()).map_err(|_| {
2996                Error::UnsupportedFeature("array datatype rank exceeds HDF5 capacity".into())
2997            })?;
2998            let element_size = datatype_element_size(datatype)?;
2999            let element_size_u32 = u32::try_from(element_size).map_err(|_| {
3000                Error::UnsupportedFeature("array datatype element size exceeds u32 capacity".into())
3001            })?;
3002            let mut bytes = Vec::new();
3003            bytes.extend_from_slice(&class_word(10, 3, 0).to_le_bytes());
3004            bytes.extend_from_slice(&element_size_u32.to_le_bytes());
3005            bytes.push(rank);
3006            for &dim in dims {
3007                let dim = u32::try_from(dim).map_err(|_| {
3008                    Error::UnsupportedFeature(
3009                        "array datatype dimension exceeds HDF5 u32 capacity".into(),
3010                    )
3011                })?;
3012                bytes.extend_from_slice(&dim.to_le_bytes());
3013            }
3014            bytes.extend_from_slice(&encode_datatype_message(base)?);
3015            Ok(bytes)
3016        }
3017    }
3018}
3019
3020fn encode_datatype_name(name: &str, context: &str) -> Result<()> {
3021    if name.is_empty() {
3022        return Err(Error::InvalidDefinition(format!(
3023            "{context} name must not be empty"
3024        )));
3025    }
3026    if name.as_bytes().contains(&0) {
3027        return Err(Error::InvalidDefinition(format!(
3028            "{context} name cannot contain NUL bytes"
3029        )));
3030    }
3031    Ok(())
3032}
3033
3034fn compound_member_offset_size(size: u32) -> usize {
3035    match size {
3036        0..=0xff => 1,
3037        0x100..=0xffff => 2,
3038        0x1_0000..=0xff_ffff => 3,
3039        _ => 4,
3040    }
3041}
3042
3043fn align_vec(bytes: &mut Vec<u8>, align: usize) {
3044    let padding = (align - (bytes.len() % align)) % align;
3045    bytes.resize(bytes.len() + padding, 0);
3046}
3047
3048fn encode_contiguous_layout_message(address: u64, size: u64) -> Vec<u8> {
3049    let mut bytes = Vec::with_capacity(18);
3050    bytes.push(3);
3051    bytes.push(1);
3052    bytes.extend_from_slice(&address.to_le_bytes());
3053    bytes.extend_from_slice(&size.to_le_bytes());
3054    bytes
3055}
3056
3057fn encode_compact_layout_message(data: &[u8]) -> Result<Vec<u8>> {
3058    let size = u16::try_from(data.len()).map_err(|_| {
3059        Error::UnsupportedFeature(
3060            "compact HDF5 dataset data exceeds u16 byte length capacity".into(),
3061        )
3062    })?;
3063    let mut bytes = Vec::with_capacity(4 + data.len());
3064    bytes.push(3);
3065    bytes.push(0);
3066    bytes.extend_from_slice(&size.to_le_bytes());
3067    bytes.extend_from_slice(data);
3068    Ok(bytes)
3069}
3070
3071/// Space-allocation-time codes for the v3 fill value message.
3072const ALLOC_TIME_EARLY: u8 = 1;
3073const ALLOC_TIME_LATE: u8 = 2;
3074const ALLOC_TIME_INCREMENTAL: u8 = 3;
3075/// Fill-value-write-time "if set" (bits 2-3) shared by every emitted message.
3076const FILL_WRITE_TIME_IFSET: u8 = 2 << 2;
3077/// Flag bit 5: the message carries a defined fill value (size + data).
3078const FILL_VALUE_DEFINED: u8 = 0x20;
3079
3080/// When the file's data for each layout is allocated. Implicit chunk indexes
3081/// require early allocation (the writer materializes every chunk up front);
3082/// libhdf5 pairs the other layouts with these defaults.
3083fn space_allocation_time(dataset: &PreparedDataset<'_>) -> u8 {
3084    match &dataset.dataset.layout {
3085        PlannedLayout::Compact => ALLOC_TIME_EARLY,
3086        PlannedLayout::Contiguous => ALLOC_TIME_LATE,
3087        PlannedLayout::Chunked { .. } => {
3088            if dataset.filters.is_empty() {
3089                ALLOC_TIME_EARLY
3090            } else {
3091                ALLOC_TIME_INCREMENTAL
3092            }
3093        }
3094    }
3095}
3096
3097fn encode_fill_value_message(fill_value: &[u8], alloc_time: u8) -> Result<Vec<u8>> {
3098    let size = u32::try_from(fill_value.len()).map_err(|_| {
3099        Error::UnsupportedFeature("HDF5 fill value exceeds u32 byte length capacity".into())
3100    })?;
3101    let mut bytes = Vec::with_capacity(6 + fill_value.len());
3102    bytes.push(3);
3103    bytes.push(alloc_time | FILL_WRITE_TIME_IFSET | FILL_VALUE_DEFINED);
3104    bytes.extend_from_slice(&size.to_le_bytes());
3105    bytes.extend_from_slice(fill_value);
3106    Ok(bytes)
3107}
3108
3109/// Fill value message for datasets without a user fill: allocation/write time
3110/// only, no defined value (readers use the library default of all zeros).
3111fn encode_default_fill_value_message(alloc_time: u8) -> Vec<u8> {
3112    vec![3, alloc_time | FILL_WRITE_TIME_IFSET]
3113}
3114
3115fn encode_filter_pipeline_message(filters: &[FilterDescription]) -> Result<Vec<u8>> {
3116    let filter_count = u8::try_from(filters.len()).map_err(|_| {
3117        Error::UnsupportedFeature("HDF5 filter pipeline exceeds u8 filter count capacity".into())
3118    })?;
3119    let mut bytes = Vec::new();
3120    bytes.push(2);
3121    bytes.push(filter_count);
3122    for filter in filters {
3123        bytes.extend_from_slice(&filter.id.to_le_bytes());
3124        if filter.id >= 256 {
3125            let name_len = filter.name.as_ref().map_or(0usize, |name| name.len() + 1);
3126            let name_len = u16::try_from(name_len).map_err(|_| {
3127                Error::UnsupportedFeature(format!(
3128                    "HDF5 filter {} name exceeds u16 byte length capacity",
3129                    filter.id
3130                ))
3131            })?;
3132            bytes.extend_from_slice(&name_len.to_le_bytes());
3133        }
3134        bytes.extend_from_slice(&0u16.to_le_bytes());
3135        let client_count = u16::try_from(filter.client_data.len()).map_err(|_| {
3136            Error::UnsupportedFeature(format!(
3137                "HDF5 filter {} client data exceeds u16 count capacity",
3138                filter.id
3139            ))
3140        })?;
3141        bytes.extend_from_slice(&client_count.to_le_bytes());
3142        if filter.id >= 256 {
3143            if let Some(name) = &filter.name {
3144                bytes.extend_from_slice(name.as_bytes());
3145                bytes.push(0);
3146            }
3147        }
3148        for value in &filter.client_data {
3149            bytes.extend_from_slice(&value.to_le_bytes());
3150        }
3151    }
3152    Ok(bytes)
3153}
3154
3155/// Flag bit 1 of a v4 chunked layout: the single-chunk index carries the
3156/// filtered chunk size and filter mask inline.
3157const V4_LAYOUT_SINGLE_INDEX_WITH_FILTER: u8 = 0x02;
3158
3159fn encode_single_chunk_layout_message(
3160    address: u64,
3161    chunk_shape: &[u64],
3162    filtered_size: u64,
3163    element_size: u32,
3164) -> Result<Vec<u8>> {
3165    let mut bytes = Vec::with_capacity(22 + chunk_shape.len() * 4 + usize::from(OFFSET_SIZE));
3166    encode_v4_chunked_layout_preamble(
3167        &mut bytes,
3168        V4_LAYOUT_SINGLE_INDEX_WITH_FILTER,
3169        chunk_shape,
3170        element_size,
3171    )?;
3172    bytes.push(1);
3173    bytes.extend_from_slice(&filtered_size.to_le_bytes());
3174    bytes.extend_from_slice(&0u32.to_le_bytes());
3175    bytes.extend_from_slice(&address.to_le_bytes());
3176    Ok(bytes)
3177}
3178
3179/// Write the v4 chunked layout preamble: version, class, flags, rank, and the
3180/// chunk dimensions with the element size appended as the trailing dimension
3181/// (readers validate that dimension against the datatype size).
3182///
3183/// Dimensions use the minimal byte width for the largest value; readers
3184/// recompute that width from the decoded dimensions and reject a mismatch.
3185fn encode_v4_chunked_layout_preamble(
3186    bytes: &mut Vec<u8>,
3187    flags: u8,
3188    chunk_shape: &[u64],
3189    element_size: u32,
3190) -> Result<()> {
3191    if chunk_shape.len() + 1 > u8::MAX as usize {
3192        return Err(Error::InvalidDefinition(
3193            "chunked dataset rank exceeds HDF5 rank field capacity".into(),
3194        ));
3195    }
3196    let max_dim = chunk_shape
3197        .iter()
3198        .copied()
3199        .chain(std::iter::once(u64::from(element_size)))
3200        .max()
3201        .unwrap_or(1);
3202    let dim_bytes = minimal_uint_width(max_dim);
3203    bytes.push(4);
3204    bytes.push(2);
3205    bytes.push(flags);
3206    bytes.push((chunk_shape.len() + 1) as u8);
3207    bytes.push(dim_bytes);
3208    for &dim in chunk_shape {
3209        write_uvar(dim, usize::from(dim_bytes), bytes);
3210    }
3211    write_uvar(u64::from(element_size), usize::from(dim_bytes), bytes);
3212    Ok(())
3213}
3214
3215/// Minimal number of bytes needed to encode `value` (at least 1).
3216fn minimal_uint_width(value: u64) -> u8 {
3217    let bits = 64 - value.leading_zeros();
3218    (bits.max(1)).div_ceil(8) as u8
3219}
3220
3221fn encode_implicit_chunked_layout_message(
3222    address: u64,
3223    chunk_shape: &[u64],
3224    element_size: u32,
3225) -> Result<Vec<u8>> {
3226    let mut bytes = Vec::with_capacity(10 + chunk_shape.len() * 4 + usize::from(OFFSET_SIZE));
3227    encode_v4_chunked_layout_preamble(&mut bytes, 0, chunk_shape, element_size)?;
3228    bytes.push(2);
3229    bytes.extend_from_slice(&address.to_le_bytes());
3230    Ok(bytes)
3231}
3232
3233fn encode_fixed_array_chunked_layout_message(
3234    address: u64,
3235    chunk_shape: &[u64],
3236    element_size: u32,
3237    page_bits: u8,
3238) -> Result<Vec<u8>> {
3239    let mut bytes = Vec::with_capacity(11 + chunk_shape.len() * 4 + usize::from(OFFSET_SIZE));
3240    encode_v4_chunked_layout_preamble(&mut bytes, 0, chunk_shape, element_size)?;
3241    bytes.push(3);
3242    bytes.push(page_bits);
3243    bytes.extend_from_slice(&address.to_le_bytes());
3244    Ok(bytes)
3245}
3246
3247/// v2 B-tree chunk-index node parameters. `split`/`merge` percentages match
3248/// libhdf5's defaults; readers ignore them for a depth-0 tree.
3249const BTREE_V2_SPLIT_PERCENT: u8 = 100;
3250const BTREE_V2_MERGE_PERCENT: u8 = 40;
3251const BTREE_V2_LEAF_OVERHEAD: u64 = 4 + 1 + 1 + 4; // signature + version + type + checksum
3252const BTREE_V2_RECORD_TYPE_UNFILTERED: u8 = 10;
3253const BTREE_V2_RECORD_TYPE_FILTERED: u8 = 11;
3254
3255/// Byte size of one v2 B-tree chunk record.
3256fn btree_v2_record_size(ndims: usize, chunk_size_len: u8, filtered: bool) -> Result<u16> {
3257    let offsets = ndims
3258        .checked_mul(8)
3259        .ok_or_else(|| Error::InvalidDefinition("chunk offset bytes overflow".into()))?;
3260    let fixed = usize::from(OFFSET_SIZE)
3261        + offsets
3262        + if filtered {
3263            usize::from(chunk_size_len) + 4
3264        } else {
3265            0
3266        };
3267    u16::try_from(fixed)
3268        .map_err(|_| Error::UnsupportedFeature("v2 B-tree chunk record exceeds u16".into()))
3269}
3270
3271/// Single-leaf node size: exactly large enough for the leaf header, all
3272/// records, and the trailing checksum, 8-byte aligned.
3273fn btree_v2_node_size(num_records: u64, record_size: u16) -> Result<u32> {
3274    let records_bytes = num_records
3275        .checked_mul(u64::from(record_size))
3276        .ok_or_else(|| Error::InvalidDefinition("v2 B-tree leaf size overflow".into()))?;
3277    let size = align_u64(
3278        checked_add_u64(BTREE_V2_LEAF_OVERHEAD, records_bytes, "v2 B-tree leaf size")?,
3279        8,
3280    );
3281    u32::try_from(size)
3282        .map_err(|_| Error::UnsupportedFeature("v2 B-tree node size exceeds u32".into()))
3283}
3284
3285fn encode_btree_v2_chunked_layout_message(
3286    address: u64,
3287    chunk_shape: &[u64],
3288    element_size: u32,
3289    node_size: u32,
3290) -> Result<Vec<u8>> {
3291    let mut bytes = Vec::with_capacity(15 + chunk_shape.len() * 4 + usize::from(OFFSET_SIZE));
3292    encode_v4_chunked_layout_preamble(&mut bytes, 0, chunk_shape, element_size)?;
3293    bytes.push(5); // index type: v2 B-tree
3294    bytes.extend_from_slice(&node_size.to_le_bytes());
3295    bytes.push(BTREE_V2_SPLIT_PERCENT);
3296    bytes.push(BTREE_V2_MERGE_PERCENT);
3297    bytes.extend_from_slice(&address.to_le_bytes());
3298    Ok(bytes)
3299}
3300
3301fn encode_btree_v2_header(
3302    record_type: u8,
3303    record_size: u16,
3304    node_size: u32,
3305    root_node_address: u64,
3306    num_records: u64,
3307) -> Result<Vec<u8>> {
3308    let mut bytes = Vec::with_capacity(4 + 1 + 1 + 4 + 2 + 2 + 1 + 1 + 8 + 2 + 8 + 4);
3309    bytes.extend_from_slice(b"BTHD");
3310    bytes.push(0); // version
3311    bytes.push(record_type);
3312    bytes.extend_from_slice(&node_size.to_le_bytes());
3313    bytes.extend_from_slice(&record_size.to_le_bytes());
3314    bytes.extend_from_slice(&0u16.to_le_bytes()); // depth 0 (single leaf)
3315    bytes.push(BTREE_V2_SPLIT_PERCENT);
3316    bytes.push(BTREE_V2_MERGE_PERCENT);
3317    bytes.extend_from_slice(&root_node_address.to_le_bytes());
3318    let num_records_u16 = u16::try_from(num_records)
3319        .map_err(|_| Error::UnsupportedFeature("v2 B-tree root record count exceeds u16".into()))?;
3320    bytes.extend_from_slice(&num_records_u16.to_le_bytes());
3321    bytes.extend_from_slice(&num_records.to_le_bytes()); // total records (length size 8)
3322    let checksum = jenkins_lookup3(&bytes);
3323    bytes.extend_from_slice(&checksum.to_le_bytes());
3324    Ok(bytes)
3325}
3326
3327/// Encode the single leaf node holding every chunk record. The node is written
3328/// at its full `node_size`; the checksum sits immediately after the records
3329/// and the remainder is zero padding.
3330fn encode_btree_v2_leaf(
3331    record_type: u8,
3332    record_size: u16,
3333    node_size: u32,
3334    entries: &[FixedArrayChunkEntry],
3335    chunk_size_len: u8,
3336) -> Result<Vec<u8>> {
3337    let filtered = record_type == BTREE_V2_RECORD_TYPE_FILTERED;
3338    let ndims = entries
3339        .first()
3340        .map_or(0, |entry| entry.scaled_offsets.len());
3341    let mut records = Vec::new();
3342    records.extend_from_slice(b"BTLF");
3343    records.push(0); // version
3344    records.push(record_type);
3345    let size_limit = if chunk_size_len >= 8 {
3346        u64::MAX
3347    } else {
3348        (1u64 << (8 * u32::from(chunk_size_len))) - 1
3349    };
3350    for entry in entries {
3351        if entry.scaled_offsets.len() != ndims {
3352            return Err(Error::InvalidDefinition(
3353                "v2 B-tree chunk records have inconsistent rank".into(),
3354            ));
3355        }
3356        records.extend_from_slice(&entry.address.to_le_bytes());
3357        if filtered {
3358            if entry.size > size_limit {
3359                return Err(Error::InvalidDefinition(format!(
3360                    "filtered chunk byte length {} exceeds the {chunk_size_len}-byte record field",
3361                    entry.size
3362                )));
3363            }
3364            write_uvar(entry.size, usize::from(chunk_size_len), &mut records);
3365            records.extend_from_slice(&entry.filter_mask.to_le_bytes());
3366        }
3367        for &scaled in &entry.scaled_offsets {
3368            records.extend_from_slice(&scaled.to_le_bytes());
3369        }
3370    }
3371    let _ = record_size;
3372    let checksum = jenkins_lookup3(&records);
3373    records.extend_from_slice(&checksum.to_le_bytes());
3374    let node_size = checked_usize(u64::from(node_size), "v2 B-tree node size")?;
3375    if records.len() > node_size {
3376        return Err(Error::InvalidDefinition(
3377            "v2 B-tree leaf records exceed node size".into(),
3378        ));
3379    }
3380    records.resize(node_size, 0);
3381    Ok(records)
3382}
3383
3384fn encode_fixed_array_chunk_index_header(
3385    num_entries: u64,
3386    chunk_size_len: u8,
3387    data_block_address: u64,
3388) -> Result<Vec<u8>> {
3389    let entry_size = u8::try_from(usize::from(OFFSET_SIZE) + usize::from(chunk_size_len) + 4)
3390        .map_err(|_| Error::UnsupportedFeature("fixed array entry size exceeds u8".into()))?;
3391    let mut bytes = Vec::with_capacity(4 + 1 + 1 + 1 + 1 + 8 + 8 + 4);
3392    bytes.extend_from_slice(b"FAHD");
3393    bytes.push(0);
3394    bytes.push(1);
3395    bytes.push(entry_size);
3396    bytes.push(fixed_array_page_bits(num_entries)?);
3397    bytes.extend_from_slice(&num_entries.to_le_bytes());
3398    bytes.extend_from_slice(&data_block_address.to_le_bytes());
3399    let checksum = jenkins_lookup3(&bytes);
3400    bytes.extend_from_slice(&checksum.to_le_bytes());
3401    Ok(bytes)
3402}
3403
3404fn encode_fixed_array_chunk_index_data_block(
3405    header_address: u64,
3406    entries: &[FixedArrayChunkEntry],
3407    chunk_size_len: u8,
3408) -> Result<Vec<u8>> {
3409    let entry_size = usize::from(OFFSET_SIZE) + usize::from(chunk_size_len) + 4;
3410    let entries_len = checked_mul_usize(entries.len(), entry_size, "fixed array entries size")?;
3411    let capacity = checked_add_usize(
3412        4 + 1 + 1 + usize::from(OFFSET_SIZE),
3413        checked_add_usize(entries_len, 4, "fixed array data block size")?,
3414        "fixed array data block size",
3415    )?;
3416    let size_limit = if chunk_size_len >= 8 {
3417        u64::MAX
3418    } else {
3419        (1u64 << (8 * u32::from(chunk_size_len))) - 1
3420    };
3421    let mut bytes = Vec::with_capacity(capacity);
3422    bytes.extend_from_slice(b"FADB");
3423    bytes.push(0);
3424    bytes.push(1);
3425    bytes.extend_from_slice(&header_address.to_le_bytes());
3426    for entry in entries {
3427        if entry.size > size_limit {
3428            return Err(Error::InvalidDefinition(format!(
3429                "filtered chunk byte length {} exceeds the {chunk_size_len}-byte entry field",
3430                entry.size
3431            )));
3432        }
3433        bytes.extend_from_slice(&entry.address.to_le_bytes());
3434        write_uvar(entry.size, usize::from(chunk_size_len), &mut bytes);
3435        bytes.extend_from_slice(&entry.filter_mask.to_le_bytes());
3436    }
3437    let checksum = jenkins_lookup3(&bytes);
3438    bytes.extend_from_slice(&checksum.to_le_bytes());
3439    Ok(bytes)
3440}
3441
3442fn class_word(class: u8, version: u8, flags: u32) -> u32 {
3443    u32::from(class) | (u32::from(version) << 4) | (flags << 8)
3444}
3445
3446fn byte_order_flag(byte_order: ByteOrder) -> u32 {
3447    match byte_order {
3448        ByteOrder::LittleEndian => 0,
3449        ByteOrder::BigEndian => 1,
3450    }
3451}
3452
3453fn string_padding_bits(padding: StringPadding) -> u32 {
3454    match padding {
3455        StringPadding::NullTerminate => 0,
3456        StringPadding::NullPad => 1,
3457        StringPadding::SpacePad => 2,
3458    }
3459}
3460
3461fn string_encoding_bits(encoding: StringEncoding) -> u32 {
3462    match encoding {
3463        StringEncoding::Ascii => 0,
3464        StringEncoding::Utf8 => 1,
3465    }
3466}
3467
3468fn datatype_element_size(datatype: &Datatype) -> Result<usize> {
3469    let size = match datatype {
3470        Datatype::FixedPoint { size, .. }
3471        | Datatype::FloatingPoint { size, .. }
3472        | Datatype::Bitfield { size, .. }
3473        | Datatype::Reference { size, .. } => Ok(*size as usize),
3474        Datatype::String {
3475            size: StringSize::Fixed(size),
3476            ..
3477        } => Ok(*size as usize),
3478        Datatype::Compound { size, .. } | Datatype::Opaque { size, .. } => Ok(*size as usize),
3479        Datatype::Array { base, dims } => {
3480            if dims.is_empty() {
3481                return Err(Error::InvalidDefinition(
3482                    "array datatype must have at least one dimension".into(),
3483                ));
3484            }
3485            if dims.contains(&0) {
3486                return Err(Error::InvalidDefinition(
3487                    "array datatype dimensions must be non-zero".into(),
3488                ));
3489            }
3490            let base_size = datatype_element_size(base)?;
3491            let count = dims.iter().try_fold(1usize, |acc, &dim| {
3492                let dim = checked_usize(dim, "array datatype dimension")?;
3493                checked_mul_usize(acc, dim, "array datatype element count")
3494            })?;
3495            checked_mul_usize(base_size, count, "array datatype byte size")
3496        }
3497        Datatype::Enum { base, .. } => datatype_element_size(base),
3498        Datatype::String {
3499            size: StringSize::Variable,
3500            ..
3501        } => Ok(16),
3502        Datatype::VarLen { .. } => Ok(16),
3503    }?;
3504    if size == 0 {
3505        return Err(Error::InvalidDefinition(
3506            "datatype byte size must be non-zero".into(),
3507        ));
3508    }
3509    Ok(size)
3510}
3511
3512fn validate_vlen_sequence_base(datatype: &Datatype) -> Result<()> {
3513    if datatype_element_size(datatype)? == 0 {
3514        return Err(Error::InvalidDefinition(
3515            "variable-length sequence base datatype must have non-zero byte size".into(),
3516        ));
3517    }
3518    match datatype {
3519        Datatype::String {
3520            size: StringSize::Variable,
3521            ..
3522        }
3523        | Datatype::VarLen { .. } => Err(Error::UnsupportedFeature(
3524            "nested heap-backed variable-length sequence bases are not emitted yet".into(),
3525        )),
3526        Datatype::Compound { fields, .. } => {
3527            for field in fields {
3528                validate_vlen_sequence_base(&field.datatype)?;
3529            }
3530            Ok(())
3531        }
3532        Datatype::Array { base, .. } | Datatype::Enum { base, .. } => {
3533            validate_vlen_sequence_base(base)
3534        }
3535        Datatype::FixedPoint { .. }
3536        | Datatype::FloatingPoint { .. }
3537        | Datatype::Bitfield { .. }
3538        | Datatype::Reference { .. }
3539        | Datatype::String {
3540            size: StringSize::Fixed(_),
3541            ..
3542        }
3543        | Datatype::Opaque { .. } => Ok(()),
3544    }
3545}
3546
3547fn validate_writer_filters(
3548    dataset: &DatasetBuilder,
3549    element_size: usize,
3550) -> Result<Vec<FilterDescription>> {
3551    if dataset.filters.is_empty() {
3552        return Ok(Vec::new());
3553    }
3554    let PlannedLayout::Chunked { chunk_shape } = &dataset.layout else {
3555        return Err(Error::InvalidDefinition(
3556            "filtered HDF5 datasets must use chunked layout".into(),
3557        ));
3558    };
3559    let _ = chunk_count(&dataset.shape, chunk_shape)?;
3560    let mut filters = Vec::with_capacity(dataset.filters.len());
3561    for filter in &dataset.filters {
3562        match filter.id {
3563            FILTER_DEFLATE => {
3564                if filter.client_data.len() != 1 || filter.client_data[0] > 9 {
3565                    return Err(Error::InvalidDefinition(
3566                        "deflate filter requires one compression level in 0..=9".into(),
3567                    ));
3568                }
3569                if filter.name.is_some() {
3570                    return Err(Error::InvalidDefinition(
3571                        "well-known HDF5 deflate filter must not carry a name".into(),
3572                    ));
3573                }
3574                filters.push(filter.clone());
3575            }
3576            FILTER_SHUFFLE => {
3577                if filter.name.is_some() {
3578                    return Err(Error::InvalidDefinition(
3579                        "well-known HDF5 shuffle filter must not carry a name".into(),
3580                    ));
3581                }
3582                let element_size_client_data = u32::try_from(element_size).map_err(|_| {
3583                    Error::UnsupportedFeature(
3584                        "shuffle filter element size exceeds u32 capacity".into(),
3585                    )
3586                })?;
3587                match filter.client_data.as_slice() {
3588                    [] => filters.push(FilterDescription {
3589                        id: FILTER_SHUFFLE,
3590                        name: None,
3591                        client_data: vec![element_size_client_data],
3592                    }),
3593                    [size] if *size == element_size_client_data => filters.push(filter.clone()),
3594                    [_] => {
3595                        return Err(Error::InvalidDefinition(format!(
3596                            "shuffle filter element size must match datatype element size {element_size}"
3597                        )));
3598                    }
3599                    _ => {
3600                        return Err(Error::InvalidDefinition(
3601                            "shuffle filter requires zero or one element-size client value".into(),
3602                        ));
3603                    }
3604                }
3605            }
3606            FILTER_FLETCHER32 => {
3607                if filter.name.is_some() {
3608                    return Err(Error::InvalidDefinition(
3609                        "well-known HDF5 Fletcher32 filter must not carry a name".into(),
3610                    ));
3611                }
3612                if !filter.client_data.is_empty() {
3613                    return Err(Error::InvalidDefinition(
3614                        "Fletcher32 filter must not carry client data".into(),
3615                    ));
3616                }
3617                filters.push(filter.clone());
3618            }
3619            other => {
3620                return Err(Error::UnsupportedFeature(format!(
3621                    "HDF5 filter id {other} is not implemented for writing"
3622                )));
3623            }
3624        }
3625    }
3626    Ok(filters)
3627}
3628
3629fn apply_write_filters(
3630    data: &[u8],
3631    filters: &[FilterDescription],
3632    element_size: usize,
3633) -> Result<Vec<u8>> {
3634    let mut current = data.to_vec();
3635    for filter in filters {
3636        current = match filter.id {
3637            FILTER_SHUFFLE => shuffle_filter(&current, element_size),
3638            FILTER_DEFLATE => deflate_filter(&current, filter)?,
3639            FILTER_FLETCHER32 => fletcher32_filter(&current),
3640            other => {
3641                return Err(Error::UnsupportedFeature(format!(
3642                    "HDF5 filter id {other} is not implemented for writing"
3643                )));
3644            }
3645        };
3646    }
3647    Ok(current)
3648}
3649
3650fn shuffle_filter(data: &[u8], element_size: usize) -> Vec<u8> {
3651    if element_size <= 1 || data.is_empty() {
3652        return data.to_vec();
3653    }
3654
3655    let n_elements = data.len() / element_size;
3656    if n_elements == 0 {
3657        return data.to_vec();
3658    }
3659
3660    let mut output = vec![0u8; data.len()];
3661    for byte_idx in 0..element_size {
3662        let dst_start = byte_idx * n_elements;
3663        for elem in 0..n_elements {
3664            output[dst_start + elem] = data[elem * element_size + byte_idx];
3665        }
3666    }
3667
3668    let complete = n_elements * element_size;
3669    if complete < data.len() {
3670        output[complete..].copy_from_slice(&data[complete..]);
3671    }
3672    output
3673}
3674
3675fn fletcher32_filter(data: &[u8]) -> Vec<u8> {
3676    let mut output = Vec::with_capacity(data.len() + 4);
3677    output.extend_from_slice(data);
3678    output.extend_from_slice(&fletcher32(data).to_le_bytes());
3679    output
3680}
3681
3682fn deflate_filter(data: &[u8], filter: &FilterDescription) -> Result<Vec<u8>> {
3683    let level = filter.client_data[0];
3684    let mut encoder = ZlibEncoder::new(Vec::new(), Compression::new(level));
3685    encoder.write_all(data)?;
3686    Ok(encoder.finish()?)
3687}
3688
3689fn ensure_datatype_matches_element<T: H5WriteElement>(datatype: &Datatype) -> Result<()> {
3690    let expected = T::hdf5_type_with_order(numeric_datatype_order(datatype)?);
3691    if &expected != datatype {
3692        return Err(Error::InvalidDefinition(format!(
3693            "typed data does not match dataset datatype: expected {datatype:?}, got {expected:?}"
3694        )));
3695    }
3696    Ok(())
3697}
3698
3699fn chunk_count(shape: &[u64], chunk_shape: &[u64]) -> Result<u64> {
3700    if shape.contains(&0) {
3701        return Ok(0);
3702    }
3703    shape
3704        .iter()
3705        .zip(chunk_shape)
3706        .try_fold(1u64, |acc, (&dim, &chunk_dim)| {
3707            checked_mul_u64(acc, dim.div_ceil(chunk_dim), "chunk count")
3708        })
3709}
3710
3711fn chunked_storage_data(
3712    raw_data: &[u8],
3713    shape: &[u64],
3714    chunk_shape: &[u64],
3715    element_size: usize,
3716) -> Result<Vec<u8>> {
3717    if shape.is_empty() {
3718        return Err(Error::InvalidDefinition(
3719            "chunked scalar HDF5 datasets are not supported".into(),
3720        ));
3721    }
3722    if chunk_shape.len() != shape.len() {
3723        return Err(Error::InvalidDefinition(
3724            "chunk shape rank must match dataset rank".into(),
3725        ));
3726    }
3727
3728    let mut chunks_per_dim = Vec::with_capacity(shape.len());
3729    for (&dim, &chunk_dim) in shape.iter().zip(chunk_shape) {
3730        if chunk_dim == 0 {
3731            return Err(Error::InvalidDefinition(
3732                "chunk dimensions must be non-zero".into(),
3733            ));
3734        }
3735        u32::try_from(chunk_dim).map_err(|_| {
3736            Error::InvalidDefinition("chunk dimension exceeds HDF5 v4 layout capacity".into())
3737        })?;
3738        chunks_per_dim.push(dim.div_ceil(chunk_dim));
3739    }
3740    if shape.contains(&0) {
3741        return Ok(Vec::new());
3742    }
3743
3744    let chunk_elements = chunk_shape.iter().try_fold(1u64, |acc, &dim| {
3745        checked_mul_u64(acc, dim, "chunk element count")
3746    })?;
3747    let element_size_u64 = u64::try_from(element_size).map_err(|_| {
3748        Error::InvalidDefinition("datatype element size exceeds u64 capacity".into())
3749    })?;
3750    let chunk_bytes_u64 = checked_mul_u64(chunk_elements, element_size_u64, "chunk byte length")?;
3751    let total_chunks = chunks_per_dim.iter().try_fold(1u64, |acc, &count| {
3752        checked_mul_u64(acc, count, "chunk count")
3753    })?;
3754    let storage_bytes_u64 = checked_mul_u64(total_chunks, chunk_bytes_u64, "chunked data length")?;
3755    let storage_bytes = checked_usize(storage_bytes_u64, "chunked data length")?;
3756    let mut storage = vec![0u8; storage_bytes];
3757
3758    let dataset_strides = row_major_strides(shape, "dataset stride")?;
3759    let chunk_strides = row_major_strides(chunk_shape, "chunk stride")?;
3760    let mut chunk_indices = vec![0u64; shape.len()];
3761
3762    for chunk_linear in 0..total_chunks {
3763        let chunk_base = checked_usize(
3764            checked_mul_u64(chunk_linear, chunk_bytes_u64, "chunk byte offset")?,
3765            "chunk byte offset",
3766        )?;
3767        let mut starts = Vec::with_capacity(shape.len());
3768        let mut counts = Vec::with_capacity(shape.len());
3769        for ((&chunk_index, &chunk_dim), &dim) in chunk_indices.iter().zip(chunk_shape).zip(shape) {
3770            let start = checked_mul_u64(chunk_index, chunk_dim, "chunk start")?;
3771            starts.push(start);
3772            counts.push((dim - start).min(chunk_dim));
3773        }
3774        let src_start =
3775            starts
3776                .iter()
3777                .zip(&dataset_strides)
3778                .try_fold(0u64, |acc, (&start, &stride)| {
3779                    checked_add_u64(
3780                        acc,
3781                        checked_mul_u64(start, stride, "chunk source element offset")?,
3782                        "chunk source element offset",
3783                    )
3784                })?;
3785        copy_chunk_rows(
3786            raw_data,
3787            &mut storage,
3788            element_size,
3789            &dataset_strides,
3790            &chunk_strides,
3791            &counts,
3792            0,
3793            src_start,
3794            0,
3795            chunk_base,
3796        )?;
3797        increment_row_major_index(&mut chunk_indices, &chunks_per_dim);
3798    }
3799
3800    Ok(storage)
3801}
3802
3803fn filtered_chunk_storage_data(
3804    raw_data: &[u8],
3805    shape: &[u64],
3806    chunk_shape: &[u64],
3807    element_size: usize,
3808    filters: &[FilterDescription],
3809    kind: ChunkIndexKind,
3810) -> Result<(Vec<u8>, Option<PreparedChunkIndex>)> {
3811    if shape.is_empty() {
3812        return Err(Error::InvalidDefinition(
3813            "chunked scalar HDF5 datasets are not supported".into(),
3814        ));
3815    }
3816    if chunk_shape.len() != shape.len() {
3817        return Err(Error::InvalidDefinition(
3818            "chunk shape rank must match dataset rank".into(),
3819        ));
3820    }
3821
3822    let mut chunks_per_dim = Vec::with_capacity(shape.len());
3823    for (&dim, &chunk_dim) in shape.iter().zip(chunk_shape) {
3824        if chunk_dim == 0 {
3825            return Err(Error::InvalidDefinition(
3826                "chunk dimensions must be non-zero".into(),
3827            ));
3828        }
3829        u32::try_from(chunk_dim).map_err(|_| {
3830            Error::InvalidDefinition("chunk dimension exceeds HDF5 v4 layout capacity".into())
3831        })?;
3832        chunks_per_dim.push(dim.div_ceil(chunk_dim));
3833    }
3834    if shape.contains(&0) {
3835        return Ok((Vec::new(), None));
3836    }
3837
3838    let chunk_elements = chunk_shape.iter().try_fold(1u64, |acc, &dim| {
3839        checked_mul_u64(acc, dim, "chunk element count")
3840    })?;
3841    let element_size_u64 = u64::try_from(element_size).map_err(|_| {
3842        Error::InvalidDefinition("datatype element size exceeds u64 capacity".into())
3843    })?;
3844    let chunk_bytes_u64 = checked_mul_u64(chunk_elements, element_size_u64, "chunk byte length")?;
3845    let chunk_bytes = checked_usize(chunk_bytes_u64, "chunk byte length")?;
3846    let total_chunks = chunks_per_dim.iter().try_fold(1u64, |acc, &count| {
3847        checked_mul_u64(acc, count, "chunk count")
3848    })?;
3849
3850    let dataset_strides = row_major_strides(shape, "dataset stride")?;
3851    let chunk_strides = row_major_strides(chunk_shape, "chunk stride")?;
3852    let mut chunk_indices = vec![0u64; shape.len()];
3853    let mut storage = Vec::new();
3854    let mut entries = Vec::with_capacity(checked_usize(total_chunks, "chunk count")?);
3855
3856    for _ in 0..total_chunks {
3857        let mut starts = Vec::with_capacity(shape.len());
3858        let mut counts = Vec::with_capacity(shape.len());
3859        for ((&chunk_index, &chunk_dim), &dim) in chunk_indices.iter().zip(chunk_shape).zip(shape) {
3860            let start = checked_mul_u64(chunk_index, chunk_dim, "chunk start")?;
3861            starts.push(start);
3862            counts.push((dim - start).min(chunk_dim));
3863        }
3864        let src_start =
3865            starts
3866                .iter()
3867                .zip(&dataset_strides)
3868                .try_fold(0u64, |acc, (&start, &stride)| {
3869                    checked_add_u64(
3870                        acc,
3871                        checked_mul_u64(start, stride, "chunk source element offset")?,
3872                        "chunk source element offset",
3873                    )
3874                })?;
3875
3876        let mut chunk = vec![0u8; chunk_bytes];
3877        copy_chunk_rows(
3878            raw_data,
3879            &mut chunk,
3880            element_size,
3881            &dataset_strides,
3882            &chunk_strides,
3883            &counts,
3884            0,
3885            src_start,
3886            0,
3887            0,
3888        )?;
3889
3890        let encoded = apply_write_filters(&chunk, filters, element_size)?;
3891        let relative_address = u64::try_from(storage.len()).map_err(|_| {
3892            Error::InvalidDefinition("filtered chunk storage exceeds u64 capacity".into())
3893        })?;
3894        let size = u64::try_from(encoded.len()).map_err(|_| {
3895            Error::InvalidDefinition("filtered chunk byte length exceeds u64 capacity".into())
3896        })?;
3897        storage.extend_from_slice(&encoded);
3898        entries.push(PreparedChunkIndexEntry {
3899            relative_address,
3900            size,
3901            filter_mask: 0,
3902            scaled_offsets: chunk_indices.clone(),
3903        });
3904
3905        increment_row_major_index(&mut chunk_indices, &chunks_per_dim);
3906    }
3907
3908    Ok((
3909        storage,
3910        Some(PreparedChunkIndex {
3911            kind,
3912            chunks: entries,
3913            chunk_size_len: fixed_array_chunk_size_len(chunk_bytes_u64),
3914        }),
3915    ))
3916}
3917
3918/// Build a B-tree v2 chunk index over an unfiltered, contiguously-laid-out
3919/// chunked dataset (the same byte layout the implicit index would use). Needed
3920/// because datasets with unlimited maximum dimensions may not use the implicit
3921/// index.
3922fn unfiltered_btree_chunk_index(
3923    shape: &[u64],
3924    chunk_shape: &[u64],
3925    element_size: usize,
3926) -> Result<Option<PreparedChunkIndex>> {
3927    if shape.is_empty() || shape.contains(&0) {
3928        return Ok(None);
3929    }
3930    let mut chunks_per_dim = Vec::with_capacity(shape.len());
3931    for (&dim, &chunk_dim) in shape.iter().zip(chunk_shape) {
3932        if chunk_dim == 0 {
3933            return Err(Error::InvalidDefinition(
3934                "chunk dimensions must be non-zero".into(),
3935            ));
3936        }
3937        chunks_per_dim.push(dim.div_ceil(chunk_dim));
3938    }
3939    let chunk_elements = chunk_shape.iter().try_fold(1u64, |acc, &dim| {
3940        checked_mul_u64(acc, dim, "chunk element count")
3941    })?;
3942    let element_size_u64 = u64::try_from(element_size).map_err(|_| {
3943        Error::InvalidDefinition("datatype element size exceeds u64 capacity".into())
3944    })?;
3945    let chunk_bytes = checked_mul_u64(chunk_elements, element_size_u64, "chunk byte length")?;
3946    let total_chunks = chunks_per_dim.iter().try_fold(1u64, |acc, &count| {
3947        checked_mul_u64(acc, count, "chunk count")
3948    })?;
3949
3950    let mut chunk_indices = vec![0u64; shape.len()];
3951    let mut entries = Vec::with_capacity(checked_usize(total_chunks, "chunk count")?);
3952    let mut relative_address = 0u64;
3953    for _ in 0..total_chunks {
3954        entries.push(PreparedChunkIndexEntry {
3955            relative_address,
3956            size: chunk_bytes,
3957            filter_mask: 0,
3958            scaled_offsets: chunk_indices.clone(),
3959        });
3960        relative_address = checked_add_u64(relative_address, chunk_bytes, "chunk offset")?;
3961        increment_row_major_index(&mut chunk_indices, &chunks_per_dim);
3962    }
3963    Ok(Some(PreparedChunkIndex {
3964        kind: ChunkIndexKind::BtreeV2,
3965        chunks: entries,
3966        // Unfiltered records store no chunk-size field.
3967        chunk_size_len: 0,
3968    }))
3969}
3970
3971/// Whether a max-shape declares any unlimited dimension.
3972fn has_unlimited_max_shape(max_shape: Option<&[u64]>) -> bool {
3973    max_shape.is_some_and(|dims| dims.contains(&UNLIMITED))
3974}
3975
3976/// Width of the encoded chunk-size field in filtered fixed-array entries.
3977/// Mirrors libhdf5, which sizes the field from the nominal chunk byte size
3978/// with one byte of headroom (filters can expand a chunk); readers derive the
3979/// same width from the dataset properties rather than the stored entry size.
3980fn fixed_array_chunk_size_len(chunk_bytes: u64) -> u8 {
3981    let log2 = 63u32.saturating_sub(chunk_bytes.leading_zeros());
3982    (1 + (log2 + 8) / 8).min(8) as u8
3983}
3984
3985#[allow(clippy::too_many_arguments)]
3986fn copy_chunk_rows(
3987    src: &[u8],
3988    dst: &mut [u8],
3989    element_size: usize,
3990    src_strides: &[u64],
3991    chunk_strides: &[u64],
3992    counts: &[u64],
3993    dim: usize,
3994    src_element: u64,
3995    dst_element: u64,
3996    dst_chunk_base: usize,
3997) -> Result<()> {
3998    let last_dim = counts.len() - 1;
3999    if dim == last_dim {
4000        let count = checked_usize(counts[dim], "chunk row element count")?;
4001        let byte_count = checked_mul_usize(count, element_size, "chunk row byte length")?;
4002        let src_start = element_byte_offset(src_element, element_size, "chunk source offset")?;
4003        let dst_relative =
4004            element_byte_offset(dst_element, element_size, "chunk destination offset")?;
4005        let dst_start = dst_chunk_base.checked_add(dst_relative).ok_or_else(|| {
4006            Error::InvalidDefinition("chunk destination offset overflows usize".into())
4007        })?;
4008        let src_end = src_start
4009            .checked_add(byte_count)
4010            .ok_or_else(|| Error::InvalidDefinition("chunk source end overflows usize".into()))?;
4011        let dst_end = dst_start.checked_add(byte_count).ok_or_else(|| {
4012            Error::InvalidDefinition("chunk destination end overflows usize".into())
4013        })?;
4014        if src_end > src.len() || dst_end > dst.len() {
4015            return Err(Error::InvalidDefinition(
4016                "chunk packing range exceeds dataset storage".into(),
4017            ));
4018        }
4019        dst[dst_start..dst_end].copy_from_slice(&src[src_start..src_end]);
4020        return Ok(());
4021    }
4022
4023    for index in 0..counts[dim] {
4024        copy_chunk_rows(
4025            src,
4026            dst,
4027            element_size,
4028            src_strides,
4029            chunk_strides,
4030            counts,
4031            dim + 1,
4032            checked_add_u64(
4033                src_element,
4034                checked_mul_u64(index, src_strides[dim], "chunk source stride")?,
4035                "chunk source stride",
4036            )?,
4037            checked_add_u64(
4038                dst_element,
4039                checked_mul_u64(index, chunk_strides[dim], "chunk destination stride")?,
4040                "chunk destination stride",
4041            )?,
4042            dst_chunk_base,
4043        )?;
4044    }
4045    Ok(())
4046}
4047
4048fn row_major_strides(dims: &[u64], context: &str) -> Result<Vec<u64>> {
4049    let mut strides = vec![1u64; dims.len()];
4050    let mut stride = 1u64;
4051    for index in (0..dims.len()).rev() {
4052        strides[index] = stride;
4053        stride = checked_mul_u64(stride, dims[index], context)?;
4054    }
4055    Ok(strides)
4056}
4057
4058fn increment_row_major_index(indices: &mut [u64], limits: &[u64]) {
4059    for dim in (0..indices.len()).rev() {
4060        indices[dim] += 1;
4061        if indices[dim] < limits[dim] {
4062            return;
4063        }
4064        indices[dim] = 0;
4065    }
4066}
4067
4068fn element_byte_offset(element_offset: u64, element_size: usize, context: &str) -> Result<usize> {
4069    let element_size = u64::try_from(element_size).map_err(|_| {
4070        Error::InvalidDefinition(format!("{context} element size exceeds u64 capacity"))
4071    })?;
4072    checked_usize(
4073        checked_mul_u64(element_offset, element_size, context)?,
4074        context,
4075    )
4076}
4077
4078fn numeric_datatype_order(datatype: &Datatype) -> Result<ByteOrder> {
4079    datatype_numeric_order(datatype).ok_or_else(|| {
4080        Error::UnsupportedFeature(format!(
4081            "typed data encoding is not implemented for {datatype:?}"
4082        ))
4083    })
4084}
4085
4086fn datatype_numeric_order(datatype: &Datatype) -> Option<ByteOrder> {
4087    match datatype {
4088        Datatype::FixedPoint { byte_order, .. } | Datatype::FloatingPoint { byte_order, .. } => {
4089            Some(*byte_order)
4090        }
4091        _ => None,
4092    }
4093}
4094
4095fn encode_fixed_string_values<S: AsRef<str>>(values: &[S]) -> Result<(Datatype, Vec<u8>)> {
4096    let mut width = 1usize;
4097    let mut encoding = StringEncoding::Ascii;
4098    for value in values {
4099        let value = value.as_ref();
4100        if value.as_bytes().contains(&0) {
4101            return Err(Error::InvalidDefinition(
4102                "fixed string values cannot contain NUL bytes".into(),
4103            ));
4104        }
4105        width = width.max(value.len());
4106        if !value.is_ascii() {
4107            encoding = StringEncoding::Utf8;
4108        }
4109    }
4110
4111    let width_u32 = u32::try_from(width).map_err(|_| {
4112        Error::InvalidDefinition("fixed string element width exceeds HDF5 capacity".into())
4113    })?;
4114    let capacity = checked_mul_usize(values.len(), width, "fixed string data byte length")?;
4115    let mut raw_data = Vec::with_capacity(capacity);
4116    for value in values {
4117        let element_end = raw_data.len().checked_add(width).ok_or_else(|| {
4118            Error::InvalidDefinition("fixed string data byte length exceeds usize capacity".into())
4119        })?;
4120        raw_data.extend_from_slice(value.as_ref().as_bytes());
4121        raw_data.resize(element_end, 0);
4122    }
4123
4124    Ok((
4125        Datatype::String {
4126            size: StringSize::Fixed(width_u32),
4127            encoding,
4128            padding: StringPadding::NullPad,
4129        },
4130        raw_data,
4131    ))
4132}
4133
4134fn expected_element_count(shape: &[u64]) -> Result<usize> {
4135    if shape.is_empty() {
4136        return Ok(1);
4137    }
4138    shape.iter().try_fold(1usize, |acc, &dim| {
4139        checked_mul_usize(
4140            acc,
4141            checked_usize(dim, "dataset dimension")?,
4142            "dataset element count",
4143        )
4144    })
4145}
4146
4147fn expected_data_len(shape: &[u64], element_size: usize) -> Result<usize> {
4148    let elements = expected_element_count(shape)?;
4149    checked_mul_usize(elements, element_size, "dataset byte length")
4150}
4151
4152fn size_width_for(value: u64) -> Result<(u8, usize)> {
4153    if value <= u8::MAX as u64 {
4154        Ok((0, 1))
4155    } else if value <= u16::MAX as u64 {
4156        Ok((1, 2))
4157    } else if value <= u32::MAX as u64 {
4158        Ok((2, 4))
4159    } else {
4160        Ok((3, 8))
4161    }
4162}
4163
4164fn write_uvar(value: u64, width: usize, dst: &mut Vec<u8>) {
4165    let bytes = value.to_le_bytes();
4166    dst.extend_from_slice(&bytes[..width]);
4167}
4168
4169fn pad_to_address(bytes: &mut Vec<u8>, address: u64) -> Result<()> {
4170    let address = checked_usize(address, "HDF5 address")?;
4171    if bytes.len() > address {
4172        return Err(Error::InvalidDefinition(format!(
4173            "internal HDF5 layout overlap: current offset {} exceeds target {address}",
4174            bytes.len()
4175        )));
4176    }
4177    bytes.resize(address, 0);
4178    Ok(())
4179}
4180
4181fn align_u64(value: u64, alignment: u64) -> u64 {
4182    debug_assert!(alignment.is_power_of_two());
4183    (value + alignment - 1) & !(alignment - 1)
4184}
4185
4186fn checked_usize(value: u64, context: &str) -> Result<usize> {
4187    usize::try_from(value).map_err(|_| {
4188        Error::InvalidDefinition(format!(
4189            "{context} value {value} exceeds platform usize capacity"
4190        ))
4191    })
4192}
4193
4194fn checked_mul_usize(lhs: usize, rhs: usize, context: &str) -> Result<usize> {
4195    lhs.checked_mul(rhs).ok_or_else(|| {
4196        Error::InvalidDefinition(format!("{context} exceeds platform usize capacity"))
4197    })
4198}
4199
4200fn checked_add_usize(lhs: usize, rhs: usize, context: &str) -> Result<usize> {
4201    lhs.checked_add(rhs).ok_or_else(|| {
4202        Error::InvalidDefinition(format!("{context} exceeds platform usize capacity"))
4203    })
4204}
4205
4206fn checked_mul_u64(lhs: u64, rhs: u64, context: &str) -> Result<u64> {
4207    lhs.checked_mul(rhs)
4208        .ok_or_else(|| Error::InvalidDefinition(format!("{context} exceeds u64 capacity")))
4209}
4210
4211fn checked_add_u64(lhs: u64, rhs: u64, context: &str) -> Result<u64> {
4212    lhs.checked_add(rhs)
4213        .ok_or_else(|| Error::InvalidDefinition(format!("{context} exceeds u64 capacity")))
4214}