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); 12] = [
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 ("versions", &["cell"], false),
25];
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
29pub enum Retrieval<'a> {
30 Response {
32 status: u16,
34 body: Option<&'a [u8]>,
36 },
37 Failed,
40}
41
42#[derive(Clone, Debug, PartialEq, Eq, Hash)]
45#[non_exhaustive]
46pub struct MeshDoc {
47 pub host: String,
49 pub bindings: String,
51 pub endpoints: BTreeMap<String, String>,
54}
55
56impl MeshDoc {
57 pub fn from_json(host: &str, body: &[u8]) -> Result<MeshDoc> {
65 let v = parse_mesh(body)?;
66 let m = v.as_object().unwrap_or(&[]);
67 if schema::get(m, "host").as_str() != Some(host) {
68 return err(ErrorCode::MeshDoc, "$.host", "the mesh document names another host");
69 }
70 if schema::get(m, "proto").as_str() != Some("datumesh/1") {
71 return err(ErrorCode::MeshDoc, "$.proto", "proto must be datumesh/1");
72 }
73 mesh_members(host, m)
74 }
75
76 pub fn url(&self, name: &str, values: &[(&str, &str)]) -> Option<String> {
81 let template = self.endpoints.get(name)?;
82 let (_, placeholders, _) = ENDPOINTS.iter().find(|(n, _, _)| *n == name)?;
83 if values.len() != placeholders.len() || !placeholders.iter().all(|p| values.iter().any(|(k, _)| k == p)) {
84 return None;
85 }
86 let mut out = template.clone();
87 for (k, v) in values {
88 out = out.replacen(&format!("{{{k}}}"), v, 1);
89 }
90 Some(out)
91 }
92}
93
94#[derive(Clone, Debug, PartialEq, Eq, Hash)]
96#[non_exhaustive]
97pub struct ResolvedMesh {
98 pub keys: KeyDoc,
100 pub mesh: MeshDoc,
102}
103
104pub fn resolve_mesh(host: &str, key_doc_got: Retrieval<'_>, mesh_doc_got: Retrieval<'_>) -> Result<ResolvedMesh> {
120 let key_body = match key_doc_got {
122 Retrieval::Response { status: 200, body: Some(b) } => b,
123 Retrieval::Response { status: 200, body: None } => {
124 return err(ErrorCode::KeyDoc, "$", "a 200 response without a key document");
125 }
126 _ => return err(ErrorCode::KeyUnavailable, "$", "the key document could not be retrieved"),
127 };
128 let Retrieval::Response { status: 200, body: Some(mesh_body) } = mesh_doc_got else {
130 return err(ErrorCode::MeshDoc, "$", "the mesh document could not be retrieved");
131 };
132 let kv = json::parse(key_body).map_err(|e| e.recode(ErrorCode::KeyDoc))?;
134 let mv = parse_mesh(mesh_body)?;
135 let mm = mv.as_object().unwrap_or(&[]);
136 if schema::get(mm, "host").as_str() != Some(host) {
138 return err(ErrorCode::MeshDoc, "$.host", "the mesh document names another host than the one fetched");
139 }
140 if schema::get(mm, "proto").as_str() != Some("datumesh/1") {
141 return err(ErrorCode::MeshDoc, "$.proto", "the mesh document's proto is not datumesh/1");
142 }
143 if let Value::Object(km) = &kv {
144 if let Some(Value::String(h)) = schema::opt(km, "host") {
145 if h != host {
146 return err(ErrorCode::MeshDoc, "$.host", "the key document names another host");
147 }
148 }
149 if let Some(Value::String(p)) = schema::opt(km, "proto") {
151 if p != "datumesh/1" {
152 return err(ErrorCode::MeshDoc, "$.proto", "the key document names another protocol");
153 }
154 }
155 }
156 let keys = key_doc(host, &kv)?;
158 let mesh = mesh_members(host, mm)?;
160 Ok(ResolvedMesh { keys, mesh })
161}
162
163fn parse_mesh(body: &[u8]) -> Result<Value> {
164 let v = json::parse(body).map_err(|e| e.recode(ErrorCode::MeshDoc))?;
165 if !matches!(v, Value::Object(_)) {
166 return err(ErrorCode::MeshDoc, "$", "a mesh document is a JSON object");
167 }
168 Ok(v)
169}
170
171fn url_on_host(u: &str, host: &str) -> bool {
174 let Some(rest) = u.strip_prefix("https://") else { return false };
175 u.len() <= limits::LICENCE_URL
176 && rest.bytes().all(|c| (0x21..=0x7E).contains(&c))
177 && rest.strip_prefix(host).is_some_and(|path| path.starts_with('/'))
178}
179
180fn mesh_members(host: &str, m: &[(String, Value)]) -> Result<MeshDoc> {
181 let md = ErrorCode::MeshDoc;
182 let bindings = match schema::get(m, "bindings") {
183 Value::String(b) if url_on_host(b, host) => b.clone(),
184 _ => return err(md, "$.bindings", "bindings is an https URL on this host"),
185 };
186 let Value::Object(em) = schema::get(m, "endpoints") else {
187 return err(md, "$.endpoints", "endpoints is an object");
188 };
189 for (name, _, required) in ENDPOINTS {
190 if required && schema::opt(em, name).is_none() {
191 return err(md, "$.endpoints", format!("the required endpoint {name} is missing"));
192 }
193 }
194 let mut endpoints = BTreeMap::new();
195 for (name, placeholders, _) in ENDPOINTS {
196 let Some(v) = schema::opt(em, name) else { continue };
197 let p = member("$.endpoints", name);
198 let Value::String(t) = v else { return err(md, &p, "an endpoint is a URL template") };
199 if !url_on_host(t, host) {
200 return err(md, &p, "an endpoint is an https URL on this host");
201 }
202 let mut rest = t.clone();
203 for ph in placeholders {
204 let token = format!("{{{ph}}}");
205 if rest.matches(&token).count() != 1 {
206 return err(md, &p, format!("the template has {token} exactly once"));
207 }
208 rest = rest.replacen(&token, "", 1);
209 }
210 if rest.contains('{') || rest.contains('}') {
212 return err(md, &p, "every brace of the template belongs to one of its placeholders");
213 }
214 endpoints.insert(name.to_string(), t.clone());
215 }
216 Ok(MeshDoc { host: host.into(), bindings, endpoints })
217}