1use 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
17fn 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
68pub 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
85pub fn parse_reference(uri: &str, signature: &Value) -> Result<Reference> {
90 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 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 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 ¶ms {
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 if params.len() > limits::ARGS {
169 return err(ErrorCode::CanonLimit, "uri.query", "more than 32 arguments");
170 }
171
172 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 ¶ms {
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 let mut args = Args::new();
203 for &(name, raw) in ¶ms {
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 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 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
237pub fn format_uri(r: &CellRef) -> String {
240 format_parts(&r.ns, &r.fn_name, &r.args, None, r.asof.as_deref())
241}
242
243pub 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
278pub 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
288pub 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}