Skip to main content

hdf5_pure/
type_builders.rs

1//! Builder types for HDF5 datatypes, attributes, datasets, and groups.
2//!
3//! Extracted from `file_writer.rs` to keep modules under the line limit.
4
5#[cfg(not(feature = "std"))]
6use alloc::{boxed::Box, string::String, string::ToString, vec, vec::Vec};
7
8use crate::attribute::AttributeMessage;
9use crate::chunked_write::{ChunkMeta, ChunkOptions, ChunkProvider};
10use crate::compound::CompoundType;
11use crate::convert::TryToUsize;
12use crate::dataspace::{Dataspace, DataspaceType};
13use crate::datatype::{
14    CharacterSet, CompoundMember, Datatype, DatatypeByteOrder, EnumMember, StringPadding,
15};
16use crate::scaleoffset::ScaleOffset;
17
18// ---- Datatype constructors ----
19
20pub fn make_f64_type() -> Datatype {
21    Datatype::FloatingPoint {
22        size: 8,
23        byte_order: DatatypeByteOrder::LittleEndian,
24        bit_offset: 0,
25        bit_precision: 64,
26        exponent_location: 52,
27        exponent_size: 11,
28        mantissa_location: 0,
29        mantissa_size: 52,
30        exponent_bias: 1023,
31    }
32}
33
34pub fn make_f32_type() -> Datatype {
35    Datatype::FloatingPoint {
36        size: 4,
37        byte_order: DatatypeByteOrder::LittleEndian,
38        bit_offset: 0,
39        bit_precision: 32,
40        exponent_location: 23,
41        exponent_size: 8,
42        mantissa_location: 0,
43        mantissa_size: 23,
44        exponent_bias: 127,
45    }
46}
47
48pub fn make_i32_type() -> Datatype {
49    Datatype::FixedPoint {
50        size: 4,
51        byte_order: DatatypeByteOrder::LittleEndian,
52        signed: true,
53        bit_offset: 0,
54        bit_precision: 32,
55    }
56}
57
58pub fn make_i64_type() -> Datatype {
59    Datatype::FixedPoint {
60        size: 8,
61        byte_order: DatatypeByteOrder::LittleEndian,
62        signed: true,
63        bit_offset: 0,
64        bit_precision: 64,
65    }
66}
67
68pub fn make_u8_type() -> Datatype {
69    Datatype::FixedPoint {
70        size: 1,
71        byte_order: DatatypeByteOrder::LittleEndian,
72        signed: false,
73        bit_offset: 0,
74        bit_precision: 8,
75    }
76}
77
78pub fn make_i8_type() -> Datatype {
79    Datatype::FixedPoint {
80        size: 1,
81        byte_order: DatatypeByteOrder::LittleEndian,
82        signed: true,
83        bit_offset: 0,
84        bit_precision: 8,
85    }
86}
87
88pub fn make_i16_type() -> Datatype {
89    Datatype::FixedPoint {
90        size: 2,
91        byte_order: DatatypeByteOrder::LittleEndian,
92        signed: true,
93        bit_offset: 0,
94        bit_precision: 16,
95    }
96}
97
98pub fn make_u16_type() -> Datatype {
99    Datatype::FixedPoint {
100        size: 2,
101        byte_order: DatatypeByteOrder::LittleEndian,
102        signed: false,
103        bit_offset: 0,
104        bit_precision: 16,
105    }
106}
107
108pub fn make_u32_type() -> Datatype {
109    Datatype::FixedPoint {
110        size: 4,
111        byte_order: DatatypeByteOrder::LittleEndian,
112        signed: false,
113        bit_offset: 0,
114        bit_precision: 32,
115    }
116}
117
118pub fn make_u64_type() -> Datatype {
119    Datatype::FixedPoint {
120        size: 8,
121        byte_order: DatatypeByteOrder::LittleEndian,
122        signed: false,
123        bit_offset: 0,
124        bit_precision: 64,
125    }
126}
127
128pub fn make_object_reference_type() -> Datatype {
129    Datatype::Reference {
130        size: 8,
131        ref_type: crate::datatype::ReferenceType::Object,
132    }
133}
134
135/// A variable-length string datatype with the given character set and
136/// null-terminated padding.
137///
138/// The character set and padding live in the variable-length datatype's own
139/// bitfields; the base element type is an 8-bit unsigned integer
140/// (`H5T_STD_U8LE`), exactly the shape the reference C library and h5py emit for
141/// a VL string (`H5Tvlen_create(H5T_C_S1)` stores the base as a 1-byte
142/// integer). Matching it byte-for-byte is what lets the C library read these
143/// datasets back into `VarLenUnicode`/`VarLenAscii` without a conversion-path
144/// error.
145pub fn make_vlen_string_type(charset: CharacterSet) -> Datatype {
146    Datatype::VariableLength {
147        is_string: true,
148        padding: Some(StringPadding::NullTerminate),
149        charset: Some(charset),
150        base_type: Box::new(make_u8_type()),
151    }
152}
153
154// ---- Compound / Enum type builders ----
155
156/// Builder for constructing HDF5 compound (struct) datatypes.
157pub struct CompoundTypeBuilder {
158    fields: Vec<(String, Datatype)>,
159}
160
161impl CompoundTypeBuilder {
162    pub fn new() -> Self {
163        Self { fields: Vec::new() }
164    }
165
166    /// Add a named field with the given datatype.
167    pub fn field(mut self, name: &str, datatype: Datatype) -> Self {
168        self.fields.push((name.to_string(), datatype));
169        self
170    }
171
172    /// Add an f64 field.
173    pub fn f64_field(self, name: &str) -> Self {
174        self.field(name, make_f64_type())
175    }
176    /// Add an f32 field.
177    pub fn f32_field(self, name: &str) -> Self {
178        self.field(name, make_f32_type())
179    }
180    /// Add an i32 field.
181    pub fn i32_field(self, name: &str) -> Self {
182        self.field(name, make_i32_type())
183    }
184    /// Add an i64 field.
185    pub fn i64_field(self, name: &str) -> Self {
186        self.field(name, make_i64_type())
187    }
188    /// Add a u8 field.
189    pub fn u8_field(self, name: &str) -> Self {
190        self.field(name, make_u8_type())
191    }
192    /// Add an i8 field.
193    pub fn i8_field(self, name: &str) -> Self {
194        self.field(name, make_i8_type())
195    }
196    /// Add an i16 field.
197    pub fn i16_field(self, name: &str) -> Self {
198        self.field(name, make_i16_type())
199    }
200    /// Add a u16 field.
201    pub fn u16_field(self, name: &str) -> Self {
202        self.field(name, make_u16_type())
203    }
204    /// Add a u32 field.
205    pub fn u32_field(self, name: &str) -> Self {
206        self.field(name, make_u32_type())
207    }
208    /// Add a u64 field.
209    pub fn u64_field(self, name: &str) -> Self {
210        self.field(name, make_u64_type())
211    }
212
213    /// Build the compound datatype.
214    pub fn build(self) -> Datatype {
215        let mut offset = 0u64;
216        let mut members = Vec::with_capacity(self.fields.len());
217        for (name, dt) in self.fields {
218            let sz = dt.type_size();
219            members.push(CompoundMember {
220                name,
221                byte_offset: offset,
222                datatype: dt,
223            });
224            offset += sz as u64;
225        }
226        Datatype::Compound {
227            #[expect(
228                clippy::cast_possible_truncation,
229                reason = "accumulated compound size is stored in the 4-byte datatype size field"
230            )]
231            size: offset as u32,
232            members,
233        }
234    }
235}
236
237impl Default for CompoundTypeBuilder {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243/// Builder for an HDF5 compound datatype with explicit field offsets and size.
244///
245/// This is the pure-Rust equivalent of creating an `H5T_COMPOUND` type and
246/// inserting fields with `H5Tinsert`. [`build`](Self::build) validates field
247/// names, bounds, and overlap before returning a datatype.
248pub struct ExplicitCompoundTypeBuilder {
249    size: u32,
250    fields: Vec<CompoundMember>,
251}
252
253impl ExplicitCompoundTypeBuilder {
254    /// Add a field at an explicit byte offset.
255    pub fn field(mut self, name: &str, byte_offset: u64, datatype: Datatype) -> Self {
256        self.fields.push(CompoundMember {
257            name: name.to_string(),
258            byte_offset,
259            datatype,
260        });
261        self
262    }
263
264    /// Add an f64 field at an explicit byte offset.
265    pub fn f64_field(self, name: &str, byte_offset: u64) -> Self {
266        self.field(name, byte_offset, make_f64_type())
267    }
268
269    /// Add an f32 field at an explicit byte offset.
270    pub fn f32_field(self, name: &str, byte_offset: u64) -> Self {
271        self.field(name, byte_offset, make_f32_type())
272    }
273
274    /// Add an i32 field at an explicit byte offset.
275    pub fn i32_field(self, name: &str, byte_offset: u64) -> Self {
276        self.field(name, byte_offset, make_i32_type())
277    }
278
279    /// Add an i64 field at an explicit byte offset.
280    pub fn i64_field(self, name: &str, byte_offset: u64) -> Self {
281        self.field(name, byte_offset, make_i64_type())
282    }
283
284    /// Add a u8 field at an explicit byte offset.
285    pub fn u8_field(self, name: &str, byte_offset: u64) -> Self {
286        self.field(name, byte_offset, make_u8_type())
287    }
288
289    /// Add an i8 field at an explicit byte offset.
290    pub fn i8_field(self, name: &str, byte_offset: u64) -> Self {
291        self.field(name, byte_offset, make_i8_type())
292    }
293
294    /// Add an i16 field at an explicit byte offset.
295    pub fn i16_field(self, name: &str, byte_offset: u64) -> Self {
296        self.field(name, byte_offset, make_i16_type())
297    }
298
299    /// Add a u16 field at an explicit byte offset.
300    pub fn u16_field(self, name: &str, byte_offset: u64) -> Self {
301        self.field(name, byte_offset, make_u16_type())
302    }
303
304    /// Add a u32 field at an explicit byte offset.
305    pub fn u32_field(self, name: &str, byte_offset: u64) -> Self {
306        self.field(name, byte_offset, make_u32_type())
307    }
308
309    /// Add a u64 field at an explicit byte offset.
310    pub fn u64_field(self, name: &str, byte_offset: u64) -> Self {
311        self.field(name, byte_offset, make_u64_type())
312    }
313
314    /// Validate and build the compound datatype.
315    pub fn build(mut self) -> Result<Datatype, crate::error::FormatError> {
316        use crate::error::FormatError;
317
318        if self.size == 0 {
319            return Err(FormatError::InvalidCompoundSize);
320        }
321        if self.fields.is_empty() {
322            return Err(FormatError::EmptyCompoundType);
323        }
324
325        for (index, field) in self.fields.iter().enumerate() {
326            if self.fields[..index]
327                .iter()
328                .any(|earlier| earlier.name == field.name)
329            {
330                return Err(FormatError::DuplicateCompoundField(field.name.clone()));
331            }
332            let field_size = field.datatype.type_size();
333            let end = field.byte_offset.checked_add(u64::from(field_size));
334            if field_size == 0 || end.is_none_or(|end| end > u64::from(self.size)) {
335                return Err(FormatError::CompoundFieldOutOfBounds {
336                    name: field.name.clone(),
337                    offset: field.byte_offset,
338                    field_size,
339                    compound_size: self.size,
340                });
341            }
342        }
343
344        self.fields.sort_by_key(|field| field.byte_offset);
345        for fields in self.fields.windows(2) {
346            let first_end = fields[0].byte_offset + u64::from(fields[0].datatype.type_size());
347            if first_end > fields[1].byte_offset {
348                return Err(FormatError::CompoundFieldOverlap {
349                    first: fields[0].name.clone(),
350                    second: fields[1].name.clone(),
351                });
352            }
353        }
354
355        Ok(Datatype::Compound {
356            size: self.size,
357            members: self.fields,
358        })
359    }
360}
361
362impl CompoundTypeBuilder {
363    /// Create a compound builder with an explicit total size and field offsets.
364    pub fn with_size(size: u32) -> ExplicitCompoundTypeBuilder {
365        ExplicitCompoundTypeBuilder {
366            size,
367            fields: Vec::new(),
368        }
369    }
370}
371
372/// Builder for constructing HDF5 enumeration datatypes.
373pub struct EnumTypeBuilder {
374    base_type: Datatype,
375    members: Vec<EnumMember>,
376}
377
378impl EnumTypeBuilder {
379    /// Create a new enum builder with i32 base type.
380    pub fn i32_based() -> Self {
381        Self {
382            base_type: make_i32_type(),
383            members: Vec::new(),
384        }
385    }
386
387    /// Create a new enum builder with u8 base type.
388    pub fn u8_based() -> Self {
389        Self {
390            base_type: make_u8_type(),
391            members: Vec::new(),
392        }
393    }
394
395    /// Add a named value.
396    pub fn value(mut self, name: &str, val: i32) -> Self {
397        self.members.push(EnumMember {
398            name: name.to_string(),
399            value: val.to_le_bytes().to_vec(),
400        });
401        self
402    }
403
404    /// Add a named u8 value.
405    pub fn u8_value(mut self, name: &str, val: u8) -> Self {
406        self.members.push(EnumMember {
407            name: name.to_string(),
408            value: vec![val],
409        });
410        self
411    }
412
413    /// Build the enumeration datatype.
414    pub fn build(self) -> Datatype {
415        let size = self.base_type.type_size();
416        Datatype::Enumeration {
417            size,
418            base_type: Box::new(self.base_type),
419            members: self.members,
420        }
421    }
422}
423
424// ---- Attribute helper ----
425
426pub(crate) fn build_attr_message(name: &str, value: &AttrValue) -> AttributeMessage {
427    match value {
428        AttrValue::F64(v) => AttributeMessage {
429            name: name.to_string(),
430            datatype: make_f64_type(),
431            dataspace: scalar_ds(),
432            raw_data: v.to_le_bytes().to_vec(),
433        },
434        AttrValue::F64Array(arr) => {
435            let mut raw = Vec::with_capacity(arr.len() * 8);
436            for v in arr {
437                raw.extend_from_slice(&v.to_le_bytes());
438            }
439            AttributeMessage {
440                name: name.to_string(),
441                datatype: make_f64_type(),
442                dataspace: simple_1d(arr.len() as u64),
443                raw_data: raw,
444            }
445        }
446        AttrValue::I64(v) => AttributeMessage {
447            name: name.to_string(),
448            datatype: make_i64_type(),
449            dataspace: scalar_ds(),
450            raw_data: v.to_le_bytes().to_vec(),
451        },
452        AttrValue::I64Array(arr) => {
453            let mut raw = Vec::with_capacity(arr.len() * 8);
454            for v in arr {
455                raw.extend_from_slice(&v.to_le_bytes());
456            }
457            AttributeMessage {
458                name: name.to_string(),
459                datatype: make_i64_type(),
460                dataspace: simple_1d(arr.len() as u64),
461                raw_data: raw,
462            }
463        }
464        AttrValue::I32(v) => AttributeMessage {
465            name: name.to_string(),
466            datatype: make_i32_type(),
467            dataspace: scalar_ds(),
468            raw_data: v.to_le_bytes().to_vec(),
469        },
470        AttrValue::U32(v) => AttributeMessage {
471            name: name.to_string(),
472            datatype: make_u32_type(),
473            dataspace: scalar_ds(),
474            raw_data: v.to_le_bytes().to_vec(),
475        },
476        AttrValue::U64(v) => AttributeMessage {
477            name: name.to_string(),
478            datatype: make_u64_type(),
479            dataspace: scalar_ds(),
480            raw_data: v.to_le_bytes().to_vec(),
481        },
482        AttrValue::String(s) => {
483            let bytes = s.as_bytes();
484            AttributeMessage {
485                name: name.to_string(),
486                datatype: Datatype::String {
487                    #[expect(
488                        clippy::cast_possible_truncation,
489                        reason = "string byte length is stored in the 4-byte fixed-string datatype size field"
490                    )]
491                    size: bytes.len() as u32,
492                    padding: StringPadding::NullPad,
493                    charset: CharacterSet::Utf8,
494                },
495                dataspace: scalar_ds(),
496                raw_data: bytes.to_vec(),
497            }
498        }
499        AttrValue::StringArray(arr) => {
500            let max_len = arr.iter().map(|s| s.len()).max().unwrap_or(0);
501            let mut raw = Vec::new();
502            for s in arr {
503                let mut b = s.as_bytes().to_vec();
504                b.resize(max_len, 0);
505                raw.extend_from_slice(&b);
506            }
507            AttributeMessage {
508                name: name.to_string(),
509                datatype: Datatype::String {
510                    #[expect(
511                        clippy::cast_possible_truncation,
512                        reason = "max string byte length is stored in the 4-byte fixed-string datatype size field"
513                    )]
514                    size: max_len as u32,
515                    padding: StringPadding::NullPad,
516                    charset: CharacterSet::Utf8,
517                },
518                dataspace: simple_1d(arr.len() as u64),
519                raw_data: raw,
520            }
521        }
522        AttrValue::AsciiString(s) => {
523            let bytes = s.as_bytes();
524            AttributeMessage {
525                name: name.to_string(),
526                datatype: Datatype::String {
527                    #[expect(
528                        clippy::cast_possible_truncation,
529                        reason = "string byte length is stored in the 4-byte fixed-string datatype size field"
530                    )]
531                    size: bytes.len() as u32,
532                    padding: StringPadding::NullPad,
533                    charset: CharacterSet::Ascii,
534                },
535                dataspace: scalar_ds(),
536                raw_data: bytes.to_vec(),
537            }
538        }
539        AttrValue::AsciiStringArray(arr) => {
540            let max_len = arr.iter().map(|s| s.len()).max().unwrap_or(0);
541            let mut raw = Vec::new();
542            for s in arr {
543                let mut b = s.as_bytes().to_vec();
544                b.resize(max_len, 0);
545                raw.extend_from_slice(&b);
546            }
547            AttributeMessage {
548                name: name.to_string(),
549                datatype: Datatype::String {
550                    #[expect(
551                        clippy::cast_possible_truncation,
552                        reason = "max string byte length is stored in the 4-byte fixed-string datatype size field"
553                    )]
554                    size: max_len as u32,
555                    padding: StringPadding::NullPad,
556                    charset: CharacterSet::Ascii,
557                },
558                dataspace: simple_1d(arr.len() as u64),
559                raw_data: raw,
560            }
561        }
562        AttrValue::VarLenAsciiArray(strings) => {
563            // MATLAB v7.3 (and matio) expect MATLAB_fields and similar
564            // variable-length ASCII arrays encoded as:
565            //   H5T_VLEN { H5T_STRING { STRSIZE=1, NULLTERM, ASCII } }
566            // — a VLEN sequence of 1-byte fixed strings. The on-disk byte
567            // layout is identical to H5T_STRING{STRSIZE=VAR} (length + heap
568            // address + object index per element; heap object holds raw
569            // bytes without null terminator), so only the datatype
570            // descriptor changes.
571            let vl_ref_size = 16usize; // 4 + 8 + 4 for offset_size=8
572            let mut raw = Vec::with_capacity(strings.len() * vl_ref_size);
573            for (i, s) in strings.iter().enumerate() {
574                #[expect(
575                    clippy::cast_possible_truncation,
576                    reason = "VLEN string length is written into the 4-byte length prefix of the variable-length reference"
577                )]
578                raw.extend_from_slice(&(s.len() as u32).to_le_bytes());
579                raw.extend_from_slice(&0u64.to_le_bytes()); // patched later
580                #[expect(
581                    clippy::cast_possible_truncation,
582                    reason = "1-based heap object index is written into the 4-byte object-index field of the variable-length reference"
583                )]
584                raw.extend_from_slice(&((i + 1) as u32).to_le_bytes());
585            }
586            AttributeMessage {
587                name: name.to_string(),
588                datatype: Datatype::VariableLength {
589                    is_string: false,
590                    padding: None,
591                    charset: None,
592                    base_type: Box::new(Datatype::String {
593                        size: 1,
594                        padding: StringPadding::NullTerminate,
595                        charset: CharacterSet::Ascii,
596                    }),
597                },
598                dataspace: simple_1d(strings.len() as u64),
599                raw_data: raw,
600            }
601        }
602    }
603}
604
605/// Build a global heap collection containing the given byte sequences.
606/// Returns the serialized collection bytes.
607pub(crate) fn build_global_heap_collection(strings: &[&str]) -> Vec<u8> {
608    let objects: Vec<&[u8]> = strings.iter().map(|s| s.as_bytes()).collect();
609    build_global_heap_collection_bytes(&objects)
610}
611
612/// Build a global heap collection from raw byte objects (no UTF-8 requirement),
613/// assigning 1-based object indices in order. Mirrors
614/// [`build_global_heap_collection`] but accepts arbitrary bytes so a faithful
615/// rewrite can carry embedded-NUL or non-UTF-8 VL-string payloads.
616pub(crate) fn build_global_heap_collection_bytes(objects: &[&[u8]]) -> Vec<u8> {
617    let length_size = 8usize;
618    let header_size = 8 + length_size; // sig(4) + ver(1) + reserved(3) + collection_size
619
620    // Calculate total size
621    let mut obj_size_total = 0usize;
622    for obj in objects {
623        let obj_header = 8 + length_size; // index(2) + refcount(2) + reserved(4) + size
624        let padded_data_len = (obj.len() + 7) & !7; // pad to 8 bytes
625        obj_size_total += obj_header + padded_data_len;
626    }
627    obj_size_total += 8 + length_size; // free space marker (full object header size)
628    let collection_size = header_size + obj_size_total;
629    // The C HDF5 library enforces a minimum collection size of 4096 bytes.
630    let min_collection_size = 4096;
631    let padded_collection = ((collection_size.max(min_collection_size)) + 7) & !7;
632
633    let mut buf = Vec::with_capacity(padded_collection);
634    // Header
635    buf.extend_from_slice(b"GCOL");
636    buf.push(1); // version
637    buf.extend_from_slice(&[0u8; 3]); // reserved
638    buf.extend_from_slice(&(padded_collection as u64).to_le_bytes());
639
640    // Objects (1-based indices)
641    for (i, obj) in objects.iter().enumerate() {
642        #[expect(
643            clippy::cast_possible_truncation,
644            reason = "1-based heap object index is written into the 2-byte heap-object index field"
645        )]
646        let index = (i + 1) as u16;
647        buf.extend_from_slice(&index.to_le_bytes());
648        buf.extend_from_slice(&1u16.to_le_bytes()); // ref_count
649        buf.extend_from_slice(&[0u8; 4]); // reserved
650        buf.extend_from_slice(&(obj.len() as u64).to_le_bytes());
651        buf.extend_from_slice(obj);
652        // Pad to 8-byte boundary
653        let padded = (obj.len() + 7) & !7;
654        for _ in obj.len()..padded {
655            buf.push(0);
656        }
657    }
658
659    // Free space marker (index 0): the C library uses this size as the total
660    // skip distance from the start of the object (including its header), so
661    // it must equal the remaining bytes in the collection from this point.
662    let free_total_size = padded_collection - buf.len();
663    buf.extend_from_slice(&0u16.to_le_bytes()); // index 0
664    buf.extend_from_slice(&0u16.to_le_bytes()); // ref_count
665    buf.extend_from_slice(&[0u8; 4]); // reserved
666    buf.extend_from_slice(&(free_total_size as u64).to_le_bytes()); // size
667
668    // Pad collection to full size
669    buf.resize(padded_collection, 0);
670
671    buf
672}
673
674/// Patch VL attribute references with the actual global heap collection address.
675/// The raw_data contains VL references with placeholder addresses (0).
676pub(crate) fn patch_vl_refs(raw_data: &mut [u8], collection_address: u64) {
677    let vl_ref_size = 16; // 4 + 8 + 4
678    let count = raw_data.len() / vl_ref_size;
679    for i in 0..count {
680        let addr_offset = i * vl_ref_size + 4; // skip sequence_length
681        raw_data[addr_offset..addr_offset + 8].copy_from_slice(&collection_address.to_le_bytes());
682    }
683}
684
685/// A single element of a VL-string dataset being written: either a null
686/// reference (no heap object) or a heap object carrying these exact bytes.
687///
688/// The two are distinct in the HDF5 model: a null reference reads back as a
689/// null/empty element with no heap object, whereas a zero-length heap object
690/// reads back as an empty string `""`. Carrying both lets a faithful rewrite
691/// reproduce the source byte-for-byte.
692#[derive(Debug, Clone, PartialEq, Eq)]
693pub(crate) enum VlStringElement {
694    /// A null reference: length 0, undefined heap address, no heap object.
695    Null,
696    /// A heap object holding these exact bytes (possibly empty).
697    Bytes(Vec<u8>),
698}
699
700/// 16-byte size of a single VL global-heap reference (offset_size = 8):
701/// length(4) + collection address(8) + object index(4).
702pub(crate) const VL_REF_SIZE: usize = 16;
703
704/// Staged VL-string dataset/attribute element data: the per-element 16-byte
705/// references (with placeholder heap addresses) plus the global heap collection
706/// holding the non-null elements' bytes.
707///
708/// Object indices are assigned 1-based and contiguous over the non-null
709/// elements in order, matching [`build_global_heap_collection_bytes`]. Null
710/// elements carry an undefined address and object index 0, and are never
711/// patched so they read back as null.
712pub(crate) struct VlStringStaging {
713    /// The dataset/attribute element bytes: one 16-byte reference per element.
714    pub refs: Vec<u8>,
715    /// The serialized global heap collection holding the non-null objects.
716    pub collection_bytes: Vec<u8>,
717    /// `true` for each element that references a heap object and must have its
718    /// address patched; `false` for a null element that must stay undefined.
719    pub patch_mask: Vec<bool>,
720}
721
722/// Stage the references and global-heap collection for a variable-length
723/// dataset/attribute from its per-element byte payloads.
724///
725/// `element_size` is the byte width of the VL base type. A VL string's base type
726/// is a single byte (`element_size == 1`), so the reference's stored `length`
727/// equals the payload's byte count. A non-string VL sequence stores an element
728/// *count* in that field, so the length written is `bytes.len() / element_size`,
729/// while the heap object still holds the exact bytes.
730pub(crate) fn stage_vl_elements(
731    elements: &[VlStringElement],
732    element_size: usize,
733) -> VlStringStaging {
734    let element_size = element_size.max(1);
735    // Collect the non-null payloads in order; their 1-based positions become
736    // the heap object indices.
737    let mut objects: Vec<&[u8]> = Vec::new();
738    let mut refs = Vec::with_capacity(elements.len() * VL_REF_SIZE);
739    let mut patch_mask = Vec::with_capacity(elements.len());
740    for element in elements {
741        match element {
742            VlStringElement::Null => {
743                // A null VL reference: HDF5 marks "no heap object" with a zero
744                // heap address (`H5T__vlen_disk_isnull` tests addr == 0), not the
745                // all-ones "undefined address" sentinel — the reference C library
746                // rejects the latter as a bad heap index when reading.
747                refs.extend_from_slice(&0u32.to_le_bytes()); // length 0
748                refs.extend_from_slice(&0u64.to_le_bytes()); // null heap address
749                refs.extend_from_slice(&0u32.to_le_bytes()); // object index 0
750                patch_mask.push(false);
751            }
752            VlStringElement::Bytes(bytes) => {
753                #[expect(
754                    clippy::cast_possible_truncation,
755                    reason = "VL element length (element count) is written into the 4-byte \
756                              length prefix of the variable-length reference"
757                )]
758                refs.extend_from_slice(&((bytes.len() / element_size) as u32).to_le_bytes());
759                refs.extend_from_slice(&0u64.to_le_bytes()); // patched later
760                let index = objects.len() + 1; // 1-based heap object index
761                #[expect(
762                    clippy::cast_possible_truncation,
763                    reason = "1-based heap object index is written into the 4-byte object-index \
764                              field of the variable-length reference"
765                )]
766                refs.extend_from_slice(&(index as u32).to_le_bytes());
767                objects.push(bytes);
768                patch_mask.push(true);
769            }
770        }
771    }
772    // A dataset of only null elements (or an empty dataset) references no heap
773    // object, so emit no collection — there is nothing to patch and a 4096-byte
774    // empty GCOL would be dead weight.
775    let collection_bytes = if objects.is_empty() {
776        Vec::new()
777    } else {
778        build_global_heap_collection_bytes(&objects)
779    };
780    VlStringStaging {
781        refs,
782        collection_bytes,
783        patch_mask,
784    }
785}
786
787/// Patch the heap address of each VL reference flagged in `patch_mask`, leaving
788/// null references (mask `false`) with their undefined address. Mirrors
789/// [`patch_vl_refs`] but skips null elements so they read back as null.
790pub(crate) fn patch_vl_refs_masked(
791    raw_data: &mut [u8],
792    patch_mask: &[bool],
793    collection_address: u64,
794) {
795    for (i, &patch) in patch_mask.iter().enumerate() {
796        if !patch {
797            continue;
798        }
799        let addr_offset = i * VL_REF_SIZE + 4; // skip sequence_length
800        raw_data[addr_offset..addr_offset + 8].copy_from_slice(&collection_address.to_le_bytes());
801    }
802}
803
804pub(crate) fn scalar_ds() -> Dataspace {
805    Dataspace {
806        space_type: DataspaceType::Scalar,
807        rank: 0,
808        dimensions: vec![],
809        max_dimensions: None,
810    }
811}
812
813pub(crate) fn simple_1d(n: u64) -> Dataspace {
814    Dataspace {
815        space_type: DataspaceType::Simple,
816        rank: 1,
817        dimensions: vec![n],
818        max_dimensions: None,
819    }
820}
821
822// ---- Attribute values ----
823
824/// Convenient attribute values for the write API.
825#[derive(Debug, Clone, PartialEq)]
826pub enum AttrValue {
827    F64(f64),
828    F64Array(Vec<f64>),
829    I32(i32),
830    I64(i64),
831    I64Array(Vec<i64>),
832    U32(u32),
833    U64(u64),
834    /// UTF-8 string attribute (null-padded).
835    String(String),
836    StringArray(Vec<String>),
837    /// Fixed-width ASCII string attribute (charset = ASCII).
838    AsciiString(String),
839    /// Array of fixed-width ASCII strings (null-padded to the longest element).
840    /// Compatible with MATLAB `MATLAB_fields` and matio.
841    AsciiStringArray(Vec<String>),
842    /// Array of variable-length ASCII strings (MATLAB_fields pattern).
843    /// Each element is a variable-length sequence of ASCII bytes.
844    /// Requires a global heap collection in the file.
845    VarLenAsciiArray(Vec<String>),
846}
847
848// ---- Dataset builder ----
849
850/// Configuration for SHINES provenance metadata.
851#[cfg(feature = "provenance")]
852#[derive(Debug, Clone)]
853pub struct ProvenanceConfig {
854    pub creator: String,
855    pub timestamp: String,
856    pub source: Option<String>,
857}
858
859/// Everything [`DatasetBuilder::with_raw_chunks_lazy`] needs to re-emit a chunked
860/// dataset by copying its source chunks verbatim (no decode/re-encode): the
861/// per-chunk sizes/masks (enough to plan the destination layout without reading
862/// any bytes), a provider that yields each chunk's bytes on demand at write time,
863/// the source filter-pipeline message, and the chunk geometry. Built by repack
864/// from a source [`Dataset`].
865pub(crate) struct RawChunkPayload {
866    /// Logical chunk dimensions (rank entries, not the trailing element size).
867    pub(crate) chunk_dims: Vec<u64>,
868    /// Datatype element size in bytes.
869    pub(crate) element_size: usize,
870    /// Full uncompressed byte size of one chunk
871    /// (`product(chunk_dims) * element_size`), identical for every chunk.
872    pub(crate) raw_size: u64,
873    /// The verbatim source `FilterPipeline` message bytes, if the source had one.
874    pub(crate) pipeline_message: Option<Vec<u8>>,
875    /// Per-chunk sizes + filter masks in dense row-major grid order, one per slot.
876    pub(crate) meta: Vec<ChunkMeta>,
877    /// Yields each chunk's compressed bytes on demand during the write, so no
878    /// more than one chunk's bytes are resident. Owns its source (e.g. an
879    /// `Arc<File>`), so it carries no borrowed lifetime.
880    ///
881    /// Wrapped in [`AssertUnwindSafe`](core::panic::AssertUnwindSafe) so the
882    /// boxed trait object does not strip the `UnwindSafe`/`RefUnwindSafe`
883    /// auto-traits from the public builder types that transitively hold it
884    /// (removing an auto-trait impl is a semver break). The assertion is sound:
885    /// the provider performs only immutable reads and leaves no broken state on
886    /// a panic. `ChunkProvider: Send + Sync` keeps the other two auto-traits.
887    pub(crate) provider: core::panic::AssertUnwindSafe<Box<dyn ChunkProvider>>,
888}
889
890/// One element of an object-reference dataset written through the builder.
891///
892/// A reference either names an object by path (resolved to that object's
893/// destination address during serialization) or carries a raw address verbatim.
894/// The raw form preserves a null reference (address 0) or an undefined reference
895/// (`HADDR_UNDEF`, all-ones) exactly, which a faithful rewrite (repack) needs.
896#[derive(Debug, Clone, PartialEq, Eq)]
897pub(crate) enum ObjectRefTarget {
898    /// Resolve to the destination address of the object at this path.
899    Path(String),
900    /// Write this exact 8-byte address (e.g. 0 for null, `u64::MAX` for
901    /// undefined).
902    Raw(u64),
903}
904
905/// Builder for datasets.
906pub struct DatasetBuilder {
907    pub(crate) name: String,
908    pub(crate) datatype: Option<Datatype>,
909    pub(crate) shape: Option<Vec<u64>>,
910    pub(crate) maxshape: Option<Vec<u64>>,
911    pub(crate) data: Option<Vec<u8>>,
912    pub(crate) attrs: Vec<(String, AttrValue)>,
913    pub(crate) chunk_options: ChunkOptions,
914    /// When set, this dataset's chunks are copied verbatim from a source file
915    /// (repack's verbatim path): the already-compressed chunk bytes, the source
916    /// filter-pipeline message, and the geometry needed to lay them out. This
917    /// takes precedence over `data` / `chunk_options` for chunked storage.
918    pub(crate) raw_chunks: Option<RawChunkPayload>,
919    /// When set, this dataset is an object-reference dataset whose element
920    /// addresses are resolved (per-element by path, or written raw) during file
921    /// serialization once every object's destination address is known.
922    pub(crate) reference_targets: Option<Vec<ObjectRefTarget>>,
923    /// When set, this dataset stores variable-length strings: `data` holds the
924    /// 16-byte references with placeholder heap addresses, and this staging
925    /// carries the global heap collection plus the mask of references to patch
926    /// once the post-data cursor is known.
927    pub(crate) vl_string_staging: Option<VlStringStaging>,
928    #[cfg(feature = "provenance")]
929    pub(crate) provenance: Option<ProvenanceConfig>,
930}
931
932impl DatasetBuilder {
933    pub(crate) fn new(name: &str) -> Self {
934        Self {
935            name: name.to_string(),
936            datatype: None,
937            shape: None,
938            maxshape: None,
939            data: None,
940            attrs: Vec::new(),
941            chunk_options: ChunkOptions::default(),
942            raw_chunks: None,
943            reference_targets: None,
944            vl_string_staging: None,
945            #[cfg(feature = "provenance")]
946            provenance: None,
947        }
948    }
949
950    pub fn with_f64_data(&mut self, data: &[f64]) -> &mut Self {
951        self.datatype = Some(make_f64_type());
952        let mut b = Vec::with_capacity(data.len() * 8);
953        for &v in data {
954            b.extend_from_slice(&v.to_le_bytes());
955        }
956        self.data = Some(b);
957        if self.shape.is_none() {
958            self.shape = Some(vec![data.len() as u64]);
959        }
960        self
961    }
962
963    pub fn with_f32_data(&mut self, data: &[f32]) -> &mut Self {
964        self.datatype = Some(make_f32_type());
965        let mut b = Vec::with_capacity(data.len() * 4);
966        for &v in data {
967            b.extend_from_slice(&v.to_le_bytes());
968        }
969        self.data = Some(b);
970        if self.shape.is_none() {
971            self.shape = Some(vec![data.len() as u64]);
972        }
973        self
974    }
975
976    pub fn with_i32_data(&mut self, data: &[i32]) -> &mut Self {
977        self.datatype = Some(make_i32_type());
978        let mut b = Vec::with_capacity(data.len() * 4);
979        for &v in data {
980            b.extend_from_slice(&v.to_le_bytes());
981        }
982        self.data = Some(b);
983        if self.shape.is_none() {
984            self.shape = Some(vec![data.len() as u64]);
985        }
986        self
987    }
988
989    pub fn with_i64_data(&mut self, data: &[i64]) -> &mut Self {
990        self.datatype = Some(make_i64_type());
991        let mut b = Vec::with_capacity(data.len() * 8);
992        for &v in data {
993            b.extend_from_slice(&v.to_le_bytes());
994        }
995        self.data = Some(b);
996        if self.shape.is_none() {
997            self.shape = Some(vec![data.len() as u64]);
998        }
999        self
1000    }
1001
1002    pub fn with_u8_data(&mut self, data: &[u8]) -> &mut Self {
1003        self.datatype = Some(make_u8_type());
1004        self.data = Some(data.to_vec());
1005        if self.shape.is_none() {
1006            self.shape = Some(vec![data.len() as u64]);
1007        }
1008        self
1009    }
1010
1011    pub fn with_i8_data(&mut self, data: &[i8]) -> &mut Self {
1012        self.datatype = Some(make_i8_type());
1013        let mut b = Vec::with_capacity(data.len());
1014        for &v in data {
1015            b.push(v as u8);
1016        }
1017        self.data = Some(b);
1018        if self.shape.is_none() {
1019            self.shape = Some(vec![data.len() as u64]);
1020        }
1021        self
1022    }
1023
1024    pub fn with_i16_data(&mut self, data: &[i16]) -> &mut Self {
1025        self.datatype = Some(make_i16_type());
1026        let mut b = Vec::with_capacity(data.len() * 2);
1027        for &v in data {
1028            b.extend_from_slice(&v.to_le_bytes());
1029        }
1030        self.data = Some(b);
1031        if self.shape.is_none() {
1032            self.shape = Some(vec![data.len() as u64]);
1033        }
1034        self
1035    }
1036
1037    pub fn with_u16_data(&mut self, data: &[u16]) -> &mut Self {
1038        self.datatype = Some(make_u16_type());
1039        let mut b = Vec::with_capacity(data.len() * 2);
1040        for &v in data {
1041            b.extend_from_slice(&v.to_le_bytes());
1042        }
1043        self.data = Some(b);
1044        if self.shape.is_none() {
1045            self.shape = Some(vec![data.len() as u64]);
1046        }
1047        self
1048    }
1049
1050    pub fn with_u32_data(&mut self, data: &[u32]) -> &mut Self {
1051        self.datatype = Some(make_u32_type());
1052        let mut b = Vec::with_capacity(data.len() * 4);
1053        for &v in data {
1054            b.extend_from_slice(&v.to_le_bytes());
1055        }
1056        self.data = Some(b);
1057        if self.shape.is_none() {
1058            self.shape = Some(vec![data.len() as u64]);
1059        }
1060        self
1061    }
1062
1063    pub fn with_u64_data(&mut self, data: &[u64]) -> &mut Self {
1064        self.datatype = Some(make_u64_type());
1065        let mut b = Vec::with_capacity(data.len() * 8);
1066        for &v in data {
1067            b.extend_from_slice(&v.to_le_bytes());
1068        }
1069        self.data = Some(b);
1070        if self.shape.is_none() {
1071            self.shape = Some(vec![data.len() as u64]);
1072        }
1073        self
1074    }
1075
1076    /// Write an object reference dataset. Each address is an 8-byte absolute
1077    /// address pointing to an object header in the file.
1078    pub fn with_reference_data(&mut self, addresses: &[u64]) -> &mut Self {
1079        self.datatype = Some(make_object_reference_type());
1080        let mut b = Vec::with_capacity(addresses.len() * 8);
1081        for &addr in addresses {
1082            b.extend_from_slice(&addr.to_le_bytes());
1083        }
1084        self.data = Some(b);
1085        if self.shape.is_none() {
1086            self.shape = Some(vec![addresses.len() as u64]);
1087        }
1088        self
1089    }
1090
1091    /// Write an object reference dataset by path. During file serialization,
1092    /// each path is resolved to the absolute address of the named object.
1093    /// Paths use `/` separators (e.g., `"#refs#/child1"`).
1094    pub fn with_path_references(&mut self, paths: &[&str]) -> &mut Self {
1095        let targets = paths
1096            .iter()
1097            .map(|s| ObjectRefTarget::Path(s.to_string()))
1098            .collect();
1099        self.with_object_references(targets)
1100    }
1101
1102    /// Write an object-reference dataset from explicit per-element targets,
1103    /// preserving null/undefined references verbatim. The datatype is set to the
1104    /// 8-byte object-reference type; each [`ObjectRefTarget::Path`] is resolved to
1105    /// its destination address during serialization, while
1106    /// [`ObjectRefTarget::Raw`] is written as-is. This is the faithful re-emit
1107    /// path used by repack. The shape defaults to `[targets.len()]` unless
1108    /// [`with_shape`](Self::with_shape) sets it.
1109    pub(crate) fn with_object_references(&mut self, targets: Vec<ObjectRefTarget>) -> &mut Self {
1110        self.datatype = Some(make_object_reference_type());
1111        // Placeholder zeros — patched once all destination addresses are known.
1112        self.data = Some(vec![0u8; targets.len() * 8]);
1113        if self.shape.is_none() {
1114            self.shape = Some(vec![targets.len() as u64]);
1115        }
1116        self.reference_targets = Some(targets);
1117        self
1118    }
1119
1120    /// Write a complex32 (f32 real/imag pair) dataset.
1121    pub fn with_complex32_data(&mut self, data: &[(f32, f32)]) -> &mut Self {
1122        let ct = CompoundTypeBuilder::new()
1123            .f32_field("real")
1124            .f32_field("imag")
1125            .build();
1126        let mut raw = Vec::with_capacity(data.len() * 8);
1127        for &(r, i) in data {
1128            raw.extend_from_slice(&r.to_le_bytes());
1129            raw.extend_from_slice(&i.to_le_bytes());
1130        }
1131        self.with_compound_data(ct, raw, data.len() as u64)
1132    }
1133
1134    /// Write a complex64 (f64 real/imag pair) dataset.
1135    pub fn with_complex64_data(&mut self, data: &[(f64, f64)]) -> &mut Self {
1136        let ct = CompoundTypeBuilder::new()
1137            .f64_field("real")
1138            .f64_field("imag")
1139            .build();
1140        let mut raw = Vec::with_capacity(data.len() * 16);
1141        for &(r, i) in data {
1142            raw.extend_from_slice(&r.to_le_bytes());
1143            raw.extend_from_slice(&i.to_le_bytes());
1144        }
1145        self.with_compound_data(ct, raw, data.len() as u64)
1146    }
1147
1148    /// Write a compound (struct) dataset.
1149    pub fn with_compound_data(
1150        &mut self,
1151        datatype: Datatype,
1152        raw_data: Vec<u8>,
1153        num_elements: u64,
1154    ) -> &mut Self {
1155        self.with_raw_data(datatype, raw_data, num_elements)
1156    }
1157
1158    /// Write a dataset from an explicit datatype and its raw element bytes.
1159    ///
1160    /// The lowest-level data entry point: `raw_data` is written verbatim as the
1161    /// dataset's storage, interpreted by `datatype`, so the caller is
1162    /// responsible for the bytes matching the datatype's on-disk layout (little
1163    /// endian, `num_elements` elements each of the datatype's size). It underpins
1164    /// the typed helpers and lets a captured `(datatype, bytes)` pair — for
1165    /// example from reading an existing dataset — be re-emitted without a typed
1166    /// helper. The shape defaults to `[num_elements]` unless
1167    /// [`with_shape`](Self::with_shape) sets it.
1168    pub fn with_raw_data(
1169        &mut self,
1170        datatype: Datatype,
1171        raw_data: Vec<u8>,
1172        num_elements: u64,
1173    ) -> &mut Self {
1174        self.datatype = Some(datatype);
1175        self.data = Some(raw_data);
1176        if self.shape.is_none() {
1177            self.shape = Some(vec![num_elements]);
1178        }
1179        self
1180    }
1181
1182    /// Stage a chunked dataset whose chunks are streamed verbatim from a source
1183    /// file one at a time, without decoding or re-encoding any chunk and without
1184    /// holding more than one chunk's bytes in memory.
1185    ///
1186    /// Repack's out-of-core verbatim path: `meta` is the per-chunk sizes + filter
1187    /// masks in dense row-major chunk-grid order (one per slot), and `provider`
1188    /// yields each chunk's already-compressed bytes on demand at write time. The
1189    /// destination layout is computed from `meta` alone, so the chunks are never
1190    /// all resident at once. `pipeline_message` is the source's `FilterPipeline`
1191    /// message bytes, reused as-is so every filter — including ones this crate
1192    /// cannot itself apply (ZFP, SZIP, unknown) — is reproduced byte-for-byte.
1193    /// `dims`/`maxshape`/`chunk_dims`/`element_size` describe the geometry. The
1194    /// shape defaults to `dims` and the chunk dimensions to `chunk_dims`. The
1195    /// provider owns its source (e.g. an `Arc<File>`), so this carries no
1196    /// borrowed lifetime.
1197    #[allow(clippy::too_many_arguments)]
1198    pub(crate) fn with_raw_chunks_lazy(
1199        &mut self,
1200        datatype: Datatype,
1201        dims: &[u64],
1202        maxshape: Option<&[u64]>,
1203        chunk_dims: &[u64],
1204        element_size: usize,
1205        pipeline_message: Option<Vec<u8>>,
1206        meta: Vec<ChunkMeta>,
1207        provider: Box<dyn ChunkProvider>,
1208    ) -> &mut Self {
1209        // Full uncompressed bytes of one chunk (drives the fixed/extensible-array
1210        // chunk-size encoding width). Saturating arithmetic matches the writer's
1211        // overflow discipline; the values come from an already-validated source
1212        // file, so this only guards against an absurd input rather than a real case.
1213        let raw_size: u64 = chunk_dims
1214            .iter()
1215            .copied()
1216            .product::<u64>()
1217            .saturating_mul(element_size as u64);
1218        self.datatype = Some(datatype);
1219        if self.shape.is_none() {
1220            self.shape = Some(dims.to_vec());
1221        }
1222        if let Some(ms) = maxshape {
1223            self.maxshape = Some(ms.to_vec());
1224        }
1225        self.chunk_options.chunk_dims = Some(chunk_dims.to_vec());
1226        self.raw_chunks = Some(RawChunkPayload {
1227            chunk_dims: chunk_dims.to_vec(),
1228            element_size,
1229            raw_size,
1230            pipeline_message,
1231            meta,
1232            provider: core::panic::AssertUnwindSafe(provider),
1233        });
1234        self
1235    }
1236
1237    /// Safely encode a slice of compound values field by field.
1238    ///
1239    /// Built-in implementations support numeric tuples with one through twelve
1240    /// fields. No Rust struct or tuple padding is copied into the file.
1241    pub fn with_compound_values<T: CompoundType>(
1242        &mut self,
1243        values: &[T],
1244    ) -> Result<&mut Self, crate::error::FormatError> {
1245        let datatype = T::datatype()?;
1246        if !matches!(datatype, Datatype::Compound { .. }) {
1247            return Err(crate::error::FormatError::TypeMismatch {
1248                expected: "Compound",
1249                actual: "non-Compound",
1250            });
1251        }
1252        let element_size = datatype.type_size().to_usize()?;
1253        if element_size == 0 {
1254            return Err(crate::error::FormatError::InvalidCompoundSize);
1255        }
1256        let mut raw = Vec::with_capacity(values.len().saturating_mul(element_size));
1257        for value in values {
1258            let start = raw.len();
1259            value.encode(&mut raw);
1260            let actual = raw.len() - start;
1261            if actual != element_size {
1262                return Err(crate::error::FormatError::DataSizeMismatch {
1263                    expected: element_size,
1264                    actual,
1265                });
1266            }
1267        }
1268        Ok(self.with_compound_data(datatype, raw, values.len() as u64))
1269    }
1270
1271    /// Write an enum dataset with i32 values.
1272    pub fn with_enum_i32_data(&mut self, datatype: Datatype, values: &[i32]) -> &mut Self {
1273        self.datatype = Some(datatype);
1274        let mut raw = Vec::with_capacity(values.len() * 4);
1275        for &v in values {
1276            raw.extend_from_slice(&v.to_le_bytes());
1277        }
1278        self.data = Some(raw);
1279        if self.shape.is_none() {
1280            self.shape = Some(vec![values.len() as u64]);
1281        }
1282        self
1283    }
1284
1285    /// Write an enum dataset with u8 values.
1286    pub fn with_enum_u8_data(&mut self, datatype: Datatype, values: &[u8]) -> &mut Self {
1287        self.datatype = Some(datatype);
1288        self.data = Some(values.to_vec());
1289        if self.shape.is_none() {
1290            self.shape = Some(vec![values.len() as u64]);
1291        }
1292        self
1293    }
1294
1295    /// Write a variable-length UTF-8 string dataset.
1296    ///
1297    /// Each element is stored as a global-heap object holding the string's
1298    /// bytes; the datatype is `H5T_VLEN { H5T_STRING { STRSIZE=VAR, ASCII or
1299    /// UTF-8 } }`. The shape defaults to `[values.len()]` unless
1300    /// [`with_shape`](Self::with_shape) sets it (use that for ND VL strings,
1301    /// passing `values` in row-major order).
1302    ///
1303    /// Empty strings become zero-length heap objects (reading back as `""`).
1304    /// For full fidelity over null-vs-empty elements, embedded NULs, non-UTF-8
1305    /// payloads, or a specific charset/padding, use
1306    /// [`with_vlen_string_elements`](Self::with_vlen_string_elements).
1307    pub fn with_vlen_strings(&mut self, values: &[&str]) -> &mut Self {
1308        let datatype = make_vlen_string_type(CharacterSet::Utf8);
1309        let elements: Vec<VlStringElement> = values
1310            .iter()
1311            .map(|s| VlStringElement::Bytes(s.as_bytes().to_vec()))
1312            .collect();
1313        self.stage_vlen_strings(datatype, &elements);
1314        self
1315    }
1316
1317    /// Write a variable-length string dataset from an explicit source datatype
1318    /// and per-element byte payloads, preserving the null-vs-empty distinction.
1319    ///
1320    /// `datatype` must be a string-shaped variable-length datatype
1321    /// (`is_string: true`, or the MATLAB `H5T_VLEN { H5T_STRING { STRSIZE=1 } }`
1322    /// shape); its charset, padding, and base type are reproduced verbatim. Each
1323    /// [`VlStringElement`] is either a null reference or a heap object holding
1324    /// exact bytes. This is the faithful re-emit path used by repack. Returns a
1325    /// [`TypeMismatch`](crate::FormatError::TypeMismatch) if `datatype` is not a
1326    /// VL-string datatype. The shape defaults to `[elements.len()]` unless
1327    /// [`with_shape`](Self::with_shape) sets it.
1328    pub(crate) fn with_vlen_string_elements(
1329        &mut self,
1330        datatype: Datatype,
1331        elements: &[VlStringElement],
1332    ) -> Result<&mut Self, crate::error::FormatError> {
1333        if !crate::vl_data::is_vlen_string_datatype(&datatype) {
1334            return Err(crate::error::FormatError::TypeMismatch {
1335                expected: "VariableLength string",
1336                actual: "non-VariableLength string",
1337            });
1338        }
1339        self.stage_vlen_strings(datatype, elements);
1340        Ok(self)
1341    }
1342
1343    /// Shared body of the VL-string write entry points: stage the references
1344    /// and global heap collection and record them on the builder.
1345    fn stage_vlen_strings(&mut self, datatype: Datatype, elements: &[VlStringElement]) {
1346        // A VL string's base type is one byte, so the reference length is the
1347        // byte count (element_size = 1).
1348        self.stage_vlen_elements(datatype, elements, 1);
1349    }
1350
1351    /// Write a *non-string* variable-length (sequence) dataset from an explicit
1352    /// source datatype and per-element byte payloads.
1353    ///
1354    /// `datatype` must be a non-string VL datatype (e.g. `H5T_VLEN
1355    /// { H5T_NATIVE_DOUBLE }`); its base type is reproduced verbatim. Each
1356    /// element's exact heap bytes are re-staged through a fresh global heap, and
1357    /// the per-element reference stores the base-type element count. This is the
1358    /// faithful re-emit path used by repack. Returns a
1359    /// [`TypeMismatch`](crate::FormatError::TypeMismatch) if `datatype` is a
1360    /// string-shaped VL datatype or not variable-length at all. The shape
1361    /// defaults to `[elements.len()]` unless [`with_shape`](Self::with_shape)
1362    /// sets it.
1363    pub(crate) fn with_vlen_sequence_elements(
1364        &mut self,
1365        datatype: Datatype,
1366        elements: &[VlStringElement],
1367    ) -> Result<&mut Self, crate::error::FormatError> {
1368        let Datatype::VariableLength { base_type, .. } = &datatype else {
1369            return Err(crate::error::FormatError::TypeMismatch {
1370                expected: "non-string VariableLength",
1371                actual: "non-VariableLength",
1372            });
1373        };
1374        if crate::vl_data::is_vlen_string_datatype(&datatype) {
1375            return Err(crate::error::FormatError::TypeMismatch {
1376                expected: "non-string VariableLength",
1377                actual: "VariableLength string",
1378            });
1379        }
1380        let element_size = base_type.type_size() as usize;
1381        if element_size == 0 {
1382            return Err(crate::error::FormatError::VlDataError(
1383                "non-string VL base type has zero size".into(),
1384            ));
1385        }
1386        self.stage_vlen_elements(datatype, elements, element_size);
1387        Ok(self)
1388    }
1389
1390    /// Shared body of the VL write entry points (string and sequence): stage the
1391    /// references and global heap collection and record them on the builder.
1392    fn stage_vlen_elements(
1393        &mut self,
1394        datatype: Datatype,
1395        elements: &[VlStringElement],
1396        element_size: usize,
1397    ) {
1398        let n = elements.len() as u64;
1399        let staging = stage_vl_elements(elements, element_size);
1400        self.datatype = Some(datatype);
1401        self.data = Some(staging.refs.clone());
1402        self.vl_string_staging = Some(staging);
1403        if self.shape.is_none() {
1404            self.shape = Some(vec![n]);
1405        }
1406    }
1407
1408    /// Write an array-typed dataset.
1409    pub fn with_array_data(
1410        &mut self,
1411        base_type: Datatype,
1412        array_dims: &[u32],
1413        raw_data: Vec<u8>,
1414        num_elements: u64,
1415    ) -> &mut Self {
1416        self.datatype = Some(Datatype::Array {
1417            base_type: Box::new(base_type),
1418            dimensions: array_dims.to_vec(),
1419        });
1420        self.data = Some(raw_data);
1421        if self.shape.is_none() {
1422            self.shape = Some(vec![num_elements]);
1423        }
1424        self
1425    }
1426
1427    pub fn with_shape(&mut self, shape: &[u64]) -> &mut Self {
1428        self.shape = Some(shape.to_vec());
1429        self
1430    }
1431
1432    /// Set the datatype without providing data.
1433    /// Use with `with_shape` for empty/zero-dimension datasets.
1434    pub fn with_dtype(&mut self, dt: Datatype) -> &mut Self {
1435        self.datatype = Some(dt);
1436        self
1437    }
1438
1439    /// Set maximum dimensions for a resizable dataset.
1440    /// Use `u64::MAX` for unlimited dimensions.
1441    pub fn with_maxshape(&mut self, maxshape: &[u64]) -> &mut Self {
1442        self.maxshape = Some(maxshape.to_vec());
1443        self
1444    }
1445
1446    pub fn set_attr(&mut self, name: &str, value: AttrValue) -> &mut Self {
1447        self.attrs.push((name.to_string(), value));
1448        self
1449    }
1450
1451    /// Enable chunked storage with given chunk dimensions.
1452    pub fn with_chunks(&mut self, chunk_dims: &[u64]) -> &mut Self {
1453        self.chunk_options.chunk_dims = Some(chunk_dims.to_vec());
1454        self
1455    }
1456
1457    /// Enable deflate compression (implies chunked if not already set).
1458    pub fn with_deflate(&mut self, level: u32) -> &mut Self {
1459        self.chunk_options.deflate_level = Some(level);
1460        self
1461    }
1462
1463    /// Enable shuffle filter (usually combined with deflate).
1464    pub fn with_shuffle(&mut self) -> &mut Self {
1465        self.chunk_options.shuffle = true;
1466        self
1467    }
1468
1469    /// Enable fletcher32 checksum.
1470    pub fn with_fletcher32(&mut self) -> &mut Self {
1471        self.chunk_options.fletcher32 = true;
1472        self
1473    }
1474
1475    /// Enable scale-offset compression (implies chunked if not already set).
1476    ///
1477    /// Scale-offset stores each chunk's values as offsets from the chunk
1478    /// minimum, packed into the fewest bits the chunk's range needs:
1479    ///
1480    /// * [`ScaleOffset::Integer`] is **lossless** for integer datasets. Pass
1481    ///   `0` to let the encoder choose the bit width per chunk (the usual
1482    ///   choice).
1483    /// * [`ScaleOffset::FloatDScale`] is **lossy** for float datasets: values
1484    ///   are rounded to the given number of decimal digits before packing.
1485    ///
1486    /// The datatype class/sign/byte-order are derived from the dataset's
1487    /// datatype when the file is written, so the mode must match the data
1488    /// (integer mode on `with_i*`/`with_u*` data, float mode on
1489    /// `with_f32`/`with_f64` data) or `finish()` / `write()` returns a
1490    /// [`FormatError`](crate::FormatError). Scale-offset is mutually exclusive
1491    /// with ZFP and replaces shuffle, but may be combined with
1492    /// [`with_deflate`](Self::with_deflate). Files are readable by the
1493    /// reference HDF5 library (filter id 6) and vice versa.
1494    pub fn with_scale_offset(&mut self, mode: ScaleOffset) -> &mut Self {
1495        self.chunk_options.scale_offset = Some(mode);
1496        self
1497    }
1498
1499    /// Enable ZFP fixed-rate compression (implies chunked if not already set).
1500    ///
1501    /// `rate` is the number of compressed bits per value. Supports f32, f64,
1502    /// i32, and i64 datasets in 1D–4D. When ZFP is active it replaces shuffle
1503    /// and deflate on the same dataset.
1504    ///
1505    /// The scalar type is derived from the dataset's datatype when the file
1506    /// is written, so any of `with_{f32,f64,i32,i64}_data` or an explicit
1507    /// `with_dtype` establishes it. `finish()` / `write()` returns
1508    /// [`FormatError::UnsupportedZfp`](crate::FormatError::UnsupportedZfp) if
1509    /// the dataset's datatype isn't one of the four supported scalar types,
1510    /// or if the chunk rank is outside 1..=4.
1511    ///
1512    /// The resulting file is byte-compatible with the reference H5Z-ZFP
1513    /// plugin (HDF5 filter ID 32013): other tools like h5py + hdf5plugin
1514    /// will read and decompress it, and vice versa.
1515    #[cfg(feature = "zfp")]
1516    pub fn with_zfp(&mut self, rate: f64) -> &mut Self {
1517        self.chunk_options.zfp_rate = Some(rate);
1518        self
1519    }
1520
1521    /// Attach SHINES provenance metadata (SHA-256, creator, timestamp).
1522    ///
1523    /// The SHA-256 hash of the raw dataset bytes is computed automatically
1524    /// during file serialization and stored as `_provenance_sha256`.
1525    #[cfg(feature = "provenance")]
1526    pub fn with_provenance(
1527        &mut self,
1528        creator: &str,
1529        timestamp: &str,
1530        source: Option<&str>,
1531    ) -> &mut Self {
1532        self.provenance = Some(ProvenanceConfig {
1533            creator: creator.to_string(),
1534            timestamp: timestamp.to_string(),
1535            source: source.map(|s| s.to_string()),
1536        });
1537        self
1538    }
1539}
1540
1541// ---- Group builder ----
1542
1543/// Builder for HDF5 groups.
1544///
1545/// Datasets, sub-groups, and attributes can be added in any order before
1546/// calling [`finish()`](GroupBuilder::finish). This is useful when the full
1547/// set of attributes is not known up front — for example, building a
1548/// MATLAB struct where `MATLAB_fields` lists every child dataset name:
1549///
1550/// ```rust
1551/// # use hdf5_pure::{FileBuilder, AttrValue};
1552/// let mut builder = FileBuilder::new();
1553/// let mut grp = builder.create_group("my_struct");
1554///
1555/// let mut fields = Vec::new();
1556/// for name in &["x", "y", "z"] {
1557///     fields.push(name.to_string());
1558///     grp.create_dataset(name).with_f64_data(&[0.0]);
1559/// }
1560///
1561/// // Attribute set after all children are created
1562/// grp.set_attr("MATLAB_fields", AttrValue::VarLenAsciiArray(fields));
1563/// builder.add_group(grp.finish());
1564/// ```
1565pub struct GroupBuilder {
1566    pub(crate) name: String,
1567    pub(crate) datasets: Vec<DatasetBuilder>,
1568    pub(crate) sub_groups: Vec<FinishedGroup>,
1569    pub(crate) attrs: Vec<(String, AttrValue)>,
1570}
1571
1572impl GroupBuilder {
1573    pub(crate) fn new(name: &str) -> Self {
1574        Self {
1575            name: name.to_string(),
1576            datasets: Vec::new(),
1577            sub_groups: Vec::new(),
1578            attrs: Vec::new(),
1579        }
1580    }
1581
1582    pub fn create_dataset(&mut self, name: &str) -> &mut DatasetBuilder {
1583        self.datasets.push(DatasetBuilder::new(name));
1584        self.datasets.last_mut().unwrap()
1585    }
1586
1587    /// Create a nested group builder. Call `.finish()` on it and then
1588    /// `add_group()` to add it to this group.
1589    pub fn create_group(&mut self, name: &str) -> GroupBuilder {
1590        GroupBuilder::new(name)
1591    }
1592
1593    /// Add a finished sub-group to this group.
1594    pub fn add_group(&mut self, group: FinishedGroup) {
1595        self.sub_groups.push(group);
1596    }
1597
1598    pub fn set_attr(&mut self, name: &str, value: AttrValue) {
1599        self.attrs.push((name.to_string(), value));
1600    }
1601
1602    /// Consume the builder, returning a FinishedGroup to add to FileWriter.
1603    pub fn finish(self) -> FinishedGroup {
1604        FinishedGroup {
1605            name: self.name,
1606            datasets: self.datasets,
1607            sub_groups: self.sub_groups,
1608            attrs: self.attrs,
1609        }
1610    }
1611}
1612
1613/// A finished group ready for the file writer.
1614pub struct FinishedGroup {
1615    pub(crate) name: String,
1616    pub(crate) datasets: Vec<DatasetBuilder>,
1617    pub(crate) sub_groups: Vec<FinishedGroup>,
1618    pub(crate) attrs: Vec<(String, AttrValue)>,
1619}