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 v2 B-tree header carries the authoritative
360                        // copies; readers consult those, so a valid default
361                        // here suffices.
362                        buf.extend_from_slice(&2048u32.to_le_bytes());
363                        buf.push(100);
364                        buf.push(40);
365                    }
366                    ChunkIndexType::SingleChunk => {
367                        // A filtered single chunk carries its on-disk size
368                        // (sizeof_size bytes) and 4-byte filter mask inline,
369                        // before the chunk address (H5Olayout.c). Only emit
370                        // them when the filtered flag is set; otherwise the
371                        // layout has no per-index parameters.
372                        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                    }
380                    // Implicit: no extra parameters.
381                    _ => {}
382                }
383
384                // Index address
385                buf.extend_from_slice(&index_address.to_le_bytes()[..sa]);
386
387                buf
388            }
389        }
390    }
391
392    // ------------------------------------------------------------------ decode
393
394    pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
395        if buf.len() < 2 {
396            return Err(FormatError::BufferTooShort {
397                needed: 2,
398                available: buf.len(),
399            });
400        }
401
402        let version = buf[0];
403        let class = buf[1];
404
405        match (version, class) {
406            (VERSION_3, CLASS_CONTIGUOUS) => {
407                let sa = ctx.sizeof_addr as usize;
408                let ss = ctx.sizeof_size as usize;
409                let mut pos = 2;
410                let needed = pos + sa + ss;
411                if buf.len() < needed {
412                    return Err(FormatError::BufferTooShort {
413                        needed,
414                        available: buf.len(),
415                    });
416                }
417                let address = read_addr(&buf[pos..], sa);
418                pos += sa;
419                let size = read_size(&buf[pos..], ss);
420                pos += ss;
421                Ok((Self::Contiguous { address, size }, pos))
422            }
423            (VERSION_3, CLASS_COMPACT) => {
424                let mut pos = 2;
425                if buf.len() < pos + 2 {
426                    return Err(FormatError::BufferTooShort {
427                        needed: pos + 2,
428                        available: buf.len(),
429                    });
430                }
431                let compact_size = u16::from_le_bytes([buf[pos], buf[pos + 1]]) as usize;
432                pos += 2;
433                if buf.len() < pos + compact_size {
434                    return Err(FormatError::BufferTooShort {
435                        needed: pos + compact_size,
436                        available: buf.len(),
437                    });
438                }
439                let data = buf[pos..pos + compact_size].to_vec();
440                pos += compact_size;
441                Ok((Self::Compact { data }, pos))
442            }
443            (VERSION_3, CLASS_CHUNKED) => {
444                // version(1) + class(1) + ndims(1) + b_tree_addr(sa)
445                // + ndims * 4-byte dimension sizes.
446                let sa = ctx.sizeof_addr as usize;
447                let mut pos = 2;
448                if buf.len() < pos + 1 {
449                    return Err(FormatError::BufferTooShort {
450                        needed: pos + 1,
451                        available: buf.len(),
452                    });
453                }
454                let ndims = buf[pos] as usize;
455                pos += 1;
456
457                // libhdf5 (H5Olayout.c) requires 2 <= ndims for chunked
458                // storage: the chunk rank plus the trailing element-size
459                // dimension. A zero or one is malformed.
460                if ndims < 2 {
461                    return Err(FormatError::InvalidData(format!(
462                        "chunked v3 layout dimensionality {ndims} is too small"
463                    )));
464                }
465
466                if buf.len() < pos + sa {
467                    return Err(FormatError::BufferTooShort {
468                        needed: pos + sa,
469                        available: buf.len(),
470                    });
471                }
472                let b_tree_address = read_addr(&buf[pos..], sa);
473                pos += sa;
474
475                let dim_data_len = ndims * 4;
476                if buf.len() < pos + dim_data_len {
477                    return Err(FormatError::BufferTooShort {
478                        needed: pos + dim_data_len,
479                        available: buf.len(),
480                    });
481                }
482                let mut chunk_dims = Vec::with_capacity(ndims);
483                for _ in 0..ndims {
484                    let d = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]])
485                        as u64;
486                    if d == 0 {
487                        return Err(FormatError::InvalidData(
488                            "chunked v3 layout has a zero chunk dimension".into(),
489                        ));
490                    }
491                    chunk_dims.push(d);
492                    pos += 4;
493                }
494
495                Ok((
496                    Self::ChunkedV3 {
497                        chunk_dims,
498                        b_tree_address,
499                    },
500                    pos,
501                ))
502            }
503            (VERSION_4 | VERSION_5, CLASS_CHUNKED) => {
504                let sa = ctx.sizeof_addr as usize;
505                let mut pos = 2;
506
507                // flags(1) + ndims(1) + enc_bytes_per_dim(1)
508                if buf.len() < pos + 3 {
509                    return Err(FormatError::BufferTooShort {
510                        needed: pos + 3,
511                        available: buf.len(),
512                    });
513                }
514                let flags = buf[pos];
515                pos += 1;
516                let ndims = buf[pos] as usize;
517                pos += 1;
518                let enc_bytes = buf[pos] as usize;
519                pos += 1;
520
521                // libhdf5 (H5Olayout.c) requires 1 <= enc_bytes <= 8;
522                // 0 produces all-zero dims, > 8 panics read_size.
523                if !(1..=8).contains(&enc_bytes) {
524                    return Err(FormatError::InvalidData(format!(
525                        "chunked layout encoded dimension size {enc_bytes} is out of range"
526                    )));
527                }
528                // Chunked storage carries the chunk rank plus the trailing
529                // element-size dimension, so ndims is at least 2.
530                if ndims < 2 {
531                    return Err(FormatError::InvalidData(format!(
532                        "chunked v4 layout dimensionality {ndims} is too small"
533                    )));
534                }
535
536                // dim sizes
537                let dim_data_len = ndims * enc_bytes;
538                if buf.len() < pos + dim_data_len {
539                    return Err(FormatError::BufferTooShort {
540                        needed: pos + dim_data_len,
541                        available: buf.len(),
542                    });
543                }
544                let mut chunk_dims = Vec::with_capacity(ndims);
545                for _ in 0..ndims {
546                    let d = read_size(&buf[pos..], enc_bytes);
547                    if d == 0 {
548                        return Err(FormatError::InvalidData(
549                            "chunked v4 layout has a zero chunk dimension".into(),
550                        ));
551                    }
552                    chunk_dims.push(d);
553                    pos += enc_bytes;
554                }
555
556                // index type
557                if buf.len() < pos + 1 {
558                    return Err(FormatError::BufferTooShort {
559                        needed: pos + 1,
560                        available: buf.len(),
561                    });
562                }
563                let idx_type_raw = buf[pos];
564                pos += 1;
565                let index_type = ChunkIndexType::from_u8(idx_type_raw).ok_or_else(|| {
566                    FormatError::UnsupportedFeature(format!("chunk index type {}", idx_type_raw))
567                })?;
568
569                // Index-type-specific parameters
570                let mut earray_params = None;
571                let mut farray_params = None;
572                let mut single_chunk_filter = None;
573
574                match index_type {
575                    ChunkIndexType::ExtensibleArray => {
576                        if buf.len() < pos + 5 {
577                            return Err(FormatError::BufferTooShort {
578                                needed: pos + 5,
579                                available: buf.len(),
580                            });
581                        }
582                        let ep = EarrayParams {
583                            max_nelmts_bits: buf[pos],
584                            idx_blk_elmts: buf[pos + 1],
585                            sup_blk_min_data_ptrs: buf[pos + 2],
586                            data_blk_min_elmts: buf[pos + 3],
587                            max_dblk_page_nelmts_bits: buf[pos + 4],
588                        };
589                        // libhdf5 rejects a zero in any of these fields.
590                        if ep.max_nelmts_bits == 0
591                            || ep.idx_blk_elmts == 0
592                            || ep.sup_blk_min_data_ptrs == 0
593                            || ep.data_blk_min_elmts == 0
594                            || ep.max_dblk_page_nelmts_bits == 0
595                        {
596                            return Err(FormatError::InvalidData(
597                                "extensible-array layout parameter is zero".into(),
598                            ));
599                        }
600                        earray_params = Some(ep);
601                        pos += 5;
602                    }
603                    ChunkIndexType::FixedArray => {
604                        if buf.len() < pos + 1 {
605                            return Err(FormatError::BufferTooShort {
606                                needed: pos + 1,
607                                available: buf.len(),
608                            });
609                        }
610                        // NOTE: libhdf5 rejects max_dblk_page_nelmts_bits == 0,
611                        // but this crate's own Fixed Array writer currently
612                        // emits 0 (it does not page). Validating it here would
613                        // reject crate-written files; left until the FA writer
614                        // is made libhdf5-conformant.
615                        farray_params = Some(FixedArrayParams {
616                            max_dblk_page_nelmts_bits: buf[pos],
617                        });
618                        pos += 1;
619                    }
620                    ChunkIndexType::BTreeV2 => {
621                        // node_size(4) + split_percent(1) + merge_percent(1).
622                        // The v2 B-tree header carries authoritative copies,
623                        // so the reader only needs to skip these.
624                        if buf.len() < pos + 6 {
625                            return Err(FormatError::BufferTooShort {
626                                needed: pos + 6,
627                                available: buf.len(),
628                            });
629                        }
630                        pos += 6;
631                    }
632                    // A single-chunk index whose "single index with
633                    // filter" flag (0x02) is set carries the filtered
634                    // chunk size (sizeof_size bytes) and a 4-byte filter
635                    // mask before the chunk address (H5Olayout.c). Retain
636                    // both: the reader needs the exact on-disk size and must
637                    // honor the per-chunk mask when reversing filters.
638                    ChunkIndexType::SingleChunk if flags & 0x02 != 0 => {
639                        let ss = ctx.sizeof_size as usize;
640                        let extra = ss + 4;
641                        if buf.len() < pos + extra {
642                            return Err(FormatError::BufferTooShort {
643                                needed: pos + extra,
644                                available: buf.len(),
645                            });
646                        }
647                        let nbytes = read_size(&buf[pos..], ss);
648                        pos += ss;
649                        let filter_mask = u32::from_le_bytes([
650                            buf[pos],
651                            buf[pos + 1],
652                            buf[pos + 2],
653                            buf[pos + 3],
654                        ]);
655                        pos += 4;
656                        single_chunk_filter = Some(SingleChunkFilter {
657                            nbytes,
658                            filter_mask,
659                        });
660                    }
661                    // Implicit, and single-chunk without the filter flag:
662                    // no extra parameters.
663                    _ => {}
664                }
665
666                // index address
667                if buf.len() < pos + sa {
668                    return Err(FormatError::BufferTooShort {
669                        needed: pos + sa,
670                        available: buf.len(),
671                    });
672                }
673                let index_address = read_addr(&buf[pos..], sa);
674                pos += sa;
675
676                Ok((
677                    Self::ChunkedV4 {
678                        flags,
679                        chunk_dims,
680                        index_type,
681                        earray_params,
682                        farray_params,
683                        single_chunk_filter,
684                        index_address,
685                    },
686                    pos,
687                ))
688            }
689            (VERSION_3, other) => Err(FormatError::UnsupportedFeature(format!(
690                "data layout class {}",
691                other
692            ))),
693            (v, _) => Err(FormatError::InvalidVersion(v)),
694        }
695    }
696}
697
698// ========================================================================= helpers
699
700/// Compute the minimum number of bytes (1-8) needed to encode `v`.
701fn enc_bytes_for_value(v: u64) -> u8 {
702    if v == 0 {
703        return 1;
704    }
705    let bits_needed = 64 - v.leading_zeros(); // 1..=64
706    bits_needed.div_ceil(8) as u8
707}
708
709// ======================================================================= tests
710
711#[cfg(test)]
712mod tests {
713    use super::*;
714
715    fn ctx8() -> FormatContext {
716        FormatContext {
717            sizeof_addr: 8,
718            sizeof_size: 8,
719        }
720    }
721
722    fn ctx4() -> FormatContext {
723        FormatContext {
724            sizeof_addr: 4,
725            sizeof_size: 4,
726        }
727    }
728
729    #[test]
730    fn roundtrip_contiguous() {
731        let msg = DataLayoutMessage::contiguous(0x1000, 4096);
732        let encoded = msg.encode(&ctx8());
733        // 2 + 8 + 8 = 18
734        assert_eq!(encoded.len(), 18);
735        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
736        assert_eq!(consumed, 18);
737        assert_eq!(decoded, msg);
738    }
739
740    #[test]
741    fn roundtrip_contiguous_ctx4() {
742        let msg = DataLayoutMessage::contiguous(0x800, 256);
743        let encoded = msg.encode(&ctx4());
744        // 2 + 4 + 4 = 10
745        assert_eq!(encoded.len(), 10);
746        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
747        assert_eq!(consumed, 10);
748        assert_eq!(decoded, msg);
749    }
750
751    #[test]
752    fn roundtrip_contiguous_unallocated() {
753        let msg = DataLayoutMessage::contiguous_unallocated(1024);
754        let encoded = msg.encode(&ctx8());
755        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
756        assert_eq!(decoded, msg);
757        match decoded {
758            DataLayoutMessage::Contiguous { address, size } => {
759                assert_eq!(address, UNDEF_ADDR);
760                assert_eq!(size, 1024);
761            }
762            _ => panic!("expected Contiguous"),
763        }
764    }
765
766    #[test]
767    fn roundtrip_contiguous_undef_ctx4() {
768        let msg = DataLayoutMessage::contiguous_unallocated(512);
769        let encoded = msg.encode(&ctx4());
770        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
771        match decoded {
772            DataLayoutMessage::Contiguous { address, .. } => {
773                assert_eq!(address, UNDEF_ADDR);
774            }
775            _ => panic!("expected Contiguous"),
776        }
777    }
778
779    #[test]
780    fn roundtrip_compact() {
781        let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
782        let msg = DataLayoutMessage::compact(data.clone());
783        let encoded = msg.encode(&ctx8());
784        // 2 + 2 + 8 = 12
785        assert_eq!(encoded.len(), 12);
786        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
787        assert_eq!(consumed, 12);
788        assert_eq!(decoded, msg);
789    }
790
791    #[test]
792    fn roundtrip_compact_empty() {
793        let msg = DataLayoutMessage::compact(vec![]);
794        let encoded = msg.encode(&ctx8());
795        assert_eq!(encoded.len(), 4); // 2 + 2 + 0
796        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
797        assert_eq!(consumed, 4);
798        assert_eq!(decoded, msg);
799    }
800
801    #[test]
802    fn decode_bad_version() {
803        let buf = [2u8, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
804        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
805        match err {
806            FormatError::InvalidVersion(2) => {}
807            other => panic!("unexpected error: {:?}", other),
808        }
809    }
810
811    #[test]
812    fn decode_unsupported_class() {
813        let buf = [3u8, 3]; // class 3 = unknown
814        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
815        match err {
816            FormatError::UnsupportedFeature(_) => {}
817            other => panic!("unexpected error: {:?}", other),
818        }
819    }
820
821    #[test]
822    fn decode_buffer_too_short() {
823        let buf = [3u8];
824        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
825        match err {
826            FormatError::BufferTooShort { .. } => {}
827            other => panic!("unexpected error: {:?}", other),
828        }
829    }
830
831    #[test]
832    fn decode_contiguous_truncated() {
833        // version=3, class=1, but not enough bytes for address+size
834        let buf = [3u8, 1, 0, 0];
835        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
836        match err {
837            FormatError::BufferTooShort { .. } => {}
838            other => panic!("unexpected error: {:?}", other),
839        }
840    }
841
842    #[test]
843    fn version_and_class_bytes() {
844        let encoded = DataLayoutMessage::contiguous(0, 0).encode(&ctx8());
845        assert_eq!(encoded[0], 3);
846        assert_eq!(encoded[1], 1);
847
848        let encoded = DataLayoutMessage::compact(vec![]).encode(&ctx8());
849        assert_eq!(encoded[0], 3);
850        assert_eq!(encoded[1], 0);
851    }
852
853    #[test]
854    fn roundtrip_chunked_v4_earray() {
855        let params = EarrayParams::default_params();
856        let msg = DataLayoutMessage::chunked_v4_earray(vec![1, 256, 256], params, 0x2000);
857        let encoded = msg.encode(&ctx8());
858        assert_eq!(encoded[0], 4); // version 4
859        assert_eq!(encoded[1], 2); // class chunked
860        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
861        assert_eq!(consumed, encoded.len());
862        assert_eq!(decoded, msg);
863    }
864
865    #[test]
866    fn roundtrip_chunked_v4_earray_ctx4() {
867        let params = EarrayParams::default_params();
868        let msg = DataLayoutMessage::chunked_v4_earray(vec![1, 128], params, 0x1000);
869        let encoded = msg.encode(&ctx4());
870        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
871        assert_eq!(consumed, encoded.len());
872        assert_eq!(decoded, msg);
873    }
874
875    #[test]
876    fn roundtrip_chunked_v4_single() {
877        let msg = DataLayoutMessage::chunked_v4_single(vec![100, 200], 0x3000);
878        let encoded = msg.encode(&ctx8());
879        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
880        assert_eq!(consumed, encoded.len());
881        assert_eq!(decoded, msg);
882    }
883
884    /// A filtered single-chunk layout (flag `0x02`) carries the chunk's
885    /// on-disk size and per-chunk filter mask inline. Decode must retain both
886    /// (not discard them), and encode↔decode must round-trip — including the
887    /// nonzero mask the reader needs to skip a filter.
888    #[test]
889    fn roundtrip_chunked_v4_single_filtered() {
890        for ctx in [ctx8(), ctx4()] {
891            let msg = DataLayoutMessage::ChunkedV4 {
892                flags: 0x02,
893                chunk_dims: vec![100, 200, 4],
894                index_type: ChunkIndexType::SingleChunk,
895                earray_params: None,
896                farray_params: None,
897                single_chunk_filter: Some(SingleChunkFilter {
898                    nbytes: 12345,
899                    filter_mask: 0b101,
900                }),
901                index_address: 0x3000,
902            };
903            let encoded = msg.encode(&ctx);
904            let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx).unwrap();
905            assert_eq!(consumed, encoded.len());
906            assert_eq!(decoded, msg);
907            // The decoded layout exposes the retained size and mask.
908            match decoded {
909                DataLayoutMessage::ChunkedV4 {
910                    single_chunk_filter: Some(scf),
911                    ..
912                } => {
913                    assert_eq!(scf.nbytes, 12345);
914                    assert_eq!(scf.filter_mask, 0b101);
915                }
916                other => panic!("expected filtered single-chunk layout, got {other:?}"),
917            }
918        }
919    }
920
921    #[test]
922    fn chunked_v4_enc_bytes() {
923        // chunk dims [1, 256, 256]: max=256, needs 2 bytes
924        let params = EarrayParams::default_params();
925        let msg = DataLayoutMessage::chunked_v4_earray(vec![1, 256, 256], params, 0x2000);
926        let encoded = msg.encode(&ctx8());
927        // version(1) + class(1) + flags(1) + ndims(1) + enc_bytes(1)
928        // + 3*2 dim bytes + index_type(1) + 5 earray params + 8 addr = 25
929        assert_eq!(encoded.len(), 25);
930        assert_eq!(encoded[4], 2); // enc_bytes_per_dim = 2
931    }
932
933    #[test]
934    fn roundtrip_chunked_v3_btree_v1() {
935        // 1-D dataset, chunk=(8), element_size=4 -> chunk_dims=[8, 4].
936        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![8, 4], 0x1234);
937        let encoded = msg.encode(&ctx8());
938        // version(1) + class(1) + ndims(1) + addr(8) + 2*4 dims = 19
939        assert_eq!(encoded.len(), 19);
940        assert_eq!(encoded[0], 3);
941        assert_eq!(encoded[1], 2);
942        assert_eq!(encoded[2], 2); // ndims
943        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
944        assert_eq!(consumed, encoded.len());
945        assert_eq!(decoded, msg);
946    }
947
948    #[test]
949    fn roundtrip_chunked_v3_btree_v1_2d_ctx4() {
950        // 2-D dataset, chunk=(2,3), element_size=8 -> chunk_dims=[2, 3, 8].
951        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![2, 3, 8], 0x800);
952        let encoded = msg.encode(&ctx4());
953        // version(1) + class(1) + ndims(1) + addr(4) + 3*4 dims = 19
954        assert_eq!(encoded.len(), 19);
955        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
956        assert_eq!(consumed, encoded.len());
957        assert_eq!(decoded, msg);
958    }
959
960    #[test]
961    fn chunked_v3_undef_btree_addr() {
962        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![16, 4], UNDEF_ADDR);
963        let encoded = msg.encode(&ctx8());
964        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
965        match decoded {
966            DataLayoutMessage::ChunkedV3 { b_tree_address, .. } => {
967                assert_eq!(b_tree_address, UNDEF_ADDR);
968            }
969            _ => panic!("expected ChunkedV3"),
970        }
971    }
972
973    #[test]
974    fn chunked_v3_rejects_ndims_too_small() {
975        // ndims = 1 is malformed for chunked storage.
976        let buf = [3u8, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
977        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
978        assert!(matches!(err, FormatError::InvalidData(_)));
979    }
980
981    #[test]
982    fn chunked_v3_rejects_zero_dim() {
983        // ndims=2, addr=0, dims=[0, 4] -> zero chunk dimension.
984        let mut buf = vec![3u8, 2, 2];
985        buf.extend_from_slice(&0u64.to_le_bytes()); // addr
986        buf.extend_from_slice(&0u32.to_le_bytes()); // dim 0 == 0
987        buf.extend_from_slice(&4u32.to_le_bytes()); // dim 1
988        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
989        assert!(matches!(err, FormatError::InvalidData(_)));
990    }
991
992    #[test]
993    fn chunked_v3_truncated() {
994        // version=3, class=2, ndims=2, but no room for addr/dims.
995        let buf = [3u8, 2, 2];
996        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
997        assert!(matches!(err, FormatError::BufferTooShort { .. }));
998    }
999
1000    #[test]
1001    fn chunked_v4_large_dims() {
1002        // Large dims requiring 4 bytes each
1003        let params = EarrayParams::default_params();
1004        let msg = DataLayoutMessage::chunked_v4_earray(vec![1, 65536], params, 0x4000);
1005        let encoded = msg.encode(&ctx8());
1006        assert_eq!(encoded[4], 3); // enc_bytes_per_dim = 3 (65536 = 0x10000, needs 3 bytes)
1007    }
1008}