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