Skip to main content

rust_hdf5/format/messages/
data_layout.rs

1//! Data layout message (type 0x08) — describes how raw data is stored.
2//!
3//! Binary layout (version 3):
4//!   Byte 0: version = 3
5//!   Byte 1: layout class (0=compact, 1=contiguous, 2=chunked)
6//!
7//!   Contiguous (class 1):
8//!     address: sizeof_addr bytes
9//!     size:    sizeof_size bytes
10//!
11//!   Compact (class 0):
12//!     compact_size: u16 LE
13//!     data:         compact_size bytes
14//!
15//! Binary layout (version 3, chunked):
16//!   Byte 0: version = 3
17//!   Byte 1: layout class = 2 (chunked)
18//!   dimensionality D(1), b_tree_address(sizeof_addr),
19//!   D 4-byte LE dimension sizes (chunk dims; last is the element size).
20//!   The chunk index is always a version-1 B-tree.
21//!
22//! Binary layout (versions 4 and 5, chunked only):
23//!   Byte 0: version = 4 or 5
24//!   Byte 1: layout class = 2 (chunked)
25//!   flags(1) + ndims(1) + enc_bytes_per_dim(1)
26//!   + dim_sizes(ndims * enc_bytes_per_dim, each LE)
27//!   + index_type(1)
28//!   + [for earray: 5 param bytes]
29//!   + index_address(sizeof_addr)
30//!
31//! Version 5 (libhdf5 2.0) differs from version 4 only in the version byte;
32//! see [`VERSION_5`] for its effect on filtered chunk indexes.
33
34use crate::format::bytes::{read_le_addr as read_addr, read_le_uint as read_size};
35use crate::format::{FormatContext, FormatError, FormatResult, UNDEF_ADDR};
36
37const VERSION_3: u8 = 3;
38const VERSION_4: u8 = 4;
39/// Layout message version 5: structurally identical to version 4; it only
40/// changes how filtered-chunk sizes are encoded inside the chunk-index data
41/// structures (a fixed `sizeof_size` field). The reader derives that width
42/// from the chunk-index header, so v5 is decoded exactly like v4.
43const VERSION_5: u8 = 5;
44const CLASS_COMPACT: u8 = 0;
45const CLASS_CONTIGUOUS: u8 = 1;
46const CLASS_CHUNKED: u8 = 2;
47
48/// Chunk index type for version-4 chunked layout.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50#[repr(u8)]
51pub enum ChunkIndexType {
52    SingleChunk = 1,
53    Implicit = 2,
54    FixedArray = 3,
55    ExtensibleArray = 4,
56    BTreeV2 = 5,
57}
58
59impl ChunkIndexType {
60    pub fn from_u8(v: u8) -> Option<Self> {
61        match v {
62            1 => Some(Self::SingleChunk),
63            2 => Some(Self::Implicit),
64            3 => Some(Self::FixedArray),
65            4 => Some(Self::ExtensibleArray),
66            5 => Some(Self::BTreeV2),
67            _ => None,
68        }
69    }
70}
71
72/// Parameters for the extensible array chunk index.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct EarrayParams {
75    pub max_nelmts_bits: u8,
76    pub idx_blk_elmts: u8,
77    pub sup_blk_min_data_ptrs: u8,
78    pub data_blk_min_elmts: u8,
79    pub max_dblk_page_nelmts_bits: u8,
80}
81
82impl EarrayParams {
83    /// Default extensible array parameters (from H5Dpkg.h).
84    pub fn default_params() -> Self {
85        Self {
86            max_nelmts_bits: 32,
87            idx_blk_elmts: 4,
88            sup_blk_min_data_ptrs: 4,
89            data_blk_min_elmts: 16,
90            max_dblk_page_nelmts_bits: 10,
91        }
92    }
93}
94
95/// Parameters for the fixed array chunk index (max_dblk_page_nelmts_bits).
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct FixedArrayParams {
98    pub max_dblk_page_nelmts_bits: u8,
99}
100
101impl FixedArrayParams {
102    pub fn default_params() -> Self {
103        Self {
104            // libhdf5 rejects 0 here; its default is 10 (1024 elements per
105            // data-block page). Must match the value the fixed-array
106            // header carries.
107            max_dblk_page_nelmts_bits: 10,
108        }
109    }
110}
111
112/// Parameters for the v2 B-tree chunk index (node size, split/merge
113/// percentages — libhdf5's creation `cparam`). The v2 B-tree header carries
114/// authoritative copies; libhdf5 reads these only at creation, but a
115/// rewritten object header must not contradict the header of the tree it
116/// points at.
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct Bt2Params {
119    pub node_size: u32,
120    pub split_percent: u8,
121    pub merge_percent: u8,
122}
123
124impl Bt2Params {
125    /// This writer's creation defaults, matching libhdf5's
126    /// `H5D_BT2_NODE_SIZE` / `H5D_BT2_SPLIT_PERC` / `H5D_BT2_MERGE_PERC`
127    /// (`H5Dpkg.h`).
128    pub fn default_params() -> Self {
129        use crate::format::chunk_index::btree_v2::{
130            BT2_MERGE_PERCENT, BT2_NODE_SIZE, BT2_SPLIT_PERCENT,
131        };
132        Self {
133            node_size: BT2_NODE_SIZE,
134            split_percent: BT2_SPLIT_PERCENT,
135            merge_percent: BT2_MERGE_PERCENT,
136        }
137    }
138}
139
140/// Filtered single-chunk index parameters.
141///
142/// When a version-4 chunked layout uses the Single Chunk index AND the
143/// layout's "single index with filter" flag (`flags & 0x02`) is set,
144/// libhdf5 stores the chunk's on-disk (post-filter) size and its per-chunk
145/// filter mask inline in the layout message rather than in a separate index
146/// structure (H5Olayout.c). The mask must be honored on read: a set bit
147/// means the corresponding filter was *not* applied to this chunk.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub struct SingleChunkFilter {
150    /// On-disk (filtered) size of the single chunk, in bytes.
151    pub nbytes: u64,
152    /// Per-chunk filter mask: bit `i` set ⟹ filter `i` (forward pipeline
153    /// order) was skipped for this chunk and must not be reversed on read.
154    pub filter_mask: u32,
155}
156
157/// Data layout message payload.
158#[derive(Debug, Clone, PartialEq)]
159pub enum DataLayoutMessage {
160    /// Contiguous storage — raw data in a single block.
161    Contiguous {
162        /// Address of raw data.  `UNDEF_ADDR` if not yet allocated.
163        address: u64,
164        /// Size of raw data in bytes.
165        size: u64,
166    },
167    /// Compact storage — raw data stored within the object header.
168    Compact {
169        /// The raw data bytes.
170        data: Vec<u8>,
171    },
172    /// Version 3 chunked storage, indexed by a version-1 B-tree.
173    ///
174    /// This is what libhdf5 / h5py writes for a chunked dataset created
175    /// with the default `libver` bounds.
176    ChunkedV3 {
177        /// Chunk dimension sizes, including the trailing element-size
178        /// dimension (so the chunk rank is `chunk_dims.len() - 1`).
179        chunk_dims: Vec<u64>,
180        /// Address of the version-1 B-tree that indexes the chunks.
181        b_tree_address: u64,
182    },
183    /// Version 4 chunked storage. Version 5 shares this exact wire format —
184    /// only the version byte differs — so both decode into this variant.
185    ChunkedV4 {
186        /// Message version byte: 4 or 5. Version 5 (libhdf5 2.0,
187        /// `H5O_LAYOUT_VERSION_5`) declares that the chunk index encodes
188        /// filtered-chunk sizes in a fixed `sizeof_size`-byte field instead
189        /// of the width derived from the chunk byte count, so a filter may
190        /// expand a chunk without overflowing the field. Readers older than
191        /// libhdf5 2.0 reject version 5.
192        version: u8,
193        flags: u8,
194        /// Chunk dimension sizes.
195        chunk_dims: Vec<u64>,
196        /// Type of chunk index structure.
197        index_type: ChunkIndexType,
198        /// Extensible array parameters (present when index_type == ExtensibleArray).
199        earray_params: Option<EarrayParams>,
200        /// Fixed array parameters (present when index_type == FixedArray).
201        farray_params: Option<FixedArrayParams>,
202        /// v2 B-tree parameters (present when index_type == BTreeV2).
203        bt2_params: Option<Bt2Params>,
204        /// Filtered single-chunk parameters (present when index_type ==
205        /// SingleChunk and the layout's filtered flag `0x02` is set).
206        single_chunk_filter: Option<SingleChunkFilter>,
207        /// Address of the chunk index structure.
208        index_address: u64,
209    },
210}
211
212impl DataLayoutMessage {
213    /// Contiguous layout with no data allocated yet.
214    pub fn contiguous_unallocated(size: u64) -> Self {
215        Self::Contiguous {
216            address: UNDEF_ADDR,
217            size,
218        }
219    }
220
221    /// Contiguous layout pointing to allocated data.
222    pub fn contiguous(address: u64, size: u64) -> Self {
223        Self::Contiguous { address, size }
224    }
225
226    /// Compact layout with inline data.
227    pub fn compact(data: Vec<u8>) -> Self {
228        Self::Compact { data }
229    }
230
231    /// Version 3 chunked layout indexed by a version-1 B-tree.
232    ///
233    /// `chunk_dims` must include the trailing element-size dimension.
234    pub fn chunked_v3_btree_v1(chunk_dims: Vec<u64>, b_tree_address: u64) -> Self {
235        Self::ChunkedV3 {
236            chunk_dims,
237            b_tree_address,
238        }
239    }
240
241    /// Version 4 chunked layout with extensible array index.
242    ///
243    /// `chunk_dims` should include the trailing element-size dimension.
244    /// For example, for a 2D dataset with chunk=(1,4) and element_size=8,
245    /// pass chunk_dims = [1, 4, 8].
246    pub fn chunked_v4_earray(
247        version: u8,
248        chunk_dims: Vec<u64>,
249        earray_params: EarrayParams,
250        index_address: u64,
251    ) -> Self {
252        Self::ChunkedV4 {
253            version,
254            flags: 0,
255            chunk_dims,
256            index_type: ChunkIndexType::ExtensibleArray,
257            earray_params: Some(earray_params),
258            farray_params: None,
259            bt2_params: None,
260            single_chunk_filter: None,
261            index_address,
262        }
263    }
264
265    /// Version 4 chunked layout with fixed array index.
266    ///
267    /// `chunk_dims` should include the trailing element-size dimension.
268    pub fn chunked_v4_farray(
269        version: u8,
270        chunk_dims: Vec<u64>,
271        farray_params: FixedArrayParams,
272        index_address: u64,
273    ) -> Self {
274        Self::ChunkedV4 {
275            version,
276            flags: 0,
277            chunk_dims,
278            index_type: ChunkIndexType::FixedArray,
279            earray_params: None,
280            farray_params: Some(farray_params),
281            bt2_params: None,
282            single_chunk_filter: None,
283            index_address,
284        }
285    }
286
287    /// Version 4 chunked layout with B-tree v2 index.
288    ///
289    /// `chunk_dims` should include the trailing element-size dimension.
290    pub fn chunked_v4_btree_v2(
291        version: u8,
292        chunk_dims: Vec<u64>,
293        bt2_params: Bt2Params,
294        index_address: u64,
295    ) -> Self {
296        Self::ChunkedV4 {
297            version,
298            flags: 0,
299            chunk_dims,
300            index_type: ChunkIndexType::BTreeV2,
301            earray_params: None,
302            farray_params: None,
303            bt2_params: Some(bt2_params),
304            single_chunk_filter: None,
305            index_address,
306        }
307    }
308
309    /// Version 4 chunked layout with single-chunk index.
310    ///
311    /// `chunk_dims` should include the trailing element-size dimension.
312    pub fn chunked_v4_single(chunk_dims: Vec<u64>, index_address: u64) -> Self {
313        Self::ChunkedV4 {
314            version: VERSION_4,
315            flags: 0,
316            chunk_dims,
317            index_type: ChunkIndexType::SingleChunk,
318            earray_params: None,
319            farray_params: None,
320            bt2_params: None,
321            single_chunk_filter: None,
322            index_address,
323        }
324    }
325
326    // ------------------------------------------------------------------ encode
327
328    pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
329        match self {
330            Self::Contiguous { address, size } => {
331                let sa = ctx.sizeof_addr as usize;
332                let ss = ctx.sizeof_size as usize;
333                let mut buf = Vec::with_capacity(2 + sa + ss);
334                buf.push(VERSION_3);
335                buf.push(CLASS_CONTIGUOUS);
336                buf.extend_from_slice(&address.to_le_bytes()[..sa]);
337                buf.extend_from_slice(&size.to_le_bytes()[..ss]);
338                buf
339            }
340            Self::Compact { data } => {
341                let mut buf = Vec::with_capacity(2 + 2 + data.len());
342                buf.push(VERSION_3);
343                buf.push(CLASS_COMPACT);
344                buf.extend_from_slice(&(data.len() as u16).to_le_bytes());
345                buf.extend_from_slice(data);
346                buf
347            }
348            Self::ChunkedV3 {
349                chunk_dims,
350                b_tree_address,
351            } => {
352                let sa = ctx.sizeof_addr as usize;
353                let ndims = chunk_dims.len() as u8;
354                let mut buf = Vec::with_capacity(3 + sa + chunk_dims.len() * 4);
355                buf.push(VERSION_3);
356                buf.push(CLASS_CHUNKED);
357                buf.push(ndims);
358                buf.extend_from_slice(&b_tree_address.to_le_bytes()[..sa]);
359                // Dimension sizes are always 4 bytes each (UINT32ENCODE).
360                for &d in chunk_dims {
361                    buf.extend_from_slice(&(d as u32).to_le_bytes());
362                }
363                buf
364            }
365            Self::ChunkedV4 {
366                version,
367                flags,
368                chunk_dims,
369                index_type,
370                earray_params,
371                farray_params,
372                bt2_params,
373                single_chunk_filter,
374                index_address,
375            } => {
376                let sa = ctx.sizeof_addr as usize;
377                let ndims = chunk_dims.len() as u8;
378
379                // Compute enc_bytes_per_dim: minimum bytes to represent the
380                // max chunk dimension value.
381                let max_dim = chunk_dims.iter().copied().max().unwrap_or(1);
382                let enc_bytes = enc_bytes_for_value(max_dim);
383
384                debug_assert!(matches!(*version, VERSION_4 | VERSION_5));
385                let mut buf = Vec::with_capacity(64);
386                buf.push(*version);
387                buf.push(CLASS_CHUNKED);
388                buf.push(*flags);
389                buf.push(ndims);
390                buf.push(enc_bytes);
391
392                // Dimension sizes
393                for &d in chunk_dims {
394                    buf.extend_from_slice(&d.to_le_bytes()[..enc_bytes as usize]);
395                }
396
397                // Index type
398                buf.push(*index_type as u8);
399
400                // Index-type-specific parameters
401                match *index_type {
402                    ChunkIndexType::ExtensibleArray => {
403                        if let Some(ref params) = earray_params {
404                            buf.push(params.max_nelmts_bits);
405                            buf.push(params.idx_blk_elmts);
406                            buf.push(params.sup_blk_min_data_ptrs);
407                            buf.push(params.data_blk_min_elmts);
408                            buf.push(params.max_dblk_page_nelmts_bits);
409                        }
410                    }
411                    ChunkIndexType::FixedArray => {
412                        if let Some(ref params) = farray_params {
413                            buf.push(params.max_dblk_page_nelmts_bits);
414                        }
415                    }
416                    ChunkIndexType::BTreeV2 => {
417                        // node_size(4) + split_percent(1) + merge_percent(1),
418                        // the same geometry the B-tree header carries — the
419                        // message must agree with the BTHD it points at, so
420                        // a reopened foreign node size is preserved, not
421                        // stamped over with this writer's default.
422                        if let Some(ref params) = bt2_params {
423                            buf.extend_from_slice(&params.node_size.to_le_bytes());
424                            buf.push(params.split_percent);
425                            buf.push(params.merge_percent);
426                        }
427                    }
428                    // A filtered single chunk carries its on-disk size
429                    // (sizeof_size bytes) and 4-byte filter mask inline, before
430                    // the chunk address (H5Olayout.c). Only emit them when the
431                    // filtered flag (0x02) is set; an unfiltered single chunk
432                    // falls through to the no-extra-parameters arm below.
433                    ChunkIndexType::SingleChunk if *flags & 0x02 != 0 => {
434                        if let Some(scf) = single_chunk_filter {
435                            let ss = ctx.sizeof_size as usize;
436                            buf.extend_from_slice(&scf.nbytes.to_le_bytes()[..ss]);
437                            buf.extend_from_slice(&scf.filter_mask.to_le_bytes());
438                        }
439                    }
440                    // Implicit: no extra parameters.
441                    _ => {}
442                }
443
444                // Index address
445                buf.extend_from_slice(&index_address.to_le_bytes()[..sa]);
446
447                buf
448            }
449        }
450    }
451
452    // ------------------------------------------------------------------ decode
453
454    pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
455        if buf.len() < 2 {
456            return Err(FormatError::BufferTooShort {
457                needed: 2,
458                available: buf.len(),
459            });
460        }
461
462        let version = buf[0];
463        let class = buf[1];
464
465        match (version, class) {
466            (VERSION_3, CLASS_CONTIGUOUS) => {
467                let sa = ctx.sizeof_addr as usize;
468                let ss = ctx.sizeof_size as usize;
469                let mut pos = 2;
470                let needed = pos + sa + ss;
471                if buf.len() < needed {
472                    return Err(FormatError::BufferTooShort {
473                        needed,
474                        available: buf.len(),
475                    });
476                }
477                let address = read_addr(&buf[pos..], sa);
478                pos += sa;
479                let size = read_size(&buf[pos..], ss);
480                pos += ss;
481                Ok((Self::Contiguous { address, size }, pos))
482            }
483            (VERSION_3, CLASS_COMPACT) => {
484                let mut pos = 2;
485                if buf.len() < pos + 2 {
486                    return Err(FormatError::BufferTooShort {
487                        needed: pos + 2,
488                        available: buf.len(),
489                    });
490                }
491                let compact_size = u16::from_le_bytes([buf[pos], buf[pos + 1]]) as usize;
492                pos += 2;
493                if buf.len() < pos + compact_size {
494                    return Err(FormatError::BufferTooShort {
495                        needed: pos + compact_size,
496                        available: buf.len(),
497                    });
498                }
499                let data = buf[pos..pos + compact_size].to_vec();
500                pos += compact_size;
501                Ok((Self::Compact { data }, pos))
502            }
503            (VERSION_3, CLASS_CHUNKED) => {
504                // version(1) + class(1) + ndims(1) + b_tree_addr(sa)
505                // + ndims * 4-byte dimension sizes.
506                let sa = ctx.sizeof_addr as usize;
507                let mut pos = 2;
508                if buf.len() < pos + 1 {
509                    return Err(FormatError::BufferTooShort {
510                        needed: pos + 1,
511                        available: buf.len(),
512                    });
513                }
514                let ndims = buf[pos] as usize;
515                pos += 1;
516
517                // libhdf5 (H5Olayout.c) requires 2 <= ndims for chunked
518                // storage: the chunk rank plus the trailing element-size
519                // dimension. A zero or one is malformed.
520                if ndims < 2 {
521                    return Err(FormatError::InvalidData(format!(
522                        "chunked v3 layout dimensionality {ndims} is too small"
523                    )));
524                }
525
526                if buf.len() < pos + sa {
527                    return Err(FormatError::BufferTooShort {
528                        needed: pos + sa,
529                        available: buf.len(),
530                    });
531                }
532                let b_tree_address = read_addr(&buf[pos..], sa);
533                pos += sa;
534
535                let dim_data_len = ndims * 4;
536                if buf.len() < pos + dim_data_len {
537                    return Err(FormatError::BufferTooShort {
538                        needed: pos + dim_data_len,
539                        available: buf.len(),
540                    });
541                }
542                let mut chunk_dims = Vec::with_capacity(ndims);
543                for _ in 0..ndims {
544                    let d = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]])
545                        as u64;
546                    if d == 0 {
547                        return Err(FormatError::InvalidData(
548                            "chunked v3 layout has a zero chunk dimension".into(),
549                        ));
550                    }
551                    chunk_dims.push(d);
552                    pos += 4;
553                }
554
555                Ok((
556                    Self::ChunkedV3 {
557                        chunk_dims,
558                        b_tree_address,
559                    },
560                    pos,
561                ))
562            }
563            (VERSION_4 | VERSION_5, CLASS_CHUNKED) => {
564                let sa = ctx.sizeof_addr as usize;
565                let mut pos = 2;
566
567                // flags(1) + ndims(1) + enc_bytes_per_dim(1)
568                if buf.len() < pos + 3 {
569                    return Err(FormatError::BufferTooShort {
570                        needed: pos + 3,
571                        available: buf.len(),
572                    });
573                }
574                let flags = buf[pos];
575                pos += 1;
576                let ndims = buf[pos] as usize;
577                pos += 1;
578                let enc_bytes = buf[pos] as usize;
579                pos += 1;
580
581                // libhdf5 (H5Olayout.c) requires 1 <= enc_bytes <= 8;
582                // 0 produces all-zero dims, > 8 panics read_size.
583                if !(1..=8).contains(&enc_bytes) {
584                    return Err(FormatError::InvalidData(format!(
585                        "chunked layout encoded dimension size {enc_bytes} is out of range"
586                    )));
587                }
588                // Chunked storage carries the chunk rank plus the trailing
589                // element-size dimension, so ndims is at least 2.
590                if ndims < 2 {
591                    return Err(FormatError::InvalidData(format!(
592                        "chunked v4 layout dimensionality {ndims} is too small"
593                    )));
594                }
595
596                // dim sizes
597                let dim_data_len = ndims * enc_bytes;
598                if buf.len() < pos + dim_data_len {
599                    return Err(FormatError::BufferTooShort {
600                        needed: pos + dim_data_len,
601                        available: buf.len(),
602                    });
603                }
604                let mut chunk_dims = Vec::with_capacity(ndims);
605                for _ in 0..ndims {
606                    let d = read_size(&buf[pos..], enc_bytes);
607                    if d == 0 {
608                        return Err(FormatError::InvalidData(
609                            "chunked v4 layout has a zero chunk dimension".into(),
610                        ));
611                    }
612                    chunk_dims.push(d);
613                    pos += enc_bytes;
614                }
615
616                // index type
617                if buf.len() < pos + 1 {
618                    return Err(FormatError::BufferTooShort {
619                        needed: pos + 1,
620                        available: buf.len(),
621                    });
622                }
623                let idx_type_raw = buf[pos];
624                pos += 1;
625                let index_type = ChunkIndexType::from_u8(idx_type_raw).ok_or_else(|| {
626                    FormatError::UnsupportedFeature(format!("chunk index type {}", idx_type_raw))
627                })?;
628
629                // Index-type-specific parameters
630                let mut earray_params = None;
631                let mut farray_params = None;
632                let mut bt2_params = None;
633                let mut single_chunk_filter = None;
634
635                match index_type {
636                    ChunkIndexType::ExtensibleArray => {
637                        if buf.len() < pos + 5 {
638                            return Err(FormatError::BufferTooShort {
639                                needed: pos + 5,
640                                available: buf.len(),
641                            });
642                        }
643                        let ep = EarrayParams {
644                            max_nelmts_bits: buf[pos],
645                            idx_blk_elmts: buf[pos + 1],
646                            sup_blk_min_data_ptrs: buf[pos + 2],
647                            data_blk_min_elmts: buf[pos + 3],
648                            max_dblk_page_nelmts_bits: buf[pos + 4],
649                        };
650                        // libhdf5 rejects a zero in any of these fields.
651                        if ep.max_nelmts_bits == 0
652                            || ep.idx_blk_elmts == 0
653                            || ep.sup_blk_min_data_ptrs == 0
654                            || ep.data_blk_min_elmts == 0
655                            || ep.max_dblk_page_nelmts_bits == 0
656                        {
657                            return Err(FormatError::InvalidData(
658                                "extensible-array layout parameter is zero".into(),
659                            ));
660                        }
661                        earray_params = Some(ep);
662                        pos += 5;
663                    }
664                    ChunkIndexType::FixedArray => {
665                        if buf.len() < pos + 1 {
666                            return Err(FormatError::BufferTooShort {
667                                needed: pos + 1,
668                                available: buf.len(),
669                            });
670                        }
671                        // NOTE: libhdf5 rejects max_dblk_page_nelmts_bits == 0,
672                        // but this crate's own Fixed Array writer currently
673                        // emits 0 (it does not page). Validating it here would
674                        // reject crate-written files; left until the FA writer
675                        // is made libhdf5-conformant.
676                        farray_params = Some(FixedArrayParams {
677                            max_dblk_page_nelmts_bits: buf[pos],
678                        });
679                        pos += 1;
680                    }
681                    ChunkIndexType::BTreeV2 => {
682                        // node_size(4) + split_percent(1) + merge_percent(1).
683                        // The v2 B-tree header carries authoritative copies;
684                        // retained so a rewritten object header re-emits the
685                        // creator's values, not this writer's defaults.
686                        if buf.len() < pos + 6 {
687                            return Err(FormatError::BufferTooShort {
688                                needed: pos + 6,
689                                available: buf.len(),
690                            });
691                        }
692                        bt2_params = Some(Bt2Params {
693                            node_size: u32::from_le_bytes([
694                                buf[pos],
695                                buf[pos + 1],
696                                buf[pos + 2],
697                                buf[pos + 3],
698                            ]),
699                            split_percent: buf[pos + 4],
700                            merge_percent: buf[pos + 5],
701                        });
702                        pos += 6;
703                    }
704                    // A single-chunk index whose "single index with
705                    // filter" flag (0x02) is set carries the filtered
706                    // chunk size (sizeof_size bytes) and a 4-byte filter
707                    // mask before the chunk address (H5Olayout.c). Retain
708                    // both: the reader needs the exact on-disk size and must
709                    // honor the per-chunk mask when reversing filters.
710                    ChunkIndexType::SingleChunk if flags & 0x02 != 0 => {
711                        let ss = ctx.sizeof_size as usize;
712                        let extra = ss + 4;
713                        if buf.len() < pos + extra {
714                            return Err(FormatError::BufferTooShort {
715                                needed: pos + extra,
716                                available: buf.len(),
717                            });
718                        }
719                        let nbytes = read_size(&buf[pos..], ss);
720                        pos += ss;
721                        let filter_mask = u32::from_le_bytes([
722                            buf[pos],
723                            buf[pos + 1],
724                            buf[pos + 2],
725                            buf[pos + 3],
726                        ]);
727                        pos += 4;
728                        single_chunk_filter = Some(SingleChunkFilter {
729                            nbytes,
730                            filter_mask,
731                        });
732                    }
733                    // Implicit, and single-chunk without the filter flag:
734                    // no extra parameters.
735                    _ => {}
736                }
737
738                // index address
739                if buf.len() < pos + sa {
740                    return Err(FormatError::BufferTooShort {
741                        needed: pos + sa,
742                        available: buf.len(),
743                    });
744                }
745                let index_address = read_addr(&buf[pos..], sa);
746                pos += sa;
747
748                Ok((
749                    Self::ChunkedV4 {
750                        version: buf[0],
751                        flags,
752                        chunk_dims,
753                        index_type,
754                        earray_params,
755                        farray_params,
756                        bt2_params,
757                        single_chunk_filter,
758                        index_address,
759                    },
760                    pos,
761                ))
762            }
763            (VERSION_3, other) => Err(FormatError::UnsupportedFeature(format!(
764                "data layout class {}",
765                other
766            ))),
767            (v, _) => Err(FormatError::InvalidVersion(v)),
768        }
769    }
770}
771
772// ========================================================================= helpers
773
774/// Compute the minimum number of bytes (1-8) needed to encode `v`.
775fn enc_bytes_for_value(v: u64) -> u8 {
776    if v == 0 {
777        return 1;
778    }
779    let bits_needed = 64 - v.leading_zeros(); // 1..=64
780    bits_needed.div_ceil(8) as u8
781}
782
783// ======================================================================= tests
784
785#[cfg(test)]
786mod tests {
787    use super::*;
788
789    fn ctx8() -> FormatContext {
790        FormatContext {
791            sizeof_addr: 8,
792            sizeof_size: 8,
793        }
794    }
795
796    fn ctx4() -> FormatContext {
797        FormatContext {
798            sizeof_addr: 4,
799            sizeof_size: 4,
800        }
801    }
802
803    #[test]
804    fn roundtrip_contiguous() {
805        let msg = DataLayoutMessage::contiguous(0x1000, 4096);
806        let encoded = msg.encode(&ctx8());
807        // 2 + 8 + 8 = 18
808        assert_eq!(encoded.len(), 18);
809        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
810        assert_eq!(consumed, 18);
811        assert_eq!(decoded, msg);
812    }
813
814    #[test]
815    fn roundtrip_contiguous_ctx4() {
816        let msg = DataLayoutMessage::contiguous(0x800, 256);
817        let encoded = msg.encode(&ctx4());
818        // 2 + 4 + 4 = 10
819        assert_eq!(encoded.len(), 10);
820        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
821        assert_eq!(consumed, 10);
822        assert_eq!(decoded, msg);
823    }
824
825    #[test]
826    fn roundtrip_contiguous_unallocated() {
827        let msg = DataLayoutMessage::contiguous_unallocated(1024);
828        let encoded = msg.encode(&ctx8());
829        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
830        assert_eq!(decoded, msg);
831        match decoded {
832            DataLayoutMessage::Contiguous { address, size } => {
833                assert_eq!(address, UNDEF_ADDR);
834                assert_eq!(size, 1024);
835            }
836            _ => panic!("expected Contiguous"),
837        }
838    }
839
840    #[test]
841    fn roundtrip_contiguous_undef_ctx4() {
842        let msg = DataLayoutMessage::contiguous_unallocated(512);
843        let encoded = msg.encode(&ctx4());
844        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
845        match decoded {
846            DataLayoutMessage::Contiguous { address, .. } => {
847                assert_eq!(address, UNDEF_ADDR);
848            }
849            _ => panic!("expected Contiguous"),
850        }
851    }
852
853    #[test]
854    fn roundtrip_compact() {
855        let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
856        let msg = DataLayoutMessage::compact(data.clone());
857        let encoded = msg.encode(&ctx8());
858        // 2 + 2 + 8 = 12
859        assert_eq!(encoded.len(), 12);
860        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
861        assert_eq!(consumed, 12);
862        assert_eq!(decoded, msg);
863    }
864
865    #[test]
866    fn roundtrip_compact_empty() {
867        let msg = DataLayoutMessage::compact(vec![]);
868        let encoded = msg.encode(&ctx8());
869        assert_eq!(encoded.len(), 4); // 2 + 2 + 0
870        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
871        assert_eq!(consumed, 4);
872        assert_eq!(decoded, msg);
873    }
874
875    #[test]
876    fn decode_bad_version() {
877        let buf = [2u8, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
878        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
879        match err {
880            FormatError::InvalidVersion(2) => {}
881            other => panic!("unexpected error: {:?}", other),
882        }
883    }
884
885    #[test]
886    fn decode_unsupported_class() {
887        let buf = [3u8, 3]; // class 3 = unknown
888        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
889        match err {
890            FormatError::UnsupportedFeature(_) => {}
891            other => panic!("unexpected error: {:?}", other),
892        }
893    }
894
895    #[test]
896    fn decode_buffer_too_short() {
897        let buf = [3u8];
898        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
899        match err {
900            FormatError::BufferTooShort { .. } => {}
901            other => panic!("unexpected error: {:?}", other),
902        }
903    }
904
905    #[test]
906    fn decode_contiguous_truncated() {
907        // version=3, class=1, but not enough bytes for address+size
908        let buf = [3u8, 1, 0, 0];
909        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
910        match err {
911            FormatError::BufferTooShort { .. } => {}
912            other => panic!("unexpected error: {:?}", other),
913        }
914    }
915
916    #[test]
917    fn version_and_class_bytes() {
918        let encoded = DataLayoutMessage::contiguous(0, 0).encode(&ctx8());
919        assert_eq!(encoded[0], 3);
920        assert_eq!(encoded[1], 1);
921
922        let encoded = DataLayoutMessage::compact(vec![]).encode(&ctx8());
923        assert_eq!(encoded[0], 3);
924        assert_eq!(encoded[1], 0);
925    }
926
927    #[test]
928    fn roundtrip_chunked_v4_earray() {
929        let params = EarrayParams::default_params();
930        let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 256, 256], params, 0x2000);
931        let encoded = msg.encode(&ctx8());
932        assert_eq!(encoded[0], 4); // version 4
933        assert_eq!(encoded[1], 2); // class chunked
934        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
935        assert_eq!(consumed, encoded.len());
936        assert_eq!(decoded, msg);
937    }
938
939    #[test]
940    fn roundtrip_chunked_v4_earray_ctx4() {
941        let params = EarrayParams::default_params();
942        let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 128], params, 0x1000);
943        let encoded = msg.encode(&ctx4());
944        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
945        assert_eq!(consumed, encoded.len());
946        assert_eq!(decoded, msg);
947    }
948
949    /// A version-5 layout differs from v4 only in the version byte; the body
950    /// encodes identically and the version must survive the round trip (a
951    /// reopen that dropped it would silently downgrade the file to v4 while
952    /// its filtered index keeps 8-byte size fields).
953    #[test]
954    fn roundtrip_chunked_v5_earray() {
955        let params = EarrayParams::default_params();
956        let v5 = DataLayoutMessage::chunked_v4_earray(5, vec![1, 256, 256], params.clone(), 0x2000);
957        let encoded = v5.encode(&ctx8());
958        assert_eq!(encoded[0], 5); // version 5
959        assert_eq!(encoded[1], 2); // class chunked
960        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
961        assert_eq!(consumed, encoded.len());
962        assert_eq!(decoded, v5);
963
964        // Same message at v4: only byte 0 differs.
965        let v4 = DataLayoutMessage::chunked_v4_earray(4, vec![1, 256, 256], params, 0x2000);
966        let encoded_v4 = v4.encode(&ctx8());
967        assert_eq!(encoded[1..], encoded_v4[1..]);
968    }
969
970    #[test]
971    fn roundtrip_chunked_v4_single() {
972        let msg = DataLayoutMessage::chunked_v4_single(vec![100, 200], 0x3000);
973        let encoded = msg.encode(&ctx8());
974        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
975        assert_eq!(consumed, encoded.len());
976        assert_eq!(decoded, msg);
977    }
978
979    /// The BTreeV2 parameters (node size, split/merge) round-trip through
980    /// the message instead of being skipped on decode and re-stamped with
981    /// defaults on encode — a rewritten object header must agree with the
982    /// BTHD it points at.
983    #[test]
984    fn roundtrip_chunked_v4_btree_v2_params() {
985        for ctx in [ctx8(), ctx4()] {
986            let msg = DataLayoutMessage::chunked_v4_btree_v2(
987                4,
988                vec![2, 2, 8],
989                Bt2Params {
990                    node_size: 512,
991                    split_percent: 90,
992                    merge_percent: 30,
993                },
994                0x2000,
995            );
996            let encoded = msg.encode(&ctx);
997            let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx).unwrap();
998            assert_eq!(consumed, encoded.len());
999            assert_eq!(decoded, msg);
1000        }
1001    }
1002
1003    /// A filtered single-chunk layout (flag `0x02`) carries the chunk's
1004    /// on-disk size and per-chunk filter mask inline. Decode must retain both
1005    /// (not discard them), and encode↔decode must round-trip — including the
1006    /// nonzero mask the reader needs to skip a filter.
1007    #[test]
1008    fn roundtrip_chunked_v4_single_filtered() {
1009        for ctx in [ctx8(), ctx4()] {
1010            let msg = DataLayoutMessage::ChunkedV4 {
1011                version: 4,
1012                flags: 0x02,
1013                chunk_dims: vec![100, 200, 4],
1014                index_type: ChunkIndexType::SingleChunk,
1015                earray_params: None,
1016                farray_params: None,
1017                bt2_params: None,
1018                single_chunk_filter: Some(SingleChunkFilter {
1019                    nbytes: 12345,
1020                    filter_mask: 0b101,
1021                }),
1022                index_address: 0x3000,
1023            };
1024            let encoded = msg.encode(&ctx);
1025            let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx).unwrap();
1026            assert_eq!(consumed, encoded.len());
1027            assert_eq!(decoded, msg);
1028            // The decoded layout exposes the retained size and mask.
1029            match decoded {
1030                DataLayoutMessage::ChunkedV4 {
1031                    single_chunk_filter: Some(scf),
1032                    ..
1033                } => {
1034                    assert_eq!(scf.nbytes, 12345);
1035                    assert_eq!(scf.filter_mask, 0b101);
1036                }
1037                other => panic!("expected filtered single-chunk layout, got {other:?}"),
1038            }
1039        }
1040    }
1041
1042    #[test]
1043    fn chunked_v4_enc_bytes() {
1044        // chunk dims [1, 256, 256]: max=256, needs 2 bytes
1045        let params = EarrayParams::default_params();
1046        let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 256, 256], params, 0x2000);
1047        let encoded = msg.encode(&ctx8());
1048        // version(1) + class(1) + flags(1) + ndims(1) + enc_bytes(1)
1049        // + 3*2 dim bytes + index_type(1) + 5 earray params + 8 addr = 25
1050        assert_eq!(encoded.len(), 25);
1051        assert_eq!(encoded[4], 2); // enc_bytes_per_dim = 2
1052    }
1053
1054    #[test]
1055    fn roundtrip_chunked_v3_btree_v1() {
1056        // 1-D dataset, chunk=(8), element_size=4 -> chunk_dims=[8, 4].
1057        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![8, 4], 0x1234);
1058        let encoded = msg.encode(&ctx8());
1059        // version(1) + class(1) + ndims(1) + addr(8) + 2*4 dims = 19
1060        assert_eq!(encoded.len(), 19);
1061        assert_eq!(encoded[0], 3);
1062        assert_eq!(encoded[1], 2);
1063        assert_eq!(encoded[2], 2); // ndims
1064        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1065        assert_eq!(consumed, encoded.len());
1066        assert_eq!(decoded, msg);
1067    }
1068
1069    #[test]
1070    fn roundtrip_chunked_v3_btree_v1_2d_ctx4() {
1071        // 2-D dataset, chunk=(2,3), element_size=8 -> chunk_dims=[2, 3, 8].
1072        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![2, 3, 8], 0x800);
1073        let encoded = msg.encode(&ctx4());
1074        // version(1) + class(1) + ndims(1) + addr(4) + 3*4 dims = 19
1075        assert_eq!(encoded.len(), 19);
1076        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
1077        assert_eq!(consumed, encoded.len());
1078        assert_eq!(decoded, msg);
1079    }
1080
1081    #[test]
1082    fn chunked_v3_undef_btree_addr() {
1083        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![16, 4], UNDEF_ADDR);
1084        let encoded = msg.encode(&ctx8());
1085        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1086        match decoded {
1087            DataLayoutMessage::ChunkedV3 { b_tree_address, .. } => {
1088                assert_eq!(b_tree_address, UNDEF_ADDR);
1089            }
1090            _ => panic!("expected ChunkedV3"),
1091        }
1092    }
1093
1094    #[test]
1095    fn chunked_v3_rejects_ndims_too_small() {
1096        // ndims = 1 is malformed for chunked storage.
1097        let buf = [3u8, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
1098        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1099        assert!(matches!(err, FormatError::InvalidData(_)));
1100    }
1101
1102    #[test]
1103    fn chunked_v3_rejects_zero_dim() {
1104        // ndims=2, addr=0, dims=[0, 4] -> zero chunk dimension.
1105        let mut buf = vec![3u8, 2, 2];
1106        buf.extend_from_slice(&0u64.to_le_bytes()); // addr
1107        buf.extend_from_slice(&0u32.to_le_bytes()); // dim 0 == 0
1108        buf.extend_from_slice(&4u32.to_le_bytes()); // dim 1
1109        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1110        assert!(matches!(err, FormatError::InvalidData(_)));
1111    }
1112
1113    #[test]
1114    fn chunked_v3_truncated() {
1115        // version=3, class=2, ndims=2, but no room for addr/dims.
1116        let buf = [3u8, 2, 2];
1117        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1118        assert!(matches!(err, FormatError::BufferTooShort { .. }));
1119    }
1120
1121    #[test]
1122    fn chunked_v4_large_dims() {
1123        // Large dims requiring 4 bytes each
1124        let params = EarrayParams::default_params();
1125        let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 65536], params, 0x4000);
1126        let encoded = msg.encode(&ctx8());
1127        assert_eq!(encoded[4], 3); // enc_bytes_per_dim = 3 (65536 = 0x10000, needs 3 bytes)
1128    }
1129}