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 | |
| 23 | use crate::{ |
| 24 | canon, |
| 25 | limit as sbj_limit, |
| 26 | }; |
| 27 | |
| 28 | use oxedyne_fe2o3_core::prelude::*; |
| 29 | use oxedyne_fe2o3_jdat::{ |
| 30 | prelude::*, |
| 31 | bdat::DecodeLimits, |
| 32 | }; |
| 33 | |
| 34 | |
| 35 | // ┌───────────────────────────────────────────────────────────────────────────┐ |
| 36 | // │ KEYS │ |
| 37 | // └───────────────────────────────────────────────────────────────────────────┘ |
| 38 | |
| 39 | /// The message text. |
| 40 | pub const KEY_BODY: &'static str = "body"; |
| 41 | /// Per-message randomness, so that two identical messages are two addresses. |
| 42 | pub const KEY_NONCE: &'static str = "nonce"; |
| 43 | /// References, at most [`limit::REFS`] of them. |
| 44 | pub const KEY_REFS: &'static str = "refs"; |
| 45 | /// The address this message answers. |
| 46 | pub const KEY_REPLY_TO: &'static str = "reply_to"; |
| 47 | /// The recipient's public key. |
| 48 | pub const KEY_TO: &'static str = "to"; |
| 49 | |
| 50 | /// A reference's own description, drawn only when resolution fails. |
| 51 | pub const KEY_FALLBACK: &'static str = "fallback"; |
| 52 | /// A reference's referent: a map with exactly one entry, whose key selects the kind. |
| 53 | pub const KEY_TARGET: &'static str = "target"; |
| 54 | |
| 55 | /// A proposal on a forge repository. |
| 56 | pub const REF_PROPOSAL: &'static str = "proposal"; |
| 57 | /// A release build. |
| 58 | pub const REF_BUILD: &'static str = "build"; |
| 59 | /// A panel in the reader's own client. |
| 60 | pub const REF_PANEL: &'static str = "panel"; |
| 61 | /// A page of the in-app guide. |
| 62 | pub const REF_GUIDE: &'static str = "guide"; |
| 63 | |
| 64 | /// A proposal's account. |
| 65 | pub const KEY_ACCOUNT: &'static str = "account"; |
| 66 | /// A proposal's repository. |
| 67 | pub const KEY_REPO: &'static str = "repo"; |
| 68 | /// A proposal's number. |
| 69 | pub const KEY_NUMBER: &'static str = "number"; |
| 70 | /// A build's identifier. |
| 71 | pub const KEY_ID: &'static str = "id"; |
| 72 | /// A panel's name. |
| 73 | pub const KEY_NAME: &'static str = "name"; |
| 74 | /// A guide page. |
| 75 | pub const KEY_PAGE: &'static str = "page"; |
| 76 | /// An anchor within a guide page. |
| 77 | pub const KEY_ANCHOR: &'static str = "anchor"; |
| 78 | |
| 79 | |
| 80 | /// Limits this schema enforces. Every one is a rejection, never a truncation. |
| 81 | pub 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)] |
| 131 | pub 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 | |
| 160 | impl 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)] |
| 180 | pub 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 | |
| 187 | impl 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)] |
| 345 | pub 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 | |
| 362 | impl 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. |
| 533 | fn 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. |
| 543 | fn 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. |
| 553 | fn 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. |
| 568 | fn 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. |
| 584 | fn 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. |
| 599 | fn 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)] |
| 627 | mod 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 | } |