Skip to main content

rust_hdf5/format/
reference.rs

1//! Reference elements and the dataspace selections region references carry.
2//!
3//! Two element layouts exist for the pre-1.12 reference kinds, both written by
4//! h5py 3.x today (`H5Tref.c`):
5//!
6//! ```text
7//! H5R_OBJECT1          sizeof_addr bytes   the target's object header address
8//! H5R_DATASET_REGION1  sizeof_addr + 4     a global-heap id: collection
9//!                                          address then a u32 object index
10//! ```
11//!
12//! The heap object a region reference points at is the target's object header
13//! address followed by the serialized selection
14//! (`H5R__encode_token_region_compat`), whose wire format lives in the `H5S`
15//! serializers (`H5S__hyper_serialize`, `H5S__point_serialize`,
16//! `H5S__all_serialize`, `H5S__none_serialize`).
17//!
18//! The 1.12 kinds — `H5R_OBJECT2`, `H5R_DATASET_REGION2` and `H5R_ATTR`, all
19//! written as `H5T_STD_REF` — share one element layout instead
20//! (`H5T__ref_disk_getsize`):
21//!
22//! ```text
23//! type (1) | flags (1) | encoded reference           an H5R_OBJECT2 with no
24//!                                                    external file, stored
25//!                                                    inline
26//! type (1) | flags (1) | size (4) | heap id          everything else, whose
27//!                                                    encoded reference is a
28//!                                                    global-heap blob
29//! ```
30//!
31//! and the encoded reference itself is `H5R__encode`: a token (its length, then
32//! the target's object header address), then — when the flags carry
33//! `H5R_IS_EXTERNAL` — the name of the file the target lives in, then the
34//! serialized selection for a region and the attribute name for an attribute
35//! reference.
36
37use crate::format::bytes::{read_le_addr, read_le_uint};
38use crate::format::messages::datatype::ReferenceKind;
39use crate::format::selection::Selection;
40use crate::format::{FormatContext, FormatError, FormatResult, UNDEF_ADDR};
41
42/// The address a reference element leads with, or `None` when it names
43/// nothing.
44///
45/// Both element layouts start with a file address, and both spell "no target"
46/// the same two ways: the all-ones undefined address `H5F_addr_decode`
47/// produces, and 0 — the superblock's own address, so never an object header,
48/// and what an unwritten (fill-value) element holds. `H5R__decode_heap`
49/// rejects both together (`!H5_addr_defined(hobjid.addr) || hobjid.addr == 0`),
50/// so this crate applies the one rule to both kinds rather than per element
51/// layout.
52fn target_address(elem: &[u8], sizeof_addr: usize) -> Option<u64> {
53    match read_le_addr(elem, sizeof_addr) {
54        0 | UNDEF_ADDR => None,
55        addr => Some(addr),
56    }
57}
58
59/// The address a `H5R_OBJECT1` element names, or `None` for a null reference.
60pub fn decode_object_element(elem: &[u8], ctx: &FormatContext) -> FormatResult<Option<u64>> {
61    let sa = ctx.sizeof_addr as usize;
62    if elem.len() < sa {
63        return Err(FormatError::BufferTooShort {
64            needed: sa,
65            available: elem.len(),
66        });
67    }
68    Ok(target_address(elem, sa))
69}
70
71/// The `(collection address, object index)` a `H5R_DATASET_REGION1` element
72/// names, or `None` when the element is a null reference.
73pub fn decode_region_element(elem: &[u8], ctx: &FormatContext) -> FormatResult<Option<(u64, u32)>> {
74    let sa = ctx.sizeof_addr as usize;
75    if elem.len() < sa + 4 {
76        return Err(FormatError::BufferTooShort {
77            needed: sa + 4,
78            available: elem.len(),
79        });
80    }
81    let Some(addr) = target_address(elem, sa) else {
82        return Ok(None);
83    };
84    let idx = u32::from_le_bytes([elem[sa], elem[sa + 1], elem[sa + 2], elem[sa + 3]]);
85    Ok(Some((addr, idx)))
86}
87
88/// Split a region reference's heap object into the target's object header
89/// address and the selection over it.
90pub fn decode_region_heap_object(
91    data: &[u8],
92    ctx: &FormatContext,
93) -> FormatResult<(u64, Selection)> {
94    let sa = ctx.sizeof_addr as usize;
95    if data.len() < sa {
96        return Err(FormatError::BufferTooShort {
97            needed: sa,
98            available: data.len(),
99        });
100    }
101    let addr = read_le_addr(data, sa);
102    let (selection, _) = Selection::decode(&data[sa..])?;
103    Ok((addr, selection))
104}
105
106/// `H5R_IS_EXTERNAL`: the encoded reference carries the name of the file the
107/// target lives in, after the token.
108const REVISED_FLAG_EXTERNAL: u8 = 0x01;
109
110/// The two bytes every 1.12 element leads with, `H5R_ENCODE_HEADER_SIZE`.
111const REVISED_HEADER: usize = 2;
112
113/// Where a 1.12 element keeps its encoded reference.
114///
115/// `H5T__ref_disk_getsize` makes the same split: an `H5R_OBJECT2` naming an
116/// object in this file is short enough to sit in the element, and every other
117/// element holds a global-heap blob id instead.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum RevisedElement<'a> {
120    /// An element naming nothing: reference type 0 over a nil blob id.
121    Null,
122    /// The encoded reference, less its two-byte header, stored in the element.
123    /// Never external: `H5T__ref_disk_getsize` takes the direct-copy arm only
124    /// for an `H5R_OBJECT2` whose flags are clear (H5Tref.c:890).
125    Inline {
126        /// The kind the element's own type byte names.
127        kind: ReferenceKind,
128        /// The encoded reference from the token onwards.
129        body: &'a [u8],
130    },
131    /// The encoded reference lives in a global-heap collection.
132    Heap {
133        /// The kind the element's own type byte names.
134        kind: ReferenceKind,
135        /// Whether the element's flags carry `H5R_IS_EXTERNAL`, in which case
136        /// the encoded reference names the file the target lives in.
137        external: bool,
138        /// Address of the collection holding the blob.
139        collection: u64,
140        /// Index of the blob's object within that collection.
141        index: u32,
142    },
143}
144
145/// Split a 1.12 reference element into its kind and the encoded reference.
146///
147/// The element's type byte is the authority on the kind, not the datatype
148/// message: libhdf5 stores every `H5T_STD_REF` as `H5R_OBJECT2` and lets each
149/// element say what it actually holds.
150pub fn decode_revised_element<'a>(
151    elem: &'a [u8],
152    ctx: &FormatContext,
153) -> FormatResult<RevisedElement<'a>> {
154    let sa = ctx.sizeof_addr as usize;
155    let heap_id = REVISED_HEADER + 4;
156    if elem.len() < REVISED_HEADER {
157        return Err(FormatError::BufferTooShort {
158            needed: REVISED_HEADER,
159            available: elem.len(),
160        });
161    }
162    let (code, flags) = (elem[0], elem[1]);
163
164    // Reference type 0 is `H5R_BADTYPE`; `H5T__ref_disk_isnull` reads such an
165    // element as null when — and only when — the blob id it carries is the nil
166    // one (a zero collection address), which is what an unwritten element
167    // holds.
168    if code == 0 {
169        if elem.len() < heap_id + sa {
170            return Err(FormatError::BufferTooShort {
171                needed: heap_id + sa,
172                available: elem.len(),
173            });
174        }
175        return match read_le_addr(&elem[heap_id..], sa) {
176            0 => Ok(RevisedElement::Null),
177            addr => Err(FormatError::InvalidData(format!(
178                "reference type 0 over a blob at {addr:#x}, which is not the nil id a null \
179                 reference carries"
180            ))),
181        };
182    }
183
184    let kind = ReferenceKind::from_code(code)
185        .filter(|k| k.is_revised())
186        .ok_or_else(|| {
187            FormatError::InvalidData(format!("reference type {code} in a 1.12 element"))
188        })?;
189
190    let external = flags & REVISED_FLAG_EXTERNAL != 0;
191
192    if !external && kind == ReferenceKind::Object2 {
193        return Ok(RevisedElement::Inline {
194            kind,
195            body: &elem[REVISED_HEADER..],
196        });
197    }
198
199    if elem.len() < heap_id + sa + 4 {
200        return Err(FormatError::BufferTooShort {
201            needed: heap_id + sa + 4,
202            available: elem.len(),
203        });
204    }
205    let collection = read_le_addr(&elem[heap_id..], sa);
206    let index = read_le_uint(&elem[heap_id + sa..heap_id + sa + 4], 4) as u32;
207    Ok(RevisedElement::Heap {
208        kind,
209        external,
210        collection,
211        index,
212    })
213}
214
215/// What one reference element holds — the encode-side mirror of
216/// [`RevisedElement`], which is what a reader splits an element back into.
217///
218/// The variant carries everything its own layout needs, so
219/// [`encode_reference_element`] is total: there is no kind it cannot write,
220/// and no way to hand it a body its layout has no room for.
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum ReferenceElementImage {
223    /// `H5R_OBJECT1`: the element is the target's object header address and
224    /// nothing else.
225    Legacy(u64),
226    /// `H5R_OBJECT2` naming an object in this file: short enough that the
227    /// whole encoded reference sits in the element
228    /// (`H5T__ref_disk_getsize`'s direct-copy arm).
229    Inline(u64),
230    /// Every other 1.12 element: the encoded reference lives in a global-heap
231    /// blob and the element carries the blob's byte count and its id
232    /// (`H5T__ref_disk_write`, which copies the two-byte header across, then
233    /// the size, then what `H5VL__native_blob_put` leaves).
234    Blob {
235        /// The kind the element's own type byte names.
236        kind: ReferenceKind,
237        /// The blob's byte count: the encoded reference less its two-byte
238        /// header, which the element keeps instead.
239        size: u32,
240        /// Address of the collection holding the blob.
241        collection: u64,
242        /// Index of the blob's object within that collection.
243        index: u32,
244    },
245}
246
247/// The image one reference element has, at the `size` its datatype declares.
248///
249/// The single owner of reference-element encoding, so every element layout
250/// this module documents is written where it is read. Anything past what the
251/// layout needs is left zero, which is where libhdf5 leaves it too: it sizes
252/// every element for the largest reference the dataset may hold and writes
253/// only as much of it as the reference uses.
254pub fn encode_reference_element(
255    image: &ReferenceElementImage,
256    size: usize,
257    ctx: &FormatContext,
258) -> FormatResult<Vec<u8>> {
259    let sa = ctx.sizeof_addr as usize;
260    let mut elem = vec![0u8; size];
261    let (what, needed) = match image {
262        ReferenceElementImage::Legacy(_) => ("an H5R_OBJECT1", sa),
263        ReferenceElementImage::Inline(_) => ("an H5R_OBJECT2", REVISED_HEADER + 1 + sa),
264        ReferenceElementImage::Blob { kind, .. } => (
265            match kind {
266                ReferenceKind::DatasetRegion2 => "an H5R_DATASET_REGION2",
267                ReferenceKind::Attr => "an H5R_ATTR",
268                _ => "a blob-backed",
269            },
270            REVISED_HEADER + 4 + sa + 4,
271        ),
272    };
273    if needed > size {
274        return Err(FormatError::InvalidData(format!(
275            "{what} element needs {needed} bytes but its datatype declares {size}"
276        )));
277    }
278    match *image {
279        ReferenceElementImage::Legacy(address) => {
280            elem[..sa].copy_from_slice(&address.to_le_bytes()[..sa]);
281        }
282        // Type, flags, then the encoded reference inline: the token's length
283        // and the token itself (`H5R__encode_obj_token`).
284        ReferenceElementImage::Inline(address) => {
285            elem[0] = ReferenceKind::Object2.code();
286            elem[1] = 0;
287            elem[2] = sa as u8;
288            let at = REVISED_HEADER + 1;
289            elem[at..at + sa].copy_from_slice(&address.to_le_bytes()[..sa]);
290        }
291        ReferenceElementImage::Blob {
292            kind,
293            size: blob_size,
294            collection,
295            index,
296        } => {
297            elem[0] = kind.code();
298            elem[1] = 0;
299            elem[REVISED_HEADER..REVISED_HEADER + 4].copy_from_slice(&blob_size.to_le_bytes());
300            let at = REVISED_HEADER + 4;
301            elem[at..at + sa].copy_from_slice(&collection.to_le_bytes()[..sa]);
302            elem[at + sa..at + sa + 4].copy_from_slice(&index.to_le_bytes());
303        }
304    }
305    Ok(elem)
306}
307
308/// The encoded reference a 1.12 blob holds, less the two-byte header the
309/// element keeps — `H5R__encode` from the token onwards, which is exactly
310/// what [`decode_revised_body`] reads back.
311///
312/// The token is written as `address`; a caller that does not know the
313/// target's object header address yet passes 0 and patches those `sa` bytes
314/// at offset [`REVISED_BLOB_TOKEN_OFFSET`] once it does.
315///
316/// `extent_rank` is the rank of the dataspace the reference's selection is
317/// over, which is what `H5R__encode_region` encodes and where it takes it
318/// from — `H5S_get_simple_extent_ndims` of the space, not the serialized
319/// selection, which is why an `H5S_SEL_ALL` region still says a rank. It is 0
320/// for the two kinds that carry no selection.
321pub fn encode_revised_blob(
322    address: u64,
323    target: &ReferenceTarget,
324    extent_rank: usize,
325    ctx: &FormatContext,
326) -> FormatResult<Vec<u8>> {
327    let sa = ctx.sizeof_addr as usize;
328    let mut blob = Vec::with_capacity(1 + sa + 16);
329    blob.push(sa as u8);
330    blob.extend_from_slice(&address.to_le_bytes()[..sa]);
331    match target {
332        ReferenceTarget::Object => {}
333        // `H5R__encode_region`: the serialized selection's length, then the
334        // extent's rank, then the selection.
335        ReferenceTarget::Region(selection) => {
336            let bytes = selection.encode()?;
337            blob.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
338            blob.extend_from_slice(&(extent_rank as u32).to_le_bytes());
339            blob.extend_from_slice(&bytes);
340        }
341        // `H5R__encode_string`: a 16-bit length, then the unterminated name.
342        ReferenceTarget::Attribute(name) => {
343            let len = u16::try_from(name.len()).map_err(|_| {
344                FormatError::InvalidData(format!(
345                    "attribute name of {} bytes does not fit a reference's 16-bit length",
346                    name.len()
347                ))
348            })?;
349            blob.extend_from_slice(&len.to_le_bytes());
350            blob.extend_from_slice(name.as_bytes());
351        }
352    }
353    Ok(blob)
354}
355
356/// Where the object token sits inside a blob [`encode_revised_blob`] built:
357/// behind the one byte that gives its length.
358pub const REVISED_BLOB_TOKEN_OFFSET: usize = 1;
359
360/// What a reference names beyond the object its token points at.
361#[derive(Debug, Clone, PartialEq, Eq)]
362pub enum ReferenceTarget {
363    /// The object itself.
364    Object,
365    /// A selection over the target dataset.
366    Region(Selection),
367    /// An attribute of the target, by name.
368    Attribute(String),
369}
370
371/// Everything `H5R__decode` recovers from an encoded reference: where the
372/// target is, and what is named there.
373#[derive(Debug, Clone, PartialEq, Eq)]
374pub struct DecodedReference {
375    /// Object header address the token names.
376    pub address: u64,
377    /// The file the target lives in, as the reference records it — the name
378    /// that file was open under when the reference was written
379    /// (`H5F_get_name`, not a canonical path). `None` when the flags do not
380    /// carry `H5R_IS_EXTERNAL`, which means the file holding the reference.
381    pub file: Option<String>,
382    /// What the reference names at that address.
383    pub target: ReferenceTarget,
384}
385
386/// Decode an encoded 1.12 reference — `H5R__decode` from the token onwards —
387/// into where its target is and what it names there, or `None` when the token
388/// names no object.
389///
390/// `external` is the element's `H5R_IS_EXTERNAL` flag, which decides whether a
391/// file name sits between the token and the kind's own payload; the flag lives
392/// in the element, not in the encoded reference, so it has to be handed in
393/// (`H5R__decode`, H5Rint.c:991-999).
394pub fn decode_revised_body(
395    kind: ReferenceKind,
396    external: bool,
397    body: &[u8],
398    ctx: &FormatContext,
399) -> FormatResult<Option<DecodedReference>> {
400    let sa = ctx.sizeof_addr as usize;
401    let mut r = Cursor::new(body);
402
403    // `H5R__decode_obj_token` stores the token's length ahead of it. The
404    // native VOL's token is the object header address (`H5VL_native_addr_to_
405    // token`), so a file whose tokens are some other width came from a
406    // connector this crate cannot follow.
407    let token_size = r.u8()? as usize;
408    if token_size != sa {
409        return Err(FormatError::UnsupportedFeature(format!(
410            "object tokens {token_size} bytes wide, not the {sa}-byte file addresses the \
411             native format uses"
412        )));
413    }
414    let token = r.take(token_size)?;
415    let Some(address) = target_address(token, sa) else {
416        return Ok(None);
417    };
418
419    // `H5R__encode` writes the file name straight after the token and before
420    // the kind's own payload (H5Rint.c:903-905).
421    let file = if external {
422        Some(decode_string(&mut r)?)
423    } else {
424        None
425    };
426
427    let target = match kind {
428        ReferenceKind::Object2 => ReferenceTarget::Object,
429        ReferenceKind::DatasetRegion2 => {
430            // `H5R__encode_region` prefixes the serialized selection with its
431            // length and the extent's rank; the selection carries the rank
432            // again, so only the length is needed to bound it.
433            let len = r.u32()? as usize;
434            let _rank = r.u32()?;
435            ReferenceTarget::Region(Selection::decode(r.take(len)?)?.0)
436        }
437        ReferenceKind::Attr => ReferenceTarget::Attribute(decode_string(&mut r)?),
438        ReferenceKind::Object1 | ReferenceKind::DatasetRegion1 => {
439            return Err(FormatError::InvalidData(format!(
440                "{kind:?} is not a 1.12 encoded reference"
441            )))
442        }
443    };
444    Ok(Some(DecodedReference {
445        address,
446        file,
447        target,
448    }))
449}
450
451/// One `H5R__encode_string` field: a 16-bit length, then that many unterminated
452/// bytes. Both a reference's file name and an attribute reference's name are
453/// written this way, so both are read back through here.
454fn decode_string(r: &mut Cursor<'_>) -> FormatResult<String> {
455    let len = r.u16()? as usize;
456    let bytes = r.take(len)?;
457    String::from_utf8(bytes.to_vec())
458        .map_err(|_| FormatError::InvalidData("a string in a reference is not UTF-8".into()))
459}
460
461/// One reference element, decoded and resolved against the file it came from.
462///
463/// `path` is the target's absolute path when the file's link structure names
464/// it, and `None` when nothing in the traversed structure points at that
465/// address — a reference into an untraversed part of the file, or a stale one
466/// left by a deletion. The address is reported either way.
467#[derive(Debug, Clone, PartialEq, Eq)]
468pub enum Reference {
469    /// An element naming no object: the undefined address libhdf5 writes for
470    /// an unset object reference, or a zeroed region-reference heap id.
471    Null,
472    /// A whole object — `H5R_OBJECT1` or `H5R_OBJECT2`.
473    Object {
474        /// Object header address of the target.
475        address: u64,
476        /// The file the target lives in; see [`Reference::file`].
477        file: Option<String>,
478        /// Absolute path of the target.
479        path: Option<String>,
480    },
481    /// A dataset plus a selection over it — `H5R_DATASET_REGION1` or
482    /// `H5R_DATASET_REGION2`.
483    Region {
484        /// Object header address of the target dataset.
485        address: u64,
486        /// The file the target dataset lives in; see [`Reference::file`].
487        file: Option<String>,
488        /// Absolute path of the target dataset.
489        path: Option<String>,
490        /// The selection the reference carries.
491        selection: Selection,
492    },
493    /// `H5R_ATTR`: one attribute of an object, by name. Only the 1.12
494    /// encodings can express it.
495    Attr {
496        /// Object header address of the object the attribute belongs to.
497        address: u64,
498        /// The file that object lives in; see [`Reference::file`].
499        file: Option<String>,
500        /// Absolute path of that object.
501        path: Option<String>,
502        /// Name of the attribute.
503        name: String,
504    },
505}
506
507impl Reference {
508    /// The file the target lives in, for a reference that names another file
509    /// — `H5Rget_file_name` on an un-opened external reference. `None` means
510    /// the file holding the reference, which is every reference libhdf5 can
511    /// write without the `H5R_IS_EXTERNAL` flag.
512    ///
513    /// The name is the one the target file was open under when the reference
514    /// was written, recorded verbatim, and [`path`](Self::path) is a path
515    /// inside *that* file.
516    pub fn file(&self) -> Option<&str> {
517        match self {
518            Self::Null => None,
519            Self::Object { file, .. } | Self::Region { file, .. } | Self::Attr { file, .. } => {
520                file.as_deref()
521            }
522        }
523    }
524
525    /// The target's absolute path, when the file names it.
526    pub fn path(&self) -> Option<&str> {
527        match self {
528            Self::Null => None,
529            Self::Object { path, .. } | Self::Region { path, .. } | Self::Attr { path, .. } => {
530                path.as_deref()
531            }
532        }
533    }
534
535    /// The target's object header address, or `None` for a null reference.
536    pub fn address(&self) -> Option<u64> {
537        match self {
538            Self::Null => None,
539            Self::Object { address, .. }
540            | Self::Region { address, .. }
541            | Self::Attr { address, .. } => Some(*address),
542        }
543    }
544
545    /// The attribute an attribute reference names; `None` for the other kinds.
546    pub fn attribute_name(&self) -> Option<&str> {
547        match self {
548            Self::Attr { name, .. } => Some(name),
549            _ => None,
550        }
551    }
552
553    /// The selection a region reference carries; `None` for the other kinds.
554    pub fn selection(&self) -> Option<&Selection> {
555        match self {
556            Self::Region { selection, .. } => Some(selection),
557            _ => None,
558        }
559    }
560
561    /// The inclusive bounding box of a region reference's selection —
562    /// `H5Sget_select_bounds` on the dereferenced region.
563    pub fn bounds(&self) -> Option<(Vec<u64>, Vec<u64>)> {
564        self.selection()?.bounds()
565    }
566
567    /// Whether this element names no object.
568    pub fn is_null(&self) -> bool {
569        matches!(self, Self::Null)
570    }
571}
572
573/// Little-endian cursor over a serialized selection.
574struct Cursor<'a> {
575    buf: &'a [u8],
576    pos: usize,
577}
578
579impl<'a> Cursor<'a> {
580    fn new(buf: &'a [u8]) -> Self {
581        Self { buf, pos: 0 }
582    }
583
584    fn take(&mut self, n: usize) -> FormatResult<&'a [u8]> {
585        let end = self.pos.checked_add(n).ok_or(FormatError::BufferTooShort {
586            needed: usize::MAX,
587            available: self.buf.len(),
588        })?;
589        if end > self.buf.len() {
590            return Err(FormatError::BufferTooShort {
591                needed: end,
592                available: self.buf.len(),
593            });
594        }
595        let out = &self.buf[self.pos..end];
596        self.pos = end;
597        Ok(out)
598    }
599
600    fn u8(&mut self) -> FormatResult<u8> {
601        Ok(self.take(1)?[0])
602    }
603
604    fn u16(&mut self) -> FormatResult<u16> {
605        let b = self.take(2)?;
606        Ok(u16::from_le_bytes([b[0], b[1]]))
607    }
608
609    fn u32(&mut self) -> FormatResult<u32> {
610        let b = self.take(4)?;
611        Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
612    }
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618
619    use crate::format::selection::{Hyperslab, HyperslabBlock, PointSelection};
620
621    fn ctx() -> FormatContext {
622        FormatContext::default_v3()
623    }
624
625    /// The heap object libhdf5 1.14.6 writes for `dset.regionref[0:3]` on a
626    /// 1-D 8-element dataset: the target's object header address followed by a
627    /// version-1 hyperslab naming one block, [0]-[2].
628    const REGION_HEAP_OBJECT: [u8; 40] = [
629        0x20, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // target address 0x320
630        0x02, 0x00, 0x00, 0x00, // H5S_SEL_HYPERSLABS
631        0x01, 0x00, 0x00, 0x00, // version 1
632        0x00, 0x00, 0x00, 0x00, // padding
633        0x10, 0x00, 0x00, 0x00, // length 16
634        0x01, 0x00, 0x00, 0x00, // rank 1
635        0x01, 0x00, 0x00, 0x00, // one block
636        0x00, 0x00, 0x00, 0x00, // start [0]
637        0x02, 0x00, 0x00, 0x00, // end [2]
638    ];
639
640    #[test]
641    fn region_heap_object_from_libhdf5_decodes() {
642        let (addr, selection) = decode_region_heap_object(&REGION_HEAP_OBJECT, &ctx()).unwrap();
643        assert_eq!(addr, 0x320);
644        assert_eq!(
645            selection,
646            Selection::Hyperslab {
647                rank: 1,
648                form: Hyperslab::Blocks(vec![HyperslabBlock {
649                    start: vec![0],
650                    end: vec![2],
651                }]),
652            }
653        );
654        assert_eq!(selection.bounds(), Some((vec![0], vec![2])));
655    }
656
657    #[test]
658    fn object_and_region_elements_report_null() {
659        assert_eq!(
660            decode_object_element(&0x320u64.to_le_bytes(), &ctx()).unwrap(),
661            Some(0x320)
662        );
663        assert_eq!(
664            decode_object_element(&[0xFF; 8], &ctx()).unwrap(),
665            None,
666            "an undefined address is a null reference"
667        );
668        assert_eq!(
669            decode_object_element(&[0; 8], &ctx()).unwrap(),
670            None,
671            "so is address 0, which h5py writes for an unset element"
672        );
673        let mut elem = [0u8; 12];
674        elem[..8].copy_from_slice(&0x820u64.to_le_bytes());
675        elem[8..].copy_from_slice(&2u32.to_le_bytes());
676        assert_eq!(
677            decode_region_element(&elem, &ctx()).unwrap(),
678            Some((0x820, 2))
679        );
680        assert_eq!(
681            decode_region_element(&[0u8; 12], &ctx()).unwrap(),
682            None,
683            "a zeroed element carries no heap id"
684        );
685    }
686
687    // The four element/blob captures below come from a file libhdf5 1.14.6
688    // wrote with `H5F_LIBVER_V112` as its low bound
689    // (`tests/fixtures/gen_revised_refs.c latest`), the combination that puts
690    // the newest selection encodings inside a reference: `matrix` is a 4x6
691    // dataset at object header address 0xC3, the region references select the
692    // hyperslab (1,2)-(2,4) and the points (0,1) and (3,5), and the attribute
693    // reference names `note`.
694
695    /// `H5R_OBJECT2`: type, flags, then the token inline.
696    const OBJ2_ELEMENT: [u8; 18] = [
697        0x02, 0x00, 0x08, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
698        0x00, 0x00, 0x00,
699    ];
700
701    /// `H5R_DATASET_REGION2`: type, flags, blob size, then the heap id.
702    const REGION2_ELEMENT: [u8; 18] = [
703        0x03, 0x00, 0x39, 0x00, 0x00, 0x00, 0xA8, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
704        0x00, 0x00, 0x00,
705    ];
706
707    /// `H5R_ATTR`, whose blob ends in the attribute name.
708    const ATTR_ELEMENT: [u8; 18] = [
709        0x04, 0x00, 0x0F, 0x00, 0x00, 0x00, 0xA8, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
710        0x00, 0x00, 0x00,
711    ];
712
713    /// The blob behind `REGION2_ELEMENT`: token, then a version-3 regular
714    /// hyperslab. libhdf5 sizes the blob for the largest reference the dataset
715    /// may hold, so the encoding stops short of the object's end.
716    const REGION2_HYPERSLAB_BLOB: [u8; 57] = [
717        0x08, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x02, 0x00,
718        0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x02, 0x00, 0x00,
719        0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00,
720        0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
721    ];
722
723    /// The blob of the second region reference: a version-2 point list.
724    const REGION2_POINT_BLOB: [u8; 57] = [
725        0x08, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x02, 0x00,
726        0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00, 0x00, 0x00,
727        0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x05, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
728        0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
729    ];
730
731    /// The blob of the attribute reference: token, name length, name.
732    const ATTR_BLOB: [u8; 15] = [
733        0x08, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x6E, 0x6F, 0x74, 0x65,
734    ];
735
736    /// The name every external reference in `tests/fixtures/ext_refs.h5`
737    /// carries: the path its target file was created under, relative to the
738    /// crate root the generator ran in.
739    const EXT_FILE: &str = "tests/fixtures/ext_ref_target.h5";
740
741    /// The first element of that fixture's `extobjrefs`: an `H5R_OBJECT2`
742    /// whose flags carry `H5R_IS_EXTERNAL`, which sends it to the heap
743    /// although its kind alone would keep it inline.
744    const EXT_OBJ2_ELEMENT: [u8; 18] = [
745        0x02, 0x01, 0x2B, 0x00, 0x00, 0x00, 0x24, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
746        0x00, 0x00, 0x00,
747    ];
748
749    /// Its blob: token, then the file name, and nothing after it.
750    const EXT_OBJ2_BLOB: [u8; 43] = [
751        0x08, 0x20, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x74, 0x65, 0x73, 0x74,
752        0x73, 0x2F, 0x66, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x73, 0x2F, 0x65, 0x78, 0x74, 0x5F,
753        0x72, 0x65, 0x66, 0x5F, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2E, 0x68, 0x35,
754    ];
755
756    /// The blob of the same fixture's `extattrrefs`: the file name sits
757    /// between the token and the attribute name, so the two strings are only
758    /// told apart by their order.
759    const EXT_ATTR_BLOB: [u8; 49] = [
760        0x08, 0x20, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x74, 0x65, 0x73, 0x74,
761        0x73, 0x2F, 0x66, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x73, 0x2F, 0x65, 0x78, 0x74, 0x5F,
762        0x72, 0x65, 0x66, 0x5F, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2E, 0x68, 0x35, 0x04, 0x00,
763        0x6E, 0x6F, 0x74, 0x65,
764    ];
765
766    /// An `H5R_OBJECT2` element keeps its encoded reference inline; the other
767    /// two kinds point at a heap blob.
768    #[test]
769    fn revised_elements_from_libhdf5_split_into_kind_and_body() {
770        let RevisedElement::Inline { kind, body } =
771            decode_revised_element(&OBJ2_ELEMENT, &ctx()).unwrap()
772        else {
773            panic!("an object reference is stored in the element");
774        };
775        assert_eq!(kind, ReferenceKind::Object2);
776        assert_eq!(
777            decode_revised_body(kind, false, body, &ctx()).unwrap(),
778            Some(DecodedReference {
779                address: 0xC3,
780                file: None,
781                target: ReferenceTarget::Object,
782            })
783        );
784
785        assert_eq!(
786            decode_revised_element(&REGION2_ELEMENT, &ctx()).unwrap(),
787            RevisedElement::Heap {
788                kind: ReferenceKind::DatasetRegion2,
789                external: false,
790                collection: 0x8A8,
791                index: 1,
792            }
793        );
794        assert_eq!(
795            decode_revised_element(&ATTR_ELEMENT, &ctx()).unwrap(),
796            RevisedElement::Heap {
797                kind: ReferenceKind::Attr,
798                external: false,
799                collection: 0x8A8,
800                index: 3,
801            }
802        );
803    }
804
805    /// The bounds of both region blobs are what `H5Sget_select_bounds` reports
806    /// for the selections the fixture generator made.
807    #[test]
808    fn revised_region_blobs_decode_to_their_selections() {
809        let hyper = decode_revised_body(
810            ReferenceKind::DatasetRegion2,
811            false,
812            &REGION2_HYPERSLAB_BLOB,
813            &ctx(),
814        )
815        .unwrap()
816        .unwrap();
817        assert_eq!(hyper.address, 0xC3);
818        let ReferenceTarget::Region(selection) = &hyper.target else {
819            panic!("a region reference names a selection");
820        };
821        assert_eq!(
822            selection.bounds(),
823            Some((vec![1, 2], vec![2, 4])),
824            "{selection:?}"
825        );
826
827        let points = decode_revised_body(
828            ReferenceKind::DatasetRegion2,
829            false,
830            &REGION2_POINT_BLOB,
831            &ctx(),
832        )
833        .unwrap()
834        .unwrap();
835        let ReferenceTarget::Region(selection) = &points.target else {
836            panic!("a region reference names a selection");
837        };
838        assert_eq!(
839            selection,
840            &Selection::Points(PointSelection {
841                rank: 2,
842                points: vec![vec![0, 1], vec![3, 5]],
843            })
844        );
845        assert_eq!(selection.bounds(), Some((vec![0, 1], vec![3, 5])));
846    }
847
848    #[test]
849    fn an_attribute_reference_carries_its_name() {
850        assert_eq!(
851            decode_revised_body(ReferenceKind::Attr, false, &ATTR_BLOB, &ctx()).unwrap(),
852            Some(DecodedReference {
853                address: 0xC3,
854                file: None,
855                target: ReferenceTarget::Attribute("note".into()),
856            })
857        );
858    }
859
860    /// An unwritten element is reference type 0 over a nil blob id
861    /// (`H5T__ref_disk_isnull`); a type byte outside the 1.12 kinds and a
862    /// foreign token width are both reported rather than read as something
863    /// else.
864    #[test]
865    fn revised_elements_that_name_nothing_or_cannot_be_followed() {
866        assert_eq!(
867            decode_revised_element(&[0u8; 18], &ctx()).unwrap(),
868            RevisedElement::Null
869        );
870
871        let mut stale = [0u8; 18];
872        stale[6] = 0xA8;
873        stale[7] = 0x08;
874        assert!(
875            matches!(
876                decode_revised_element(&stale, &ctx()).unwrap_err(),
877                FormatError::InvalidData(_)
878            ),
879            "reference type 0 over a live blob is not a null reference"
880        );
881
882        let mut old_code = OBJ2_ELEMENT;
883        old_code[0] = ReferenceKind::DatasetRegion1.code();
884        assert!(matches!(
885            decode_revised_element(&old_code, &ctx()).unwrap_err(),
886            FormatError::InvalidData(_)
887        ));
888
889        let mut foreign = ATTR_BLOB;
890        foreign[0] = 16;
891        assert!(
892            matches!(
893                decode_revised_body(ReferenceKind::Attr, false, &foreign, &ctx()).unwrap_err(),
894                FormatError::UnsupportedFeature(_)
895            ),
896            "a token that is not a file address belongs to another VOL connector"
897        );
898
899        let mut unset = OBJ2_ELEMENT;
900        unset[3] = 0;
901        let RevisedElement::Inline { kind, body } = decode_revised_element(&unset, &ctx()).unwrap()
902        else {
903            panic!("an object reference is stored in the element");
904        };
905        assert_eq!(
906            decode_revised_body(kind, false, body, &ctx()).unwrap(),
907            None,
908            "a zero token names no object"
909        );
910    }
911
912    /// An element flagged `H5R_IS_EXTERNAL` names the file its target lives
913    /// in, and takes the heap even when its kind would otherwise be stored
914    /// inline (`H5T__ref_disk_getsize`, H5Tref.c:890).
915    #[test]
916    fn an_external_reference_names_the_file_its_target_is_in() {
917        assert_eq!(
918            decode_revised_element(&EXT_OBJ2_ELEMENT, &ctx()).unwrap(),
919            RevisedElement::Heap {
920                kind: ReferenceKind::Object2,
921                external: true,
922                collection: 0x824,
923                index: 1,
924            }
925        );
926        assert_eq!(
927            decode_revised_body(ReferenceKind::Object2, true, &EXT_OBJ2_BLOB, &ctx()).unwrap(),
928            Some(DecodedReference {
929                address: 0x320,
930                file: Some(EXT_FILE.into()),
931                target: ReferenceTarget::Object,
932            })
933        );
934
935        // The file name precedes the kind's own payload, so reading it as if
936        // the reference were internal would take the name for the payload.
937        assert_eq!(
938            decode_revised_body(ReferenceKind::Attr, true, &EXT_ATTR_BLOB, &ctx()).unwrap(),
939            Some(DecodedReference {
940                address: 0x320,
941                file: Some(EXT_FILE.into()),
942                target: ReferenceTarget::Attribute("note".into()),
943            })
944        );
945        let internal = decode_revised_body(ReferenceKind::Attr, false, &EXT_ATTR_BLOB, &ctx())
946            .unwrap()
947            .unwrap();
948        assert_eq!(
949            internal.target,
950            ReferenceTarget::Attribute(EXT_FILE.into()),
951            "the flag is what tells the file name from the attribute name"
952        );
953    }
954
955    /// Every 1.12 element this crate writes is the image libhdf5 wrote for the
956    /// same reference: the inline object form, and the blob id the other two
957    /// kinds carry.
958    #[test]
959    fn revised_elements_encode_to_the_libhdf5_images() {
960        assert_eq!(
961            encode_reference_element(&ReferenceElementImage::Inline(0xC3), 18, &ctx()).unwrap(),
962            OBJ2_ELEMENT
963        );
964        assert_eq!(
965            encode_reference_element(
966                &ReferenceElementImage::Blob {
967                    kind: ReferenceKind::DatasetRegion2,
968                    size: REGION2_HYPERSLAB_BLOB.len() as u32,
969                    collection: 0x8A8,
970                    index: 1,
971                },
972                18,
973                &ctx()
974            )
975            .unwrap(),
976            REGION2_ELEMENT
977        );
978        assert_eq!(
979            encode_reference_element(
980                &ReferenceElementImage::Blob {
981                    kind: ReferenceKind::Attr,
982                    size: ATTR_BLOB.len() as u32,
983                    collection: 0x8A8,
984                    index: 3,
985                },
986                18,
987                &ctx()
988            )
989            .unwrap(),
990            ATTR_ELEMENT
991        );
992        // The pre-1.12 element is the address and nothing else.
993        assert_eq!(
994            encode_reference_element(&ReferenceElementImage::Legacy(0xC3), 8, &ctx()).unwrap(),
995            [0xC3, 0, 0, 0, 0, 0, 0, 0]
996        );
997    }
998
999    /// A blob-backed element needs more room than an inline one; a datatype
1000    /// too narrow for the layout is reported rather than truncated.
1001    #[test]
1002    fn an_element_narrower_than_its_layout_is_refused() {
1003        let err = encode_reference_element(
1004            &ReferenceElementImage::Blob {
1005                kind: ReferenceKind::Attr,
1006                size: 15,
1007                collection: 0x8A8,
1008                index: 3,
1009            },
1010            11,
1011            &ctx(),
1012        )
1013        .unwrap_err();
1014        assert!(matches!(err, FormatError::InvalidData(_)), "{err:?}");
1015        assert!(encode_reference_element(&ReferenceElementImage::Inline(0xC3), 11, &ctx()).is_ok());
1016    }
1017
1018    /// A blob this crate encodes says the same reference the libhdf5 blob it
1019    /// came from says, and is read back by the same decoder.
1020    ///
1021    /// Byte equality holds for everything but the serialized selection, which
1022    /// this crate writes in the version-1 block-list form every bounded
1023    /// selection takes here while the fixture's `latest` bound produced the
1024    /// version-3 one. Both are `H5S_decode` input; the length field ahead of
1025    /// the selection is what bounds it, and it is written from the bytes
1026    /// actually produced.
1027    #[test]
1028    fn revised_blobs_encode_to_what_libhdf5_reads_back() {
1029        let attr = decode_revised_body(ReferenceKind::Attr, false, &ATTR_BLOB, &ctx())
1030            .unwrap()
1031            .unwrap();
1032        assert_eq!(
1033            encode_revised_blob(attr.address, &attr.target, 0, &ctx()).unwrap(),
1034            ATTR_BLOB
1035        );
1036
1037        for (kind, golden) in [
1038            (ReferenceKind::DatasetRegion2, &REGION2_HYPERSLAB_BLOB[..]),
1039            (ReferenceKind::DatasetRegion2, &REGION2_POINT_BLOB[..]),
1040        ] {
1041            let DecodedReference {
1042                address, target, ..
1043            } = decode_revised_body(kind, false, golden, &ctx())
1044                .unwrap()
1045                .unwrap();
1046            let blob = encode_revised_blob(address, &target, 2, &ctx()).unwrap();
1047            // Token and rank are byte-identical; the selection is re-encoded.
1048            assert_eq!(blob[..9], golden[..9]);
1049            assert_eq!(blob[13..17], golden[13..17], "the extent rank");
1050            let selection_len = u32::from_le_bytes(blob[9..13].try_into().unwrap()) as usize;
1051            assert_eq!(blob.len(), 17 + selection_len);
1052            let back = decode_revised_body(kind, false, &blob, &ctx())
1053                .unwrap()
1054                .unwrap();
1055            assert_eq!(back.address, address);
1056            let (ReferenceTarget::Region(was), ReferenceTarget::Region(now)) =
1057                (&target, &back.target)
1058            else {
1059                panic!("a region reference names a selection");
1060            };
1061            // Version 1 spells a regular hyperslab as the blocks it covers, so
1062            // what survives is the region, not the form it was written in.
1063            assert_eq!(
1064                was.to_boxes(&[4, 6]).unwrap(),
1065                now.to_boxes(&[4, 6]).unwrap()
1066            );
1067            assert_eq!(was.bounds(), now.bounds());
1068        }
1069
1070        // `H5S_SEL_ALL` serializes without a rank, and the blob says one
1071        // anyway, because `H5R__encode_region` reads it from the dataspace.
1072        let all =
1073            encode_revised_blob(0xC3, &ReferenceTarget::Region(Selection::All), 3, &ctx()).unwrap();
1074        assert_eq!(u32::from_le_bytes(all[13..17].try_into().unwrap()), 3);
1075        assert_eq!(
1076            decode_revised_body(ReferenceKind::DatasetRegion2, false, &all, &ctx()).unwrap(),
1077            Some(DecodedReference {
1078                address: 0xC3,
1079                file: None,
1080                target: ReferenceTarget::Region(Selection::All),
1081            })
1082        );
1083
1084        // An object reference's blob is the token alone; that is also the body
1085        // an `H5R_OBJECT2` element carries inline.
1086        assert_eq!(
1087            encode_revised_blob(0xC3, &ReferenceTarget::Object, 0, &ctx()).unwrap(),
1088            OBJ2_ELEMENT[2..11]
1089        );
1090    }
1091
1092    /// A truncated selection is reported, not read past.
1093    #[test]
1094    fn a_truncated_selection_is_refused() {
1095        let err = Selection::decode(&REGION_HEAP_OBJECT[8..20]).unwrap_err();
1096        assert!(
1097            matches!(err, FormatError::BufferTooShort { .. }),
1098            "unexpected error: {err:?}"
1099        );
1100    }
1101}