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                    ChunkIndexType::SingleChunk => {
593                        // A single-chunk index whose "single index with
594                        // filter" flag (0x02) is set carries the filtered
595                        // chunk size (sizeof_size bytes) and a 4-byte filter
596                        // mask before the chunk address (H5Olayout.c).
597                        if flags & 0x02 != 0 {
598                            let extra = ctx.sizeof_size as usize + 4;
599                            if buf.len() < pos + extra {
600                                return Err(FormatError::BufferTooShort {
601                                    needed: pos + extra,
602                                    available: buf.len(),
603                                });
604                            }
605                            pos += extra;
606                        }
607                    }
608                    // Implicit: no extra parameters.
609                    _ => {}
610                }
611
612                // index address
613                if buf.len() < pos + sa {
614                    return Err(FormatError::BufferTooShort {
615                        needed: pos + sa,
616                        available: buf.len(),
617                    });
618                }
619                let index_address = read_addr(&buf[pos..], sa);
620                pos += sa;
621
622                Ok((
623                    Self::ChunkedV4 {
624                        flags,
625                        chunk_dims,
626                        index_type,
627                        earray_params,
628                        farray_params,
629                        index_address,
630                    },
631                    pos,
632                ))
633            }
634            (VERSION_3, other) => Err(FormatError::UnsupportedFeature(format!(
635                "data layout class {}",
636                other
637            ))),
638            (v, _) => Err(FormatError::InvalidVersion(v)),
639        }
640    }
641}
642
643// ========================================================================= helpers
644
645/// Compute the minimum number of bytes (1-8) needed to encode `v`.
646fn enc_bytes_for_value(v: u64) -> u8 {
647    if v == 0 {
648        return 1;
649    }
650    let bits_needed = 64 - v.leading_zeros(); // 1..=64
651    bits_needed.div_ceil(8) as u8
652}
653
654// ======================================================================= tests
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659
660    fn ctx8() -> FormatContext {
661        FormatContext {
662            sizeof_addr: 8,
663            sizeof_size: 8,
664        }
665    }
666
667    fn ctx4() -> FormatContext {
668        FormatContext {
669            sizeof_addr: 4,
670            sizeof_size: 4,
671        }
672    }
673
674    #[test]
675    fn roundtrip_contiguous() {
676        let msg = DataLayoutMessage::contiguous(0x1000, 4096);
677        let encoded = msg.encode(&ctx8());
678        // 2 + 8 + 8 = 18
679        assert_eq!(encoded.len(), 18);
680        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
681        assert_eq!(consumed, 18);
682        assert_eq!(decoded, msg);
683    }
684
685    #[test]
686    fn roundtrip_contiguous_ctx4() {
687        let msg = DataLayoutMessage::contiguous(0x800, 256);
688        let encoded = msg.encode(&ctx4());
689        // 2 + 4 + 4 = 10
690        assert_eq!(encoded.len(), 10);
691        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
692        assert_eq!(consumed, 10);
693        assert_eq!(decoded, msg);
694    }
695
696    #[test]
697    fn roundtrip_contiguous_unallocated() {
698        let msg = DataLayoutMessage::contiguous_unallocated(1024);
699        let encoded = msg.encode(&ctx8());
700        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
701        assert_eq!(decoded, msg);
702        match decoded {
703            DataLayoutMessage::Contiguous { address, size } => {
704                assert_eq!(address, UNDEF_ADDR);
705                assert_eq!(size, 1024);
706            }
707            _ => panic!("expected Contiguous"),
708        }
709    }
710
711    #[test]
712    fn roundtrip_contiguous_undef_ctx4() {
713        let msg = DataLayoutMessage::contiguous_unallocated(512);
714        let encoded = msg.encode(&ctx4());
715        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
716        match decoded {
717            DataLayoutMessage::Contiguous { address, .. } => {
718                assert_eq!(address, UNDEF_ADDR);
719            }
720            _ => panic!("expected Contiguous"),
721        }
722    }
723
724    #[test]
725    fn roundtrip_compact() {
726        let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
727        let msg = DataLayoutMessage::compact(data.clone());
728        let encoded = msg.encode(&ctx8());
729        // 2 + 2 + 8 = 12
730        assert_eq!(encoded.len(), 12);
731        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
732        assert_eq!(consumed, 12);
733        assert_eq!(decoded, msg);
734    }
735
736    #[test]
737    fn roundtrip_compact_empty() {
738        let msg = DataLayoutMessage::compact(vec![]);
739        let encoded = msg.encode(&ctx8());
740        assert_eq!(encoded.len(), 4); // 2 + 2 + 0
741        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
742        assert_eq!(consumed, 4);
743        assert_eq!(decoded, msg);
744    }
745
746    #[test]
747    fn decode_bad_version() {
748        let buf = [2u8, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
749        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
750        match err {
751            FormatError::InvalidVersion(2) => {}
752            other => panic!("unexpected error: {:?}", other),
753        }
754    }
755
756    #[test]
757    fn decode_unsupported_class() {
758        let buf = [3u8, 3]; // class 3 = unknown
759        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
760        match err {
761            FormatError::UnsupportedFeature(_) => {}
762            other => panic!("unexpected error: {:?}", other),
763        }
764    }
765
766    #[test]
767    fn decode_buffer_too_short() {
768        let buf = [3u8];
769        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
770        match err {
771            FormatError::BufferTooShort { .. } => {}
772            other => panic!("unexpected error: {:?}", other),
773        }
774    }
775
776    #[test]
777    fn decode_contiguous_truncated() {
778        // version=3, class=1, but not enough bytes for address+size
779        let buf = [3u8, 1, 0, 0];
780        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
781        match err {
782            FormatError::BufferTooShort { .. } => {}
783            other => panic!("unexpected error: {:?}", other),
784        }
785    }
786
787    #[test]
788    fn version_and_class_bytes() {
789        let encoded = DataLayoutMessage::contiguous(0, 0).encode(&ctx8());
790        assert_eq!(encoded[0], 3);
791        assert_eq!(encoded[1], 1);
792
793        let encoded = DataLayoutMessage::compact(vec![]).encode(&ctx8());
794        assert_eq!(encoded[0], 3);
795        assert_eq!(encoded[1], 0);
796    }
797
798    #[test]
799    fn roundtrip_chunked_v4_earray() {
800        let params = EarrayParams::default_params();
801        let msg = DataLayoutMessage::chunked_v4_earray(vec![1, 256, 256], params, 0x2000);
802        let encoded = msg.encode(&ctx8());
803        assert_eq!(encoded[0], 4); // version 4
804        assert_eq!(encoded[1], 2); // class chunked
805        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
806        assert_eq!(consumed, encoded.len());
807        assert_eq!(decoded, msg);
808    }
809
810    #[test]
811    fn roundtrip_chunked_v4_earray_ctx4() {
812        let params = EarrayParams::default_params();
813        let msg = DataLayoutMessage::chunked_v4_earray(vec![1, 128], params, 0x1000);
814        let encoded = msg.encode(&ctx4());
815        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
816        assert_eq!(consumed, encoded.len());
817        assert_eq!(decoded, msg);
818    }
819
820    #[test]
821    fn roundtrip_chunked_v4_single() {
822        let msg = DataLayoutMessage::chunked_v4_single(vec![100, 200], 0x3000);
823        let encoded = msg.encode(&ctx8());
824        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
825        assert_eq!(consumed, encoded.len());
826        assert_eq!(decoded, msg);
827    }
828
829    #[test]
830    fn chunked_v4_enc_bytes() {
831        // chunk dims [1, 256, 256]: max=256, needs 2 bytes
832        let params = EarrayParams::default_params();
833        let msg = DataLayoutMessage::chunked_v4_earray(vec![1, 256, 256], params, 0x2000);
834        let encoded = msg.encode(&ctx8());
835        // version(1) + class(1) + flags(1) + ndims(1) + enc_bytes(1)
836        // + 3*2 dim bytes + index_type(1) + 5 earray params + 8 addr = 25
837        assert_eq!(encoded.len(), 25);
838        assert_eq!(encoded[4], 2); // enc_bytes_per_dim = 2
839    }
840
841    #[test]
842    fn roundtrip_chunked_v3_btree_v1() {
843        // 1-D dataset, chunk=(8), element_size=4 -> chunk_dims=[8, 4].
844        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![8, 4], 0x1234);
845        let encoded = msg.encode(&ctx8());
846        // version(1) + class(1) + ndims(1) + addr(8) + 2*4 dims = 19
847        assert_eq!(encoded.len(), 19);
848        assert_eq!(encoded[0], 3);
849        assert_eq!(encoded[1], 2);
850        assert_eq!(encoded[2], 2); // ndims
851        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
852        assert_eq!(consumed, encoded.len());
853        assert_eq!(decoded, msg);
854    }
855
856    #[test]
857    fn roundtrip_chunked_v3_btree_v1_2d_ctx4() {
858        // 2-D dataset, chunk=(2,3), element_size=8 -> chunk_dims=[2, 3, 8].
859        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![2, 3, 8], 0x800);
860        let encoded = msg.encode(&ctx4());
861        // version(1) + class(1) + ndims(1) + addr(4) + 3*4 dims = 19
862        assert_eq!(encoded.len(), 19);
863        let (decoded, consumed) = DataLayoutMessage::decode(&encoded, &ctx4()).unwrap();
864        assert_eq!(consumed, encoded.len());
865        assert_eq!(decoded, msg);
866    }
867
868    #[test]
869    fn chunked_v3_undef_btree_addr() {
870        let msg = DataLayoutMessage::chunked_v3_btree_v1(vec![16, 4], UNDEF_ADDR);
871        let encoded = msg.encode(&ctx8());
872        let (decoded, _) = DataLayoutMessage::decode(&encoded, &ctx8()).unwrap();
873        match decoded {
874            DataLayoutMessage::ChunkedV3 { b_tree_address, .. } => {
875                assert_eq!(b_tree_address, UNDEF_ADDR);
876            }
877            _ => panic!("expected ChunkedV3"),
878        }
879    }
880
881    #[test]
882    fn chunked_v3_rejects_ndims_too_small() {
883        // ndims = 1 is malformed for chunked storage.
884        let buf = [3u8, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
885        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
886        assert!(matches!(err, FormatError::InvalidData(_)));
887    }
888
889    #[test]
890    fn chunked_v3_rejects_zero_dim() {
891        // ndims=2, addr=0, dims=[0, 4] -> zero chunk dimension.
892        let mut buf = vec![3u8, 2, 2];
893        buf.extend_from_slice(&0u64.to_le_bytes()); // addr
894        buf.extend_from_slice(&0u32.to_le_bytes()); // dim 0 == 0
895        buf.extend_from_slice(&4u32.to_le_bytes()); // dim 1
896        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
897        assert!(matches!(err, FormatError::InvalidData(_)));
898    }
899
900    #[test]
901    fn chunked_v3_truncated() {
902        // version=3, class=2, ndims=2, but no room for addr/dims.
903        let buf = [3u8, 2, 2];
904        let err = DataLayoutMessage::decode(&buf, &ctx8()).unwrap_err();
905        assert!(matches!(err, FormatError::BufferTooShort { .. }));
906    }
907
908    #[test]
909    fn chunked_v4_large_dims() {
910        // Large dims requiring 4 bytes each
911        let params = EarrayParams::default_params();
912        let msg = DataLayoutMessage::chunked_v4_earray(vec![1, 65536], params, 0x4000);
913        let encoded = msg.encode(&ctx8());
914        assert_eq!(encoded[4], 3); // enc_bytes_per_dim = 3 (65536 = 0x10000, needs 3 bytes)
915    }
916}