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