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, 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#[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 => (&["error"], &["fn_hash"]),
107 AnswerOutcome::Error => (&["error"], &["detail", "fn_hash"]),
109 AnswerOutcome::Pending => (&["estimate_seconds", "job", "poll", "request"], &[]),
110 }
111 }
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
116#[non_exhaustive]
117pub enum TrustLabel {
118 Primary,
120 Computed,
122 Verified,
124 Unverified,
126}
127
128impl TrustLabel {
129 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#[derive(Clone, Debug, PartialEq, Eq)]
152#[non_exhaustive]
153pub struct SlicePage {
154 pub slice: Slice,
156 pub members: Vec<Answer>,
158 pub next: Option<String>,
160}
161
162impl SlicePage {
163 pub fn new(slice: Slice, members: Vec<Answer>, next: Option<String>) -> SlicePage {
166 SlicePage { slice, members, next }
167 }
168}
169
170#[derive(Clone, Debug, PartialEq, Eq)]
177#[non_exhaustive]
178pub struct Answer {
179 pub outcome: AnswerOutcome,
181 pub receipt: Option<Receipt>,
183 pub trust: Option<TrustLabel>,
185 pub age_seconds: Option<u64>,
187 pub superseded_by: Option<String>,
189 pub reason: Option<String>,
191 pub quote: Option<Quote>,
193 pub retry_after_seconds: Option<u64>,
195 pub error: Option<ErrorCode>,
197 pub fault: Option<Fault>,
199 pub detail: Option<ForeignDetail>,
202 pub fn_hash: Option<String>,
204 pub pending: Option<Pending>,
206 pub page: Option<SlicePage>,
208 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 pub fn ok(receipt: Receipt, trust: TrustLabel) -> Answer {
251 Answer { receipt: Some(receipt), trust: Some(trust), ..bare(AnswerOutcome::Ok) }
252 }
253
254 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 pub fn not_covered() -> Answer {
266 bare(AnswerOutcome::NotCovered)
267 }
268
269 pub fn unlicensed() -> Answer {
271 bare(AnswerOutcome::Unlicensed)
272 }
273
274 pub fn payment_required(quote: Quote) -> Answer {
276 Answer { quote: Some(quote), ..bare(AnswerOutcome::PaymentRequired) }
277 }
278
279 pub fn rate_limited(retry_after_seconds: u64) -> Answer {
281 Answer { retry_after_seconds: Some(retry_after_seconds), ..bare(AnswerOutcome::RateLimited) }
282 }
283
284 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 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 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 pub fn pending(p: Pending) -> Answer {
322 Answer { pending: Some(p), ..bare(AnswerOutcome::Pending) }
323 }
324
325 pub fn page(page: SlicePage) -> Answer {
327 Answer { page: Some(page), ..bare(AnswerOutcome::Ok) }
328 }
329
330 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 pub fn with_fault(self, fault: Fault) -> Answer {
342 Answer { fault: Some(fault), ..self }
343 }
344
345 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 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 pub fn with_status(self, status: u16) -> Answer {
361 Answer { status, ..self }
362 }
363
364 pub fn status(&self) -> u16 {
366 self.status
367 }
368
369 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 pub fn to_json(&self) -> Vec<u8> {
429 self.to_value().to_jcs()
430 }
431
432 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
473enum 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
503pub 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#[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 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 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 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 pub fn with_after(self, after: BTreeMap<String, Scalar>) -> SliceRequest {
550 SliceRequest { after: Some(after), ..self }
551 }
552
553 pub fn slice_id(&self) -> &str {
555 &self.slice_id
556 }
557
558 pub fn asof(&self) -> Option<&str> {
560 self.asof.as_deref()
561 }
562
563 pub fn fn_hash(&self) -> Option<&str> {
565 self.pin.as_deref()
566 }
567
568 pub fn mesh(&self) -> Option<&str> {
570 self.mesh.as_deref()
571 }
572
573 pub fn after(&self) -> Option<&BTreeMap<String, Scalar>> {
575 self.after.as_ref()
576 }
577}
578
579pub 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#[derive(Clone, Debug, PartialEq, Eq)]
590#[non_exhaustive]
591pub enum Batch {
592 Items(Vec<Result<Answer>>),
594 Refused(Box<Answer>),
597}
598
599pub 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
651pub 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
675fn 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 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 if !outcome.status_ok(status) {
706 return env_err("$.outcome", format!("status {status} does not match outcome {oname}"));
707 }
708 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 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 if let Some(rv) = schema::opt(m, "receipt") {
722 a.receipt = Some(member_receipt(rv, keys, "$.receipt")?);
723 }
724 if let Ctx::Cell(req) = ctx {
726 bind_cell(&a, req)?;
727 }
728 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
742fn 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 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
825fn 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
839fn 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
854fn 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 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 if status != Pending::STATUS {
909 return env_err("$.outcome", format!("status {status} does not match outcome pending"));
910 }
911 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 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
931fn 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
946fn 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 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 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 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}