Skip to main content

datumesh_core/
mesh.rs

1//! Mesh documents (9.6) and cross-mesh resolution, `spec-0.4.1`. A verifier never needs them.
2
3use 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::licence::Declarations;
10use crate::schema;
11
12/// The endpoints of 9.6: name, placeholders, whether required.
13const ENDPOINTS: [(&str, &[&str], bool); 12] = [
14    ("blob", &["sha256"], false),
15    ("cell", &["cell"], true),
16    ("cells", &[], false),
17    ("descriptor", &["hash"], true),
18    ("explain", &["cell"], false),
19    ("find", &[], false),
20    ("jobs", &["job"], true),
21    ("manifest", &["slice_id", "members_hash"], true),
22    ("receipt", &["hash"], true),
23    ("slice", &["cell"], false),
24    ("verify", &[], false),
25    ("versions", &["cell"], false),
26];
27
28/// What one HTTPS `GET` of a document gave, as the caller's HTTP client saw it.
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub enum Retrieval<'a> {
31    /// A response: its status and body (`None` when there was none).
32    Response {
33        /// The integer HTTP status.
34        status: u16,
35        /// The body.
36        body: Option<&'a [u8]>,
37    },
38    /// No response: a network, DNS, TLS or timeout failure, a redirect, or a retrieval the
39    /// client abandoned.
40    Failed,
41}
42
43/// A checked mesh document (9.6): the host's bindings URL and its known endpoint templates.
44/// Unknown members are dropped.
45#[derive(Clone, Debug, PartialEq, Eq, Hash)]
46#[non_exhaustive]
47pub struct MeshDoc {
48    /// The host it was fetched from (and names).
49    pub host: String,
50    /// The URL of the bindings resource (9.7).
51    pub bindings: String,
52    /// The known endpoints present, by name (`cell`, `receipt`, `jobs`, ...): URL templates
53    /// with their placeholders, such as `https://<host>/cell/{cell}`.
54    pub endpoints: BTreeMap<String, String>,
55}
56
57impl MeshDoc {
58    /// Parses and checks a mesh document fetched from `host`, on its own (without the key
59    /// document): strict JSON, an object, `host` equal to `host`, `proto` `"datumesh/1"`, then
60    /// `bindings`, `endpoints` (an object holding every required endpoint) and each known
61    /// endpoint in ascending name order (9.6): on this host (beginning `https://<host>/`), with
62    /// its placeholders each once and no other brace. Unknown members are not checked. Every
63    /// failure is `E_MESH_DOC`. Cross-mesh resolution also needs the key document to agree:
64    /// see [`resolve_mesh`].
65    pub fn from_json(host: &str, body: &[u8]) -> Result<MeshDoc> {
66        let v = parse_mesh(body)?;
67        let m = v.as_object().unwrap_or(&[]);
68        if schema::get(m, "host").as_str() != Some(host) {
69            return err(ErrorCode::MeshDoc, "$.host", "the mesh document names another host");
70        }
71        if schema::get(m, "proto").as_str() != Some("datumesh/1") {
72            return err(ErrorCode::MeshDoc, "$.proto", "proto must be datumesh/1");
73        }
74        mesh_members(host, m)
75    }
76
77    /// The URL of endpoint `name` with its placeholders replaced: `values` must give each of
78    /// the template's placeholders exactly once and nothing else (by name, without braces).
79    /// Values are inserted as given: percent-encode them first where 5.1 says. `None` for an
80    /// endpoint the document does not have, or values that do not match its placeholders.
81    pub fn url(&self, name: &str, values: &[(&str, &str)]) -> Option<String> {
82        let template = self.endpoints.get(name)?;
83        let (_, placeholders, _) = ENDPOINTS.iter().find(|(n, _, _)| *n == name)?;
84        if values.len() != placeholders.len() || !placeholders.iter().all(|p| values.iter().any(|(k, _)| k == p)) {
85            return None;
86        }
87        let mut out = template.clone();
88        for (k, v) in values {
89            out = out.replacen(&format!("{{{k}}}"), v, 1);
90        }
91        Some(out)
92    }
93}
94
95/// A host accepted by cross-mesh resolution: its key document and its mesh document.
96#[derive(Clone, Debug, PartialEq, Eq, Hash)]
97#[non_exhaustive]
98pub struct ResolvedMesh {
99    /// The key document (9.2).
100    pub keys: KeyDoc,
101    /// The mesh document (9.6).
102    pub mesh: MeshDoc,
103    /// The key document's `declarations` (9.2, `spec-0.4.3`), checked with it in step 5; `None`
104    /// when it has none.
105    pub declarations: Option<Declarations>,
106}
107
108/// Cross-mesh resolution of `host` (9.6, `spec-0.4.1`), from what `GET
109/// https://<host>/.well-known/datumesh.json` and `GET
110/// https://<host>/.well-known/datumesh-mesh.json` gave. Separate from verification, which never
111/// needs it; it fetches nothing. In the order of 9.6, the first failure is reported:
112///
113/// 1. the key document retrieved as in 9.3 (`E_KEY_UNAVAILABLE`; a 200 without a body is
114///    `E_KEY_DOC`);
115/// 2. the mesh document retrieved: the integer status 200 with a body (`E_MESH_DOC`);
116/// 3. both parsed strictly: the key document (`E_KEY_DOC`), then the mesh document, an object
117///    (`E_MESH_DOC`);
118/// 4. the two describe the same mesh (`E_MESH_DOC`): the mesh document's `host` is `host` and its
119///    `proto` `"datumesh/1"`; the key document's `host` and `proto`, each where it is a string in
120///    an object, too (anything else about the key document is left to step 5);
121/// 5. the key document by 9.2 (`E_KEY_DOC`);
122/// 6. the mesh document's members (`E_MESH_DOC`, see [`MeshDoc::from_json`]).
123pub fn resolve_mesh(host: &str, key_doc_got: Retrieval<'_>, mesh_doc_got: Retrieval<'_>) -> Result<ResolvedMesh> {
124    // 1.
125    let key_body = match key_doc_got {
126        Retrieval::Response { status: 200, body: Some(b) } => b,
127        Retrieval::Response { status: 200, body: None } => {
128            return err(ErrorCode::KeyDoc, "$", "a 200 response without a key document");
129        }
130        _ => return err(ErrorCode::KeyUnavailable, "$", "the key document could not be retrieved"),
131    };
132    // 2.
133    let Retrieval::Response { status: 200, body: Some(mesh_body) } = mesh_doc_got else {
134        return err(ErrorCode::MeshDoc, "$", "the mesh document could not be retrieved");
135    };
136    // 3.
137    let kv = json::parse(key_body).map_err(|e| e.recode(ErrorCode::KeyDoc))?;
138    let mv = parse_mesh(mesh_body)?;
139    let mm = mv.as_object().unwrap_or(&[]);
140    // 4.
141    if schema::get(mm, "host").as_str() != Some(host) {
142        return err(ErrorCode::MeshDoc, "$.host", "the mesh document names another host than the one fetched");
143    }
144    if schema::get(mm, "proto").as_str() != Some("datumesh/1") {
145        return err(ErrorCode::MeshDoc, "$.proto", "the mesh document's proto is not datumesh/1");
146    }
147    if let Value::Object(km) = &kv {
148        if let Some(Value::String(h)) = schema::opt(km, "host") {
149            if h != host {
150                return err(ErrorCode::MeshDoc, "$.host", "the key document names another host");
151            }
152        }
153        // Compared only where it is a string (3f79313); otherwise step 5 gives E_KEY_DOC.
154        if let Some(Value::String(p)) = schema::opt(km, "proto") {
155            if p != "datumesh/1" {
156                return err(ErrorCode::MeshDoc, "$.proto", "the key document names another protocol");
157            }
158        }
159    }
160    // 5.
161    let doc = key_doc(host, &kv)?;
162    // 6.
163    let mesh = mesh_members(host, mm)?;
164    Ok(ResolvedMesh { keys: doc.keys, mesh, declarations: doc.declarations })
165}
166
167fn parse_mesh(body: &[u8]) -> Result<Value> {
168    let v = json::parse(body).map_err(|e| e.recode(ErrorCode::MeshDoc))?;
169    if !matches!(v, Value::Object(_)) {
170        return err(ErrorCode::MeshDoc, "$", "a mesh document is a JSON object");
171    }
172    Ok(v)
173}
174
175/// A URL by the licence URL rule on `host`: `https://`, printable ASCII, at most 512
176/// characters, its authority exactly `host`, followed by a path.
177fn url_on_host(u: &str, host: &str) -> bool {
178    let Some(rest) = u.strip_prefix("https://") else { return false };
179    u.len() <= limits::LICENCE_URL
180        && rest.bytes().all(|c| (0x21..=0x7E).contains(&c))
181        && rest.strip_prefix(host).is_some_and(|path| path.starts_with('/'))
182}
183
184fn mesh_members(host: &str, m: &[(String, Value)]) -> Result<MeshDoc> {
185    let md = ErrorCode::MeshDoc;
186    let bindings = match schema::get(m, "bindings") {
187        Value::String(b) if url_on_host(b, host) => b.clone(),
188        _ => return err(md, "$.bindings", "bindings is an https URL on this host"),
189    };
190    let Value::Object(em) = schema::get(m, "endpoints") else {
191        return err(md, "$.endpoints", "endpoints is an object");
192    };
193    for (name, _, required) in ENDPOINTS {
194        if required && schema::opt(em, name).is_none() {
195            return err(md, "$.endpoints", format!("the required endpoint {name} is missing"));
196        }
197    }
198    let mut endpoints = BTreeMap::new();
199    for (name, placeholders, _) in ENDPOINTS {
200        let Some(v) = schema::opt(em, name) else { continue };
201        let p = member("$.endpoints", name);
202        let Value::String(t) = v else { return err(md, &p, "an endpoint is a URL template") };
203        if !url_on_host(t, host) {
204            return err(md, &p, "an endpoint is an https URL on this host");
205        }
206        let mut rest = t.clone();
207        for ph in placeholders {
208            let token = format!("{{{ph}}}");
209            if rest.matches(&token).count() != 1 {
210                return err(md, &p, format!("the template has {token} exactly once"));
211            }
212            rest = rest.replacen(&token, "", 1);
213        }
214        // Every brace belongs to a placeholder (3f79313).
215        if rest.contains('{') || rest.contains('}') {
216            return err(md, &p, "every brace of the template belongs to one of its placeholders");
217        }
218        endpoints.insert(name.to_string(), t.clone());
219    }
220    Ok(MeshDoc { host: host.into(), bindings, endpoints })
221}