1use crate::error::{err, index, member, ErrorCode, Result};
4use crate::json::{self, jcs_sorted, Value};
5use crate::lexical::{self, limits};
6use crate::model::{self, Arg, Args, Cell, Scalar, Type};
7use crate::schema;
8
9const MEMBERS: [&str; 14] = [
10 "asof",
11 "cell",
12 "computed_by",
13 "inputs",
14 "issued",
15 "licence",
16 "proto",
17 "publisher",
18 "sig",
19 "summary",
20 "tx",
21 "type",
22 "valid",
23 "value",
24];
25
26#[derive(Clone, Debug, PartialEq, Eq, Hash)]
29pub struct Receipt {
30 pub asof: String,
32 pub cell: Cell,
34 pub computed_by: String,
36 pub inputs: Vec<String>,
38 pub issued: String,
40 pub licence: String,
42 pub publisher: String,
44 pub sig: String,
46 pub summary: String,
48 pub tx: String,
50 pub value: Scalar,
52 pub valid: Args,
54 payload: Vec<u8>,
55}
56
57impl Receipt {
58 pub fn from_json(bytes: &[u8]) -> Result<Receipt> {
60 Receipt::from_value(&json::parse(bytes)?)
61 }
62
63 pub fn from_value(v: &Value) -> Result<Receipt> {
65 json::check_value_depth(v)?;
66 structure(v)?;
67 let r = canonical(v)?;
68 r.consistency()?;
69 Ok(r)
70 }
71
72 pub fn payload(&self) -> &[u8] {
74 &self.payload
75 }
76
77 pub fn hash(&self) -> String {
79 crate::crypto::sha256_hex(&self.payload)
80 }
81
82 pub fn ty(&self) -> Type {
84 self.value.ty()
85 }
86
87 pub fn signer_host(&self) -> &str {
89 self.computed_by.split_once('#').map_or("", |(h, _)| h)
90 }
91
92 pub fn key_id(&self) -> &str {
94 self.computed_by.split_once('#').map_or("", |(_, k)| k)
95 }
96
97 fn consistency(&self) -> Result<()> {
98 for (k, a) in &self.cell.args {
99 if *a == Arg::Slice {
100 return err(ErrorCode::Slice, &member("$.cell.args", k), "a receipt is for one cell, not a slice");
101 }
102 }
103 for (k, a) in &self.valid {
104 if *a == Arg::Slice {
105 return err(ErrorCode::Slice, &member("$.valid", k), "a receipt is for one cell, not a slice");
106 }
107 }
108 if self.publisher != self.cell.ns {
109 return err(ErrorCode::PublisherMismatch, "$.publisher", "publisher must equal cell.ns");
110 }
111 for (k, a) in &self.valid {
112 if self.cell.args.get(k) != Some(a) {
113 return err(ErrorCode::ValidMismatch, "$.valid", format!("valid.{k} does not echo cell.args.{k}"));
114 }
115 if !a.scalar().is_some_and(|s| s.ty().may_be_valid_time()) {
116 return err(ErrorCode::ValidMismatch, "$.valid", "valid-time arguments are dates, instants or strings");
117 }
118 }
119 if !(self.tx <= self.asof && self.asof <= self.issued) {
120 return err(ErrorCode::TimeOrder, "$", "requires tx <= asof <= issued");
121 }
122 Ok(())
123 }
124}
125
126fn structure(v: &Value) -> Result<()> {
127 let m = schema::object(v, "$")?;
128 schema::exact(m, &MEMBERS, &[], "$", ErrorCode::Schema)?;
129 if schema::get(m, "proto").as_str() != Some("datumesh/1") {
130 return err(ErrorCode::Schema, "$.proto", "proto must be 'datumesh/1'");
131 }
132 for (k, val) in jcs_sorted(m) {
133 let p = member("$", k);
134 match k.as_str() {
135 "cell" => model::cell_structure(val, &p)?,
136 "valid" => model::args_structure(val, &p)?,
137 "inputs" => {
138 if !matches!(val, Value::Array(a) if a.iter().all(|x| matches!(x, Value::String(_)))) {
139 return err(ErrorCode::Schema, &p, "inputs must be an array of strings");
140 }
141 }
142 "value" => {
143 if !matches!(val, Value::String(_) | Value::Bool(_)) {
144 return err(ErrorCode::Schema, &p, "value must be a string or a boolean");
145 }
146 }
147 _ => {
148 if !matches!(val, Value::String(_)) {
149 return err(ErrorCode::Schema, &p, format!("{k} must be a string"));
150 }
151 }
152 }
153 }
154 Ok(())
155}
156
157fn canonical(v: &Value) -> Result<Receipt> {
158 let m = schema::object(v, "$")?;
159 let s = |name: &str| schema::string(m, name, "$");
160 let asof = s("asof")?;
161 lexical::check_instant(asof, "$.asof")?;
162 let cell = model::cell_canonical(schema::get(m, "cell"), "$.cell")?;
163 let computed_by = s("computed_by")?;
164 lexical::check_key_ref(computed_by, "$.computed_by")?;
165 let inputs: Vec<&str> = match schema::get(m, "inputs") {
166 Value::Array(a) => a.iter().filter_map(Value::as_str).collect(),
167 _ => Vec::new(),
168 };
169 for (i, h) in inputs.iter().enumerate() {
170 lexical::check_hex(h, 64, &index("$.inputs", i))?;
171 }
172 if inputs.windows(2).any(|w| w[0] >= w[1]) {
173 return err(ErrorCode::CanonSet, "$.inputs", "entries must be strictly ascending");
174 }
175 if inputs.len() > limits::INPUTS {
176 return err(ErrorCode::CanonLimit, "$.inputs", "more than 4096 inputs");
177 }
178 let issued = s("issued")?;
179 lexical::check_instant(issued, "$.issued")?;
180 let licence = s("licence")?;
181 lexical::check_licence(licence, "$.licence")?;
182 let publisher = s("publisher")?;
183 lexical::check_namespace(publisher, "$.publisher")?;
184 let sig = s("sig")?;
185 lexical::check_hex(sig, 128, "$.sig")?;
186 let summary = s("summary")?;
187 lexical::check_text(summary, limits::SUMMARY, false, "$.summary")?;
188 if summary.is_empty() {
189 return err(ErrorCode::CanonString, "$.summary", "summary must not be empty");
190 }
191 let tx = s("tx")?;
192 lexical::check_instant(tx, "$.tx")?;
193 let ty = Type::parse(s("type")?, "$.type")?;
194 let valid = model::args_canonical(schema::get(m, "valid"), "$.valid")?;
195 let value = match (ty, schema::get(m, "value")) {
196 (Type::Boolean, Value::Bool(b)) => Scalar::Boolean(*b),
197 (Type::Boolean, _) => return err(ErrorCode::CanonType, "$.value", "boolean value must be JSON true or false"),
198 (t, Value::String(text)) => {
199 model::check_lexical(t, text, "$.value")?;
200 model::unchecked(t, text)
201 }
202 (t, _) => return err(ErrorCode::CanonType, "$.value", format!("{} value must be a JSON string", t.as_str())),
203 };
204 let without_sig: Vec<(String, Value)> = m.iter().filter(|(k, _)| k != "sig").cloned().collect();
205 let payload = Value::Object(without_sig).to_jcs();
206 Ok(Receipt {
207 asof: asof.into(),
208 cell,
209 computed_by: computed_by.into(),
210 inputs: inputs.iter().map(|x| x.to_string()).collect(),
211 issued: issued.into(),
212 licence: licence.into(),
213 publisher: publisher.into(),
214 sig: sig.into(),
215 summary: summary.into(),
216 tx: tx.into(),
217 value,
218 valid,
219 payload,
220 })
221}
222
223pub fn receipt_payload(bytes: &[u8]) -> Result<Vec<u8>> {
225 Ok(Receipt::from_json(bytes)?.payload)
226}
227
228pub fn receipt_hash(bytes: &[u8]) -> Result<String> {
230 Ok(Receipt::from_json(bytes)?.hash())
231}