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::schema;
10
11/// The endpoints of 9.6: name, placeholders, whether required.
12const 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/// What one HTTPS `GET` of a document gave, as the caller's HTTP client saw it.
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
29pub enum Retrieval<'a> {
30    /// A response: its status and body (`None` when there was none).
31    Response {
32        /// The integer HTTP status.
33        status: u16,
34        /// The body.
35        body: Option<&'a [u8]>,
36    },
37    /// No response: a network, DNS, TLS or timeout failure, a redirect, or a retrieval the
38    /// client abandoned.
39    Failed,
40}
41
42/// A checked mesh document (9.6): the host's bindings URL and its known endpoint templates.
43/// Unknown members are dropped.
44#[derive(Clone, Debug, PartialEq, Eq, Hash)]
45#[non_exhaustive]
46pub struct MeshDoc {
47    /// The host it was fetched from (and names).
48    pub host: String,
49    /// The URL of the bindings resource (9.7).
50    pub bindings: String,
51    /// The known endpoints present, by name (`cell`, `receipt`, `jobs`, ...): URL templates
52    /// with their placeholders, such as `https://<host>/cell/{cell}`.
53    pub endpoints: BTreeMap<String, String>,
54}
55
56impl MeshDoc {
57    /// Parses and checks a mesh document fetched from `host`, on its own (without the key
58    /// document): strict JSON, an object, `host` equal to `host`, `proto` `"datumesh/1"`, then
59    /// `bindings`, `endpoints` (an object holding every required endpoint) and each known
60    /// endpoint in ascending name order (9.6): on this host (beginning `https://<host>/`), with
61    /// its placeholders each once and no other brace. Unknown members are not checked. Every
62    /// failure is `E_MESH_DOC`. Cross-mesh resolution also needs the key document to agree:
63    /// see [`resolve_mesh`].
64    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    /// The URL of endpoint `name` with its placeholders replaced: `values` must give each of
77    /// the template's placeholders exactly once and nothing else (by name, without braces).
78    /// Values are inserted as given: percent-encode them first where 5.1 says. `None` for an
79    /// endpoint the document does not have, or values that do not match its placeholders.
80    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/// A host accepted by cross-mesh resolution: its key document and its mesh document.
95#[derive(Clone, Debug, PartialEq, Eq, Hash)]
96#[non_exhaustive]
97pub struct ResolvedMesh {
98    /// The key document (9.2).
99    pub keys: KeyDoc,
100    /// The mesh document (9.6).
101    pub mesh: MeshDoc,
102}
103
104/// Cross-mesh resolution of `host` (9.6, `spec-0.4.1`), from what `GET
105/// https://<host>/.well-known/datumesh.json` and `GET
106/// https://<host>/.well-known/datumesh-mesh.json` gave. Separate from verification, which never
107/// needs it; it fetches nothing. In the order of 9.6, the first failure is reported:
108///
109/// 1. the key document retrieved as in 9.3 (`E_KEY_UNAVAILABLE`; a 200 without a body is
110///    `E_KEY_DOC`);
111/// 2. the mesh document retrieved: the integer status 200 with a body (`E_MESH_DOC`);
112/// 3. both parsed strictly: the key document (`E_KEY_DOC`), then the mesh document, an object
113///    (`E_MESH_DOC`);
114/// 4. the two describe the same mesh (`E_MESH_DOC`): the mesh document's `host` is `host` and its
115///    `proto` `"datumesh/1"`; the key document's `host` and `proto`, each where it is a string in
116///    an object, too (anything else about the key document is left to step 5);
117/// 5. the key document by 9.2 (`E_KEY_DOC`);
118/// 6. the mesh document's members (`E_MESH_DOC`, see [`MeshDoc::from_json`]).
119pub fn resolve_mesh(host: &str, key_doc_got: Retrieval<'_>, mesh_doc_got: Retrieval<'_>) -> Result<ResolvedMesh> {
120    // 1.
121    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    // 2.
129    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    // 3.
133    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    // 4.
137    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        // Compared only where it is a string (3f79313); otherwise step 5 gives E_KEY_DOC.
150        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    // 5.
157    let keys = key_doc(host, &kv)?;
158    // 6.
159    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
171/// A URL by the licence URL rule on `host`: `https://`, printable ASCII, at most 512
172/// characters, its authority exactly `host`, followed by a path.
173fn 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        // Every brace belongs to a placeholder (3f79313).
211        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}