canic_backup/topology/
mod.rs1use crate::hash::sha256_hex;
2use candid::Principal;
3use serde::{Deserialize, Serialize};
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
78#[cfg(test)]
79mod tests;