Skip to main content

hdf5_pure/
dataspace.rs

1//! HDF5 Dataspace message parsing (message type 0x0001).
2
3#[cfg(not(feature = "std"))]
4use alloc::vec::Vec;
5
6use crate::bytes;
7use crate::error::FormatError;
8
9/// Type of dataspace.
10#[derive(Debug, Clone, PartialEq)]
11pub enum DataspaceType {
12    /// Scalar (single element).
13    Scalar,
14    /// Simple (N-dimensional array).
15    Simple,
16    /// Null (no data).
17    Null,
18}
19
20/// Parsed HDF5 dataspace message.
21#[derive(Debug, Clone, PartialEq)]
22pub struct Dataspace {
23    /// The type of this dataspace.
24    pub space_type: DataspaceType,
25    /// Number of dimensions (0 for scalar).
26    pub rank: u8,
27    /// Current dimension sizes.
28    pub dimensions: Vec<u64>,
29    /// The maximum size of each dimension, when the dataspace message stores them.
30    pub max_dimensions: Option<Vec<MaxExtent>>,
31}
32
33impl Dataspace {
34    /// Parse a dataspace message from raw message bytes.
35    ///
36    /// `length_size` is needed for dimension value width (from superblock).
37    pub fn parse(data: &[u8], length_size: u8) -> Result<Dataspace, FormatError> {
38        bytes::ensure_len(data, 0, 4)?;
39
40        let version = data[0];
41        let rank = data[1];
42        let flags = data[2];
43
44        let (space_type, header_size) = match version {
45            1 => {
46                // v1: byte 3 is reserved, then 4 reserved bytes
47                bytes::ensure_len(data, 0, 8)?;
48                let st = if rank == 0 {
49                    DataspaceType::Scalar
50                } else {
51                    DataspaceType::Simple
52                };
53                (st, 8usize)
54            }
55            2 => {
56                // v2: byte 3 is type
57                let type_byte = data[3];
58                let st = match type_byte {
59                    0 => DataspaceType::Scalar,
60                    1 => DataspaceType::Simple,
61                    2 => DataspaceType::Null,
62                    _ => return Err(FormatError::InvalidDataspaceType(type_byte)),
63                };
64                (st, 4usize)
65            }
66            _ => return Err(FormatError::InvalidDataspaceVersion(version)),
67        };
68
69        let ls = length_size as usize;
70        let mut pos = header_size;
71
72        // Read current dimensions
73        let mut dimensions = Vec::with_capacity(rank as usize);
74        for _ in 0..rank {
75            let dim = bytes::read_length(data, pos, length_size)?;
76            dimensions.push(dim);
77            pos += ls;
78        }
79
80        // Read max dimensions if flags bit 0 is set
81        let max_dimensions = if flags & 0x01 != 0 {
82            let mut max_dims = Vec::with_capacity(rank as usize);
83            for _ in 0..rank {
84                max_dims.push(MaxExtent::from_length(bytes::read_length(
85                    data,
86                    pos,
87                    length_size,
88                )?));
89                pos += ls;
90            }
91            Some(max_dims)
92        } else {
93            None
94        };
95
96        // A v1 dataspace with flags bit 1 set carries rank × length_size bytes
97        // of permutation indices here. Nothing below reads `pos`, so they need
98        // no skipping; this crate does not expose them.
99
100        Ok(Dataspace {
101            space_type,
102            rank,
103            dimensions,
104            max_dimensions,
105        })
106    }
107
108    /// Serialize dataspace to HDF5 message bytes (v2 format).
109    pub fn serialize(&self, length_size: u8) -> Vec<u8> {
110        let mut buf = Vec::new();
111        buf.push(2); // version 2
112        buf.push(self.rank);
113        let flags = if self.max_dimensions.is_some() {
114            0x01
115        } else {
116            0x00
117        };
118        buf.push(flags);
119        let type_byte = match self.space_type {
120            DataspaceType::Scalar => 0,
121            DataspaceType::Simple => 1,
122            DataspaceType::Null => 2,
123        };
124        buf.push(type_byte);
125        for &dim in &self.dimensions {
126            Self::write_length(&mut buf, dim, length_size);
127        }
128        if let Some(ref max_dims) = self.max_dimensions {
129            for &md in max_dims {
130                Self::write_length(&mut buf, md.to_length(), length_size);
131            }
132        }
133        buf
134    }
135
136    fn write_length(buf: &mut Vec<u8>, val: u64, size: u8) {
137        #[expect(
138            clippy::cast_possible_truncation,
139            reason = "size is the length byte width chosen to hold val, so each arm casts to a width that fits by construction"
140        )]
141        match size {
142            2 => buf.extend_from_slice(&(val as u16).to_le_bytes()),
143            4 => buf.extend_from_slice(&(val as u32).to_le_bytes()),
144            8 => buf.extend_from_slice(&val.to_le_bytes()),
145            _ => {}
146        }
147    }
148
149    /// Total number of elements. Scalar = 1, Null = 0.
150    ///
151    /// The count saturates at `u64::MAX` rather than panicking when a malformed
152    /// dataspace declares dimensions whose product overflows `u64`. Every caller
153    /// treats the saturated value as an impossibly large element count, so the
154    /// downstream size and limit checks reject it as an error instead.
155    pub fn num_elements(&self) -> u64 {
156        match self.space_type {
157            DataspaceType::Null => 0,
158            DataspaceType::Scalar => 1,
159            DataspaceType::Simple => {
160                if self.dimensions.is_empty() {
161                    0
162                } else {
163                    self.dimensions
164                        .iter()
165                        .fold(1u64, |acc, &dim| acc.saturating_mul(dim))
166                }
167            }
168        }
169    }
170
171    /// Returns the maximum dimensions, when some maximum exceeds its current dimension.
172    ///
173    /// A dataspace with no maximum dimensions describes a dataset of a fixed shape, and so
174    /// does one whose every maximum is fixed at the current dimension: both give `None`.
175    pub(crate) fn extensible_max_dimensions(&self) -> Option<&[MaxExtent]> {
176        let max_dims = self.max_dimensions.as_deref()?;
177        let fixed_at_current = max_dims.len() == self.dimensions.len()
178            && max_dims
179                .iter()
180                .zip(&self.dimensions)
181                .all(|(max, &dim)| max.size() == Some(dim));
182        (!fixed_at_current).then_some(max_dims)
183    }
184}
185
186/// The maximum size of one dimension of a dataspace.
187///
188/// A dataspace message stores a maximum size for each dimension, and the special "unlimited
189/// size" marks a dimension the data may expand along indefinitely. The maximum sizes are
190/// defined in "The Dataspace Message" of the [format specification, version 4.0][spec]. The C
191/// library calls the unlimited size `H5S_UNLIMITED`.
192///
193/// # Examples
194///
195/// ```
196/// use hdf5_pure::MaxExtent;
197///
198/// // A dataset that grows along its first dimension and keeps three columns.
199/// let maxshape = [MaxExtent::Unlimited, MaxExtent::Fixed(3)];
200/// assert_eq!(maxshape[0].size(), None);
201/// assert_eq!(maxshape[1].size(), Some(3));
202/// ```
203///
204/// [spec]: https://support.hdfgroup.org/documentation/hdf5/latest/_f_m_t4.html#subsubsec_fmt4_dataobject_hdr_msg_simple
205#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
206pub enum MaxExtent {
207    /// The dimension grows to this many elements and no further.
208    Fixed(u64),
209    /// The dimension grows without bound.
210    Unlimited,
211}
212
213impl MaxExtent {
214    /// Interprets a maximum dimension size read from a dataspace message.
215    ///
216    /// Only an eight-byte field holds the unlimited size, so a maximum read from a narrower
217    /// one is always fixed.
218    pub(crate) const fn from_length(length: u64) -> Self {
219        if length == UNLIMITED_LENGTH {
220            Self::Unlimited
221        } else {
222            Self::Fixed(length)
223        }
224    }
225
226    /// Returns the number of elements the dimension reaches at most, or `None` for a
227    /// dimension that grows without bound.
228    pub const fn size(self) -> Option<u64> {
229        match self {
230            Self::Fixed(size) => Some(size),
231            Self::Unlimited => None,
232        }
233    }
234
235    /// Returns `true` if a dimension of `dim` elements stays within this maximum.
236    pub(crate) const fn admits(self, dim: u64) -> bool {
237        match self {
238            Self::Fixed(size) => size >= dim,
239            Self::Unlimited => true,
240        }
241    }
242
243    /// Returns the maximum size as a dataspace message stores it, all ones for
244    /// [`Self::Unlimited`].
245    pub(crate) const fn to_length(self) -> u64 {
246        match self {
247            Self::Fixed(size) => size,
248            Self::Unlimited => UNLIMITED_LENGTH,
249        }
250    }
251
252    const fn is_unlimited(self) -> bool {
253        matches!(self, Self::Unlimited)
254    }
255}
256
257/// A dataset's shape beside the maximum shape that bounds it.
258///
259/// [`Extent::new`] checks the two against each other, so a writer that takes an `Extent` has a
260/// maximum shape of the shape's own rank, with every dimension inside its maximum.
261#[derive(Clone, Copy, Debug)]
262pub(crate) struct Extent<'a> {
263    /// The current size of each dimension.
264    dims: &'a [u64],
265    /// The maximum size of each dimension, `None` for a dataset of a fixed shape.
266    max_dims: Option<&'a [MaxExtent]>,
267}
268
269impl<'a> Extent<'a> {
270    /// Pairs a shape with the maximum shape that bounds it.
271    ///
272    /// # Errors
273    ///
274    /// Returns a reason for the caller to report if `max_dims` and `dims` differ in rank, if a
275    /// maximum is smaller than the dimension it bounds, or if a maximum is
276    /// [`MaxExtent::Fixed`] at the unlimited size, which [`Dataspace::serialize`] writes as
277    /// [`MaxExtent::Unlimited`].
278    pub(crate) fn new(
279        dims: &'a [u64],
280        max_dims: Option<&'a [MaxExtent]>,
281    ) -> Result<Self, &'static str> {
282        if let Some(max_dims) = max_dims {
283            if max_dims.len() != dims.len() {
284                return Err("maxshape must have the same rank as the dataset shape");
285            }
286            if max_dims
287                .iter()
288                .zip(dims)
289                .any(|(max, &dim)| !max.admits(dim))
290            {
291                return Err("maxshape must be at least the current shape in every dimension");
292            }
293            if max_dims.contains(&MaxExtent::Fixed(UNLIMITED_LENGTH)) {
294                return Err(
295                    "a fixed maximum of u64::MAX is the format's unlimited marker; \
296                     name MaxExtent::Unlimited for a dimension that grows without bound",
297                );
298            }
299        }
300        Ok(Self { dims, max_dims })
301    }
302
303    pub(crate) const fn dims(self) -> &'a [u64] {
304        self.dims
305    }
306
307    /// Returns how many dimensions grow without bound.
308    ///
309    /// A dataset of a fixed shape has none.
310    pub(crate) fn unlimited_dimensions(self) -> usize {
311        self.max_dims.map_or(0, |max_dims| {
312            max_dims.iter().filter(|max| max.is_unlimited()).count()
313        })
314    }
315}
316
317// The maximum dimension size a dataspace message stores for a dimension that grows
318// without bound, the format's "unlimited size". The C library defines `H5S_UNLIMITED` as
319// `HSIZE_UNDEF`, which is `UINT64_MAX` (`H5Spublic.h` and `H5public.h`, HDF5 2.2.0). A
320// message written with a narrower "Size of Lengths" cannot hold it: `H5O__sdspace_decode`
321// zero-extends the field it reads (`H5F_DECODE_LENGTH` in `H5Osdspace.c`, HDF5 2.2.0), so
322// only an eight-byte field decodes to this value.
323const UNLIMITED_LENGTH: u64 = u64::MAX;
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    fn build_v1_dataspace(rank: u8, flags: u8, dims: &[u64], max_dims: Option<&[u64]>) -> Vec<u8> {
330        let length_size = 8u8;
331        let mut buf = Vec::new();
332        buf.push(1); // version
333        buf.push(rank);
334        buf.push(flags);
335        buf.push(0); // reserved
336        buf.extend_from_slice(&[0u8; 4]); // reserved(4)
337        for &d in dims {
338            buf.extend_from_slice(&d.to_le_bytes());
339        }
340        if let Some(md) = max_dims {
341            for &d in md {
342                buf.extend_from_slice(&d.to_le_bytes());
343            }
344        }
345        let _ = length_size;
346        buf
347    }
348
349    fn build_v2_dataspace(
350        rank: u8,
351        flags: u8,
352        type_byte: u8,
353        dims: &[u64],
354        max_dims: Option<&[u64]>,
355    ) -> Vec<u8> {
356        let mut buf = Vec::new();
357        buf.push(2); // version
358        buf.push(rank);
359        buf.push(flags);
360        buf.push(type_byte);
361        for &d in dims {
362            buf.extend_from_slice(&d.to_le_bytes());
363        }
364        if let Some(md) = max_dims {
365            for &d in md {
366                buf.extend_from_slice(&d.to_le_bytes());
367            }
368        }
369        buf
370    }
371
372    #[test]
373    fn scalar_v1() {
374        let data = build_v1_dataspace(0, 0, &[], None);
375        let ds = Dataspace::parse(&data, 8).unwrap();
376        assert_eq!(ds.space_type, DataspaceType::Scalar);
377        assert_eq!(ds.rank, 0);
378        assert!(ds.dimensions.is_empty());
379        assert!(ds.max_dimensions.is_none());
380        assert_eq!(ds.num_elements(), 1);
381    }
382
383    #[test]
384    fn null_v2() {
385        let data = build_v2_dataspace(0, 0, 2, &[], None);
386        let ds = Dataspace::parse(&data, 8).unwrap();
387        assert_eq!(ds.space_type, DataspaceType::Null);
388        assert_eq!(ds.num_elements(), 0);
389    }
390
391    #[test]
392    fn simple_1d() {
393        let data = build_v1_dataspace(1, 0, &[5], None);
394        let ds = Dataspace::parse(&data, 8).unwrap();
395        assert_eq!(ds.space_type, DataspaceType::Simple);
396        assert_eq!(ds.rank, 1);
397        assert_eq!(ds.dimensions, vec![5]);
398        assert!(ds.max_dimensions.is_none());
399        assert_eq!(ds.num_elements(), 5);
400    }
401
402    #[test]
403    fn simple_2d() {
404        let data = build_v1_dataspace(2, 0, &[3, 4], None);
405        let ds = Dataspace::parse(&data, 8).unwrap();
406        assert_eq!(ds.rank, 2);
407        assert_eq!(ds.dimensions, vec![3, 4]);
408        assert_eq!(ds.num_elements(), 12);
409    }
410
411    #[test]
412    fn simple_3d_with_max_dims_unlimited() {
413        let data = build_v1_dataspace(3, 0x01, &[2, 3, 4], Some(&[10, u64::MAX, 100]));
414        let ds = Dataspace::parse(&data, 8).unwrap();
415        assert_eq!(ds.rank, 3);
416        assert_eq!(ds.dimensions, vec![2, 3, 4]);
417        let md = ds.max_dimensions.clone().unwrap();
418        assert_eq!(
419            md,
420            vec![
421                MaxExtent::Fixed(10),
422                MaxExtent::Unlimited,
423                MaxExtent::Fixed(100)
424            ]
425        );
426        assert_eq!(ds.num_elements(), 24);
427    }
428
429    #[test]
430    fn num_elements_saturates_on_overflow() {
431        // A malformed dataspace can declare dimensions whose product exceeds
432        // u64::MAX. num_elements() must saturate instead of panicking, so the
433        // callers can reject the impossibly large count as an error.
434        let data = build_v1_dataspace(3, 0, &[1 << 40, 1 << 40, 1 << 40], None);
435        let ds = Dataspace::parse(&data, 8).unwrap();
436        assert_eq!(ds.num_elements(), u64::MAX);
437    }
438
439    #[test]
440    fn v2_simple() {
441        let data = build_v2_dataspace(1, 0, 1, &[7], None);
442        let ds = Dataspace::parse(&data, 8).unwrap();
443        assert_eq!(ds.space_type, DataspaceType::Simple);
444        assert_eq!(ds.dimensions, vec![7]);
445    }
446
447    #[test]
448    fn v2_scalar() {
449        let data = build_v2_dataspace(0, 0, 0, &[], None);
450        let ds = Dataspace::parse(&data, 8).unwrap();
451        assert_eq!(ds.space_type, DataspaceType::Scalar);
452        assert_eq!(ds.num_elements(), 1);
453    }
454
455    #[test]
456    fn v1_with_4byte_length() {
457        let mut buf = Vec::new();
458        buf.push(1); // version
459        buf.push(1); // rank
460        buf.push(0); // flags
461        buf.push(0); // reserved
462        buf.extend_from_slice(&[0u8; 4]); // reserved(4)
463        buf.extend_from_slice(&10u32.to_le_bytes()); // dim with length_size=4
464        let ds = Dataspace::parse(&buf, 4).unwrap();
465        assert_eq!(ds.dimensions, vec![10]);
466    }
467
468    #[test]
469    fn truncated_data_error() {
470        let data = [1u8, 2]; // too short
471        let err = Dataspace::parse(&data, 8).unwrap_err();
472        assert!(matches!(err, FormatError::UnexpectedEof { .. }));
473    }
474
475    #[test]
476    fn invalid_version_error() {
477        let data = [5u8, 0, 0, 0, 0, 0, 0, 0];
478        let err = Dataspace::parse(&data, 8).unwrap_err();
479        assert_eq!(err, FormatError::InvalidDataspaceVersion(5));
480    }
481
482    #[test]
483    fn invalid_v2_type_error() {
484        let data = build_v2_dataspace(0, 0, 5, &[], None);
485        let err = Dataspace::parse(&data, 8).unwrap_err();
486        assert_eq!(err, FormatError::InvalidDataspaceType(5));
487    }
488
489    #[test]
490    fn v1_with_max_dims() {
491        let data = build_v1_dataspace(1, 0x01, &[5], Some(&[10]));
492        let ds = Dataspace::parse(&data, 8).unwrap();
493        assert_eq!(ds.max_dimensions, Some(vec![MaxExtent::Fixed(10)]));
494    }
495
496    #[test]
497    fn all_ones_max_dimension_is_unlimited_only_at_eight_bytes() {
498        let eight = build_v1_dataspace(1, 0x01, &[5], Some(&[u64::MAX]));
499        let ds = Dataspace::parse(&eight, 8).unwrap();
500        assert_eq!(ds.max_dimensions, Some(vec![MaxExtent::Unlimited]));
501
502        // Four bytes cannot hold `H5S_UNLIMITED`: the C library zero-extends
503        // the field, so all ones in four bytes is the finite maximum 2^32 - 1.
504        let mut four = Vec::new();
505        four.extend_from_slice(&[1, 1, 0x01, 0]);
506        four.extend_from_slice(&[0u8; 4]);
507        four.extend_from_slice(&5u32.to_le_bytes());
508        four.extend_from_slice(&u32::MAX.to_le_bytes());
509        let ds = Dataspace::parse(&four, 4).unwrap();
510        assert_eq!(
511            ds.max_dimensions,
512            Some(vec![MaxExtent::Fixed(u64::from(u32::MAX))])
513        );
514    }
515
516    #[test]
517    fn serialize_writes_all_ones_for_an_unlimited_maximum() {
518        let ds = Dataspace {
519            space_type: DataspaceType::Simple,
520            rank: 2,
521            dimensions: vec![4, 6],
522            max_dimensions: Some(vec![MaxExtent::Unlimited, MaxExtent::Fixed(6)]),
523        };
524        let bytes = ds.serialize(8);
525        assert_eq!(bytes[..4], [2, 2, 0x01, 1]);
526        assert_eq!(bytes[20..28], u64::MAX.to_le_bytes());
527        assert_eq!(bytes[28..36], 6u64.to_le_bytes());
528        assert_eq!(Dataspace::parse(&bytes, 8).unwrap(), ds);
529    }
530
531    #[test]
532    fn extensible_max_dimensions_skips_a_maximum_at_the_current_shape() {
533        let fixed = Dataspace {
534            space_type: DataspaceType::Simple,
535            rank: 2,
536            dimensions: vec![4, 6],
537            max_dimensions: Some(vec![MaxExtent::Fixed(4), MaxExtent::Fixed(6)]),
538        };
539        assert_eq!(fixed.extensible_max_dimensions(), None);
540
541        let none = Dataspace {
542            max_dimensions: None,
543            ..fixed.clone()
544        };
545        assert_eq!(none.extensible_max_dimensions(), None);
546
547        let grown = Dataspace {
548            max_dimensions: Some(vec![MaxExtent::Fixed(4), MaxExtent::Unlimited]),
549            ..fixed
550        };
551        assert_eq!(
552            grown.extensible_max_dimensions(),
553            Some(&[MaxExtent::Fixed(4), MaxExtent::Unlimited][..])
554        );
555    }
556
557    #[test]
558    fn an_extent_joins_a_shape_with_a_maximum_that_bounds_it() {
559        let extent = Extent::new(&[4, 6], Some(&[MaxExtent::Unlimited, MaxExtent::Fixed(6)]))
560            .expect("an unlimited dimension bounds anything, and 6 bounds 6");
561        assert_eq!(extent.dims(), &[4, 6]);
562        assert_eq!(extent.unlimited_dimensions(), 1);
563
564        let bounded = Extent::new(&[4, 6], None).unwrap();
565        assert_eq!(bounded.unlimited_dimensions(), 0);
566    }
567
568    #[test]
569    fn an_extent_rejects_a_maximum_shape_that_disagrees_with_its_shape() {
570        assert_eq!(
571            Extent::new(&[4], Some(&[MaxExtent::Fixed(4), MaxExtent::Fixed(4)])).unwrap_err(),
572            "maxshape must have the same rank as the dataset shape"
573        );
574        assert_eq!(
575            Extent::new(&[4], Some(&[MaxExtent::Fixed(3)])).unwrap_err(),
576            "maxshape must be at least the current shape in every dimension"
577        );
578    }
579
580    #[test]
581    fn an_extent_rejects_a_fixed_maximum_at_the_unlimited_size() {
582        let err = Extent::new(&[4], Some(&[MaxExtent::Fixed(u64::MAX)])).unwrap_err();
583        assert!(err.contains("the format's unlimited marker"), "{err}");
584    }
585
586    #[test]
587    fn max_extent_reports_its_size_and_bounds_a_dimension() {
588        assert_eq!(MaxExtent::Fixed(7).size(), Some(7));
589        assert_eq!(MaxExtent::Unlimited.size(), None);
590        assert!(!MaxExtent::Fixed(7).is_unlimited());
591        assert!(MaxExtent::Unlimited.is_unlimited());
592        assert!(MaxExtent::Fixed(7).admits(7));
593        assert!(!MaxExtent::Fixed(7).admits(8));
594        assert!(MaxExtent::Unlimited.admits(u64::MAX));
595    }
596}