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