canic_backup/topology/
mod.rs1use candid::Principal;
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4
5#[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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
22pub struct TopologyHash {
23 pub algorithm: String,
24 pub input: String,
25 pub hash: String,
26}
27
28pub struct TopologyHasher;
33
34impl TopologyHasher {
35 #[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 #[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
57fn 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
68fn optional_principal(value: Option<Principal>) -> String {
70 value.map_or_else(|| "null".to_string(), |pid| pid.to_string())
71}
72
73fn optional_str(value: Option<&str>) -> &str {
75 value.unwrap_or("null")
76}
77
78fn 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
89const 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;