canic_host/registry/
mod.rs1use candid::{CandidType, Decode, Principal};
2use serde::Deserialize;
3use serde_json::Value;
4use thiserror::Error as ThisError;
5
6#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct RegistryEntry {
12 pub pid: String,
13 pub role: Option<String>,
14 pub kind: Option<String>,
15 pub parent_pid: Option<String>,
16 pub module_hash: Option<String>,
17}
18
19#[derive(Debug, ThisError)]
24pub enum RegistryParseError {
25 #[error("registry JSON must be an array, {{\"Ok\": [...]}}, or ICP response_bytes envelope")]
26 InvalidRegistryJsonShape,
27
28 #[error("registry response_bytes was not valid hex")]
29 InvalidResponseBytes,
30
31 #[error("registry response rejected: {0}")]
32 Rejected(String),
33
34 #[error("could not decode registry response_bytes: {0}")]
35 Candid(String),
36
37 #[error(transparent)]
38 Json(#[from] serde_json::Error),
39}
40
41pub fn parse_registry_entries(
43 registry_json: &str,
44) -> Result<Vec<RegistryEntry>, RegistryParseError> {
45 let data = serde_json::from_str::<Value>(registry_json)?;
46 if let Some(entries) = parse_registry_entries_json(&data) {
47 return Ok(entries);
48 }
49 if let Some(entries) = parse_registry_entries_response_bytes(&data)? {
50 return Ok(entries);
51 }
52
53 Err(RegistryParseError::InvalidRegistryJsonShape)
54}
55
56fn parse_registry_entries_json(data: &Value) -> Option<Vec<RegistryEntry>> {
57 let entries = data
58 .get("Ok")
59 .and_then(Value::as_array)
60 .or_else(|| data.as_array())?;
61
62 Some(entries.iter().filter_map(parse_registry_entry).collect())
63}
64
65fn parse_registry_entries_response_bytes(
66 data: &Value,
67) -> Result<Option<Vec<RegistryEntry>>, RegistryParseError> {
68 let Some(bytes) = data.get("response_bytes").and_then(Value::as_str) else {
69 return Ok(None);
70 };
71 let bytes = hex_to_bytes(bytes).ok_or(RegistryParseError::InvalidResponseBytes)?;
72 let response = Decode!(
73 &bytes,
74 Result<SubnetRegistryResponseWire, CanicErrorWire>
75 )
76 .map_err(|err| RegistryParseError::Candid(err.to_string()))?;
77 let response = response.map_err(|err| RegistryParseError::Rejected(err.message))?;
78 Ok(Some(response.to_registry_entries()))
79}
80
81fn parse_registry_entry(value: &Value) -> Option<RegistryEntry> {
83 let pid = value.get("pid").and_then(Value::as_str)?.to_string();
84 let role = value
85 .get("role")
86 .and_then(Value::as_str)
87 .map(str::to_string);
88 let parent_pid = value
89 .get("record")
90 .and_then(|record| record.get("parent_pid"))
91 .and_then(parse_optional_principal);
92 let kind = value
93 .get("kind")
94 .or_else(|| value.get("record").and_then(|record| record.get("kind")))
95 .and_then(Value::as_str)
96 .map(str::to_string);
97 let module_hash = value
98 .get("record")
99 .and_then(|record| record.get("module_hash"))
100 .and_then(parse_module_hash);
101
102 Some(RegistryEntry {
103 pid,
104 role,
105 kind,
106 parent_pid,
107 module_hash,
108 })
109}
110
111fn parse_module_hash(value: &Value) -> Option<String> {
113 if value.is_null() {
114 return None;
115 }
116 if let Some(text) = value.as_str() {
117 return Some(text.to_string());
118 }
119 let bytes = value
120 .as_array()?
121 .iter()
122 .map(|item| {
123 let value = item.as_u64()?;
124 u8::try_from(value).ok()
125 })
126 .collect::<Option<Vec<_>>>()?;
127 Some(hex_bytes(&bytes))
128}
129
130fn parse_optional_principal(value: &Value) -> Option<String> {
132 if value.is_null() {
133 return None;
134 }
135 if let Some(text) = value.as_str() {
136 return Some(text.to_string());
137 }
138 value
139 .as_array()
140 .and_then(|items| items.first())
141 .and_then(Value::as_str)
142 .map(str::to_string)
143}
144
145fn hex_bytes(bytes: &[u8]) -> String {
146 const HEX: &[u8; 16] = b"0123456789abcdef";
147 let mut out = String::with_capacity(bytes.len() * 2);
148 for byte in bytes {
149 out.push(char::from(HEX[usize::from(byte >> 4)]));
150 out.push(char::from(HEX[usize::from(byte & 0x0f)]));
151 }
152 out
153}
154
155fn hex_to_bytes(text: &str) -> Option<Vec<u8>> {
156 if !text.len().is_multiple_of(2) {
157 return None;
158 }
159 text.as_bytes()
160 .chunks_exact(2)
161 .map(|pair| {
162 let high = hex_value(pair[0])?;
163 let low = hex_value(pair[1])?;
164 Some((high << 4) | low)
165 })
166 .collect()
167}
168
169const fn hex_value(byte: u8) -> Option<u8> {
170 match byte {
171 b'0'..=b'9' => Some(byte - b'0'),
172 b'a'..=b'f' => Some(byte - b'a' + 10),
173 b'A'..=b'F' => Some(byte - b'A' + 10),
174 _ => None,
175 }
176}
177
178#[derive(CandidType, Deserialize)]
183struct SubnetRegistryResponseWire(Vec<SubnetRegistryEntryWire>);
184
185impl SubnetRegistryResponseWire {
186 fn to_registry_entries(&self) -> Vec<RegistryEntry> {
187 self.0
188 .iter()
189 .map(SubnetRegistryEntryWire::to_registry_entry)
190 .collect()
191 }
192}
193
194#[derive(CandidType, Deserialize)]
199struct SubnetRegistryEntryWire {
200 pid: Principal,
201 role: String,
202 record: CanisterInfoWire,
203}
204
205impl SubnetRegistryEntryWire {
206 fn to_registry_entry(&self) -> RegistryEntry {
207 let pid = self.pid.to_text();
208 let record_pid = self.record.pid.to_text();
209 debug_assert_eq!(record_pid, pid);
210 let role = if self.role.is_empty() {
211 self.record.role.clone()
212 } else {
213 self.role.clone()
214 };
215 RegistryEntry {
216 pid,
217 role: Some(role),
218 kind: None,
219 parent_pid: self.record.parent_pid.as_ref().map(Principal::to_text),
220 module_hash: self.record.module_hash.as_deref().map(hex_bytes),
221 }
222 }
223}
224
225#[derive(CandidType, Deserialize)]
230struct CanisterInfoWire {
231 pid: Principal,
232 role: String,
233 parent_pid: Option<Principal>,
234 module_hash: Option<Vec<u8>>,
235}
236
237#[derive(CandidType, Deserialize)]
242struct CanicErrorWire {
243 message: String,
244}
245
246#[cfg(test)]
247mod tests;