Skip to main content

pamoja_ros2/
typehash.rs

1//! Message type identity: the DDS type name and the RIHS01 type hash.
2//!
3//! Two peers only exchange a message if they agree on its type. ROS 2 pins that agreement two ways:
4//! a DDS type name derived from the interface (`std_msgs/msg/String` becomes
5//! `std_msgs::msg::dds_::String_`), and a structural type hash (REP-2011's RIHS01, a SHA-256 over
6//! the type's description, written `RIHS01_` followed by 64 hex digits). This module derives the
7//! type name and parses and formats the hash. Computing the hash from a type description is part of
8//! the live bridge, where it is checked against the value `rosidl` emits.
9
10use alloc::format;
11use alloc::string::String;
12use core::fmt;
13
14/// The length in bytes of a RIHS01 hash (a SHA-256 digest).
15const HASH_LEN: usize = 32;
16
17/// A parsed ROS 2 type hash in the RIHS01 scheme (REP-2011).
18///
19/// The string form is `RIHS01_` followed by 64 lowercase hex digits, the version prefix plus a
20/// SHA-256 digest of the type's description.
21///
22/// # Examples
23///
24/// ```
25/// use pamoja_ros2::typehash::TypeHash;
26///
27/// // The published hash of std_msgs/msg/String round-trips through parse and display.
28/// let text = "RIHS01_df668c740482bbd48fb39d76a70dfd4bd59db1288021743503259e948f6b1a18";
29/// let hash = TypeHash::parse(text).unwrap();
30/// assert_eq!(hash.to_string(), text);
31/// ```
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub struct TypeHash {
34    digest: [u8; HASH_LEN],
35}
36
37impl TypeHash {
38    /// Parses a RIHS01 hash string.
39    ///
40    /// # Arguments
41    ///
42    /// * `text` - the candidate hash, expected as `RIHS01_` plus 64 lowercase hex digits.
43    ///
44    /// # Returns
45    ///
46    /// `Some(hash)` if `text` is a well-formed RIHS01 string, otherwise `None`.
47    pub fn parse(text: &str) -> Option<Self> {
48        let hex = text.strip_prefix("RIHS01_")?;
49        if hex.len() != HASH_LEN * 2 {
50            return None;
51        }
52        let bytes = hex.as_bytes();
53        let mut digest = [0u8; HASH_LEN];
54        let mut i = 0;
55        while i < HASH_LEN {
56            let hi = hex_value(bytes[i * 2])?;
57            let lo = hex_value(bytes[i * 2 + 1])?;
58            digest[i] = (hi << 4) | lo;
59            i += 1;
60        }
61        Some(Self { digest })
62    }
63
64    /// Returns the raw 32-byte digest.
65    ///
66    /// # Returns
67    ///
68    /// The SHA-256 digest carried by the hash.
69    pub fn digest(&self) -> [u8; HASH_LEN] {
70        self.digest
71    }
72}
73
74impl fmt::Display for TypeHash {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        f.write_str("RIHS01_")?;
77        for byte in self.digest {
78            write!(f, "{byte:02x}")?;
79        }
80        Ok(())
81    }
82}
83
84// Returns the value of a single lowercase-or-digit hex character, or `None` if it is not hex.
85fn hex_value(c: u8) -> Option<u8> {
86    match c {
87        b'0'..=b'9' => Some(c - b'0'),
88        b'a'..=b'f' => Some(c - b'a' + 10),
89        _ => None,
90    }
91}
92
93/// Derives the DDS type name from a ROS 2 interface type.
94///
95/// # Arguments
96///
97/// * `ros_type` - the interface type as `package/namespace/Type`, for example `std_msgs/msg/String`.
98///
99/// # Returns
100///
101/// `Some(dds_name)` such as `std_msgs::msg::dds_::String_`, joining the parts with `::`, inserting
102/// the `dds_` namespace, and suffixing the type with `_`; `None` if `ros_type` is not three
103/// non-empty `/`-separated parts.
104///
105/// # Examples
106///
107/// ```
108/// use pamoja_ros2::typehash::dds_type_name;
109///
110/// assert_eq!(dds_type_name("std_msgs/msg/String").as_deref(), Some("std_msgs::msg::dds_::String_"));
111/// assert_eq!(
112///     dds_type_name("example_interfaces/srv/AddTwoInts").as_deref(),
113///     Some("example_interfaces::srv::dds_::AddTwoInts_"),
114/// );
115/// assert_eq!(dds_type_name("std_msgs/String"), None); // missing the namespace part
116/// ```
117pub fn dds_type_name(ros_type: &str) -> Option<String> {
118    let mut parts = ros_type.split('/');
119    let package = parts.next().filter(|p| !p.is_empty())?;
120    let namespace = parts.next().filter(|p| !p.is_empty())?;
121    let type_name = parts.next().filter(|p| !p.is_empty())?;
122    if parts.next().is_some() {
123        return None;
124    }
125    Some(format!("{package}::{namespace}::dds_::{type_name}_"))
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn parses_and_formats_the_published_hash() {
134        let text = "RIHS01_df668c740482bbd48fb39d76a70dfd4bd59db1288021743503259e948f6b1a18";
135        let hash = TypeHash::parse(text).unwrap();
136        assert_eq!(hash.to_string(), text);
137        assert_eq!(hash.digest()[0], 0xdf);
138        assert_eq!(hash.digest()[31], 0x18);
139    }
140
141    #[test]
142    fn rejects_malformed_hashes() {
143        assert!(TypeHash::parse("RIHS01_tooshort").is_none());
144        assert!(TypeHash::parse("df668c74").is_none()); // missing prefix
145        assert!(TypeHash::parse(
146            "RIHS02_df668c740482bbd48fb39d76a70dfd4bd59db1288021743503259e948f6b1a18"
147        )
148        .is_none());
149        // Uppercase hex is not the canonical lowercase form.
150        assert!(TypeHash::parse(
151            "RIHS01_DF668C740482BBD48FB39D76A70DFD4BD59DB1288021743503259E948F6B1A18"
152        )
153        .is_none());
154    }
155
156    #[test]
157    fn derives_dds_type_names() {
158        assert_eq!(
159            dds_type_name("std_msgs/msg/String").as_deref(),
160            Some("std_msgs::msg::dds_::String_"),
161        );
162        assert_eq!(
163            dds_type_name("geometry_msgs/msg/Twist").as_deref(),
164            Some("geometry_msgs::msg::dds_::Twist_"),
165        );
166        assert_eq!(
167            dds_type_name("example_interfaces/srv/AddTwoInts").as_deref(),
168            Some("example_interfaces::srv::dds_::AddTwoInts_"),
169        );
170        assert_eq!(dds_type_name("std_msgs/String"), None);
171        assert_eq!(dds_type_name("a/b/c/d"), None);
172    }
173}