Skip to main content

rustyhdf5_format/
shared_message.rs

1//! HDF5 Shared Object Header Message resolution.
2//!
3//! When a header message has its "shared" flag (bit 1 of msg_flags) set,
4//! the message data is not the actual message content but a reference
5//! to a shared copy stored elsewhere.
6//!
7//! Shared message reference types:
8//! - Type 0: shared in the same object header (not typically used)
9//! - Type 1: shared in another object header (version 1-2)
10//! - Type 2: shared in the SOHM table (via fractal heap, version 3)
11//! - Type 3: shared in another object header (version 3)
12//!
13//! SOHM table structures:
14//! - SharedMessageTable message (0x000F) in superblock extension: version + table_addr + nindexes
15//! - SMTB structure at table_addr: per-index metadata (type flags, addresses, etc.)
16//! - SMLI list structure: simple list of shared message entries
17//! - B-tree v2 type 7: indexed shared message entries
18
19#[cfg(not(feature = "std"))]
20use alloc::vec::Vec;
21
22use crate::btree_v2::{BTreeV2Header, collect_btree_v2_records};
23use crate::error::FormatError;
24use crate::fractal_heap::FractalHeapHeader;
25use crate::message_type::MessageType;
26use crate::object_header::ObjectHeader;
27
28/// Fractal heap ID length for SOHM entries (fixed at 8 bytes).
29const FHEAP_ID_LEN: usize = 8;
30
31/// A resolved shared message reference.
32#[derive(Debug, Clone)]
33pub struct SharedMessageRef {
34    /// The type of shared message reference.
35    pub ref_type: u8,
36    /// Version of the shared message encoding.
37    pub version: u8,
38    /// Address of the object header containing the shared message (type 1, 3).
39    pub object_header_address: Option<u64>,
40    /// Fractal heap ID for type 2 (SOHM) references.
41    pub heap_id: Option<[u8; FHEAP_ID_LEN]>,
42}
43
44/// Parsed Shared Message Table message (type 0x000F from superblock extension).
45#[derive(Debug, Clone)]
46pub struct SohmTableMessage {
47    /// Version of the shared message table message.
48    pub version: u8,
49    /// Address of the SOHM table (SMTB structure).
50    pub table_address: u64,
51    /// Number of shared message indexes.
52    pub nindexes: u8,
53}
54
55/// A single SOHM index entry from the SMTB table.
56#[derive(Debug, Clone)]
57pub struct SohmIndex {
58    /// Index type: 0 = list, 1 = B-tree.
59    pub index_type: u8,
60    /// Bitmask of message types stored in this index.
61    pub mesg_types: u16,
62    /// Minimum message size to share.
63    pub min_mesg_size: u32,
64    /// Maximum messages before converting list to B-tree.
65    pub list_max: u16,
66    /// Minimum messages before converting B-tree back to list.
67    pub btree_min: u16,
68    /// Number of messages currently in this index.
69    pub num_messages: u16,
70    /// Address of the list (SMLI) or B-tree v2 header (BTHD).
71    pub index_addr: u64,
72    /// Address of the fractal heap for this index.
73    pub heap_addr: u64,
74}
75
76/// Parsed SOHM table (SMTB structure).
77#[derive(Debug, Clone)]
78pub struct SohmTable {
79    /// The indexes in this table.
80    pub indexes: Vec<SohmIndex>,
81}
82
83/// A single entry in a SOHM list or B-tree.
84#[derive(Debug, Clone)]
85pub struct SohmEntry {
86    /// Location: 0 = in fractal heap, 1 = in object header.
87    pub location: u8,
88    /// Hash of the message.
89    pub hash: u32,
90    /// Fractal heap ID (when location = 0).
91    pub heap_id: Option<[u8; FHEAP_ID_LEN]>,
92    /// Reference count (when location = 0).
93    pub ref_count: Option<u32>,
94    /// Message index within OH (when location = 1).
95    pub mesg_index: Option<u16>,
96    /// Object header address (when location = 1).
97    pub oh_addr: Option<u64>,
98}
99
100fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
101    let s = size as usize;
102    if pos + s > data.len() {
103        return Err(FormatError::UnexpectedEof {
104            expected: pos + s,
105            available: data.len(),
106        });
107    }
108    Ok(match size {
109        2 => u16::from_le_bytes([data[pos], data[pos + 1]]) as u64,
110        4 => u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as u64,
111        8 => u64::from_le_bytes([
112            data[pos], data[pos + 1], data[pos + 2], data[pos + 3],
113            data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7],
114        ]),
115        _ => return Err(FormatError::InvalidOffsetSize(size)),
116    })
117}
118
119fn ensure_len(data: &[u8], pos: usize, needed: usize) -> Result<(), FormatError> {
120    match pos.checked_add(needed) {
121        Some(end) if end <= data.len() => Ok(()),
122        _ => Err(FormatError::UnexpectedEof {
123            expected: pos.saturating_add(needed),
124            available: data.len(),
125        }),
126    }
127}
128
129/// Check whether a header message has its shared flag set.
130pub fn is_shared(msg_flags: u8) -> bool {
131    msg_flags & 0x02 != 0
132}
133
134/// Parse a shared message reference from the message data.
135///
136/// When the shared flag is set on a message, the data contains a reference
137/// instead of the actual message content.
138pub fn parse_shared_ref(
139    data: &[u8],
140    offset_size: u8,
141) -> Result<SharedMessageRef, FormatError> {
142    ensure_len(data, 0, 2)?;
143    let version = data[0];
144    let ref_type = data[1];
145
146    match version {
147        1 | 2 => {
148            // v1/v2: reserved(6) + address(offset_size)
149            let pos = 2 + 6; // skip reserved bytes
150            ensure_len(data, pos, offset_size as usize)?;
151            let addr = read_offset(data, pos, offset_size)?;
152            Ok(SharedMessageRef {
153                ref_type,
154                version,
155                object_header_address: Some(addr),
156                heap_id: None,
157            })
158        }
159        3 => {
160            match ref_type {
161                1 | 3 => {
162                    // type 1/3: message in another object header
163                    // v3 layout: version(1) + type(1) + address(offset_size)
164                    ensure_len(data, 2, offset_size as usize)?;
165                    let addr = read_offset(data, 2, offset_size)?;
166                    Ok(SharedMessageRef {
167                        ref_type,
168                        version,
169                        object_header_address: Some(addr),
170                        heap_id: None,
171                    })
172                }
173                2 => {
174                    // type 2: SOHM table (fractal heap ID)
175                    ensure_len(data, 2, FHEAP_ID_LEN)?;
176                    let mut id = [0u8; FHEAP_ID_LEN];
177                    id.copy_from_slice(&data[2..2 + FHEAP_ID_LEN]);
178                    Ok(SharedMessageRef {
179                        ref_type,
180                        version,
181                        object_header_address: None,
182                        heap_id: Some(id),
183                    })
184                }
185                _ => Err(FormatError::InvalidSharedMessageVersion(ref_type)),
186            }
187        }
188        _ => Err(FormatError::InvalidSharedMessageVersion(version)),
189    }
190}
191
192// ---- SOHM Table Message (0x000F) parsing ----
193
194/// Parse a Shared Message Table message (type 0x000F) from the superblock extension.
195///
196/// Format: version(1) + table_address(offset_size) + nindexes(1)
197pub fn parse_sohm_table_message(
198    data: &[u8],
199    offset_size: u8,
200) -> Result<SohmTableMessage, FormatError> {
201    ensure_len(data, 0, 1)?;
202    let version = data[0];
203    if version != 0 {
204        return Err(FormatError::InvalidSohmTableVersion(version));
205    }
206    let pos = 1;
207    ensure_len(data, pos, offset_size as usize + 1)?;
208    let table_address = read_offset(data, pos, offset_size)?;
209    let nindexes = data[pos + offset_size as usize];
210    Ok(SohmTableMessage {
211        version,
212        table_address,
213        nindexes,
214    })
215}
216
217// ---- SMTB table parsing ----
218
219/// Parse the SOHM table structure (signature "SMTB") from the file.
220///
221/// Each index entry: index_type(1) + mesg_types(2) + min_mesg_size(4) +
222///   list_max(2) + btree_min(2) + num_messages(2) + index_addr(offset_size) +
223///   heap_addr(offset_size)
224pub fn parse_sohm_table(
225    file_data: &[u8],
226    table_addr: usize,
227    nindexes: u8,
228    offset_size: u8,
229) -> Result<SohmTable, FormatError> {
230    ensure_len(file_data, table_addr, 4)?;
231    if &file_data[table_addr..table_addr + 4] != b"SMTB" {
232        return Err(FormatError::InvalidSohmTableSignature);
233    }
234    let mut pos = table_addr + 4;
235    let os = offset_size as usize;
236    let entry_size = 1 + 2 + 4 + 2 + 2 + 2 + os + os; // 13 + 2*offset_size
237
238    let mut indexes = Vec::with_capacity(nindexes as usize);
239    for _ in 0..nindexes {
240        ensure_len(file_data, pos, entry_size)?;
241        let index_type = file_data[pos];
242        pos += 1;
243        let mesg_types = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
244        pos += 2;
245        let min_mesg_size = u32::from_le_bytes([
246            file_data[pos], file_data[pos + 1], file_data[pos + 2], file_data[pos + 3],
247        ]);
248        pos += 4;
249        let list_max = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
250        pos += 2;
251        let btree_min = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
252        pos += 2;
253        let num_messages = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
254        pos += 2;
255        let index_addr = read_offset(file_data, pos, offset_size)?;
256        pos += os;
257        let heap_addr = read_offset(file_data, pos, offset_size)?;
258        pos += os;
259
260        indexes.push(SohmIndex {
261            index_type,
262            mesg_types,
263            min_mesg_size,
264            list_max,
265            btree_min,
266            num_messages,
267            index_addr,
268            heap_addr,
269        });
270    }
271    // 4-byte checksum follows (skip for now)
272    Ok(SohmTable { indexes })
273}
274
275// ---- SMLI list parsing ----
276
277/// Compute the size of a single SOHM entry in a list or B-tree record.
278///
279/// Entry: location(1) + hash(4) + max(oh_entry, heap_entry)
280/// OH entry: mesg_index(2) + oh_addr(offset_size)
281/// Heap entry: heap_id(8) + ref_count(4) = 12
282fn sohm_entry_size(offset_size: u8) -> usize {
283    let oh_size = 2 + offset_size as usize;
284    let heap_size = FHEAP_ID_LEN + 4;
285    1 + 4 + oh_size.max(heap_size)
286}
287
288/// Parse a single SOHM entry from raw bytes.
289fn parse_sohm_entry(data: &[u8], offset_size: u8) -> Result<SohmEntry, FormatError> {
290    let entry_sz = sohm_entry_size(offset_size);
291    ensure_len(data, 0, entry_sz)?;
292    let location = data[0];
293    let hash = u32::from_le_bytes([data[1], data[2], data[3], data[4]]);
294    let pos = 5;
295
296    if location == 0 {
297        // In fractal heap
298        ensure_len(data, pos, FHEAP_ID_LEN + 4)?;
299        let mut heap_id = [0u8; FHEAP_ID_LEN];
300        heap_id.copy_from_slice(&data[pos..pos + FHEAP_ID_LEN]);
301        let ref_count = u32::from_le_bytes([
302            data[pos + FHEAP_ID_LEN],
303            data[pos + FHEAP_ID_LEN + 1],
304            data[pos + FHEAP_ID_LEN + 2],
305            data[pos + FHEAP_ID_LEN + 3],
306        ]);
307        Ok(SohmEntry {
308            location,
309            hash,
310            heap_id: Some(heap_id),
311            ref_count: Some(ref_count),
312            mesg_index: None,
313            oh_addr: None,
314        })
315    } else {
316        // In object header
317        ensure_len(data, pos, 2 + offset_size as usize)?;
318        let mesg_index = u16::from_le_bytes([data[pos], data[pos + 1]]);
319        let oh_addr = read_offset(data, pos + 2, offset_size)?;
320        Ok(SohmEntry {
321            location,
322            hash,
323            heap_id: None,
324            ref_count: None,
325            mesg_index: Some(mesg_index),
326            oh_addr: Some(oh_addr),
327        })
328    }
329}
330
331/// Parse a SOHM list (signature "SMLI") and return all entries.
332pub fn parse_sohm_list(
333    file_data: &[u8],
334    list_addr: usize,
335    num_messages: u16,
336    offset_size: u8,
337) -> Result<Vec<SohmEntry>, FormatError> {
338    ensure_len(file_data, list_addr, 4)?;
339    if &file_data[list_addr..list_addr + 4] != b"SMLI" {
340        return Err(FormatError::InvalidSohmListSignature);
341    }
342    let entry_sz = sohm_entry_size(offset_size);
343    let mut pos = list_addr + 4;
344    let mut entries = Vec::with_capacity(num_messages as usize);
345    for _ in 0..num_messages {
346        ensure_len(file_data, pos, entry_sz)?;
347        let entry = parse_sohm_entry(&file_data[pos..], offset_size)?;
348        entries.push(entry);
349        pos += entry_sz;
350    }
351    Ok(entries)
352}
353
354/// Parse SOHM entries from a B-tree v2 type 7 index.
355pub fn parse_sohm_btree_entries(
356    file_data: &[u8],
357    btree_addr: usize,
358    offset_size: u8,
359    length_size: u8,
360) -> Result<Vec<SohmEntry>, FormatError> {
361    let header = BTreeV2Header::parse(file_data, btree_addr, offset_size, length_size)?;
362    let records = collect_btree_v2_records(file_data, &header, offset_size, length_size)?;
363    let mut entries = Vec::with_capacity(records.len());
364    for rec in &records {
365        let entry = parse_sohm_entry(&rec.data, offset_size)?;
366        entries.push(entry);
367    }
368    Ok(entries)
369}
370
371// ---- SOHM resolution ----
372
373/// Find the SOHM index that handles the given message type.
374fn find_index_for_msg_type(table: &SohmTable, msg_type: MessageType) -> Option<&SohmIndex> {
375    let type_bit = 1u16 << msg_type.to_u16();
376    table.indexes.iter().find(|idx| idx.mesg_types & type_bit != 0)
377}
378
379fn is_undefined(val: u64, offset_size: u8) -> bool {
380    match offset_size {
381        2 => val == 0xFFFF,
382        4 => val == 0xFFFF_FFFF,
383        8 => val == 0xFFFF_FFFF_FFFF_FFFF,
384        _ => false,
385    }
386}
387
388/// Resolve a type 2 (SOHM) shared message reference.
389///
390/// Uses the heap ID from the shared ref to read the message data from
391/// the fractal heap associated with the matching SOHM index.
392pub fn resolve_sohm_message(
393    file_data: &[u8],
394    heap_id: &[u8; FHEAP_ID_LEN],
395    sohm_table: &SohmTable,
396    target_msg_type: MessageType,
397    offset_size: u8,
398    length_size: u8,
399) -> Result<Vec<u8>, FormatError> {
400    let index = find_index_for_msg_type(sohm_table, target_msg_type).ok_or(
401        FormatError::InvalidSharedMessageVersion(2),
402    )?;
403
404    if is_undefined(index.heap_addr, offset_size) {
405        return Err(FormatError::InvalidSharedMessageVersion(2));
406    }
407
408    let fh_header = FractalHeapHeader::parse(
409        file_data, index.heap_addr as usize, offset_size, length_size,
410    )?;
411    fh_header.read_managed_object(file_data, heap_id, offset_size)
412}
413
414/// Resolve a shared message to its actual message data.
415///
416/// For type 1/3 (shared in another object header), reads the target object header
417/// and finds the message of the specified type.
418/// For type 2 (SOHM), uses the fractal heap from the SOHM table.
419pub fn resolve_shared_message(
420    file_data: &[u8],
421    shared_ref: &SharedMessageRef,
422    target_msg_type: MessageType,
423    offset_size: u8,
424    length_size: u8,
425) -> Result<Vec<u8>, FormatError> {
426    resolve_shared_message_with_sohm(
427        file_data, shared_ref, target_msg_type, offset_size, length_size, None,
428    )
429}
430
431/// Resolve a shared message, optionally using a SOHM table for type 2 refs.
432pub fn resolve_shared_message_with_sohm(
433    file_data: &[u8],
434    shared_ref: &SharedMessageRef,
435    target_msg_type: MessageType,
436    offset_size: u8,
437    length_size: u8,
438    sohm_table: Option<&SohmTable>,
439) -> Result<Vec<u8>, FormatError> {
440    match shared_ref.ref_type {
441        1 | 3 => {
442            let addr = shared_ref.object_header_address.ok_or(
443                FormatError::UnexpectedEof {
444                    expected: 1,
445                    available: 0,
446                }
447            )?;
448            let target_header =
449                ObjectHeader::parse(file_data, addr as usize, offset_size, length_size)?;
450            for msg in &target_header.messages {
451                if msg.msg_type == target_msg_type && !is_shared(msg.flags) {
452                    return Ok(msg.data.clone());
453                }
454            }
455            // The message at that OH address is the message itself
456            // In many cases with type 1, the entire OH at that address IS the shared message
457            // Try returning the first message of any type that isn't Nil
458            for msg in &target_header.messages {
459                if msg.msg_type == target_msg_type {
460                    return Ok(msg.data.clone());
461                }
462            }
463            // Fall back to first non-nil message
464            for msg in &target_header.messages {
465                if msg.msg_type != MessageType::Nil {
466                    return Ok(msg.data.clone());
467                }
468            }
469            Err(FormatError::UnexpectedEof {
470                expected: 1,
471                available: 0,
472            })
473        }
474        2 => {
475            let heap_id = shared_ref.heap_id.as_ref().ok_or(
476                FormatError::InvalidSharedMessageVersion(2),
477            )?;
478            let table = sohm_table.ok_or(
479                FormatError::InvalidSharedMessageVersion(2),
480            )?;
481            resolve_sohm_message(
482                file_data, heap_id, table, target_msg_type, offset_size, length_size,
483            )
484        }
485        _ => {
486            Err(FormatError::InvalidSharedMessageVersion(shared_ref.ref_type))
487        }
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    #[test]
496    fn is_shared_flag() {
497        assert!(!is_shared(0x00));
498        assert!(!is_shared(0x01));
499        assert!(is_shared(0x02));
500        assert!(is_shared(0x03));
501        assert!(is_shared(0x06));
502    }
503
504    #[test]
505    fn parse_v3_type1_ref() {
506        let mut data = Vec::new();
507        data.push(3); // version
508        data.push(1); // type 1 = shared in another OH
509        data.extend_from_slice(&0x1234u64.to_le_bytes()); // address
510
511        let shared = parse_shared_ref(&data, 8).unwrap();
512        assert_eq!(shared.version, 3);
513        assert_eq!(shared.ref_type, 1);
514        assert_eq!(shared.object_header_address, Some(0x1234));
515        assert!(shared.heap_id.is_none());
516    }
517
518    #[test]
519    fn parse_v3_type3_ref() {
520        let mut data = Vec::new();
521        data.push(3); // version
522        data.push(3); // type 3 = shared in another OH (v3 encoding)
523        data.extend_from_slice(&0xABCDu64.to_le_bytes());
524
525        let shared = parse_shared_ref(&data, 8).unwrap();
526        assert_eq!(shared.version, 3);
527        assert_eq!(shared.ref_type, 3);
528        assert_eq!(shared.object_header_address, Some(0xABCD));
529    }
530
531    #[test]
532    fn parse_v1_ref() {
533        let mut data = Vec::new();
534        data.push(1); // version
535        data.push(0); // type
536        data.extend_from_slice(&[0u8; 6]); // reserved
537        data.extend_from_slice(&0x5678u64.to_le_bytes());
538
539        let shared = parse_shared_ref(&data, 8).unwrap();
540        assert_eq!(shared.version, 1);
541        assert_eq!(shared.object_header_address, Some(0x5678));
542    }
543
544    #[test]
545    fn parse_v2_ref() {
546        let mut data = Vec::new();
547        data.push(2); // version
548        data.push(0); // type
549        data.extend_from_slice(&[0u8; 6]); // reserved
550        data.extend_from_slice(&0x9000u32.to_le_bytes());
551
552        let shared = parse_shared_ref(&data, 4).unwrap();
553        assert_eq!(shared.version, 2);
554        assert_eq!(shared.object_header_address, Some(0x9000));
555    }
556
557    #[test]
558    fn parse_v3_type2_sohm() {
559        let mut data = Vec::new();
560        data.push(3); // version
561        data.push(2); // type 2 = SOHM heap
562        data.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44]);
563
564        let shared = parse_shared_ref(&data, 8).unwrap();
565        assert_eq!(shared.version, 3);
566        assert_eq!(shared.ref_type, 2);
567        assert_eq!(shared.object_header_address, None);
568        assert_eq!(
569            shared.heap_id,
570            Some([0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44])
571        );
572    }
573
574    #[test]
575    fn parse_v3_type2_too_short() {
576        let mut data = Vec::new();
577        data.push(3); // version
578        data.push(2); // type 2 = SOHM heap
579        data.extend_from_slice(&[0xAA, 0xBB]); // only 2 bytes, need 8
580
581        let err = parse_shared_ref(&data, 8).unwrap_err();
582        assert!(matches!(err, FormatError::UnexpectedEof { .. }));
583    }
584
585    #[test]
586    fn invalid_version() {
587        let data = vec![99, 0];
588        let err = parse_shared_ref(&data, 8).unwrap_err();
589        assert_eq!(err, FormatError::InvalidSharedMessageVersion(99));
590    }
591
592    #[test]
593    fn truncated_data() {
594        let data = vec![3u8]; // too short
595        let err = parse_shared_ref(&data, 8).unwrap_err();
596        assert!(matches!(err, FormatError::UnexpectedEof { .. }));
597    }
598
599    #[test]
600    fn parse_four_byte_offsets() {
601        let mut data = Vec::new();
602        data.push(3); // version
603        data.push(1); // type 1
604        data.extend_from_slice(&0x1000u32.to_le_bytes());
605
606        let shared = parse_shared_ref(&data, 4).unwrap();
607        assert_eq!(shared.object_header_address, Some(0x1000));
608    }
609
610    // ---- SOHM table message tests ----
611
612    #[test]
613    fn parse_sohm_table_message_8byte() {
614        let mut data = Vec::new();
615        data.push(0); // version
616        data.extend_from_slice(&0x2000u64.to_le_bytes()); // table address
617        data.push(3); // nindexes
618
619        let msg = parse_sohm_table_message(&data, 8).unwrap();
620        assert_eq!(msg.version, 0);
621        assert_eq!(msg.table_address, 0x2000);
622        assert_eq!(msg.nindexes, 3);
623    }
624
625    #[test]
626    fn parse_sohm_table_message_4byte() {
627        let mut data = Vec::new();
628        data.push(0); // version
629        data.extend_from_slice(&0x1000u32.to_le_bytes()); // table address
630        data.push(1); // nindexes
631
632        let msg = parse_sohm_table_message(&data, 4).unwrap();
633        assert_eq!(msg.table_address, 0x1000);
634        assert_eq!(msg.nindexes, 1);
635    }
636
637    #[test]
638    fn parse_sohm_table_message_bad_version() {
639        let data = vec![1]; // version 1 is invalid
640        let err = parse_sohm_table_message(&data, 8).unwrap_err();
641        assert_eq!(err, FormatError::InvalidSohmTableVersion(1));
642    }
643
644    // ---- SMTB table tests ----
645
646    fn build_smtb(indexes: &[SohmIndex], offset_size: u8) -> Vec<u8> {
647        let mut buf = Vec::new();
648        buf.extend_from_slice(b"SMTB");
649        for idx in indexes {
650            buf.push(idx.index_type);
651            buf.extend_from_slice(&idx.mesg_types.to_le_bytes());
652            buf.extend_from_slice(&idx.min_mesg_size.to_le_bytes());
653            buf.extend_from_slice(&idx.list_max.to_le_bytes());
654            buf.extend_from_slice(&idx.btree_min.to_le_bytes());
655            buf.extend_from_slice(&idx.num_messages.to_le_bytes());
656            match offset_size {
657                4 => {
658                    buf.extend_from_slice(&(idx.index_addr as u32).to_le_bytes());
659                    buf.extend_from_slice(&(idx.heap_addr as u32).to_le_bytes());
660                }
661                8 => {
662                    buf.extend_from_slice(&idx.index_addr.to_le_bytes());
663                    buf.extend_from_slice(&idx.heap_addr.to_le_bytes());
664                }
665                _ => {}
666            }
667        }
668        // Checksum placeholder
669        buf.extend_from_slice(&[0u8; 4]);
670        buf
671    }
672
673    #[test]
674    fn parse_smtb_one_index() {
675        let indexes = vec![SohmIndex {
676            index_type: 0,
677            mesg_types: 0x0008, // Datatype
678            min_mesg_size: 50,
679            list_max: 50,
680            btree_min: 40,
681            num_messages: 2,
682            index_addr: 0x3000,
683            heap_addr: 0x4000,
684        }];
685        let data = build_smtb(&indexes, 8);
686        let table = parse_sohm_table(&data, 0, 1, 8).unwrap();
687        assert_eq!(table.indexes.len(), 1);
688        assert_eq!(table.indexes[0].index_type, 0);
689        assert_eq!(table.indexes[0].mesg_types, 0x0008);
690        assert_eq!(table.indexes[0].min_mesg_size, 50);
691        assert_eq!(table.indexes[0].num_messages, 2);
692        assert_eq!(table.indexes[0].index_addr, 0x3000);
693        assert_eq!(table.indexes[0].heap_addr, 0x4000);
694    }
695
696    #[test]
697    fn parse_smtb_two_indexes_4byte() {
698        let indexes = vec![
699            SohmIndex {
700                index_type: 0, mesg_types: 0x0008, min_mesg_size: 50,
701                list_max: 50, btree_min: 40, num_messages: 1,
702                index_addr: 0x1000, heap_addr: 0x2000,
703            },
704            SohmIndex {
705                index_type: 1, mesg_types: 0x0002, min_mesg_size: 100,
706                list_max: 25, btree_min: 15, num_messages: 5,
707                index_addr: 0x5000, heap_addr: 0x6000,
708            },
709        ];
710        let data = build_smtb(&indexes, 4);
711        let table = parse_sohm_table(&data, 0, 2, 4).unwrap();
712        assert_eq!(table.indexes.len(), 2);
713        assert_eq!(table.indexes[1].index_type, 1);
714        assert_eq!(table.indexes[1].mesg_types, 0x0002);
715        assert_eq!(table.indexes[1].num_messages, 5);
716        assert_eq!(table.indexes[1].index_addr, 0x5000);
717    }
718
719    #[test]
720    fn parse_smtb_bad_signature() {
721        let mut data = vec![0u8; 32];
722        data[0..4].copy_from_slice(b"XXXX");
723        let err = parse_sohm_table(&data, 0, 1, 8).unwrap_err();
724        assert_eq!(err, FormatError::InvalidSohmTableSignature);
725    }
726
727    // ---- SOHM entry tests ----
728
729    #[test]
730    fn parse_heap_entry() {
731        let mut data = Vec::new();
732        data.push(0); // location = heap
733        data.extend_from_slice(&0x12345678u32.to_le_bytes()); // hash
734        data.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]); // heap_id
735        data.extend_from_slice(&3u32.to_le_bytes()); // ref_count
736        // Pad to entry size (entry_size for 8-byte offsets = 1+4+max(10,12)=17)
737        // OH size with 8-byte offsets = 2+8=10, heap size = 12, max=12
738        // Total entry: 1+4+12=17
739        // We wrote 1+4+8+4=17 bytes — no padding needed
740        let entry = parse_sohm_entry(&data, 8).unwrap();
741        assert_eq!(entry.location, 0);
742        assert_eq!(entry.hash, 0x12345678);
743        assert_eq!(entry.heap_id, Some([1, 2, 3, 4, 5, 6, 7, 8]));
744        assert_eq!(entry.ref_count, Some(3));
745        assert!(entry.oh_addr.is_none());
746    }
747
748    #[test]
749    fn parse_oh_entry() {
750        let mut data = Vec::new();
751        data.push(1); // location = OH
752        data.extend_from_slice(&0xAABBCCDDu32.to_le_bytes()); // hash
753        data.extend_from_slice(&5u16.to_le_bytes()); // mesg_index
754        data.extend_from_slice(&0x7000u64.to_le_bytes()); // oh_addr
755        // OH entry: 2+8=10 bytes, heap entry: 12 bytes, so max=12, need 2 bytes padding
756        data.extend_from_slice(&[0u8; 2]);
757
758        let entry = parse_sohm_entry(&data, 8).unwrap();
759        assert_eq!(entry.location, 1);
760        assert_eq!(entry.hash, 0xAABBCCDD);
761        assert_eq!(entry.mesg_index, Some(5));
762        assert_eq!(entry.oh_addr, Some(0x7000));
763        assert!(entry.heap_id.is_none());
764    }
765
766    // ---- SMLI list tests ----
767
768    fn build_smli(entries: &[SohmEntry], offset_size: u8) -> Vec<u8> {
769        let mut buf = Vec::new();
770        buf.extend_from_slice(b"SMLI");
771        let entry_sz = sohm_entry_size(offset_size);
772        for entry in entries {
773            let start = buf.len();
774            buf.push(entry.location);
775            buf.extend_from_slice(&entry.hash.to_le_bytes());
776            if entry.location == 0 {
777                buf.extend_from_slice(entry.heap_id.as_ref().unwrap());
778                buf.extend_from_slice(&entry.ref_count.unwrap().to_le_bytes());
779            } else {
780                buf.extend_from_slice(&entry.mesg_index.unwrap().to_le_bytes());
781                match offset_size {
782                    4 => buf.extend_from_slice(&(entry.oh_addr.unwrap() as u32).to_le_bytes()),
783                    8 => buf.extend_from_slice(&entry.oh_addr.unwrap().to_le_bytes()),
784                    _ => {}
785                }
786            }
787            // Pad to entry_sz
788            let written = buf.len() - start;
789            if written < entry_sz {
790                buf.resize(buf.len() + entry_sz - written, 0);
791            }
792        }
793        buf.extend_from_slice(&[0u8; 4]); // checksum
794        buf
795    }
796
797    #[test]
798    fn parse_smli_two_entries() {
799        let entries = vec![
800            SohmEntry {
801                location: 0, hash: 0x1111,
802                heap_id: Some([10, 20, 30, 40, 50, 60, 70, 80]),
803                ref_count: Some(1), mesg_index: None, oh_addr: None,
804            },
805            SohmEntry {
806                location: 0, hash: 0x2222,
807                heap_id: Some([11, 21, 31, 41, 51, 61, 71, 81]),
808                ref_count: Some(2), mesg_index: None, oh_addr: None,
809            },
810        ];
811        let data = build_smli(&entries, 8);
812        let parsed = parse_sohm_list(&data, 0, 2, 8).unwrap();
813        assert_eq!(parsed.len(), 2);
814        assert_eq!(parsed[0].hash, 0x1111);
815        assert_eq!(parsed[0].heap_id, Some([10, 20, 30, 40, 50, 60, 70, 80]));
816        assert_eq!(parsed[1].hash, 0x2222);
817        assert_eq!(parsed[1].ref_count, Some(2));
818    }
819
820    #[test]
821    fn parse_smli_bad_signature() {
822        let data = vec![b'X', b'X', b'X', b'X'];
823        let err = parse_sohm_list(&data, 0, 0, 8).unwrap_err();
824        assert_eq!(err, FormatError::InvalidSohmListSignature);
825    }
826
827    // ---- Index lookup tests ----
828
829    #[test]
830    fn find_index_for_datatype() {
831        let table = SohmTable {
832            indexes: vec![
833                SohmIndex {
834                    index_type: 0,
835                    mesg_types: 0x0008, // bit 3 = Datatype (0x0003)
836                    min_mesg_size: 50, list_max: 50, btree_min: 40,
837                    num_messages: 1, index_addr: 0x1000, heap_addr: 0x2000,
838                },
839            ],
840        };
841        let idx = find_index_for_msg_type(&table, MessageType::Datatype);
842        assert!(idx.is_some());
843        assert_eq!(idx.unwrap().heap_addr, 0x2000);
844    }
845
846    #[test]
847    fn find_index_no_match() {
848        let table = SohmTable {
849            indexes: vec![
850                SohmIndex {
851                    index_type: 0,
852                    mesg_types: 0x0002, // bit 1 = Dataspace
853                    min_mesg_size: 50, list_max: 50, btree_min: 40,
854                    num_messages: 1, index_addr: 0x1000, heap_addr: 0x2000,
855                },
856            ],
857        };
858        let idx = find_index_for_msg_type(&table, MessageType::Datatype);
859        assert!(idx.is_none());
860    }
861
862    #[test]
863    fn entry_size_calculations() {
864        // With 8-byte offsets: OH=2+8=10, heap=12, entry=1+4+12=17
865        assert_eq!(sohm_entry_size(8), 17);
866        // With 4-byte offsets: OH=2+4=6, heap=12, entry=1+4+12=17
867        assert_eq!(sohm_entry_size(4), 17);
868        // With 2-byte offsets: OH=2+2=4, heap=12, entry=1+4+12=17
869        assert_eq!(sohm_entry_size(2), 17);
870    }
871}