Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_sbj/src/post.rs

38.3 KiB, 3 runs

created by r1870400018:22239, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! `daimond/post/0` — a signed message payload.
2//!
3//! The second schema this container carries, and the first that is not a node tree. An oxeweb
4//! document is a tree because a document is one; a message is a record with five fields, so its
5//! payload is a single canonical map and the whole of §4 does not apply to it.
6//!
7//! Everything the sender means is inside the payload, which is what the envelope's `hash` covers
8//! and therefore what the signature commits to. There is deliberately no `from` field: the author
9//! is the envelope's `author`, so there is no second place to say who wrote it and no spoofable
10//! name to disagree with the key. For the same reason there is no `created` field here — a
11//! timestamp is the envelope's, advisory, and a payload that carried its own would be asserting a
12//! clock nobody can check.
13//!
14//! The canonical rules of `SPEC.md` §3 apply unchanged, and are what makes one message one
15//! address: fixed widths, a `Dat::Map` rather than an `OrdMap`, lowercase ASCII keys, absent
16//! optional fields omitted rather than encoded as `none`, and [`decode`] re-encoding what it
17//! decoded and demanding the same bytes back.
18//!
19//! `body` is a `BU32` and never a `BU8`. A `BU8` carries a single length byte and so truncates
20//! silently past 255 bytes, which for a message body is a defect that would only appear once
21//! somebody wrote a long one.
22
23use crate::{
24 canon,
25 limit as sbj_limit,
26};
27
28use oxedyne_fe2o3_core::prelude::*;
29use oxedyne_fe2o3_jdat::{
30 prelude::*,
31 bdat::DecodeLimits,
32};
33
34
35// ┌───────────────────────────────────────────────────────────────────────────┐
36// │ KEYS │
37// └───────────────────────────────────────────────────────────────────────────┘
38
39/// The message text.
40pub const KEY_BODY: &'static str = "body";
41/// Per-message randomness, so that two identical messages are two addresses.
42pub const KEY_NONCE: &'static str = "nonce";
43/// References, at most [`limit::REFS`] of them.
44pub const KEY_REFS: &'static str = "refs";
45/// The address this message answers.
46pub const KEY_REPLY_TO: &'static str = "reply_to";
47/// The recipient's public key.
48pub const KEY_TO: &'static str = "to";
49
50/// A reference's own description, drawn only when resolution fails.
51pub const KEY_FALLBACK: &'static str = "fallback";
52/// A reference's referent: a map with exactly one entry, whose key selects the kind.
53pub const KEY_TARGET: &'static str = "target";
54
55/// A proposal on a forge repository.
56pub const REF_PROPOSAL: &'static str = "proposal";
57/// A release build.
58pub const REF_BUILD: &'static str = "build";
59/// A panel in the reader's own client.
60pub const REF_PANEL: &'static str = "panel";
61/// A page of the in-app guide.
62pub const REF_GUIDE: &'static str = "guide";
63
64/// A proposal's account.
65pub const KEY_ACCOUNT: &'static str = "account";
66/// A proposal's repository.
67pub const KEY_REPO: &'static str = "repo";
68/// A proposal's number.
69pub const KEY_NUMBER: &'static str = "number";
70/// A build's identifier.
71pub const KEY_ID: &'static str = "id";
72/// A panel's name.
73pub const KEY_NAME: &'static str = "name";
74/// A guide page.
75pub const KEY_PAGE: &'static str = "page";
76/// An anchor within a guide page.
77pub const KEY_ANCHOR: &'static str = "anchor";
78
79
80/// Limits this schema enforces. Every one is a rejection, never a truncation.
81pub mod limit {
82 /// The most a message body may carry, in bytes of UTF-8.
83 ///
84 /// A message is prose a person reads in a panel, not a document; anything longer wants to be a
85 /// document, which this container already has a schema for. The number is revisable on
86 /// evidence, as `SPEC.md` §5's are; what is fixed is that there is one, since a body with no
87 /// ceiling is a body that sets the relay's storage.
88 pub const BODY_BYTES: usize = 8 * 1024;
89 /// The most references one message may carry.
90 ///
91 /// Four, because a reference is resolved lazily by the reader and each resolution is metered
92 /// against that reader's own allowance. A message that could carry fifty would let a sender
93 /// spend a stranger's quota by being opened.
94 pub const REFS: usize = 4;
95 /// The exact width of the per-message nonce.
96 pub const NONCE_BYTES: usize = 16;
97 /// The exact width of a public key.
98 pub const KEY_BYTES: usize = 32;
99 /// The exact width of an address, matching the v0 hash scheme's digest.
100 pub const ADDR_BYTES: usize = 32;
101 /// The most a reference's fallback description may carry, in bytes of UTF-8.
102 pub const FALLBACK_BYTES: usize = 256;
103 /// The most any single reference field may carry, in bytes of UTF-8.
104 pub const FIELD_BYTES: usize = 128;
105 /// Decoding depth for a payload of this schema.
106 ///
107 /// A post is a flat record holding one list of maps, so three levels is already more than its
108 /// shape can reach. It is far below `SPEC.md` §5's tree limit because nothing here recurses,
109 /// and a limit set to what the shape needs refuses a nested value before it is looked at.
110 pub const DEPTH: usize = 8;
111}
112
113
114// ┌───────────────────────────────────────────────────────────────────────────┐
115// │ REFERENCES │
116// └───────────────────────────────────────────────────────────────────────────┘
117
118/// What a reference points at.
119///
120/// An enum rather than a string and a bag of fields, because the four referents have four identity
121/// schemes: a proposal is named by three parts, a build by one opaque id, a panel by a name the
122/// reader's own client resolves, and a guide page by a page and an optional anchor. A single
123/// "target string" would put the parsing in the renderer, which is where a malformed reference
124/// becomes a drawing bug rather than a rejection.
125///
126/// All four are **public anchors**: globally named, resolvable by anybody holding a session, and
127/// safe to send. Private, device-local pointers — a chat, a workspace file — are deliberately
128/// absent, because the other party cannot dereference one and an interface must never draw a
129/// pressable chip that will always fail.
130#[derive(Clone, Debug, PartialEq, Eq)]
131pub enum Target {
132 /// A proposal on a forge repository.
133 Proposal {
134 /// The owning account.
135 account: String,
136 /// The repository.
137 repo: String,
138 /// The proposal number.
139 number: u32,
140 },
141 /// A release build, by the identifier a release stamp carries.
142 Build {
143 /// The build identifier.
144 id: String,
145 },
146 /// A panel in the reader's own client. A surface, not an object.
147 Panel {
148 /// The panel's name.
149 name: String,
150 },
151 /// A page of the in-app guide, and optionally an anchor within it.
152 Guide {
153 /// The page.
154 page: String,
155 /// An anchor within the page.
156 anchor: Option<String>,
157 },
158}
159
160impl Target {
161 /// The key that selects this kind in the encoded form.
162 pub fn key(&self) -> &'static str {
163 match self {
164 Self::Proposal { .. } => REF_PROPOSAL,
165 Self::Build { .. } => REF_BUILD,
166 Self::Panel { .. } => REF_PANEL,
167 Self::Guide { .. } => REF_GUIDE,
168 }
169 }
170}
171
172/// One reference: what it points at, and what to say when that cannot be resolved.
173///
174/// The wire carries the referent and a fallback description, and **never a rendered title**. A
175/// sender-supplied title is a lie waiting to happen, since a proposal can be renamed or closed
176/// after the message is signed, and it is an injection surface besides — arbitrary sender text
177/// drawn as though it were a forge record. The reader resolves the referent itself and draws the
178/// fallback only on failure, as plain text, framed as the sender's own description of it.
179#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct Reference {
181 /// What is pointed at.
182 pub target: Target,
183 /// The sender's description, drawn only when resolution fails.
184 pub fallback: String,
185}
186
187impl Reference {
188 /// Encodes this reference as a canonical daticle.
189 pub fn to_dat(&self) -> Outcome<Dat> {
190 let mut inner = DaticleMap::new();
191 match &self.target {
192 Target::Proposal { account, repo, number } => {
193 inner.insert(dat!(KEY_ACCOUNT), Dat::Str(account.clone()));
194 inner.insert(dat!(KEY_NUMBER), Dat::U32(*number));
195 inner.insert(dat!(KEY_REPO), Dat::Str(repo.clone()));
196 },
197 Target::Build { id } => {
198 inner.insert(dat!(KEY_ID), Dat::Str(id.clone()));
199 },
200 Target::Panel { name } => {
201 inner.insert(dat!(KEY_NAME), Dat::Str(name.clone()));
202 },
203 Target::Guide { page, anchor } => {
204 // An absent anchor is OMITTED, never encoded as `none`: SPEC.md §3 rule 4, so that
205 // one reference has one encoding.
206 if let Some(a) = anchor {
207 inner.insert(dat!(KEY_ANCHOR), Dat::Str(a.clone()));
208 }
209 inner.insert(dat!(KEY_PAGE), Dat::Str(page.clone()));
210 },
211 }
212 let mut target = DaticleMap::new();
213 target.insert(dat!(self.target.key()), Dat::Map(inner));
214
215 let mut map = DaticleMap::new();
216 map.insert(dat!(KEY_FALLBACK), Dat::Str(self.fallback.clone()));
217 map.insert(dat!(KEY_TARGET), Dat::Map(target));
218 Ok(Dat::Map(map))
219 }
220
221 /// Reads a reference, refusing anything this schema does not admit.
222 pub fn from_dat(d: &Dat) -> Outcome<Self> {
223 let map = match d {
224 Dat::Map(m) => m,
225 other => return Err(err!(
226 "A reference must be a Dat::Map, found a {:?}.", other.kind();
227 Invalid, Input, Mismatch)),
228 };
229 if map.len() != 2 {
230 return Err(err!(
231 "A reference carries exactly the keys \"{}\" and \"{}\", found {} keys.",
232 KEY_FALLBACK, KEY_TARGET, map.len();
233 Invalid, Input));
234 }
235 let fallback = res!(get_str(map, KEY_FALLBACK));
236 res!(check_text(&fallback, KEY_FALLBACK, limit::FALLBACK_BYTES));
237
238 let target = match res!(get(map, KEY_TARGET)) {
239 Dat::Map(m) => m,
240 other => return Err(err!(
241 "A reference's \"{}\" must be a Dat::Map, found a {:?}.",
242 KEY_TARGET, other.kind();
243 Invalid, Input, Mismatch)),
244 };
245 // Exactly one entry, whose key selects the kind — the same rule, and for the same reason,
246 // as a link address in SPEC.md §4.3. A map with none is a reference to nothing; a map with
247 // two is a reference the reader would have to choose between.
248 if target.len() != 1 {
249 return Err(err!(
250 "A reference's \"{}\" is a map with exactly one entry, whose key names the kind. \
251 Found {} entries.", KEY_TARGET, target.len();
252 Invalid, Input));
253 }
254 let (kind_key, body) = match target.iter().next() {
255 Some((k, v)) => (k, v),
256 None => return Err(err!(
257 "A reference's \"{}\" is empty.", KEY_TARGET;
258 Invalid, Input, Missing)),
259 };
260 let kind = match kind_key {
261 Dat::Str(s) => s.clone(),
262 other => return Err(err!(
263 "A reference kind must be named by a string, found a {:?}.", other.kind();
264 Invalid, Input, Mismatch)),
265 };
266 let inner = match body {
267 Dat::Map(m) => m,
268 other => return Err(err!(
269 "The reference kind \"{}\" must carry a Dat::Map, found a {:?}.",
270 kind, other.kind();
271 Invalid, Input, Mismatch)),
272 };
273
274 let target = match kind.as_str() {
275 REF_PROPOSAL => {
276 res!(exact_keys(inner, &[KEY_ACCOUNT, KEY_NUMBER, KEY_REPO], REF_PROPOSAL));
277 let account = res!(get_str(inner, KEY_ACCOUNT));
278 let repo = res!(get_str(inner, KEY_REPO));
279 res!(check_text(&account, KEY_ACCOUNT, limit::FIELD_BYTES));
280 res!(check_text(&repo, KEY_REPO, limit::FIELD_BYTES));
281 Target::Proposal {
282 account,
283 repo,
284 number: res!(get_u32(inner, KEY_NUMBER)),
285 }
286 },
287 REF_BUILD => {
288 res!(exact_keys(inner, &[KEY_ID], REF_BUILD));
289 let id = res!(get_str(inner, KEY_ID));
290 res!(check_text(&id, KEY_ID, limit::FIELD_BYTES));
291 Target::Build { id }
292 },
293 REF_PANEL => {
294 res!(exact_keys(inner, &[KEY_NAME], REF_PANEL));
295 let name = res!(get_str(inner, KEY_NAME));
296 res!(check_text(&name, KEY_NAME, limit::FIELD_BYTES));
297 Target::Panel { name }
298 },
299 REF_GUIDE => {
300 // The anchor is optional, so the key set is checked against both admissible shapes
301 // rather than one.
302 let allowed: &[&str] = if inner.contains_key(&dat!(KEY_ANCHOR)) {
303 &[KEY_ANCHOR, KEY_PAGE]
304 } else {
305 &[KEY_PAGE]
306 };
307 res!(exact_keys(inner, allowed, REF_GUIDE));
308 let page = res!(get_str(inner, KEY_PAGE));
309 res!(check_text(&page, KEY_PAGE, limit::FIELD_BYTES));
310 let anchor = match inner.get(&dat!(KEY_ANCHOR)) {
311 Some(Dat::Str(a)) => {
312 res!(check_text(a, KEY_ANCHOR, limit::FIELD_BYTES));
313 Some(a.clone())
314 },
315 Some(other) => return Err(err!(
316 "A guide reference's \"{}\" must be a string, found a {:?}.",
317 KEY_ANCHOR, other.kind();
318 Invalid, Input, Mismatch)),
319 None => None,
320 };
321 Target::Guide { page, anchor }
322 },
323 other => return Err(err!(
324 "\"{}\" is not a reference kind this schema admits. The four are \"{}\", \"{}\", \
325 \"{}\" and \"{}\". A private, device-local pointer is deliberately not among them: \
326 the other party cannot resolve one, and a chip that will always fail must not be \
327 drawn.", other, REF_PROPOSAL, REF_BUILD, REF_PANEL, REF_GUIDE;
328 Invalid, Input, Unknown)),
329 };
330 Ok(Self { target, fallback })
331 }
332}
333
334
335// ┌───────────────────────────────────────────────────────────────────────────┐
336// │ THE POST │
337// └───────────────────────────────────────────────────────────────────────────┘
338
339/// A `daimond/post/0` payload.
340///
341/// Every field here is inside the tree region, so every field is covered by the envelope's `hash`
342/// and therefore by its signature. A relay handling this artefact can add nothing to it, remove
343/// nothing from it, and rewrite nothing in it without the signature ceasing to verify.
344#[derive(Clone, Debug, PartialEq, Eq)]
345pub struct Post {
346 /// The message text. Prose, not markup.
347 pub body: String,
348 /// The recipient's public key.
349 pub to: Vec<u8>,
350 /// Per-message randomness.
351 ///
352 /// Signed, so that a replay cannot mint a fresh one, and present so that two identical bodies
353 /// sent to one recipient are two distinct addresses rather than one message that appears to
354 /// have been sent once.
355 pub nonce: Vec<u8>,
356 /// The address this message answers, if it answers one.
357 pub reply_to: Option<Vec<u8>>,
358 /// References, at most [`limit::REFS`].
359 pub refs: Vec<Reference>,
360}
361
362impl Post {
363 /// Encodes this post as a canonical daticle.
364 pub fn to_dat(&self) -> Outcome<Dat> {
365 let mut map = DaticleMap::new();
366 map.insert(dat!(KEY_BODY), Dat::BU32(self.body.as_bytes().to_vec()));
367 map.insert(dat!(KEY_NONCE), Dat::BU8(self.nonce.clone()));
368 // An empty list and an absent one would be two encodings of one message, and so two
369 // addresses: SPEC.md §3 rules 4 and 8. Omitted when empty, and `from_dat` refuses a list
370 // that is present and empty.
371 if !self.refs.is_empty() {
372 let mut list = Vec::with_capacity(self.refs.len());
373 for r in &self.refs {
374 list.push(res!(r.to_dat()));
375 }
376 map.insert(dat!(KEY_REFS), Dat::List(list));
377 }
378 if let Some(a) = &self.reply_to {
379 map.insert(dat!(KEY_REPLY_TO), Dat::BU8(a.clone()));
380 }
381 map.insert(dat!(KEY_TO), Dat::BU8(self.to.clone()));
382 Ok(Dat::Map(map))
383 }
384
385 /// Reads a post from a daticle, enforcing every rule this schema declares.
386 pub fn from_dat(d: &Dat) -> Outcome<Self> {
387 let map = match d {
388 Dat::Map(m) => m,
389 Dat::OrdMap(_) => return Err(err!(
390 "SPEC.md §3 rule 2: a post payload is a Dat::Map, never a Dat::OrdMap. An OrdMap \
391 follows the author's typing rather than the keys, so the same message would have \
392 as many addresses as there are orders to write it in.";
393 Invalid, Input, Mismatch)),
394 other => return Err(err!(
395 "A post payload must be a Dat::Map, found a {:?}.", other.kind();
396 Invalid, Input, Mismatch)),
397 };
398 // The key set is checked whole, both ways: a missing key is a message that does not say
399 // what it must, and an unknown key is a field somebody signed that no reader will draw.
400 let allowed: Vec<&str> = {
401 let mut v = vec![KEY_BODY, KEY_NONCE, KEY_TO];
402 if map.contains_key(&dat!(KEY_REFS)) { v.push(KEY_REFS); }
403 if map.contains_key(&dat!(KEY_REPLY_TO)) { v.push(KEY_REPLY_TO); }
404 v
405 };
406 res!(exact_keys(map, &allowed, "post"));
407
408 let body_bytes = match res!(get(map, KEY_BODY)) {
409 Dat::BU32(b) => b.clone(),
410 Dat::BU8(_) | Dat::BU16(_) | Dat::BU64(_) => return Err(err!(
411 "The post key \"{}\" must be a BU32. A narrower byte string truncates a long \
412 message silently, and a wider one is a second encoding of the same value.",
413 KEY_BODY;
414 Invalid, Input, Mismatch)),
415 other => return Err(err!(
416 "The post key \"{}\" must be a BU32, found a {:?}.", KEY_BODY, other.kind();
417 Invalid, Input, Mismatch)),
418 };
419 if body_bytes.len() > limit::BODY_BYTES {
420 return Err(err!(
421 "The message body is {} bytes, exceeding the limit of {}. It is refused rather \
422 than truncated: half a message is not a shorter message.",
423 body_bytes.len(), limit::BODY_BYTES;
424 Invalid, Input, LimitReached));
425 }
426 let body = match String::from_utf8(body_bytes) {
427 Ok(s) => s,
428 Err(e) => return Err(err!(
429 "The message body is not valid UTF-8: {}.", e;
430 Invalid, Input, Decode)),
431 };
432 // The body is carried as bytes, so `canon`'s string rules do not reach it and this schema
433 // applies them itself. Without that a message could hold two encodings of one text and so
434 // two addresses, which is exactly what SPEC.md §3 rule 5 exists to prevent.
435 res!(canon::check_string(&body));
436
437 let to = res!(get_bytes(map, KEY_TO, limit::KEY_BYTES));
438 let nonce = res!(get_bytes(map, KEY_NONCE, limit::NONCE_BYTES));
439
440 let reply_to = match map.get(&dat!(KEY_REPLY_TO)) {
441 Some(_) => Some(res!(get_bytes(map, KEY_REPLY_TO, limit::ADDR_BYTES))),
442 None => None,
443 };
444
445 let refs = match map.get(&dat!(KEY_REFS)) {
446 Some(Dat::List(items)) => {
447 if items.is_empty() {
448 return Err(err!(
449 "SPEC.md §3 rule 8: the post carries an empty \"{}\" list. A thing a \
450 reader would draw identically whether present or absent gives one message \
451 two encodings, and so two addresses. Omit the key.", KEY_REFS;
452 Invalid, Input));
453 }
454 if items.len() > limit::REFS {
455 return Err(err!(
456 "The post carries {} references, exceeding the limit of {}. Each is \
457 resolved lazily against the READER's own metered allowance, so a message \
458 that could carry many would spend a stranger's quota by being opened.",
459 items.len(), limit::REFS;
460 Invalid, Input, LimitReached));
461 }
462 let mut out = Vec::with_capacity(items.len());
463 for (i, item) in items.iter().enumerate() {
464 out.push(res!(Reference::from_dat(item).map_err(|e| err!(e,
465 "Reference {} of {} is not one this schema admits.", i, items.len();
466 Invalid, Input))));
467 }
468 out
469 },
470 Some(Dat::Vek(_)) => return Err(err!(
471 "SPEC.md §3 rule 7: \"{}\" is a Dat::List, never a Dat::Vek, even where every \
472 element shares a kind.", KEY_REFS;
473 Invalid, Input, Mismatch)),
474 Some(other) => return Err(err!(
475 "The post key \"{}\" must be a list, found a {:?}.", KEY_REFS, other.kind();
476 Invalid, Input, Mismatch)),
477 None => Vec::new(),
478 };
479
480 Ok(Self { body, to, nonce, reply_to, refs })
481 }
482
483 /// Encodes this post to the canonical bytes that become the tree region.
484 pub fn encode(&self) -> Outcome<Vec<u8>> {
485 let d = res!(self.to_dat());
486 // Read straight back, so that a post which cannot be decoded can never be signed. Signing
487 // bytes no reader will accept produces an artefact that is valid to its author and refused
488 // by everybody else, which is the worst of the failures available here.
489 res!(Self::from_dat(&d));
490 let bytes = res!(d.to_bytes(Vec::new()));
491 if bytes.len() > sbj_limit::TREE_BYTES {
492 return Err(err!(
493 "The encoded post is {} bytes, exceeding the tree region limit of {}.",
494 bytes.len(), sbj_limit::TREE_BYTES;
495 Invalid, Input, LimitReached));
496 }
497 Ok(bytes)
498 }
499
500 /// Decodes a post from the bytes of a tree region, which must be consumed exactly.
501 ///
502 /// The bytes are re-encoded and compared with what came in, which is what enforces the
503 /// byte-level rules a decoded value can no longer show: a duplicate key collapses into one
504 /// entry when BDAT builds its map, and a length written in more bytes than it needs decodes to
505 /// the same number. Both survive only in the bytes.
506 pub fn decode(buf: &[u8]) -> Outcome<Self> {
507 let lims = DecodeLimits::new(limit::DEPTH, sbj_limit::TREE_BYTES);
508 let (d, n) = res!(Dat::from_bytes_limited(buf, &lims));
509 if n != buf.len() {
510 return Err(err!(
511 "The post payload occupies {} of the {} bytes supplied, leaving {} trailing.",
512 n, buf.len(), buf.len() - n;
513 Invalid, Input, Decode));
514 }
515 let re = res!(d.to_bytes(Vec::new()));
516 if re != buf {
517 return Err(err!(
518 "The post payload is not in canonical form: it re-encodes to {} bytes against the \
519 {} supplied, so it carries a duplicate key, a non-minimal length, or a \
520 non-canonical map. See SPEC.md §3.", re.len(), buf.len();
521 Invalid, Input, Decode));
522 }
523 Self::from_dat(&d)
524 }
525}
526
527
528// ┌───────────────────────────────────────────────────────────────────────────┐
529// │ FIELD READERS │
530// └───────────────────────────────────────────────────────────────────────────┘
531
532/// Returns a required key's value, or an error naming the key that is missing.
533fn get<'a>(map: &'a DaticleMap, key: &str) -> Outcome<&'a Dat> {
534 match map.get(&dat!(key)) {
535 Some(d) => Ok(d),
536 None => Err(err!(
537 "The post is missing the required key \"{}\".", key;
538 Invalid, Input, Missing)),
539 }
540}
541
542/// Reads a required string key.
543fn get_str(map: &DaticleMap, key: &str) -> Outcome<String> {
544 match res!(get(map, key)) {
545 Dat::Str(s) => Ok(s.clone()),
546 other => Err(err!(
547 "The key \"{}\" must carry a string, found a {:?}.", key, other.kind();
548 Invalid, Input, Mismatch)),
549 }
550}
551
552/// Reads a required `u32` key, refusing any other width.
553fn get_u32(map: &DaticleMap, key: &str) -> Outcome<u32> {
554 match res!(get(map, key)) {
555 Dat::U32(n) => Ok(*n),
556 other => Err(err!(
557 "SPEC.md §3 rule 6: the key \"{}\" is declared a u32 and must be encoded as exactly \
558 that width, found a {:?}. A promoted or demoted integer gives one message two \
559 encodings.", key, other.kind();
560 Invalid, Input, Mismatch)),
561 }
562}
563
564/// Reads a required `BU8` key of an exact width.
565///
566/// The width is exact rather than bounded because every one of these is a key, a nonce or an
567/// address, and each has one size. A short one is not a smaller key; it is a different thing.
568fn get_bytes(map: &DaticleMap, key: &str, width: usize) -> Outcome<Vec<u8>> {
569 let b = match res!(get(map, key)) {
570 Dat::BU8(b) => b.clone(),
571 other => return Err(err!(
572 "The key \"{}\" must carry a BU8, found a {:?}.", key, other.kind();
573 Invalid, Input, Mismatch)),
574 };
575 if b.len() != width {
576 return Err(err!(
577 "The key \"{}\" carries {} bytes and must carry exactly {}.", key, b.len(), width;
578 Invalid, Input, Mismatch));
579 }
580 Ok(b)
581}
582
583/// Checks a string field against the canonical text rules and a byte ceiling.
584fn check_text(s: &str, key: &str, max: usize) -> Outcome<()> {
585 if s.len() > max {
586 return Err(err!(
587 "The field \"{}\" is {} bytes, exceeding the limit of {}.", key, s.len(), max;
588 Invalid, Input, LimitReached));
589 }
590 res!(canon::check_string(s));
591 Ok(())
592}
593
594/// Requires a map to carry exactly the named keys — no more, and no fewer.
595///
596/// Both directions, because they catch different faults. A missing key is a message that does not
597/// say something it must. An unknown key is a field the sender signed and no reader will ever
598/// draw, which is worse than useless: it is covered by the signature, so it looks like meaning.
599fn exact_keys(map: &DaticleMap, allowed: &[&str], what: &str) -> Outcome<()> {
600 for k in allowed {
601 if !map.contains_key(&dat!(*k)) {
602 return Err(err!(
603 "The {} is missing the required key \"{}\".", what, k;
604 Invalid, Input, Missing));
605 }
606 }
607 for k in map.keys() {
608 let name = match k {
609 Dat::Str(s) => s.clone(),
610 other => return Err(err!(
611 "SPEC.md §3 rule 3: a map key must be a string, found a {:?}.", other.kind();
612 Invalid, Input, Mismatch)),
613 };
614 res!(canon::check_key_string(&name));
615 if !allowed.iter().any(|a| *a == name.as_str()) {
616 return Err(err!(
617 "The {} carries the key \"{}\", which this schema does not admit. The admitted \
618 keys are: {}.", what, name, allowed.join(", ");
619 Invalid, Input, Unknown));
620 }
621 }
622 Ok(())
623}
624
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629
630 /// A plausible post, with fixed contents.
631 fn sample() -> Post {
632 Post {
633 body: fmt!("The crop is in, and the second field can wait."),
634 to: vec![0xA1; limit::KEY_BYTES],
635 nonce: vec![0xB2; limit::NONCE_BYTES],
636 reply_to: None,
637 refs: Vec::new(),
638 }
639 }
640
641 /// Every reference kind, once.
642 fn every_ref() -> Vec<Reference> {
643 vec![
644 Reference {
645 target: Target::Proposal {
646 account: fmt!("oxedyne"),
647 repo: fmt!("daimond"),
648 number: 17,
649 },
650 fallback: fmt!("the proposal about the panel showing nothing when signed out"),
651 },
652 Reference {
653 target: Target::Build { id: fmt!("f9f68b75c73b") },
654 fallback: fmt!("the build this was fixed in"),
655 },
656 Reference {
657 target: Target::Panel { name: fmt!("spend") },
658 fallback: fmt!("the Spending panel"),
659 },
660 Reference {
661 target: Target::Guide {
662 page: fmt!("improve"),
663 anchor: Some(fmt!("voices")),
664 },
665 fallback: fmt!("the guide section on voices"),
666 },
667 ]
668 }
669
670 /// A reference kind this build does not know is REFUSED, and never quietly dropped.
671 ///
672 /// The property that decides what a future fifth kind costs. Refused, and the message that
673 /// carries it is refused whole: an old build meets a new reference and says so, which is a
674 /// clean cliff. Dropped, the same build would render a message with a reference missing from
675 /// it, show no sign that anything was removed, and hash to an address the sender never
676 /// computed — a message quietly saying less than its author signed.
677 ///
678 /// `daimond/share/0` is reserved as a SCHEMA rather than as a fifth kind here (see
679 /// `crate::SCHEMA_SHARE`), so nothing is expected to take this route. It is pinned anyway,
680 /// because the cost of the decision being revisited depends on it.
681 #[test]
682 fn test_an_unknown_reference_kind_is_refused_not_dropped() -> Outcome<()> {
683 let mut inner = DaticleMap::new();
684 inner.insert(dat!("id"), Dat::Str(fmt!("a-diamond")));
685 let mut target = DaticleMap::new();
686 target.insert(dat!("share"), Dat::Map(inner));
687 let mut r = DaticleMap::new();
688 r.insert(dat!(KEY_FALLBACK), Dat::Str(fmt!("a Diamond somebody sent")));
689 r.insert(dat!(KEY_TARGET), Dat::Map(target));
690
691 match Reference::from_dat(&Dat::Map(r.clone())) {
692 Ok(_) => return Err(err!(
693 "A reference of an unknown kind was read."; Test, Invalid)),
694 Err(e) => {
695 let msg = fmt!("{}", e);
696 assert!(msg.contains("share"), "The refusal does not name the kind: {}", msg);
697 assert!(msg.contains(REF_PROPOSAL), "The refusal does not say what is admitted.");
698 },
699 }
700
701 // And the message carrying it is refused WHOLE, rather than arriving with one reference
702 // fewer than its author signed.
703 let mut p = match res!(sample().to_dat()) {
704 Dat::Map(m) => m,
705 other => return Err(err!(
706 "A post encodes as a map, and this is a {:?}.", other.kind(); Test, Bug)),
707 };
708 p.insert(dat!(KEY_REFS), Dat::List(vec![Dat::Map(r)]));
709 match Post::from_dat(&Dat::Map(p)) {
710 Ok(_) => Err(err!(
711 "A post carrying an unknown reference kind was read, so the reference was dropped \
712 and the message says less than its author signed."; Test, Invalid)),
713 Err(_) => Ok(()),
714 }
715 }
716
717 #[test]
718 fn test_round_trip_minimal() -> Outcome<()> {
719 let p = sample();
720 let bytes = res!(p.encode());
721 let back = res!(Post::decode(&bytes));
722 assert_eq!(p, back);
723 Ok(())
724 }
725
726 #[test]
727 fn test_round_trip_every_field() -> Outcome<()> {
728 let mut p = sample();
729 p.reply_to = Some(vec![0xC3; limit::ADDR_BYTES]);
730 p.refs = every_ref();
731 let bytes = res!(p.encode());
732 let back = res!(Post::decode(&bytes));
733 assert_eq!(p, back);
734 assert_eq!(back.refs.len(), 4);
735 Ok(())
736 }
737
738 /// A guide reference without an anchor must not encode the absence as `none`.
739 #[test]
740 fn test_guide_anchor_is_omitted_not_none() -> Outcome<()> {
741 let mut p = sample();
742 p.refs = vec![Reference {
743 target: Target::Guide { page: fmt!("improve"), anchor: None },
744 fallback: fmt!("the guide"),
745 }];
746 let bytes = res!(p.encode());
747 let back = res!(Post::decode(&bytes));
748 assert_eq!(p, back);
749 // The two shapes must be different bytes, or the anchor is not carrying meaning.
750 let mut q = p.clone();
751 q.refs = vec![Reference {
752 target: Target::Guide { page: fmt!("improve"), anchor: Some(fmt!("voices")) },
753 fallback: fmt!("the guide"),
754 }];
755 assert_ne!(res!(q.encode()), bytes);
756 Ok(())
757 }
758
759 #[test]
760 fn test_trailing_bytes_refused() -> Outcome<()> {
761 let mut bytes = res!(sample().encode());
762 bytes.push(0x00);
763 match Post::decode(&bytes) {
764 Ok(_) => Err(err!("A payload with a trailing byte was accepted."; Test, Invalid)),
765 Err(_) => Ok(()),
766 }
767 }
768
769 /// The body is a BU32 because a BU8 truncates past 255 bytes.
770 #[test]
771 fn test_body_must_be_bu32() -> Outcome<()> {
772 let mut map = DaticleMap::new();
773 map.insert(dat!(KEY_BODY), Dat::BU8(b"short enough to fit".to_vec()));
774 map.insert(dat!(KEY_NONCE), Dat::BU8(vec![0xB2; limit::NONCE_BYTES]));
775 map.insert(dat!(KEY_TO), Dat::BU8(vec![0xA1; limit::KEY_BYTES]));
776 match Post::from_dat(&Dat::Map(map)) {
777 Ok(_) => Err(err!("A body encoded as a BU8 was accepted."; Test, Invalid)),
778 Err(_) => Ok(()),
779 }
780 }
781
782 /// A body longer than the ceiling is refused, not truncated.
783 #[test]
784 fn test_body_over_the_limit_refused() -> Outcome<()> {
785 let mut p = sample();
786 p.body = "a".repeat(limit::BODY_BYTES + 1);
787 match p.encode() {
788 Ok(_) => Err(err!("An oversized body was accepted."; Test, Invalid)),
789 Err(_) => Ok(()),
790 }
791 }
792
793 /// A body at exactly the ceiling is accepted, so the limit is a boundary and not a scare.
794 #[test]
795 fn test_body_at_the_limit_accepted() -> Outcome<()> {
796 let mut p = sample();
797 p.body = "a".repeat(limit::BODY_BYTES);
798 let bytes = res!(p.encode());
799 assert_eq!(res!(Post::decode(&bytes)).body.len(), limit::BODY_BYTES);
800 Ok(())
801 }
802
803 /// Text that displays identically must encode identically, or one message has two addresses.
804 #[test]
805 fn test_body_not_nfc_refused() -> Outcome<()> {
806 let mut p = sample();
807 p.body = fmt!("cafe\u{0301}"); // e + combining acute, not the composed form
808 match p.encode() {
809 Ok(_) => Err(err!("A body that is not in NFC was accepted."; Test, Invalid)),
810 Err(_) => Ok(()),
811 }
812 }
813
814 #[test]
815 fn test_body_control_character_refused() -> Outcome<()> {
816 let mut p = sample();
817 p.body = fmt!("before\u{0}after");
818 match p.encode() {
819 Ok(_) => Err(err!("A body carrying a NUL was accepted."; Test, Invalid)),
820 Err(_) => Ok(()),
821 }
822 }
823
824 /// A carriage return is refused so that one line ending has one encoding.
825 #[test]
826 fn test_body_carriage_return_refused() -> Outcome<()> {
827 let mut p = sample();
828 p.body = fmt!("one\r\ntwo");
829 match p.encode() {
830 Ok(_) => Err(err!("A body carrying a carriage return was accepted."; Test, Invalid)),
831 Err(_) => Ok(()),
832 }
833 }
834
835 /// A tab and a newline are ordinary text and must survive.
836 #[test]
837 fn test_body_tab_and_newline_accepted() -> Outcome<()> {
838 let mut p = sample();
839 p.body = fmt!("one\ttwo\nthree");
840 let bytes = res!(p.encode());
841 assert_eq!(res!(Post::decode(&bytes)).body, p.body);
842 Ok(())
843 }
844
845 #[test]
846 fn test_nonce_must_be_exact_width() -> Outcome<()> {
847 let mut p = sample();
848 p.nonce = vec![0xB2; limit::NONCE_BYTES - 1];
849 match p.encode() {
850 Ok(_) => Err(err!("A short nonce was accepted."; Test, Invalid)),
851 Err(_) => Ok(()),
852 }
853 }
854
855 #[test]
856 fn test_recipient_key_must_be_exact_width() -> Outcome<()> {
857 let mut p = sample();
858 p.to = vec![0xA1; limit::KEY_BYTES + 1];
859 match p.encode() {
860 Ok(_) => Err(err!("An overlong recipient key was accepted."; Test, Invalid)),
861 Err(_) => Ok(()),
862 }
863 }
864
865 /// An empty list and an absent one would be two encodings of one message.
866 #[test]
867 fn test_empty_refs_list_refused() -> Outcome<()> {
868 let mut map = DaticleMap::new();
869 map.insert(dat!(KEY_BODY), Dat::BU32(b"hello".to_vec()));
870 map.insert(dat!(KEY_NONCE), Dat::BU8(vec![0xB2; limit::NONCE_BYTES]));
871 map.insert(dat!(KEY_REFS), Dat::List(Vec::new()));
872 map.insert(dat!(KEY_TO), Dat::BU8(vec![0xA1; limit::KEY_BYTES]));
873 match Post::from_dat(&Dat::Map(map)) {
874 Ok(_) => Err(err!("An empty refs list was accepted."; Test, Invalid)),
875 Err(_) => Ok(()),
876 }
877 }
878
879 /// Omitting refs entirely is the correct encoding, and it must work.
880 #[test]
881 fn test_absent_refs_accepted() -> Outcome<()> {
882 let bytes = res!(sample().encode());
883 assert!(res!(Post::decode(&bytes)).refs.is_empty());
884 Ok(())
885 }
886
887 #[test]
888 fn test_five_references_refused() -> Outcome<()> {
889 let mut p = sample();
890 p.refs = every_ref();
891 p.refs.push(Reference {
892 target: Target::Panel { name: fmt!("work") },
893 fallback: fmt!("one too many"),
894 });
895 match p.encode() {
896 Ok(_) => Err(err!("Five references were accepted."; Test, Invalid)),
897 Err(_) => Ok(()),
898 }
899 }
900
901 #[test]
902 fn test_four_references_accepted() -> Outcome<()> {
903 let mut p = sample();
904 p.refs = every_ref();
905 assert_eq!(p.refs.len(), limit::REFS);
906 let bytes = res!(p.encode());
907 assert_eq!(res!(Post::decode(&bytes)).refs.len(), limit::REFS);
908 Ok(())
909 }
910
911 /// A private, device-local pointer is refused by name rather than drawn as a dead chip.
912 #[test]
913 fn test_unknown_reference_kind_refused() -> Outcome<()> {
914 let mut inner = DaticleMap::new();
915 inner.insert(dat!("id"), Dat::Str(fmt!("chat-14")));
916 let mut target = DaticleMap::new();
917 target.insert(dat!("chat"), Dat::Map(inner));
918 let mut r = DaticleMap::new();
919 r.insert(dat!(KEY_FALLBACK), Dat::Str(fmt!("that chat")));
920 r.insert(dat!(KEY_TARGET), Dat::Map(target));
921 match Reference::from_dat(&Dat::Map(r)) {
922 Ok(_) => Err(err!("A chat reference was accepted."; Test, Invalid)),
923 Err(_) => Ok(()),
924 }
925 }
926
927 /// A target naming two kinds is a reference the reader would have to choose between.
928 #[test]
929 fn test_target_with_two_entries_refused() -> Outcome<()> {
930 let mut one = DaticleMap::new();
931 one.insert(dat!(KEY_NAME), Dat::Str(fmt!("spend")));
932 let mut two = DaticleMap::new();
933 two.insert(dat!(KEY_ID), Dat::Str(fmt!("f9f68b75c73b")));
934 let mut target = DaticleMap::new();
935 target.insert(dat!(REF_PANEL), Dat::Map(one));
936 target.insert(dat!(REF_BUILD), Dat::Map(two));
937 let mut r = DaticleMap::new();
938 r.insert(dat!(KEY_FALLBACK), Dat::Str(fmt!("either of those")));
939 r.insert(dat!(KEY_TARGET), Dat::Map(target));
940 match Reference::from_dat(&Dat::Map(r)) {
941 Ok(_) => Err(err!("A target naming two kinds was accepted."; Test, Invalid)),
942 Err(_) => Ok(()),
943 }
944 }
945
946 /// A sender-supplied title has nowhere to go: an unadmitted key is refused.
947 #[test]
948 fn test_sender_supplied_title_refused() -> Outcome<()> {
949 let mut inner = DaticleMap::new();
950 inner.insert(dat!(KEY_ID), Dat::Str(fmt!("f9f68b75c73b")));
951 inner.insert(dat!("title"), Dat::Str(fmt!("Fixed everything, click here")));
952 let mut target = DaticleMap::new();
953 target.insert(dat!(REF_BUILD), Dat::Map(inner));
954 let mut r = DaticleMap::new();
955 r.insert(dat!(KEY_FALLBACK), Dat::Str(fmt!("a build")));
956 r.insert(dat!(KEY_TARGET), Dat::Map(target));
957 match Reference::from_dat(&Dat::Map(r)) {
958 Ok(_) => Err(err!("A sender-supplied title was accepted."; Test, Invalid)),
959 Err(_) => Ok(()),
960 }
961 }
962
963 #[test]
964 fn test_unknown_post_key_refused() -> Outcome<()> {
965 let mut map = DaticleMap::new();
966 map.insert(dat!(KEY_BODY), Dat::BU32(b"hello".to_vec()));
967 map.insert(dat!(KEY_NONCE), Dat::BU8(vec![0xB2; limit::NONCE_BYTES]));
968 map.insert(dat!(KEY_TO), Dat::BU8(vec![0xA1; limit::KEY_BYTES]));
969 map.insert(dat!("from"), Dat::Str(fmt!("somebody else")));
970 match Post::from_dat(&Dat::Map(map)) {
971 Ok(_) => Err(err!("A post carrying a `from` field was accepted."; Test, Invalid)),
972 Err(_) => Ok(()),
973 }
974 }
975
976 #[test]
977 fn test_missing_key_refused() -> Outcome<()> {
978 let mut map = DaticleMap::new();
979 map.insert(dat!(KEY_BODY), Dat::BU32(b"hello".to_vec()));
980 map.insert(dat!(KEY_TO), Dat::BU8(vec![0xA1; limit::KEY_BYTES]));
981 match Post::from_dat(&Dat::Map(map)) {
982 Ok(_) => Err(err!("A post with no nonce was accepted."; Test, Invalid)),
983 Err(_) => Ok(()),
984 }
985 }
986
987 #[test]
988 fn test_ordmap_refused() -> Outcome<()> {
989 let ord = oxedyne_fe2o3_jdat::map::create_dat_ordmap(vec![
990 (dat!(KEY_BODY), Dat::BU32(b"hello".to_vec())),
991 (dat!(KEY_NONCE), Dat::BU8(vec![0xB2; limit::NONCE_BYTES])),
992 (dat!(KEY_TO), Dat::BU8(vec![0xA1; limit::KEY_BYTES])),
993 ]);
994 match Post::from_dat(&ord) {
995 Ok(_) => Err(err!("An OrdMap payload was accepted."; Test, Invalid)),
996 Err(_) => Ok(()),
997 }
998 }
999
1000 /// The proposal number is declared a u32 and must be encoded as exactly that width.
1001 #[test]
1002 fn test_proposal_number_width_is_fixed() -> Outcome<()> {
1003 let mut inner = DaticleMap::new();
1004 inner.insert(dat!(KEY_ACCOUNT), Dat::Str(fmt!("oxedyne")));
1005 inner.insert(dat!(KEY_NUMBER), Dat::U16(17)); // declared u32
1006 inner.insert(dat!(KEY_REPO), Dat::Str(fmt!("daimond")));
1007 let mut target = DaticleMap::new();
1008 target.insert(dat!(REF_PROPOSAL), Dat::Map(inner));
1009 let mut r = DaticleMap::new();
1010 r.insert(dat!(KEY_FALLBACK), Dat::Str(fmt!("a proposal")));
1011 r.insert(dat!(KEY_TARGET), Dat::Map(target));
1012 match Reference::from_dat(&Dat::Map(r)) {
1013 Ok(_) => Err(err!("A demoted integer width was accepted."; Test, Invalid)),
1014 Err(_) => Ok(()),
1015 }
1016 }
1017
1018 /// Two identical bodies to one recipient are two addresses, because the nonce is signed.
1019 #[test]
1020 fn test_the_nonce_separates_identical_messages() -> Outcome<()> {
1021 let a = sample();
1022 let mut b = sample();
1023 b.nonce = vec![0xB3; limit::NONCE_BYTES];
1024 assert_eq!(a.body, b.body);
1025 assert_ne!(res!(a.encode()), res!(b.encode()));
1026 Ok(())
1027 }
1028}