Skip to main content

datumesh_core/
answer.rs

1//! Envelopes of `spec-0.4.2` (5.5-5.7): every outcome and trust label, the `fn_hash` echo, slice
2//! pages and batches, checked and built.
3
4use std::collections::BTreeMap;
5
6use crate::descriptor::cmp_lexical;
7use crate::envelope::{
8    check_job_and_poll, env_err, integer_member, quote, EnvelopeExtras, Fault, Outcome, Pending, Quote, Request, Trust,
9    BAD_REQUEST_CODES,
10};
11use crate::error::{Error, ErrorCode, Result};
12use crate::json::{self, Value};
13use crate::keys::Keys;
14use crate::lexical::{self, limits};
15use crate::model::{Arg, Scalar};
16use crate::receipt::Receipt;
17use crate::schema;
18use crate::slice::Slice;
19use crate::Envelope;
20
21/// The nine outcomes of SPEC 5.5 (`spec-0.4.2`), `pending` included.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
23#[non_exhaustive]
24pub enum AnswerOutcome {
25    /// 200.
26    Ok,
27    /// 200.
28    Stale,
29    /// 404.
30    NotCovered,
31    /// 403.
32    Unlicensed,
33    /// 402.
34    PaymentRequired,
35    /// 429.
36    RateLimited,
37    /// 400.
38    BadRequest,
39    /// 500-599.
40    Error,
41    /// 202.
42    Pending,
43}
44
45impl AnswerOutcome {
46    /// The outcome as written in JSON.
47    pub const fn as_str(self) -> &'static str {
48        match self {
49            AnswerOutcome::Ok => "ok",
50            AnswerOutcome::Stale => "stale",
51            AnswerOutcome::NotCovered => "not_covered",
52            AnswerOutcome::Unlicensed => "unlicensed",
53            AnswerOutcome::PaymentRequired => "payment_required",
54            AnswerOutcome::RateLimited => "rate_limited",
55            AnswerOutcome::BadRequest => "bad_request",
56            AnswerOutcome::Error => "error",
57            AnswerOutcome::Pending => "pending",
58        }
59    }
60
61    /// The outcome of its JSON name.
62    pub fn from_name(s: &str) -> Option<AnswerOutcome> {
63        Some(match s {
64            "ok" => AnswerOutcome::Ok,
65            "stale" => AnswerOutcome::Stale,
66            "not_covered" => AnswerOutcome::NotCovered,
67            "unlicensed" => AnswerOutcome::Unlicensed,
68            "payment_required" => AnswerOutcome::PaymentRequired,
69            "rate_limited" => AnswerOutcome::RateLimited,
70            "bad_request" => AnswerOutcome::BadRequest,
71            "error" => AnswerOutcome::Error,
72            "pending" => AnswerOutcome::Pending,
73            _ => return None,
74        })
75    }
76
77    /// The HTTP status a mesh sends with it (500 for `error`, whose exact 5xx is the mesh's).
78    pub const fn status(self) -> u16 {
79        match self {
80            AnswerOutcome::Ok | AnswerOutcome::Stale => 200,
81            AnswerOutcome::NotCovered => 404,
82            AnswerOutcome::Unlicensed => 403,
83            AnswerOutcome::PaymentRequired => 402,
84            AnswerOutcome::RateLimited => 429,
85            AnswerOutcome::BadRequest => 400,
86            AnswerOutcome::Error => 500,
87            AnswerOutcome::Pending => 202,
88        }
89    }
90
91    fn status_ok(self, status: u16) -> bool {
92        match self {
93            AnswerOutcome::Error => (500..=599).contains(&status),
94            other => other.status() == status,
95        }
96    }
97
98    fn members(self) -> (&'static [&'static str], &'static [&'static str]) {
99        match self {
100            AnswerOutcome::Ok => (&["receipt", "trust"], &["superseded_by", "reason", "fn_hash"]),
101            AnswerOutcome::Stale => (&["receipt", "trust", "age_seconds"], &["superseded_by", "reason", "fn_hash"]),
102            AnswerOutcome::NotCovered => (&[], &["fault", "error", "fn_hash"]),
103            AnswerOutcome::Unlicensed => (&[], &["fn_hash"]),
104            AnswerOutcome::PaymentRequired => (&["quote"], &["fn_hash"]),
105            AnswerOutcome::RateLimited => (&["retry_after_seconds"], &["fn_hash"]),
106            AnswerOutcome::BadRequest | AnswerOutcome::Error => (&["error"], &["fn_hash"]),
107            AnswerOutcome::Pending => (&["estimate_seconds", "job", "poll", "request"], &[]),
108        }
109    }
110}
111
112/// The four trust labels of SPEC 5.5 (`spec-0.4.2`), `computed` included.
113#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
114#[non_exhaustive]
115pub enum TrustLabel {
116    /// The publisher answered from its own records.
117    Primary,
118    /// Not primary; the mesh asked evaluated the cell and signed the receipt itself.
119    Computed,
120    /// Not primary, signed by another host, and the mesh recomputed it.
121    Verified,
122    /// Not primary, signed by another host, not recomputed.
123    Unverified,
124}
125
126impl TrustLabel {
127    /// The label as written in JSON.
128    pub const fn as_str(self) -> &'static str {
129        match self {
130            TrustLabel::Primary => "primary",
131            TrustLabel::Computed => "computed",
132            TrustLabel::Verified => "verified",
133            TrustLabel::Unverified => "unverified",
134        }
135    }
136
137    fn from_name(s: &str) -> Option<TrustLabel> {
138        Some(match s {
139            "primary" => TrustLabel::Primary,
140            "computed" => TrustLabel::Computed,
141            "verified" => TrustLabel::Verified,
142            "unverified" => TrustLabel::Unverified,
143            _ => return None,
144        })
145    }
146}
147
148/// A slice page (5.6): the `ok` answer of the slice endpoint.
149#[derive(Clone, Debug, PartialEq, Eq)]
150#[non_exhaustive]
151pub struct SlicePage {
152    /// The slice object requested, with the function hash its name resolved to (or the pin).
153    pub slice: Slice,
154    /// The member envelopes, `ok` or `stale`, in ascending key order.
155    pub members: Vec<Answer>,
156    /// The URL of the following page; `None` on the last.
157    pub next: Option<String>,
158}
159
160impl SlicePage {
161    /// A page of `members` of `slice`, with `next` when there is a following page. Not checked
162    /// until written and checked back.
163    pub fn new(slice: Slice, members: Vec<Answer>, next: Option<String>) -> SlicePage {
164        SlicePage { slice, members, next }
165    }
166}
167
168/// An envelope of `spec-0.4.2` (5.5), checked or to be written: every outcome, trust label and
169/// member, with a slice page (5.6) for the `ok` answer of the slice endpoint. As a builder: one
170/// constructor per outcome, `with_*` for the optional members, [`Answer::to_json`] for the body
171/// and [`Answer::status`] for the HTTP status; check the body back with [`check_answer`] (or
172/// [`check_slice_answer`]) to hold it to every rule.
173#[derive(Clone, Debug, PartialEq, Eq)]
174#[non_exhaustive]
175pub struct Answer {
176    /// The outcome.
177    pub outcome: AnswerOutcome,
178    /// The receipt (`ok`, `stale`; not a slice page's).
179    pub receipt: Option<Receipt>,
180    /// The trust label (`ok`, `stale`).
181    pub trust: Option<TrustLabel>,
182    /// Seconds since the mesh last confirmed the receipt (`stale`).
183    pub age_seconds: Option<u64>,
184    /// Hash of a newer receipt for the same cell.
185    pub superseded_by: Option<String>,
186    /// Why the receipt was superseded.
187    pub reason: Option<String>,
188    /// The quote (`payment_required`).
189    pub quote: Option<Quote>,
190    /// Seconds to wait (`rate_limited`).
191    pub retry_after_seconds: Option<u64>,
192    /// The code (`bad_request`, `error`, and `E_NAME_UNBOUND` in `not_covered`).
193    pub error: Option<ErrorCode>,
194    /// The formula evaluation fault (`not_covered`).
195    pub fault: Option<Fault>,
196    /// The function the answer is of: a pin's echo, or the hash the name resolved to.
197    pub fn_hash: Option<String>,
198    /// The `pending` envelope's members.
199    pub pending: Option<Pending>,
200    /// The slice page (`ok` at the slice endpoint).
201    pub page: Option<SlicePage>,
202    /// The HTTP status to send (`error`'s exact 5xx); checked answers carry the one received.
203    status: u16,
204}
205
206fn bare(outcome: AnswerOutcome) -> Answer {
207    Answer {
208        outcome,
209        receipt: None,
210        trust: None,
211        age_seconds: None,
212        superseded_by: None,
213        reason: None,
214        quote: None,
215        retry_after_seconds: None,
216        error: None,
217        fault: None,
218        fn_hash: None,
219        pending: None,
220        page: None,
221        status: outcome.status(),
222    }
223}
224
225fn code_allowed(outcome: AnswerOutcome, code: ErrorCode, job_url: bool) -> bool {
226    match outcome {
227        AnswerOutcome::BadRequest => {
228            BAD_REQUEST_CODES.contains(&code)
229                || matches!(code, ErrorCode::CanonHex | ErrorCode::SliceAtCell)
230                || (code == ErrorCode::JobUnknown && job_url)
231        }
232        AnswerOutcome::NotCovered => code == ErrorCode::NameUnbound,
233        AnswerOutcome::Error => !matches!(
234            code,
235            ErrorCode::Envelope | ErrorCode::JobUnknown | ErrorCode::SliceAtCell | ErrorCode::NameUnbound
236        ),
237        _ => false,
238    }
239}
240
241impl Answer {
242    /// `ok` (200): `receipt`, served with `trust`.
243    pub fn ok(receipt: Receipt, trust: TrustLabel) -> Answer {
244        Answer { receipt: Some(receipt), trust: Some(trust), ..bare(AnswerOutcome::Ok) }
245    }
246
247    /// `stale` (200): the last receipt known, last confirmed `age_seconds` ago.
248    pub fn stale(receipt: Receipt, trust: TrustLabel, age_seconds: u64) -> Answer {
249        Answer {
250            receipt: Some(receipt),
251            trust: Some(trust),
252            age_seconds: Some(age_seconds),
253            ..bare(AnswerOutcome::Stale)
254        }
255    }
256
257    /// `not_covered` (404).
258    pub fn not_covered() -> Answer {
259        bare(AnswerOutcome::NotCovered)
260    }
261
262    /// `unlicensed` (403).
263    pub fn unlicensed() -> Answer {
264        bare(AnswerOutcome::Unlicensed)
265    }
266
267    /// `payment_required` (402) with its quote.
268    pub fn payment_required(quote: Quote) -> Answer {
269        Answer { quote: Some(quote), ..bare(AnswerOutcome::PaymentRequired) }
270    }
271
272    /// `rate_limited` (429): retry after this many seconds.
273    pub fn rate_limited(retry_after_seconds: u64) -> Answer {
274        Answer { retry_after_seconds: Some(retry_after_seconds), ..bare(AnswerOutcome::RateLimited) }
275    }
276
277    /// `bad_request` (400) with a code it can carry (5.5: a parse code, `E_CANON_HEX`,
278    /// `E_SLICE_AT_CELL`, or `E_JOB_UNKNOWN` at a job URL); `E_ENVELOPE` otherwise.
279    pub fn bad_request(code: ErrorCode) -> Result<Answer> {
280        if !code_allowed(AnswerOutcome::BadRequest, code, true) {
281            return env_err("$.error", format!("bad_request cannot carry {code}"));
282        }
283        Ok(Answer { error: Some(code), ..bare(AnswerOutcome::BadRequest) })
284    }
285
286    /// `error` (500; see [`Answer::with_status`]) with a code it can carry (any of 10.2 but
287    /// `E_ENVELOPE`, `E_JOB_UNKNOWN`, `E_SLICE_AT_CELL` and `E_NAME_UNBOUND`).
288    pub fn error(code: ErrorCode) -> Result<Answer> {
289        if !code_allowed(AnswerOutcome::Error, code, false) {
290            return env_err("$.error", format!("error cannot carry {code}"));
291        }
292        Ok(Answer { error: Some(code), ..bare(AnswerOutcome::Error) })
293    }
294
295    /// `pending` (202).
296    pub fn pending(p: Pending) -> Answer {
297        Answer { pending: Some(p), ..bare(AnswerOutcome::Pending) }
298    }
299
300    /// The `ok` answer of the slice endpoint (5.6).
301    pub fn page(page: SlicePage) -> Answer {
302        Answer { page: Some(page), ..bare(AnswerOutcome::Ok) }
303    }
304
305    /// With the `fn_hash` echo (64 lowercase hex digits; not on `pending`); `E_ENVELOPE`
306    /// otherwise.
307    pub fn with_fn_hash(self, fn_hash: &str) -> Result<Answer> {
308        if self.outcome == AnswerOutcome::Pending || lexical::check_hex(fn_hash, 64, "").is_err() {
309            return env_err("$.fn_hash", "fn_hash is 64 lowercase hex digits, not on pending");
310        }
311        Ok(Answer { fn_hash: Some(fn_hash.into()), ..self })
312    }
313
314    /// A `not_covered` answer with a formula evaluation fault. Not checked here: a fault with
315    /// `error`, or on another outcome, fails [`check_answer`].
316    pub fn with_fault(self, fault: Fault) -> Answer {
317        Answer { fault: Some(fault), ..self }
318    }
319
320    /// A `not_covered` answer with `error` (only `E_NAME_UNBOUND`, and not with a fault);
321    /// `E_ENVELOPE` otherwise.
322    pub fn with_error(self, code: ErrorCode) -> Result<Answer> {
323        if self.outcome != AnswerOutcome::NotCovered || self.fault.is_some() || code != ErrorCode::NameUnbound {
324            return env_err("$.error", "not_covered carries E_NAME_UNBOUND only, and not with a fault");
325        }
326        Ok(Answer { error: Some(code), ..self })
327    }
328
329    /// An `ok` or `stale` answer pointing to a newer receipt, with an optional reason.
330    pub fn with_superseded(self, superseded_by: &str, reason: Option<&str>) -> Answer {
331        Answer { superseded_by: Some(superseded_by.into()), reason: reason.map(String::from), ..self }
332    }
333
334    /// The HTTP status to send, for an `error` within 500-599 (other outcomes have one status).
335    pub fn with_status(self, status: u16) -> Answer {
336        Answer { status, ..self }
337    }
338
339    /// The HTTP status: the outcome's, or for a checked answer the one received.
340    pub fn status(&self) -> u16 {
341        self.status
342    }
343
344    /// The JSON form of the envelope.
345    pub fn to_value(&self) -> Value {
346        if let Some(p) = &self.pending {
347            return p.to_value();
348        }
349        let mut m: Vec<(String, Value)> = vec![("outcome".into(), Value::String(self.outcome.as_str().into()))];
350        let s = |x: &str| Value::String(x.into());
351        let n = |x: u64| json::parse(x.to_string().as_bytes()).unwrap_or(Value::Null);
352        if let Some(r) = &self.receipt {
353            m.push(("receipt".into(), receipt_value(r)));
354        }
355        if let Some(t) = self.trust {
356            m.push(("trust".into(), s(t.as_str())));
357        }
358        if let Some(a) = self.age_seconds {
359            m.push(("age_seconds".into(), n(a)));
360        }
361        if let Some(x) = &self.superseded_by {
362            m.push(("superseded_by".into(), s(x)));
363        }
364        if let Some(x) = &self.reason {
365            m.push(("reason".into(), s(x)));
366        }
367        if let Some(q) = &self.quote {
368            m.push((
369                "quote".into(),
370                Value::Object(vec![
371                    ("amount".into(), s(&q.amount)),
372                    ("authorise".into(), s(&q.authorise)),
373                    ("currency".into(), s(&q.currency)),
374                ]),
375            ));
376        }
377        if let Some(a) = self.retry_after_seconds {
378            m.push(("retry_after_seconds".into(), n(a)));
379        }
380        if let Some(e) = self.error {
381            m.push(("error".into(), s(e.as_str())));
382        }
383        if let Some(f) = self.fault {
384            m.push(("fault".into(), s(f.as_str())));
385        }
386        if let Some(h) = &self.fn_hash {
387            m.push(("fn_hash".into(), s(h)));
388        }
389        if let Some(p) = &self.page {
390            m.push(("slice".into(), p.slice.to_value()));
391            m.push(("members".into(), Value::Array(p.members.iter().map(Answer::to_value).collect())));
392            if let Some(x) = &p.next {
393                m.push(("next".into(), s(x)));
394            }
395        }
396        Value::Object(m)
397    }
398
399    /// The envelope's body: its canonical JSON text (JCS).
400    pub fn to_json(&self) -> Vec<u8> {
401        self.to_value().to_jcs()
402    }
403
404    /// The `spec-0.4.1` form, for the 0.3.x functions: not `pending`, not `computed`.
405    pub(crate) fn into_envelope(self) -> Result<(Envelope, EnvelopeExtras)> {
406        let outcome = match self.outcome {
407            AnswerOutcome::Ok => Outcome::Ok,
408            AnswerOutcome::Stale => Outcome::Stale,
409            AnswerOutcome::NotCovered => Outcome::NotCovered,
410            AnswerOutcome::Unlicensed => Outcome::Unlicensed,
411            AnswerOutcome::PaymentRequired => Outcome::PaymentRequired,
412            AnswerOutcome::RateLimited => Outcome::RateLimited,
413            AnswerOutcome::BadRequest => Outcome::BadRequest,
414            AnswerOutcome::Error => Outcome::Error,
415            AnswerOutcome::Pending => return env_err("$.outcome", "unknown outcome \"pending\""),
416        };
417        let trust = match self.trust {
418            None => None,
419            Some(TrustLabel::Primary) => Some(Trust::Primary),
420            Some(TrustLabel::Verified) => Some(Trust::Verified),
421            Some(TrustLabel::Unverified) => Some(Trust::Unverified),
422            Some(_) => return env_err("$.trust", "trust is primary, verified or unverified"),
423        };
424        let env = Envelope {
425            outcome,
426            receipt: self.receipt,
427            trust,
428            age_seconds: self.age_seconds,
429            superseded_by: self.superseded_by,
430            reason: self.reason,
431            quote: self.quote,
432            retry_after_seconds: self.retry_after_seconds,
433            error: self.error,
434        };
435        Ok((env, EnvelopeExtras { fault: self.fault, fn_hash: self.fn_hash }))
436    }
437}
438
439fn receipt_value(r: &Receipt) -> Value {
440    let mut members = json::parse(r.payload()).ok().and_then(|v| v.as_object().map(<[_]>::to_vec)).unwrap_or_default();
441    members.push(("sig".into(), Value::String(r.sig().into())));
442    Value::Object(members)
443}
444
445/// What an envelope is checked against: a cell request, the slice endpoint's request, or
446/// nothing (a batch's refusal).
447enum Ctx<'a> {
448    Cell(&'a Request),
449    Slice(&'a SliceRequest),
450    None,
451}
452
453impl Ctx<'_> {
454    fn pin(&self) -> Option<&str> {
455        match self {
456            Ctx::Cell(r) => r.pin.as_deref(),
457            Ctx::Slice(s) => s.pin.as_deref(),
458            Ctx::None => None,
459        }
460    }
461
462    fn mesh(&self) -> Option<&str> {
463        match self {
464            Ctx::Cell(r) => r.mesh.as_deref(),
465            Ctx::Slice(s) => s.mesh.as_deref(),
466            Ctx::None => None,
467        }
468    }
469
470    fn job_url(&self) -> bool {
471        matches!(self, Ctx::Cell(r) if r.job.is_some())
472    }
473}
474
475/// Checks an envelope (5.5) answering a `/cell` request or a job URL, of any outcome and trust
476/// label, in the order of 5.5: strict JSON, members, HTTP status, the receipt through step 6 of
477/// 10.1, the binding to `request` (cell id, `asof`, the pin and its echo, the echoed request
478/// and job of `pending`), then `trust` (with `computed` against the signer and `poll` against
479/// the host when `request` names the mesh asked) and `superseded_by`. Never touches the
480/// network.
481pub fn check_answer(status: u16, body: &[u8], keys: &Keys, request: &Request) -> Result<Answer> {
482    let v = json::parse(body)?;
483    check_value(status, &v, keys, &Ctx::Cell(request), "$")
484}
485
486/// A slice request (5.6): the slice id asked for, the `asof`, the pin, the host asked, and for
487/// a following page the key of the last member received (`after`).
488#[derive(Clone, Debug, PartialEq, Eq, Hash)]
489pub struct SliceRequest {
490    slice_id: String,
491    asof: Option<String>,
492    pin: Option<String>,
493    mesh: Option<String>,
494    after: Option<BTreeMap<String, Scalar>>,
495}
496
497impl SliceRequest {
498    /// A request for the slice with id `slice_id` (64 lowercase hex digits) as of `asof` (an
499    /// instant) or now; anything else is [`crate::InvalidRequest`].
500    pub fn new(slice_id: &str, asof: Option<&str>) -> core::result::Result<SliceRequest, crate::InvalidRequest> {
501        let r = Request::new(slice_id, asof)?;
502        Ok(SliceRequest { slice_id: r.cell_id, asof: r.asof, pin: None, mesh: None, after: None })
503    }
504
505    /// Pinned to `fn_hash` (64 lowercase hex digits).
506    pub fn with_pin(self, fn_hash: &str) -> core::result::Result<SliceRequest, crate::InvalidRequest> {
507        let r = Request::new(&self.slice_id, None)?.with_pin(fn_hash)?;
508        Ok(SliceRequest { pin: r.pin, ..self })
509    }
510
511    /// Naming the host asked (a DNS name): `next` and `computed` are then checked against it.
512    pub fn with_mesh(self, host: &str) -> core::result::Result<SliceRequest, crate::InvalidRequest> {
513        let r = Request::new(&self.slice_id, None)?.with_mesh(host)?;
514        Ok(SliceRequest { mesh: r.mesh, ..self })
515    }
516
517    /// For a following page: the key of the last member received, from each open argument's
518    /// name to its value. Other names are ignored; a cursor lacking an open argument, or whose
519    /// value has another type than the members' key, cannot be ordered against (`E_ENVELOPE`
520    /// from [`check_slice_answer`]).
521    pub fn with_after(self, after: BTreeMap<String, Scalar>) -> SliceRequest {
522        SliceRequest { after: Some(after), ..self }
523    }
524
525    /// The slice id asked for.
526    pub fn slice_id(&self) -> &str {
527        &self.slice_id
528    }
529
530    /// The `asof` asked for.
531    pub fn asof(&self) -> Option<&str> {
532        self.asof.as_deref()
533    }
534
535    /// The pin.
536    pub fn fn_hash(&self) -> Option<&str> {
537        self.pin.as_deref()
538    }
539
540    /// The host asked.
541    pub fn mesh(&self) -> Option<&str> {
542        self.mesh.as_deref()
543    }
544
545    /// The key after which this page's members come.
546    pub fn after(&self) -> Option<&BTreeMap<String, Scalar>> {
547        self.after.as_ref()
548    }
549}
550
551/// Checks an answer of the slice endpoint (5.6) against `request`: `stale` and `pending` are
552/// not answers there; `ok` is a page, checked as 5.6 says (its members, the slice id, `next`,
553/// then each member's receipt through step 6 of 10.1, membership, `asof`, trust and key order
554/// after `after`); every other outcome as in 5.5.
555pub fn check_slice_answer(status: u16, body: &[u8], keys: &Keys, request: &SliceRequest) -> Result<Answer> {
556    let v = json::parse(body)?;
557    check_value(status, &v, keys, &Ctx::Slice(request), "$")
558}
559
560/// The answer to a batch (5.7).
561#[derive(Clone, Debug, PartialEq, Eq)]
562#[non_exhaustive]
563pub enum Batch {
564    /// 200: one result per request, in request order; a failing item is its error, in place.
565    Items(Vec<Result<Answer>>),
566    /// The whole batch refused: one envelope (`unlicensed`, `payment_required`, `rate_limited`,
567    /// `bad_request` or `error`).
568    Refused(Box<Answer>),
569}
570
571/// Checks the answer to a batch (5.7) of `requests`: for 200, an array with one
572/// `{envelope, status}` per request (`status` an integer 100-599), else `E_ENVELOPE` for the
573/// whole; then each item as an envelope (5.5) with its status against its own request, a
574/// failing item reported in its place. Any other status is one envelope of a refusal outcome,
575/// with nothing to bind.
576pub fn check_batch(status: u16, body: &[u8], keys: &Keys, requests: &[Request]) -> Result<Batch> {
577    let v = json::parse(body)?;
578    if status != 200 {
579        if !matches!(v, Value::Object(_)) {
580            return env_err("$", "a refused batch is one envelope");
581        }
582        let a = check_value(status, &v, keys, &Ctx::None, "$")?;
583        if !matches!(
584            a.outcome,
585            AnswerOutcome::Unlicensed
586                | AnswerOutcome::PaymentRequired
587                | AnswerOutcome::RateLimited
588                | AnswerOutcome::BadRequest
589                | AnswerOutcome::Error
590        ) {
591            return env_err(
592                "$.outcome",
593                "a batch is refused with unlicensed, payment_required, rate_limited, bad_request or error",
594            );
595        }
596        return Ok(Batch::Refused(Box::new(a)));
597    }
598    let Value::Array(items) = &v else { return env_err("$", "a batch answer is an array") };
599    if items.len() != requests.len() {
600        return env_err("$", format!("{} items for {} requests", items.len(), requests.len()));
601    }
602    let mut parts = Vec::with_capacity(items.len());
603    for (i, item) in items.iter().enumerate() {
604        let p = format!("$[{i}]");
605        let m = schema::object_or(item, &p, ErrorCode::Envelope, "an item")?;
606        schema::exact(m, &["envelope", "status"], &[], &p, ErrorCode::Envelope)?;
607        let st = integer_member(schema::get(m, "status"), &format!("{p}.status"))?;
608        if !(100..=599).contains(&st) {
609            return env_err(&format!("{p}.status"), "an item's status is 100-599");
610        }
611        parts.push((schema::get(m, "envelope"), st as u16));
612    }
613    Ok(Batch::Items(
614        parts
615            .into_iter()
616            .zip(requests)
617            .enumerate()
618            .map(|(i, ((env, st), req))| check_value(st, env, keys, &Ctx::Cell(req), &format!("$[{i}].envelope")))
619            .collect(),
620    ))
621}
622
623/// A batch answer's body (5.7): one `{envelope, status}` per answer, in order.
624pub fn batch_body(items: &[(u16, Answer)]) -> Vec<u8> {
625    let n = |x: u16| json::parse(x.to_string().as_bytes()).unwrap_or(Value::Null);
626    Value::Array(
627        items
628            .iter()
629            .map(|(st, a)| Value::Object(vec![("envelope".into(), a.to_value()), ("status".into(), n(*st))]))
630            .collect(),
631    )
632    .to_jcs()
633}
634
635fn nested(e: Error, prefix: &str) -> Error {
636    if prefix == "$" {
637        return e;
638    }
639    let inner = e.path().strip_prefix('$').unwrap_or(e.path());
640    Error::new(e.code(), format!("{prefix}{inner}"), e.detail())
641}
642
643fn check_value(status: u16, v: &Value, keys: &Keys, ctx: &Ctx<'_>, at: &str) -> Result<Answer> {
644    check_inner(status, v, keys, ctx).map_err(|e| nested(e, at))
645}
646
647/// A URL `https://<DNS name>/<path>` of printable ASCII, at most 512 characters.
648fn url_host(u: &str) -> Option<&str> {
649    let rest = u.strip_prefix("https://")?;
650    if u.len() > limits::LICENCE_URL || !rest.bytes().all(|c| (0x21..=0x7E).contains(&c)) {
651        return None;
652    }
653    let (host, _) = rest.split_once('/')?;
654    lexical::check_dns_name(host, "").ok().map(|()| host)
655}
656
657fn check_inner(status: u16, v: &Value, keys: &Keys, ctx: &Ctx<'_>) -> Result<Answer> {
658    // 2.
659    let m = match v {
660        Value::Object(m) if matches!(schema::opt(m, "outcome"), Some(Value::String(_))) => m,
661        _ => return env_err("$", "an envelope is an object with a string outcome"),
662    };
663    let oname = schema::get(m, "outcome").as_str().unwrap_or("");
664    let Some(outcome) = AnswerOutcome::from_name(oname) else {
665        return env_err("$.outcome", format!("unknown outcome {oname:?}"));
666    };
667    let slice_ctx = matches!(ctx, Ctx::Slice(_));
668    if slice_ctx && matches!(outcome, AnswerOutcome::Stale | AnswerOutcome::Pending) {
669        return env_err("$.outcome", "stale and pending are not answers of the slice endpoint");
670    }
671    if outcome == AnswerOutcome::Pending {
672        return check_pending(status, m, ctx);
673    }
674    let is_page = slice_ctx && outcome == AnswerOutcome::Ok;
675    let mut a = check_members(m, outcome, ctx, is_page)?;
676    // 3.
677    if !outcome.status_ok(status) {
678        return env_err("$.outcome", format!("status {status} does not match outcome {oname}"));
679    }
680    a.status = status;
681    // 3a. What the request decides without the receipt: the pin echo (E_JOB_UNKNOWN only at
682    // a job URL was checked with the members; pending is checked apart).
683    pin_echo(&a, ctx.pin(), "$.fn_hash")?;
684    if is_page {
685        let Ctx::Slice(req) = ctx else { return env_err("$", "a page answers a slice request") };
686        return check_page(m, a, keys, req);
687    }
688    // 4.
689    if let Some(rv) = schema::opt(m, "receipt") {
690        a.receipt = Some(member_receipt(rv, keys, "$.receipt")?);
691    }
692    // 5.
693    if let Ctx::Cell(req) = ctx {
694        bind_cell(&a, req)?;
695    }
696    // 6.
697    if let Some(r) = &a.receipt {
698        trust_check(&a, r, ctx.mesh(), "$")?;
699    }
700    Ok(a)
701}
702
703fn member_receipt(rv: &Value, keys: &Keys, path: &str) -> Result<Receipt> {
704    crate::verify::signature_valid_value(rv, keys).map_err(|(e, _)| {
705        let inner = e.path().strip_prefix('$').unwrap_or(e.path());
706        Error::new(e.code(), format!("{path}{inner}"), e.detail())
707    })
708}
709
710/// 5.5 step 2 for every outcome but `pending`; for a page, its own members.
711fn check_members(m: &[(String, Value)], outcome: AnswerOutcome, ctx: &Ctx<'_>, is_page: bool) -> Result<Answer> {
712    let (req, opt): (&[&str], &[&str]) =
713        if is_page { (&["members", "slice"], &["next", "fn_hash"]) } else { outcome.members() };
714    let mut allowed: Vec<&str> = vec!["outcome"];
715    allowed.extend_from_slice(req);
716    schema::exact(m, &allowed, opt, "$", ErrorCode::Envelope)?;
717    let mut a = bare(outcome);
718    if let Some(r) = schema::opt(m, "receipt") {
719        if !matches!(r, Value::Object(_)) {
720            return env_err("$.receipt", "receipt is a receipt object");
721        }
722    }
723    if let Some(t) = schema::opt(m, "trust") {
724        a.trust = match t.as_str().and_then(TrustLabel::from_name) {
725            Some(t) => Some(t),
726            None => return env_err("$.trust", "trust is primary, computed, verified or unverified"),
727        };
728    }
729    if let Some(x) = schema::opt(m, "age_seconds") {
730        a.age_seconds = Some(integer_member(x, "$.age_seconds")?);
731    }
732    if let Some(x) = schema::opt(m, "retry_after_seconds") {
733        a.retry_after_seconds = Some(integer_member(x, "$.retry_after_seconds")?);
734    }
735    if let Some(s) = schema::opt(m, "superseded_by") {
736        let s = s.as_str().unwrap_or("");
737        lexical::check_hex(s, 64, "$.superseded_by").map_err(|e| e.recode(ErrorCode::Envelope))?;
738        a.superseded_by = Some(s.into());
739    }
740    if let Some(r) = schema::opt(m, "reason") {
741        let Some(r) = r.as_str() else { return env_err("$.reason", "reason must be a string") };
742        if schema::opt(m, "superseded_by").is_none() {
743            return env_err("$.reason", "reason is only present with superseded_by");
744        }
745        lexical::check_text(r, limits::SUMMARY, false, "$.reason").map_err(|e| e.recode(ErrorCode::Envelope))?;
746        if r.is_empty() {
747            return env_err("$.reason", "reason must not be empty");
748        }
749        a.reason = Some(r.into());
750    }
751    if let Some(q) = schema::opt(m, "quote") {
752        a.quote = Some(quote(q)?);
753    }
754    if let Some(e) = schema::opt(m, "error") {
755        let code = e.as_str().and_then(ErrorCode::from_code);
756        match code {
757            Some(c) if code_allowed(outcome, c, ctx.job_url()) => a.error = Some(c),
758            _ => return env_err("$.error", format!("{e:?} is not a code this outcome can carry")),
759        }
760    }
761    if let Some(f) = schema::opt(m, "fault") {
762        let Some(fault) = f.as_str().and_then(Fault::from_code) else {
763            return env_err("$.fault", format!("{f:?} is not a formula evaluation fault"));
764        };
765        if a.error.is_some() {
766            return env_err("$.fault", "not_covered carries a fault or an error, not both");
767        }
768        a.fault = Some(fault);
769    }
770    if let Some(h) = schema::opt(m, "fn_hash") {
771        let h = h.as_str().unwrap_or("");
772        lexical::check_hex(h, 64, "$.fn_hash").map_err(|e| e.recode(ErrorCode::Envelope))?;
773        a.fn_hash = Some(h.into());
774    }
775    if let Some(n) = schema::opt(m, "next") {
776        if n.as_str().and_then(url_host).is_none() {
777            return env_err("$.next", "next is https://, a DNS name, then a path, at most 512 characters");
778        }
779    }
780    Ok(a)
781}
782
783/// The receipt answers the request (5.5 step 5).
784fn bind_cell(a: &Answer, req: &Request) -> Result<()> {
785    let Some(r) = &a.receipt else { return Ok(()) };
786    if r.cell().id() != req.cell_id {
787        return env_err("$.receipt.cell", "the receipt is for another cell than the one requested");
788    }
789    if let Some(asked) = &req.asof {
790        if r.asof() > asked.as_str() {
791            return env_err("$.receipt.asof", "the receipt's asof is after the requested asof");
792        }
793    }
794    Ok(())
795}
796
797/// The pin echo (5.5 step 3a): with a pin, `fn_hash` required (but on `rate_limited` and
798/// `bad_request`) and equal to it.
799fn pin_echo(a: &Answer, pin: Option<&str>, path: &str) -> Result<()> {
800    if let Some(p) = pin {
801        match &a.fn_hash {
802            Some(h) if h != p => return env_err(path, "the fn_hash echo is not the pin"),
803            None if !matches!(a.outcome, AnswerOutcome::RateLimited | AnswerOutcome::BadRequest) => {
804                return env_err(path, "an answer to a pinned request echoes the pin");
805            }
806            _ => {}
807        }
808    }
809    Ok(())
810}
811
812/// 5.5 step 6: trust against the receipt (and the host asked, when known), then `fn_hash`
813/// against the receipt's `cell.fn_hash`, then `superseded_by`.
814fn trust_check(a: &Answer, r: &Receipt, mesh: Option<&str>, at: &str) -> Result<()> {
815    let p = |m: &str| format!("{at}.{m}");
816    let primary = r.signer_host() == lexical::host_of(&r.cell().ns) && r.inputs().is_empty();
817    let t = a.trust.unwrap_or(TrustLabel::Unverified);
818    if (t == TrustLabel::Primary) != primary {
819        return env_err(
820            &p("trust"),
821            if primary { "a primary receipt is labelled primary" } else { "the receipt is not primary" },
822        );
823    }
824    if let Some(host) = mesh {
825        match t {
826            TrustLabel::Computed if r.signer_host() != host => {
827                return env_err(&p("trust"), "computed: the receipt is signed by another host than the one asked");
828            }
829            TrustLabel::Verified | TrustLabel::Unverified if r.signer_host() == host => {
830                return env_err(&p("trust"), "the mesh asked signed this receipt: its trust is computed");
831            }
832            _ => {}
833        }
834    }
835    if a.fn_hash.as_deref().is_some_and(|h| h != r.cell().fn_hash) {
836        return env_err(&p("fn_hash"), "fn_hash differs from the receipt's cell.fn_hash");
837    }
838    if a.superseded_by.as_deref() == Some(r.hash()) {
839        return env_err(&p("superseded_by"), "a receipt cannot supersede itself");
840    }
841    Ok(())
842}
843
844fn check_pending(status: u16, m: &[(String, Value)], ctx: &Ctx<'_>) -> Result<Answer> {
845    // 2.
846    schema::exact(m, &["estimate_seconds", "job", "outcome", "poll", "request"], &[], "$", ErrorCode::Envelope)?;
847    let estimate = integer_member(schema::get(m, "estimate_seconds"), "$.estimate_seconds")?;
848    let Some(job) = schema::get(m, "job").as_str() else { return env_err("$.job", "job must be a string") };
849    let Some(poll) = schema::get(m, "poll").as_str() else { return env_err("$.poll", "poll must be a string") };
850    check_job_and_poll(job, poll)?;
851    let rm = schema::object_or(schema::get(m, "request"), "$.request", ErrorCode::Envelope, "request")?;
852    schema::exact(rm, &["cell_id"], &["asof", "fn_hash"], "$.request", ErrorCode::Envelope)?;
853    let cell_id = schema::string_or(rm, "cell_id", "$.request", ErrorCode::Envelope)?;
854    let opt = |name: &str| -> Result<Option<&str>> {
855        match schema::opt(rm, name) {
856            None => Ok(None),
857            Some(_) => schema::string_or(rm, name, "$.request", ErrorCode::Envelope).map(Some),
858        }
859    };
860    let invalid = |e: crate::InvalidRequest| Error::new(ErrorCode::Envelope, "$.request", e.to_string());
861    let mut echoed = Request::new(cell_id, opt("asof")?).map_err(invalid)?;
862    if let Some(p) = opt("fn_hash")? {
863        echoed = echoed.with_pin(p).map_err(invalid)?;
864    }
865    // 3.
866    if status != Pending::STATUS {
867        return env_err("$.outcome", format!("status {status} does not match outcome pending"));
868    }
869    // 5.
870    let Ctx::Cell(request) = ctx else { return env_err("$.outcome", "pending answers a cell request") };
871    if echoed.cell_id != request.cell_id || echoed.asof != request.asof || echoed.pin != request.pin {
872        return env_err("$.request", "the echoed request is not the request made");
873    }
874    if let Some(polled) = &request.job {
875        if job != polled {
876            return env_err("$.job", "the envelope is for another job than the one polled");
877        }
878    }
879    // 6. poll on the host asked.
880    if let Some(host) = &request.mesh {
881        if url_host(poll) != Some(host.as_str()) {
882            return env_err("$.poll", "poll is on another host than the one asked");
883        }
884    }
885    let p = Pending::new(job, estimate, poll, echoed)?;
886    Ok(Answer { pending: Some(p), ..bare(AnswerOutcome::Pending) })
887}
888
889/// The key of a member: its values of the slice's open (and ranged) arguments, by name.
890fn key_of(r: &Receipt, names: &[String]) -> Vec<Scalar> {
891    names.iter().filter_map(|n| r.cell().args.get(n).and_then(Arg::scalar).cloned()).collect()
892}
893
894fn cmp_keys(a: &[Scalar], b: &[Scalar]) -> core::cmp::Ordering {
895    for (x, y) in a.iter().zip(b) {
896        let o = if x.ty() == y.ty() { cmp_lexical(x.ty(), x.lexical(), y.lexical()) } else { x.ty().cmp(&y.ty()) };
897        if o != core::cmp::Ordering::Equal {
898            return o;
899        }
900    }
901    a.len().cmp(&b.len())
902}
903
904/// 5.6: a page bound to its request, the page first, then each member completely.
905fn check_page(m: &[(String, Value)], mut a: Answer, keys: &Keys, req: &SliceRequest) -> Result<Answer> {
906    let Value::Array(items) = schema::get(m, "members") else {
907        return env_err("$.members", "members is an array of envelopes");
908    };
909    let slice = Slice::from_value(schema::get(m, "slice")).map_err(|e| e.recode(ErrorCode::Envelope))?;
910    let next = schema::opt(m, "next").and_then(Value::as_str).map(String::from);
911    // 2. The page: the slice, its fn_hash, next, the cursor.
912    if slice.id() != req.slice_id {
913        return env_err("$.slice", "the page is of another slice than the one requested");
914    }
915    if a.fn_hash.as_deref().is_some_and(|h| h != slice.fn_hash()) {
916        return env_err("$.fn_hash", "fn_hash differs from the slice's");
917    }
918    if let (Some(host), Some(n)) = (&req.mesh, &next) {
919        if url_host(n) != Some(host.as_str()) {
920            return env_err("$.next", "next is on another host than the one asked");
921        }
922    }
923    // The cursor on the open arguments only; other names are ignored.
924    let names = slice.key_names();
925    let mut last: Option<Vec<Scalar>> = match &req.after {
926        None => None,
927        Some(after) => {
928            let key: Vec<Scalar> = names.iter().filter_map(|n| after.get(n).cloned()).collect();
929            if key.len() != names.len() {
930                return env_err("$", "the cursor lacks an open argument of the slice");
931            }
932            Some(key)
933        }
934    };
935    // 3. Each member, completely, in order.
936    let mut out = Vec::with_capacity(items.len());
937    for (i, item) in items.iter().enumerate() {
938        let p = format!("$.members[{i}]");
939        let mm = match item {
940            Value::Object(mm) => mm,
941            _ => return env_err(&p, "a member is an envelope"),
942        };
943        let o = match schema::get(mm, "outcome").as_str().and_then(AnswerOutcome::from_name) {
944            Some(o @ (AnswerOutcome::Ok | AnswerOutcome::Stale)) => o,
945            _ => return env_err(&p, "a member is an ok or stale envelope"),
946        };
947        let mut ma = check_members(mm, o, &Ctx::None, false).map_err(|e| nested(e, &p))?;
948        let r = member_receipt(schema::get(mm, "receipt"), keys, &format!("{p}.receipt"))?;
949        if !slice.contains(&r) {
950            return env_err(&p, "the member's cell is not a member of the slice");
951        }
952        if let Some(asked) = &req.asof {
953            if r.asof() > asked.as_str() {
954                return env_err(&format!("{p}.receipt.asof"), "the member's asof is after the requested asof");
955            }
956        }
957        trust_check(&ma, &r, req.mesh.as_deref(), &p)?;
958        let key = key_of(&r, &names);
959        if let Some(prev) = &last {
960            if key.len() != prev.len() || key.iter().zip(prev).any(|(x, y)| x.ty() != y.ty()) {
961                return env_err(&p, "the member's key has other types than the previous key (or the cursor)");
962            }
963            if cmp_keys(&key, prev) != core::cmp::Ordering::Greater {
964                return env_err(&p, "members are in strictly ascending key order, after the cursor");
965            }
966        }
967        last = Some(key);
968        ma.receipt = Some(r);
969        out.push(ma);
970    }
971    a.page = Some(SlicePage { slice, members: out, next });
972    Ok(a)
973}