Skip to main content

datumesh_core/
uri.rs

1//! Cell URIs (5.1-5.4) and the HTTPS mapping (5.3).
2
3use crate::error::{err, ErrorCode, Result};
4use crate::json::{jcs_sorted, Value};
5use crate::lexical::{self, limits};
6use crate::model::{check_lexical, unchecked, Arg, Args, CellRef, Reference, Type};
7use crate::schema;
8
9fn is_literal(c: u8) -> bool {
10    c.is_ascii_alphanumeric() || matches!(c, b'-' | b'.' | b'_' | b'~' | b':')
11}
12
13fn qpath(name: &str) -> String {
14    format!("uri.query.{name}")
15}
16
17/// Decodes a parameter value (5.2): literal characters as themselves, everything else as
18/// `%` and two uppercase hex digits, and the result UTF-8. Failures are `E_CANON_PERCENT`.
19fn percent_decode(raw: &str, path: &str) -> Result<String> {
20    let b = raw.as_bytes();
21    let mut out = Vec::with_capacity(b.len());
22    let mut i = 0;
23    while i < b.len() {
24        let c = b[i];
25        if c == b'%' {
26            let hex = |x: u8| match x {
27                b'0'..=b'9' => Some(x - b'0'),
28                b'A'..=b'F' => Some(x - b'A' + 10),
29                _ => None,
30            };
31            let (Some(h), Some(l)) = (b.get(i + 1).and_then(|&x| hex(x)), b.get(i + 2).and_then(|&x| hex(x))) else {
32                return err(ErrorCode::CanonPercent, path, "percent-encoding must be % and two uppercase hex digits");
33            };
34            let byte = (h << 4) | l;
35            if is_literal(byte) {
36                return err(
37                    ErrorCode::CanonPercent,
38                    path,
39                    format!("{:?} must not be percent-encoded", char::from(byte)),
40                );
41            }
42            out.push(byte);
43            i += 3;
44        } else if is_literal(c) {
45            out.push(c);
46            i += 1;
47        } else {
48            let shown = raw[i..].chars().next().unwrap_or('?');
49            return err(ErrorCode::CanonPercent, path, format!("{shown:?} must be percent-encoded"));
50        }
51    }
52    String::from_utf8(out).or_else(|_| err(ErrorCode::CanonPercent, path, "decoded bytes are not UTF-8"))
53}
54
55fn percent_encode(s: &str, out: &mut String) {
56    const HEX: &[u8; 16] = b"0123456789ABCDEF";
57    for &c in s.as_bytes() {
58        if is_literal(c) {
59            out.push(char::from(c));
60        } else {
61            out.push('%');
62            out.push(char::from(HEX[usize::from(c >> 4)]));
63            out.push(char::from(HEX[usize::from(c & 0xF)]));
64        }
65    }
66}
67
68/// Parses a cell URI against a function signature (5.4), reporting the first failure in the
69/// order 5.4 defines.
70///
71/// `signature` is the descriptor's `signature` member as JSON. It is taken as given (a
72/// descriptor checked under 7), except that it must be readable: an object with an `args`
73/// object whose members are objects with a string `type` (`E_SCHEMA` otherwise).
74///
75/// Its [`CellRef`] has no pin: a pinned URI (`fn_hash=`, `spec-0.4.2`) is refused with
76/// `E_CANON_ARG_NAME` (`fn_hash` is a reserved argument name). [`parse_reference`] reads it.
77pub fn parse_uri(uri: &str, signature: &Value) -> Result<CellRef> {
78    let r = parse_reference(uri, signature)?;
79    if r.fn_hash.is_some() {
80        return err(ErrorCode::CanonArgName, &qpath("fn_hash"), "a pinned URI: read it with parse_reference");
81    }
82    Ok(CellRef { ns: r.ns, fn_name: r.fn_name, args: r.args, asof: r.asof })
83}
84
85/// Parses a cell URI, pinned or not, against a function signature (5.4, `spec-0.4.2`),
86/// reporting the first failure in the order 5.4 defines: the pin `fn_hash=<64 hex>` comes after
87/// the arguments and before `asof`, and is checked (`E_CANON_HEX`) after the arguments' values
88/// and before `asof`. For a pinned URI, `signature` is that of the pinned descriptor.
89pub fn parse_reference(uri: &str, signature: &Value) -> Result<Reference> {
90    // 1. Outer grammar.
91    let bad = |d: &str| err(ErrorCode::ParseUri, "$", d.to_string());
92    let Some(rest) = uri.strip_prefix("cell://") else {
93        return bad("not cell://<namespace>/<function>[?<query>]");
94    };
95    if rest.contains('#') {
96        return bad("a cell URI has no fragment");
97    }
98    let (path, query) = match rest.split_once('?') {
99        Some((p, q)) => (p, Some(q)),
100        None => (rest, None),
101    };
102    let authority = path.split('/').next().unwrap_or("");
103    if authority.contains('@') || authority.contains(':') {
104        return bad("a cell URI has no userinfo or port");
105    }
106    let mut params: Vec<(&str, &str)> = Vec::new();
107    if let Some(q) = query {
108        if q.is_empty() {
109            return bad("empty query");
110        }
111        for p in q.split('&') {
112            match p.split_once('=') {
113                Some(kv) => params.push(kv),
114                None => return bad(&format!("parameter without '=': {p:?}")),
115            }
116        }
117    }
118
119    // 2. Namespace and function names.
120    let Some((ns, f)) = path.rsplit_once('/') else {
121        return err(ErrorCode::CanonName, "uri.path", "missing namespace");
122    };
123    lexical::check_namespace(ns, "uri.path")?;
124    lexical::check_name(f, "uri.path")?;
125    if path.len() > limits::FUNCTION_NAME {
126        return err(ErrorCode::CanonLimit, "uri.path", "function name longer than 512 characters");
127    }
128
129    // 3. Parameter names: asof last, then the pin last of the rest.
130    let asof_raw = match params.last() {
131        Some(&("asof", v)) => {
132            params.pop();
133            Some(v)
134        }
135        _ => None,
136    };
137    let pin_raw = match params.last() {
138        Some(&("fn_hash", v)) => {
139            params.pop();
140            Some(v)
141        }
142        _ => None,
143    };
144    let mut prev: Option<&str> = None;
145    for &(name, _) in &params {
146        let p = qpath(name);
147        if name == "asof" {
148            let code = if asof_raw.is_some() { ErrorCode::CanonArgDuplicate } else { ErrorCode::CanonArgOrder };
149            return err(code, &p, "asof is the last parameter, at most once");
150        }
151        if name == "fn_hash" {
152            let code = if pin_raw.is_some() { ErrorCode::CanonArgDuplicate } else { ErrorCode::CanonArgOrder };
153            return err(code, &p, "the pin fn_hash comes after the arguments, before asof, at most once");
154        }
155        lexical::check_arg_name(name, &p)?;
156        if let Some(pr) = prev {
157            if name == pr {
158                return err(ErrorCode::CanonArgDuplicate, &p, "argument repeated");
159            }
160            if name < pr {
161                return err(ErrorCode::CanonArgOrder, &p, "arguments must be sorted by name");
162            }
163        }
164        prev = Some(name);
165    }
166
167    // 4. Count.
168    if params.len() > limits::ARGS {
169        return err(ErrorCode::CanonLimit, "uri.query", "more than 32 arguments");
170    }
171
172    // 5. The signature, then the arguments against it.
173    crate::json::check_value_depth(signature)
174        .map_err(|e| crate::error::Error::new(e.code(), "signature", e.detail()))?;
175    let sig = schema::object(signature, "signature")?;
176    let decls = match schema::opt(sig, "args") {
177        Some(Value::Object(m)) => m,
178        _ => return err(ErrorCode::Schema, "signature.args", "the signature has no args object"),
179    };
180    for (k, d) in jcs_sorted(decls) {
181        let readable = matches!(d.get("type"), Some(Value::String(_))) && d.as_object().is_some();
182        if !readable {
183            return err(
184                ErrorCode::Schema,
185                &format!("signature.args.{k}"),
186                "a declaration is an object with a string type",
187            );
188        }
189    }
190    for &(name, _) in &params {
191        if schema::opt(decls, name).is_none() {
192            return err(ErrorCode::ArgUnknown, &qpath(name), format!("function declares no argument {name:?}"));
193        }
194    }
195    for (k, _) in jcs_sorted(decls) {
196        if !params.iter().any(|&(n, _)| n == k) {
197            return err(ErrorCode::ArgMissing, "uri.query", format!("argument {k:?} absent; use {k}=* for a slice"));
198        }
199    }
200
201    // 6. Values.
202    let mut args = Args::new();
203    for &(name, raw) in &params {
204        let p = qpath(name);
205        if raw == "*" {
206            args.insert(name.into(), Arg::Slice);
207            continue;
208        }
209        let text = percent_decode(raw, &p)?;
210        let tname = schema::get(decls, name).get("type").and_then(Value::as_str).unwrap_or("");
211        let ty = Type::parse(tname, &p)?;
212        check_lexical(ty, &text, &p)?;
213        args.insert(name.into(), Arg::Value(unchecked(ty, &text)));
214    }
215
216    // 7. The pin: 64 lowercase hex digits, as written.
217    let fn_hash = match pin_raw {
218        None => None,
219        Some(raw) => {
220            lexical::check_hex(raw, 64, &qpath("fn_hash"))?;
221            Some(raw.to_string())
222        }
223    };
224
225    // 8. asof.
226    let asof = match asof_raw {
227        None => None,
228        Some(raw) => {
229            let text = percent_decode(raw, "uri.query.asof")?;
230            lexical::check_instant(&text, "uri.query.asof")?;
231            Some(text)
232        }
233    };
234    Ok(Reference { ns: ns.into(), fn_name: f.into(), args, fn_hash, asof })
235}
236
237/// Writes the one URI of a cell reference (5.2). `format_uri(&parse_uri(u, s)?) == u` for
238/// every accepted `u`.
239pub fn format_uri(r: &CellRef) -> String {
240    format_parts(&r.ns, &r.fn_name, &r.args, None, r.asof.as_deref())
241}
242
243/// Writes the one URI of a reference (5.2), its pin as `&fn_hash=` before `&asof=`.
244/// `format_reference(&parse_reference(u, s)?) == u` for every accepted `u`.
245pub fn format_reference(r: &Reference) -> String {
246    format_parts(&r.ns, &r.fn_name, &r.args, r.fn_hash.as_deref(), r.asof.as_deref())
247}
248
249fn format_parts(ns: &str, f: &str, args: &Args, pin: Option<&str>, asof: Option<&str>) -> String {
250    let mut out = format!("cell://{ns}/{f}");
251    let mut first = true;
252    let mut sep = |out: &mut String| {
253        out.push(if first { '?' } else { '&' });
254        first = false;
255    };
256    for (name, arg) in args {
257        sep(&mut out);
258        out.push_str(name);
259        out.push('=');
260        match arg {
261            Arg::Slice => out.push('*'),
262            Arg::Value(s) => percent_encode(s.lexical(), &mut out),
263        }
264    }
265    if let Some(h) = pin {
266        sep(&mut out);
267        out.push_str("fn_hash=");
268        out.push_str(h);
269    }
270    if let Some(a) = asof {
271        sep(&mut out);
272        out.push_str("asof=");
273        percent_encode(a, &mut out);
274    }
275    out
276}
277
278/// The HTTPS URL of a cell URI on mesh host `mesh` (5.3): `https://<mesh>/cell/<rest>`.
279/// `mesh` must be a DNS name (`E_CANON_NAME`), `uri` must start with `cell://` (`E_PARSE_URI`).
280pub fn https_url(mesh: &str, uri: &str) -> Result<String> {
281    lexical::check_dns_name(mesh, "mesh")?;
282    let Some(rest) = uri.strip_prefix("cell://") else {
283        return err(ErrorCode::ParseUri, "$", "not a cell URI");
284    };
285    Ok(format!("https://{mesh}/cell/{rest}"))
286}
287
288/// The cell URI served at an HTTPS URL on mesh host `mesh` (5.3), by stripping the prefix.
289/// `None` when the URL is not under `https://<mesh>/cell/`.
290pub fn cell_uri_from_https(mesh: &str, url: &str) -> Option<String> {
291    let rest = url.strip_prefix("https://")?.strip_prefix(mesh)?.strip_prefix("/cell/")?;
292    Some(format!("cell://{rest}"))
293}