1use serde_json::Value;
2use thiserror::Error as ThisError;
3
4#[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#[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
30pub 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
44fn 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
74fn 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
93fn 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 #[test]
129 fn registry_entries_parse_wrapped_cli_json() {
130 let entries = parse_registry_entries(®istry_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}