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