Skip to main content

canic_host/registry/
mod.rs

1use candid::{CandidType, Decode, Principal};
2use serde::Deserialize;
3use serde_json::Value;
4use thiserror::Error as ThisError;
5
6///
7/// RegistryEntry
8///
9
10#[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///
20/// RegistryParseError
21///
22
23#[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
41/// Parse the wrapped subnet registry JSON shape.
42pub 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
81// Parse one registry entry from registry JSON.
82fn 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
111// Parse optional wasm module hash JSON emitted as bytes or text.
112fn 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
130// Parse optional principal JSON emitted as null, string, or optional vector form.
131fn 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///
179/// SubnetRegistryResponseWire
180///
181
182#[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///
195/// SubnetRegistryEntryWire
196///
197
198#[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///
226/// CanisterInfoWire
227///
228
229#[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///
238/// CanicErrorWire
239///
240
241#[derive(CandidType, Deserialize)]
242struct CanicErrorWire {
243    message: String,
244}
245
246#[cfg(test)]
247mod tests;