Skip to main content

rust_hdf5/format/messages/
data_layout.rs

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