Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_sbj/src/share.rs

49.5 KiB, 25 runs

created by r1870400018:22689, 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/share/0` — one person sending another a copy of something they own.
2//!
3//! A share **carries** what it sends. The files travel inside the payload, sealed to the
4//! recipient, and what lands is theirs: their copy, in their workspace, under their own key. They
5//! may change it, and the sender never sees the change; the sender may change theirs, and the
6//! receiver never sees that either. There is no shared content key that outlives an edit, nothing
7//! to revoke, and nobody's storage but the receiver's own. That is the whole design, and every
8//! field below follows from it.
9//!
10//! It is a schema rather than a fifth [`crate::post::Target`] for the reason written beside
11//! [`crate::SCHEMA_SHARE`], and the argument that settles it is the last one: a share must carry a
12//! consent bit the signature covers, and a `Reference` carries exactly two keys and refuses a
13//! third.
14//!
15//! # The consent bit
16//!
17//! Data travels freely. Code does not. A shared Diamond that carries a page is carrying **a
18//! program written by another person**, and the receiver decides whether to run it — so the
19//! artefact says, in the part the author signed, whether there is anything to decide. A flag a
20//! relay could add or strip is not a consent flag, which is why [`KEY_CODE`] is inside the payload
21//! and not in a wrapper around it.
22//!
23//! [`KEY_CODE`] is **required and always written**, never omitted when false. An omitted false
24//! would be indistinguishable from a sender whose build had never heard of the field, and the one
25//! thing a receiver must be able to tell apart is "they said there is no code" from "they did not
26//! say".
27//!
28//! And the claim is **checked against the files**, both ways (see [`code_file`]). A payload
29//! carrying a page under `code: false` is refused, so the bit cannot hide a program; a payload
30//! claiming code and carrying none is refused too, so a sender cannot cry wolf and teach people to
31//! wave the question away. The bit is not therefore redundant with the files, which is the obvious
32//! objection to it: it is the SENDER's reading of [`CODE_SUFFIXES`], pinned at signing time, so a
33//! later build that learns of a suffix this one does not know will disagree with an old artefact
34//! rather than quietly decide for the receiver.
35//!
36//! # What a share may not carry
37//!
38//! Five paths are refused outright, and each is refused here rather than left to a client, so
39//! that every implementation refuses the same things:
40//!
41//! - `.daimond/` — the meta, the append-only log, the link sidecar. The log is a record of what
42//! agents did in the SENDER's Diamond, and nobody sending a recipe means to send that.
43//! - `versions/` — the sender's own history, which is theirs and which would multiply the size of
44//! the share by the length of it.
45//! - `capp.json` — the delivery record, which says which bytes were delivered and at what template
46//! version. It is a record of a delivery that never happened to the receiver, and one doctored
47//! by the sender would pin the receiver's copy against every future template fix on THEIR
48//! machine, which they never chose. A copy that arrives without one is a case the receiving
49//! client already knows how to handle: it asks.
50//! - `triggers.json` — automation that fires with nobody pressing anything. It arms on the
51//! RECEIVER's money, and a client cannot save them from it by leaving it switched off: `on:
52//! false` does not disarm a trigger, and a leaf that appears in a pause tree plays. Refused in
53//! the FORMAT rather than by the sending client, because a receiver's exposure must not depend
54//! on which build the sender was running.
55//! - `STATE.md` — folder marks and build commands on the SENDER's disk. It is the one standing
56//! file that is about a machine rather than about the work, it is rebuilt on the first turn in
57//! the copy, and a path on somebody's disk is exactly what does not leave their device.
58//!
59//! The last two are the ones that are not about tidiness, and a template has refused both since it
60//! existed (`daimond` `src/protocol.rs`, `TEMPLATE_DROP_EXACT`). A share carries the same files to
61//! the same people, so the two lists agreeing is the point rather than a coincidence.
62//!
63//! The canonical rules of `SPEC.md` §3 apply unchanged. Two of them do real work here that they do
64//! not do for a message: [`KEY_FILES`] is ordered by path and refuses a duplicate, since a set of
65//! files written in two orders would be two addresses for one Diamond; and a path is refused
66//! rather than normalised, since normalising is exactly how one file comes to have two spellings.
67
68use crate::{
69 canon,
70 limit as sbj_limit,
71};
72
73use oxedyne_fe2o3_core::prelude::*;
74use oxedyne_fe2o3_jdat::{
75 prelude::*,
76 bdat::DecodeLimits,
77};
78
79
80// ┌───────────────────────────────────────────────────────────────────────────┐
81// │ KEYS │
82// └───────────────────────────────────────────────────────────────────────────┘
83
84/// Whether the files include executable page code. See the module note.
85pub const KEY_CODE: &'static str = "code";
86/// The files, ordered by path.
87pub const KEY_FILES: &'static str = "files";
88/// The shared thing's display name.
89pub const KEY_NAME: &'static str = "name";
90/// Per-share randomness, so that two identical shares are two addresses.
91pub const KEY_NONCE: &'static str = "nonce";
92/// The sender's covering sentence, if they wrote one.
93pub const KEY_NOTE: &'static str = "note";
94/// The recipient's public key.
95pub const KEY_TO: &'static str = "to";
96
97/// One file's contents.
98pub const KEY_BODY: &'static str = "body";
99/// One file's path, relative to the Diamond's own folder.
100pub const KEY_PATH: &'static str = "path";
101
102
103/// The path prefixes a share may not carry, and why.
104///
105/// Checked as a prefix here and as the whole path in [`REFUSED_EXACT`], which is what `capp.json`
106/// needs: a file called `capp.json` inside a folder of the receiver's own making is ordinary data,
107/// and the delivery record is the one at the root.
108pub const REFUSED_PREFIXES: &[&'static str] = &[".daimond/", "versions/"];
109
110/// The exact paths a share may not carry, each with the reason it is refused.
111///
112/// Whole paths rather than prefixes, and at the ROOT: a `triggers.json` a receiver writes inside a
113/// folder of their own is ordinary data, and only the one the app arms from is automation.
114///
115/// It was one path and it is three. The two that joined it are the ones that are not about
116/// tidiness, and the module header argues both; the reason they are HERE rather than in the
117/// sending client is that a receiver's exposure must not depend on which build the sender ran.
118pub const REFUSED_EXACT: &[(&'static str, &'static str)] = &[
119 ("capp.json",
120 "It is a DELIVERY record: it says which bytes were delivered to that instance and at what template version, and it decides which files a future template fix may replace. The receiver was not delivered to; they were given a copy by a person. One carried across from somebody else's machine would pin their copy against updates they never chose, and a doctored one would do it on purpose. A copy with no record is a case the receiving client already knows: it asks."),
121 ("triggers.json",
122 "It is ARMED AUTOMATION. A trigger fires with nobody pressing anything, and it would fire on the receiver's account and spend the receiver's money because they accepted a gift. Sending it switched off is not an answer: `on: false` does not disarm a trigger -- the pause tree is the authority and a leaf that appears in it plays -- so a share that carried one and said it was off would be worse than one that carries none. The receiver sets up their own."),
123 ("STATE.md",
124 "It names the SENDER's own machine: the folders they marked and the command they build with. A path on somebody's disk does not leave their device, and this file is the one standing file that is about a machine rather than about the work. The copy rebuilds it on its first turn, from the receiver's own folders."),
125];
126
127/// Is this exact path one a share may not carry? Answers the reason where it is.
128pub fn refused_exact(path: &str) -> Option<&'static str> {
129 REFUSED_EXACT.iter().find(|(name, _)| *name == path).map(|(_, why)| *why)
130}
131
132/// The suffixes that make a file code rather than data.
133///
134/// A closed set, matched case-insensitively on ASCII. It is closed for the same reason the icon
135/// names of `SPEC.md` §4.2 are: a reader knows exactly which files it will hand to an engine, and
136/// a set that grew by guessing would be a set that admitted the first thing nobody thought of.
137///
138/// Case-insensitively because a suffix check that is not is one `.HTML` away from being no check
139/// at all, and the receiving side stores a file under the name it was sent under.
140pub const CODE_SUFFIXES: &[&'static str] = &[".htm", ".html", ".js", ".mjs", ".svg", ".wasm"];
141
142
143/// Limits this schema enforces. Every one is a rejection, never a truncation.
144pub mod limit {
145 /// The most files one share may carry.
146 ///
147 /// Sixty-four. A capp is a page, a memory, an index and a handful of seeded tables — under ten
148 /// — and each file in a share is examined and written on arrival, so the number bounds what
149 /// opening one costs. The figure is revisable on evidence, as `SPEC.md` §5's are; that there is
150 /// one is not.
151 pub const FILES: usize = 64;
152 /// The most all the file bodies together may carry, in bytes.
153 ///
154 /// Two mebibytes, and the reason is the RECEIVER's, not the format's. A Daimond sync parcel
155 /// carries at most six mebibytes across every Diamond an account holds, and a Diamond that does
156 /// not fit is left out of the parcel entirely rather than trimmed. A share larger than a third
157 /// of that budget is a share that would stop travelling between the receiver's own devices the
158 /// day it arrived, which is a worse failure than being refused now.
159 pub const TOTAL_BYTES: usize = 2 * 1024 * 1024;
160 /// The most one file's path may carry, in bytes of UTF-8.
161 pub const PATH_BYTES: usize = 256;
162 /// The most the display name may carry, in bytes of UTF-8.
163 pub const NAME_BYTES: usize = 128;
164 /// The most the covering note may carry, in bytes of UTF-8.
165 ///
166 /// A sentence, not a letter. A letter is a `daimond/post/0` message, which carries eight
167 /// kibibytes and is the thing built for prose; this is the line that says what the gift is.
168 pub const NOTE_BYTES: usize = 512;
169 /// The exact width of the per-share nonce.
170 pub const NONCE_BYTES: usize = 16;
171 /// The exact width of a public key.
172 pub const KEY_BYTES: usize = 32;
173 /// Decoding depth for a payload of this schema.
174 ///
175 /// A share is a flat record holding one list of flat maps, so four levels is its whole shape
176 /// and eight is already past anything it can reach. Far below `SPEC.md` §5's tree limit because
177 /// nothing here recurses, and a limit set to what the shape needs refuses a nested value before
178 /// it is looked at.
179 pub const DEPTH: usize = 8;
180}
181
182
183// ┌───────────────────────────────────────────────────────────────────────────┐
184// │ CODE │
185// └───────────────────────────────────────────────────────────────────────────┘
186
187/// Whether a path names a file this version considers code.
188///
189/// The suffix and nothing else. What a file contains is not consulted, deliberately: a rule about
190/// contents would be a rule a reader had to run over every byte of every share before it could say
191/// whether there was a question to ask, and it would answer differently for the same file on two
192/// builds. A suffix is a fact about the name, and the name is what the receiving side stores.
193pub fn is_code_path(path: &str) -> bool {
194 let lower = path.to_ascii_lowercase();
195 CODE_SUFFIXES.iter().any(|s| lower.ends_with(s))
196}
197
198/// The first file in a list that is code, or `None` when none of them is.
199///
200/// The FIRST rather than a count, because the error names it: "this share says it carries no code
201/// and carries `crystal.html`" is a sentence a person can act on, and "1 code file" is not.
202pub fn code_file(files: &[File]) -> Option<&File> {
203 files.iter().find(|f| is_code_path(&f.path))
204}
205
206
207// ┌───────────────────────────────────────────────────────────────────────────┐
208// │ ONE FILE │
209// └───────────────────────────────────────────────────────────────────────────┘
210
211/// One file of a share: where it goes, and what is in it.
212///
213/// The body is bytes and is held to no text rule, because a Diamond holds pictures as well as
214/// prose and a canonical encoding of bytes is the bytes. The PATH is a string and is held to every
215/// rule `SPEC.md` §3 has for one, since two spellings of one path would be two addresses for one
216/// Diamond.
217#[derive(Clone, Debug, PartialEq, Eq)]
218pub struct File {
219 /// Where the file goes, relative to the receiver's copy of the Diamond.
220 pub path: String,
221 /// What is in it.
222 pub body: Vec<u8>,
223}
224
225impl File {
226 /// Encodes this file as a canonical daticle.
227 pub fn to_dat(&self) -> Outcome<Dat> {
228 let mut map = DaticleMap::new();
229 map.insert(dat!(KEY_BODY), Dat::BU32(self.body.clone()));
230 map.insert(dat!(KEY_PATH), Dat::Str(self.path.clone()));
231 Ok(Dat::Map(map))
232 }
233
234 /// Reads a file, refusing anything this schema does not admit.
235 pub fn from_dat(d: &Dat) -> Outcome<Self> {
236 let map = match d {
237 Dat::Map(m) => m,
238 other => return Err(err!(
239 "A shared file must be a Dat::Map, found a {:?}.", other.kind();
240 Invalid, Input, Mismatch)),
241 };
242 res!(exact_keys(map, &[KEY_BODY, KEY_PATH], "shared file"));
243
244 let path = match res!(get(map, KEY_PATH)) {
245 Dat::Str(s) => s.clone(),
246 other => return Err(err!(
247 "A shared file's \"{}\" must be a string, found a {:?}.", KEY_PATH, other.kind();
248 Invalid, Input, Mismatch)),
249 };
250 res!(check_path(&path));
251
252 let body = match res!(get(map, KEY_BODY)) {
253 Dat::BU32(b) => b.clone(),
254 Dat::BU8(_) | Dat::BU16(_) | Dat::BU64(_) => return Err(err!(
255 "The shared file \"{}\" carries its contents in a byte string that is not a BU32. A \
256 narrower one truncates silently past its width, and a wider one is a second \
257 encoding of the same value.", path;
258 Invalid, Input, Mismatch)),
259 other => return Err(err!(
260 "The shared file \"{}\" must carry its contents in a BU32, found a {:?}.",
261 path, other.kind();
262 Invalid, Input, Mismatch)),
263 };
264 Ok(Self { path, body })
265 }
266}
267
268/// Checks a path against every rule this schema has for one.
269///
270/// **Refused rather than normalised**, which is where this parts company with the client-side
271/// `safePath` it otherwise matches. That function is handed an untrusted request and drops an
272/// empty or `.` segment on the way to a real file; this is deciding what a signed artefact means,
273/// and there a path that needed tidying is a path with two spellings and so a Diamond with two
274/// addresses. Every check below is a rejection.
275pub fn check_path(path: &str) -> Outcome<()> {
276 if path.is_empty() {
277 return Err(err!(
278 "A shared file carries an empty path."; Invalid, Input, Missing));
279 }
280 if path.len() > limit::PATH_BYTES {
281 return Err(err!(
282 "The shared path \"{}\" is {} bytes, exceeding the limit of {}.",
283 path, path.len(), limit::PATH_BYTES;
284 Invalid, Input, LimitReached));
285 }
286 // The §3 rule 5 string rules: UTF-8 already, and now NFC, no control characters, no carriage
287 // return. A path spelled with a combining accent displays as the composed one and hashes
288 // differently, which for a file name is two files that look like one.
289 res!(canon::check_string(path));
290
291 if path.contains('\\') {
292 return Err(err!(
293 "The shared path \"{}\" carries a backslash. A path is joined with \"/\" and nothing \
294 else, so a backslash is either a separator this format does not have or a character in \
295 a name that will not survive being written down.", path;
296 Invalid, Input));
297 }
298 if path.starts_with('/') {
299 return Err(err!(
300 "The shared path \"{}\" is absolute. Every path in a share is relative to the \
301 receiver's own copy of the Diamond, and an absolute one names a place on their machine \
302 that the sender cannot know and must not reach.", path;
303 Invalid, Input));
304 }
305 // A scheme, by the same rule `safePath` uses: a letter, then letters, digits, `+`, `.` or `-`,
306 // then a colon. `c:/x` and `data:…` are both caught, and neither is a relative path.
307 if let Some(colon) = path.find(':') {
308 let head = &path[..colon];
309 if !head.is_empty()
310 && head.starts_with(|c: char| c.is_ascii_alphabetic())
311 && head.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '.' || c == '-')
312 {
313 return Err(err!(
314 "The shared path \"{}\" begins with what reads as a scheme, \"{}:\". A share \
315 carries files, never locations.", path, head;
316 Invalid, Input));
317 }
318 }
319 for seg in path.split('/') {
320 if seg.is_empty() {
321 return Err(err!(
322 "The shared path \"{}\" carries an empty segment. It is refused rather than \
323 tidied: a path that needs tidying has two spellings, and two spellings of one file \
324 are two addresses for one Diamond.", path;
325 Invalid, Input));
326 }
327 if seg == "." || seg == ".." {
328 return Err(err!(
329 "The shared path \"{}\" carries a \"{}\" segment. A share reaches nothing outside \
330 the Diamond it is a copy of, and a path that walks is refused rather than \
331 resolved.", path, seg;
332 Invalid, Input));
333 }
334 }
335
336 for prefix in REFUSED_PREFIXES {
337 if path.starts_with(prefix) {
338 return Err(err!(
339 "The shared path \"{}\" is under \"{}\", which a share may not carry. That folder \
340 holds the SENDER's own record — the stamps the sync merge decides on, the link \
341 sidecar, and the append-only log of what agents did in their copy. A person \
342 sending a recipe does not mean to send that, and the receiver's copy is new: its \
343 record starts empty because nothing has happened in it yet.", path, prefix;
344 Invalid, Input));
345 }
346 }
347 if let Some(why) = refused_exact(path) {
348 return Err(err!("A share may not carry \"{}\". {}", path, why; Invalid, Input));
349 }
350 Ok(())
351}
352
353
354// ┌───────────────────────────────────────────────────────────────────────────┐
355// │ THE SHARE │
356// └───────────────────────────────────────────────────────────────────────────┘
357
358/// A `daimond/share/0` payload.
359///
360/// Every field is inside the payload region, so every field is covered by the envelope's `hash`
361/// and therefore by its signature. A relay handling this artefact can add nothing to it, remove
362/// nothing from it, and rewrite nothing in it — including [`Share::code`] — without the signature
363/// ceasing to verify.
364///
365/// There is no sender field and no timestamp, for the reasons `crate::post` gives: the author is
366/// the envelope's `author`, and the time is the envelope's and advisory. There is also **no
367/// identifier of the sender's Diamond**, which is particular to this schema. The receiver's copy
368/// is a new Diamond with an identifier of their own making, so an identifier that travelled would
369/// either be a field nobody read or a way for one person's share to land on top of another
370/// person's Diamond.
371#[derive(Clone, Debug, PartialEq, Eq)]
372pub struct Share {
373 /// The shared thing's display name. Advisory, exactly as a card's label is.
374 pub name: String,
375 /// The recipient's public key.
376 pub to: Vec<u8>,
377 /// Per-share randomness, so two identical shares are two addresses.
378 pub nonce: Vec<u8>,
379 /// The sender's covering sentence, if they wrote one.
380 pub note: Option<String>,
381 /// The files, ordered by path and each path carried once.
382 pub files: Vec<File>,
383 /// Whether the files include executable page code.
384 ///
385 /// The sender's own claim, signed, and checked against the files both ways. See the module
386 /// note for why it is here rather than derived, and why it is always written.
387 pub code: bool,
388}
389
390impl Share {
391
392 /// Builds a share, putting the files in canonical order and stating the code claim for the
393 /// caller.
394 ///
395 /// The claim is computed here rather than taken as an argument because a caller who could
396 /// supply it could supply the wrong one, and the only honest value at composition time is what
397 /// the files say. [`Share::code`] remains a field, and remains signed, because it is the value
398 /// THIS build computed and a later one may disagree with; what this constructor removes is the
399 /// chance to disagree with it on purpose.
400 pub fn new(
401 name: String,
402 to: Vec<u8>,
403 nonce: Vec<u8>,
404 note: Option<String>,
405 files: Vec<File>,
406 )
407 -> Self
408 {
409 let mut files = files;
410 files.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
411 let code = code_file(&files).is_some();
412 Self { name, to, nonce, note, files, code }
413 }
414
415 /// Encodes this share as a canonical daticle.
416 pub fn to_dat(&self) -> Outcome<Dat> {
417 let mut map = DaticleMap::new();
418 // Always written, never omitted when false. An omitted false and a sender whose build had
419 // never heard of the field are the same bytes, and those are the two things a receiver must
420 // be able to tell apart.
421 map.insert(dat!(KEY_CODE), Dat::Bool(self.code));
422 let mut list = Vec::with_capacity(self.files.len());
423 for f in &self.files {
424 list.push(res!(f.to_dat()));
425 }
426 map.insert(dat!(KEY_FILES), Dat::List(list));
427 map.insert(dat!(KEY_NAME), Dat::Str(self.name.clone()));
428 map.insert(dat!(KEY_NONCE), Dat::BU8(self.nonce.clone()));
429 // An absent note is OMITTED, never encoded as `none` or as an empty string: SPEC.md §3
430 // rules 4 and 8, so that one share has one encoding.
431 if let Some(n) = &self.note {
432 map.insert(dat!(KEY_NOTE), Dat::Str(n.clone()));
433 }
434 map.insert(dat!(KEY_TO), Dat::BU8(self.to.clone()));
435 Ok(Dat::Map(map))
436 }
437
438 /// Reads a share, enforcing every rule this schema declares.
439 pub fn from_dat(d: &Dat) -> Outcome<Self> {
440 let map = match d {
441 Dat::Map(m) => m,
442 Dat::OrdMap(_) => return Err(err!(
443 "SPEC.md §3 rule 2: a share payload is a Dat::Map, never a Dat::OrdMap. An OrdMap \
444 follows the author's typing rather than the keys, so the same share would have as \
445 many addresses as there are orders to write it in.";
446 Invalid, Input, Mismatch)),
447 other => return Err(err!(
448 "A share payload must be a Dat::Map, found a {:?}.", other.kind();
449 Invalid, Input, Mismatch)),
450 };
451 let allowed: Vec<&str> = {
452 let mut v = vec![KEY_CODE, KEY_FILES, KEY_NAME, KEY_NONCE, KEY_TO];
453 if map.contains_key(&dat!(KEY_NOTE)) { v.push(KEY_NOTE); }
454 v
455 };
456 res!(exact_keys(map, &allowed, "share"));
457
458 let name = match res!(get(map, KEY_NAME)) {
459 Dat::Str(s) => s.clone(),
460 other => return Err(err!(
461 "The share key \"{}\" must be a string, found a {:?}.", KEY_NAME, other.kind();
462 Invalid, Input, Mismatch)),
463 };
464 res!(check_text(&name, KEY_NAME, limit::NAME_BYTES));
465
466 let to = res!(get_bytes(map, KEY_TO, limit::KEY_BYTES));
467 let nonce = res!(get_bytes(map, KEY_NONCE, limit::NONCE_BYTES));
468
469 let note = match map.get(&dat!(KEY_NOTE)) {
470 Some(Dat::Str(s)) => {
471 if s.is_empty() {
472 return Err(err!(
473 "SPEC.md §3 rule 8: the share carries an empty \"{}\". A note a reader \
474 would draw identically whether present or absent gives one share two \
475 encodings, and so two addresses. Omit the key.", KEY_NOTE;
476 Invalid, Input));
477 }
478 res!(check_text(s, KEY_NOTE, limit::NOTE_BYTES));
479 Some(s.clone())
480 },
481 Some(other) => return Err(err!(
482 "The share key \"{}\" must be a string, found a {:?}.", KEY_NOTE, other.kind();
483 Invalid, Input, Mismatch)),
484 None => None,
485 };
486
487 let code = match res!(get(map, KEY_CODE)) {
488 Dat::Bool(b) => *b,
489 other => return Err(err!(
490 "The share key \"{}\" must be a bool, found a {:?}. It is the sender's signed \
491 statement about whether this share carries a program, and a reader that could not \
492 read it would be asking a person to consent to something nobody described.",
493 KEY_CODE, other.kind();
494 Invalid, Input, Mismatch)),
495 };
496
497 let files = match res!(get(map, KEY_FILES)) {
498 Dat::List(items) => {
499 if items.is_empty() {
500 return Err(err!(
501 "The share carries no files. A share is a copy of something, and a copy of \
502 nothing is not a smaller share; it is not one.";
503 Invalid, Input, Missing));
504 }
505 if items.len() > limit::FILES {
506 return Err(err!(
507 "The share carries {} files, exceeding the limit of {}. Each is examined \
508 and written on the RECEIVER's machine when the share is opened.",
509 items.len(), limit::FILES;
510 Invalid, Input, LimitReached));
511 }
512 let mut out: Vec<File> = Vec::with_capacity(items.len());
513 let mut total: usize = 0;
514 for (i, item) in items.iter().enumerate() {
515 let f = res!(File::from_dat(item).map_err(|e| err!(e,
516 "File {} of {} is not one this schema admits.", i, items.len();
517 Invalid, Input)));
518 // Ordered by path, and each path once. A set of files written in two orders
519 // would be two addresses for one Diamond, and the same file twice is a share
520 // whose meaning depends on which entry the receiver writes last.
521 if let Some(prev) = out.last() {
522 if f.path.as_bytes() == prev.path.as_bytes() {
523 return Err(err!(
524 "The share carries the path \"{}\" twice. Which copy the receiver \
525 ends up with would then depend on the order they were written in.",
526 f.path;
527 Invalid, Input));
528 }
529 if f.path.as_bytes() < prev.path.as_bytes() {
530 return Err(err!(
531 "The share's files are not in path order: \"{}\" follows \"{}\". \
532 The order is fixed so that one set of files has one encoding, and \
533 so one address; it is refused rather than sorted, because sorting \
534 it would be accepting a second encoding and quietly rewriting it.",
535 f.path, prev.path;
536 Invalid, Input));
537 }
538 }
539 total = total.saturating_add(f.body.len());
540 out.push(f);
541 }
542 if total > limit::TOTAL_BYTES {
543 return Err(err!(
544 "The share's files carry {} bytes together, exceeding the limit of {}. It \
545 is refused rather than trimmed: a share missing a file is not a smaller \
546 share, and the ceiling is the receiver's sync budget rather than this \
547 format's.", total, limit::TOTAL_BYTES;
548 Invalid, Input, LimitReached));
549 }
550 out
551 },
552 Dat::Vek(_) => return Err(err!(
553 "SPEC.md §3 rule 7: \"{}\" is a Dat::List, never a Dat::Vek, even where every \
554 element shares a kind.", KEY_FILES;
555 Invalid, Input, Mismatch)),
556 other => return Err(err!(
557 "The share key \"{}\" must be a list, found a {:?}.", KEY_FILES, other.kind();
558 Invalid, Input, Mismatch)),
559 };
560
561 // The consent bit against the files, both ways. Neither direction is a formality: one stops
562 // a program arriving under a claim that there is none, and the other stops a sender asking
563 // for consent they do not need, which is how a person learns to wave the question away.
564 match (code, code_file(&files)) {
565 (false, Some(f)) => return Err(err!(
566 "The share states that it carries no code, and carries \"{}\". The claim is the \
567 sender's, it is signed, and it is what a receiver is asked to consent to before \
568 anything runs, so a share that contradicts its own claim is refused rather than \
569 corrected.", f.path;
570 Invalid, Input, Mismatch)),
571 (true, None) => return Err(err!(
572 "The share states that it carries code, and carries none. It is refused rather \
573 than accepted as harmless caution: a receiver asked to consent to a program that \
574 is not there is a receiver being taught that the question does not mean anything.";
575 Invalid, Input, Mismatch)),
576 _ => {},
577 }
578
579 Ok(Self { name, to, nonce, note, files, code })
580 }
581
582 /// Encodes this share to the canonical bytes that become the payload region.
583 pub fn encode(&self) -> Outcome<Vec<u8>> {
584 let d = res!(self.to_dat());
585 // Read straight back, so that a share which cannot be decoded can never be signed. Signing
586 // bytes no reader will accept produces an artefact that is valid to its author and refused
587 // by everybody else.
588 res!(Self::from_dat(&d));
589 let bytes = res!(d.to_bytes(Vec::new()));
590 if bytes.len() > sbj_limit::TREE_BYTES {
591 return Err(err!(
592 "The encoded share is {} bytes, exceeding the payload region limit of {}.",
593 bytes.len(), sbj_limit::TREE_BYTES;
594 Invalid, Input, LimitReached));
595 }
596 Ok(bytes)
597 }
598
599 /// Decodes a share from the bytes of a payload region, which must be consumed exactly.
600 ///
601 /// The bytes are re-encoded and compared with what came in, which is what enforces the
602 /// byte-level rules a decoded value can no longer show: a duplicate key collapses into one
603 /// entry when BDAT builds its map, and a length written in more bytes than it needs decodes to
604 /// the same number. Both survive only in the bytes.
605 pub fn decode(buf: &[u8]) -> Outcome<Self> {
606 let lims = DecodeLimits::new(limit::DEPTH, sbj_limit::TREE_BYTES);
607 let (d, n) = res!(Dat::from_bytes_limited(buf, &lims));
608 if n != buf.len() {
609 return Err(err!(
610 "The share payload occupies {} of the {} bytes supplied, leaving {} trailing.",
611 n, buf.len(), buf.len() - n;
612 Invalid, Input, Decode));
613 }
614 let re = res!(d.to_bytes(Vec::new()));
615 if re != buf {
616 return Err(err!(
617 "The share payload is not in canonical form: it re-encodes to {} bytes against the \
618 {} supplied, so it carries a duplicate key, a non-minimal length, or a \
619 non-canonical map. See SPEC.md §3.", re.len(), buf.len();
620 Invalid, Input, Decode));
621 }
622 Self::from_dat(&d)
623 }
624}
625
626
627// ┌───────────────────────────────────────────────────────────────────────────┐
628// │ FIELD READERS │
629// └───────────────────────────────────────────────────────────────────────────┘
630
631/// Returns a required key's value, or an error naming the key that is missing.
632fn get<'a>(map: &'a DaticleMap, key: &str) -> Outcome<&'a Dat> {
633 match map.get(&dat!(key)) {
634 Some(d) => Ok(d),
635 None => Err(err!(
636 "The share is missing the required key \"{}\".", key;
637 Invalid, Input, Missing)),
638 }
639}
640
641/// Reads a required `BU8` key of an exact width.
642///
643/// Exact rather than bounded because every one of these is a key or a nonce, and each has one
644/// size. A short one is not a smaller key; it is a different thing.
645fn get_bytes(map: &DaticleMap, key: &str, width: usize) -> Outcome<Vec<u8>> {
646 let b = match res!(get(map, key)) {
647 Dat::BU8(b) => b.clone(),
648 other => return Err(err!(
649 "The share key \"{}\" must carry a BU8, found a {:?}.", key, other.kind();
650 Invalid, Input, Mismatch)),
651 };
652 if b.len() != width {
653 return Err(err!(
654 "The share key \"{}\" carries {} bytes and must carry exactly {}.", key, b.len(), width;
655 Invalid, Input, Mismatch));
656 }
657 Ok(b)
658}
659
660/// Checks a string field against the canonical text rules and a byte ceiling.
661fn check_text(s: &str, key: &str, max: usize) -> Outcome<()> {
662 if s.len() > max {
663 return Err(err!(
664 "The share's \"{}\" is {} bytes, exceeding the limit of {}.", key, s.len(), max;
665 Invalid, Input, LimitReached));
666 }
667 res!(canon::check_string(s));
668 Ok(())
669}
670
671/// Requires a map to carry exactly the named keys — no more, and no fewer.
672///
673/// Both directions, because they catch different faults. A missing key is a share that does not
674/// say something it must. An unknown key is a field the sender signed and no reader will ever
675/// draw, which is worse than useless: it is covered by the signature, so it looks like meaning.
676fn exact_keys(map: &DaticleMap, allowed: &[&str], what: &str) -> Outcome<()> {
677 for k in allowed {
678 if !map.contains_key(&dat!(*k)) {
679 return Err(err!(
680 "The {} is missing the required key \"{}\".", what, k;
681 Invalid, Input, Missing));
682 }
683 }
684 for k in map.keys() {
685 let name = match k {
686 Dat::Str(s) => s.clone(),
687 other => return Err(err!(
688 "SPEC.md §3 rule 3: a map key must be a string, found a {:?}.", other.kind();
689 Invalid, Input, Mismatch)),
690 };
691 res!(canon::check_key_string(&name));
692 if !allowed.iter().any(|a| *a == name.as_str()) {
693 return Err(err!(
694 "The {} carries the key \"{}\", which this schema does not admit. The admitted \
695 keys are: {}.", what, name, allowed.join(", ");
696 Invalid, Input, Unknown));
697 }
698 }
699 Ok(())
700}
701
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706
707 /// A plausible share of data alone, with fixed contents.
708 fn sample() -> Share {
709 Share::new(
710 fmt!("Sourdough"),
711 vec![0xA1; limit::KEY_BYTES],
712 vec![0xB2; limit::NONCE_BYTES],
713 None,
714 vec![
715 File { path: fmt!("crystal.json"), body: b"{\"loaves\":3}".to_vec() },
716 File { path: fmt!("bakes/2026.jsonl"), body: b"{\"day\":1}\n".to_vec() },
717 ],
718 )
719 }
720
721 /// The same share, carrying a page.
722 fn sample_capp() -> Share {
723 let mut files = sample().files;
724 files.push(File { path: fmt!("crystal.html"), body: b"<p>hello</p>".to_vec() });
725 Share::new(
726 fmt!("Life log"),
727 vec![0xA1; limit::KEY_BYTES],
728 vec![0xB2; limit::NONCE_BYTES],
729 Some(fmt!("The food log we talked about.")),
730 files,
731 )
732 }
733
734 #[test]
735 fn test_round_trip_data_only() -> Outcome<()> {
736 let s = sample();
737 assert!(!s.code, "A share of two data files claims to carry code.");
738 let bytes = res!(s.encode());
739 let back = res!(Share::decode(&bytes));
740 assert_eq!(s, back);
741 Ok(())
742 }
743
744 #[test]
745 fn test_round_trip_with_a_capp() -> Outcome<()> {
746 let s = sample_capp();
747 assert!(s.code, "A share carrying crystal.html does not claim to carry code.");
748 let bytes = res!(s.encode());
749 let back = res!(Share::decode(&bytes));
750 assert_eq!(s, back);
751 assert!(back.code);
752 Ok(())
753 }
754
755 /// The property the whole schema exists for: a program cannot travel under a claim of none.
756 #[test]
757 fn test_a_page_under_a_false_code_claim_is_refused() -> Outcome<()> {
758 let mut s = sample_capp();
759 s.code = false; // the sender lies, or a build computes it differently
760 match s.encode() {
761 Ok(_) => Err(err!(
762 "A share carrying a page under `code: false` was encoded, so a program could \
763 arrive as data."; Test, Invalid)),
764 Err(e) => {
765 let msg = fmt!("{}", e);
766 assert!(msg.contains("crystal.html"),
767 "The refusal does not name the file that is code: {}", msg);
768 Ok(())
769 },
770 }
771 }
772
773 /// And the other way, so the bit cannot be set for effect.
774 #[test]
775 fn test_a_code_claim_with_no_code_is_refused() -> Outcome<()> {
776 let mut s = sample();
777 s.code = true;
778 match s.encode() {
779 Ok(_) => Err(err!(
780 "A share claiming code and carrying none was encoded."; Test, Invalid)),
781 Err(_) => Ok(()),
782 }
783 }
784
785 /// The claim survives the wire, which is the point of it being signed rather than derived.
786 #[test]
787 fn test_the_code_bit_is_in_the_bytes() -> Outcome<()> {
788 let data = res!(sample().encode());
789 let capp = res!(sample_capp().encode());
790 assert_ne!(data, capp);
791 // And a payload with the bit flipped is not a payload this schema reads.
792 let mut m = match res!(sample().to_dat()) {
793 Dat::Map(m) => m,
794 other => return Err(err!(
795 "A share encodes as a map, and this is a {:?}.", other.kind(); Test, Bug)),
796 };
797 m.insert(dat!(KEY_CODE), Dat::Bool(true));
798 match Share::from_dat(&Dat::Map(m)) {
799 Ok(_) => Err(err!(
800 "A share whose code bit was flipped on the wire was read."; Test, Invalid)),
801 Err(_) => Ok(()),
802 }
803 }
804
805 /// The bit is IN THE BYTES the address is taken over, so changing it changes the address.
806 ///
807 /// This is the payload half of "a flag a relay could add or strip is not a consent flag". The
808 /// artefact half is in `doc.rs`: the signature covers the address, so a carrier that changed
809 /// the bit would have to forge a signature to go with it.
810 ///
811 /// The bytes are built by hand rather than through `encode`, because the whole point is a
812 /// payload this schema would refuse to write: a `code` that disagrees with the files. A
813 /// carrier is not held to the schema, so the test must not be either.
814 #[test]
815 fn test_flipping_the_code_bit_changes_the_address() -> Outcome<()> {
816 let honest = res!(sample().encode());
817 let mut m = match res!(sample().to_dat()) {
818 Dat::Map(m) => m,
819 other => return Err(err!(
820 "A share encodes as a map, and this is a {:?}.", other.kind(); Test, Bug)),
821 };
822 m.insert(dat!(KEY_CODE), Dat::Bool(true));
823 let tampered = res!(Dat::Map(m).to_bytes(Vec::new()));
824 assert_ne!(tampered, honest,
825 "Setting the consent bit did not change a byte, so nothing signed covers it.");
826 // And the tampered bytes are refused on the way in, so a carrier gains nothing even where
827 // the container is not consulted.
828 assert!(Share::decode(&tampered).is_err(),
829 "A share whose consent bit was set by somebody other than its author was read.");
830 Ok(())
831 }
832
833 /// A code suffix in capitals is still a code suffix.
834 #[test]
835 fn test_the_suffix_check_ignores_case() -> Outcome<()> {
836 assert!(is_code_path("Crystal.HTML"));
837 assert!(is_code_path("a/b/PAGE.Js"));
838 assert!(!is_code_path("crystal.json"));
839 assert!(!is_code_path("notes.html.md"));
840 let s = Share::new(
841 fmt!("Shouting"),
842 vec![0xA1; limit::KEY_BYTES],
843 vec![0xB2; limit::NONCE_BYTES],
844 None,
845 vec![File { path: fmt!("PAGE.HTML"), body: b"<p>x</p>".to_vec() }],
846 );
847 assert!(s.code, "A file called PAGE.HTML was not counted as code.");
848 Ok(())
849 }
850
851 #[test]
852 fn test_files_must_be_in_path_order() -> Outcome<()> {
853 let s = sample();
854 let mut m = match res!(s.to_dat()) {
855 Dat::Map(m) => m,
856 other => return Err(err!(
857 "A share encodes as a map, and this is a {:?}.", other.kind(); Test, Bug)),
858 };
859 let mut list = Vec::new();
860 for f in s.files.iter().rev() {
861 list.push(res!(f.to_dat()));
862 }
863 m.insert(dat!(KEY_FILES), Dat::List(list));
864 match Share::from_dat(&Dat::Map(m)) {
865 Ok(_) => Err(err!(
866 "Files out of path order were accepted, so one Diamond has as many addresses as \
867 there are orders to list its files in."; Test, Invalid)),
868 Err(_) => Ok(()),
869 }
870 }
871
872 #[test]
873 fn test_a_duplicate_path_is_refused() -> Outcome<()> {
874 let s = Share::new(
875 fmt!("Twice"),
876 vec![0xA1; limit::KEY_BYTES],
877 vec![0xB2; limit::NONCE_BYTES],
878 None,
879 vec![
880 File { path: fmt!("a.json"), body: b"1".to_vec() },
881 File { path: fmt!("a.json"), body: b"2".to_vec() },
882 ],
883 );
884 match s.encode() {
885 Ok(_) => Err(err!("Two files at one path were accepted."; Test, Invalid)),
886 Err(_) => Ok(()),
887 }
888 }
889
890 /// The constructor puts the files in order, so a caller cannot mint a second address by
891 /// listing them differently.
892 #[test]
893 fn test_new_orders_the_files() -> Outcome<()> {
894 let a = Share::new(fmt!("N"), vec![0xA1; 32], vec![0xB2; 16], None, vec![
895 File { path: fmt!("b.json"), body: b"2".to_vec() },
896 File { path: fmt!("a.json"), body: b"1".to_vec() },
897 ]);
898 let b = Share::new(fmt!("N"), vec![0xA1; 32], vec![0xB2; 16], None, vec![
899 File { path: fmt!("a.json"), body: b"1".to_vec() },
900 File { path: fmt!("b.json"), body: b"2".to_vec() },
901 ]);
902 assert_eq!(res!(a.encode()), res!(b.encode()));
903 Ok(())
904 }
905
906 /// Each refused path, refused, and each saying which rule it broke.
907 #[test]
908 fn test_the_refused_paths() -> Outcome<()> {
909 for (path, says) in [
910 (".daimond/log.jsonl", ".daimond/"),
911 ("versions/3/crystal.json", "versions/"),
912 ("capp.json", "capp.json"),
913 ("triggers.json", "triggers.json"),
914 ("STATE.md", "STATE.md"),
915 ] {
916 match check_path(path) {
917 Ok(()) => return Err(err!(
918 "The path \"{}\" was accepted into a share.", path; Test, Invalid)),
919 Err(e) => {
920 let msg = fmt!("{}", e);
921 assert!(msg.contains(says),
922 "The refusal of \"{}\" does not name what it broke: {}", path, msg);
923 },
924 }
925 }
926 // And each is refused only where it means what it says: the delivery record is the one at
927 // the root, and a folder of the receiver's own making may hold anything.
928 res!(check_path("recipes/capp.json"));
929 res!(check_path("notes/versions/old.md"));
930 res!(check_path("saved/triggers.json"));
931 res!(check_path("docs/STATE.md"));
932 Ok(())
933 }
934
935 /// A share may not carry armed automation, and it is the FORMAT that says so.
936 ///
937 /// REMOVE THE `triggers.json` ENTRY FROM [`REFUSED_EXACT`] AND THIS GOES RED, which is the
938 /// whole of what it is for: the sending client refuses the file too, and a check that drove
939 /// only the client would pass on a build whose sender was somebody else's.
940 ///
941 /// A trigger fires with nobody pressing anything. It would arm on the receiver's account, be
942 /// governed by the receiver's pause tree -- where a leaf that appears PLAYS -- and spend the
943 /// receiver's money, because they accepted a gift. Switching it off in the file is not the
944 /// answer and the reason is in the constant.
945 #[test]
946 fn test_a_share_may_not_carry_a_trigger() -> Outcome<()> {
947 match check_path("triggers.json") {
948 Ok(()) => return Err(err!(
949 "A share accepted \"triggers.json\": automation that fires with nobody pressing \
950 anything, on the receiver's account and at the receiver's expense."; Test, Invalid)),
951 Err(e) => {
952 let msg = fmt!("{}", e);
953 // The refusal has to say WHY, because the sender reads it and the only thing they
954 // can do about it is understand it.
955 assert!(msg.contains("fires with nobody pressing anything"),
956 "The refusal does not say what a trigger does: {}", msg);
957 },
958 }
959 // And it is refused where it is ENCODED, not merely where a path is checked -- so a caller
960 // that built the payload by hand is refused as well.
961 let s = Share::new(fmt!("N"), vec![0xA1; 32], vec![0xB2; 16], None, vec![
962 File { path: fmt!("triggers.json"), body: b"[{\"on\":true}]".to_vec() },
963 ]);
964 match s.encode() {
965 Ok(_) => Err(err!(
966 "A share carrying \"triggers.json\" encoded."; Test, Invalid)),
967 Err(_) => Ok(()),
968 }
969 }
970
971 /// A share may not carry the sender's own machine.
972 ///
973 /// REMOVE THE `STATE.md` ENTRY FROM [`REFUSED_EXACT`] AND THIS GOES RED. `STATE.md` holds the
974 /// folders the sender marked and the command they build with -- paths on their disk, which do
975 /// not leave their device. The copy rebuilds it on its first turn from the receiver's own.
976 #[test]
977 fn test_a_share_may_not_carry_the_senders_machine() -> Outcome<()> {
978 match check_path("STATE.md") {
979 Ok(()) => return Err(err!(
980 "A share accepted \"STATE.md\", which names folders on the sender's own disk.";
981 Test, Invalid)),
982 Err(e) => {
983 let msg = fmt!("{}", e);
984 assert!(msg.contains("SENDER's own machine"),
985 "The refusal does not say whose machine it names: {}", msg);
986 },
987 }
988 let s = Share::new(fmt!("N"), vec![0xA1; 32], vec![0xB2; 16], None, vec![
989 File { path: fmt!("STATE.md"), body: b"Marked: /home/somebody/work\n".to_vec() },
990 ]);
991 match s.encode() {
992 Ok(_) => Err(err!("A share carrying \"STATE.md\" encoded."; Test, Invalid)),
993 Err(_) => Ok(()),
994 }
995 }
996
997 #[test]
998 fn test_a_walking_path_is_refused() -> Outcome<()> {
999 for path in ["../secrets.json", "a/../../b.json", "a/./b.json", "/etc/passwd",
1000 "a//b.json", "a\\b.json", "data:text/plain,x", "c:/notes.md"]
1001 {
1002 if check_path(path).is_ok() {
1003 return Err(err!(
1004 "The path \"{}\" was accepted into a share.", path; Test, Invalid));
1005 }
1006 }
1007 Ok(())
1008 }
1009
1010 /// A path is refused rather than tidied, which is what makes one file one address.
1011 #[test]
1012 fn test_a_path_is_not_normalised() -> Outcome<()> {
1013 // `a/./b.json` and `a/b.json` would be the same file after tidying and are different bytes,
1014 // so accepting the first would give one Diamond two addresses.
1015 assert!(check_path("a/./b.json").is_err());
1016 res!(check_path("a/b.json"));
1017 Ok(())
1018 }
1019
1020 #[test]
1021 fn test_an_empty_share_is_refused() -> Outcome<()> {
1022 let mut m = match res!(sample().to_dat()) {
1023 Dat::Map(m) => m,
1024 other => return Err(err!(
1025 "A share encodes as a map, and this is a {:?}.", other.kind(); Test, Bug)),
1026 };
1027 m.insert(dat!(KEY_FILES), Dat::List(Vec::new()));
1028 match Share::from_dat(&Dat::Map(m)) {
1029 Ok(_) => Err(err!("A share of no files was accepted."; Test, Invalid)),
1030 Err(_) => Ok(()),
1031 }
1032 }
1033
1034 #[test]
1035 fn test_an_empty_note_is_refused() -> Outcome<()> {
1036 let mut s = sample();
1037 s.note = Some(String::new());
1038 match s.encode() {
1039 Ok(_) => Err(err!(
1040 "An empty note was accepted, so a share with nothing to say has two encodings.";
1041 Test, Invalid)),
1042 Err(_) => Ok(()),
1043 }
1044 }
1045
1046 /// An absent note is omitted, and the two shapes are different bytes.
1047 #[test]
1048 fn test_the_note_is_omitted_not_none() -> Outcome<()> {
1049 let bare = res!(sample().encode());
1050 let mut s = sample();
1051 s.note = Some(fmt!("Here you are."));
1052 assert_ne!(res!(s.encode()), bare);
1053 assert_eq!(res!(Share::decode(&bare)).note, None);
1054 Ok(())
1055 }
1056
1057 #[test]
1058 fn test_trailing_bytes_refused() -> Outcome<()> {
1059 let mut bytes = res!(sample().encode());
1060 bytes.push(0x00);
1061 match Share::decode(&bytes) {
1062 Ok(_) => Err(err!("A payload with a trailing byte was accepted."; Test, Invalid)),
1063 Err(_) => Ok(()),
1064 }
1065 }
1066
1067 #[test]
1068 fn test_ordmap_refused() -> Outcome<()> {
1069 let ord = oxedyne_fe2o3_jdat::map::create_dat_ordmap(vec![
1070 (dat!(KEY_CODE), Dat::Bool(false)),
1071 (dat!(KEY_NAME), Dat::Str(fmt!("N"))),
1072 ]);
1073 match Share::from_dat(&ord) {
1074 Ok(_) => Err(err!("An OrdMap payload was accepted."; Test, Invalid)),
1075 Err(_) => Ok(()),
1076 }
1077 }
1078
1079 #[test]
1080 fn test_unknown_key_refused() -> Outcome<()> {
1081 let mut m = match res!(sample().to_dat()) {
1082 Dat::Map(m) => m,
1083 other => return Err(err!(
1084 "A share encodes as a map, and this is a {:?}.", other.kind(); Test, Bug)),
1085 };
1086 m.insert(dat!("from"), Dat::Str(fmt!("somebody else")));
1087 match Share::from_dat(&Dat::Map(m)) {
1088 Ok(_) => Err(err!("A share carrying a `from` field was accepted."; Test, Invalid)),
1089 Err(_) => Ok(()),
1090 }
1091 }
1092
1093 /// The code bit is required, not optional-and-false-by-default.
1094 #[test]
1095 fn test_a_missing_code_bit_is_refused() -> Outcome<()> {
1096 let mut m = match res!(sample().to_dat()) {
1097 Dat::Map(m) => m,
1098 other => return Err(err!(
1099 "A share encodes as a map, and this is a {:?}.", other.kind(); Test, Bug)),
1100 };
1101 m.remove(&dat!(KEY_CODE));
1102 match Share::from_dat(&Dat::Map(m)) {
1103 Ok(_) => Err(err!(
1104 "A share with no code claim was read, so 'they said no' and 'they did not say' \
1105 are the same artefact."; Test, Invalid)),
1106 Err(_) => Ok(()),
1107 }
1108 }
1109
1110 #[test]
1111 fn test_nonce_and_key_widths_are_exact() -> Outcome<()> {
1112 let mut s = sample();
1113 s.nonce = vec![0xB2; limit::NONCE_BYTES - 1];
1114 assert!(s.encode().is_err(), "A short nonce was accepted.");
1115 let mut s = sample();
1116 s.to = vec![0xA1; limit::KEY_BYTES + 1];
1117 assert!(s.encode().is_err(), "An overlong recipient key was accepted.");
1118 Ok(())
1119 }
1120
1121 /// Two identical shares to one recipient are two addresses, because the nonce is signed.
1122 #[test]
1123 fn test_the_nonce_separates_identical_shares() -> Outcome<()> {
1124 let a = sample();
1125 let mut b = sample();
1126 b.nonce = vec![0xB3; limit::NONCE_BYTES];
1127 assert_eq!(a.files, b.files);
1128 assert_ne!(res!(a.encode()), res!(b.encode()));
1129 Ok(())
1130 }
1131
1132 #[test]
1133 fn test_too_many_files_refused() -> Outcome<()> {
1134 let mut files = Vec::new();
1135 for i in 0..(limit::FILES + 1) {
1136 files.push(File { path: fmt!("f{:04}.json", i), body: b"{}".to_vec() });
1137 }
1138 let s = Share::new(fmt!("Many"), vec![0xA1; 32], vec![0xB2; 16], None, files);
1139 match s.encode() {
1140 Ok(_) => Err(err!("More files than the limit were accepted."; Test, Invalid)),
1141 Err(_) => Ok(()),
1142 }
1143 }
1144
1145 /// The ceiling is a boundary and not a scare: exactly the limit is accepted.
1146 #[test]
1147 fn test_files_at_the_limit_accepted() -> Outcome<()> {
1148 let mut files = Vec::new();
1149 for i in 0..limit::FILES {
1150 files.push(File { path: fmt!("f{:04}.json", i), body: b"{}".to_vec() });
1151 }
1152 let s = Share::new(fmt!("Many"), vec![0xA1; 32], vec![0xB2; 16], None, files);
1153 let bytes = res!(s.encode());
1154 assert_eq!(res!(Share::decode(&bytes)).files.len(), limit::FILES);
1155 Ok(())
1156 }
1157
1158 #[test]
1159 fn test_total_bytes_over_the_limit_refused() -> Outcome<()> {
1160 let s = Share::new(fmt!("Heavy"), vec![0xA1; 32], vec![0xB2; 16], None, vec![
1161 File { path: fmt!("a.bin"), body: vec![0u8; limit::TOTAL_BYTES / 2] },
1162 File { path: fmt!("b.bin"), body: vec![0u8; limit::TOTAL_BYTES / 2 + 1] },
1163 ]);
1164 match s.encode() {
1165 Ok(_) => Err(err!("A share over the byte ceiling was accepted."; Test, Invalid)),
1166 Err(_) => Ok(()),
1167 }
1168 }
1169
1170 /// A file body is BYTES and is held to no text rule: a Diamond holds pictures too.
1171 #[test]
1172 fn test_a_body_may_be_arbitrary_bytes() -> Outcome<()> {
1173 let s = Share::new(fmt!("Picture"), vec![0xA1; 32], vec![0xB2; 16], None, vec![
1174 File { path: fmt!("shot.png"), body: vec![0x89, 0x50, 0x4E, 0x47, 0x00, 0xFF] },
1175 ]);
1176 let bytes = res!(s.encode());
1177 assert_eq!(res!(Share::decode(&bytes)).files[0].body, s.files[0].body);
1178 Ok(())
1179 }
1180
1181 /// A path, unlike a body, is text and is held to §3 rule 5.
1182 #[test]
1183 fn test_a_path_must_be_nfc() -> Outcome<()> {
1184 assert!(check_path("cafe\u{0301}/notes.md").is_err(),
1185 "A path with a combining accent was accepted, so one file has two spellings.");
1186 res!(check_path("caf\u{e9}/notes.md"));
1187 Ok(())
1188 }
1189}