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