Skip to main content

hub_codegen/
hash.rs

1use sha2::{Digest, Sha256};
2use std::collections::HashMap;
3
4/// Compute SHA-256 hash of content, returning first 16 hex characters
5/// This matches Plexus hash format (16-char hex string)
6pub fn compute_hash(content: &str) -> String {
7    let mut hasher = Sha256::new();
8    hasher.update(content.as_bytes());
9    let result = hasher.finalize();
10    // Convert to hex and take first 16 characters (64 bits) like Plexus
11    format!("{:x}", result)[..16].to_string()
12}
13
14/// Compute hash of IR fragment
15/// Takes serialized IR content and returns deterministic hash
16pub fn compute_ir_hash(ir_content: &str) -> String {
17    compute_hash(ir_content)
18}
19
20/// Compute hash of a single generated file
21pub fn compute_file_hash(file_content: &str) -> String {
22    compute_hash(file_content)
23}
24
25/// Compute composite hash of all files in a plugin
26/// Files are sorted by name for deterministic hashing
27pub fn compute_plugin_hash(files: &HashMap<String, String>) -> String {
28    let mut content = String::new();
29
30    // Sort file names for deterministic ordering
31    let mut file_names: Vec<_> = files.keys().collect();
32    file_names.sort();
33
34    // Hash each file with its path as prefix
35    for name in file_names {
36        let file_hash = compute_file_hash(&files[name]);
37        content.push_str(&format!("{}:{}\n", name, file_hash));
38    }
39
40    compute_hash(&content)
41}
42
43/// Compute hash map of individual file hashes
44/// Returns map of filename -> hash for each file
45pub fn compute_file_hashes(files: &HashMap<String, String>) -> HashMap<String, String> {
46    files
47        .iter()
48        .map(|(name, content)| (name.clone(), compute_file_hash(content)))
49        .collect()
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn test_compute_hash_deterministic() {
58        let content = "hello world";
59        let hash1 = compute_hash(content);
60        let hash2 = compute_hash(content);
61        assert_eq!(hash1, hash2, "Hash should be deterministic");
62    }
63
64    #[test]
65    fn test_compute_hash_length() {
66        let hash = compute_hash("test");
67        assert_eq!(hash.len(), 16, "Hash should be 16 characters");
68    }
69
70    #[test]
71    fn test_compute_hash_different_inputs() {
72        let hash1 = compute_hash("foo");
73        let hash2 = compute_hash("bar");
74        assert_ne!(hash1, hash2, "Different inputs should produce different hashes");
75    }
76
77    #[test]
78    fn test_compute_plugin_hash_sorted() {
79        let mut files1 = HashMap::new();
80        files1.insert("a.ts".to_string(), "content a".to_string());
81        files1.insert("b.ts".to_string(), "content b".to_string());
82
83        let mut files2 = HashMap::new();
84        files2.insert("b.ts".to_string(), "content b".to_string());
85        files2.insert("a.ts".to_string(), "content a".to_string());
86
87        let hash1 = compute_plugin_hash(&files1);
88        let hash2 = compute_plugin_hash(&files2);
89
90        assert_eq!(
91            hash1, hash2,
92            "Plugin hash should be same regardless of insertion order"
93        );
94    }
95
96    #[test]
97    fn test_compute_file_hashes() {
98        let mut files = HashMap::new();
99        files.insert("types.ts".to_string(), "type Foo = string;".to_string());
100        files.insert("methods.ts".to_string(), "export function bar() {}".to_string());
101
102        let hashes = compute_file_hashes(&files);
103
104        assert_eq!(hashes.len(), 2);
105        assert!(hashes.contains_key("types.ts"));
106        assert!(hashes.contains_key("methods.ts"));
107        assert_eq!(hashes["types.ts"].len(), 16);
108        assert_eq!(hashes["methods.ts"].len(), 16);
109    }
110}