1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
23#[non_exhaustive]
24pub enum AnswerOutcome {
25 Ok,
27 Stale,
29 NotCovered,
31 Unlicensed,
33 PaymentRequired,
35 RateLimited,
37 BadRequest,
39 Error,
41 Pending,
43}
44
45impl AnswerOutcome {
46 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
114#[non_exhaustive]
115pub enum TrustLabel {
116 Primary,
118 Computed,
120 Verified,
122 Unverified,
124}
125
126impl TrustLabel {
127 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#[derive(Clone, Debug, PartialEq, Eq)]
150#[non_exhaustive]
151pub struct SlicePage {
152 pub slice: Slice,
154 pub members: Vec<Answer>,
156 pub next: Option<String>,
158}
159
160impl SlicePage {
161 pub fn new(slice: Slice, members: Vec<Answer>, next: Option<String>) -> SlicePage {
164 SlicePage { slice, members, next }
165 }
166}
167
168#[derive(Clone, Debug, PartialEq, Eq)]
174#[non_exhaustive]
175pub struct Answer {
176 pub outcome: AnswerOutcome,
178 pub receipt: Option<Receipt>,
180 pub trust: Option<TrustLabel>,
182 pub age_seconds: Option<u64>,
184 pub superseded_by: Option<String>,
186 pub reason: Option<String>,
188 pub quote: Option<Quote>,
190 pub retry_after_seconds: Option<u64>,
192 pub error: Option<ErrorCode>,
194 pub fault: Option<Fault>,
196 pub fn_hash: Option<String>,
198 pub pending: Option<Pending>,
200 pub page: Option<SlicePage>,
202 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 pub fn ok(receipt: Receipt, trust: TrustLabel) -> Answer {
244 Answer { receipt: Some(receipt), trust: Some(trust), ..bare(AnswerOutcome::Ok) }
245 }
246
247 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 pub fn not_covered() -> Answer {
259 bare(AnswerOutcome::NotCovered)
260 }
261
262 pub fn unlicensed() -> Answer {
264 bare(AnswerOutcome::Unlicensed)
265 }
266
267 pub fn payment_required(quote: Quote) -> Answer {
269 Answer { quote: Some(quote), ..bare(AnswerOutcome::PaymentRequired) }
270 }
271
272 pub fn rate_limited(retry_after_seconds: u64) -> Answer {
274 Answer { retry_after_seconds: Some(retry_after_seconds), ..bare(AnswerOutcome::RateLimited) }
275 }
276
277 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 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 pub fn pending(p: Pending) -> Answer {
297 Answer { pending: Some(p), ..bare(AnswerOutcome::Pending) }
298 }
299
300 pub fn page(page: SlicePage) -> Answer {
302 Answer { page: Some(page), ..bare(AnswerOutcome::Ok) }
303 }
304
305 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 pub fn with_fault(self, fault: Fault) -> Answer {
317 Answer { fault: Some(fault), ..self }
318 }
319
320 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 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 pub fn with_status(self, status: u16) -> Answer {
336 Answer { status, ..self }
337 }
338
339 pub fn status(&self) -> u16 {
341 self.status
342 }
343
344 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 pub fn to_json(&self) -> Vec<u8> {
401 self.to_value().to_jcs()
402 }
403
404 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
445enum 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
475pub 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#[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 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 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 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 pub fn with_after(self, after: BTreeMap<String, Scalar>) -> SliceRequest {
522 SliceRequest { after: Some(after), ..self }
523 }
524
525 pub fn slice_id(&self) -> &str {
527 &self.slice_id
528 }
529
530 pub fn asof(&self) -> Option<&str> {
532 self.asof.as_deref()
533 }
534
535 pub fn fn_hash(&self) -> Option<&str> {
537 self.pin.as_deref()
538 }
539
540 pub fn mesh(&self) -> Option<&str> {
542 self.mesh.as_deref()
543 }
544
545 pub fn after(&self) -> Option<&BTreeMap<String, Scalar>> {
547 self.after.as_ref()
548 }
549}
550
551pub 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#[derive(Clone, Debug, PartialEq, Eq)]
562#[non_exhaustive]
563pub enum Batch {
564 Items(Vec<Result<Answer>>),
566 Refused(Box<Answer>),
569}
570
571pub 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
623pub 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
647fn 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 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 if !outcome.status_ok(status) {
678 return env_err("$.outcome", format!("status {status} does not match outcome {oname}"));
679 }
680 a.status = status;
681 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 if let Some(rv) = schema::opt(m, "receipt") {
690 a.receipt = Some(member_receipt(rv, keys, "$.receipt")?);
691 }
692 if let Ctx::Cell(req) = ctx {
694 bind_cell(&a, req)?;
695 }
696 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
710fn 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
783fn 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
797fn 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
812fn 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 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 if status != Pending::STATUS {
867 return env_err("$.outcome", format!("status {status} does not match outcome pending"));
868 }
869 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 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
889fn 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
904fn 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 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 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 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}