Skip to main content

rustyhdf5_format/
link_message.rs

1//! HDF5 Link message parsing (message type 0x0006).
2
3#[cfg(not(feature = "std"))]
4use alloc::{string::String, vec::Vec};
5
6use crate::datatype::CharacterSet;
7use crate::error::FormatError;
8
9/// The type of a link in an HDF5 v2 group.
10#[derive(Debug, Clone, PartialEq)]
11pub enum LinkTarget {
12    /// Hard link pointing to an object header address.
13    Hard { object_header_address: u64 },
14    /// Soft (symbolic) link with a target path string.
15    Soft { target_path: String },
16    /// External link pointing to a file and object path within it.
17    External {
18        filename: String,
19        object_path: String,
20    },
21}
22
23/// A parsed HDF5 Link message (type 0x0006).
24#[derive(Debug, Clone, PartialEq)]
25pub struct LinkMessage {
26    /// Name of this link.
27    pub name: String,
28    /// What this link points to.
29    pub link_target: LinkTarget,
30    /// Creation order, if tracked.
31    pub creation_order: Option<u64>,
32    /// Character set of the link name.
33    pub charset: CharacterSet,
34}
35
36fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
37    let s = size as usize;
38    if pos + s > data.len() {
39        return Err(FormatError::UnexpectedEof {
40            expected: pos + s,
41            available: data.len(),
42        });
43    }
44    Ok(match size {
45        1 => data[pos] as u64,
46        2 => u16::from_le_bytes([data[pos], data[pos + 1]]) as u64,
47        4 => u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as u64,
48        8 => u64::from_le_bytes([
49            data[pos],
50            data[pos + 1],
51            data[pos + 2],
52            data[pos + 3],
53            data[pos + 4],
54            data[pos + 5],
55            data[pos + 6],
56            data[pos + 7],
57        ]),
58        _ => {
59            return Err(FormatError::InvalidOffsetSize(size));
60        }
61    })
62}
63
64fn ensure_len(data: &[u8], pos: usize, needed: usize) -> Result<(), FormatError> {
65    match pos.checked_add(needed) {
66        Some(end) if end <= data.len() => Ok(()),
67        _ => Err(FormatError::UnexpectedEof {
68            expected: pos.saturating_add(needed),
69            available: data.len(),
70        }),
71    }
72}
73
74impl LinkMessage {
75    /// Serialize link message to HDF5 message bytes.
76    pub fn serialize(&self, offset_size: u8) -> Vec<u8> {
77        let mut buf = Vec::new();
78        buf.push(1); // version
79
80        let name_bytes = self.name.as_bytes();
81        let name_len = name_bytes.len();
82        let name_size_width: u8 = if name_len <= 0xFF { 1 }
83            else if name_len <= 0xFFFF { 2 }
84            else { 4 };
85
86        let is_hard = matches!(self.link_target, LinkTarget::Hard { .. });
87        let has_link_type = !is_hard;
88        let has_creation_order = self.creation_order.is_some();
89        let has_charset = self.charset != CharacterSet::Ascii;
90
91        let mut flags: u8 = 0;
92        // Bits 0-1: size of name length field
93        let size_bits = match name_size_width { 1 => 0u8, 2 => 1, 4 => 2, _ => 3 };
94        flags |= size_bits;
95        // Bit 2: creation order present
96        if has_creation_order { flags |= 0x04; }
97        // Bit 3: link type present
98        if has_link_type { flags |= 0x08; }
99        // Bit 4: charset present
100        if has_charset { flags |= 0x10; }
101        buf.push(flags);
102
103        if has_link_type {
104            match &self.link_target {
105                LinkTarget::Soft { .. } => buf.push(1),
106                LinkTarget::External { .. } => buf.push(64),
107                _ => {}
108            }
109        }
110
111        if let Some(co) = self.creation_order {
112            buf.extend_from_slice(&co.to_le_bytes());
113        }
114
115        if has_charset {
116            buf.push(match self.charset {
117                CharacterSet::Ascii => 0,
118                CharacterSet::Utf8 => 1,
119            });
120        }
121
122        match name_size_width {
123            1 => buf.push(name_len as u8),
124            2 => buf.extend_from_slice(&(name_len as u16).to_le_bytes()),
125            4 => buf.extend_from_slice(&(name_len as u32).to_le_bytes()),
126            _ => buf.extend_from_slice(&(name_len as u64).to_le_bytes()),
127        }
128        buf.extend_from_slice(name_bytes);
129
130        match &self.link_target {
131            LinkTarget::Hard { object_header_address } => {
132                match offset_size {
133                    2 => buf.extend_from_slice(&(*object_header_address as u16).to_le_bytes()),
134                    4 => buf.extend_from_slice(&(*object_header_address as u32).to_le_bytes()),
135                    8 => buf.extend_from_slice(&object_header_address.to_le_bytes()),
136                    _ => {}
137                }
138            }
139            LinkTarget::Soft { target_path } => {
140                let path_bytes = target_path.as_bytes();
141                buf.extend_from_slice(&(path_bytes.len() as u16).to_le_bytes());
142                buf.extend_from_slice(path_bytes);
143            }
144            LinkTarget::External { filename, object_path } => {
145                let mut ext_data = Vec::new();
146                ext_data.push(0); // flags
147                ext_data.extend_from_slice(filename.as_bytes());
148                ext_data.push(0);
149                ext_data.extend_from_slice(object_path.as_bytes());
150                ext_data.push(0);
151                buf.extend_from_slice(&(ext_data.len() as u16).to_le_bytes());
152                buf.extend_from_slice(&ext_data);
153            }
154        }
155
156        buf
157    }
158
159    /// Parse a Link message from raw message data.
160    ///
161    /// `offset_size` is needed for hard link target addresses.
162    pub fn parse(data: &[u8], offset_size: u8) -> Result<LinkMessage, FormatError> {
163        ensure_len(data, 0, 2)?;
164
165        let version = data[0];
166        if version != 1 {
167            return Err(FormatError::InvalidLinkVersion(version));
168        }
169
170        let flags = data[1];
171        // Bits 0-1: size of the name length field (1/2/4/8 bytes)
172        let name_size_field_width = match flags & 0x03 {
173            0 => 1u8,
174            1 => 2,
175            2 => 4,
176            3 => 8,
177            _ => unreachable!(),
178        };
179        // Bit 2: creation order field present
180        let has_creation_order = flags & 0x04 != 0;
181        // Bit 3: link type field present
182        let has_link_type = flags & 0x08 != 0;
183        // Bit 4: link name character set field present
184        let has_charset = flags & 0x10 != 0;
185
186        let mut pos = 2;
187
188        // Link type
189        let link_type_code = if has_link_type {
190            ensure_len(data, pos, 1)?;
191            let v = data[pos];
192            pos += 1;
193            v
194        } else {
195            0 // hard link
196        };
197
198        // Creation order
199        let creation_order = if has_creation_order {
200            ensure_len(data, pos, 8)?;
201            let co = u64::from_le_bytes([
202                data[pos],
203                data[pos + 1],
204                data[pos + 2],
205                data[pos + 3],
206                data[pos + 4],
207                data[pos + 5],
208                data[pos + 6],
209                data[pos + 7],
210            ]);
211            pos += 8;
212            Some(co)
213        } else {
214            None
215        };
216
217        // Character set
218        let charset = if has_charset {
219            ensure_len(data, pos, 1)?;
220            let cs = data[pos];
221            pos += 1;
222            match cs {
223                0 => CharacterSet::Ascii,
224                1 => CharacterSet::Utf8,
225                _ => return Err(FormatError::InvalidCharacterSet(cs)),
226            }
227        } else {
228            CharacterSet::Ascii
229        };
230
231        // Link name length
232        let name_len = read_offset(data, pos, name_size_field_width)? as usize;
233        pos += name_size_field_width as usize;
234
235        // Link name
236        ensure_len(data, pos, name_len)?;
237        let name = String::from_utf8_lossy(&data[pos..pos + name_len]).into_owned();
238        pos += name_len;
239
240        // Link target data
241        let link_target = match link_type_code {
242            0 => {
243                // Hard link
244                let addr = read_offset(data, pos, offset_size)?;
245                LinkTarget::Hard {
246                    object_header_address: addr,
247                }
248            }
249            1 => {
250                // Soft link
251                ensure_len(data, pos, 2)?;
252                let soft_len =
253                    u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
254                pos += 2;
255                ensure_len(data, pos, soft_len)?;
256                let target_path =
257                    String::from_utf8_lossy(&data[pos..pos + soft_len]).into_owned();
258                LinkTarget::Soft { target_path }
259            }
260            64 => {
261                // External link
262                ensure_len(data, pos, 2)?;
263                let ext_len =
264                    u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
265                pos += 2;
266                ensure_len(data, pos, ext_len)?;
267                let ext_data = &data[pos..pos + ext_len];
268                // External link value: flags(1) + null-terminated filename + null-terminated obj path
269                // Skip the flags byte
270                let start = if !ext_data.is_empty() { 1 } else { 0 };
271                let rest = &ext_data[start..];
272                let null1 = rest.iter().position(|&b| b == 0).unwrap_or(rest.len());
273                let filename =
274                    String::from_utf8_lossy(&rest[..null1]).into_owned();
275                let after_null1 = if null1 + 1 < rest.len() {
276                    null1 + 1
277                } else {
278                    rest.len()
279                };
280                let rest2 = &rest[after_null1..];
281                let null2 = rest2.iter().position(|&b| b == 0).unwrap_or(rest2.len());
282                let object_path =
283                    String::from_utf8_lossy(&rest2[..null2]).into_owned();
284                LinkTarget::External {
285                    filename,
286                    object_path,
287                }
288            }
289            other => return Err(FormatError::InvalidLinkType(other)),
290        };
291
292        Ok(LinkMessage {
293            name,
294            link_target,
295            creation_order,
296            charset,
297        })
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    /// Build a hard link message with given parameters.
306    fn build_hard_link(
307        name: &str,
308        addr: u64,
309        offset_size: u8,
310        creation_order: Option<u64>,
311        charset: Option<u8>,
312        name_size_width: u8, // 1, 2, 4
313    ) -> Vec<u8> {
314        let mut buf = Vec::new();
315        buf.push(1); // version
316
317        let mut flags: u8 = 0;
318        // Bits 0-1: name length field size
319        let size_bits = match name_size_width {
320            1 => 0u8,
321            2 => 1,
322            4 => 2,
323            8 => 3,
324            _ => 0,
325        };
326        flags |= size_bits;
327        // Bit 2: creation order present
328        if creation_order.is_some() {
329            flags |= 0x04;
330        }
331        // hard link: don't set bit 3 (link type field not present)
332        // Bit 4: charset present
333        if charset.is_some() {
334            flags |= 0x10;
335        }
336        buf.push(flags);
337
338        // no link_type field for hard links (bit 1 not set)
339
340        if let Some(co) = creation_order {
341            buf.extend_from_slice(&co.to_le_bytes());
342        }
343
344        if let Some(cs) = charset {
345            buf.push(cs);
346        }
347
348        // name length
349        let name_len = name.len();
350        match name_size_width {
351            1 => buf.push(name_len as u8),
352            2 => buf.extend_from_slice(&(name_len as u16).to_le_bytes()),
353            4 => buf.extend_from_slice(&(name_len as u32).to_le_bytes()),
354            8 => buf.extend_from_slice(&(name_len as u64).to_le_bytes()),
355            _ => {}
356        }
357
358        buf.extend_from_slice(name.as_bytes());
359
360        // hard link data: address
361        match offset_size {
362            4 => buf.extend_from_slice(&(addr as u32).to_le_bytes()),
363            8 => buf.extend_from_slice(&addr.to_le_bytes()),
364            _ => {}
365        }
366
367        buf
368    }
369
370    #[test]
371    fn hard_link_ascii_no_creation_order() {
372        let data = build_hard_link("mydata", 0x1000, 8, None, None, 1);
373        let msg = LinkMessage::parse(&data, 8).unwrap();
374        assert_eq!(msg.name, "mydata");
375        assert_eq!(
376            msg.link_target,
377            LinkTarget::Hard {
378                object_header_address: 0x1000
379            }
380        );
381        assert_eq!(msg.creation_order, None);
382        assert_eq!(msg.charset, CharacterSet::Ascii);
383    }
384
385    #[test]
386    fn hard_link_utf8_with_creation_order() {
387        let data = build_hard_link("données", 0x2000, 8, Some(42), Some(1), 1);
388        let msg = LinkMessage::parse(&data, 8).unwrap();
389        assert_eq!(msg.name, "données");
390        assert_eq!(
391            msg.link_target,
392            LinkTarget::Hard {
393                object_header_address: 0x2000
394            }
395        );
396        assert_eq!(msg.creation_order, Some(42));
397        assert_eq!(msg.charset, CharacterSet::Utf8);
398    }
399
400    #[test]
401    fn soft_link() {
402        let target = "/group1/dataset";
403        let mut data = Vec::new();
404        data.push(1); // version
405        data.push(0x08); // flags: bit 3 = link type present, name size = 1 byte (bits 0-1 = 0)
406        data.push(1); // link type = soft
407        data.push(4); // name length = 4
408        data.extend_from_slice(b"link");
409        data.extend_from_slice(&(target.len() as u16).to_le_bytes());
410        data.extend_from_slice(target.as_bytes());
411
412        let msg = LinkMessage::parse(&data, 8).unwrap();
413        assert_eq!(msg.name, "link");
414        assert_eq!(
415            msg.link_target,
416            LinkTarget::Soft {
417                target_path: target.to_string()
418            }
419        );
420    }
421
422    #[test]
423    fn name_length_2bytes() {
424        let data = build_hard_link("test", 0x500, 8, None, None, 2);
425        let msg = LinkMessage::parse(&data, 8).unwrap();
426        assert_eq!(msg.name, "test");
427    }
428
429    #[test]
430    fn name_length_4bytes() {
431        let data = build_hard_link("abcd", 0x600, 8, None, None, 4);
432        let msg = LinkMessage::parse(&data, 8).unwrap();
433        assert_eq!(msg.name, "abcd");
434    }
435
436    #[test]
437    fn invalid_version() {
438        let data = vec![2, 0, 0, 0]; // version 2
439        let err = LinkMessage::parse(&data, 8).unwrap_err();
440        assert_eq!(err, FormatError::InvalidLinkVersion(2));
441    }
442
443    #[test]
444    fn invalid_link_type() {
445        let mut data = Vec::new();
446        data.push(1); // version
447        data.push(0x08); // flags: bit 3 = link type present
448        data.push(99); // invalid link type
449        data.push(1); // name length = 1
450        data.push(b'x');
451        let err = LinkMessage::parse(&data, 8).unwrap_err();
452        assert_eq!(err, FormatError::InvalidLinkType(99));
453    }
454}