Skip to main content

canic_backup/topology/
mod.rs

1use candid::Principal;
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4
5///
6/// TopologyRecord
7///
8
9#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
10pub struct TopologyRecord {
11    pub pid: Principal,
12    pub parent_pid: Option<Principal>,
13    pub role: String,
14    pub module_hash: Option<String>,
15}
16
17///
18/// TopologyHash
19///
20
21#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
22pub struct TopologyHash {
23    pub algorithm: String,
24    pub input: String,
25    pub hash: String,
26}
27
28///
29/// TopologyHasher
30///
31
32pub struct TopologyHasher;
33
34impl TopologyHasher {
35    /// Compute the canonical SHA-256 topology hash for discovery invariants.
36    #[must_use]
37    pub fn hash(records: &[TopologyRecord]) -> TopologyHash {
38        let input = Self::canonical_input(records);
39        let hash = sha256_hex(input.as_bytes());
40
41        TopologyHash {
42            algorithm: "sha256".to_string(),
43            input: "sorted(pid,parent_pid,role,module_hash)".to_string(),
44            hash,
45        }
46    }
47
48    /// Build the stable canonical topology input used by `hash`.
49    #[must_use]
50    pub fn canonical_input(records: &[TopologyRecord]) -> String {
51        let mut rows = records.iter().map(canonical_row).collect::<Vec<_>>();
52        rows.sort();
53        rows.join("\n")
54    }
55}
56
57// Encode one topology record with explicit null markers for optional fields.
58fn canonical_row(record: &TopologyRecord) -> String {
59    format!(
60        "pid={}|parent_pid={}|role={}|module_hash={}",
61        record.pid,
62        optional_principal(record.parent_pid),
63        record.role,
64        optional_str(record.module_hash.as_deref())
65    )
66}
67
68// Encode optional principals with a stable null marker.
69fn optional_principal(value: Option<Principal>) -> String {
70    value.map_or_else(|| "null".to_string(), |pid| pid.to_string())
71}
72
73// Encode optional string fields with a stable null marker.
74fn optional_str(value: Option<&str>) -> &str {
75    value.unwrap_or("null")
76}
77
78// Compute lowercase hexadecimal SHA-256 without adding another dependency.
79fn sha256_hex(bytes: &[u8]) -> String {
80    let digest = Sha256::digest(bytes);
81    let mut out = String::with_capacity(digest.len() * 2);
82    for byte in digest {
83        out.push(hex_char(byte >> 4));
84        out.push(hex_char(byte & 0x0f));
85    }
86    out
87}
88
89// Convert one four-bit nibble to lowercase hexadecimal.
90const fn hex_char(nibble: u8) -> char {
91    match nibble {
92        0..=9 => (b'0' + nibble) as char,
93        10..=15 => (b'a' + (nibble - 10)) as char,
94        _ => unreachable!(),
95    }
96}
97
98#[cfg(test)]
99mod tests;