Skip to main content

hdf5_reader/messages/
layout.rs

1//! HDF5 Data Layout message (type 0x0008).
2//!
3//! Describes how raw data for a dataset is stored: compact (inline in the
4//! object header), contiguous (a single block in the file), or chunked
5//! (split into fixed-size chunks, indexed by a B-tree).
6
7use crate::error::{Error, Result};
8use crate::io::Cursor;
9pub use hdf5_core::{ChunkIndexing, DataLayout, DataLayoutMessage};
10
11/// Parse a data layout message.
12pub fn parse(
13    cursor: &mut Cursor<'_>,
14    offset_size: u8,
15    length_size: u8,
16    msg_size: usize,
17) -> Result<DataLayoutMessage> {
18    let start = cursor.position();
19    let version = cursor.read_u8()?;
20
21    let layout = match version {
22        1 | 2 => parse_v1_v2(cursor, offset_size, length_size, version)?,
23        3 => parse_v3(cursor, offset_size, length_size)?,
24        4 | 5 => parse_v4_v5(cursor, offset_size, length_size, version)?,
25        v => return Err(Error::UnsupportedLayoutVersion(v)),
26    };
27
28    let consumed = (cursor.position() - start) as usize;
29    if consumed < msg_size {
30        cursor.skip(msg_size - consumed)?;
31    }
32
33    Ok(DataLayoutMessage { layout })
34}
35
36// ---------------------------------------------------------------------------
37// Version 1 / 2
38// ---------------------------------------------------------------------------
39
40fn parse_v1_v2(
41    cursor: &mut Cursor<'_>,
42    offset_size: u8,
43    _length_size: u8,
44    version: u8,
45) -> Result<DataLayout> {
46    let dimensionality = cursor.read_u8()?;
47    let layout_class = cursor.read_u8()?;
48    let _reserved = cursor.read_bytes(if version == 1 { 5 } else { 3 })?;
49
50    // For v1 there is an optional compact data size field.
51    // data_address is only meaningful for contiguous and chunked.
52    let data_address = if layout_class != 0 {
53        cursor.read_offset(offset_size)?
54    } else {
55        // For compact, there is no address; skip the offset-sized field.
56        cursor.read_offset(offset_size)?
57    };
58
59    // Read dimension sizes. Each is 4 bytes. The number of dimensions:
60    // For contiguous: dimensionality values (unused data size).
61    // For chunked: (dimensionality-1) chunk dims + 1 element size.
62    let mut dim_values = Vec::with_capacity(dimensionality as usize);
63    for _ in 0..dimensionality {
64        dim_values.push(cursor.read_u32_le()?);
65    }
66
67    match layout_class {
68        0 => {
69            // Compact
70            let compact_size = cursor.read_u32_le()? as usize;
71            let data = cursor.read_bytes(compact_size)?.to_vec();
72            Ok(DataLayout::Compact { data })
73        }
74        1 => {
75            // Contiguous
76            // Size is not explicitly stored in v1/v2 for contiguous. The dims
77            // encode the logical size but the actual file extent comes from the
78            // dataspace * element size. We store the product as size.
79            let size = if dim_values.is_empty() {
80                0
81            } else {
82                dim_values.iter().map(|d| *d as u64).product()
83            };
84            Ok(DataLayout::Contiguous {
85                address: data_address,
86                size,
87            })
88        }
89        2 => {
90            // Chunked — last dimension is the element size
91            let (element_size, chunk_dims) = if dim_values.is_empty() {
92                (0u32, vec![])
93            } else {
94                let es = *dim_values.last().unwrap();
95                let cd: Vec<u32> = dim_values[..dim_values.len() - 1].to_vec();
96                (es, cd)
97            };
98            Ok(DataLayout::Chunked {
99                address: data_address,
100                dims: chunk_dims,
101                element_size,
102                chunk_indexing: None,
103            })
104        }
105        c => Err(Error::UnsupportedLayoutClass(c)),
106    }
107}
108
109// ---------------------------------------------------------------------------
110// Version 3
111// ---------------------------------------------------------------------------
112
113fn parse_v3(cursor: &mut Cursor<'_>, offset_size: u8, length_size: u8) -> Result<DataLayout> {
114    let layout_class = cursor.read_u8()?;
115
116    match layout_class {
117        0 => {
118            // Compact
119            let size = cursor.read_u16_le()? as usize;
120            let data = cursor.read_bytes(size)?.to_vec();
121            Ok(DataLayout::Compact { data })
122        }
123        1 => {
124            // Contiguous
125            let address = cursor.read_offset(offset_size)?;
126            let size = cursor.read_length(length_size)?;
127            Ok(DataLayout::Contiguous { address, size })
128        }
129        2 => {
130            // Chunked
131            let dimensionality = cursor.read_u8()?;
132            let address = cursor.read_offset(offset_size)?;
133
134            // (dimensionality - 1) chunk dims + 1 element size (each 4 bytes)
135            let n = dimensionality as usize;
136            let mut raw_dims = Vec::with_capacity(n);
137            for _ in 0..n {
138                raw_dims.push(cursor.read_u32_le()?);
139            }
140
141            let (element_size, chunk_dims) = if raw_dims.is_empty() {
142                (0, vec![])
143            } else {
144                let es = *raw_dims.last().unwrap();
145                let cd = raw_dims[..raw_dims.len() - 1].to_vec();
146                (es, cd)
147            };
148
149            Ok(DataLayout::Chunked {
150                address,
151                dims: chunk_dims,
152                element_size,
153                chunk_indexing: None,
154            })
155        }
156        c => Err(Error::UnsupportedLayoutClass(c)),
157    }
158}
159
160// ---------------------------------------------------------------------------
161// Version 4
162// ---------------------------------------------------------------------------
163
164/// Parse v4/v5 layout messages.
165///
166/// v4/v5 chunked layouts store only the chunk dimensions here; element size
167/// is derived from the datatype. Filtered chunk-index records switched from
168/// `length_size` to `offset_size` in v5.
169fn parse_v4_v5(
170    cursor: &mut Cursor<'_>,
171    offset_size: u8,
172    length_size: u8,
173    version: u8,
174) -> Result<DataLayout> {
175    let layout_class = cursor.read_u8()?;
176
177    match layout_class {
178        0 => {
179            // Compact
180            let size = cursor.read_u16_le()? as usize;
181            let data = cursor.read_bytes(size)?.to_vec();
182            Ok(DataLayout::Compact { data })
183        }
184        1 => {
185            // Contiguous
186            let address = cursor.read_offset(offset_size)?;
187            let size = cursor.read_u64_le()?;
188            Ok(DataLayout::Contiguous { address, size })
189        }
190        2 => {
191            let start = cursor.clone();
192            let direct = parse_v4_v5_chunked(cursor, offset_size, length_size, version, false);
193            match direct {
194                Ok(layout) => Ok(layout),
195                Err(err) if version == 4 && should_retry_v4_chunked_parse(&err) => {
196                    *cursor = start;
197                    parse_v4_v5_chunked(cursor, offset_size, length_size, version, true)
198                }
199                Err(err) => Err(err),
200            }
201        }
202        c => Err(Error::UnsupportedLayoutClass(c)),
203    }
204}
205
206fn parse_v4_v5_chunked(
207    cursor: &mut Cursor<'_>,
208    offset_size: u8,
209    length_size: u8,
210    version: u8,
211    legacy_dim_size_encoding: bool,
212) -> Result<DataLayout> {
213    let flags = cursor.read_u8()?;
214    let ndims_raw = cursor.read_u8()? as usize;
215    let dim_size_enc = cursor.read_u8()?;
216    let dim_bytes = if legacy_dim_size_encoding {
217        dim_size_enc as usize + 1
218    } else {
219        dim_size_enc as usize
220    };
221
222    let mut dims = Vec::with_capacity(ndims_raw);
223    for _ in 0..ndims_raw {
224        dims.push(cursor.read_uvar(dim_bytes)? as u32);
225    }
226
227    let index_type = cursor.read_u8()?;
228    let chunk_size_len = if version >= 5 {
229        offset_size
230    } else {
231        length_size
232    };
233    let chunk_indexing = parse_chunk_indexing_v4_v5(cursor, flags, index_type, chunk_size_len)?;
234    let address = cursor.read_offset(offset_size)?;
235
236    Ok(DataLayout::Chunked {
237        address,
238        dims,
239        element_size: 0,
240        chunk_indexing: Some(chunk_indexing),
241    })
242}
243
244fn should_retry_v4_chunked_parse(err: &Error) -> bool {
245    match err {
246        Error::UnexpectedEof { .. } | Error::UnsupportedChunkIndexType(_) => true,
247        Error::InvalidData(msg) => msg.starts_with("unsupported variable integer size:"),
248        _ => false,
249    }
250}
251
252/// Parse chunk indexing for v4/v5 layout.
253/// On-disk values: 1=SingleChunk, 2=Implicit, 3=FixedArray, 4=ExtensibleArray, 5=BTreeV2
254fn parse_chunk_indexing_v4_v5(
255    cursor: &mut Cursor<'_>,
256    flags: u8,
257    index_type: u8,
258    chunk_size_len: u8,
259) -> Result<ChunkIndexing> {
260    match index_type {
261        1 => {
262            // Single chunk
263            let idx_flags = if (flags & 0x01) != 0 {
264                let filtered_size = cursor.read_u64_le()?;
265                let filter_mask = cursor.read_u32_le()?;
266                Some((filtered_size, filter_mask))
267            } else {
268                None
269            };
270            let (fs, fm) = idx_flags.unwrap_or((0, 0));
271            Ok(ChunkIndexing::SingleChunk {
272                filtered_size: fs,
273                filters: fm,
274            })
275        }
276        2 => Ok(ChunkIndexing::Implicit),
277        3 => {
278            let page_bits = cursor.read_u8()?;
279            Ok(ChunkIndexing::FixedArray {
280                page_bits,
281                chunk_size_len,
282            })
283        }
284        4 => {
285            let max_bits = cursor.read_u8()?;
286            let index_bits = cursor.read_u8()?;
287            let min_pointers = cursor.read_u8()?;
288            let min_elements = cursor.read_u8()?;
289            let _max_dblk_page_bits = cursor.read_u8()?;
290            Ok(ChunkIndexing::ExtensibleArray {
291                max_bits,
292                index_bits,
293                min_pointers,
294                min_elements,
295                chunk_size_len,
296            })
297        }
298        5 => {
299            cursor.skip(6)?;
300            Ok(ChunkIndexing::BTreeV2)
301        }
302        t => Err(Error::UnsupportedChunkIndexType(t)),
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn parse_v3_contiguous() {
312        let mut data = vec![
313            0x03, // version 3
314            0x01, // layout class = contiguous
315        ];
316        // address (8 bytes)
317        data.extend_from_slice(&0x1000u64.to_le_bytes());
318        // size (8 bytes)
319        data.extend_from_slice(&4096u64.to_le_bytes());
320
321        let mut cursor = Cursor::new(&data);
322        let msg = parse(&mut cursor, 8, 8, data.len()).unwrap();
323        match &msg.layout {
324            DataLayout::Contiguous { address, size } => {
325                assert_eq!(*address, 0x1000);
326                assert_eq!(*size, 4096);
327            }
328            other => panic!("expected Contiguous, got {:?}", other),
329        }
330    }
331
332    #[test]
333    fn parse_v3_compact() {
334        let mut data = vec![
335            0x03, // version 3
336            0x00, // layout class = compact
337        ];
338        // compact size = 4
339        data.extend_from_slice(&4u16.to_le_bytes());
340        // inline data
341        data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]);
342
343        let mut cursor = Cursor::new(&data);
344        let msg = parse(&mut cursor, 8, 8, data.len()).unwrap();
345        match &msg.layout {
346            DataLayout::Compact { data } => {
347                assert_eq!(data, &[0x01, 0x02, 0x03, 0x04]);
348            }
349            other => panic!("expected Compact, got {:?}", other),
350        }
351    }
352
353    #[test]
354    fn parse_v3_chunked() {
355        let mut data = vec![
356            0x03, // version 3
357            0x02, // layout class = chunked
358            0x03, // dimensionality = 3 (2 chunk dims + 1 element size)
359        ];
360        // address
361        data.extend_from_slice(&0x2000u64.to_le_bytes());
362        // dim[0] = 256
363        data.extend_from_slice(&256u32.to_le_bytes());
364        // dim[1] = 128
365        data.extend_from_slice(&128u32.to_le_bytes());
366        // element size = 4
367        data.extend_from_slice(&4u32.to_le_bytes());
368
369        let mut cursor = Cursor::new(&data);
370        let msg = parse(&mut cursor, 8, 8, data.len()).unwrap();
371        match &msg.layout {
372            DataLayout::Chunked {
373                address,
374                dims,
375                element_size,
376                chunk_indexing,
377            } => {
378                assert_eq!(*address, 0x2000);
379                assert_eq!(dims, &[256, 128]);
380                assert_eq!(*element_size, 4);
381                assert!(chunk_indexing.is_none());
382            }
383            other => panic!("expected Chunked, got {:?}", other),
384        }
385    }
386
387    #[test]
388    fn parse_v4_chunked_direct_dim_size_encoding() {
389        let mut data = vec![
390            0x04, // version 4
391            0x02, // layout class = chunked
392            0x00, // flags
393            0x02, // ndims
394            0x04, // 4 bytes per dimension
395        ];
396        data.extend_from_slice(&3u32.to_le_bytes());
397        data.extend_from_slice(&5u32.to_le_bytes());
398        data.push(0x03); // fixed array indexing
399        data.push(0x00); // page bits
400        data.extend_from_slice(&0x1122_3344_5566_7788u64.to_le_bytes());
401
402        let mut cursor = Cursor::new(&data);
403        let msg = parse(&mut cursor, 8, 8, data.len()).unwrap();
404        match &msg.layout {
405            DataLayout::Chunked {
406                address,
407                dims,
408                element_size,
409                chunk_indexing,
410            } => {
411                assert_eq!(*address, 0x1122_3344_5566_7788);
412                assert_eq!(dims, &[3, 5]);
413                assert_eq!(*element_size, 0);
414                match chunk_indexing {
415                    Some(ChunkIndexing::FixedArray {
416                        page_bits,
417                        chunk_size_len,
418                    }) => {
419                        assert_eq!(*page_bits, 0);
420                        assert_eq!(*chunk_size_len, 8);
421                    }
422                    other => panic!("expected FixedArray indexing, got {:?}", other),
423                }
424            }
425            other => panic!("expected Chunked, got {:?}", other),
426        }
427    }
428
429    #[test]
430    fn parse_v4_chunked_legacy_dim_size_encoding() {
431        let mut data = vec![
432            0x04, // version 4
433            0x02, // layout class = chunked
434            0x00, // flags
435            0x02, // ndims
436            0x03, // legacy encoding: 4 bytes per dimension stored as 3
437        ];
438        data.extend_from_slice(&3u32.to_le_bytes());
439        data.extend_from_slice(&5u32.to_le_bytes());
440        data.push(0x03); // fixed array indexing
441        data.push(0x00); // page bits
442        data.extend_from_slice(&0x8877_6655_4433_2211u64.to_le_bytes());
443
444        let mut cursor = Cursor::new(&data);
445        let msg = parse(&mut cursor, 8, 8, data.len()).unwrap();
446        match &msg.layout {
447            DataLayout::Chunked {
448                address,
449                dims,
450                element_size,
451                chunk_indexing,
452            } => {
453                assert_eq!(*address, 0x8877_6655_4433_2211);
454                assert_eq!(dims, &[3, 5]);
455                assert_eq!(*element_size, 0);
456                match chunk_indexing {
457                    Some(ChunkIndexing::FixedArray {
458                        page_bits,
459                        chunk_size_len,
460                    }) => {
461                        assert_eq!(*page_bits, 0);
462                        assert_eq!(*chunk_size_len, 8);
463                    }
464                    other => panic!("expected FixedArray indexing, got {:?}", other),
465                }
466            }
467            other => panic!("expected Chunked, got {:?}", other),
468        }
469    }
470}