Skip to main content

rust_hdf5/format/messages/
dataspace.rs

1//! Dataspace message (type 0x01) — describes dataset dimensionality.
2//!
3//! Binary layout (version 2):
4//!   Byte 0: version = 2
5//!   Byte 1: dimensionality (ndims, 0–32)
6//!   Byte 2: flags (bit 0 = max dims present)
7//!   Byte 3: type (0 = scalar, 1 = simple, 2 = null)
8//!   Then ndims * sizeof_size bytes for current dimensions
9//!   Then (if flag bit 0) ndims * sizeof_size bytes for max dimensions
10
11use crate::format::bytes::read_le_uint as read_size;
12use crate::format::{FormatContext, FormatError, FormatResult};
13
14const VERSION: u8 = 2;
15const FLAG_MAX_DIMS: u8 = 0x01;
16
17/// Dataspace type field values.
18const DS_TYPE_SCALAR: u8 = 0;
19const DS_TYPE_SIMPLE: u8 = 1;
20const DS_TYPE_NULL: u8 = 2;
21
22/// Which of the three dataspace classes a message describes (`H5Osdspace.c`'s
23/// type byte / `H5S_class_t`).
24///
25/// `Scalar` (rank 0, exactly one element) and `Null` (no elements at all) both
26/// carry an empty `dims`, so the class cannot be inferred from `dims` alone —
27/// it must be tracked explicitly, matching upstream's dedicated type field.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum DataspaceClass {
30    /// Rank 0: a single element, no dimensions.
31    Scalar,
32    /// Rank >= 1: an ordinary N-dimensional dataspace.
33    Simple,
34    /// No elements at all (`H5Screate(H5S_NULL)`) — distinct from a scalar
35    /// or from a `Simple` dataspace with a zero-length dimension.
36    Null,
37}
38
39/// Dataspace message payload.
40#[derive(Debug, Clone, PartialEq)]
41pub struct DataspaceMessage {
42    /// Which dataspace class this message describes.
43    pub class: DataspaceClass,
44    /// Current dimension sizes. Empty for `Scalar` and `Null`.
45    pub dims: Vec<u64>,
46    /// Maximum dimension sizes; an entry of `u64::MAX` means unlimited.
47    ///
48    /// `None` is upstream's `H5S_extent_t.max == NULL`: the extent carries no
49    /// maximum array at all, which is a different thing from a maximum that
50    /// happens to equal the current dimensions. Only decoding a message with
51    /// `H5S_VALID_MAX` clear produces it (H5Osdspace.c:188-194) — every extent
52    /// the API builds is given one (H5S.c:1293-1299) — and it survives being
53    /// rewritten, because `H5S_read` and `H5S_write` decode and re-encode the
54    /// one extent (H5S.c:1100, H5S.c:1039) and the encoder raises the flag
55    /// only for a non-null array (H5Osdspace.c:271-272).
56    ///
57    /// Upstream reads the absent array as the current dimensions wherever a
58    /// bound is reported (`H5S_extent_get_dims`, H5S.c:968-973), which is what
59    /// the crate's consumers do too; `H5S_set_extent` is the exception that
60    /// treats it as no bound at all (H5S.c:1777).
61    pub max_dims: Option<Vec<u64>>,
62}
63
64impl DataspaceMessage {
65    // ------------------------------------------------------------------ factories
66
67    /// A scalar dataspace (rank 0, no max dims).
68    pub fn scalar() -> Self {
69        Self {
70            class: DataspaceClass::Scalar,
71            dims: Vec::new(),
72            max_dims: None,
73        }
74    }
75
76    /// The NULL dataspace: no elements at all. Unlike [`scalar`](Self::scalar),
77    /// a dataset with this dataspace holds zero bytes of data.
78    pub fn null() -> Self {
79        Self {
80            class: DataspaceClass::Null,
81            dims: Vec::new(),
82            max_dims: None,
83        }
84    }
85
86    /// A simple dataspace with fixed dimensions (max == current). An empty
87    /// `dims` yields a scalar dataspace, matching how upstream never emits a
88    /// `Simple`-class dataspace at rank 0.
89    ///
90    /// The maximum is filled in here, not at encode time, because that is
91    /// where upstream fills it in: `H5S_set_extent_simple` allocates
92    /// `extent.max` for every simple extent and copies the current dimensions
93    /// into it when the caller named no maximum (H5S.c:1293-1299).
94    pub fn simple(dims: &[u64]) -> Self {
95        let class = Self::class_for_rank(dims.len());
96        Self {
97            class,
98            dims: dims.to_vec(),
99            max_dims: match class {
100                DataspaceClass::Simple => Some(dims.to_vec()),
101                DataspaceClass::Scalar | DataspaceClass::Null => None,
102            },
103        }
104    }
105
106    /// A simple dataspace where every dimension is unlimited.
107    pub fn unlimited(current: &[u64]) -> Self {
108        Self {
109            class: Self::class_for_rank(current.len()),
110            dims: current.to_vec(),
111            max_dims: Some(vec![u64::MAX; current.len()]),
112        }
113    }
114
115    /// `Scalar` at rank 0 (matching upstream, which never writes a
116    /// `Simple`-class dataspace with zero dimensions), `Simple` otherwise.
117    fn class_for_rank(ndims: usize) -> DataspaceClass {
118        if ndims == 0 {
119            DataspaceClass::Scalar
120        } else {
121            DataspaceClass::Simple
122        }
123    }
124
125    /// Whether this is the NULL dataspace (no elements at all).
126    pub fn is_null(&self) -> bool {
127        self.class == DataspaceClass::Null
128    }
129
130    /// The number of elements in the extent (`H5S_GET_EXTENT_NPOINTS`): none
131    /// for a null dataspace, one for a scalar, the product of the dimensions
132    /// otherwise. `None` when that product does not fit in a `u64`.
133    pub fn element_count(&self) -> Option<u64> {
134        match self.class {
135            DataspaceClass::Null => Some(0),
136            DataspaceClass::Scalar => Some(1),
137            DataspaceClass::Simple => self.dims.iter().try_fold(1u64, |n, &d| n.checked_mul(d)),
138        }
139    }
140
141    // ------------------------------------------------------------------ encode
142
143    pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
144        self.encode_for(ctx, crate::format::ObjectFormat::Modern)
145    }
146
147    /// The message version libhdf5 would stamp on this dataspace in a file of
148    /// this `format` — `H5S__set_version`: the floor from
149    /// `H5O_sdspace_ver_bounds`, raised to 2 when the class needs it. Only the
150    /// NULL class does: version 1 has no type field, so it cannot say "no
151    /// elements" at all, and upstream `H5S_set_version` raises the same way.
152    fn version_for(&self, format: crate::format::ObjectFormat) -> u8 {
153        let needed = if self.class == DataspaceClass::Null {
154            2
155        } else {
156            1
157        };
158        needed.max(format.dataspace_version())
159    }
160
161    /// Encode for a file of the given object format.
162    ///
163    /// A version-1 dataspace differs only in its prefix: no type byte, and
164    /// five reserved bytes where version 2 has one, so the header is 8 bytes
165    /// rather than 4. The dimensions that follow are identical.
166    pub fn encode_for(&self, ctx: &FormatContext, format: crate::format::ObjectFormat) -> Vec<u8> {
167        let version = self.version_for(format);
168        let ndims = self.dims.len();
169        let ss = ctx.sizeof_size as usize;
170        // `H5O__sdspace_encode` raises `H5S_VALID_MAX` for a non-null maximum
171        // array and for nothing else (H5Osdspace.c:271-272), so an extent that
172        // reached us without one is written without one.
173        let max_dims = self.max_dims.as_deref();
174        let has_max = max_dims.is_some();
175        let flags: u8 = if has_max { FLAG_MAX_DIMS } else { 0 };
176
177        let ds_type = match self.class {
178            DataspaceClass::Scalar => DS_TYPE_SCALAR,
179            DataspaceClass::Simple => DS_TYPE_SIMPLE,
180            DataspaceClass::Null => DS_TYPE_NULL,
181        };
182
183        let prefix_len = if version == 1 { 8 } else { 4 };
184        let body_len = prefix_len + ndims * ss + if has_max { ndims * ss } else { 0 };
185        let mut buf = Vec::with_capacity(body_len);
186
187        buf.push(version);
188        buf.push(ndims as u8);
189        buf.push(flags);
190        if version == 1 {
191            // reserved byte, then a reserved word: version 1 has no type
192            // field, and a rank-0 version-1 dataspace is scalar by definition.
193            buf.extend_from_slice(&[0u8; 5]);
194        } else {
195            buf.push(ds_type);
196        }
197
198        // current dimensions
199        for &d in &self.dims {
200            buf.extend_from_slice(&d.to_le_bytes()[..ss]);
201        }
202
203        // max dimensions
204        if let Some(maxes) = max_dims {
205            for &m in maxes {
206                buf.extend_from_slice(&m.to_le_bytes()[..ss]);
207            }
208        }
209
210        buf
211    }
212
213    // ------------------------------------------------------------------ decode
214
215    pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
216        if buf.len() < 4 {
217            return Err(FormatError::BufferTooShort {
218                needed: 4,
219                available: buf.len(),
220            });
221        }
222
223        let version = buf[0];
224        match version {
225            1 => Self::decode_v1(buf, ctx),
226            VERSION => Self::decode_v2(buf, ctx),
227            _ => Err(FormatError::InvalidVersion(version)),
228        }
229    }
230
231    /// Decode version 2 dataspace message.
232    fn decode_v2(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
233        let ndims = buf[1] as usize;
234        let flags = buf[2];
235        let class = match buf[3] {
236            DS_TYPE_SCALAR => DataspaceClass::Scalar,
237            DS_TYPE_SIMPLE => DataspaceClass::Simple,
238            DS_TYPE_NULL => DataspaceClass::Null,
239            other => {
240                return Err(FormatError::InvalidData(format!(
241                    "dataspace type byte {other} is not scalar(0)/simple(1)/null(2)"
242                )))
243            }
244        };
245        let has_max = (flags & FLAG_MAX_DIMS) != 0;
246        let ss = ctx.sizeof_size as usize;
247
248        let needed = 4 + ndims * ss + if has_max { ndims * ss } else { 0 };
249        if buf.len() < needed {
250            return Err(FormatError::BufferTooShort {
251                needed,
252                available: buf.len(),
253            });
254        }
255
256        let mut pos = 4;
257
258        let mut dims = Vec::with_capacity(ndims);
259        for _ in 0..ndims {
260            dims.push(read_size(&buf[pos..], ss));
261            pos += ss;
262        }
263
264        let max_dims = if has_max {
265            let mut v = Vec::with_capacity(ndims);
266            for _ in 0..ndims {
267                v.push(read_size(&buf[pos..], ss));
268                pos += ss;
269            }
270            Some(v)
271        } else {
272            None
273        };
274
275        Ok((
276            Self {
277                class,
278                dims,
279                max_dims,
280            },
281            pos,
282        ))
283    }
284
285    /// Decode version 1 dataspace message.
286    ///
287    /// Version 1 layout:
288    /// ```text
289    /// Byte 0: version = 1
290    /// Byte 1: ndims
291    /// Byte 2: flags (bit 0 = max dims present, bit 1 = permutation indices present)
292    /// Byte 3: reserved
293    /// Bytes 4-7: reserved (4 bytes)
294    /// Then ndims * sizeof_size bytes for current dimensions
295    /// Then (if flag bit 0) ndims * sizeof_size bytes for max dimensions
296    /// Then (if flag bit 1) ndims * sizeof_size bytes for permutation indices
297    /// ```
298    fn decode_v1(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
299        if buf.len() < 8 {
300            return Err(FormatError::BufferTooShort {
301                needed: 8,
302                available: buf.len(),
303            });
304        }
305
306        let ndims = buf[1] as usize;
307        let flags = buf[2];
308        let has_max = (flags & FLAG_MAX_DIMS) != 0;
309        let has_perm = (flags & 0x02) != 0;
310        let ss = ctx.sizeof_size as usize;
311
312        // Header is 8 bytes for v1 (4 fixed + 4 reserved)
313        let mut needed = 8 + ndims * ss;
314        if has_max {
315            needed += ndims * ss;
316        }
317        if has_perm {
318            needed += ndims * ss;
319        }
320        if buf.len() < needed {
321            return Err(FormatError::BufferTooShort {
322                needed,
323                available: buf.len(),
324            });
325        }
326
327        let mut pos = 8; // skip version(1) + ndims(1) + flags(1) + reserved(1) + reserved(4)
328
329        let mut dims = Vec::with_capacity(ndims);
330        for _ in 0..ndims {
331            dims.push(read_size(&buf[pos..], ss));
332            pos += ss;
333        }
334
335        let max_dims = if has_max {
336            let mut v = Vec::with_capacity(ndims);
337            for _ in 0..ndims {
338                v.push(read_size(&buf[pos..], ss));
339                pos += ss;
340            }
341            Some(v)
342        } else {
343            None
344        };
345
346        // Skip permutation indices if present
347        if has_perm {
348            pos += ndims * ss;
349        }
350
351        // Version 1 predates the NULL dataspace (added with the version-2
352        // message, `H5Osdspace.c`): it carries no type byte, so the class is
353        // always inferred from rank, same as `simple()`/`unlimited()`.
354        Ok((
355            Self {
356                class: Self::class_for_rank(ndims),
357                dims,
358                max_dims,
359            },
360            pos,
361        ))
362    }
363}
364
365// ======================================================================= tests
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    fn ctx8() -> FormatContext {
372        FormatContext {
373            sizeof_addr: 8,
374            sizeof_size: 8,
375        }
376    }
377
378    fn ctx4() -> FormatContext {
379        FormatContext {
380            sizeof_addr: 4,
381            sizeof_size: 4,
382        }
383    }
384
385    #[test]
386    fn roundtrip_scalar() {
387        let msg = DataspaceMessage::scalar();
388        let encoded = msg.encode(&ctx8());
389        assert_eq!(encoded.len(), 4); // version + ndims + flags + type
390        let (decoded, consumed) = DataspaceMessage::decode(&encoded, &ctx8()).unwrap();
391        assert_eq!(consumed, 4);
392        assert_eq!(decoded, msg);
393    }
394
395    /// A simple dataspace whose maximum the caller left implicit is written
396    /// with the maximum `H5S_set_extent_simple` fills in from the current
397    /// dimensions (H5S.c:1292-1299), so the message names them and is that
398    /// much longer.
399    #[test]
400    fn roundtrip_simple_1d() {
401        let msg = DataspaceMessage::simple(&[100]);
402        let encoded = msg.encode(&ctx8());
403        // 4 header + 1*8 dims + 1*8 max dims = 20
404        assert_eq!(encoded.len(), 20);
405        assert_eq!(encoded[2], FLAG_MAX_DIMS);
406        let (decoded, consumed) = DataspaceMessage::decode(&encoded, &ctx8()).unwrap();
407        assert_eq!(consumed, 20);
408        assert_eq!(decoded.class, msg.class);
409        assert_eq!(decoded.dims, msg.dims);
410        assert_eq!(decoded.max_dims, Some(vec![100]));
411    }
412
413    #[test]
414    fn roundtrip_simple_3d_ctx4() {
415        let msg = DataspaceMessage::simple(&[10, 20, 30]);
416        let encoded = msg.encode(&ctx4());
417        // 4 + 3*4 dims + 3*4 max dims = 28
418        assert_eq!(encoded.len(), 28);
419        let (decoded, consumed) = DataspaceMessage::decode(&encoded, &ctx4()).unwrap();
420        assert_eq!(consumed, 28);
421        assert_eq!(decoded.class, msg.class);
422        assert_eq!(decoded.dims, msg.dims);
423        assert_eq!(decoded.max_dims, Some(vec![10, 20, 30]));
424    }
425
426    #[test]
427    fn roundtrip_unlimited() {
428        let msg = DataspaceMessage::unlimited(&[5, 10]);
429        let encoded = msg.encode(&ctx8());
430        // 4 + 2*8 dims + 2*8 max = 36
431        assert_eq!(encoded.len(), 36);
432        let (decoded, consumed) = DataspaceMessage::decode(&encoded, &ctx8()).unwrap();
433        assert_eq!(consumed, 36);
434        assert_eq!(decoded, msg);
435        assert_eq!(decoded.max_dims.as_ref().unwrap(), &vec![u64::MAX; 2]);
436    }
437
438    #[test]
439    fn roundtrip_partial_max() {
440        let msg = DataspaceMessage {
441            class: DataspaceClass::Simple,
442            dims: vec![3, 4],
443            max_dims: Some(vec![100, u64::MAX]),
444        };
445        let encoded = msg.encode(&ctx8());
446        let (decoded, _) = DataspaceMessage::decode(&encoded, &ctx8()).unwrap();
447        assert_eq!(decoded, msg);
448    }
449
450    #[test]
451    fn decode_bad_version() {
452        let buf = [99u8, 0, 0, 0]; // version 99 — unsupported
453        let err = DataspaceMessage::decode(&buf, &ctx8()).unwrap_err();
454        match err {
455            FormatError::InvalidVersion(99) => {}
456            other => panic!("unexpected error: {:?}", other),
457        }
458    }
459
460    #[test]
461    fn decode_v1_simple_1d() {
462        // Build a version 1 dataspace: 1D, dims=[100], no max
463        let mut buf = vec![
464            1, // version 1
465            1, // ndims = 1
466            0, // flags (no max dims)
467            0, // reserved
468        ];
469        buf.extend_from_slice(&[0u8; 4]); // reserved (4 bytes)
470        buf.extend_from_slice(&100u64.to_le_bytes()); // dims[0] = 100
471
472        let (msg, consumed) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
473        assert_eq!(consumed, 16); // 8 header + 8 dim
474        assert_eq!(msg.dims, vec![100]);
475        assert_eq!(msg.max_dims, None);
476    }
477
478    /// A message that stores no maximum is re-encoded without one, at either
479    /// version. Upstream keeps the state because `H5O__sdspace_encode` raises
480    /// `H5S_VALID_MAX` only for a non-null `extent.max` (H5Osdspace.c:271-272)
481    /// and the decoder leaves it null when the flag was clear
482    /// (H5Osdspace.c:188-194); a re-encode that invented a maximum would make
483    /// the message eight bytes per dimension longer than the one libhdf5
484    /// writes for the same extent.
485    #[test]
486    fn a_decoded_message_with_no_maximum_re_encodes_without_one() {
487        let mut buf = vec![1, 1, 0, 0];
488        buf.extend_from_slice(&[0u8; 4]);
489        buf.extend_from_slice(&100u64.to_le_bytes());
490
491        let (msg, _) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
492        assert_eq!(msg.max_dims, None);
493        assert_eq!(
494            msg.encode_for(&ctx8(), crate::format::ObjectFormat::Legacy),
495            buf
496        );
497
498        let modern = msg.encode_for(&ctx8(), crate::format::ObjectFormat::Modern);
499        assert_eq!(modern.len(), 12, "4-byte prefix and one dimension");
500        assert_eq!(modern[2], 0, "H5S_VALID_MAX stays clear");
501        let (round, _) = DataspaceMessage::decode(&modern, &ctx8()).unwrap();
502        assert_eq!(round, msg);
503    }
504
505    /// The constructors fill the maximum in the way `H5S_set_extent_simple`
506    /// does (H5S.c:1293-1299), so nothing the crate builds reaches the encoder
507    /// without one — the absent maximum above is only ever read from a file.
508    #[test]
509    fn a_constructed_simple_dataspace_carries_its_maximum() {
510        assert_eq!(DataspaceMessage::simple(&[3, 4]).max_dims, Some(vec![3, 4]));
511        assert_eq!(DataspaceMessage::simple(&[]).max_dims, None);
512        assert_eq!(DataspaceMessage::scalar().max_dims, None);
513        assert_eq!(DataspaceMessage::null().max_dims, None);
514    }
515
516    #[test]
517    fn decode_v1_with_max_dims() {
518        let mut buf = vec![
519            1, // version 1
520            2, // ndims = 2
521            1, // flags = has max dims
522            0, // reserved
523        ];
524        buf.extend_from_slice(&[0u8; 4]); // reserved
525        buf.extend_from_slice(&10u64.to_le_bytes()); // dims[0] = 10
526        buf.extend_from_slice(&20u64.to_le_bytes()); // dims[1] = 20
527        buf.extend_from_slice(&u64::MAX.to_le_bytes()); // max_dims[0] = unlimited
528        buf.extend_from_slice(&100u64.to_le_bytes()); // max_dims[1] = 100
529
530        let (msg, consumed) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
531        assert_eq!(consumed, 40); // 8 + 2*8 + 2*8
532        assert_eq!(msg.dims, vec![10, 20]);
533        assert_eq!(msg.max_dims, Some(vec![u64::MAX, 100]));
534    }
535
536    #[test]
537    fn decode_buffer_too_short() {
538        let buf = [2u8, 1, 0]; // version ok, but too short (need 4 header bytes)
539        let err = DataspaceMessage::decode(&buf, &ctx8()).unwrap_err();
540        match err {
541            FormatError::BufferTooShort { .. } => {}
542            other => panic!("unexpected error: {:?}", other),
543        }
544    }
545
546    #[test]
547    fn decode_buffer_too_short_for_dims() {
548        // ndims=1, no max, sizeof_size=8 => need 4 + 8 = 12 bytes, give 6
549        let buf = [2u8, 1, 0, 1, 0, 0];
550        let err = DataspaceMessage::decode(&buf, &ctx8()).unwrap_err();
551        match err {
552            FormatError::BufferTooShort {
553                needed: 12,
554                available: 6,
555            } => {}
556            other => panic!("unexpected error: {:?}", other),
557        }
558    }
559
560    #[test]
561    fn version_byte_is_two() {
562        let msg = DataspaceMessage::simple(&[42]);
563        let encoded = msg.encode(&ctx8());
564        assert_eq!(encoded[0], 2);
565    }
566
567    #[test]
568    fn roundtrip_null() {
569        let msg = DataspaceMessage::null();
570        let encoded = msg.encode(&ctx8());
571        assert_eq!(encoded.len(), 4); // version + ndims + flags + type, no dims
572        assert_eq!(encoded[3], 2); // type byte: null
573        let (decoded, consumed) = DataspaceMessage::decode(&encoded, &ctx8()).unwrap();
574        assert_eq!(consumed, 4);
575        assert_eq!(decoded, msg);
576        assert!(decoded.is_null());
577    }
578
579    /// A NULL dataspace and a scalar dataspace both have an empty `dims`,
580    /// but must not decode to the same message: only the type byte tells
581    /// them apart, and it must round-trip distinctly.
582    #[test]
583    fn null_and_scalar_are_distinct() {
584        let null = DataspaceMessage::null();
585        let scalar = DataspaceMessage::scalar();
586        assert_ne!(null, scalar);
587        assert!(null.is_null());
588        assert!(!scalar.is_null());
589
590        let null_encoded = null.encode(&ctx8());
591        let scalar_encoded = scalar.encode(&ctx8());
592        assert_ne!(null_encoded[3], scalar_encoded[3]);
593
594        let (null_decoded, _) = DataspaceMessage::decode(&null_encoded, &ctx8()).unwrap();
595        let (scalar_decoded, _) = DataspaceMessage::decode(&scalar_encoded, &ctx8()).unwrap();
596        assert!(null_decoded.is_null());
597        assert!(!scalar_decoded.is_null());
598    }
599
600    #[test]
601    fn decode_v2_bad_type_byte() {
602        // version 2, ndims=0, flags=0, type=3 (not scalar/simple/null)
603        let buf = [2u8, 0, 0, 3];
604        let err = DataspaceMessage::decode(&buf, &ctx8()).unwrap_err();
605        match err {
606            FormatError::InvalidData(_) => {}
607            other => panic!("unexpected error: {:?}", other),
608        }
609    }
610
611    #[test]
612    fn decode_v1_is_never_null() {
613        // Version 1 has no type byte; rank 0 must decode as Scalar, not Null.
614        let buf = [1u8, 0, 0, 0, 0, 0, 0, 0];
615        let (msg, _) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
616        assert!(!msg.is_null());
617        assert_eq!(msg.class, DataspaceClass::Scalar);
618    }
619
620    /// The 24 bytes libhdf5 1.14.6 wrote for the dataspace of a shape-(6,)
621    /// dataset in a default (superblock-0) file: version 1, an 8-byte prefix,
622    /// then current and maximum dimensions.
623    #[test]
624    fn a_legacy_dataspace_matches_the_bytes_libhdf5_wrote() {
625        let ds = DataspaceMessage {
626            class: DataspaceClass::Simple,
627            dims: vec![6],
628            max_dims: Some(vec![6]),
629        };
630        let buf = ds.encode_for(&ctx8(), crate::format::ObjectFormat::Legacy);
631        assert_eq!(
632            buf,
633            vec![0x01, 0x01, 0x01, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0]
634        );
635        let (back, consumed) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
636        assert_eq!(consumed, buf.len());
637        assert_eq!(back, ds);
638    }
639
640    /// Version 1 has no type field, so it cannot say "no elements"; upstream
641    /// `H5S_set_version` raises a null dataspace to version 2 whatever the
642    /// bound says, and so must this.
643    #[test]
644    fn a_null_dataspace_stays_at_version_2_in_a_legacy_file() {
645        let buf = DataspaceMessage::null().encode_for(&ctx8(), crate::format::ObjectFormat::Legacy);
646        assert_eq!(buf[0], 2);
647        assert_eq!(buf[3], DS_TYPE_NULL);
648        let (back, _) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
649        assert!(back.is_null());
650    }
651
652    /// A scalar has rank 0, so version 1's extra prefix bytes are the whole
653    /// message; nothing about the class reaches the file, and the decoder has
654    /// to infer it from the rank exactly as libhdf5 does.
655    #[test]
656    fn a_legacy_scalar_dataspace_round_trips_as_scalar() {
657        let buf =
658            DataspaceMessage::scalar().encode_for(&ctx8(), crate::format::ObjectFormat::Legacy);
659        assert_eq!(buf, vec![1, 0, 0, 0, 0, 0, 0, 0]);
660        let (back, _) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
661        assert_eq!(back.class, DataspaceClass::Scalar);
662    }
663}