Skip to main content

canic_host/
registry.rs

1use serde_json::Value;
2use thiserror::Error as ThisError;
3
4///
5/// RegistryEntry
6///
7
8#[derive(Clone, Debug, Eq, PartialEq)]
9pub struct RegistryEntry {
10    pub pid: String,
11    pub role: Option<String>,
12    pub kind: Option<String>,
13    pub parent_pid: Option<String>,
14    pub module_hash: Option<String>,
15}
16
17///
18/// RegistryParseError
19///
20
21#[derive(Debug, ThisError)]
22pub enum RegistryParseError {
23    #[error("registry JSON must be an array or {{\"Ok\": [...]}}")]
24    InvalidRegistryJsonShape,
25
26    #[error(transparent)]
27    Json(#[from] serde_json::Error),
28}
29
30/// Parse the wrapped subnet registry JSON shape.
31pub fn parse_registry_entries(
32    registry_json: &str,
33) -> Result<Vec<RegistryEntry>, RegistryParseError> {
34    let data = serde_json::from_str::<Value>(registry_json)?;
35    let entries = data
36        .get("Ok")
37        .and_then(Value::as_array)
38        .or_else(|| data.as_array())
39        .ok_or(RegistryParseError::InvalidRegistryJsonShape)?;
40
41    Ok(entries.iter().filter_map(parse_registry_entry).collect())
42}
43
44// Parse one registry entry from registry JSON.
45fn parse_registry_entry(value: &Value) -> Option<RegistryEntry> {
46    let pid = value.get("pid").and_then(Value::as_str)?.to_string();
47    let role = value
48        .get("role")
49        .and_then(Value::as_str)
50        .map(str::to_string);
51    let parent_pid = value
52        .get("record")
53        .and_then(|record| record.get("parent_pid"))
54        .and_then(parse_optional_principal);
55    let kind = value
56        .get("kind")
57        .or_else(|| value.get("record").and_then(|record| record.get("kind")))
58        .and_then(Value::as_str)
59        .map(str::to_string);
60    let module_hash = value
61        .get("record")
62        .and_then(|record| record.get("module_hash"))
63        .and_then(parse_module_hash);
64
65    Some(RegistryEntry {
66        pid,
67        role,
68        kind,
69        parent_pid,
70        module_hash,
71    })
72}
73
74// Parse optional wasm module hash JSON emitted as bytes or text.
75fn parse_module_hash(value: &Value) -> Option<String> {
76    if value.is_null() {
77        return None;
78    }
79    if let Some(text) = value.as_str() {
80        return Some(text.to_string());
81    }
82    let bytes = value
83        .as_array()?
84        .iter()
85        .map(|item| {
86            let value = item.as_u64()?;
87            u8::try_from(value).ok()
88        })
89        .collect::<Option<Vec<_>>>()?;
90    Some(hex_bytes(&bytes))
91}
92
93// Parse optional principal JSON emitted as null, string, or optional vector form.
94fn parse_optional_principal(value: &Value) -> Option<String> {
95    if value.is_null() {
96        return None;
97    }
98    if let Some(text) = value.as_str() {
99        return Some(text.to_string());
100    }
101    value
102        .as_array()
103        .and_then(|items| items.first())
104        .and_then(Value::as_str)
105        .map(str::to_string)
106}
107
108fn hex_bytes(bytes: &[u8]) -> String {
109    const HEX: &[u8; 16] = b"0123456789abcdef";
110    let mut out = String::with_capacity(bytes.len() * 2);
111    for byte in bytes {
112        out.push(char::from(HEX[usize::from(byte >> 4)]));
113        out.push(char::from(HEX[usize::from(byte & 0x0f)]));
114    }
115    out
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    const ROOT_TEXT: &str = "aaaaa-aa";
123    const APP_TEXT: &str = "renrk-eyaaa-aaaaa-aaada-cai";
124    const WORKER_TEXT: &str = "rno2w-sqaaa-aaaaa-aaacq-cai";
125    const HASH: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
126
127    // Ensure registry parsing accepts the wrapped registry JSON shape.
128    #[test]
129    fn registry_entries_parse_wrapped_cli_json() {
130        let entries = parse_registry_entries(&registry_json()).expect("parse registry");
131
132        assert_eq!(
133            entries,
134            vec![
135                RegistryEntry {
136                    pid: ROOT_TEXT.to_string(),
137                    role: Some("root".to_string()),
138                    kind: Some("root".to_string()),
139                    parent_pid: None,
140                    module_hash: None,
141                },
142                RegistryEntry {
143                    pid: APP_TEXT.to_string(),
144                    role: Some("app".to_string()),
145                    kind: Some("singleton".to_string()),
146                    parent_pid: Some(ROOT_TEXT.to_string()),
147                    module_hash: Some("01ab".to_string()),
148                },
149                RegistryEntry {
150                    pid: WORKER_TEXT.to_string(),
151                    role: Some("worker".to_string()),
152                    kind: Some("replica".to_string()),
153                    parent_pid: Some(APP_TEXT.to_string()),
154                    module_hash: Some(HASH.to_string()),
155                },
156            ]
157        );
158    }
159
160    fn registry_json() -> String {
161        serde_json::json!({
162            "Ok": [
163                {
164                    "pid": ROOT_TEXT,
165                    "role": "root",
166                    "record": {
167                        "pid": ROOT_TEXT,
168                        "role": "root",
169                        "kind": "root",
170                        "parent_pid": null,
171                        "module_hash": null
172                    }
173                },
174                {
175                    "pid": APP_TEXT,
176                    "role": "app",
177                    "kind": "singleton",
178                    "record": {
179                        "pid": APP_TEXT,
180                        "role": "app",
181                        "parent_pid": [ROOT_TEXT],
182                        "module_hash": [1, 171]
183                    }
184                },
185                {
186                    "pid": WORKER_TEXT,
187                    "role": "worker",
188                    "kind": "replica",
189                    "record": {
190                        "pid": WORKER_TEXT,
191                        "role": "worker",
192                        "parent_pid": [APP_TEXT],
193                        "module_hash": HASH
194                    }
195                }
196            ]
197        })
198        .to_string()
199    }
200}