use std::collections::BTreeMap;
use crate::error::{err, member, ErrorCode, Result};
use crate::json::{self, Value};
use crate::keys::{key_doc, KeyDoc};
use crate::lexical::limits;
use crate::schema;
const ENDPOINTS: [(&str, &[&str], bool); 11] = [
("blob", &["sha256"], false),
("cell", &["cell"], true),
("cells", &[], false),
("descriptor", &["hash"], true),
("explain", &["cell"], false),
("find", &[], false),
("jobs", &["job"], true),
("manifest", &["slice_id", "members_hash"], true),
("receipt", &["hash"], true),
("slice", &["cell"], false),
("verify", &[], false),
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Retrieval<'a> {
Response {
status: u16,
body: Option<&'a [u8]>,
},
Failed,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct MeshDoc {
pub host: String,
pub bindings: String,
pub endpoints: BTreeMap<String, String>,
}
impl MeshDoc {
pub fn from_json(host: &str, body: &[u8]) -> Result<MeshDoc> {
let v = parse_mesh(body)?;
let m = v.as_object().unwrap_or(&[]);
if schema::get(m, "host").as_str() != Some(host) {
return err(ErrorCode::MeshDoc, "$.host", "the mesh document names another host");
}
if schema::get(m, "proto").as_str() != Some("datumesh/1") {
return err(ErrorCode::MeshDoc, "$.proto", "proto must be datumesh/1");
}
mesh_members(host, m)
}
pub fn url(&self, name: &str, values: &[(&str, &str)]) -> Option<String> {
let template = self.endpoints.get(name)?;
let (_, placeholders, _) = ENDPOINTS.iter().find(|(n, _, _)| *n == name)?;
if values.len() != placeholders.len() || !placeholders.iter().all(|p| values.iter().any(|(k, _)| k == p)) {
return None;
}
let mut out = template.clone();
for (k, v) in values {
out = out.replacen(&format!("{{{k}}}"), v, 1);
}
Some(out)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct ResolvedMesh {
pub keys: KeyDoc,
pub mesh: MeshDoc,
}
pub fn resolve_mesh(host: &str, key_doc_got: Retrieval<'_>, mesh_doc_got: Retrieval<'_>) -> Result<ResolvedMesh> {
let key_body = match key_doc_got {
Retrieval::Response { status: 200, body: Some(b) } => b,
Retrieval::Response { status: 200, body: None } => {
return err(ErrorCode::KeyDoc, "$", "a 200 response without a key document");
}
_ => return err(ErrorCode::KeyUnavailable, "$", "the key document could not be retrieved"),
};
let Retrieval::Response { status: 200, body: Some(mesh_body) } = mesh_doc_got else {
return err(ErrorCode::MeshDoc, "$", "the mesh document could not be retrieved");
};
let kv = json::parse(key_body).map_err(|e| e.recode(ErrorCode::KeyDoc))?;
let mv = parse_mesh(mesh_body)?;
let mm = mv.as_object().unwrap_or(&[]);
if schema::get(mm, "host").as_str() != Some(host) {
return err(ErrorCode::MeshDoc, "$.host", "the mesh document names another host than the one fetched");
}
if schema::get(mm, "proto").as_str() != Some("datumesh/1") {
return err(ErrorCode::MeshDoc, "$.proto", "the mesh document's proto is not datumesh/1");
}
if let Value::Object(km) = &kv {
if let Some(Value::String(h)) = schema::opt(km, "host") {
if h != host {
return err(ErrorCode::MeshDoc, "$.host", "the key document names another host");
}
}
if let Some(Value::String(p)) = schema::opt(km, "proto") {
if p != "datumesh/1" {
return err(ErrorCode::MeshDoc, "$.proto", "the key document names another protocol");
}
}
}
let keys = key_doc(host, &kv)?;
let mesh = mesh_members(host, mm)?;
Ok(ResolvedMesh { keys, mesh })
}
fn parse_mesh(body: &[u8]) -> Result<Value> {
let v = json::parse(body).map_err(|e| e.recode(ErrorCode::MeshDoc))?;
if !matches!(v, Value::Object(_)) {
return err(ErrorCode::MeshDoc, "$", "a mesh document is a JSON object");
}
Ok(v)
}
fn url_on_host(u: &str, host: &str) -> bool {
let Some(rest) = u.strip_prefix("https://") else { return false };
u.len() <= limits::LICENCE_URL
&& rest.bytes().all(|c| (0x21..=0x7E).contains(&c))
&& rest.strip_prefix(host).is_some_and(|path| path.starts_with('/'))
}
fn mesh_members(host: &str, m: &[(String, Value)]) -> Result<MeshDoc> {
let md = ErrorCode::MeshDoc;
let bindings = match schema::get(m, "bindings") {
Value::String(b) if url_on_host(b, host) => b.clone(),
_ => return err(md, "$.bindings", "bindings is an https URL on this host"),
};
let Value::Object(em) = schema::get(m, "endpoints") else {
return err(md, "$.endpoints", "endpoints is an object");
};
for (name, _, required) in ENDPOINTS {
if required && schema::opt(em, name).is_none() {
return err(md, "$.endpoints", format!("the required endpoint {name} is missing"));
}
}
let mut endpoints = BTreeMap::new();
for (name, placeholders, _) in ENDPOINTS {
let Some(v) = schema::opt(em, name) else { continue };
let p = member("$.endpoints", name);
let Value::String(t) = v else { return err(md, &p, "an endpoint is a URL template") };
if !url_on_host(t, host) {
return err(md, &p, "an endpoint is an https URL on this host");
}
let mut rest = t.clone();
for ph in placeholders {
let token = format!("{{{ph}}}");
if rest.matches(&token).count() != 1 {
return err(md, &p, format!("the template has {token} exactly once"));
}
rest = rest.replacen(&token, "", 1);
}
if rest.contains('{') || rest.contains('}') {
return err(md, &p, "every brace of the template belongs to one of its placeholders");
}
endpoints.insert(name.to_string(), t.clone());
}
Ok(MeshDoc { host: host.into(), bindings, endpoints })
}