Skip to main content

ros2_types/
hash.rs

1//! RIHS01 hash calculation
2//!
3//! Implements the ROS Interface Hashing Standard version 1
4
5use crate::{Result, types::TypeDescriptionMsg};
6use sha2::{Digest, Sha256};
7
8/// RIHS01 version prefix
9const RIHS01_PREFIX: &str = "RIHS01_";
10
11/// Calculate the RIHS01 type hash for a type description
12///
13/// This function implements the ROS Interface Hashing Standard version 1,
14/// which creates a canonical JSON representation of the type and hashes it
15/// with SHA256.
16///
17/// The hash format is: `RIHS01_<64_character_hex_sha256>`
18///
19/// Implementation details matching rosidl_generator_type_description:
20/// 1. default_value fields are removed from all field descriptions
21/// 2. JSON uses Python-style separators: ', ' and ': ' (space after comma and colon)
22/// 3. Keys are not alphabetically sorted (preserves insertion order)
23/// 4. Top-level key order: "type_description" first, then "referenced_type_descriptions"
24/// 5. Referenced type descriptions are sorted alphabetically by type_name
25///
26/// # Arguments
27///
28/// * `type_description` - The complete type description including referenced types
29///
30/// # Errors
31///
32/// Returns an error if JSON serialization fails
33pub fn calculate_type_hash(type_description: &TypeDescriptionMsg) -> Result<String> {
34    // Create canonical JSON representation matching rosidl format
35    // Per rosidl_generator_type_description/__init__.py:calculate_type_hash():
36    // 1. Remove default_value fields from all fields
37    // 2. Use separators=(', ', ': ') - note the space after comma and colon
38    // 3. sort_keys=False - Python 3.7+ dicts preserve insertion order
39    // 4. Key order: "type_description" FIRST, then "referenced_type_descriptions"
40
41    // Build JSON manually to control exact key ordering
42    fn escape_json_string(s: &str) -> String {
43        // Simple JSON string escaping
44        s.replace('\\', "\\\\")
45            .replace('"', "\\\"")
46            .replace('\n', "\\n")
47            .replace('\r', "\\r")
48            .replace('\t', "\\t")
49    }
50
51    fn field_to_json(field: &crate::types::Field) -> String {
52        format!(
53            r#"{{"name": "{}", "type": {{"type_id": {}, "capacity": {}, "string_capacity": {}, "nested_type_name": "{}"}}}}"#,
54            escape_json_string(&field.name),
55            field.field_type.type_id,
56            field.field_type.capacity,
57            field.field_type.string_capacity,
58            escape_json_string(&field.field_type.nested_type_name)
59        )
60    }
61
62    fn type_desc_to_json(td: &crate::types::IndividualTypeDescription) -> String {
63        let fields_json: Vec<String> = td.fields.iter().map(field_to_json).collect();
64        format!(
65            r#"{{"type_name": "{}", "fields": [{}]}}"#,
66            escape_json_string(&td.type_name),
67            fields_json.join(", ")
68        )
69    }
70
71    // Build the complete JSON structure with exact key ordering
72    let type_desc_json = type_desc_to_json(&type_description.type_description);
73
74    // Sort referenced type descriptions by type_name (as rosidl does)
75    let mut sorted_refs = type_description.referenced_type_descriptions.clone();
76    sorted_refs.sort_by(|a, b| a.type_name.cmp(&b.type_name));
77
78    let ref_types_json: Vec<String> = sorted_refs.iter().map(type_desc_to_json).collect();
79
80    let hashable_repr = format!(
81        r#"{{"type_description": {}, "referenced_type_descriptions": [{}]}}"#,
82        type_desc_json,
83        ref_types_json.join(", ")
84    );
85
86    // Calculate SHA256 hash
87    let mut hasher = Sha256::new();
88    hasher.update(hashable_repr.as_bytes());
89    let hash_result = hasher.finalize();
90
91    // Format as RIHS01 hash string
92    let hash_hex = format!("{:x}", hash_result);
93    Ok(format!("{}{}", RIHS01_PREFIX, hash_hex))
94}
95
96/// Parse a RIHS hash string and extract version and hash value
97///
98/// # Arguments
99///
100/// * `rihs_str` - RIHS formatted string (e.g., "RIHS01_abc123...")
101///
102/// # Returns
103///
104/// Returns `(version, hash_value)` tuple
105///
106/// # Errors
107///
108/// Returns an error if the string is not in valid RIHS format
109pub fn parse_rihs_string(rihs_str: &str) -> Result<(u32, String)> {
110    if !rihs_str.starts_with("RIHS") {
111        return Err(crate::error::InvalidRihsFormat::MissingPrefix.into());
112    }
113
114    let parts: Vec<&str> = rihs_str.split('_').collect();
115    if parts.len() != 2 {
116        return Err(crate::error::InvalidRihsFormat::InvalidStructure.into());
117    }
118
119    let version_str = parts[0]
120        .strip_prefix("RIHS")
121        .ok_or(crate::error::InvalidRihsFormat::VersionExtractionFailed)?;
122
123    let version = version_str.parse::<u32>().map_err(|_| {
124        crate::error::InvalidRihsFormat::InvalidVersionNumber {
125            version_str: version_str.to_string(),
126        }
127    })?;
128
129    let hash_value = parts[1].to_string();
130
131    Ok((version, hash_value))
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::types::{Field, FieldType, IndividualTypeDescription};
138
139    #[test]
140    fn test_parse_rihs_string() {
141        let rihs = "RIHS01_abc123def456";
142        let (version, hash) = parse_rihs_string(rihs).unwrap();
143        assert_eq!(version, 1);
144        assert_eq!(hash, "abc123def456");
145    }
146
147    #[test]
148    fn test_parse_rihs_invalid() {
149        assert!(parse_rihs_string("invalid").is_err());
150        assert!(parse_rihs_string("RIHS_nope").is_err());
151        assert!(parse_rihs_string("RIHS01").is_err());
152    }
153
154    #[test]
155    fn test_calculate_hash_format() {
156        let type_desc = IndividualTypeDescription::new(
157            "test_pkg/msg/TestMsg",
158            vec![Field::new(
159                "field1",
160                FieldType::primitive(crate::types::FIELD_TYPE_INT32),
161            )],
162        );
163
164        let msg = TypeDescriptionMsg::new(type_desc, vec![]);
165        let hash = calculate_type_hash(&msg).unwrap();
166
167        assert!(hash.starts_with(RIHS01_PREFIX));
168        assert_eq!(hash.len(), RIHS01_PREFIX.len() + 64); // SHA256 = 64 hex chars
169    }
170}