Skip to main content

hdf5_reader/messages/
link.rs

1//! HDF5 Link message (type 0x0006).
2//!
3//! Used in v2+ object headers to describe group membership. Each link
4//! message names one child and specifies its target (hard link to an
5//! object header address, soft link path, or external file + path).
6
7use crate::error::{Error, Result};
8use crate::io::Cursor;
9pub use hdf5_core::{LinkMessage, LinkTarget};
10
11/// Parse a link message (version 1).
12pub fn parse(
13    cursor: &mut Cursor<'_>,
14    offset_size: u8,
15    _length_size: u8,
16    msg_size: usize,
17) -> Result<LinkMessage> {
18    let start = cursor.position();
19    let version = cursor.read_u8()?;
20
21    if version != 1 {
22        return Err(Error::UnsupportedLinkVersion(version));
23    }
24
25    let flags = cursor.read_u8()?;
26
27    // Bit 3: link type field present
28    let link_type = if (flags & 0x08) != 0 {
29        cursor.read_u8()?
30    } else {
31        0 // default = hard link
32    };
33
34    // Bit 2: creation order present
35    let creation_order = if (flags & 0x04) != 0 {
36        Some(cursor.read_u64_le()?)
37    } else {
38        None
39    };
40
41    // Bit 4: link name encoding (0 = ASCII, 1 = UTF-8)
42    // We handle both the same way since our strings are already UTF-8.
43    let _name_encoding = if (flags & 0x10) != 0 {
44        cursor.read_u8()?
45    } else {
46        0
47    };
48
49    // Name length — size depends on bits 0-1 of flags
50    let name_len_size = 1 << (flags & 0x03);
51    let name_len = cursor.read_uvar(name_len_size)? as usize;
52
53    let name = cursor.read_fixed_string(name_len)?;
54
55    // Link target
56    let target = match link_type {
57        0 => {
58            // Hard link
59            let address = cursor.read_offset(offset_size)?;
60            LinkTarget::Hard { address }
61        }
62        1 => {
63            // Soft link
64            let path_len = cursor.read_u16_le()? as usize;
65            let path = cursor.read_fixed_string(path_len)?;
66            LinkTarget::Soft { path }
67        }
68        64 => {
69            // External link
70            let info_len = cursor.read_u16_le()? as usize;
71            let info_start = cursor.position();
72            // Version + flags of the external link info
73            let _ext_flags = cursor.read_u8()?;
74            let filename = cursor.read_null_terminated_string()?;
75            let path = cursor.read_null_terminated_string()?;
76            // Skip any remaining bytes in the info block
77            let info_consumed = (cursor.position() - info_start) as usize;
78            if info_consumed < info_len {
79                cursor.skip(info_len - info_consumed)?;
80            }
81            LinkTarget::External { filename, path }
82        }
83        t => return Err(Error::UnsupportedLinkType(t)),
84    };
85
86    let consumed = (cursor.position() - start) as usize;
87    if consumed < msg_size {
88        cursor.skip(msg_size - consumed)?;
89    }
90
91    Ok(LinkMessage {
92        name,
93        target,
94        creation_order,
95    })
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn parse_hard_link() {
104        let mut data = vec![
105            0x01, // version 1
106            0x00, // flags: name_len_size=1 byte, no extras
107        ];
108        // name length (1 byte) = 5
109        data.push(0x05);
110        // name = "hello"
111        data.extend_from_slice(b"hello");
112        // hard link address (8 bytes)
113        data.extend_from_slice(&0x3000u64.to_le_bytes());
114
115        let mut cursor = Cursor::new(&data);
116        let msg = parse(&mut cursor, 8, 8, data.len()).unwrap();
117        assert_eq!(msg.name, "hello");
118        assert!(msg.creation_order.is_none());
119        match &msg.target {
120            LinkTarget::Hard { address } => assert_eq!(*address, 0x3000),
121            other => panic!("expected Hard, got {:?}", other),
122        }
123    }
124
125    #[test]
126    fn parse_soft_link() {
127        let mut data = vec![
128            0x01, // version 1
129            0x08, // flags: bit 3 = link type present, name_len_size=1
130        ];
131        // link type = 1 (soft)
132        data.push(0x01);
133        // name length = 3
134        data.push(0x03);
135        // name = "lnk"
136        data.extend_from_slice(b"lnk");
137        // soft link: path length (u16) + path
138        data.extend_from_slice(&7u16.to_le_bytes());
139        data.extend_from_slice(b"/a/b/c\0");
140
141        let mut cursor = Cursor::new(&data);
142        let msg = parse(&mut cursor, 8, 8, data.len()).unwrap();
143        assert_eq!(msg.name, "lnk");
144        match &msg.target {
145            LinkTarget::Soft { path } => assert_eq!(path, "/a/b/c"),
146            other => panic!("expected Soft, got {:?}", other),
147        }
148    }
149
150    #[test]
151    fn parse_hard_link_with_creation_order() {
152        let mut data = vec![
153            0x01, // version 1
154            0x04, // flags: bit 2 = creation order present
155        ];
156        // creation order (u64) = 42
157        data.extend_from_slice(&42u64.to_le_bytes());
158        // name length = 1
159        data.push(0x01);
160        // name = "x"
161        data.push(b'x');
162        // hard link address
163        data.extend_from_slice(&0x5000u64.to_le_bytes());
164
165        let mut cursor = Cursor::new(&data);
166        let msg = parse(&mut cursor, 8, 8, data.len()).unwrap();
167        assert_eq!(msg.name, "x");
168        assert_eq!(msg.creation_order, Some(42));
169    }
170}