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    // ------------------------------------------------------------------ encode
131
132    pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
133        self.encode_for(ctx, crate::format::ObjectFormat::Modern)
134    }
135
136    /// The message version libhdf5 would stamp on this dataspace in a file of
137    /// this `format` — `H5S__set_version`: the floor from
138    /// `H5O_sdspace_ver_bounds`, raised to 2 when the class needs it. Only the
139    /// NULL class does: version 1 has no type field, so it cannot say "no
140    /// elements" at all, and upstream `H5S_set_version` raises the same way.
141    fn version_for(&self, format: crate::format::ObjectFormat) -> u8 {
142        let needed = if self.class == DataspaceClass::Null {
143            2
144        } else {
145            1
146        };
147        needed.max(format.dataspace_version())
148    }
149
150    /// Encode for a file of the given object format.
151    ///
152    /// A version-1 dataspace differs only in its prefix: no type byte, and
153    /// five reserved bytes where version 2 has one, so the header is 8 bytes
154    /// rather than 4. The dimensions that follow are identical.
155    pub fn encode_for(&self, ctx: &FormatContext, format: crate::format::ObjectFormat) -> Vec<u8> {
156        let version = self.version_for(format);
157        let ndims = self.dims.len();
158        let ss = ctx.sizeof_size as usize;
159        // `H5O__sdspace_encode` raises `H5S_VALID_MAX` for a non-null maximum
160        // array and for nothing else (H5Osdspace.c:271-272), so an extent that
161        // reached us without one is written without one.
162        let max_dims = self.max_dims.as_deref();
163        let has_max = max_dims.is_some();
164        let flags: u8 = if has_max { FLAG_MAX_DIMS } else { 0 };
165
166        let ds_type = match self.class {
167            DataspaceClass::Scalar => DS_TYPE_SCALAR,
168            DataspaceClass::Simple => DS_TYPE_SIMPLE,
169            DataspaceClass::Null => DS_TYPE_NULL,
170        };
171
172        let prefix_len = if version == 1 { 8 } else { 4 };
173        let body_len = prefix_len + ndims * ss + if has_max { ndims * ss } else { 0 };
174        let mut buf = Vec::with_capacity(body_len);
175
176        buf.push(version);
177        buf.push(ndims as u8);
178        buf.push(flags);
179        if version == 1 {
180            // reserved byte, then a reserved word: version 1 has no type
181            // field, and a rank-0 version-1 dataspace is scalar by definition.
182            buf.extend_from_slice(&[0u8; 5]);
183        } else {
184            buf.push(ds_type);
185        }
186
187        // current dimensions
188        for &d in &self.dims {
189            buf.extend_from_slice(&d.to_le_bytes()[..ss]);
190        }
191
192        // max dimensions
193        if let Some(maxes) = max_dims {
194            for &m in maxes {
195                buf.extend_from_slice(&m.to_le_bytes()[..ss]);
196            }
197        }
198
199        buf
200    }
201
202    // ------------------------------------------------------------------ decode
203
204    pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
205        if buf.len() < 4 {
206            return Err(FormatError::BufferTooShort {
207                needed: 4,
208                available: buf.len(),
209            });
210        }
211
212        let version = buf[0];
213        match version {
214            1 => Self::decode_v1(buf, ctx),
215            VERSION => Self::decode_v2(buf, ctx),
216            _ => Err(FormatError::InvalidVersion(version)),
217        }
218    }
219
220    /// Decode version 2 dataspace message.
221    fn decode_v2(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
222        let ndims = buf[1] as usize;
223        let flags = buf[2];
224        let class = match buf[3] {
225            DS_TYPE_SCALAR => DataspaceClass::Scalar,
226            DS_TYPE_SIMPLE => DataspaceClass::Simple,
227            DS_TYPE_NULL => DataspaceClass::Null,
228            other => {
229                return Err(FormatError::InvalidData(format!(
230                    "dataspace type byte {other} is not scalar(0)/simple(1)/null(2)"
231                )))
232            }
233        };
234        let has_max = (flags & FLAG_MAX_DIMS) != 0;
235        let ss = ctx.sizeof_size as usize;
236
237        let needed = 4 + ndims * ss + if has_max { ndims * ss } else { 0 };
238        if buf.len() < needed {
239            return Err(FormatError::BufferTooShort {
240                needed,
241                available: buf.len(),
242            });
243        }
244
245        let mut pos = 4;
246
247        let mut dims = Vec::with_capacity(ndims);
248        for _ in 0..ndims {
249            dims.push(read_size(&buf[pos..], ss));
250            pos += ss;
251        }
252
253        let max_dims = if has_max {
254            let mut v = Vec::with_capacity(ndims);
255            for _ in 0..ndims {
256                v.push(read_size(&buf[pos..], ss));
257                pos += ss;
258            }
259            Some(v)
260        } else {
261            None
262        };
263
264        Ok((
265            Self {
266                class,
267                dims,
268                max_dims,
269            },
270            pos,
271        ))
272    }
273
274    /// Decode version 1 dataspace message.
275    ///
276    /// Version 1 layout:
277    /// ```text
278    /// Byte 0: version = 1
279    /// Byte 1: ndims
280    /// Byte 2: flags (bit 0 = max dims present, bit 1 = permutation indices present)
281    /// Byte 3: reserved
282    /// Bytes 4-7: reserved (4 bytes)
283    /// Then ndims * sizeof_size bytes for current dimensions
284    /// Then (if flag bit 0) ndims * sizeof_size bytes for max dimensions
285    /// Then (if flag bit 1) ndims * sizeof_size bytes for permutation indices
286    /// ```
287    fn decode_v1(buf: &[u8], ctx: &FormatContext) -> FormatResult<(Self, usize)> {
288        if buf.len() < 8 {
289            return Err(FormatError::BufferTooShort {
290                needed: 8,
291                available: buf.len(),
292            });
293        }
294
295        let ndims = buf[1] as usize;
296        let flags = buf[2];
297        let has_max = (flags & FLAG_MAX_DIMS) != 0;
298        let has_perm = (flags & 0x02) != 0;
299        let ss = ctx.sizeof_size as usize;
300
301        // Header is 8 bytes for v1 (4 fixed + 4 reserved)
302        let mut needed = 8 + ndims * ss;
303        if has_max {
304            needed += ndims * ss;
305        }
306        if has_perm {
307            needed += ndims * ss;
308        }
309        if buf.len() < needed {
310            return Err(FormatError::BufferTooShort {
311                needed,
312                available: buf.len(),
313            });
314        }
315
316        let mut pos = 8; // skip version(1) + ndims(1) + flags(1) + reserved(1) + reserved(4)
317
318        let mut dims = Vec::with_capacity(ndims);
319        for _ in 0..ndims {
320            dims.push(read_size(&buf[pos..], ss));
321            pos += ss;
322        }
323
324        let max_dims = if has_max {
325            let mut v = Vec::with_capacity(ndims);
326            for _ in 0..ndims {
327                v.push(read_size(&buf[pos..], ss));
328                pos += ss;
329            }
330            Some(v)
331        } else {
332            None
333        };
334
335        // Skip permutation indices if present
336        if has_perm {
337            pos += ndims * ss;
338        }
339
340        // Version 1 predates the NULL dataspace (added with the version-2
341        // message, `H5Osdspace.c`): it carries no type byte, so the class is
342        // always inferred from rank, same as `simple()`/`unlimited()`.
343        Ok((
344            Self {
345                class: Self::class_for_rank(ndims),
346                dims,
347                max_dims,
348            },
349            pos,
350        ))
351    }
352}
353
354// ======================================================================= tests
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    fn ctx8() -> FormatContext {
361        FormatContext {
362            sizeof_addr: 8,
363            sizeof_size: 8,
364        }
365    }
366
367    fn ctx4() -> FormatContext {
368        FormatContext {
369            sizeof_addr: 4,
370            sizeof_size: 4,
371        }
372    }
373
374    #[test]
375    fn roundtrip_scalar() {
376        let msg = DataspaceMessage::scalar();
377        let encoded = msg.encode(&ctx8());
378        assert_eq!(encoded.len(), 4); // version + ndims + flags + type
379        let (decoded, consumed) = DataspaceMessage::decode(&encoded, &ctx8()).unwrap();
380        assert_eq!(consumed, 4);
381        assert_eq!(decoded, msg);
382    }
383
384    /// A simple dataspace whose maximum the caller left implicit is written
385    /// with the maximum `H5S_set_extent_simple` fills in from the current
386    /// dimensions (H5S.c:1292-1299), so the message names them and is that
387    /// much longer.
388    #[test]
389    fn roundtrip_simple_1d() {
390        let msg = DataspaceMessage::simple(&[100]);
391        let encoded = msg.encode(&ctx8());
392        // 4 header + 1*8 dims + 1*8 max dims = 20
393        assert_eq!(encoded.len(), 20);
394        assert_eq!(encoded[2], FLAG_MAX_DIMS);
395        let (decoded, consumed) = DataspaceMessage::decode(&encoded, &ctx8()).unwrap();
396        assert_eq!(consumed, 20);
397        assert_eq!(decoded.class, msg.class);
398        assert_eq!(decoded.dims, msg.dims);
399        assert_eq!(decoded.max_dims, Some(vec![100]));
400    }
401
402    #[test]
403    fn roundtrip_simple_3d_ctx4() {
404        let msg = DataspaceMessage::simple(&[10, 20, 30]);
405        let encoded = msg.encode(&ctx4());
406        // 4 + 3*4 dims + 3*4 max dims = 28
407        assert_eq!(encoded.len(), 28);
408        let (decoded, consumed) = DataspaceMessage::decode(&encoded, &ctx4()).unwrap();
409        assert_eq!(consumed, 28);
410        assert_eq!(decoded.class, msg.class);
411        assert_eq!(decoded.dims, msg.dims);
412        assert_eq!(decoded.max_dims, Some(vec![10, 20, 30]));
413    }
414
415    #[test]
416    fn roundtrip_unlimited() {
417        let msg = DataspaceMessage::unlimited(&[5, 10]);
418        let encoded = msg.encode(&ctx8());
419        // 4 + 2*8 dims + 2*8 max = 36
420        assert_eq!(encoded.len(), 36);
421        let (decoded, consumed) = DataspaceMessage::decode(&encoded, &ctx8()).unwrap();
422        assert_eq!(consumed, 36);
423        assert_eq!(decoded, msg);
424        assert_eq!(decoded.max_dims.as_ref().unwrap(), &vec![u64::MAX; 2]);
425    }
426
427    #[test]
428    fn roundtrip_partial_max() {
429        let msg = DataspaceMessage {
430            class: DataspaceClass::Simple,
431            dims: vec![3, 4],
432            max_dims: Some(vec![100, u64::MAX]),
433        };
434        let encoded = msg.encode(&ctx8());
435        let (decoded, _) = DataspaceMessage::decode(&encoded, &ctx8()).unwrap();
436        assert_eq!(decoded, msg);
437    }
438
439    #[test]
440    fn decode_bad_version() {
441        let buf = [99u8, 0, 0, 0]; // version 99 — unsupported
442        let err = DataspaceMessage::decode(&buf, &ctx8()).unwrap_err();
443        match err {
444            FormatError::InvalidVersion(99) => {}
445            other => panic!("unexpected error: {:?}", other),
446        }
447    }
448
449    #[test]
450    fn decode_v1_simple_1d() {
451        // Build a version 1 dataspace: 1D, dims=[100], no max
452        let mut buf = vec![
453            1, // version 1
454            1, // ndims = 1
455            0, // flags (no max dims)
456            0, // reserved
457        ];
458        buf.extend_from_slice(&[0u8; 4]); // reserved (4 bytes)
459        buf.extend_from_slice(&100u64.to_le_bytes()); // dims[0] = 100
460
461        let (msg, consumed) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
462        assert_eq!(consumed, 16); // 8 header + 8 dim
463        assert_eq!(msg.dims, vec![100]);
464        assert_eq!(msg.max_dims, None);
465    }
466
467    /// A message that stores no maximum is re-encoded without one, at either
468    /// version. Upstream keeps the state because `H5O__sdspace_encode` raises
469    /// `H5S_VALID_MAX` only for a non-null `extent.max` (H5Osdspace.c:271-272)
470    /// and the decoder leaves it null when the flag was clear
471    /// (H5Osdspace.c:188-194); a re-encode that invented a maximum would make
472    /// the message eight bytes per dimension longer than the one libhdf5
473    /// writes for the same extent.
474    #[test]
475    fn a_decoded_message_with_no_maximum_re_encodes_without_one() {
476        let mut buf = vec![1, 1, 0, 0];
477        buf.extend_from_slice(&[0u8; 4]);
478        buf.extend_from_slice(&100u64.to_le_bytes());
479
480        let (msg, _) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
481        assert_eq!(msg.max_dims, None);
482        assert_eq!(
483            msg.encode_for(&ctx8(), crate::format::ObjectFormat::Legacy),
484            buf
485        );
486
487        let modern = msg.encode_for(&ctx8(), crate::format::ObjectFormat::Modern);
488        assert_eq!(modern.len(), 12, "4-byte prefix and one dimension");
489        assert_eq!(modern[2], 0, "H5S_VALID_MAX stays clear");
490        let (round, _) = DataspaceMessage::decode(&modern, &ctx8()).unwrap();
491        assert_eq!(round, msg);
492    }
493
494    /// The constructors fill the maximum in the way `H5S_set_extent_simple`
495    /// does (H5S.c:1293-1299), so nothing the crate builds reaches the encoder
496    /// without one — the absent maximum above is only ever read from a file.
497    #[test]
498    fn a_constructed_simple_dataspace_carries_its_maximum() {
499        assert_eq!(DataspaceMessage::simple(&[3, 4]).max_dims, Some(vec![3, 4]));
500        assert_eq!(DataspaceMessage::simple(&[]).max_dims, None);
501        assert_eq!(DataspaceMessage::scalar().max_dims, None);
502        assert_eq!(DataspaceMessage::null().max_dims, None);
503    }
504
505    #[test]
506    fn decode_v1_with_max_dims() {
507        let mut buf = vec![
508            1, // version 1
509            2, // ndims = 2
510            1, // flags = has max dims
511            0, // reserved
512        ];
513        buf.extend_from_slice(&[0u8; 4]); // reserved
514        buf.extend_from_slice(&10u64.to_le_bytes()); // dims[0] = 10
515        buf.extend_from_slice(&20u64.to_le_bytes()); // dims[1] = 20
516        buf.extend_from_slice(&u64::MAX.to_le_bytes()); // max_dims[0] = unlimited
517        buf.extend_from_slice(&100u64.to_le_bytes()); // max_dims[1] = 100
518
519        let (msg, consumed) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
520        assert_eq!(consumed, 40); // 8 + 2*8 + 2*8
521        assert_eq!(msg.dims, vec![10, 20]);
522        assert_eq!(msg.max_dims, Some(vec![u64::MAX, 100]));
523    }
524
525    #[test]
526    fn decode_buffer_too_short() {
527        let buf = [2u8, 1, 0]; // version ok, but too short (need 4 header bytes)
528        let err = DataspaceMessage::decode(&buf, &ctx8()).unwrap_err();
529        match err {
530            FormatError::BufferTooShort { .. } => {}
531            other => panic!("unexpected error: {:?}", other),
532        }
533    }
534
535    #[test]
536    fn decode_buffer_too_short_for_dims() {
537        // ndims=1, no max, sizeof_size=8 => need 4 + 8 = 12 bytes, give 6
538        let buf = [2u8, 1, 0, 1, 0, 0];
539        let err = DataspaceMessage::decode(&buf, &ctx8()).unwrap_err();
540        match err {
541            FormatError::BufferTooShort {
542                needed: 12,
543                available: 6,
544            } => {}
545            other => panic!("unexpected error: {:?}", other),
546        }
547    }
548
549    #[test]
550    fn version_byte_is_two() {
551        let msg = DataspaceMessage::simple(&[42]);
552        let encoded = msg.encode(&ctx8());
553        assert_eq!(encoded[0], 2);
554    }
555
556    #[test]
557    fn roundtrip_null() {
558        let msg = DataspaceMessage::null();
559        let encoded = msg.encode(&ctx8());
560        assert_eq!(encoded.len(), 4); // version + ndims + flags + type, no dims
561        assert_eq!(encoded[3], 2); // type byte: null
562        let (decoded, consumed) = DataspaceMessage::decode(&encoded, &ctx8()).unwrap();
563        assert_eq!(consumed, 4);
564        assert_eq!(decoded, msg);
565        assert!(decoded.is_null());
566    }
567
568    /// A NULL dataspace and a scalar dataspace both have an empty `dims`,
569    /// but must not decode to the same message: only the type byte tells
570    /// them apart, and it must round-trip distinctly.
571    #[test]
572    fn null_and_scalar_are_distinct() {
573        let null = DataspaceMessage::null();
574        let scalar = DataspaceMessage::scalar();
575        assert_ne!(null, scalar);
576        assert!(null.is_null());
577        assert!(!scalar.is_null());
578
579        let null_encoded = null.encode(&ctx8());
580        let scalar_encoded = scalar.encode(&ctx8());
581        assert_ne!(null_encoded[3], scalar_encoded[3]);
582
583        let (null_decoded, _) = DataspaceMessage::decode(&null_encoded, &ctx8()).unwrap();
584        let (scalar_decoded, _) = DataspaceMessage::decode(&scalar_encoded, &ctx8()).unwrap();
585        assert!(null_decoded.is_null());
586        assert!(!scalar_decoded.is_null());
587    }
588
589    #[test]
590    fn decode_v2_bad_type_byte() {
591        // version 2, ndims=0, flags=0, type=3 (not scalar/simple/null)
592        let buf = [2u8, 0, 0, 3];
593        let err = DataspaceMessage::decode(&buf, &ctx8()).unwrap_err();
594        match err {
595            FormatError::InvalidData(_) => {}
596            other => panic!("unexpected error: {:?}", other),
597        }
598    }
599
600    #[test]
601    fn decode_v1_is_never_null() {
602        // Version 1 has no type byte; rank 0 must decode as Scalar, not Null.
603        let buf = [1u8, 0, 0, 0, 0, 0, 0, 0];
604        let (msg, _) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
605        assert!(!msg.is_null());
606        assert_eq!(msg.class, DataspaceClass::Scalar);
607    }
608
609    /// The 24 bytes libhdf5 1.14.6 wrote for the dataspace of a shape-(6,)
610    /// dataset in a default (superblock-0) file: version 1, an 8-byte prefix,
611    /// then current and maximum dimensions.
612    #[test]
613    fn a_legacy_dataspace_matches_the_bytes_libhdf5_wrote() {
614        let ds = DataspaceMessage {
615            class: DataspaceClass::Simple,
616            dims: vec![6],
617            max_dims: Some(vec![6]),
618        };
619        let buf = ds.encode_for(&ctx8(), crate::format::ObjectFormat::Legacy);
620        assert_eq!(
621            buf,
622            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]
623        );
624        let (back, consumed) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
625        assert_eq!(consumed, buf.len());
626        assert_eq!(back, ds);
627    }
628
629    /// Version 1 has no type field, so it cannot say "no elements"; upstream
630    /// `H5S_set_version` raises a null dataspace to version 2 whatever the
631    /// bound says, and so must this.
632    #[test]
633    fn a_null_dataspace_stays_at_version_2_in_a_legacy_file() {
634        let buf = DataspaceMessage::null().encode_for(&ctx8(), crate::format::ObjectFormat::Legacy);
635        assert_eq!(buf[0], 2);
636        assert_eq!(buf[3], DS_TYPE_NULL);
637        let (back, _) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
638        assert!(back.is_null());
639    }
640
641    /// A scalar has rank 0, so version 1's extra prefix bytes are the whole
642    /// message; nothing about the class reaches the file, and the decoder has
643    /// to infer it from the rank exactly as libhdf5 does.
644    #[test]
645    fn a_legacy_scalar_dataspace_round_trips_as_scalar() {
646        let buf =
647            DataspaceMessage::scalar().encode_for(&ctx8(), crate::format::ObjectFormat::Legacy);
648        assert_eq!(buf, vec![1, 0, 0, 0, 0, 0, 0, 0]);
649        let (back, _) = DataspaceMessage::decode(&buf, &ctx8()).unwrap();
650        assert_eq!(back.class, DataspaceClass::Scalar);
651    }
652}