Skip to main content

rust_hdf5/format/
sohm.rs

1//! Shared object header messages (SOHM).
2//!
3//! A message that several objects would encode identically — most often a
4//! datatype, dataspace or attribute — can be stored once and referenced from
5//! every object header that uses it. The referencing header then holds a
6//! *pointer*, not the message: either a fractal-heap ID into the SOHM heap, or
7//! the address of the object header that owns the message (a committed
8//! datatype). The `H5O_MSG_FLAG_SHARED` bit on the message says which of the
9//! two shapes the body has.
10//!
11//! Resolving a pointer needs the SOHM master table, whose address comes from
12//! the superblock extension's shared-message-table message: it maps a message
13//! type to the fractal heap holding that type's shared messages.
14//!
15//! Upstream: `H5Oshared.c` (`H5O__shared_decode`, `H5O__shared_read`),
16//! `H5SM.c` (`H5SM_get_fheap_addr`, `H5SM__type_to_flag`), `H5SMcache.c`
17//! (`H5SM__cache_table_deserialize`).
18
19use crate::format::bytes::read_le_uint as read_uint;
20use crate::format::checksum::{checksum_metadata, jenkins_lookup3};
21use crate::format::messages::{
22    MSG_ATTRIBUTE, MSG_DATASPACE, MSG_DATATYPE, MSG_FILL_VALUE, MSG_FILL_VALUE_OLD,
23    MSG_FILTER_PIPELINE,
24};
25use crate::format::{FormatContext, FormatError, FormatResult};
26
27/// SOHM master table signature.
28pub const SMTB_SIGNATURE: [u8; 4] = *b"SMTB";
29/// SOHM list index signature.
30pub const SMLI_SIGNATURE: [u8; 4] = *b"SMLI";
31
32/// Length of a SOHM fractal-heap ID (`H5O_FHEAP_ID_LEN`). Unlike other heaps,
33/// the SOHM heap's ID length is fixed by the format, not read from the heap.
34pub const SOHM_HEAP_ID_LEN: usize = 8;
35
36/// Offset of the heap ID inside a heap-shared pointer: past the version and
37/// type bytes [`SharedMessagePointer::encode_sohm`] writes.
38pub const SOHM_POINTER_HEAP_ID_AT: usize = 2;
39
40/// `H5SM_IN_HEAP`: the record's message body is in the index's fractal heap.
41pub const SOHM_IN_HEAP: u8 = 0;
42/// `H5SM_IN_OH`: the record's message body is a message of an object header.
43pub const SOHM_IN_OH: u8 = 1;
44
45/// Index form stored in an index header's `index_type` byte (`H5SM_index_type_t`).
46pub const SOHM_INDEX_LIST: u8 = 0;
47/// The B-tree form of the same field.
48pub const SOHM_INDEX_BTREE: u8 = 1;
49
50/// v2 B-tree record type of a SOHM index (`H5B2_SOHM_INDEX_ID`).
51pub const BT2_TYPE_SOHM_INDEX: u8 = 7;
52
53/// Node size of a SOHM index B-tree (`H5SM_B2_NODE_SIZE`).
54pub const SOHM_B2_NODE_SIZE: u32 = 512;
55
56/// Most indexes a file may declare (`H5O_SHMESG_MAX_NINDEXES`).
57pub const MAX_SOHM_INDEXES: usize = 8;
58
59/// The hash an index records a message under (`H5SM__write_mesg`): Jenkins
60/// lookup3 over the *encoded* message body, seeded with the message type id.
61/// Two message classes that happen to encode identically therefore land on
62/// different records, which is what lets one index cover several classes.
63pub fn message_hash(body: &[u8], msg_type: u8) -> u32 {
64    jenkins_lookup3(body, u32::from(msg_type))
65}
66
67/// On-disk size of one index record (`H5SM_SOHM_ENTRY_SIZE`): a location byte
68/// and a hash, then whichever of the two location-specific bodies is larger.
69pub fn record_size(ctx: &FormatContext) -> usize {
70    let sa = ctx.sizeof_addr as usize;
71    // Heap form: reference count + heap id. Object-header form: reserved byte,
72    // message type, message index, header address.
73    1 + 4 + (4 + SOHM_HEAP_ID_LEN).max(1 + 1 + 2 + sa)
74}
75
76/// One index record: the hash every record carries, and whichever of the two
77/// bodies `H5SM_sohm_t` holds (`H5SM__message_encode`, H5SMmessage.c:265-292).
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct SohmRecord {
80    /// [`message_hash`] of the body this record names.
81    pub hash: u32,
82    /// Where that body is.
83    pub location: SohmRecordLocation,
84}
85
86/// The two forms a record's body takes.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum SohmRecordLocation {
89    /// `H5SM_IN_HEAP`: the body is a heap object of the index's fractal heap.
90    InHeap {
91        /// Messages referring to this body — pointers plus, when a first copy
92        /// was left literal, that copy (`H5SM__write_mesg` opens the count at
93        /// 2 when it moves an in-header body to the heap, H5SM.c:1298-1306).
94        ref_count: u32,
95        /// Fractal-heap ID of the body.
96        heap_id: [u8; SOHM_HEAP_ID_LEN],
97    },
98    /// `H5SM_IN_OH`: the body is still a literal message of the object header
99    /// that first wrote it, which carries [`MSG_FLAG_SHAREABLE`] and no
100    /// pointer (H5SM.c:1400-1417).
101    ///
102    /// [`MSG_FLAG_SHAREABLE`]: crate::format::messages::MSG_FLAG_SHAREABLE
103    InObjectHeader {
104        /// Class of the message that holds the body.
105        msg_type: u8,
106        /// Its creation index within that header, which
107        /// `H5O_msg_get_crt_index` reports as 0 for every class carrying
108        /// `H5O_SHARE_IN_OHDR` — only the attribute class has a
109        /// `get_crt_index` callback (H5Oattr.c:82).
110        index: u16,
111        /// Address of the object header holding it.
112        oh_addr: u64,
113    },
114}
115
116impl SohmRecord {
117    /// Encode the record (`H5SM__message_encode`).
118    pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
119        let size = record_size(ctx);
120        let mut buf = Vec::with_capacity(size);
121        match self.location {
122            SohmRecordLocation::InHeap { ref_count, heap_id } => {
123                buf.push(SOHM_IN_HEAP);
124                buf.extend_from_slice(&self.hash.to_le_bytes());
125                buf.extend_from_slice(&ref_count.to_le_bytes());
126                buf.extend_from_slice(&heap_id);
127            }
128            SohmRecordLocation::InObjectHeader {
129                msg_type,
130                index,
131                oh_addr,
132            } => {
133                buf.push(SOHM_IN_OH);
134                buf.extend_from_slice(&self.hash.to_le_bytes());
135                // A reserved byte libhdf5 writes zero and never reads.
136                buf.push(0);
137                buf.push(msg_type);
138                buf.extend_from_slice(&index.to_le_bytes());
139                buf.extend_from_slice(&oh_addr.to_le_bytes()[..ctx.sizeof_addr as usize]);
140            }
141        }
142        buf.resize(size, 0);
143        buf
144    }
145}
146
147/// Space a list index occupies (`H5SM_LIST_SIZE`), which is sized for
148/// `list_max` records however few are in use.
149pub fn list_size(ctx: &FormatContext, list_max: u16) -> usize {
150    4 + record_size(ctx) * list_max as usize + 4
151}
152
153/// Encode a list index (`SMLI`, `H5SM__cache_list_serialize`).
154///
155/// The image covers only the records in use: the checksum follows the last
156/// one, and the rest of the [`list_size`] block the index occupies is left
157/// untouched. A reader sizes the buffer from `list_max` but checksums exactly
158/// this prefix, so trailing bytes are never part of the sum.
159pub fn encode_list(records: &[SohmRecord], ctx: &FormatContext) -> Vec<u8> {
160    let mut buf = SMLI_SIGNATURE.to_vec();
161    for record in records {
162        buf.extend_from_slice(&record.encode(ctx));
163    }
164    let sum = checksum_metadata(&buf);
165    buf.extend_from_slice(&sum.to_le_bytes());
166    buf
167}
168
169/// Where a shared message's body actually lives (`H5O_SHARE_TYPE_*`).
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum SharedLocation {
172    /// Not shared: the body is the message itself.
173    Unshared,
174    /// In the SOHM fractal heap, addressed by a heap ID.
175    Sohm,
176    /// In another object header (a committed datatype).
177    Committed,
178    /// Shared, but stored in this same object header.
179    Here,
180}
181
182impl SharedLocation {
183    fn from_byte(b: u8) -> Self {
184        match b {
185            1 => Self::Sohm,
186            2 => Self::Committed,
187            3 => Self::Here,
188            _ => Self::Unshared,
189        }
190    }
191}
192
193/// A decoded shared-message pointer: the body of any object-header message
194/// whose `H5O_MSG_FLAG_SHARED` bit is set.
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct SharedMessagePointer {
197    /// Encoding version: 1, 2 or 3.
198    pub version: u8,
199    /// Which storage the pointer names.
200    pub location: SharedLocation,
201    /// Fractal-heap ID, for `Sohm`.
202    pub heap_id: [u8; SOHM_HEAP_ID_LEN],
203    /// Object header address, for `Committed` (and for the version-1 form,
204    /// which is always committed).
205    pub oh_addr: u64,
206}
207
208impl SharedMessagePointer {
209    /// Decode the pointer that stands in for the message body.
210    pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<Self> {
211        let sa = ctx.sizeof_addr as usize;
212        let ss = ctx.sizeof_size as usize;
213        need(buf, 2)?;
214        let version = buf[0];
215        if version == 0 || version > 3 {
216            return Err(FormatError::InvalidVersion(version));
217        }
218
219        // The type byte is unused before version 2: those messages are always
220        // committed datatypes.
221        let mut location = if version >= 2 {
222            SharedLocation::from_byte(buf[1])
223        } else {
224            SharedLocation::Committed
225        };
226        let mut pos = 2;
227
228        let mut heap_id = [0u8; SOHM_HEAP_ID_LEN];
229        let mut oh_addr = 0u64;
230        if version == 1 {
231            // 6 reserved bytes, then a stripped-down symbol table entry: a
232            // local-heap address that is skipped, then the object header's.
233            pos += 6;
234            need(buf, pos + ss + sa)?;
235            pos += ss;
236            oh_addr = read_uint(&buf[pos..], sa);
237        } else if location == SharedLocation::Sohm {
238            if version < 3 {
239                return Err(FormatError::InvalidData(
240                    "heap-shared message pointer requires version 3".into(),
241                ));
242            }
243            need(buf, pos + SOHM_HEAP_ID_LEN)?;
244            heap_id.copy_from_slice(&buf[pos..pos + SOHM_HEAP_ID_LEN]);
245        } else {
246            // Before version 3 the committed flag did not exist, so anything
247            // that is not heap-shared is a committed datatype.
248            if version < 3 {
249                location = SharedLocation::Committed;
250            }
251            need(buf, pos + sa)?;
252            oh_addr = read_uint(&buf[pos..], sa);
253        }
254
255        Ok(Self {
256            version,
257            location,
258            heap_id,
259            oh_addr,
260        })
261    }
262
263    /// The pointer a dataset (or attribute) built on a committed datatype
264    /// stores in place of the message body.
265    ///
266    /// `H5O__shared_encode` picks version 2 for `H5O_SHARE_TYPE_COMMITTED` —
267    /// version 3 exists for the heap form, which needs a flag byte version 2
268    /// has no room for — so a committed pointer libhdf5 wrote and one written
269    /// here agree byte for byte.
270    pub fn committed(oh_addr: u64) -> Self {
271        Self {
272            version: 2,
273            location: SharedLocation::Committed,
274            heap_id: [0u8; SOHM_HEAP_ID_LEN],
275            oh_addr,
276        }
277    }
278
279    /// Encode the committed form — the inverse of [`decode`](Self::decode)
280    /// for the one shape this crate writes.
281    ///
282    /// Only the committed form is produced, so this takes the address rather
283    /// than a pointer and cannot fail: nothing here shares through the SOHM
284    /// heap, and the version-1 form libhdf5 no longer writes has a symbol
285    /// table entry in it that nothing here can fill.
286    pub fn encode_committed(oh_addr: u64, ctx: &FormatContext) -> Vec<u8> {
287        let sa = ctx.sizeof_addr as usize;
288        let mut buf = Vec::with_capacity(2 + sa);
289        buf.push(2); // H5O_SHARED_VERSION_2
290        buf.push(2); // H5O_SHARE_TYPE_COMMITTED
291        buf.extend_from_slice(&oh_addr.to_le_bytes()[..sa]);
292        buf
293    }
294
295    /// The pointer a header stores in place of a message whose body was moved
296    /// to the shared-message fractal heap.
297    ///
298    /// Version 3, where the committed form stays at version 2: only version 3
299    /// has the type byte free to mean `H5O_SHARE_TYPE_SOHM`, and its body is
300    /// the fixed-width heap ID rather than an address, so it does not follow
301    /// the file's address size.
302    pub fn encode_sohm(heap_id: [u8; SOHM_HEAP_ID_LEN]) -> Vec<u8> {
303        let mut buf = Vec::with_capacity(2 + SOHM_HEAP_ID_LEN);
304        buf.push(3); // H5O_SHARED_VERSION_3
305        buf.push(1); // H5O_SHARE_TYPE_SOHM
306        buf.extend_from_slice(&heap_id);
307        buf
308    }
309}
310
311/// One SOHM index header, as stored in the master table.
312#[derive(Debug, Clone, PartialEq, Eq)]
313pub struct SohmIndexHeader {
314    /// 0 = list, 1 = v2 B-tree. The index is a write-side lookup structure;
315    /// reading a shared message needs only `heap_addr`.
316    pub index_type: u8,
317    /// Bit mask of the message types this index covers (`H5SM__type_to_flag`).
318    pub mesg_types: u16,
319    /// Smallest message this index will share.
320    pub min_mesg_size: u32,
321    /// Message count at which a list index becomes a B-tree.
322    pub list_max: u16,
323    /// Message count at which a B-tree index becomes a list again.
324    pub btree_min: u16,
325    /// Messages currently in the index.
326    pub num_messages: u16,
327    /// Address of the list or B-tree.
328    pub index_addr: u64,
329    /// Address of the fractal heap holding this index's message bodies.
330    pub heap_addr: u64,
331}
332
333/// The SOHM master table (`SMTB`): one index header per shared-message index.
334#[derive(Debug, Clone, Default, PartialEq, Eq)]
335pub struct SohmMasterTable {
336    /// Index headers, in table order.
337    pub indexes: Vec<SohmIndexHeader>,
338}
339
340/// Version of a SOHM index header (`H5SM_LIST_VERSION`).
341const SM_LIST_VERSION: u8 = 0;
342
343impl SohmMasterTable {
344    /// On-disk size of a table with `nindexes` index headers
345    /// (`H5SM_TABLE_SIZE` / `H5SM_INDEX_HEADER_SIZE`).
346    pub fn encoded_size(ctx: &FormatContext, nindexes: u8) -> usize {
347        let sa = ctx.sizeof_addr as usize;
348        let per_index = 1 + 1 + 2 + 4 + 2 + 2 + 2 + sa + sa;
349        4 + nindexes as usize * per_index + 4
350    }
351
352    /// Decode the master table. `nindexes` comes from the shared-message-table
353    /// message in the superblock extension — the table itself does not store
354    /// it.
355    pub fn decode(buf: &[u8], ctx: &FormatContext, nindexes: u8) -> FormatResult<Self> {
356        let sa = ctx.sizeof_addr as usize;
357        let size = Self::encoded_size(ctx, nindexes);
358        need(buf, size)?;
359        if buf[0..4] != SMTB_SIGNATURE {
360            return Err(FormatError::InvalidSignature);
361        }
362
363        let stored =
364            u32::from_le_bytes([buf[size - 4], buf[size - 3], buf[size - 2], buf[size - 1]]);
365        let computed = checksum_metadata(&buf[..size - 4]);
366        if stored != computed {
367            return Err(FormatError::ChecksumMismatch {
368                expected: stored,
369                computed,
370            });
371        }
372
373        let mut pos = 4;
374        let mut indexes = Vec::with_capacity(nindexes as usize);
375        for _ in 0..nindexes {
376            let version = buf[pos];
377            if version != SM_LIST_VERSION {
378                return Err(FormatError::InvalidVersion(version));
379            }
380            pos += 1;
381            let index_type = buf[pos];
382            pos += 1;
383            let mesg_types = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
384            pos += 2;
385            let min_mesg_size =
386                u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
387            pos += 4;
388            let list_max = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
389            pos += 2;
390            let btree_min = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
391            pos += 2;
392            let num_messages = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
393            pos += 2;
394            let index_addr = read_uint(&buf[pos..], sa);
395            pos += sa;
396            let heap_addr = read_uint(&buf[pos..], sa);
397            pos += sa;
398            indexes.push(SohmIndexHeader {
399                index_type,
400                mesg_types,
401                min_mesg_size,
402                list_max,
403                btree_min,
404                num_messages,
405                index_addr,
406                heap_addr,
407            });
408        }
409
410        Ok(Self { indexes })
411    }
412
413    /// Encode the master table (`H5SM__cache_table_serialize`). The index
414    /// count is not stored here — the shared-message-table message in the
415    /// superblock extension is the only place it is written.
416    pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
417        let sa = ctx.sizeof_addr as usize;
418        let mut buf = SMTB_SIGNATURE.to_vec();
419        for index in &self.indexes {
420            buf.push(SM_LIST_VERSION);
421            buf.push(index.index_type);
422            buf.extend_from_slice(&index.mesg_types.to_le_bytes());
423            buf.extend_from_slice(&index.min_mesg_size.to_le_bytes());
424            buf.extend_from_slice(&index.list_max.to_le_bytes());
425            buf.extend_from_slice(&index.btree_min.to_le_bytes());
426            buf.extend_from_slice(&index.num_messages.to_le_bytes());
427            buf.extend_from_slice(&index.index_addr.to_le_bytes()[..sa]);
428            buf.extend_from_slice(&index.heap_addr.to_le_bytes()[..sa]);
429        }
430        let sum = checksum_metadata(&buf);
431        buf.extend_from_slice(&sum.to_le_bytes());
432        buf
433    }
434
435    /// Address of the fractal heap holding shared messages of `msg_type`, as
436    /// `H5SM_get_fheap_addr` resolves it.
437    pub fn heap_addr(&self, msg_type: u8) -> Option<u64> {
438        let flag = type_flag(msg_type)?;
439        self.indexes
440            .iter()
441            .find(|i| i.mesg_types & flag != 0)
442            .map(|i| i.heap_addr)
443    }
444}
445
446/// The index bit for a message type (`H5SM__type_to_flag`). Only the five
447/// shareable types have one; the old fill-value message shares the new one's
448/// bit, matching upstream.
449pub fn type_flag(msg_type: u8) -> Option<u16> {
450    let id = match msg_type {
451        MSG_DATASPACE | MSG_DATATYPE | MSG_FILL_VALUE | MSG_FILTER_PIPELINE | MSG_ATTRIBUTE => {
452            msg_type
453        }
454        MSG_FILL_VALUE_OLD => MSG_FILL_VALUE,
455        _ => return None,
456    };
457    Some(1u16 << id)
458}
459
460fn need(buf: &[u8], n: usize) -> FormatResult<()> {
461    if buf.len() < n {
462        Err(FormatError::BufferTooShort {
463            needed: n,
464            available: buf.len(),
465        })
466    } else {
467        Ok(())
468    }
469}
470
471// ======================================================================= tests
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    fn ctx() -> FormatContext {
478        FormatContext::default_v3()
479    }
480
481    fn index_header(mesg_types: u16, heap_addr: u64) -> Vec<u8> {
482        let mut b = vec![SM_LIST_VERSION, 0];
483        b.extend_from_slice(&mesg_types.to_le_bytes());
484        b.extend_from_slice(&0u32.to_le_bytes()); // min_mesg_size
485        b.extend_from_slice(&50u16.to_le_bytes()); // list_max
486        b.extend_from_slice(&40u16.to_le_bytes()); // btree_min
487        b.extend_from_slice(&7u16.to_le_bytes()); // num_messages
488        b.extend_from_slice(&0x1234u64.to_le_bytes()); // index_addr
489        b.extend_from_slice(&heap_addr.to_le_bytes());
490        b
491    }
492
493    fn master_table(headers: &[Vec<u8>]) -> Vec<u8> {
494        let mut b = SMTB_SIGNATURE.to_vec();
495        for h in headers {
496            b.extend_from_slice(h);
497        }
498        let sum = checksum_metadata(&b);
499        b.extend_from_slice(&sum.to_le_bytes());
500        b
501    }
502
503    #[test]
504    fn master_table_roundtrip() {
505        let buf = master_table(&[index_header(1 << MSG_DATATYPE, 0x8000)]);
506        assert_eq!(buf.len(), SohmMasterTable::encoded_size(&ctx(), 1));
507        let t = SohmMasterTable::decode(&buf, &ctx(), 1).unwrap();
508        assert_eq!(t.indexes.len(), 1);
509        assert_eq!(t.indexes[0].num_messages, 7);
510        assert_eq!(t.indexes[0].heap_addr, 0x8000);
511    }
512
513    #[test]
514    fn master_table_rejects_a_corrupt_checksum() {
515        let mut buf = master_table(&[index_header(1 << MSG_DATATYPE, 0x8000)]);
516        let n = buf.len();
517        buf[n - 1] ^= 0xff;
518        assert!(matches!(
519            SohmMasterTable::decode(&buf, &ctx(), 1).unwrap_err(),
520            FormatError::ChecksumMismatch { .. }
521        ));
522    }
523
524    #[test]
525    fn master_table_rejects_a_bad_index_version() {
526        let mut hdr = index_header(1 << MSG_DATATYPE, 0x8000);
527        hdr[0] = 1;
528        let buf = master_table(&[hdr]);
529        assert!(matches!(
530            SohmMasterTable::decode(&buf, &ctx(), 1).unwrap_err(),
531            FormatError::InvalidVersion(1)
532        ));
533    }
534
535    /// The heap is picked by the index whose type mask covers the message,
536    /// not by index order.
537    #[test]
538    fn heap_address_is_selected_by_message_type() {
539        let buf = master_table(&[
540            index_header(1 << MSG_ATTRIBUTE, 0x1000),
541            index_header((1 << MSG_DATATYPE) | (1 << MSG_DATASPACE), 0x2000),
542        ]);
543        let t = SohmMasterTable::decode(&buf, &ctx(), 2).unwrap();
544        assert_eq!(t.heap_addr(MSG_ATTRIBUTE), Some(0x1000));
545        assert_eq!(t.heap_addr(MSG_DATATYPE), Some(0x2000));
546        assert_eq!(t.heap_addr(MSG_DATASPACE), Some(0x2000));
547        // Not a shareable type at all.
548        assert_eq!(t.heap_addr(crate::format::messages::MSG_DATA_LAYOUT), None);
549    }
550
551    /// `H5SM__type_to_flag` maps the old fill-value message onto the new
552    /// one's bit, so a file sharing fill values finds one index either way.
553    #[test]
554    fn old_fill_value_shares_the_new_fill_value_bit() {
555        assert_eq!(type_flag(MSG_FILL_VALUE_OLD), type_flag(MSG_FILL_VALUE));
556        assert_eq!(type_flag(MSG_FILL_VALUE), Some(1 << MSG_FILL_VALUE));
557        assert_eq!(type_flag(crate::format::messages::MSG_LINK), None);
558    }
559
560    /// The bytes h5py's `h5d.create` from a committed TypeID left in the
561    /// dataset's datatype message: version 2, `H5O_SHARE_TYPE_COMMITTED`,
562    /// then the datatype object header's address.
563    #[test]
564    fn committed_pointer_encodes_the_bytes_h5o_shared_encode_writes() {
565        let buf = SharedMessagePointer::encode_committed(0x320, &ctx());
566        let mut want = vec![2u8, 2u8];
567        want.extend_from_slice(&0x320u64.to_le_bytes());
568        assert_eq!(buf, want);
569        assert_eq!(
570            SharedMessagePointer::decode(&buf, &ctx()).unwrap(),
571            SharedMessagePointer::committed(0x320)
572        );
573    }
574
575    /// A four-byte address file narrows the pointer to match.
576    #[test]
577    fn committed_pointer_follows_the_address_width() {
578        let ctx4 = FormatContext {
579            sizeof_addr: 4,
580            sizeof_size: 4,
581        };
582        let buf = SharedMessagePointer::encode_committed(0x1234, &ctx4);
583        assert_eq!(buf, vec![2u8, 2, 0x34, 0x12, 0, 0]);
584        assert_eq!(
585            SharedMessagePointer::decode(&buf, &ctx4).unwrap().oh_addr,
586            0x1234
587        );
588    }
589
590    #[test]
591    fn version_three_heap_pointer_carries_a_heap_id() {
592        let mut buf = vec![3u8, 1u8];
593        buf.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
594        let p = SharedMessagePointer::decode(&buf, &ctx()).unwrap();
595        assert_eq!(p.location, SharedLocation::Sohm);
596        assert_eq!(p.heap_id, [1, 2, 3, 4, 5, 6, 7, 8]);
597    }
598
599    #[test]
600    fn version_three_committed_pointer_carries_an_address() {
601        let mut buf = vec![3u8, 2u8];
602        buf.extend_from_slice(&0x4321u64.to_le_bytes());
603        let p = SharedMessagePointer::decode(&buf, &ctx()).unwrap();
604        assert_eq!(p.location, SharedLocation::Committed);
605        assert_eq!(p.oh_addr, 0x4321);
606    }
607
608    /// Version 1 has six reserved bytes and a local-heap address before the
609    /// object header address, and no type byte at all.
610    #[test]
611    fn version_one_pointer_skips_the_symbol_table_entry_prefix() {
612        let mut buf = vec![1u8, 0u8];
613        buf.extend_from_slice(&[0u8; 6]);
614        buf.extend_from_slice(&0xdeadu64.to_le_bytes()); // local heap address
615        buf.extend_from_slice(&0x9999u64.to_le_bytes());
616        let p = SharedMessagePointer::decode(&buf, &ctx()).unwrap();
617        assert_eq!(p.location, SharedLocation::Committed);
618        assert_eq!(p.oh_addr, 0x9999);
619    }
620
621    /// The bytes libhdf5 1.14.6 writes for a dataset sharing `/t` at address
622    /// 800, taken verbatim from an h5py-written file.
623    #[test]
624    fn a_libhdf5_committed_datatype_reference_names_its_object_header() {
625        let buf = [0x02, 0x02, 0x20, 0x03, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
626        let p = SharedMessagePointer::decode(&buf, &ctx()).unwrap();
627        assert_eq!(p.location, SharedLocation::Committed);
628        assert_eq!(p.oh_addr, 800);
629    }
630
631    /// Version 2 predates the committed flag: a non-heap pointer is a
632    /// committed datatype whatever the type byte says.
633    #[test]
634    fn version_two_non_heap_pointer_is_committed() {
635        let mut buf = vec![2u8, 0u8];
636        buf.extend_from_slice(&0x77u64.to_le_bytes());
637        let p = SharedMessagePointer::decode(&buf, &ctx()).unwrap();
638        assert_eq!(p.location, SharedLocation::Committed);
639        assert_eq!(p.oh_addr, 0x77);
640    }
641
642    #[test]
643    fn heap_pointer_before_version_three_is_rejected() {
644        let mut buf = vec![2u8, 1u8];
645        buf.extend_from_slice(&[0u8; 8]);
646        assert!(matches!(
647            SharedMessagePointer::decode(&buf, &ctx()).unwrap_err(),
648            FormatError::InvalidData(_)
649        ));
650    }
651
652    #[test]
653    fn pointer_rejects_unknown_versions() {
654        assert!(matches!(
655            SharedMessagePointer::decode(&[0u8, 1u8], &ctx()).unwrap_err(),
656            FormatError::InvalidVersion(0)
657        ));
658        assert!(matches!(
659            SharedMessagePointer::decode(&[4u8, 1u8], &ctx()).unwrap_err(),
660            FormatError::InvalidVersion(4)
661        ));
662    }
663
664    /// The dataspace and datatype messages `sohm_list.h5` shares, with the
665    /// hash libhdf5 filed each under. Seeding lookup3 with the message type
666    /// id is what the seed is *for*: the same bytes under another class must
667    /// not collide with these.
668    #[test]
669    fn message_hash_matches_the_fixture_records() {
670        // A simple dataspace of [8] with max dims, as libhdf5 encoded it.
671        let sdspace = [
672            1u8, 1, 1, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0,
673        ];
674        assert_eq!(message_hash(&sdspace, MSG_DATASPACE), 701521455);
675        // H5T_IEEE_F64LE.
676        let dtype = [
677            0x11u8, 0x20, 0x3f, 0x00, 8, 0, 0, 0, 0, 0, 0x40, 0x00, 0x34, 0x0b, 0x00, 0x34, 0xff,
678            0x03, 0x00, 0x00,
679        ];
680        assert_eq!(message_hash(&dtype, MSG_DATATYPE), 3573483313);
681        assert_ne!(
682            message_hash(&sdspace, MSG_DATASPACE),
683            message_hash(&sdspace, MSG_DATATYPE)
684        );
685    }
686
687    #[test]
688    fn record_is_seventeen_bytes_for_eight_byte_addresses() {
689        assert_eq!(record_size(&ctx()), 17);
690        assert_eq!(
691            record_size(&FormatContext {
692                sizeof_addr: 4,
693                sizeof_size: 4
694            }),
695            17
696        );
697    }
698
699    /// The four records `sohm_list.h5` holds, byte for byte, including the
700    /// checksum that follows the last one rather than the end of the block.
701    #[test]
702    fn list_index_encodes_the_fixture_image() {
703        let heaped = |hash, ref_count, heap_id| SohmRecord {
704            hash,
705            location: SohmRecordLocation::InHeap { ref_count, heap_id },
706        };
707        let records = [
708            heaped(701521455, 5, [0x00, 0x42, 0, 0, 0, 0, 0x18, 0x00]),
709            heaped(3573483313, 1, [0x00, 0x16, 0, 0, 0, 0, 0x14, 0x00]),
710            heaped(826238635, 1, [0x00, 0x2a, 0, 0, 0, 0, 0x18, 0x00]),
711            heaped(2575530442, 4, [0x00, 0x7a, 0, 0, 0, 0, 0x38, 0x00]),
712        ];
713        let image = encode_list(&records, &ctx());
714        let want = concat!(
715            "534d4c49",
716            "002f5ed029050000000042000000001800",
717            "003107ffd4010000000016000000001400",
718            "00ab663f3101000000002a000000001800",
719            "00ca79839904000000007a000000003800",
720            "cbfec07c",
721        );
722        assert_eq!(hex(&image), want);
723        // The block the index occupies is sized for `list_max` records; the
724        // image stops after the ones in use.
725        assert_eq!(list_size(&ctx(), 50), 858);
726        assert!(image.len() < list_size(&ctx(), 50));
727    }
728
729    /// The other record form (`H5SM__message_encode`'s `else` branch): a
730    /// location byte, the hash, a reserved zero, the message class, a
731    /// creation index and the address of the header holding the body.
732    #[test]
733    fn an_object_header_record_names_the_header_holding_the_body() {
734        let record = SohmRecord {
735            hash: 701521455,
736            location: SohmRecordLocation::InObjectHeader {
737                msg_type: MSG_DATASPACE,
738                index: 0,
739                oh_addr: 0x0349,
740            },
741        };
742        assert_eq!(
743            hex(&record.encode(&ctx())),
744            concat!(
745                "01",               // H5SM_IN_OH
746                "2f5ed029",         // hash, little-endian
747                "00",               // reserved
748                "01",               // message type: dataspace
749                "0000",             // creation index
750                "4903000000000000", // object header address
751            )
752        );
753        assert_eq!(record.encode(&ctx()).len(), record_size(&ctx()));
754    }
755
756    fn hex(bytes: &[u8]) -> String {
757        bytes.iter().map(|b| format!("{b:02x}")).collect()
758    }
759
760    /// `sohm_list.h5`'s master table: one list index over dataspace, datatype
761    /// and attribute messages.
762    #[test]
763    fn master_table_encodes_what_decode_reads_back() {
764        let table = SohmMasterTable {
765            indexes: vec![SohmIndexHeader {
766                index_type: SOHM_INDEX_LIST,
767                mesg_types: (1 << MSG_DATASPACE) | (1 << MSG_DATATYPE) | (1 << MSG_ATTRIBUTE),
768                min_mesg_size: 0,
769                list_max: 50,
770                btree_min: 40,
771                num_messages: 4,
772                index_addr: 1125,
773                heap_addr: 1983,
774            }],
775        };
776        let image = table.encode(&ctx());
777        assert_eq!(image.len(), SohmMasterTable::encoded_size(&ctx(), 1));
778        assert_eq!(&image[..4], &SMTB_SIGNATURE);
779        assert_eq!(
780            u16::from_le_bytes([image[6], image[7]]),
781            0x100a,
782            "the type mask libhdf5 wrote for DTYPE|SDSPACE|ATTR"
783        );
784        assert_eq!(SohmMasterTable::decode(&image, &ctx(), 1).unwrap(), table);
785    }
786
787    #[test]
788    fn heap_pointer_is_a_version_three_message() {
789        let id = [0x00, 0x7a, 0, 0, 0, 0, 0x38, 0x00];
790        let buf = SharedMessagePointer::encode_sohm(id);
791        assert_eq!(hex(&buf), "0301007a000000003800");
792        let p = SharedMessagePointer::decode(&buf, &ctx()).unwrap();
793        assert_eq!(p.location, SharedLocation::Sohm);
794        assert_eq!(p.heap_id, id);
795    }
796
797    #[test]
798    fn pointer_rejects_a_truncated_body() {
799        assert!(matches!(
800            SharedMessagePointer::decode(&[3u8, 1u8, 0, 0], &ctx()).unwrap_err(),
801            FormatError::BufferTooShort { .. }
802        ));
803    }
804}