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 (versions 3, 4 and 5):
4//!   Byte 0: version = 3, 4 or 5
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//! The compact and contiguous bodies are identical in all three versions —
16//! `H5O__layout_decode` reads them without consulting the version — so a
17//! contiguous dataset written under `libver` v1.10 bounds (version 4) decodes
18//! exactly like the version-3 one written under the default bounds.
19//!
20//! Binary layout (version 3, chunked):
21//!   Byte 0: version = 3
22//!   Byte 1: layout class = 2 (chunked)
23//!   dimensionality D(1), b_tree_address(sizeof_addr),
24//!   D 4-byte LE dimension sizes (chunk dims; last is the element size).
25//!   The chunk index is always a version-1 B-tree.
26//!
27//! Binary layout (versions 4 and 5, chunked only):
28//!   Byte 0: version = 4 or 5
29//!   Byte 1: layout class = 2 (chunked)
30//!   flags(1) + ndims(1) + enc_bytes_per_dim(1)
31//!   + dim_sizes(ndims * enc_bytes_per_dim, each LE)
32//!   + index_type(1)
33//!   + [for earray: 5 param bytes]
34//!   + index_address(sizeof_addr)
35//!
36//! Version 5 (libhdf5 2.0) differs from version 4 only in the version byte;
37//! see `VERSION_5` for its effect on filtered chunk indexes.
38//!
39//! Binary layout (versions 4 and 5, virtual only):
40//!   Byte 0: version = 4 or 5
41//!   Byte 1: layout class = 3 (virtual)
42//!   heap_address(sizeof_addr) + heap_index(4, u32 LE)
43//!
44//! The virtual mapping list itself (source/virtual file names and
45//! selections) is not inline: `heap_address`/`heap_index` name a global
46//! heap object holding it (H5D__virtual_load_layout, H5Dvirtual.c) —
47//! decoded separately by [`crate::format::messages::virtual_mapping`].
48
49use crate::format::bytes::{read_le_addr as read_addr, read_le_uint as read_size};
50use crate::format::{FormatContext, FormatError, FormatResult, UNDEF_ADDR};
51
52/// Oldest layout message version libhdf5 accepts (`H5O_LAYOUT_VERSION_1`).
53/// Versions 1 and 2 put the dimensionality ahead of the storage class and
54/// omit the contiguous data size, which the dataset code has to derive from
55/// the dataspace; this decoder does not model that shape.
56const VERSION_1: u8 = 1;
57const VERSION_2: u8 = 2;
58const VERSION_3: u8 = 3;
59const VERSION_4: u8 = 4;
60/// Layout message version 5: structurally identical to version 4; it only
61/// changes how filtered-chunk sizes are encoded inside the chunk-index data
62/// structures (a fixed `sizeof_size` field). The reader derives that width
63/// from the chunk-index header, so v5 is decoded exactly like v4.
64const VERSION_5: u8 = 5;
65
66/// `H5O_LAYOUT_VERSION_DEFAULT` (H5Oprivate.h:451), the version a dataset
67/// creation property list starts its layout message at (`H5D_DEF_LAYOUT_*`,
68/// H5Pdcpl.c:124). Every version-selection rule takes the maximum of this and
69/// what the bound or the chunk asks for, so no layout message this writer
70/// emits falls below it — not even in a file whose bound's row is version 1.
71pub const LAYOUT_VERSION_DEFAULT: u8 = VERSION_3;
72
73const CLASS_COMPACT: u8 = 0;
74const CLASS_CONTIGUOUS: u8 = 1;
75const CLASS_CHUNKED: u8 = 2;
76const CLASS_VIRTUAL: u8 = 3;
77
78/// Chunk index type for version-4 chunked layout.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80#[repr(u8)]
81pub enum ChunkIndexType {
82    SingleChunk = 1,
83    Implicit = 2,
84    FixedArray = 3,
85    ExtensibleArray = 4,
86    BTreeV2 = 5,
87}
88
89impl ChunkIndexType {
90    pub fn from_u8(v: u8) -> Option<Self> {
91        match v {
92            1 => Some(Self::SingleChunk),
93            2 => Some(Self::Implicit),
94            3 => Some(Self::FixedArray),
95            4 => Some(Self::ExtensibleArray),
96            5 => Some(Self::BTreeV2),
97            _ => None,
98        }
99    }
100}
101
102/// Parameters for the extensible array chunk index.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct EarrayParams {
105    pub max_nelmts_bits: u8,
106    pub idx_blk_elmts: u8,
107    pub sup_blk_min_data_ptrs: u8,
108    pub data_blk_min_elmts: u8,
109    pub max_dblk_page_nelmts_bits: u8,
110}
111
112impl EarrayParams {
113    /// Default extensible array parameters (from H5Dpkg.h).
114    pub fn default_params() -> Self {
115        Self {
116            max_nelmts_bits: 32,
117            idx_blk_elmts: 4,
118            sup_blk_min_data_ptrs: 4,
119            data_blk_min_elmts: 16,
120            max_dblk_page_nelmts_bits: 10,
121        }
122    }
123}
124
125/// Parameters for the fixed array chunk index (max_dblk_page_nelmts_bits).
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct FixedArrayParams {
128    pub max_dblk_page_nelmts_bits: u8,
129}
130
131impl FixedArrayParams {
132    pub fn default_params() -> Self {
133        Self {
134            // libhdf5 rejects 0 here; its default is 10 (1024 elements per
135            // data-block page). Must match the value the fixed-array
136            // header carries.
137            max_dblk_page_nelmts_bits: 10,
138        }
139    }
140}
141
142/// Parameters for the v2 B-tree chunk index (node size, split/merge
143/// percentages — libhdf5's creation `cparam`). The v2 B-tree header carries
144/// authoritative copies; libhdf5 reads these only at creation, but a
145/// rewritten object header must not contradict the header of the tree it
146/// points at.
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct Bt2Params {
149    pub node_size: u32,
150    pub split_percent: u8,
151    pub merge_percent: u8,
152}
153
154impl Bt2Params {
155    /// This writer's creation defaults, matching libhdf5's
156    /// `H5D_BT2_NODE_SIZE` / `H5D_BT2_SPLIT_PERC` / `H5D_BT2_MERGE_PERC`
157    /// (`H5Dpkg.h`).
158    pub fn default_params() -> Self {
159        use crate::format::chunk_index::btree_v2::{
160            BT2_MERGE_PERCENT, BT2_NODE_SIZE, BT2_SPLIT_PERCENT,
161        };
162        Self {
163            node_size: BT2_NODE_SIZE,
164            split_percent: BT2_SPLIT_PERCENT,
165            merge_percent: BT2_MERGE_PERCENT,
166        }
167    }
168}
169
170/// Filtered single-chunk index parameters.
171///
172/// When a version-4 chunked layout uses the Single Chunk index AND the
173/// layout's "single index with filter" flag (`flags & 0x02`) is set,
174/// libhdf5 stores the chunk's on-disk (post-filter) size and its per-chunk
175/// filter mask inline in the layout message rather than in a separate index
176/// structure (H5Olayout.c). The mask must be honored on read: a set bit
177/// means the corresponding filter was *not* applied to this chunk.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub struct SingleChunkFilter {
180    /// On-disk (filtered) size of the single chunk, in bytes.
181    pub nbytes: u64,
182    /// Per-chunk filter mask: bit `i` set ⟹ filter `i` (forward pipeline
183    /// order) was skipped for this chunk and must not be reversed on read.
184    pub filter_mask: u32,
185}
186
187/// Data layout message payload.
188#[derive(Debug, Clone, PartialEq)]
189pub enum DataLayoutMessage {
190    /// Contiguous storage — raw data in a single block.
191    Contiguous {
192        /// Address of raw data.  `UNDEF_ADDR` if not yet allocated.
193        address: u64,
194        /// Size of raw data in bytes.
195        size: u64,
196    },
197    /// Compact storage — raw data stored within the object header.
198    Compact {
199        /// The raw data bytes.
200        data: Vec<u8>,
201    },
202    /// Version 3 chunked storage, indexed by a version-1 B-tree.
203    ///
204    /// This is what libhdf5 / h5py writes for a chunked dataset created
205    /// with the default `libver` bounds.
206    ChunkedV3 {
207        /// Chunk dimension sizes, including the trailing element-size
208        /// dimension (so the chunk rank is `chunk_dims.len() - 1`).
209        chunk_dims: Vec<u64>,
210        /// Address of the version-1 B-tree that indexes the chunks.
211        b_tree_address: u64,
212    },
213    /// Version 4 chunked storage. Version 5 shares this exact wire format —
214    /// only the version byte differs — so both decode into this variant.
215    ChunkedV4 {
216        /// Message version byte: 4 or 5. Version 5 (libhdf5 2.0,
217        /// `H5O_LAYOUT_VERSION_5`) declares that the chunk index encodes
218        /// filtered-chunk sizes in a fixed `sizeof_size`-byte field instead
219        /// of the width derived from the chunk byte count, so a filter may
220        /// expand a chunk without overflowing the field. Readers older than
221        /// libhdf5 2.0 reject version 5.
222        version: u8,
223        flags: u8,
224        /// Chunk dimension sizes.
225        chunk_dims: Vec<u64>,
226        /// Type of chunk index structure.
227        index_type: ChunkIndexType,
228        /// Extensible array parameters (present when index_type == ExtensibleArray).
229        earray_params: Option<EarrayParams>,
230        /// Fixed array parameters (present when index_type == FixedArray).
231        farray_params: Option<FixedArrayParams>,
232        /// v2 B-tree parameters (present when index_type == BTreeV2).
233        bt2_params: Option<Bt2Params>,
234        /// Filtered single-chunk parameters (present when index_type ==
235        /// SingleChunk and the layout's filtered flag `0x02` is set).
236        single_chunk_filter: Option<SingleChunkFilter>,
237        /// Address of the chunk index structure.
238        index_address: u64,
239    },
240    /// Virtual dataset storage (H5D_VIRTUAL): the layout carries no data
241    /// address of its own. `heap_address`/`heap_index` name the global
242    /// heap object holding the mapping list — decode it with
243    /// [`crate::format::messages::virtual_mapping::VirtualMappingList`].
244    Virtual {
245        /// Message version byte: 4 or 5 (virtual layout did not exist
246        /// before version 4; version 5 is identical here).
247        version: u8,
248        /// Address of the global heap collection holding the mapping list.
249        heap_address: u64,
250        /// 1-based index of the mapping-list object within that
251        /// collection. `0` means no mapping list has been written yet
252        /// (a virtual dataset created but never given any mappings).
253        heap_index: u32,
254    },
255}
256
257impl DataLayoutMessage {
258    /// The checks libhdf5 makes between a layout and the dataset's sibling
259    /// dataspace and datatype messages as the dataset opens, gathered in the
260    /// one place the three decode side by side:
261    ///
262    /// - A chunked layout's stored dimensionality is the chunk rank plus one
263    ///   trailing element-size dimension, so it must be exactly one more
264    ///   than the dataspace rank (`H5O__layout_decode`, H5Olayout.c;
265    ///   HDFGroup/hdf5#6508, CVE-2026-19025). Left to chunk I/O, disagreeing
266    ///   ranks decode the index keys at the wrong rank and the chunk grid
267    ///   cannot be indexed.
268    /// - A compact layout's stored bytes must be exactly the extent's
269    ///   element count times the stored element size (`H5D__compact_init`,
270    ///   H5Dcompact.c:255-269). Left to I/O, a short payload is read past
271    ///   its end by any selection that reaches the missing elements.
272    ///
273    /// Contiguous and virtual layouts have neither and always pass.
274    pub fn check_against_dataset(
275        &self,
276        dataspace: &crate::format::messages::dataspace::DataspaceMessage,
277        datatype: &crate::format::messages::datatype::DatatypeMessage,
278        ctx: &FormatContext,
279    ) -> FormatResult<()> {
280        match self {
281            Self::ChunkedV3 { chunk_dims, .. } | Self::ChunkedV4 { chunk_dims, .. } => {
282                let rank = dataspace.dims.len();
283                if chunk_dims.len() != rank + 1 {
284                    return Err(FormatError::InvalidData(format!(
285                        "chunk dimensionality {} over a rank-{rank} dataspace; the chunk rank \
286                         plus the element-size dimension must be {}",
287                        chunk_dims.len(),
288                        rank + 1
289                    )));
290                }
291                Ok(())
292            }
293            Self::Compact { data } => {
294                let dt_size = datatype.element_size_ctx(ctx) as u64;
295                let nelmts = dataspace.element_count();
296                let Some(expected) = nelmts.and_then(|n| n.checked_mul(dt_size)) else {
297                    return Err(FormatError::InvalidData(
298                        "the size of the dataset's compact storage overflows".into(),
299                    ));
300                };
301                if data.len() as u64 != expected {
302                    return Err(FormatError::InvalidData(format!(
303                        "compact storage holds {} bytes but the dataset's {} elements of \
304                         {dt_size} bytes need {expected}",
305                        data.len(),
306                        nelmts.unwrap_or(0)
307                    )));
308                }
309                Ok(())
310            }
311            Self::Contiguous { .. } | Self::Virtual { .. } => Ok(()),
312        }
313    }
314
315    /// The storage class and message version, for a message that has to name
316    /// which layout it is talking about.
317    pub fn describe(&self) -> &'static str {
318        match self {
319            Self::Contiguous { .. } => "contiguous",
320            Self::Compact { .. } => "compact (version 3)",
321            Self::ChunkedV3 { .. } => "chunked, version-1 B-tree index (layout version 3)",
322            Self::ChunkedV4 { .. } => "chunked (layout version 4 or 5)",
323            Self::Virtual { .. } => "virtual",
324        }
325    }
326
327    /// Contiguous layout with no data allocated yet.
328    pub fn contiguous_unallocated(size: u64) -> Self {
329        Self::Contiguous {
330            address: UNDEF_ADDR,
331            size,
332        }
333    }
334
335    /// Contiguous layout pointing to allocated data.
336    pub fn contiguous(address: u64, size: u64) -> Self {
337        Self::Contiguous { address, size }
338    }
339
340    /// Compact layout with inline data.
341    pub fn compact(data: Vec<u8>) -> Self {
342        Self::Compact { data }
343    }
344
345    /// Version 3 chunked layout indexed by a version-1 B-tree.
346    ///
347    /// `chunk_dims` must include the trailing element-size dimension.
348    pub fn chunked_v3_btree_v1(chunk_dims: Vec<u64>, b_tree_address: u64) -> Self {
349        Self::ChunkedV3 {
350            chunk_dims,
351            b_tree_address,
352        }
353    }
354
355    /// Version 4 chunked layout with extensible array index.
356    ///
357    /// `chunk_dims` should include the trailing element-size dimension.
358    /// For example, for a 2D dataset with chunk=(1,4) and element_size=8,
359    /// pass chunk_dims = [1, 4, 8].
360    pub fn chunked_v4_earray(
361        version: u8,
362        chunk_dims: Vec<u64>,
363        earray_params: EarrayParams,
364        index_address: u64,
365    ) -> Self {
366        Self::ChunkedV4 {
367            version,
368            flags: 0,
369            chunk_dims,
370            index_type: ChunkIndexType::ExtensibleArray,
371            earray_params: Some(earray_params),
372            farray_params: None,
373            bt2_params: None,
374            single_chunk_filter: None,
375            index_address,
376        }
377    }
378
379    /// Version 4 chunked layout with fixed array index.
380    ///
381    /// `chunk_dims` should include the trailing element-size dimension.
382    pub fn chunked_v4_farray(
383        version: u8,
384        chunk_dims: Vec<u64>,
385        farray_params: FixedArrayParams,
386        index_address: u64,
387    ) -> Self {
388        Self::ChunkedV4 {
389            version,
390            flags: 0,
391            chunk_dims,
392            index_type: ChunkIndexType::FixedArray,
393            earray_params: None,
394            farray_params: Some(farray_params),
395            bt2_params: None,
396            single_chunk_filter: None,
397            index_address,
398        }
399    }
400
401    /// Version 4 chunked layout with B-tree v2 index.
402    ///
403    /// `chunk_dims` should include the trailing element-size dimension.
404    pub fn chunked_v4_btree_v2(
405        version: u8,
406        chunk_dims: Vec<u64>,
407        bt2_params: Bt2Params,
408        index_address: u64,
409    ) -> Self {
410        Self::ChunkedV4 {
411            version,
412            flags: 0,
413            chunk_dims,
414            index_type: ChunkIndexType::BTreeV2,
415            earray_params: None,
416            farray_params: None,
417            bt2_params: Some(bt2_params),
418            single_chunk_filter: None,
419            index_address,
420        }
421    }
422
423    /// Version 4 chunked layout with the implicit index — no index structure
424    /// at all: `index_address` is the start of one contiguous run holding
425    /// every chunk of the maximum-extent grid in row-major order, so a
426    /// chunk's address is arithmetic (`H5D__none_idx_get_addr`, H5Dnone.c).
427    ///
428    /// `chunk_dims` should include the trailing element-size dimension.
429    pub fn chunked_v4_implicit(version: u8, chunk_dims: Vec<u64>, index_address: u64) -> Self {
430        Self::ChunkedV4 {
431            version,
432            flags: 0,
433            chunk_dims,
434            index_type: ChunkIndexType::Implicit,
435            earray_params: None,
436            farray_params: None,
437            bt2_params: None,
438            single_chunk_filter: None,
439            index_address,
440        }
441    }
442
443    /// Virtual dataset layout pointing at a global-heap mapping list.
444    pub fn virtual_layout(version: u8, heap_address: u64, heap_index: u32) -> Self {
445        Self::Virtual {
446            version,
447            heap_address,
448            heap_index,
449        }
450    }
451
452    /// Version 4 chunked layout with single-chunk index.
453    ///
454    /// `chunk_dims` should include the trailing element-size dimension.
455    pub fn chunked_v4_single(chunk_dims: Vec<u64>, index_address: u64) -> Self {
456        Self::ChunkedV4 {
457            version: VERSION_4,
458            flags: 0,
459            chunk_dims,
460            index_type: ChunkIndexType::SingleChunk,
461            earray_params: None,
462            farray_params: None,
463            bt2_params: None,
464            single_chunk_filter: None,
465            index_address,
466        }
467    }
468
469    /// Version 4 chunked layout with a *filtered* single-chunk index: the
470    /// "single index with filter" flag (`0x02`) is set and the chunk's
471    /// on-disk size and filter mask are carried inline
472    /// (`H5O_LAYOUT_CHUNK_SINGLE_INDEX_WITH_FILTER`, H5Dsingle.c).
473    ///
474    /// `chunk_dims` should include the trailing element-size dimension.
475    pub fn chunked_v4_single_filtered(
476        chunk_dims: Vec<u64>,
477        index_address: u64,
478        nbytes: u64,
479        filter_mask: u32,
480    ) -> Self {
481        Self::ChunkedV4 {
482            version: VERSION_4,
483            flags: 0x02,
484            chunk_dims,
485            index_type: ChunkIndexType::SingleChunk,
486            earray_params: None,
487            farray_params: None,
488            bt2_params: None,
489            single_chunk_filter: Some(SingleChunkFilter {
490                nbytes,
491                filter_mask,
492            }),
493            index_address,
494        }
495    }
496
497    // ------------------------------------------------------------------ encode
498
499    pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
500        match self {
501            Self::Contiguous { address, size } => {
502                let sa = ctx.sizeof_addr as usize;
503                let ss = ctx.sizeof_size as usize;
504                let mut buf = Vec::with_capacity(2 + sa + ss);
505                buf.push(VERSION_3);
506                buf.push(CLASS_CONTIGUOUS);
507                buf.extend_from_slice(&address.to_le_bytes()[..sa]);
508                buf.extend_from_slice(&size.to_le_bytes()[..ss]);
509                buf
510            }
511            Self::Compact { data } => {
512                let mut buf = Vec::with_capacity(2 + 2 + data.len());
513                buf.push(VERSION_3);
514                buf.push(CLASS_COMPACT);
515                buf.extend_from_slice(&(data.len() as u16).to_le_bytes());
516                buf.extend_from_slice(data);
517                buf
518            }
519            Self::ChunkedV3 {
520                chunk_dims,
521                b_tree_address,
522            } => {
523                let sa = ctx.sizeof_addr as usize;
524                let ndims = chunk_dims.len() as u8;
525                let mut buf = Vec::with_capacity(3 + sa + chunk_dims.len() * 4);
526                buf.push(VERSION_3);
527                buf.push(CLASS_CHUNKED);
528                buf.push(ndims);
529                buf.extend_from_slice(&b_tree_address.to_le_bytes()[..sa]);
530                // Dimension sizes are always 4 bytes each (UINT32ENCODE).
531                for &d in chunk_dims {
532                    buf.extend_from_slice(&(d as u32).to_le_bytes());
533                }
534                buf
535            }
536            Self::ChunkedV4 {
537                version,
538                flags,
539                chunk_dims,
540                index_type,
541                earray_params,
542                farray_params,
543                bt2_params,
544                single_chunk_filter,
545                index_address,
546            } => {
547                let sa = ctx.sizeof_addr as usize;
548                let ndims = chunk_dims.len() as u8;
549
550                // Compute enc_bytes_per_dim: minimum bytes to represent the
551                // max chunk dimension value.
552                let max_dim = chunk_dims.iter().copied().max().unwrap_or(1);
553                let enc_bytes = enc_bytes_for_value(max_dim);
554
555                debug_assert!(matches!(*version, VERSION_4 | VERSION_5));
556                let mut buf = Vec::with_capacity(64);
557                buf.push(*version);
558                buf.push(CLASS_CHUNKED);
559                buf.push(*flags);
560                buf.push(ndims);
561                buf.push(enc_bytes);
562
563                // Dimension sizes
564                for &d in chunk_dims {
565                    buf.extend_from_slice(&d.to_le_bytes()[..enc_bytes as usize]);
566                }
567
568                // Index type
569                buf.push(*index_type as u8);
570
571                // Index-type-specific parameters
572                match *index_type {
573                    ChunkIndexType::ExtensibleArray => {
574                        if let Some(ref params) = earray_params {
575                            buf.push(params.max_nelmts_bits);
576                            buf.push(params.idx_blk_elmts);
577                            buf.push(params.sup_blk_min_data_ptrs);
578                            buf.push(params.data_blk_min_elmts);
579                            buf.push(params.max_dblk_page_nelmts_bits);
580                        }
581                    }
582                    ChunkIndexType::FixedArray => {
583                        if let Some(ref params) = farray_params {
584                            buf.push(params.max_dblk_page_nelmts_bits);
585                        }
586                    }
587                    ChunkIndexType::BTreeV2 => {
588                        // node_size(4) + split_percent(1) + merge_percent(1),
589                        // the same geometry the B-tree header carries — the
590                        // message must agree with the BTHD it points at, so
591                        // a reopened foreign node size is preserved, not
592                        // stamped over with this writer's default.
593                        if let Some(ref params) = bt2_params {
594                            buf.extend_from_slice(&params.node_size.to_le_bytes());
595                            buf.push(params.split_percent);
596                            buf.push(params.merge_percent);
597                        }
598                    }
599                    // A filtered single chunk carries its on-disk size
600                    // (sizeof_size bytes) and 4-byte filter mask inline, before
601                    // the chunk address (H5Olayout.c). Only emit them when the
602                    // filtered flag (0x02) is set; an unfiltered single chunk
603                    // falls through to the no-extra-parameters arm below.
604                    ChunkIndexType::SingleChunk if *flags & 0x02 != 0 => {
605                        if let Some(scf) = single_chunk_filter {
606                            let ss = ctx.sizeof_size as usize;
607                            buf.extend_from_slice(&scf.nbytes.to_le_bytes()[..ss]);
608                            buf.extend_from_slice(&scf.filter_mask.to_le_bytes());
609                        }
610                    }
611                    // Implicit: no extra parameters.
612                    _ => {}
613                }
614
615                // Index address
616                buf.extend_from_slice(&index_address.to_le_bytes()[..sa]);
617
618                buf
619            }
620            Self::Virtual {
621                version,
622                heap_address,
623                heap_index,
624            } => {
625                let sa = ctx.sizeof_addr as usize;
626                debug_assert!(matches!(*version, VERSION_4 | VERSION_5));
627                let mut buf = Vec::with_capacity(2 + sa + 4);
628                buf.push(*version);
629                buf.push(CLASS_VIRTUAL);
630                buf.extend_from_slice(&heap_address.to_le_bytes()[..sa]);
631                buf.extend_from_slice(&heap_index.to_le_bytes());
632                buf
633            }
634        }
635    }
636
637    // ------------------------------------------------------------------ decode
638
639    pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
640        if buf.len() < 2 {
641            return Err(FormatError::BufferTooShort {
642                needed: 2,
643                available: buf.len(),
644            });
645        }
646
647        let version = buf[0];
648        let class = buf[1];
649
650        // libhdf5 validates the version once and then reads the body by
651        // storage class (`H5O__layout_decode`); only the chunked body differs
652        // between version 3 and versions 4/5. Enumerating (version, class)
653        // pairs instead made every version this decoder had not been taught
654        // about look like a bad version — which is how a perfectly ordinary
655        // contiguous dataset in a v1.10 file (layout version 4) came back as
656        // `InvalidVersion` and vanished from the catalog.
657        match version {
658            VERSION_1 | VERSION_2 => {
659                return Err(FormatError::UnsupportedFeature(format!(
660                    "data layout message version {version}"
661                )))
662            }
663            VERSION_3 | VERSION_4 | VERSION_5 => {}
664            v => return Err(FormatError::InvalidVersion(v)),
665        }
666
667        match class {
668            CLASS_CONTIGUOUS => {
669                let sa = ctx.sizeof_addr as usize;
670                let ss = ctx.sizeof_size as usize;
671                let mut pos = 2;
672                let needed = pos + sa + ss;
673                if buf.len() < needed {
674                    return Err(FormatError::BufferTooShort {
675                        needed,
676                        available: buf.len(),
677                    });
678                }
679                let address = read_addr(&buf[pos..], sa);
680                pos += sa;
681                let size = read_size(&buf[pos..], ss);
682                pos += ss;
683                Ok((Self::Contiguous { address, size }, pos))
684            }
685            CLASS_COMPACT => {
686                let mut pos = 2;
687                if buf.len() < pos + 2 {
688                    return Err(FormatError::BufferTooShort {
689                        needed: pos + 2,
690                        available: buf.len(),
691                    });
692                }
693                let compact_size = u16::from_le_bytes([buf[pos], buf[pos + 1]]) as usize;
694                pos += 2;
695                if buf.len() < pos + compact_size {
696                    return Err(FormatError::BufferTooShort {
697                        needed: pos + compact_size,
698                        available: buf.len(),
699                    });
700                }
701                let data = buf[pos..pos + compact_size].to_vec();
702                pos += compact_size;
703                Ok((Self::Compact { data }, pos))
704            }
705            CLASS_CHUNKED if version == VERSION_3 => {
706                // version(1) + class(1) + ndims(1) + b_tree_addr(sa)
707                // + ndims * 4-byte dimension sizes.
708                let sa = ctx.sizeof_addr as usize;
709                let mut pos = 2;
710                if buf.len() < pos + 1 {
711                    return Err(FormatError::BufferTooShort {
712                        needed: pos + 1,
713                        available: buf.len(),
714                    });
715                }
716                let ndims = buf[pos] as usize;
717                pos += 1;
718
719                // libhdf5 (H5Olayout.c) requires 2 <= ndims for chunked
720                // storage: the chunk rank plus the trailing element-size
721                // dimension. A zero or one is malformed.
722                if ndims < 2 {
723                    return Err(FormatError::InvalidData(format!(
724                        "chunked v3 layout dimensionality {ndims} is too small"
725                    )));
726                }
727
728                if buf.len() < pos + sa {
729                    return Err(FormatError::BufferTooShort {
730                        needed: pos + sa,
731                        available: buf.len(),
732                    });
733                }
734                let b_tree_address = read_addr(&buf[pos..], sa);
735                pos += sa;
736
737                let dim_data_len = ndims * 4;
738                if buf.len() < pos + dim_data_len {
739                    return Err(FormatError::BufferTooShort {
740                        needed: pos + dim_data_len,
741                        available: buf.len(),
742                    });
743                }
744                let mut chunk_dims = Vec::with_capacity(ndims);
745                for _ in 0..ndims {
746                    let d = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]])
747                        as u64;
748                    if d == 0 {
749                        return Err(FormatError::InvalidData(
750                            "chunked v3 layout has a zero chunk dimension".into(),
751                        ));
752                    }
753                    chunk_dims.push(d);
754                    pos += 4;
755                }
756
757                Ok((
758                    Self::ChunkedV3 {
759                        chunk_dims,
760                        b_tree_address,
761                    },
762                    pos,
763                ))
764            }
765            CLASS_CHUNKED => {
766                let sa = ctx.sizeof_addr as usize;
767                let mut pos = 2;
768
769                // flags(1) + ndims(1) + enc_bytes_per_dim(1)
770                if buf.len() < pos + 3 {
771                    return Err(FormatError::BufferTooShort {
772                        needed: pos + 3,
773                        available: buf.len(),
774                    });
775                }
776                let flags = buf[pos];
777                pos += 1;
778                let ndims = buf[pos] as usize;
779                pos += 1;
780                let enc_bytes = buf[pos] as usize;
781                pos += 1;
782
783                // libhdf5 (H5Olayout.c) requires 1 <= enc_bytes <= 8;
784                // 0 produces all-zero dims, > 8 panics read_size.
785                if !(1..=8).contains(&enc_bytes) {
786                    return Err(FormatError::InvalidData(format!(
787                        "chunked layout encoded dimension size {enc_bytes} is out of range"
788                    )));
789                }
790                // Chunked storage carries the chunk rank plus the trailing
791                // element-size dimension, so ndims is at least 2.
792                if ndims < 2 {
793                    return Err(FormatError::InvalidData(format!(
794                        "chunked v4 layout dimensionality {ndims} is too small"
795                    )));
796                }
797
798                // dim sizes
799                let dim_data_len = ndims * enc_bytes;
800                if buf.len() < pos + dim_data_len {
801                    return Err(FormatError::BufferTooShort {
802                        needed: pos + dim_data_len,
803                        available: buf.len(),
804                    });
805                }
806                let mut chunk_dims = Vec::with_capacity(ndims);
807                for _ in 0..ndims {
808                    let d = read_size(&buf[pos..], enc_bytes);
809                    if d == 0 {
810                        return Err(FormatError::InvalidData(
811                            "chunked v4 layout has a zero chunk dimension".into(),
812                        ));
813                    }
814                    chunk_dims.push(d);
815                    pos += enc_bytes;
816                }
817
818                // index type
819                if buf.len() < pos + 1 {
820                    return Err(FormatError::BufferTooShort {
821                        needed: pos + 1,
822                        available: buf.len(),
823                    });
824                }
825                let idx_type_raw = buf[pos];
826                pos += 1;
827                let index_type = ChunkIndexType::from_u8(idx_type_raw).ok_or_else(|| {
828                    FormatError::UnsupportedFeature(format!("chunk index type {}", idx_type_raw))
829                })?;
830
831                // Index-type-specific parameters
832                let mut earray_params = None;
833                let mut farray_params = None;
834                let mut bt2_params = None;
835                let mut single_chunk_filter = None;
836
837                match index_type {
838                    ChunkIndexType::ExtensibleArray => {
839                        if buf.len() < pos + 5 {
840                            return Err(FormatError::BufferTooShort {
841                                needed: pos + 5,
842                                available: buf.len(),
843                            });
844                        }
845                        let ep = EarrayParams {
846                            max_nelmts_bits: buf[pos],
847                            idx_blk_elmts: buf[pos + 1],
848                            sup_blk_min_data_ptrs: buf[pos + 2],
849                            data_blk_min_elmts: buf[pos + 3],
850                            max_dblk_page_nelmts_bits: buf[pos + 4],
851                        };
852                        // libhdf5 rejects a zero in any of these fields.
853                        if ep.max_nelmts_bits == 0
854                            || ep.idx_blk_elmts == 0
855                            || ep.sup_blk_min_data_ptrs == 0
856                            || ep.data_blk_min_elmts == 0
857                            || ep.max_dblk_page_nelmts_bits == 0
858                        {
859                            return Err(FormatError::InvalidData(
860                                "extensible-array layout parameter is zero".into(),
861                            ));
862                        }
863                        earray_params = Some(ep);
864                        pos += 5;
865                    }
866                    ChunkIndexType::FixedArray => {
867                        if buf.len() < pos + 1 {
868                            return Err(FormatError::BufferTooShort {
869                                needed: pos + 1,
870                                available: buf.len(),
871                            });
872                        }
873                        // NOTE: libhdf5 rejects max_dblk_page_nelmts_bits == 0,
874                        // but this crate's own Fixed Array writer currently
875                        // emits 0 (it does not page). Validating it here would
876                        // reject crate-written files; left until the FA writer
877                        // is made libhdf5-conformant.
878                        farray_params = Some(FixedArrayParams {
879                            max_dblk_page_nelmts_bits: buf[pos],
880                        });
881                        pos += 1;
882                    }
883                    ChunkIndexType::BTreeV2 => {
884                        // node_size(4) + split_percent(1) + merge_percent(1).
885                        // The v2 B-tree header carries authoritative copies;
886                        // retained so a rewritten object header re-emits the
887                        // creator's values, not this writer's defaults.
888                        if buf.len() < pos + 6 {
889                            return Err(FormatError::BufferTooShort {
890                                needed: pos + 6,
891                                available: buf.len(),
892                            });
893                        }
894                        bt2_params = Some(Bt2Params {
895                            node_size: u32::from_le_bytes([
896                                buf[pos],
897                                buf[pos + 1],
898                                buf[pos + 2],
899                                buf[pos + 3],
900                            ]),
901                            split_percent: buf[pos + 4],
902                            merge_percent: buf[pos + 5],
903                        });
904                        pos += 6;
905                    }
906                    // A single-chunk index whose "single index with
907                    // filter" flag (0x02) is set carries the filtered
908                    // chunk size (sizeof_size bytes) and a 4-byte filter
909                    // mask before the chunk address (H5Olayout.c). Retain
910                    // both: the reader needs the exact on-disk size and must
911                    // honor the per-chunk mask when reversing filters.
912                    ChunkIndexType::SingleChunk if flags & 0x02 != 0 => {
913                        let ss = ctx.sizeof_size as usize;
914                        let extra = ss + 4;
915                        if buf.len() < pos + extra {
916                            return Err(FormatError::BufferTooShort {
917                                needed: pos + extra,
918                                available: buf.len(),
919                            });
920                        }
921                        let nbytes = read_size(&buf[pos..], ss);
922                        pos += ss;
923                        let filter_mask = u32::from_le_bytes([
924                            buf[pos],
925                            buf[pos + 1],
926                            buf[pos + 2],
927                            buf[pos + 3],
928                        ]);
929                        pos += 4;
930                        single_chunk_filter = Some(SingleChunkFilter {
931                            nbytes,
932                            filter_mask,
933                        });
934                    }
935                    // Implicit, and single-chunk without the filter flag:
936                    // no extra parameters.
937                    _ => {}
938                }
939
940                // index address
941                if buf.len() < pos + sa {
942                    return Err(FormatError::BufferTooShort {
943                        needed: pos + sa,
944                        available: buf.len(),
945                    });
946                }
947                let index_address = read_addr(&buf[pos..], sa);
948                pos += sa;
949
950                Ok((
951                    Self::ChunkedV4 {
952                        version: buf[0],
953                        flags,
954                        chunk_dims,
955                        index_type,
956                        earray_params,
957                        farray_params,
958                        bt2_params,
959                        single_chunk_filter,
960                        index_address,
961                    },
962                    pos,
963                ))
964            }
965            CLASS_VIRTUAL => {
966                // libhdf5 (H5Olayout.c) rejects a virtual layout below
967                // version 4 outright ("invalid layout version with virtual
968                // layout") — the class did not exist before version 4, so a
969                // version-3 message can never legitimately carry it.
970                if version == VERSION_3 {
971                    return Err(FormatError::InvalidVersion(VERSION_3));
972                }
973                let sa = ctx.sizeof_addr as usize;
974                let mut pos = 2;
975                if buf.len() < pos + sa {
976                    return Err(FormatError::BufferTooShort {
977                        needed: pos + sa,
978                        available: buf.len(),
979                    });
980                }
981                let heap_address = read_addr(&buf[pos..], sa);
982                pos += sa;
983
984                if buf.len() < pos + 4 {
985                    return Err(FormatError::BufferTooShort {
986                        needed: pos + 4,
987                        available: buf.len(),
988                    });
989                }
990                let heap_index =
991                    u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
992                pos += 4;
993
994                Ok((
995                    Self::Virtual {
996                        version: buf[0],
997                        heap_address,
998                        heap_index,
999                    },
1000                    pos,
1001                ))
1002            }
1003            other => Err(FormatError::UnsupportedFeature(format!(
1004                "data layout class {}",
1005                other
1006            ))),
1007        }
1008    }
1009}
1010
1011// ========================================================================= helpers
1012
1013/// Compute the minimum number of bytes (1-8) needed to encode `v`.
1014fn enc_bytes_for_value(v: u64) -> u8 {
1015    if v == 0 {
1016        return 1;
1017    }
1018    let bits_needed = 64 - v.leading_zeros(); // 1..=64
1019    bits_needed.div_ceil(8) as u8
1020}
1021
1022// ======================================================================= tests
1023
1024#[cfg(test)]
1025mod tests {
1026    use super::*;
1027
1028    fn ctx8() -> FormatContext {
1029        FormatContext {
1030            sizeof_addr: 8,
1031            sizeof_size: 8,
1032        }
1033    }
1034
1035    fn ctx4() -> FormatContext {
1036        FormatContext {
1037            sizeof_addr: 4,
1038            sizeof_size: 4,
1039        }
1040    }
1041
1042    #[test]
1043    fn roundtrip_contiguous() {
1044        let msg = DataLayoutMessage::contiguous(0x1000, 4096);
1045        let encoded = msg.encode(&ctx8());
1046        // 2 + 8 + 8 = 18
1047        assert_eq!(encoded.len(), 18);
1048        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1049        assert_eq!(consumed, 18);
1050        assert_eq!(decoded, msg);
1051    }
1052
1053    #[test]
1054    fn roundtrip_contiguous_ctx4() {
1055        let msg = DataLayoutMessage::contiguous(0x800, 256);
1056        let encoded = msg.encode(&ctx4());
1057        // 2 + 4 + 4 = 10
1058        assert_eq!(encoded.len(), 10);
1059        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
1060        assert_eq!(consumed, 10);
1061        assert_eq!(decoded, msg);
1062    }
1063
1064    #[test]
1065    fn roundtrip_contiguous_unallocated() {
1066        let msg = DataLayoutMessage::contiguous_unallocated(1024);
1067        let encoded = msg.encode(&ctx8());
1068        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1069        assert_eq!(decoded, msg);
1070        match decoded {
1071            DataLayoutMessage::Contiguous { address, size } => {
1072                assert_eq!(address, UNDEF_ADDR);
1073                assert_eq!(size, 1024);
1074            }
1075            _ => panic!("expected Contiguous"),
1076        }
1077    }
1078
1079    #[test]
1080    fn roundtrip_contiguous_undef_ctx4() {
1081        let msg = DataLayoutMessage::contiguous_unallocated(512);
1082        let encoded = msg.encode(&ctx4());
1083        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
1084        match decoded {
1085            DataLayoutMessage::Contiguous { address, .. } => {
1086                assert_eq!(address, UNDEF_ADDR);
1087            }
1088            _ => panic!("expected Contiguous"),
1089        }
1090    }
1091
1092    #[test]
1093    fn roundtrip_compact() {
1094        let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
1095        let msg = DataLayoutMessage::compact(data.clone());
1096        let encoded = msg.encode(&ctx8());
1097        // 2 + 2 + 8 = 12
1098        assert_eq!(encoded.len(), 12);
1099        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1100        assert_eq!(consumed, 12);
1101        assert_eq!(decoded, msg);
1102    }
1103
1104    #[test]
1105    fn roundtrip_compact_empty() {
1106        let msg = DataLayoutMessage::compact(vec![]);
1107        let encoded = msg.encode(&ctx8());
1108        assert_eq!(encoded.len(), 4); // 2 + 2 + 0
1109        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1110        assert_eq!(consumed, 4);
1111        assert_eq!(decoded, msg);
1112    }
1113
1114    /// Versions 1 and 2 are legal layout versions libhdf5 still reads, so
1115    /// they are reported as an unsupported feature (which the catalog surfaces
1116    /// by name) rather than as a bad version.
1117    #[test]
1118    fn decode_legacy_version_is_unsupported_not_invalid() {
1119        for version in [1u8, 2] {
1120            let mut buf = vec![version, 1];
1121            buf.extend_from_slice(&[0u8; 16]);
1122            let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1123            match err {
1124                FormatError::UnsupportedFeature(ref s) => {
1125                    assert!(s.contains(&version.to_string()), "{s}")
1126                }
1127                other => panic!("unexpected error for version {version}: {other:?}"),
1128            }
1129        }
1130    }
1131
1132    #[test]
1133    fn decode_bad_version() {
1134        for version in [0u8, 6, 255] {
1135            let mut buf = vec![version, 1];
1136            buf.extend_from_slice(&[0u8; 16]);
1137            let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1138            match err {
1139                FormatError::InvalidVersion(v) if v == version => {}
1140                other => panic!("unexpected error for version {version}: {other:?}"),
1141            }
1142        }
1143    }
1144
1145    /// The bug this guards: h5py writing under `libver=("v110","v110")` emits
1146    /// a *version 4* contiguous layout message whose body is byte-identical to
1147    /// the version-3 one. Rejecting it dropped the dataset from the catalog
1148    /// entirely, while a chunked dataset in the same file listed fine.
1149    #[test]
1150    fn decode_contiguous_and_compact_at_every_modern_version() {
1151        for version in [3u8, 4, 5] {
1152            let mut contig = vec![version, CLASS_CONTIGUOUS];
1153            contig.extend_from_slice(&0x800u64.to_le_bytes());
1154            contig.extend_from_slice(&64u64.to_le_bytes());
1155            let (decoded, consumed) = DataLayoutMessage::decode(&contig, &ctx8()).unwrap();
1156            assert_eq!(consumed, contig.len());
1157            assert_eq!(
1158                decoded,
1159                DataLayoutMessage::Contiguous {
1160                    address: 0x800,
1161                    size: 64
1162                }
1163            );
1164
1165            let payload = [1u8, 2, 3, 4];
1166            let mut compact = vec![version, CLASS_COMPACT];
1167            compact.extend_from_slice(&(payload.len() as u16).to_le_bytes());
1168            compact.extend_from_slice(&payload);
1169            let (decoded, consumed) = DataLayoutMessage::decode(&compact, &ctx8()).unwrap();
1170            assert_eq!(consumed, compact.len());
1171            assert_eq!(
1172                decoded,
1173                DataLayoutMessage::Compact {
1174                    data: payload.to_vec()
1175                }
1176            );
1177        }
1178    }
1179
1180    #[test]
1181    fn decode_unsupported_class() {
1182        let buf = [3u8, 4]; // class 4 = unknown (0-3 are all defined)
1183        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1184        match err {
1185            FormatError::UnsupportedFeature(_) => {}
1186            other => panic!("unexpected error: {:?}", other),
1187        }
1188    }
1189
1190    #[test]
1191    fn decode_buffer_too_short() {
1192        let buf = [3u8];
1193        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1194        match err {
1195            FormatError::BufferTooShort { .. } => {}
1196            other => panic!("unexpected error: {:?}", other),
1197        }
1198    }
1199
1200    #[test]
1201    fn decode_contiguous_truncated() {
1202        // version=3, class=1, but not enough bytes for address+size
1203        let buf = [3u8, 1, 0, 0];
1204        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1205        match err {
1206            FormatError::BufferTooShort { .. } => {}
1207            other => panic!("unexpected error: {:?}", other),
1208        }
1209    }
1210
1211    #[test]
1212    fn version_and_class_bytes() {
1213        let encoded = DataLayoutMessage::contiguous(0, 0).encode(&ctx8());
1214        assert_eq!(encoded[0], 3);
1215        assert_eq!(encoded[1], 1);
1216
1217        let encoded = DataLayoutMessage::compact(vec![]).encode(&ctx8());
1218        assert_eq!(encoded[0], 3);
1219        assert_eq!(encoded[1], 0);
1220    }
1221
1222    #[test]
1223    fn roundtrip_chunked_v4_earray() {
1224        let params = EarrayParams::default_params();
1225        let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 256, 256], params, 0x2000);
1226        let encoded = msg.encode(&ctx8());
1227        assert_eq!(encoded[0], 4); // version 4
1228        assert_eq!(encoded[1], 2); // class chunked
1229        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1230        assert_eq!(consumed, encoded.len());
1231        assert_eq!(decoded, msg);
1232    }
1233
1234    #[test]
1235    fn roundtrip_chunked_v4_earray_ctx4() {
1236        let params = EarrayParams::default_params();
1237        let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 128], params, 0x1000);
1238        let encoded = msg.encode(&ctx4());
1239        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
1240        assert_eq!(consumed, encoded.len());
1241        assert_eq!(decoded, msg);
1242    }
1243
1244    /// A version-5 layout differs from v4 only in the version byte; the body
1245    /// encodes identically and the version must survive the round trip (a
1246    /// reopen that dropped it would silently downgrade the file to v4 while
1247    /// its filtered index keeps 8-byte size fields).
1248    #[test]
1249    fn roundtrip_chunked_v5_earray() {
1250        let params = EarrayParams::default_params();
1251        let v5 = DataLayoutMessage::chunked_v4_earray(5, vec![1, 256, 256], params.clone(), 0x2000);
1252        let encoded = v5.encode(&ctx8());
1253        assert_eq!(encoded[0], 5); // version 5
1254        assert_eq!(encoded[1], 2); // class chunked
1255        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1256        assert_eq!(consumed, encoded.len());
1257        assert_eq!(decoded, v5);
1258
1259        // Same message at v4: only byte 0 differs.
1260        let v4 = DataLayoutMessage::chunked_v4_earray(4, vec![1, 256, 256], params, 0x2000);
1261        let encoded_v4 = v4.encode(&ctx8());
1262        assert_eq!(encoded[1..], encoded_v4[1..]);
1263    }
1264
1265    #[test]
1266    fn roundtrip_chunked_v4_single() {
1267        let msg = DataLayoutMessage::chunked_v4_single(vec![100, 200], 0x3000);
1268        let encoded = msg.encode(&ctx8());
1269        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1270        assert_eq!(consumed, encoded.len());
1271        assert_eq!(decoded, msg);
1272    }
1273
1274    /// The BTreeV2 parameters (node size, split/merge) round-trip through
1275    /// the message instead of being skipped on decode and re-stamped with
1276    /// defaults on encode — a rewritten object header must agree with the
1277    /// BTHD it points at.
1278    #[test]
1279    fn roundtrip_chunked_v4_btree_v2_params() {
1280        for ctx in [ctx8(), ctx4()] {
1281            let msg = DataLayoutMessage::chunked_v4_btree_v2(
1282                4,
1283                vec![2, 2, 8],
1284                Bt2Params {
1285                    node_size: 512,
1286                    split_percent: 90,
1287                    merge_percent: 30,
1288                },
1289                0x2000,
1290            );
1291            let encoded = msg.encode(&ctx);
1292            let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx).unwrap();
1293            assert_eq!(consumed, encoded.len());
1294            assert_eq!(decoded, msg);
1295        }
1296    }
1297
1298    /// A filtered single-chunk layout (flag `0x02`) carries the chunk's
1299    /// on-disk size and per-chunk filter mask inline. Decode must retain both
1300    /// (not discard them), and encode↔decode must round-trip — including the
1301    /// nonzero mask the reader needs to skip a filter.
1302    #[test]
1303    fn roundtrip_chunked_v4_single_filtered() {
1304        for ctx in [ctx8(), ctx4()] {
1305            let msg = DataLayoutMessage::ChunkedV4 {
1306                version: 4,
1307                flags: 0x02,
1308                chunk_dims: vec![100, 200, 4],
1309                index_type: ChunkIndexType::SingleChunk,
1310                earray_params: None,
1311                farray_params: None,
1312                bt2_params: None,
1313                single_chunk_filter: Some(SingleChunkFilter {
1314                    nbytes: 12345,
1315                    filter_mask: 0b101,
1316                }),
1317                index_address: 0x3000,
1318            };
1319            let encoded = msg.encode(&ctx);
1320            let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx).unwrap();
1321            assert_eq!(consumed, encoded.len());
1322            assert_eq!(decoded, msg);
1323            // The decoded layout exposes the retained size and mask.
1324            match decoded {
1325                DataLayoutMessage::ChunkedV4 {
1326                    single_chunk_filter: Some(scf),
1327                    ..
1328                } => {
1329                    assert_eq!(scf.nbytes, 12345);
1330                    assert_eq!(scf.filter_mask, 0b101);
1331                }
1332                other => panic!("expected filtered single-chunk layout, got {other:?}"),
1333            }
1334        }
1335    }
1336
1337    #[test]
1338    fn chunked_v4_enc_bytes() {
1339        // chunk dims [1, 256, 256]: max=256, needs 2 bytes
1340        let params = EarrayParams::default_params();
1341        let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 256, 256], params, 0x2000);
1342        let encoded = msg.encode(&ctx8());
1343        // version(1) + class(1) + flags(1) + ndims(1) + enc_bytes(1)
1344        // + 3*2 dim bytes + index_type(1) + 5 earray params + 8 addr = 25
1345        assert_eq!(encoded.len(), 25);
1346        assert_eq!(encoded[4], 2); // enc_bytes_per_dim = 2
1347    }
1348
1349    #[test]
1350    fn roundtrip_chunked_v3_btree_v1() {
1351        // 1-D dataset, chunk=(8), element_size=4 -> chunk_dims=[8, 4].
1352        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![8, 4], 0x1234);
1353        let encoded = msg.encode(&ctx8());
1354        // version(1) + class(1) + ndims(1) + addr(8) + 2*4 dims = 19
1355        assert_eq!(encoded.len(), 19);
1356        assert_eq!(encoded[0], 3);
1357        assert_eq!(encoded[1], 2);
1358        assert_eq!(encoded[2], 2); // ndims
1359        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1360        assert_eq!(consumed, encoded.len());
1361        assert_eq!(decoded, msg);
1362    }
1363
1364    #[test]
1365    fn roundtrip_chunked_v3_btree_v1_2d_ctx4() {
1366        // 2-D dataset, chunk=(2,3), element_size=8 -> chunk_dims=[2, 3, 8].
1367        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![2, 3, 8], 0x800);
1368        let encoded = msg.encode(&ctx4());
1369        // version(1) + class(1) + ndims(1) + addr(4) + 3*4 dims = 19
1370        assert_eq!(encoded.len(), 19);
1371        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
1372        assert_eq!(consumed, encoded.len());
1373        assert_eq!(decoded, msg);
1374    }
1375
1376    #[test]
1377    fn chunked_v3_undef_btree_addr() {
1378        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![16, 4], UNDEF_ADDR);
1379        let encoded = msg.encode(&ctx8());
1380        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1381        match decoded {
1382            DataLayoutMessage::ChunkedV3 { b_tree_address, .. } => {
1383                assert_eq!(b_tree_address, UNDEF_ADDR);
1384            }
1385            _ => panic!("expected ChunkedV3"),
1386        }
1387    }
1388
1389    #[test]
1390    fn chunked_v3_rejects_ndims_too_small() {
1391        // ndims = 1 is malformed for chunked storage.
1392        let buf = [3u8, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
1393        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1394        assert!(matches!(err, FormatError::InvalidData(_)));
1395    }
1396
1397    #[test]
1398    fn chunked_v3_rejects_zero_dim() {
1399        // ndims=2, addr=0, dims=[0, 4] -> zero chunk dimension.
1400        let mut buf = vec![3u8, 2, 2];
1401        buf.extend_from_slice(&0u64.to_le_bytes()); // addr
1402        buf.extend_from_slice(&0u32.to_le_bytes()); // dim 0 == 0
1403        buf.extend_from_slice(&4u32.to_le_bytes()); // dim 1
1404        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1405        assert!(matches!(err, FormatError::InvalidData(_)));
1406    }
1407
1408    #[test]
1409    fn chunked_v3_truncated() {
1410        // version=3, class=2, ndims=2, but no room for addr/dims.
1411        let buf = [3u8, 2, 2];
1412        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1413        assert!(matches!(err, FormatError::BufferTooShort { .. }));
1414    }
1415
1416    #[test]
1417    fn roundtrip_virtual_layout() {
1418        for ctx in [ctx8(), ctx4()] {
1419            for version in [4u8, 5u8] {
1420                let msg = DataLayoutMessage::virtual_layout(version, 0x5000, 3);
1421                let encoded = msg.encode(&ctx);
1422                assert_eq!(encoded[0], version);
1423                assert_eq!(encoded[1], CLASS_VIRTUAL);
1424                let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx).unwrap();
1425                assert_eq!(consumed, encoded.len());
1426                assert_eq!(decoded, msg);
1427            }
1428        }
1429    }
1430
1431    #[test]
1432    fn virtual_layout_undefined_heap_address() {
1433        // A virtual dataset created but never given any mappings: no heap
1434        // object exists yet, so the address is UNDEF and the index is 0.
1435        let msg = DataLayoutMessage::virtual_layout(4, UNDEF_ADDR, 0);
1436        let encoded = msg.encode(&ctx8());
1437        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1438        match decoded {
1439            DataLayoutMessage::Virtual {
1440                heap_address,
1441                heap_index,
1442                ..
1443            } => {
1444                assert_eq!(heap_address, UNDEF_ADDR);
1445                assert_eq!(heap_index, 0);
1446            }
1447            other => panic!("expected Virtual, got {other:?}"),
1448        }
1449    }
1450
1451    /// libhdf5 rejects a virtual layout below version 4 outright — the
1452    /// class did not exist before version 4 (H5Olayout.c: "invalid layout
1453    /// version with virtual layout").
1454    #[test]
1455    fn virtual_layout_rejects_version_3() {
1456        let buf = [VERSION_3, CLASS_VIRTUAL];
1457        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1458        assert!(matches!(err, FormatError::InvalidVersion(VERSION_3)));
1459    }
1460
1461    #[test]
1462    fn chunked_v4_large_dims() {
1463        // Large dims requiring 4 bytes each
1464        let params = EarrayParams::default_params();
1465        let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 65536], params, 0x4000);
1466        let encoded = msg.encode(&ctx8());
1467        assert_eq!(encoded[4], 3); // enc_bytes_per_dim = 3 (65536 = 0x10000, needs 3 bytes)
1468    }
1469
1470    /// `check_against_dataset` at each boundary of "chunk dimensionality is the
1471    /// dataspace rank plus one": exactly one more passes, one fewer and one
1472    /// more than that fail, and a layout without chunks never fails.
1473    #[test]
1474    fn check_against_dataset_chunk_rank_boundaries() {
1475        use crate::format::messages::dataspace::DataspaceMessage;
1476        use crate::format::messages::datatype::DatatypeMessage;
1477        let ctx = FormatContext::default_v3();
1478        let i32_t = DatatypeMessage::i32_type();
1479        let rank2 = DataspaceMessage::simple(&[3, 4]);
1480        // chunk_dims = [2, 2, elem] over a rank-2 dataspace: rank + 1.
1481        let v3 = DataLayoutMessage::chunked_v3_btree_v1(vec![2, 2, 4], 0x1000);
1482        assert!(v3.check_against_dataset(&rank2, &i32_t, &ctx).is_ok());
1483        let v4 = DataLayoutMessage::chunked_v4_single(vec![2, 2, 4], 0x1000);
1484        assert!(v4.check_against_dataset(&rank2, &i32_t, &ctx).is_ok());
1485
1486        // One too few: the fixture's patched byte, [2, 2, 4] read as rank-2
1487        // chunks of 4-byte elements over a rank-3 dataspace.
1488        let rank3 = DataspaceMessage::simple(&[3, 4, 5]);
1489        let err = v3.check_against_dataset(&rank3, &i32_t, &ctx).unwrap_err();
1490        assert!(
1491            matches!(err, FormatError::InvalidData(ref s) if s.contains("must be 4")),
1492            "{err}"
1493        );
1494        assert!(v4.check_against_dataset(&rank3, &i32_t, &ctx).is_err());
1495
1496        // One too many, and the scalar extent a chunked layout never fits.
1497        let rank1 = DataspaceMessage::simple(&[8]);
1498        assert!(v3.check_against_dataset(&rank1, &i32_t, &ctx).is_err());
1499        assert!(v3
1500            .check_against_dataset(&DataspaceMessage::scalar(), &i32_t, &ctx)
1501            .is_err());
1502
1503        // No chunks, no rank to disagree.
1504        let contiguous = DataLayoutMessage::contiguous_unallocated(96);
1505        assert!(contiguous
1506            .check_against_dataset(&rank3, &i32_t, &ctx)
1507            .is_ok());
1508        assert!(contiguous
1509            .check_against_dataset(&DataspaceMessage::scalar(), &i32_t, &ctx)
1510            .is_ok());
1511    }
1512
1513    /// One case per boundary of the compact size rule: exact passes; one
1514    /// element short or long fails; a scalar holds one element, a null
1515    /// dataspace none; the vlen size is the stored reference, not the
1516    /// default; an extent whose product overflows is refused, not wrapped.
1517    #[test]
1518    fn check_against_dataset_compact_size_boundaries() {
1519        use crate::format::messages::dataspace::DataspaceMessage;
1520        use crate::format::messages::datatype::DatatypeMessage;
1521        let ctx = FormatContext::default_v3();
1522        let i32_t = DatatypeMessage::i32_type();
1523        let d = DataspaceMessage::simple(&[3, 4]);
1524        let ok = DataLayoutMessage::compact(vec![0u8; 48]);
1525        assert!(ok.check_against_dataset(&d, &i32_t, &ctx).is_ok());
1526        let short = DataLayoutMessage::compact(vec![0u8; 44]);
1527        let err = short.check_against_dataset(&d, &i32_t, &ctx).unwrap_err();
1528        assert!(
1529            matches!(err, FormatError::InvalidData(ref s) if s.contains("44 bytes") && s.contains("need 48")),
1530            "{err}"
1531        );
1532        let long = DataLayoutMessage::compact(vec![0u8; 52]);
1533        assert!(long.check_against_dataset(&d, &i32_t, &ctx).is_err());
1534
1535        let one = DataLayoutMessage::compact(vec![0u8; 4]);
1536        assert!(one
1537            .check_against_dataset(&DataspaceMessage::scalar(), &i32_t, &ctx)
1538            .is_ok());
1539        assert!(one
1540            .check_against_dataset(&DataspaceMessage::null(), &i32_t, &ctx)
1541            .is_err());
1542        let none = DataLayoutMessage::compact(vec![]);
1543        assert!(none
1544            .check_against_dataset(&DataspaceMessage::null(), &i32_t, &ctx)
1545            .is_ok());
1546        assert!(none
1547            .check_against_dataset(&DataspaceMessage::simple(&[0, 5]), &i32_t, &ctx)
1548            .is_ok());
1549
1550        // A vlen element is stored as a reference of sizeof_addr + 8 bytes.
1551        let vlen = DatatypeMessage::vlen_string_ascii();
1552        let small_ctx = FormatContext {
1553            sizeof_addr: 4,
1554            sizeof_size: 4,
1555        };
1556        let refs = DataLayoutMessage::compact(vec![0u8; 2 * 12]);
1557        assert!(refs
1558            .check_against_dataset(&DataspaceMessage::simple(&[2]), &vlen, &small_ctx)
1559            .is_ok());
1560        assert!(refs
1561            .check_against_dataset(&DataspaceMessage::simple(&[2]), &vlen, &ctx)
1562            .is_err());
1563
1564        let huge = DataspaceMessage::simple(&[u64::MAX, 2]);
1565        assert!(ok.check_against_dataset(&huge, &i32_t, &ctx).is_err());
1566        let huge_bytes = DataspaceMessage::simple(&[u64::MAX / 2]);
1567        assert!(ok.check_against_dataset(&huge_bytes, &i32_t, &ctx).is_err());
1568    }
1569}