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)]
34pub struct Receipt {
35 asof: String,
36 cell: Cell,
37 computed_by: String,
38 inputs: Vec<String>,
39 issued: String,
40 licence: String,
41 publisher: String,
42 sig: String,
43 summary: String,
44 tx: String,
45 value: Scalar,
46 valid: Args,
47 payload: Vec<u8>,
48 hash: String,
49}
50
51impl Receipt {
52 pub fn from_json(bytes: &[u8]) -> Result<Receipt> {
54 Receipt::from_value(&json::parse(bytes)?)
55 }
56
57 pub fn from_value(v: &Value) -> Result<Receipt> {
59 json::check_value_depth(v)?;
60 structure(v)?;
61 let r = canonical(v)?;
62 r.consistency()?;
63 Ok(r)
64 }
65
66 pub fn payload(&self) -> &[u8] {
68 &self.payload
69 }
70
71 pub fn hash(&self) -> &str {
73 &self.hash
74 }
75
76 pub fn asof(&self) -> &str {
78 &self.asof
79 }
80
81 pub fn cell(&self) -> &Cell {
83 &self.cell
84 }
85
86 pub fn computed_by(&self) -> &str {
88 &self.computed_by
89 }
90
91 pub fn inputs(&self) -> &[String] {
93 &self.inputs
94 }
95
96 pub fn issued(&self) -> &str {
98 &self.issued
99 }
100
101 pub fn licence(&self) -> &str {
103 &self.licence
104 }
105
106 pub fn publisher(&self) -> &str {
108 &self.publisher
109 }
110
111 pub fn sig(&self) -> &str {
113 &self.sig
114 }
115
116 pub fn summary(&self) -> &str {
118 &self.summary
119 }
120
121 pub fn tx(&self) -> &str {
123 &self.tx
124 }
125
126 pub fn value(&self) -> &Scalar {
128 &self.value
129 }
130
131 pub fn valid(&self) -> &Args {
133 &self.valid
134 }
135
136 pub fn ty(&self) -> Type {
138 self.value.ty()
139 }
140
141 pub fn signer_host(&self) -> &str {
143 self.computed_by.split_once('#').map_or("", |(h, _)| h)
144 }
145
146 pub fn key_id(&self) -> &str {
148 self.computed_by.split_once('#').map_or("", |(_, k)| k)
149 }
150
151 #[cfg(feature = "sign")]
153 pub(crate) fn with_sig(mut self, sig: String) -> Receipt {
154 self.sig = sig;
155 self
156 }
157
158 fn consistency(&self) -> Result<()> {
159 for (k, a) in &self.cell.args {
160 if *a == Arg::Slice {
161 return err(ErrorCode::Slice, &member("$.cell.args", k), "a receipt is for one cell, not a slice");
162 }
163 }
164 for (k, a) in &self.valid {
165 if *a == Arg::Slice {
166 return err(ErrorCode::Slice, &member("$.valid", k), "a receipt is for one cell, not a slice");
167 }
168 }
169 if self.publisher != self.cell.ns {
170 return err(ErrorCode::PublisherMismatch, "$.publisher", "publisher must equal cell.ns");
171 }
172 for (k, a) in &self.valid {
173 if self.cell.args.get(k) != Some(a) {
174 return err(ErrorCode::ValidMismatch, "$.valid", format!("valid.{k} does not echo cell.args.{k}"));
175 }
176 if !a.scalar().is_some_and(|s| s.ty().may_be_valid_time()) {
177 return err(ErrorCode::ValidMismatch, "$.valid", "valid-time arguments are dates, instants or strings");
178 }
179 }
180 if !(self.tx <= self.asof && self.asof <= self.issued) {
181 return err(ErrorCode::TimeOrder, "$", "requires tx <= asof <= issued");
182 }
183 Ok(())
184 }
185}
186
187fn structure(v: &Value) -> Result<()> {
188 let m = schema::object(v, "$")?;
189 schema::exact(m, &MEMBERS, &[], "$", ErrorCode::Schema)?;
190 if schema::get(m, "proto").as_str() != Some("datumesh/1") {
191 return err(ErrorCode::Schema, "$.proto", "proto must be 'datumesh/1'");
192 }
193 for (k, val) in jcs_sorted(m) {
194 let p = member("$", k);
195 match k.as_str() {
196 "cell" => model::cell_structure(val, &p)?,
197 "valid" => model::args_structure(val, &p)?,
198 "inputs" => {
199 if !matches!(val, Value::Array(a) if a.iter().all(|x| matches!(x, Value::String(_)))) {
200 return err(ErrorCode::Schema, &p, "inputs must be an array of strings");
201 }
202 }
203 "value" => {
204 if !matches!(val, Value::String(_) | Value::Bool(_)) {
205 return err(ErrorCode::Schema, &p, "value must be a string or a boolean");
206 }
207 }
208 _ => {
209 if !matches!(val, Value::String(_)) {
210 return err(ErrorCode::Schema, &p, format!("{k} must be a string"));
211 }
212 }
213 }
214 }
215 Ok(())
216}
217
218fn canonical(v: &Value) -> Result<Receipt> {
219 let m = schema::object(v, "$")?;
220 let s = |name: &str| schema::string(m, name, "$");
221 let asof = s("asof")?;
222 lexical::check_instant(asof, "$.asof")?;
223 let cell = model::cell_canonical(schema::get(m, "cell"), "$.cell")?;
224 let computed_by = s("computed_by")?;
225 lexical::check_key_ref(computed_by, "$.computed_by")?;
226 let inputs: Vec<&str> = match schema::get(m, "inputs") {
227 Value::Array(a) => a.iter().filter_map(Value::as_str).collect(),
228 _ => Vec::new(),
229 };
230 for (i, h) in inputs.iter().enumerate() {
231 lexical::check_hex(h, 64, &index("$.inputs", i))?;
232 }
233 if inputs.windows(2).any(|w| w[0] >= w[1]) {
234 return err(ErrorCode::CanonSet, "$.inputs", "entries must be strictly ascending");
235 }
236 if inputs.len() > limits::INPUTS {
237 return err(ErrorCode::CanonLimit, "$.inputs", "more than 4096 inputs");
238 }
239 let issued = s("issued")?;
240 lexical::check_instant(issued, "$.issued")?;
241 let licence = s("licence")?;
242 lexical::check_licence(licence, "$.licence")?;
243 let publisher = s("publisher")?;
244 lexical::check_namespace(publisher, "$.publisher")?;
245 let sig = s("sig")?;
246 lexical::check_hex(sig, 128, "$.sig")?;
247 let summary = s("summary")?;
248 lexical::check_text(summary, limits::SUMMARY, false, "$.summary")?;
249 if summary.is_empty() {
250 return err(ErrorCode::CanonString, "$.summary", "summary must not be empty");
251 }
252 let tx = s("tx")?;
253 lexical::check_instant(tx, "$.tx")?;
254 let ty = Type::parse(s("type")?, "$.type")?;
255 let valid = model::args_canonical(schema::get(m, "valid"), "$.valid")?;
256 let value = match (ty, schema::get(m, "value")) {
257 (Type::Boolean, Value::Bool(b)) => Scalar::Boolean(*b),
258 (Type::Boolean, _) => return err(ErrorCode::CanonType, "$.value", "boolean value must be JSON true or false"),
259 (t, Value::String(text)) => {
260 model::check_lexical(t, text, "$.value")?;
261 model::unchecked(t, text)
262 }
263 (t, _) => return err(ErrorCode::CanonType, "$.value", format!("{} value must be a JSON string", t.as_str())),
264 };
265 let without_sig: Vec<(String, Value)> = m.iter().filter(|(k, _)| k != "sig").cloned().collect();
266 let payload = Value::Object(without_sig).to_jcs();
267 let hash = crate::crypto::sha256_hex(&payload);
268 Ok(Receipt {
269 asof: asof.into(),
270 cell,
271 computed_by: computed_by.into(),
272 inputs: inputs.iter().map(|x| x.to_string()).collect(),
273 issued: issued.into(),
274 licence: licence.into(),
275 publisher: publisher.into(),
276 sig: sig.into(),
277 summary: summary.into(),
278 tx: tx.into(),
279 value,
280 valid,
281 payload,
282 hash,
283 })
284}
285
286pub fn receipt_payload(bytes: &[u8]) -> Result<Vec<u8>> {
288 Ok(Receipt::from_json(bytes)?.payload)
289}
290
291pub fn receipt_hash(bytes: &[u8]) -> Result<String> {
293 Ok(Receipt::from_json(bytes)?.hash)
294}