1use std::collections::BTreeMap;
4
5use crate::error::{err, member, ErrorCode, Result};
6use crate::json::{self, Value};
7use crate::keys::{key_doc, KeyDoc};
8use crate::lexical::limits;
9use crate::schema;
10
11const ENDPOINTS: [(&str, &[&str], bool); 11] = [
13 ("blob", &["sha256"], false),
14 ("cell", &["cell"], true),
15 ("cells", &[], false),
16 ("descriptor", &["hash"], true),
17 ("explain", &["cell"], false),
18 ("find", &[], false),
19 ("jobs", &["job"], true),
20 ("manifest", &["slice_id", "members_hash"], true),
21 ("receipt", &["hash"], true),
22 ("slice", &["cell"], false),
23 ("verify", &[], false),
24];
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28pub enum Retrieval<'a> {
29 Response {
31 status: u16,
33 body: Option<&'a [u8]>,
35 },
36 Failed,
39}
40
41#[derive(Clone, Debug, PartialEq, Eq, Hash)]
44#[non_exhaustive]
45pub struct MeshDoc {
46 pub host: String,
48 pub bindings: String,
50 pub endpoints: BTreeMap<String, String>,
53}
54
55impl MeshDoc {
56 pub fn from_json(host: &str, body: &[u8]) -> Result<MeshDoc> {
64 let v = parse_mesh(body)?;
65 let m = v.as_object().unwrap_or(&[]);
66 if schema::get(m, "host").as_str() != Some(host) {
67 return err(ErrorCode::MeshDoc, "$.host", "the mesh document names another host");
68 }
69 if schema::get(m, "proto").as_str() != Some("datumesh/1") {
70 return err(ErrorCode::MeshDoc, "$.proto", "proto must be datumesh/1");
71 }
72 mesh_members(host, m)
73 }
74
75 pub fn url(&self, name: &str, values: &[(&str, &str)]) -> Option<String> {
80 let template = self.endpoints.get(name)?;
81 let (_, placeholders, _) = ENDPOINTS.iter().find(|(n, _, _)| *n == name)?;
82 if values.len() != placeholders.len() || !placeholders.iter().all(|p| values.iter().any(|(k, _)| k == p)) {
83 return None;
84 }
85 let mut out = template.clone();
86 for (k, v) in values {
87 out = out.replacen(&format!("{{{k}}}"), v, 1);
88 }
89 Some(out)
90 }
91}
92
93#[derive(Clone, Debug, PartialEq, Eq, Hash)]
95#[non_exhaustive]
96pub struct ResolvedMesh {
97 pub keys: KeyDoc,
99 pub mesh: MeshDoc,
101}
102
103pub fn resolve_mesh(host: &str, key_doc_got: Retrieval<'_>, mesh_doc_got: Retrieval<'_>) -> Result<ResolvedMesh> {
119 let key_body = match key_doc_got {
121 Retrieval::Response { status: 200, body: Some(b) } => b,
122 Retrieval::Response { status: 200, body: None } => {
123 return err(ErrorCode::KeyDoc, "$", "a 200 response without a key document");
124 }
125 _ => return err(ErrorCode::KeyUnavailable, "$", "the key document could not be retrieved"),
126 };
127 let Retrieval::Response { status: 200, body: Some(mesh_body) } = mesh_doc_got else {
129 return err(ErrorCode::MeshDoc, "$", "the mesh document could not be retrieved");
130 };
131 let kv = json::parse(key_body).map_err(|e| e.recode(ErrorCode::KeyDoc))?;
133 let mv = parse_mesh(mesh_body)?;
134 let mm = mv.as_object().unwrap_or(&[]);
135 if schema::get(mm, "host").as_str() != Some(host) {
137 return err(ErrorCode::MeshDoc, "$.host", "the mesh document names another host than the one fetched");
138 }
139 if schema::get(mm, "proto").as_str() != Some("datumesh/1") {
140 return err(ErrorCode::MeshDoc, "$.proto", "the mesh document's proto is not datumesh/1");
141 }
142 if let Value::Object(km) = &kv {
143 if let Some(Value::String(h)) = schema::opt(km, "host") {
144 if h != host {
145 return err(ErrorCode::MeshDoc, "$.host", "the key document names another host");
146 }
147 }
148 if let Some(Value::String(p)) = schema::opt(km, "proto") {
150 if p != "datumesh/1" {
151 return err(ErrorCode::MeshDoc, "$.proto", "the key document names another protocol");
152 }
153 }
154 }
155 let keys = key_doc(host, &kv)?;
157 let mesh = mesh_members(host, mm)?;
159 Ok(ResolvedMesh { keys, mesh })
160}
161
162fn parse_mesh(body: &[u8]) -> Result<Value> {
163 let v = json::parse(body).map_err(|e| e.recode(ErrorCode::MeshDoc))?;
164 if !matches!(v, Value::Object(_)) {
165 return err(ErrorCode::MeshDoc, "$", "a mesh document is a JSON object");
166 }
167 Ok(v)
168}
169
170fn url_on_host(u: &str, host: &str) -> bool {
173 let Some(rest) = u.strip_prefix("https://") else { return false };
174 u.len() <= limits::LICENCE_URL
175 && rest.bytes().all(|c| (0x21..=0x7E).contains(&c))
176 && rest.strip_prefix(host).is_some_and(|path| path.starts_with('/'))
177}
178
179fn mesh_members(host: &str, m: &[(String, Value)]) -> Result<MeshDoc> {
180 let md = ErrorCode::MeshDoc;
181 let bindings = match schema::get(m, "bindings") {
182 Value::String(b) if url_on_host(b, host) => b.clone(),
183 _ => return err(md, "$.bindings", "bindings is an https URL on this host"),
184 };
185 let Value::Object(em) = schema::get(m, "endpoints") else {
186 return err(md, "$.endpoints", "endpoints is an object");
187 };
188 for (name, _, required) in ENDPOINTS {
189 if required && schema::opt(em, name).is_none() {
190 return err(md, "$.endpoints", format!("the required endpoint {name} is missing"));
191 }
192 }
193 let mut endpoints = BTreeMap::new();
194 for (name, placeholders, _) in ENDPOINTS {
195 let Some(v) = schema::opt(em, name) else { continue };
196 let p = member("$.endpoints", name);
197 let Value::String(t) = v else { return err(md, &p, "an endpoint is a URL template") };
198 if !url_on_host(t, host) {
199 return err(md, &p, "an endpoint is an https URL on this host");
200 }
201 let mut rest = t.clone();
202 for ph in placeholders {
203 let token = format!("{{{ph}}}");
204 if rest.matches(&token).count() != 1 {
205 return err(md, &p, format!("the template has {token} exactly once"));
206 }
207 rest = rest.replacen(&token, "", 1);
208 }
209 if rest.contains('{') || rest.contains('}') {
211 return err(md, &p, "every brace of the template belongs to one of its placeholders");
212 }
213 endpoints.insert(name.to_string(), t.clone());
214 }
215 Ok(MeshDoc { host: host.into(), bindings, endpoints })
216}