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 storage class and message version, for a message that has to name
259    /// which layout it is talking about.
260    pub fn describe(&self) -> &'static str {
261        match self {
262            Self::Contiguous { .. } => "contiguous",
263            Self::Compact { .. } => "compact (version 3)",
264            Self::ChunkedV3 { .. } => "chunked, version-1 B-tree index (layout version 3)",
265            Self::ChunkedV4 { .. } => "chunked (layout version 4 or 5)",
266            Self::Virtual { .. } => "virtual",
267        }
268    }
269
270    /// Contiguous layout with no data allocated yet.
271    pub fn contiguous_unallocated(size: u64) -> Self {
272        Self::Contiguous {
273            address: UNDEF_ADDR,
274            size,
275        }
276    }
277
278    /// Contiguous layout pointing to allocated data.
279    pub fn contiguous(address: u64, size: u64) -> Self {
280        Self::Contiguous { address, size }
281    }
282
283    /// Compact layout with inline data.
284    pub fn compact(data: Vec<u8>) -> Self {
285        Self::Compact { data }
286    }
287
288    /// Version 3 chunked layout indexed by a version-1 B-tree.
289    ///
290    /// `chunk_dims` must include the trailing element-size dimension.
291    pub fn chunked_v3_btree_v1(chunk_dims: Vec<u64>, b_tree_address: u64) -> Self {
292        Self::ChunkedV3 {
293            chunk_dims,
294            b_tree_address,
295        }
296    }
297
298    /// Version 4 chunked layout with extensible array index.
299    ///
300    /// `chunk_dims` should include the trailing element-size dimension.
301    /// For example, for a 2D dataset with chunk=(1,4) and element_size=8,
302    /// pass chunk_dims = [1, 4, 8].
303    pub fn chunked_v4_earray(
304        version: u8,
305        chunk_dims: Vec<u64>,
306        earray_params: EarrayParams,
307        index_address: u64,
308    ) -> Self {
309        Self::ChunkedV4 {
310            version,
311            flags: 0,
312            chunk_dims,
313            index_type: ChunkIndexType::ExtensibleArray,
314            earray_params: Some(earray_params),
315            farray_params: None,
316            bt2_params: None,
317            single_chunk_filter: None,
318            index_address,
319        }
320    }
321
322    /// Version 4 chunked layout with fixed array index.
323    ///
324    /// `chunk_dims` should include the trailing element-size dimension.
325    pub fn chunked_v4_farray(
326        version: u8,
327        chunk_dims: Vec<u64>,
328        farray_params: FixedArrayParams,
329        index_address: u64,
330    ) -> Self {
331        Self::ChunkedV4 {
332            version,
333            flags: 0,
334            chunk_dims,
335            index_type: ChunkIndexType::FixedArray,
336            earray_params: None,
337            farray_params: Some(farray_params),
338            bt2_params: None,
339            single_chunk_filter: None,
340            index_address,
341        }
342    }
343
344    /// Version 4 chunked layout with B-tree v2 index.
345    ///
346    /// `chunk_dims` should include the trailing element-size dimension.
347    pub fn chunked_v4_btree_v2(
348        version: u8,
349        chunk_dims: Vec<u64>,
350        bt2_params: Bt2Params,
351        index_address: u64,
352    ) -> Self {
353        Self::ChunkedV4 {
354            version,
355            flags: 0,
356            chunk_dims,
357            index_type: ChunkIndexType::BTreeV2,
358            earray_params: None,
359            farray_params: None,
360            bt2_params: Some(bt2_params),
361            single_chunk_filter: None,
362            index_address,
363        }
364    }
365
366    /// Version 4 chunked layout with the implicit index — no index structure
367    /// at all: `index_address` is the start of one contiguous run holding
368    /// every chunk of the maximum-extent grid in row-major order, so a
369    /// chunk's address is arithmetic (`H5D__none_idx_get_addr`, H5Dnone.c).
370    ///
371    /// `chunk_dims` should include the trailing element-size dimension.
372    pub fn chunked_v4_implicit(version: u8, chunk_dims: Vec<u64>, index_address: u64) -> Self {
373        Self::ChunkedV4 {
374            version,
375            flags: 0,
376            chunk_dims,
377            index_type: ChunkIndexType::Implicit,
378            earray_params: None,
379            farray_params: None,
380            bt2_params: None,
381            single_chunk_filter: None,
382            index_address,
383        }
384    }
385
386    /// Virtual dataset layout pointing at a global-heap mapping list.
387    pub fn virtual_layout(version: u8, heap_address: u64, heap_index: u32) -> Self {
388        Self::Virtual {
389            version,
390            heap_address,
391            heap_index,
392        }
393    }
394
395    /// Version 4 chunked layout with single-chunk index.
396    ///
397    /// `chunk_dims` should include the trailing element-size dimension.
398    pub fn chunked_v4_single(chunk_dims: Vec<u64>, index_address: u64) -> Self {
399        Self::ChunkedV4 {
400            version: VERSION_4,
401            flags: 0,
402            chunk_dims,
403            index_type: ChunkIndexType::SingleChunk,
404            earray_params: None,
405            farray_params: None,
406            bt2_params: None,
407            single_chunk_filter: None,
408            index_address,
409        }
410    }
411
412    /// Version 4 chunked layout with a *filtered* single-chunk index: the
413    /// "single index with filter" flag (`0x02`) is set and the chunk's
414    /// on-disk size and filter mask are carried inline
415    /// (`H5O_LAYOUT_CHUNK_SINGLE_INDEX_WITH_FILTER`, H5Dsingle.c).
416    ///
417    /// `chunk_dims` should include the trailing element-size dimension.
418    pub fn chunked_v4_single_filtered(
419        chunk_dims: Vec<u64>,
420        index_address: u64,
421        nbytes: u64,
422        filter_mask: u32,
423    ) -> Self {
424        Self::ChunkedV4 {
425            version: VERSION_4,
426            flags: 0x02,
427            chunk_dims,
428            index_type: ChunkIndexType::SingleChunk,
429            earray_params: None,
430            farray_params: None,
431            bt2_params: None,
432            single_chunk_filter: Some(SingleChunkFilter {
433                nbytes,
434                filter_mask,
435            }),
436            index_address,
437        }
438    }
439
440    // ------------------------------------------------------------------ encode
441
442    pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
443        match self {
444            Self::Contiguous { address, size } => {
445                let sa = ctx.sizeof_addr as usize;
446                let ss = ctx.sizeof_size as usize;
447                let mut buf = Vec::with_capacity(2 + sa + ss);
448                buf.push(VERSION_3);
449                buf.push(CLASS_CONTIGUOUS);
450                buf.extend_from_slice(&address.to_le_bytes()[..sa]);
451                buf.extend_from_slice(&size.to_le_bytes()[..ss]);
452                buf
453            }
454            Self::Compact { data } => {
455                let mut buf = Vec::with_capacity(2 + 2 + data.len());
456                buf.push(VERSION_3);
457                buf.push(CLASS_COMPACT);
458                buf.extend_from_slice(&(data.len() as u16).to_le_bytes());
459                buf.extend_from_slice(data);
460                buf
461            }
462            Self::ChunkedV3 {
463                chunk_dims,
464                b_tree_address,
465            } => {
466                let sa = ctx.sizeof_addr as usize;
467                let ndims = chunk_dims.len() as u8;
468                let mut buf = Vec::with_capacity(3 + sa + chunk_dims.len() * 4);
469                buf.push(VERSION_3);
470                buf.push(CLASS_CHUNKED);
471                buf.push(ndims);
472                buf.extend_from_slice(&b_tree_address.to_le_bytes()[..sa]);
473                // Dimension sizes are always 4 bytes each (UINT32ENCODE).
474                for &d in chunk_dims {
475                    buf.extend_from_slice(&(d as u32).to_le_bytes());
476                }
477                buf
478            }
479            Self::ChunkedV4 {
480                version,
481                flags,
482                chunk_dims,
483                index_type,
484                earray_params,
485                farray_params,
486                bt2_params,
487                single_chunk_filter,
488                index_address,
489            } => {
490                let sa = ctx.sizeof_addr as usize;
491                let ndims = chunk_dims.len() as u8;
492
493                // Compute enc_bytes_per_dim: minimum bytes to represent the
494                // max chunk dimension value.
495                let max_dim = chunk_dims.iter().copied().max().unwrap_or(1);
496                let enc_bytes = enc_bytes_for_value(max_dim);
497
498                debug_assert!(matches!(*version, VERSION_4 | VERSION_5));
499                let mut buf = Vec::with_capacity(64);
500                buf.push(*version);
501                buf.push(CLASS_CHUNKED);
502                buf.push(*flags);
503                buf.push(ndims);
504                buf.push(enc_bytes);
505
506                // Dimension sizes
507                for &d in chunk_dims {
508                    buf.extend_from_slice(&d.to_le_bytes()[..enc_bytes as usize]);
509                }
510
511                // Index type
512                buf.push(*index_type as u8);
513
514                // Index-type-specific parameters
515                match *index_type {
516                    ChunkIndexType::ExtensibleArray => {
517                        if let Some(ref params) = earray_params {
518                            buf.push(params.max_nelmts_bits);
519                            buf.push(params.idx_blk_elmts);
520                            buf.push(params.sup_blk_min_data_ptrs);
521                            buf.push(params.data_blk_min_elmts);
522                            buf.push(params.max_dblk_page_nelmts_bits);
523                        }
524                    }
525                    ChunkIndexType::FixedArray => {
526                        if let Some(ref params) = farray_params {
527                            buf.push(params.max_dblk_page_nelmts_bits);
528                        }
529                    }
530                    ChunkIndexType::BTreeV2 => {
531                        // node_size(4) + split_percent(1) + merge_percent(1),
532                        // the same geometry the B-tree header carries — the
533                        // message must agree with the BTHD it points at, so
534                        // a reopened foreign node size is preserved, not
535                        // stamped over with this writer's default.
536                        if let Some(ref params) = bt2_params {
537                            buf.extend_from_slice(&params.node_size.to_le_bytes());
538                            buf.push(params.split_percent);
539                            buf.push(params.merge_percent);
540                        }
541                    }
542                    // A filtered single chunk carries its on-disk size
543                    // (sizeof_size bytes) and 4-byte filter mask inline, before
544                    // the chunk address (H5Olayout.c). Only emit them when the
545                    // filtered flag (0x02) is set; an unfiltered single chunk
546                    // falls through to the no-extra-parameters arm below.
547                    ChunkIndexType::SingleChunk if *flags & 0x02 != 0 => {
548                        if let Some(scf) = single_chunk_filter {
549                            let ss = ctx.sizeof_size as usize;
550                            buf.extend_from_slice(&scf.nbytes.to_le_bytes()[..ss]);
551                            buf.extend_from_slice(&scf.filter_mask.to_le_bytes());
552                        }
553                    }
554                    // Implicit: no extra parameters.
555                    _ => {}
556                }
557
558                // Index address
559                buf.extend_from_slice(&index_address.to_le_bytes()[..sa]);
560
561                buf
562            }
563            Self::Virtual {
564                version,
565                heap_address,
566                heap_index,
567            } => {
568                let sa = ctx.sizeof_addr as usize;
569                debug_assert!(matches!(*version, VERSION_4 | VERSION_5));
570                let mut buf = Vec::with_capacity(2 + sa + 4);
571                buf.push(*version);
572                buf.push(CLASS_VIRTUAL);
573                buf.extend_from_slice(&heap_address.to_le_bytes()[..sa]);
574                buf.extend_from_slice(&heap_index.to_le_bytes());
575                buf
576            }
577        }
578    }
579
580    // ------------------------------------------------------------------ decode
581
582    pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
583        if buf.len() < 2 {
584            return Err(FormatError::BufferTooShort {
585                needed: 2,
586                available: buf.len(),
587            });
588        }
589
590        let version = buf[0];
591        let class = buf[1];
592
593        // libhdf5 validates the version once and then reads the body by
594        // storage class (`H5O__layout_decode`); only the chunked body differs
595        // between version 3 and versions 4/5. Enumerating (version, class)
596        // pairs instead made every version this decoder had not been taught
597        // about look like a bad version — which is how a perfectly ordinary
598        // contiguous dataset in a v1.10 file (layout version 4) came back as
599        // `InvalidVersion` and vanished from the catalog.
600        match version {
601            VERSION_1 | VERSION_2 => {
602                return Err(FormatError::UnsupportedFeature(format!(
603                    "data layout message version {version}"
604                )))
605            }
606            VERSION_3 | VERSION_4 | VERSION_5 => {}
607            v => return Err(FormatError::InvalidVersion(v)),
608        }
609
610        match class {
611            CLASS_CONTIGUOUS => {
612                let sa = ctx.sizeof_addr as usize;
613                let ss = ctx.sizeof_size as usize;
614                let mut pos = 2;
615                let needed = pos + sa + ss;
616                if buf.len() < needed {
617                    return Err(FormatError::BufferTooShort {
618                        needed,
619                        available: buf.len(),
620                    });
621                }
622                let address = read_addr(&buf[pos..], sa);
623                pos += sa;
624                let size = read_size(&buf[pos..], ss);
625                pos += ss;
626                Ok((Self::Contiguous { address, size }, pos))
627            }
628            CLASS_COMPACT => {
629                let mut pos = 2;
630                if buf.len() < pos + 2 {
631                    return Err(FormatError::BufferTooShort {
632                        needed: pos + 2,
633                        available: buf.len(),
634                    });
635                }
636                let compact_size = u16::from_le_bytes([buf[pos], buf[pos + 1]]) as usize;
637                pos += 2;
638                if buf.len() < pos + compact_size {
639                    return Err(FormatError::BufferTooShort {
640                        needed: pos + compact_size,
641                        available: buf.len(),
642                    });
643                }
644                let data = buf[pos..pos + compact_size].to_vec();
645                pos += compact_size;
646                Ok((Self::Compact { data }, pos))
647            }
648            CLASS_CHUNKED if version == VERSION_3 => {
649                // version(1) + class(1) + ndims(1) + b_tree_addr(sa)
650                // + ndims * 4-byte dimension sizes.
651                let sa = ctx.sizeof_addr as usize;
652                let mut pos = 2;
653                if buf.len() < pos + 1 {
654                    return Err(FormatError::BufferTooShort {
655                        needed: pos + 1,
656                        available: buf.len(),
657                    });
658                }
659                let ndims = buf[pos] as usize;
660                pos += 1;
661
662                // libhdf5 (H5Olayout.c) requires 2 <= ndims for chunked
663                // storage: the chunk rank plus the trailing element-size
664                // dimension. A zero or one is malformed.
665                if ndims < 2 {
666                    return Err(FormatError::InvalidData(format!(
667                        "chunked v3 layout dimensionality {ndims} is too small"
668                    )));
669                }
670
671                if buf.len() < pos + sa {
672                    return Err(FormatError::BufferTooShort {
673                        needed: pos + sa,
674                        available: buf.len(),
675                    });
676                }
677                let b_tree_address = read_addr(&buf[pos..], sa);
678                pos += sa;
679
680                let dim_data_len = ndims * 4;
681                if buf.len() < pos + dim_data_len {
682                    return Err(FormatError::BufferTooShort {
683                        needed: pos + dim_data_len,
684                        available: buf.len(),
685                    });
686                }
687                let mut chunk_dims = Vec::with_capacity(ndims);
688                for _ in 0..ndims {
689                    let d = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]])
690                        as u64;
691                    if d == 0 {
692                        return Err(FormatError::InvalidData(
693                            "chunked v3 layout has a zero chunk dimension".into(),
694                        ));
695                    }
696                    chunk_dims.push(d);
697                    pos += 4;
698                }
699
700                Ok((
701                    Self::ChunkedV3 {
702                        chunk_dims,
703                        b_tree_address,
704                    },
705                    pos,
706                ))
707            }
708            CLASS_CHUNKED => {
709                let sa = ctx.sizeof_addr as usize;
710                let mut pos = 2;
711
712                // flags(1) + ndims(1) + enc_bytes_per_dim(1)
713                if buf.len() < pos + 3 {
714                    return Err(FormatError::BufferTooShort {
715                        needed: pos + 3,
716                        available: buf.len(),
717                    });
718                }
719                let flags = buf[pos];
720                pos += 1;
721                let ndims = buf[pos] as usize;
722                pos += 1;
723                let enc_bytes = buf[pos] as usize;
724                pos += 1;
725
726                // libhdf5 (H5Olayout.c) requires 1 <= enc_bytes <= 8;
727                // 0 produces all-zero dims, > 8 panics read_size.
728                if !(1..=8).contains(&enc_bytes) {
729                    return Err(FormatError::InvalidData(format!(
730                        "chunked layout encoded dimension size {enc_bytes} is out of range"
731                    )));
732                }
733                // Chunked storage carries the chunk rank plus the trailing
734                // element-size dimension, so ndims is at least 2.
735                if ndims < 2 {
736                    return Err(FormatError::InvalidData(format!(
737                        "chunked v4 layout dimensionality {ndims} is too small"
738                    )));
739                }
740
741                // dim sizes
742                let dim_data_len = ndims * enc_bytes;
743                if buf.len() < pos + dim_data_len {
744                    return Err(FormatError::BufferTooShort {
745                        needed: pos + dim_data_len,
746                        available: buf.len(),
747                    });
748                }
749                let mut chunk_dims = Vec::with_capacity(ndims);
750                for _ in 0..ndims {
751                    let d = read_size(&buf[pos..], enc_bytes);
752                    if d == 0 {
753                        return Err(FormatError::InvalidData(
754                            "chunked v4 layout has a zero chunk dimension".into(),
755                        ));
756                    }
757                    chunk_dims.push(d);
758                    pos += enc_bytes;
759                }
760
761                // index type
762                if buf.len() < pos + 1 {
763                    return Err(FormatError::BufferTooShort {
764                        needed: pos + 1,
765                        available: buf.len(),
766                    });
767                }
768                let idx_type_raw = buf[pos];
769                pos += 1;
770                let index_type = ChunkIndexType::from_u8(idx_type_raw).ok_or_else(|| {
771                    FormatError::UnsupportedFeature(format!("chunk index type {}", idx_type_raw))
772                })?;
773
774                // Index-type-specific parameters
775                let mut earray_params = None;
776                let mut farray_params = None;
777                let mut bt2_params = None;
778                let mut single_chunk_filter = None;
779
780                match index_type {
781                    ChunkIndexType::ExtensibleArray => {
782                        if buf.len() < pos + 5 {
783                            return Err(FormatError::BufferTooShort {
784                                needed: pos + 5,
785                                available: buf.len(),
786                            });
787                        }
788                        let ep = EarrayParams {
789                            max_nelmts_bits: buf[pos],
790                            idx_blk_elmts: buf[pos + 1],
791                            sup_blk_min_data_ptrs: buf[pos + 2],
792                            data_blk_min_elmts: buf[pos + 3],
793                            max_dblk_page_nelmts_bits: buf[pos + 4],
794                        };
795                        // libhdf5 rejects a zero in any of these fields.
796                        if ep.max_nelmts_bits == 0
797                            || ep.idx_blk_elmts == 0
798                            || ep.sup_blk_min_data_ptrs == 0
799                            || ep.data_blk_min_elmts == 0
800                            || ep.max_dblk_page_nelmts_bits == 0
801                        {
802                            return Err(FormatError::InvalidData(
803                                "extensible-array layout parameter is zero".into(),
804                            ));
805                        }
806                        earray_params = Some(ep);
807                        pos += 5;
808                    }
809                    ChunkIndexType::FixedArray => {
810                        if buf.len() < pos + 1 {
811                            return Err(FormatError::BufferTooShort {
812                                needed: pos + 1,
813                                available: buf.len(),
814                            });
815                        }
816                        // NOTE: libhdf5 rejects max_dblk_page_nelmts_bits == 0,
817                        // but this crate's own Fixed Array writer currently
818                        // emits 0 (it does not page). Validating it here would
819                        // reject crate-written files; left until the FA writer
820                        // is made libhdf5-conformant.
821                        farray_params = Some(FixedArrayParams {
822                            max_dblk_page_nelmts_bits: buf[pos],
823                        });
824                        pos += 1;
825                    }
826                    ChunkIndexType::BTreeV2 => {
827                        // node_size(4) + split_percent(1) + merge_percent(1).
828                        // The v2 B-tree header carries authoritative copies;
829                        // retained so a rewritten object header re-emits the
830                        // creator's values, not this writer's defaults.
831                        if buf.len() < pos + 6 {
832                            return Err(FormatError::BufferTooShort {
833                                needed: pos + 6,
834                                available: buf.len(),
835                            });
836                        }
837                        bt2_params = Some(Bt2Params {
838                            node_size: u32::from_le_bytes([
839                                buf[pos],
840                                buf[pos + 1],
841                                buf[pos + 2],
842                                buf[pos + 3],
843                            ]),
844                            split_percent: buf[pos + 4],
845                            merge_percent: buf[pos + 5],
846                        });
847                        pos += 6;
848                    }
849                    // A single-chunk index whose "single index with
850                    // filter" flag (0x02) is set carries the filtered
851                    // chunk size (sizeof_size bytes) and a 4-byte filter
852                    // mask before the chunk address (H5Olayout.c). Retain
853                    // both: the reader needs the exact on-disk size and must
854                    // honor the per-chunk mask when reversing filters.
855                    ChunkIndexType::SingleChunk if flags & 0x02 != 0 => {
856                        let ss = ctx.sizeof_size as usize;
857                        let extra = ss + 4;
858                        if buf.len() < pos + extra {
859                            return Err(FormatError::BufferTooShort {
860                                needed: pos + extra,
861                                available: buf.len(),
862                            });
863                        }
864                        let nbytes = read_size(&buf[pos..], ss);
865                        pos += ss;
866                        let filter_mask = u32::from_le_bytes([
867                            buf[pos],
868                            buf[pos + 1],
869                            buf[pos + 2],
870                            buf[pos + 3],
871                        ]);
872                        pos += 4;
873                        single_chunk_filter = Some(SingleChunkFilter {
874                            nbytes,
875                            filter_mask,
876                        });
877                    }
878                    // Implicit, and single-chunk without the filter flag:
879                    // no extra parameters.
880                    _ => {}
881                }
882
883                // index address
884                if buf.len() < pos + sa {
885                    return Err(FormatError::BufferTooShort {
886                        needed: pos + sa,
887                        available: buf.len(),
888                    });
889                }
890                let index_address = read_addr(&buf[pos..], sa);
891                pos += sa;
892
893                Ok((
894                    Self::ChunkedV4 {
895                        version: buf[0],
896                        flags,
897                        chunk_dims,
898                        index_type,
899                        earray_params,
900                        farray_params,
901                        bt2_params,
902                        single_chunk_filter,
903                        index_address,
904                    },
905                    pos,
906                ))
907            }
908            CLASS_VIRTUAL => {
909                // libhdf5 (H5Olayout.c) rejects a virtual layout below
910                // version 4 outright ("invalid layout version with virtual
911                // layout") — the class did not exist before version 4, so a
912                // version-3 message can never legitimately carry it.
913                if version == VERSION_3 {
914                    return Err(FormatError::InvalidVersion(VERSION_3));
915                }
916                let sa = ctx.sizeof_addr as usize;
917                let mut pos = 2;
918                if buf.len() < pos + sa {
919                    return Err(FormatError::BufferTooShort {
920                        needed: pos + sa,
921                        available: buf.len(),
922                    });
923                }
924                let heap_address = read_addr(&buf[pos..], sa);
925                pos += sa;
926
927                if buf.len() < pos + 4 {
928                    return Err(FormatError::BufferTooShort {
929                        needed: pos + 4,
930                        available: buf.len(),
931                    });
932                }
933                let heap_index =
934                    u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
935                pos += 4;
936
937                Ok((
938                    Self::Virtual {
939                        version: buf[0],
940                        heap_address,
941                        heap_index,
942                    },
943                    pos,
944                ))
945            }
946            other => Err(FormatError::UnsupportedFeature(format!(
947                "data layout class {}",
948                other
949            ))),
950        }
951    }
952}
953
954// ========================================================================= helpers
955
956/// Compute the minimum number of bytes (1-8) needed to encode `v`.
957fn enc_bytes_for_value(v: u64) -> u8 {
958    if v == 0 {
959        return 1;
960    }
961    let bits_needed = 64 - v.leading_zeros(); // 1..=64
962    bits_needed.div_ceil(8) as u8
963}
964
965// ======================================================================= tests
966
967#[cfg(test)]
968mod tests {
969    use super::*;
970
971    fn ctx8() -> FormatContext {
972        FormatContext {
973            sizeof_addr: 8,
974            sizeof_size: 8,
975        }
976    }
977
978    fn ctx4() -> FormatContext {
979        FormatContext {
980            sizeof_addr: 4,
981            sizeof_size: 4,
982        }
983    }
984
985    #[test]
986    fn roundtrip_contiguous() {
987        let msg = DataLayoutMessage::contiguous(0x1000, 4096);
988        let encoded = msg.encode(&ctx8());
989        // 2 + 8 + 8 = 18
990        assert_eq!(encoded.len(), 18);
991        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
992        assert_eq!(consumed, 18);
993        assert_eq!(decoded, msg);
994    }
995
996    #[test]
997    fn roundtrip_contiguous_ctx4() {
998        let msg = DataLayoutMessage::contiguous(0x800, 256);
999        let encoded = msg.encode(&ctx4());
1000        // 2 + 4 + 4 = 10
1001        assert_eq!(encoded.len(), 10);
1002        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
1003        assert_eq!(consumed, 10);
1004        assert_eq!(decoded, msg);
1005    }
1006
1007    #[test]
1008    fn roundtrip_contiguous_unallocated() {
1009        let msg = DataLayoutMessage::contiguous_unallocated(1024);
1010        let encoded = msg.encode(&ctx8());
1011        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1012        assert_eq!(decoded, msg);
1013        match decoded {
1014            DataLayoutMessage::Contiguous { address, size } => {
1015                assert_eq!(address, UNDEF_ADDR);
1016                assert_eq!(size, 1024);
1017            }
1018            _ => panic!("expected Contiguous"),
1019        }
1020    }
1021
1022    #[test]
1023    fn roundtrip_contiguous_undef_ctx4() {
1024        let msg = DataLayoutMessage::contiguous_unallocated(512);
1025        let encoded = msg.encode(&ctx4());
1026        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
1027        match decoded {
1028            DataLayoutMessage::Contiguous { address, .. } => {
1029                assert_eq!(address, UNDEF_ADDR);
1030            }
1031            _ => panic!("expected Contiguous"),
1032        }
1033    }
1034
1035    #[test]
1036    fn roundtrip_compact() {
1037        let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
1038        let msg = DataLayoutMessage::compact(data.clone());
1039        let encoded = msg.encode(&ctx8());
1040        // 2 + 2 + 8 = 12
1041        assert_eq!(encoded.len(), 12);
1042        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1043        assert_eq!(consumed, 12);
1044        assert_eq!(decoded, msg);
1045    }
1046
1047    #[test]
1048    fn roundtrip_compact_empty() {
1049        let msg = DataLayoutMessage::compact(vec![]);
1050        let encoded = msg.encode(&ctx8());
1051        assert_eq!(encoded.len(), 4); // 2 + 2 + 0
1052        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1053        assert_eq!(consumed, 4);
1054        assert_eq!(decoded, msg);
1055    }
1056
1057    /// Versions 1 and 2 are legal layout versions libhdf5 still reads, so
1058    /// they are reported as an unsupported feature (which the catalog surfaces
1059    /// by name) rather than as a bad version.
1060    #[test]
1061    fn decode_legacy_version_is_unsupported_not_invalid() {
1062        for version in [1u8, 2] {
1063            let mut buf = vec![version, 1];
1064            buf.extend_from_slice(&[0u8; 16]);
1065            let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1066            match err {
1067                FormatError::UnsupportedFeature(ref s) => {
1068                    assert!(s.contains(&version.to_string()), "{s}")
1069                }
1070                other => panic!("unexpected error for version {version}: {other:?}"),
1071            }
1072        }
1073    }
1074
1075    #[test]
1076    fn decode_bad_version() {
1077        for version in [0u8, 6, 255] {
1078            let mut buf = vec![version, 1];
1079            buf.extend_from_slice(&[0u8; 16]);
1080            let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1081            match err {
1082                FormatError::InvalidVersion(v) if v == version => {}
1083                other => panic!("unexpected error for version {version}: {other:?}"),
1084            }
1085        }
1086    }
1087
1088    /// The bug this guards: h5py writing under `libver=("v110","v110")` emits
1089    /// a *version 4* contiguous layout message whose body is byte-identical to
1090    /// the version-3 one. Rejecting it dropped the dataset from the catalog
1091    /// entirely, while a chunked dataset in the same file listed fine.
1092    #[test]
1093    fn decode_contiguous_and_compact_at_every_modern_version() {
1094        for version in [3u8, 4, 5] {
1095            let mut contig = vec![version, CLASS_CONTIGUOUS];
1096            contig.extend_from_slice(&0x800u64.to_le_bytes());
1097            contig.extend_from_slice(&64u64.to_le_bytes());
1098            let (decoded, consumed) = DataLayoutMessage::decode(&contig, &ctx8()).unwrap();
1099            assert_eq!(consumed, contig.len());
1100            assert_eq!(
1101                decoded,
1102                DataLayoutMessage::Contiguous {
1103                    address: 0x800,
1104                    size: 64
1105                }
1106            );
1107
1108            let payload = [1u8, 2, 3, 4];
1109            let mut compact = vec![version, CLASS_COMPACT];
1110            compact.extend_from_slice(&(payload.len() as u16).to_le_bytes());
1111            compact.extend_from_slice(&payload);
1112            let (decoded, consumed) = DataLayoutMessage::decode(&compact, &ctx8()).unwrap();
1113            assert_eq!(consumed, compact.len());
1114            assert_eq!(
1115                decoded,
1116                DataLayoutMessage::Compact {
1117                    data: payload.to_vec()
1118                }
1119            );
1120        }
1121    }
1122
1123    #[test]
1124    fn decode_unsupported_class() {
1125        let buf = [3u8, 4]; // class 4 = unknown (0-3 are all defined)
1126        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1127        match err {
1128            FormatError::UnsupportedFeature(_) => {}
1129            other => panic!("unexpected error: {:?}", other),
1130        }
1131    }
1132
1133    #[test]
1134    fn decode_buffer_too_short() {
1135        let buf = [3u8];
1136        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1137        match err {
1138            FormatError::BufferTooShort { .. } => {}
1139            other => panic!("unexpected error: {:?}", other),
1140        }
1141    }
1142
1143    #[test]
1144    fn decode_contiguous_truncated() {
1145        // version=3, class=1, but not enough bytes for address+size
1146        let buf = [3u8, 1, 0, 0];
1147        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1148        match err {
1149            FormatError::BufferTooShort { .. } => {}
1150            other => panic!("unexpected error: {:?}", other),
1151        }
1152    }
1153
1154    #[test]
1155    fn version_and_class_bytes() {
1156        let encoded = DataLayoutMessage::contiguous(0, 0).encode(&ctx8());
1157        assert_eq!(encoded[0], 3);
1158        assert_eq!(encoded[1], 1);
1159
1160        let encoded = DataLayoutMessage::compact(vec![]).encode(&ctx8());
1161        assert_eq!(encoded[0], 3);
1162        assert_eq!(encoded[1], 0);
1163    }
1164
1165    #[test]
1166    fn roundtrip_chunked_v4_earray() {
1167        let params = EarrayParams::default_params();
1168        let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 256, 256], params, 0x2000);
1169        let encoded = msg.encode(&ctx8());
1170        assert_eq!(encoded[0], 4); // version 4
1171        assert_eq!(encoded[1], 2); // class chunked
1172        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1173        assert_eq!(consumed, encoded.len());
1174        assert_eq!(decoded, msg);
1175    }
1176
1177    #[test]
1178    fn roundtrip_chunked_v4_earray_ctx4() {
1179        let params = EarrayParams::default_params();
1180        let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 128], params, 0x1000);
1181        let encoded = msg.encode(&ctx4());
1182        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
1183        assert_eq!(consumed, encoded.len());
1184        assert_eq!(decoded, msg);
1185    }
1186
1187    /// A version-5 layout differs from v4 only in the version byte; the body
1188    /// encodes identically and the version must survive the round trip (a
1189    /// reopen that dropped it would silently downgrade the file to v4 while
1190    /// its filtered index keeps 8-byte size fields).
1191    #[test]
1192    fn roundtrip_chunked_v5_earray() {
1193        let params = EarrayParams::default_params();
1194        let v5 = DataLayoutMessage::chunked_v4_earray(5, vec![1, 256, 256], params.clone(), 0x2000);
1195        let encoded = v5.encode(&ctx8());
1196        assert_eq!(encoded[0], 5); // version 5
1197        assert_eq!(encoded[1], 2); // class chunked
1198        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1199        assert_eq!(consumed, encoded.len());
1200        assert_eq!(decoded, v5);
1201
1202        // Same message at v4: only byte 0 differs.
1203        let v4 = DataLayoutMessage::chunked_v4_earray(4, vec![1, 256, 256], params, 0x2000);
1204        let encoded_v4 = v4.encode(&ctx8());
1205        assert_eq!(encoded[1..], encoded_v4[1..]);
1206    }
1207
1208    #[test]
1209    fn roundtrip_chunked_v4_single() {
1210        let msg = DataLayoutMessage::chunked_v4_single(vec![100, 200], 0x3000);
1211        let encoded = msg.encode(&ctx8());
1212        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1213        assert_eq!(consumed, encoded.len());
1214        assert_eq!(decoded, msg);
1215    }
1216
1217    /// The BTreeV2 parameters (node size, split/merge) round-trip through
1218    /// the message instead of being skipped on decode and re-stamped with
1219    /// defaults on encode — a rewritten object header must agree with the
1220    /// BTHD it points at.
1221    #[test]
1222    fn roundtrip_chunked_v4_btree_v2_params() {
1223        for ctx in [ctx8(), ctx4()] {
1224            let msg = DataLayoutMessage::chunked_v4_btree_v2(
1225                4,
1226                vec![2, 2, 8],
1227                Bt2Params {
1228                    node_size: 512,
1229                    split_percent: 90,
1230                    merge_percent: 30,
1231                },
1232                0x2000,
1233            );
1234            let encoded = msg.encode(&ctx);
1235            let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx).unwrap();
1236            assert_eq!(consumed, encoded.len());
1237            assert_eq!(decoded, msg);
1238        }
1239    }
1240
1241    /// A filtered single-chunk layout (flag `0x02`) carries the chunk's
1242    /// on-disk size and per-chunk filter mask inline. Decode must retain both
1243    /// (not discard them), and encode↔decode must round-trip — including the
1244    /// nonzero mask the reader needs to skip a filter.
1245    #[test]
1246    fn roundtrip_chunked_v4_single_filtered() {
1247        for ctx in [ctx8(), ctx4()] {
1248            let msg = DataLayoutMessage::ChunkedV4 {
1249                version: 4,
1250                flags: 0x02,
1251                chunk_dims: vec![100, 200, 4],
1252                index_type: ChunkIndexType::SingleChunk,
1253                earray_params: None,
1254                farray_params: None,
1255                bt2_params: None,
1256                single_chunk_filter: Some(SingleChunkFilter {
1257                    nbytes: 12345,
1258                    filter_mask: 0b101,
1259                }),
1260                index_address: 0x3000,
1261            };
1262            let encoded = msg.encode(&ctx);
1263            let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx).unwrap();
1264            assert_eq!(consumed, encoded.len());
1265            assert_eq!(decoded, msg);
1266            // The decoded layout exposes the retained size and mask.
1267            match decoded {
1268                DataLayoutMessage::ChunkedV4 {
1269                    single_chunk_filter: Some(scf),
1270                    ..
1271                } => {
1272                    assert_eq!(scf.nbytes, 12345);
1273                    assert_eq!(scf.filter_mask, 0b101);
1274                }
1275                other => panic!("expected filtered single-chunk layout, got {other:?}"),
1276            }
1277        }
1278    }
1279
1280    #[test]
1281    fn chunked_v4_enc_bytes() {
1282        // chunk dims [1, 256, 256]: max=256, needs 2 bytes
1283        let params = EarrayParams::default_params();
1284        let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 256, 256], params, 0x2000);
1285        let encoded = msg.encode(&ctx8());
1286        // version(1) + class(1) + flags(1) + ndims(1) + enc_bytes(1)
1287        // + 3*2 dim bytes + index_type(1) + 5 earray params + 8 addr = 25
1288        assert_eq!(encoded.len(), 25);
1289        assert_eq!(encoded[4], 2); // enc_bytes_per_dim = 2
1290    }
1291
1292    #[test]
1293    fn roundtrip_chunked_v3_btree_v1() {
1294        // 1-D dataset, chunk=(8), element_size=4 -> chunk_dims=[8, 4].
1295        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![8, 4], 0x1234);
1296        let encoded = msg.encode(&ctx8());
1297        // version(1) + class(1) + ndims(1) + addr(8) + 2*4 dims = 19
1298        assert_eq!(encoded.len(), 19);
1299        assert_eq!(encoded[0], 3);
1300        assert_eq!(encoded[1], 2);
1301        assert_eq!(encoded[2], 2); // ndims
1302        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1303        assert_eq!(consumed, encoded.len());
1304        assert_eq!(decoded, msg);
1305    }
1306
1307    #[test]
1308    fn roundtrip_chunked_v3_btree_v1_2d_ctx4() {
1309        // 2-D dataset, chunk=(2,3), element_size=8 -> chunk_dims=[2, 3, 8].
1310        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![2, 3, 8], 0x800);
1311        let encoded = msg.encode(&ctx4());
1312        // version(1) + class(1) + ndims(1) + addr(4) + 3*4 dims = 19
1313        assert_eq!(encoded.len(), 19);
1314        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
1315        assert_eq!(consumed, encoded.len());
1316        assert_eq!(decoded, msg);
1317    }
1318
1319    #[test]
1320    fn chunked_v3_undef_btree_addr() {
1321        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![16, 4], UNDEF_ADDR);
1322        let encoded = msg.encode(&ctx8());
1323        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1324        match decoded {
1325            DataLayoutMessage::ChunkedV3 { b_tree_address, .. } => {
1326                assert_eq!(b_tree_address, UNDEF_ADDR);
1327            }
1328            _ => panic!("expected ChunkedV3"),
1329        }
1330    }
1331
1332    #[test]
1333    fn chunked_v3_rejects_ndims_too_small() {
1334        // ndims = 1 is malformed for chunked storage.
1335        let buf = [3u8, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
1336        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1337        assert!(matches!(err, FormatError::InvalidData(_)));
1338    }
1339
1340    #[test]
1341    fn chunked_v3_rejects_zero_dim() {
1342        // ndims=2, addr=0, dims=[0, 4] -> zero chunk dimension.
1343        let mut buf = vec![3u8, 2, 2];
1344        buf.extend_from_slice(&0u64.to_le_bytes()); // addr
1345        buf.extend_from_slice(&0u32.to_le_bytes()); // dim 0 == 0
1346        buf.extend_from_slice(&4u32.to_le_bytes()); // dim 1
1347        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1348        assert!(matches!(err, FormatError::InvalidData(_)));
1349    }
1350
1351    #[test]
1352    fn chunked_v3_truncated() {
1353        // version=3, class=2, ndims=2, but no room for addr/dims.
1354        let buf = [3u8, 2, 2];
1355        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1356        assert!(matches!(err, FormatError::BufferTooShort { .. }));
1357    }
1358
1359    #[test]
1360    fn roundtrip_virtual_layout() {
1361        for ctx in [ctx8(), ctx4()] {
1362            for version in [4u8, 5u8] {
1363                let msg = DataLayoutMessage::virtual_layout(version, 0x5000, 3);
1364                let encoded = msg.encode(&ctx);
1365                assert_eq!(encoded[0], version);
1366                assert_eq!(encoded[1], CLASS_VIRTUAL);
1367                let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx).unwrap();
1368                assert_eq!(consumed, encoded.len());
1369                assert_eq!(decoded, msg);
1370            }
1371        }
1372    }
1373
1374    #[test]
1375    fn virtual_layout_undefined_heap_address() {
1376        // A virtual dataset created but never given any mappings: no heap
1377        // object exists yet, so the address is UNDEF and the index is 0.
1378        let msg = DataLayoutMessage::virtual_layout(4, UNDEF_ADDR, 0);
1379        let encoded = msg.encode(&ctx8());
1380        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
1381        match decoded {
1382            DataLayoutMessage::Virtual {
1383                heap_address,
1384                heap_index,
1385                ..
1386            } => {
1387                assert_eq!(heap_address, UNDEF_ADDR);
1388                assert_eq!(heap_index, 0);
1389            }
1390            other => panic!("expected Virtual, got {other:?}"),
1391        }
1392    }
1393
1394    /// libhdf5 rejects a virtual layout below version 4 outright — the
1395    /// class did not exist before version 4 (H5Olayout.c: "invalid layout
1396    /// version with virtual layout").
1397    #[test]
1398    fn virtual_layout_rejects_version_3() {
1399        let buf = [VERSION_3, CLASS_VIRTUAL];
1400        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
1401        assert!(matches!(err, FormatError::InvalidVersion(VERSION_3)));
1402    }
1403
1404    #[test]
1405    fn chunked_v4_large_dims() {
1406        // Large dims requiring 4 bytes each
1407        let params = EarrayParams::default_params();
1408        let msg = DataLayoutMessage::chunked_v4_earray(4, vec![1, 65536], params, 0x4000);
1409        let encoded = msg.encode(&ctx8());
1410        assert_eq!(encoded[4], 3); // enc_bytes_per_dim = 3 (65536 = 0x10000, needs 3 bytes)
1411    }
1412}