canic_backup/topology/
mod.rs1#[cfg(test)]
8mod tests;
9
10use crate::hash::sha256_hex;
11
12use candid::Principal;
13use serde::{Deserialize, Serialize};
14
15#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
23pub struct TopologyRecord {
24 pub pid: Principal,
25 pub parent_pid: Option<Principal>,
26 pub role: String,
27 pub module_hash: Option<String>,
28}
29
30#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
38pub struct TopologyHash {
39 pub algorithm: String,
40 pub input: String,
41 pub hash: String,
42}
43
44pub struct TopologyHasher;
52
53impl TopologyHasher {
54 #[must_use]
56 pub fn hash(records: &[TopologyRecord]) -> TopologyHash {
57 let input = Self::canonical_input(records);
58 let hash = sha256_hex(input.as_bytes());
59
60 TopologyHash {
61 algorithm: "sha256".to_string(),
62 input: "sorted(pid,parent_pid,role,module_hash)".to_string(),
63 hash,
64 }
65 }
66
67 #[must_use]
69 pub fn canonical_input(records: &[TopologyRecord]) -> String {
70 let mut rows = records.iter().map(canonical_row).collect::<Vec<_>>();
71 rows.sort();
72 rows.join("\n")
73 }
74}
75
76fn canonical_row(record: &TopologyRecord) -> String {
78 format!(
79 "pid={}|parent_pid={}|role={}|module_hash={}",
80 record.pid,
81 optional_principal(record.parent_pid),
82 record.role,
83 optional_str(record.module_hash.as_deref())
84 )
85}
86
87fn optional_principal(value: Option<Principal>) -> String {
89 value.map_or_else(|| "null".to_string(), |pid| pid.to_string())
90}
91
92fn optional_str(value: Option<&str>) -> &str {
94 value.unwrap_or("null")
95}