Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_sbj/examples/gen_fixtures.rs

67.9 KiB, 57 runs

created by r1870400018:21944, 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//! Writes the conformance fixtures of `SPEC.md` §7.
2//!
3//! Run it with `cargo run -p sbj --example gen_fixtures`. Every artefact under `fixtures/` comes
4//! from here, so that when the format changes the fixtures are rebuilt rather than patched, and no
5//! byte of them is a byte nobody can reproduce.
6//!
7//! Two things this deliberately does not do. It does not sign with a fresh key, since a fixture that
8//! changes on every run is not a fixture: the key is committed at `fixtures/key.jdat`, and is
9//! generated once, the first time this runs against an empty directory. And it does not ask the
10//! implementation what it thinks of a bad file: every `reject.jdat` is written from `SPEC.md`, by
11//! hand, so that the suite tests the code against the specification rather than against itself.
12
13#[path = "../tests/common/mod.rs"]
14mod common;
15
16use common::{
17 tree,
18 Keys,
19 Meta,
20 Reject,
21 Stage,
22 ALIEN_CODE,
23 DOC_JDAT,
24 DOC_SBJ,
25 KEY_FILE,
26 META_JDAT,
27 README_FILE,
28 REJECT_JDAT,
29 TIME,
30};
31
32use oxedyne_fe2o3_sbj::{
33 canon,
34 card::{
35 self,
36 Card,
37 Role,
38 },
39 doc::{
40 self,
41 Payload,
42 },
43 envelope,
44 kinds::{
45 NodeKind,
46 ReservedKind,
47 },
48 post::{
49 self,
50 Post,
51 Reference,
52 Target,
53 },
54 share::{
55 self,
56 Share,
57 },
58 validate,
59 SCHEMA_CARD,
60 SCHEMA_DOC,
61 SCHEMA_POST,
62 SCHEMA_SHARE,
63};
64
65use oxedyne_fe2o3_core::prelude::*;
66use oxedyne_fe2o3_crypto::sign::SignatureScheme;
67use oxedyne_fe2o3_jdat::prelude::*;
68
69use std::{
70 fs,
71 path::{
72 Path,
73 PathBuf,
74 },
75};
76
77/// A schema no validator in this build reads, for the fixture that declares one.
78const SCHEMA_FOREIGN: &'static str = "oxeweb/cmd/0";
79
80fn main() {
81 // The deepest legal document nests daticles 770 deep, and encoding one costs a stack frame at
82 // every level, so the work runs on a thread with a stack that can hold it.
83 let thread = match std::thread::Builder::new()
84 .name("gen_fixtures".to_string())
85 .stack_size(common::STACK_BYTES)
86 .spawn(generate)
87 {
88 Ok(thread) => thread,
89 Err(e) => {
90 println!("Could not spawn the generator thread: {}", e);
91 std::process::exit(1);
92 },
93 };
94 match thread.join() {
95 Ok(Ok(n)) => println!("Wrote {} fixtures.", n),
96 Ok(Err(e)) => {
97 println!("{}", e);
98 std::process::exit(1);
99 },
100 Err(_) => {
101 println!("The generator thread did not return.");
102 std::process::exit(1);
103 },
104 }
105}
106
107/// Writes every fixture, and returns how many were written.
108fn generate() -> Outcome<usize> {
109
110 let root = common::fixtures_dir();
111 res!(fs::create_dir_all(&root), IO, File);
112
113 // The key is generated once and committed. A fixture signed by a key that changed would carry a
114 // different signature every run, and a suite that had to be regenerated to pass would test
115 // nothing.
116 let keys = if root.join(KEY_FILE).exists() {
117 res!(Keys::load(&root))
118 } else {
119 let keys = res!(Keys::generate());
120 res!(keys.save(&root));
121 keys
122 };
123
124 res!(common::write_bytes(&root.join(README_FILE), readme().as_bytes()));
125
126 let n = res!(acceptance(&root, &keys)) + res!(rejection(&root, &keys))
127 + res!(payloads(&root, &keys)) + res!(shares(&root, &keys));
128
129 // A fixture directory nothing above wrote is a fixture nobody can regenerate, and the suite will
130 // refuse to skip it, so it is caught here rather than there.
131 let mut dirs = 0;
132 for entry in res!(fs::read_dir(&root), IO, File) {
133 if res!(entry, IO, File).path().is_dir() {
134 dirs += 1;
135 }
136 }
137 if dirs != n {
138 return Err(err!(
139 "{} fixtures were written, and {} directories are in {}. A directory nothing here wrote \
140 is stale: delete it, or write the fixture that belongs in it.", n, dirs, root.display();
141 Invalid, Mismatch));
142 }
143 Ok(n)
144}
145
146/// The fixtures a reader must accept.
147fn acceptance(
148 root: &Path,
149 keys: &Keys,
150)
151 -> Outcome<usize>
152{
153 let author = res!(keys.author.signer());
154
155 res!(accept(root, &author, "empty", &tree::empty(), false,
156 "A document with no content at all: the smallest thing that is still a document."));
157
158 res!(accept(root, &author, "one_para", &tree::one_para(), false,
159 "One paragraph of one text run."));
160
161 res!(accept(root, &author, "every_kind", &tree::every_kind(), false,
162 "Every v0 node kind once, including code and quote, a link by the typed address form and an \
163 image by content hash, an optional field present, another absent, and a node carrying no \
164 children."));
165
166 res!(accept(root, &author, "styled", &tree::styled(), false,
167 "A document exercising the style table of §4.4: an inherited property (size) and a self-only \
168 property (bg), a box naming one style and a paragraph within it naming another, each \
169 resolving to a table entry."));
170
171 res!(accept(root, &author, "align_is_local", &tree::align_is_local(), false,
172 "A box naming `align: justify`, holding a paragraph that names nothing. `align` is self-only \
173 (§4.4), so it aligns the lines of the node that named it, and the box has none: the \
174 paragraph inside must be set exactly as it would be with no style at all. The property \
175 inherits in CSS and does not in the format, so a reader built on a browser will justify the \
176 paragraph unless it is stopped, and then one document says two things."));
177
178 res!(accept(root, &author, "link_by_name", &tree::link_by_name(), false,
179 "A link whose typed address (§4.3) is a NAMES name."));
180
181 res!(accept(root, &author, "link_by_hash", &tree::link_by_hash(), false,
182 "A link whose typed address (§4.3) is a b32 content hash."));
183
184 res!(accept(root, &author, "unknown_kind_fallback", &tree::unknown_fallback(), false,
185 "An unknown node kind carrying a non-empty fallback of known nodes (§4.5), which a reader \
186 that does not implement the kind renders and validates in its place, so the document is \
187 accepted. The unknown node also carries an uninterpreted field, held only to §3."));
188
189 res!(accept(root, &author, "indexed", &tree::one_para(), true,
190 "The document of the one_para fixture, written with the optional index of §1.4 appended. \
191 The index lies outside the hash, so this is the same document at the same address, and its \
192 hash is the hash of one_para."));
193
194 res!(accept(root, &author, "depth_at_limit", &res!(tree::chain(common::depth_limit())), false,
195 "Nesting at the depth limit of §5: a doc at the head of a chain of boxes, 256 nodes deep."));
196
197 res!(accept(root, &author, "size_at_limit", &res!(tree::sized(common::tree_limit())), false,
198 "A tree region of exactly the size limit of §5, to the byte."));
199
200 Ok(11)
201}
202
203/// Writes an acceptance fixture: the tree, the artefact, and what the artefact must turn out to be.
204fn accept(
205 root: &Path,
206 author: &SignatureScheme,
207 name: &str,
208 tree: &Dat,
209 index: bool,
210 note: &str,
211)
212 -> Outcome<()>
213{
214 let dir = res!(fresh_dir(root, name));
215 let buf = if index {
216 res!(doc::write_with_index(tree, SCHEMA_DOC, author, TIME))
217 } else {
218 res!(doc::write(tree, SCHEMA_DOC, author, TIME))
219 };
220 let env = res!(doc::verify_only(&buf));
221 let stats = res!(validate::validate(tree, SCHEMA_DOC));
222 let meta = Meta {
223 schema: env.schema.clone(),
224 time: env.time,
225 hash: env.hash.clone(),
226 tree_len: env.tree_len,
227 nodes: Some(try_into!(u64, stats.nodes)),
228 depth: Some(try_into!(u64, stats.depth)),
229 index,
230 note: note.to_string(),
231 };
232 res!(common::write_bytes(&dir.join(DOC_JDAT), res!(common::to_jdat(tree)).as_bytes()));
233 res!(common::write_bytes(&dir.join(DOC_SBJ), &buf));
234 res!(common::write_bytes(
235 &dir.join(META_JDAT),
236 res!(common::to_jdat_plain(&meta.to_dat())).as_bytes(),
237 ));
238 Ok(())
239}
240
241/// The fixtures a reader must refuse, and the reason each must be refused for.
242fn rejection(
243 root: &Path,
244 keys: &Keys,
245)
246 -> Outcome<usize>
247{
248 let author = res!(keys.author.signer());
249 let impostor = res!(keys.impostor.signer());
250
251 // A good document, and the file that carries it. Every fixture below is this file with one
252 // thing wrong with it, or a file assembled around one tree that is wrong in one way.
253 let good = res!(canon::encode(&tree::every_kind()));
254 let file = res!(common::assemble(&res!(common::seal(&good, SCHEMA_DOC, &author, TIME)), &good));
255
256 // -- Step 1: the header. ------------------------------------------------------------------
257
258 // A file that is perfectly good BDAT and is not an SBJ file at all: the encoded tree of a real
259 // document, with nothing in front of it.
260 res!(reject(root, "bdat_not_sbj", &good, None, Reject {
261 stage: Stage::Header,
262 rule: "SPEC.md §1.1: a reader that does not recognise the magic stops.".to_string(),
263 says: "Not an SBJ file".to_string(),
264 node: None,
265 offset: None,
266 note: "Valid BDAT: the encoded tree of a real document, with no SBJ header in front of \
267 it. Nothing about a bare daticle says which format it belongs to, so the magic is \
268 what says so, and its absence is a rejection rather than a guess.".to_string(),
269 }));
270
271 let mut bad = file.clone();
272 bad[1] ^= 0xFF;
273 res!(reject(root, "bad_magic", &bad, None, Reject {
274 stage: Stage::Header,
275 rule: "SPEC.md §1.1: the magic is 'SBJ\\0'.".to_string(),
276 says: "Not an SBJ file".to_string(),
277 node: None,
278 offset: None,
279 note: "Byte 1 of the magic is flipped. Everything after it is a valid document."
280 .to_string(),
281 }));
282
283 let mut bad = file.clone();
284 bad[5] = 1;
285 res!(reject(root, "bad_version", &bad, None, Reject {
286 stage: Stage::Header,
287 rule: "SPEC.md §1.1: a reader that reads a major version it does not implement stops."
288 .to_string(),
289 says: "not implemented here".to_string(),
290 node: None,
291 offset: None,
292 note: "The major version is 1, and this reads version 0. It does not guess.".to_string(),
293 }));
294
295 // -- Step 2: the envelope. ----------------------------------------------------------------
296
297 let env = res!(common::seal(&good, SCHEMA_DOC, &author, TIME));
298 let mut map = match res!(env.to_dat()) {
299 Dat::Map(map) => map,
300 d => return Err(err!("An envelope is a map, found a {:?}.", d.kind(); Bug, Invalid)),
301 };
302 map.remove(&dat!(envelope::KEY_TIME));
303 let bad = res!(common::assemble_raw(&Dat::Map(map), &good));
304 res!(reject(root, "envelope_missing_key", &bad, Some(&tree::every_kind()), Reject {
305 stage: Stage::Envelope,
306 rule: "SPEC.md §1.2: the envelope carries exactly these keys, all required.".to_string(),
307 says: "missing the required key \"time\"".to_string(),
308 node: None,
309 offset: None,
310 note: "The 'time' key is gone from the envelope map. The tree behind it is a good \
311 document, and is never reached.".to_string(),
312 }));
313
314 // -- Step 3: the tree region. -------------------------------------------------------------
315
316 let bad = file[..file.len() - 1].to_vec();
317 res!(reject(root, "truncated_tree", &bad, None, Reject {
318 stage: Stage::Region,
319 rule: "SPEC.md §2 step 3: a tree region shorter than declared is a rejection, not a \
320 truncation.".to_string(),
321 says: "shorter than declared".to_string(),
322 node: None,
323 offset: None,
324 note: "The last byte of the tree region is gone. The envelope still declares the length \
325 the region had, and the reader believes neither the bytes nor the envelope: it \
326 refuses the file.".to_string(),
327 }));
328
329 let over = res!(tree::sized(common::tree_limit() + 1));
330 let over_bytes = res!(canon::encode(&over));
331 let bad = res!(common::assemble(
332 &res!(common::seal(&over_bytes, SCHEMA_DOC, &author, TIME)),
333 &over_bytes,
334 ));
335 res!(reject(root, "size_over_limit", &bad, None, Reject {
336 stage: Stage::Region,
337 rule: "SPEC.md §5: the tree region size limit is 4 MiB, enforced before decoding."
338 .to_string(),
339 says: "exceeding the limit".to_string(),
340 node: None,
341 offset: None,
342 note: "A tree region of 4 MiB and one byte, correctly hashed and correctly signed. The \
343 limit is enforced on the envelope's word alone, before a byte of the region is \
344 hashed, let alone decoded.".to_string(),
345 }));
346
347 // -- Step 4: the hash. --------------------------------------------------------------------
348
349 let start = res!(common::tree_start(&file));
350 let mut bad = file.clone();
351 bad[start + 3] ^= 0x01;
352 res!(reject(root, "corrupt_tree_byte", &bad, None, Reject {
353 stage: Stage::Hash,
354 rule: "SPEC.md §2 step 4: the tree region hashes to what the envelope declares."
355 .to_string(),
356 says: "hashes to".to_string(),
357 node: None,
358 offset: None,
359 note: "One bit of one byte of the tree region is flipped. The hash is the document's \
360 address, so a tree that does not hash to it is not this document.".to_string(),
361 }));
362
363 // The author signs the corrupted hash, so the signature is sound and the hash alone is wrong.
364 let mut env = res!(common::seal(&good, SCHEMA_DOC, &author, TIME));
365 env.hash[0] ^= 0x01;
366 res!(common::resign(&mut env, &author));
367 let bad = res!(common::assemble(&env, &good));
368 res!(reject(root, "bad_hash", &bad, Some(&tree::every_kind()), Reject {
369 stage: Stage::Hash,
370 rule: "SPEC.md §2 step 4: a hash that is not the hash of the tree region is a rejection."
371 .to_string(),
372 says: "hashes to".to_string(),
373 node: None,
374 offset: None,
375 note: "The hash in the envelope is corrupted, and the author has signed the corrupted \
376 hash, so the signature is sound and only the hash is wrong. The reader hashes the \
377 region itself rather than taking the envelope's word for it.".to_string(),
378 }));
379
380 // -- Step 5: the signature. ---------------------------------------------------------------
381
382 let mut env = res!(common::seal(&good, SCHEMA_DOC, &author, TIME));
383 env.sig[0] ^= 0x01;
384 let bad = res!(common::assemble(&env, &good));
385 res!(reject(root, "bad_sig", &bad, Some(&tree::every_kind()), Reject {
386 stage: Stage::Sig,
387 rule: "SPEC.md §2 step 5: the signature is verified over the signing input, under the \
388 author's key.".to_string(),
389 says: "not a signature by the author".to_string(),
390 node: None,
391 offset: None,
392 note: "One bit of the signature is flipped. The hash is right, so the document is at the \
393 address it claims to be at; nobody has vouched for it.".to_string(),
394 }));
395
396 // A perfectly good signature, made by a key that is not the author the envelope names.
397 let mut env = res!(common::seal(&good, SCHEMA_DOC, &impostor, TIME));
398 env.author = keys.author.pk.clone();
399 let bad = res!(common::assemble(&env, &good));
400 res!(reject(root, "wrong_key", &bad, Some(&tree::every_kind()), Reject {
401 stage: Stage::Sig,
402 rule: "SPEC.md §2 step 5: the signature must be the author's.".to_string(),
403 says: "not a signature by the author".to_string(),
404 node: None,
405 offset: None,
406 note: "A sound signature over the right signing input, made by a key that is not the \
407 author the envelope names. A signature nobody checks against a key is not a \
408 signature.".to_string(),
409 }));
410
411 // -- Step 6: decoding, and the canonical encoding rules of §3. ----------------------------
412
413 // The tree region is one byte short of the tree it holds, and the author has hashed and signed
414 // the short region, so every step that touches no content passes. The tree is cut off.
415 let short = &good[..good.len() - 1];
416 let mut env = res!(common::seal(short, SCHEMA_DOC, &author, TIME));
417 env.tree_len = try_into!(u64, short.len());
418 res!(common::resign(&mut env, &author));
419 let bad = res!(common::assemble(&env, short));
420 res!(reject(root, "tree_longer_than_tree_len", &bad, None, Reject {
421 stage: Stage::Decode,
422 rule: "SPEC.md §2: a tree region that does not hold a whole tree is a rejection."
423 .to_string(),
424 says: SAYS_TRUNCATED.to_string(),
425 node: None,
426 offset: None,
427 note: "'tree_len' understates the tree by one byte, and the author has hashed and signed \
428 the region it declares, so the file verifies. The declared region holds a tree cut \
429 off one byte from its end, and the decoder says so.".to_string(),
430 }));
431
432 let mut region = good.clone();
433 region.push(0x00);
434 let bad = res!(common::assemble(
435 &res!(common::seal(&region, SCHEMA_DOC, &author, TIME)),
436 &region,
437 ));
438 res!(reject(root, "bytes_trailing_the_tree", &bad, None, Reject {
439 stage: Stage::Decode,
440 rule: "SPEC.md §3: a document encodes to exactly one byte string.".to_string(),
441 says: "canonical".to_string(),
442 node: None,
443 offset: None,
444 note: "The tree region carries the tree and one byte more, all of it hashed and signed. \
445 A byte the tree does not need gives the document a second address, so it is no \
446 part of a canonical encoding.".to_string(),
447 }));
448
449 res!(canon_reject(root, &author, "canon_rule1_undeclared_field", Some(1), "rule 1",
450 "SPEC.md §3 rule 1: field types are fixed by the schema.",
451 "The heading carries a field the schema does not declare. A reader that ignored it would \
452 accept two byte strings for one document.",
453 tree::doc_with_heading(common::map(vec![
454 ("colour", Dat::Str("red".to_string())),
455 ("level", Dat::U8(2)),
456 ("children", Dat::List(vec![common::text("A heading")])),
457 ])),
458 ));
459
460 res!(canon_reject(root, &author, "canon_rule2_ordmap", Some(1), "rule 2",
461 "SPEC.md §3 rule 2: maps are Dat::Map, never Dat::OrdMap.",
462 "The heading's payload is an OrdMap, whose order follows the author's typing rather than \
463 its keys, so the same heading typed in another order is a different byte string.",
464 tree::doc_with_heading(create_dat_ordmap(vec![
465 (Dat::Str("level".to_string()), Dat::U8(2)),
466 (Dat::Str("children".to_string()), Dat::List(vec![common::text("A heading")])),
467 ])),
468 ));
469
470 res!(canon_reject(root, &author, "canon_rule3_uppercase_key", Some(1), "rule 3",
471 "SPEC.md §3 rule 3: map keys are strings, lowercase ASCII.",
472 "The heading's level is spelled 'Level'. A key that may be spelled two ways is a document \
473 with two addresses.",
474 tree::doc_with_heading(common::map(vec![
475 ("Level", Dat::U8(2)),
476 ("children", Dat::List(vec![common::text("A heading")])),
477 ])),
478 ));
479
480 // A duplicate key cannot be built as a tree at all: BDAT decodes a map into a BTreeMap, which
481 // collapses the duplicate, so the tree that comes out is perfectly canonical. It survives only
482 // in the bytes, so the bytes are built by hand here, and the first byte at which they differ
483 // from the canonical encoding of what they decode to is computed here too, rather than read out
484 // of the error the implementation happens to give.
485 let dup = res!(duplicate_key_bytes());
486 let (decoded, n) = res!(Dat::from_bytes(&dup));
487 if n != dup.len() {
488 return Err(err!(
489 "The hand-built bytes of the duplicate key fixture do not decode whole."; Bug, Invalid));
490 }
491 let canonical = res!(canon::encode(&decoded));
492 let at = match common::first_diff(&canonical, &dup) {
493 Some(at) => at,
494 None => return Err(err!("The duplicate key did not change the bytes."; Bug, Invalid)),
495 };
496 let bad = res!(common::assemble(
497 &res!(common::seal(&dup, SCHEMA_DOC, &author, TIME)),
498 &dup,
499 ));
500 res!(reject(root, "canon_rule3_duplicate_key", &bad, None, Reject {
501 stage: Stage::Decode,
502 rule: "SPEC.md §3 rule 3: no map key may appear twice.".to_string(),
503 says: "rule 3".to_string(),
504 node: None,
505 offset: Some(try_into!(u64, at)),
506 note: "The doc's payload map carries the key 'lang' twice. The tree it decodes to is \
507 perfectly canonical, since a BTreeMap cannot hold a duplicate, so the duplicate \
508 survives only in the bytes: one tree, two byte strings, two addresses. The offset \
509 is the first byte at which the bytes differ from the canonical encoding of the \
510 tree they decode to.".to_string(),
511 }));
512
513 res!(canon_reject(root, &author, "canon_rule4_empty_children", Some(1), "rule 4",
514 "SPEC.md §3 rule 4: no redundant wrappers, and a node with no children omits the key.",
515 "The heading carries an empty children list rather than omitting the key, which gives a \
516 childless heading two encodings.",
517 tree::doc_with_heading(common::map(vec![
518 ("level", Dat::U8(2)),
519 ("children", Dat::List(Vec::new())),
520 ])),
521 ));
522
523 res!(canon_reject(root, &author, "canon_rule4_opt_none", Some(1), "rule 4",
524 "SPEC.md §3 rule 4: an absent optional field is omitted, not encoded as a none.",
525 "The section's optional title is written as a none rather than left out, so an untitled \
526 section has two encodings.",
527 tree::doc_of(vec![
528 common::node(NodeKind::Section, common::map(vec![
529 ("title", Dat::Opt(Box::new(None))),
530 ("children", Dat::List(vec![
531 common::node(NodeKind::Para, common::map(vec![
532 ("children", Dat::List(vec![common::text("A paragraph.")])),
533 ])),
534 ])),
535 ])),
536 ]),
537 ));
538
539 res!(canon_reject(root, &author, "canon_rule5_control_char", Some(2), "rule 5",
540 "SPEC.md §3 rule 5: strings carry no C0 or C1 control characters other than tab and \
541 newline.",
542 "The text run carries a carriage return, so one line ending would have two encodings.",
543 tree::doc_with_heading(common::map(vec![
544 ("level", Dat::U8(2)),
545 ("children", Dat::List(vec![common::text("a carriage\rreturn")])),
546 ])),
547 ));
548
549 res!(canon_reject(root, &author, "canon_rule5_not_nfc", Some(2), "rule 5",
550 "SPEC.md §3 rule 5: strings are in Unicode NFC.",
551 "The text run spells café with a combining acute accent rather than the composed letter. \
552 It displays identically to the composed form and hashes differently, so the one document \
553 would have two addresses.",
554 tree::doc_with_heading(common::map(vec![
555 ("level", Dat::U8(2)),
556 ("children", Dat::List(vec![common::text("cafe\u{0301}")])),
557 ])),
558 ));
559
560 res!(canon_reject(root, &author, "canon_rule6_int_width", Some(1), "rules 1 and 6",
561 "SPEC.md §3 rule 6: integers are exactly the declared width, with no promotion and no \
562 demotion.",
563 "The heading's level is a u32 where the schema declares a u8. Both decode to the number 2, \
564 and they are different byte strings, so they are two addresses for one document.",
565 tree::doc_with_heading(common::map(vec![
566 ("level", Dat::U32(2)),
567 ("children", Dat::List(vec![common::text("A heading")])),
568 ])),
569 ));
570
571 let vek = match Vek::try_from(vec![common::text("A heading")]) {
572 Ok(vek) => vek,
573 Err(e) => return Err(err!(e, "Could not build a Vek of one text run."; Bug, Invalid)),
574 };
575 res!(canon_reject(root, &author, "canon_rule7_vek_children", Some(1), "rule 7",
576 "SPEC.md §3 rule 7: lists are Dat::List, not Dat::Vek, even where every element shares a \
577 kind.",
578 "The heading's children sit in a Vek. Every child of a heading is a node, so a Vek is \
579 always available, and always a second encoding of the same list.",
580 tree::doc_with_heading(common::map(vec![
581 ("level", Dat::U8(2)),
582 ("children", Dat::Vek(vek)),
583 ])),
584 ));
585
586 // SPEC §4.5: an unknown kind is canonical enough to decode, so canon accepts it, and the
587 // validator is what refuses one whose payload carries no non-empty fallback of known nodes. The
588 // alien here carries an ordinary map with an uninterpreted field but no fallback, so it verifies,
589 // decodes canonically, and is refused at validation.
590 res!(schema_reject(root, &author, "unknown_kind", 1,
591 "fallback",
592 &fmt!("SPEC.md §4.5: an unknown kind code {} is permitted only with a non-empty fallback of \
593 known nodes.", ALIEN_CODE),
594 "The document's one child declares a kind code that names no v0 node kind and carries no \
595 fallback. The bytes are canonical, so the file verifies and decodes; the vocabulary rule of \
596 §4.5 is what refuses it, naming the node and the missing fallback.",
597 tree::doc_of(vec![
598 common::alien(common::map(vec![
599 ("rows", Dat::Str("no fallback here".to_string())),
600 ])),
601 ]),
602 ));
603
604 let deep = res!(tree::chain(common::depth_limit() + 1));
605 let deep_bytes = res!(common::encode_unchecked(&deep));
606 let bad = res!(common::assemble(
607 &res!(common::seal(&deep_bytes, SCHEMA_DOC, &author, TIME)),
608 &deep_bytes,
609 ));
610 res!(reject(root, "depth_over_limit", &bad, None, Reject {
611 stage: Stage::Decode,
612 rule: fmt!("SPEC.md §5: the nesting depth limit is {}, enforced during decoding.",
613 common::depth_limit()),
614 says: fmt!("past the limit of {}", common::depth_limit()),
615 node: Some(try_into!(u64, common::depth_limit())),
616 offset: None,
617 note: fmt!("A doc at the head of a chain of boxes {} nodes deep, correctly hashed and \
618 correctly signed. A tiny file describing a deep nest is the cheapest attack there \
619 is against a recursive decoder, so the depth limit is the decoder's rather than \
620 the validator's.", common::depth_limit() + 1),
621 }));
622
623 // -- Step 7: the schema, and the limits that are not the decoder's. -----------------------
624
625 let foreign = res!(canon::encode(&tree::one_para()));
626 let bad = res!(common::assemble(
627 &res!(common::seal(&foreign, SCHEMA_FOREIGN, &author, TIME)),
628 &foreign,
629 ));
630 res!(reject(root, "foreign_schema", &bad, Some(&tree::one_para()), Reject {
631 stage: Stage::Validate,
632 rule: "SPEC.md §1.2: the envelope declares the schema of its payload.".to_string(),
633 says: SCHEMA_FOREIGN.to_string(),
634 node: None,
635 offset: None,
636 note: "A sound envelope over a sound tree, declaring a schema this build does not \
637 validate. The container carries any schema, and a reader that validates one \
638 refuses the others rather than reading them as though they were documents."
639 .to_string(),
640 }));
641
642 res!(schema_reject(root, &author, "para_in_para", 2,
643 "admits inline content only",
644 "SPEC.md §4.2: a para takes inline content.",
645 "A paragraph inside a paragraph. The tree is canonical and the file verifies; the \
646 vocabulary is what refuses it.",
647 tree::doc_of(vec![
648 common::node(NodeKind::Para, common::map(vec![
649 ("children", Dat::List(vec![
650 common::node(NodeKind::Para, common::map(vec![
651 ("children", Dat::List(vec![common::text("Inner")])),
652 ])),
653 ])),
654 ])),
655 ]),
656 ));
657
658 res!(schema_reject(root, &author, "heading_level_0", 1,
659 "1..=6",
660 "SPEC.md §4.2: a heading's level runs from 1 to 6.",
661 "A heading of level 0. The field is a u8, exactly as the schema declares, and 0 is not a \
662 heading level.",
663 tree::doc_with_heading(common::map(vec![
664 ("level", Dat::U8(0)),
665 ("children", Dat::List(vec![common::text("A heading")])),
666 ])),
667 ));
668
669 res!(schema_reject(root, &author, "heading_level_7", 1,
670 "1..=6",
671 "SPEC.md §4.2: a heading's level runs from 1 to 6.",
672 "A heading of level 7, one past the deepest heading the format has.",
673 tree::doc_with_heading(common::map(vec![
674 ("level", Dat::U8(7)),
675 ("children", Dat::List(vec![common::text("A heading")])),
676 ])),
677 ));
678
679 res!(schema_reject(root, &author, "empty_list", 1,
680 "must carry at least one",
681 "SPEC.md §4.2: a list is marked `+` and carries at least one item.",
682 "A list with an ordered field but no items. A document or section may be empty, but an \
683 empty list is a construction error rather than intent.",
684 tree::doc_of(vec![
685 common::node(NodeKind::List, common::map(vec![
686 ("ordered", Dat::Bool(false)),
687 ])),
688 ]),
689 ));
690
691 // A style field naming an entry the table does not define (§4.4). The table defines 'callout',
692 // and the box names 'ghost'. The bytes are canonical, so the fault is the validator's: a style
693 // name must resolve to a table entry, named at the node that made the reference.
694 res!(schema_reject(root, &author, "style_missing_entry", 1,
695 "ghost",
696 "SPEC.md §4.4: a node's style field must name an entry the document's style table defines.",
697 "A box names the style 'ghost', which the document's style table, defining only 'callout', \
698 does not. A style error cannot escape the node that made it, and this one is named at the \
699 box.",
700 common::node(NodeKind::Doc, common::map(vec![
701 ("title", Dat::Str("A style with no entry".to_string())),
702 ("lang", Dat::Str("en".to_string())),
703 ("styles", common::map(vec![
704 ("callout", common::map(vec![
705 ("bg", Dat::Str("muted".to_string())),
706 ])),
707 ])),
708 ("children", Dat::List(vec![
709 common::node(NodeKind::Boxx, common::map(vec![
710 ("style", Dat::Str("ghost".to_string())),
711 ("children", Dat::List(vec![
712 common::node(NodeKind::Para, common::map(vec![
713 ("children", Dat::List(vec![common::text("in a box")])),
714 ])),
715 ])),
716 ])),
717 ])),
718 ])),
719 ));
720
721 // A style record whose value is out of its enumeration (§4.4). The 'bg' property is a palette
722 // name, and 'purple' is not one. The bytes are canonical, since canon pins only the type of a
723 // palette value and not its membership, so the validator is what refuses it, at the doc where the
724 // table is validated.
725 res!(schema_reject(root, &author, "style_out_of_enum", 0,
726 "purple",
727 "SPEC.md §4.4: a palette property carries a palette name; the palette is ink, muted, accent, \
728 bg.",
729 "The style 'callout' declares a background of 'purple', which is not a palette name. The \
730 style table is validated at the doc, so the rejection names node 0 and the offending style.",
731 common::node(NodeKind::Doc, common::map(vec![
732 ("title", Dat::Str("A colour off the palette".to_string())),
733 ("lang", Dat::Str("en".to_string())),
734 ("styles", common::map(vec![
735 ("callout", common::map(vec![
736 ("bg", Dat::Str("purple".to_string())),
737 ])),
738 ])),
739 ("children", Dat::List(vec![
740 common::node(NodeKind::Para, common::map(vec![
741 ("children", Dat::List(vec![common::text("a paragraph")])),
742 ])),
743 ])),
744 ])),
745 ));
746
747 // A malformed link address carrying two entries (§4.3). An address is a single-entry map, and
748 // this one names both a name and a hash. Canon pins each entry's type but not the count, so the
749 // bytes are canonical and the validator refuses the address, naming the link node.
750 res!(schema_reject(root, &author, "link_two_entries", 2,
751 "not a valid link address",
752 "SPEC.md §4.3: a link address is a map with exactly one entry.",
753 "The link's 'to' address carries both a name and a hash. A typed address selects one kind, \
754 so two entries is not an address at all, and the reader refuses it at the door rather than \
755 letting the renderer choose.",
756 tree::doc_of(vec![
757 common::node(NodeKind::Para, common::map(vec![
758 ("children", Dat::List(vec![
759 common::node(NodeKind::Link, common::map(vec![
760 ("to", common::map(vec![
761 ("name", Dat::Str("news.cricket".to_string())),
762 ("hash", Dat::from([0x9fu8; 32])),
763 ])),
764 ("children", Dat::List(vec![common::text("a link")])),
765 ])),
766 ])),
767 ])),
768 ]),
769 ));
770
771 // SPEC §4.2: the codes 14 and 15 are reserved to the chrome and to applications, and the document
772 // schema admits them nowhere. The bytes of all three fixtures below are canonical, since canon has
773 // no schema for a code outside the vocabulary and holds such a node only to §3; the validator is
774 // what refuses them, and it refuses them by name.
775
776 res!(schema_reject(root, &author, "reserved_edit_in_doc", 1,
777 "may not carry an edit node",
778 "SPEC.md §4.2: the kind code 14, `edit`, is reserved to the chrome and to applications, and \
779 the document schema admits the kinds 1 to 13 and no others.",
780 "A document carrying an editable text field. An `edit` is a facility of the engine, reached \
781 by the chrome's address bar and by an application's own form fields, and a document may not \
782 ask for one. The refusal names the kind and says that a document may not carry it.",
783 tree::doc_of(vec![
784 common::reserved(ReservedKind::Edit, common::map(vec![
785 ("placeholder", Dat::Str("Search the oxeweb".to_string())),
786 ])),
787 ]),
788 ));
789
790 res!(schema_reject(root, &author, "reserved_surface_in_doc", 1,
791 "may not carry a surface node",
792 "SPEC.md §4.2: the kind code 15, `surface`, is reserved to applications, and the document \
793 schema admits the kinds 1 to 13 and no others.",
794 "A document carrying a pane for an application to paint. A `surface` is the one place \
795 anything but the author's own data reaches the screen, so a document that could name one \
796 would be a program, and the whole design turns on a document never being one.",
797 tree::doc_of(vec![
798 common::reserved(ReservedKind::Surface, common::map(vec![
799 ("app", Dat::Str("app.modeller".to_string())),
800 ])),
801 ]),
802 ));
803
804 // The hole that §4.5 would leave open if a reserved code were treated as merely unknown. The
805 // fallback here is everything §4.5 asks of one -- a non-empty list of known nodes, which validate
806 // in full -- and it buys the surface nothing. Were it otherwise, an author could put a surface in
807 // a document today, under a fallback that renders innocently, and every reader that later learned
808 // what code 15 meant would begin honouring it: a document that became a program by waiting.
809 res!(schema_reject(root, &author, "reserved_surface_with_fallback_still_refused", 1,
810 "whether or not it carries a fallback",
811 "SPEC.md §4.5: a fallback admits a kind the reader has never heard of, and never one the \
812 reader knows the document schema does not admit.",
813 "A document carrying a `surface` that also carries a valid, non-empty fallback of known \
814 nodes. A fallback is forward compatibility for an unknown code, and code 15 is not unknown: \
815 the reader knows exactly what it has been handed, and knows a document may not carry it, so \
816 it refuses it with the fallback and without.",
817 tree::doc_of(vec![
818 common::reserved(ReservedKind::Surface, common::map(vec![
819 ("app", Dat::Str("app.modeller".to_string())),
820 ("fallback", Dat::List(vec![
821 common::node(NodeKind::Para, common::map(vec![
822 ("children", Dat::List(vec![
823 common::text("A picture of a teapot, rendered by nobody."),
824 ])),
825 ])),
826 ])),
827 ])),
828 ]),
829 ));
830
831 Ok(35)
832}
833
834/// What the decoder says of a tree region that ends before the tree in it does.
835const SAYS_TRUNCATED: &'static str = "Not enough bytes";
836
837/// Writes a fixture whose tree breaks a canonical encoding rule of §3.
838///
839/// The tree is encoded without being checked, since `canon::encode` refuses to give an address to a
840/// document that has no canonical form, which is exactly what the rule being broken means. The bytes
841/// are then hashed and signed like any other, so that the rejection can only be the rule's.
842fn canon_reject(
843 root: &Path,
844 author: &SignatureScheme,
845 name: &str,
846 node: Option<u64>,
847 says: &str,
848 rule: &str,
849 note: &str,
850 tree: Dat,
851)
852 -> Outcome<()>
853{
854 let bytes = res!(common::encode_unchecked(&tree));
855 let bad = res!(common::assemble(
856 &res!(common::seal(&bytes, SCHEMA_DOC, author, TIME)),
857 &bytes,
858 ));
859 reject(root, name, &bad, Some(&tree), Reject {
860 stage: Stage::Decode,
861 rule: rule.to_string(),
862 says: says.to_string(),
863 node,
864 offset: None,
865 note: note.to_string(),
866 })
867}
868
869/// Writes a fixture whose tree is canonical and whose vocabulary is wrong.
870///
871/// The bytes are canonical, so the fixture isolates the schema: everything up to and including the
872/// decoding of the tree succeeds, and validation is what refuses it.
873fn schema_reject(
874 root: &Path,
875 author: &SignatureScheme,
876 name: &str,
877 node: u64,
878 says: &str,
879 rule: &str,
880 note: &str,
881 tree: Dat,
882)
883 -> Outcome<()>
884{
885 let bytes = res!(canon::encode(&tree));
886 let bad = res!(common::assemble(
887 &res!(common::seal(&bytes, SCHEMA_DOC, author, TIME)),
888 &bytes,
889 ));
890 reject(root, name, &bad, Some(&tree), Reject {
891 stage: Stage::Validate,
892 rule: rule.to_string(),
893 says: says.to_string(),
894 node: Some(node),
895 offset: None,
896 note: note.to_string(),
897 })
898}
899
900/// Writes a rejection fixture: the bad artefact, the tree behind it where there is one, and the
901/// failure the reader must produce.
902fn reject(
903 root: &Path,
904 name: &str,
905 buf: &[u8],
906 tree: Option<&Dat>,
907 dec: Reject,
908)
909 -> Outcome<()>
910{
911 let dir = res!(fresh_dir(root, name));
912 res!(common::write_bytes(&dir.join(DOC_SBJ), buf));
913 res!(common::write_bytes(
914 &dir.join(REJECT_JDAT),
915 res!(common::to_jdat_plain(&dec.to_dat())).as_bytes(),
916 ));
917 // A rejection fixture carries the tree in text form only where the tree region of the artefact
918 // is exactly the encoding of that tree. Where the fault is in the bytes rather than in the tree,
919 // there is no tree to carry, and the suite requires none.
920 if let Some(tree) = tree {
921 res!(common::write_bytes(&dir.join(DOC_JDAT), res!(common::to_jdat(tree)).as_bytes()));
922 }
923 Ok(())
924}
925
926/// Empties and returns a fixture's directory, so that a regeneration leaves nothing stale behind.
927fn fresh_dir(root: &Path, name: &str) -> Outcome<PathBuf> {
928 let dir = root.join(name);
929 if dir.exists() {
930 res!(fs::remove_dir_all(&dir), IO, File);
931 }
932 res!(fs::create_dir_all(&dir), IO, File);
933 Ok(dir)
934}
935
936/// The bytes of a doc whose payload map carries the key `lang` twice.
937///
938/// A map is a `BTreeMap` once decoded, so a duplicate key exists only on the wire. The bytes are
939/// therefore written entry by entry, in the order a `BTreeMap` puts them in, with one entry written
940/// twice.
941fn duplicate_key_bytes() -> Outcome<Vec<u8>> {
942
943 let kids = Dat::List(vec![
944 common::node(NodeKind::Para, common::map(vec![
945 ("children", Dat::List(vec![common::text("One paragraph.")])),
946 ])),
947 ]);
948
949 let mut inner = Vec::new();
950 inner = res!(Dat::Str("children".to_string()).to_bytes(inner));
951 inner = res!(kids.to_bytes(inner));
952 for _ in 0..2 { // The duplicate.
953 inner = res!(Dat::Str("lang".to_string()).to_bytes(inner));
954 inner = res!(Dat::Str("en".to_string()).to_bytes(inner));
955 }
956 inner = res!(Dat::Str("title".to_string()).to_bytes(inner));
957 inner = res!(Dat::Str("A document".to_string()).to_bytes(inner));
958
959 let mut payload = vec![Dat::MAP_CODE];
960 payload = res!(Dat::C64(try_into!(u64, inner.len())).to_bytes(payload));
961 payload.extend_from_slice(&inner);
962
963 let mut buf = vec![Dat::USR_CODE];
964 buf.extend_from_slice(&NodeKind::Doc.code().to_be_bytes());
965 buf.push(Dat::OPT_SOME_CODE);
966 buf.extend_from_slice(&payload);
967 Ok(buf)
968}
969
970/// What `fixtures/README.md` says.
971fn readme() -> String {
972 fmt!("\
973# SBJ conformance fixtures
974
975The teeth of `SPEC.md` §7. Written by `examples/gen_fixtures.rs`, run by `tests/conformance.rs`, and
976regenerated rather than patched:
977
978 cargo run -p oxedyne_fe2o3_sbj --example gen_fixtures
979 cargo test -p oxedyne_fe2o3_sbj
980
981Each fixture is a directory.
982
983**Acceptance** fixtures carry `doc.jdat`, the payload in JDAT text form and the source of truth;
984`doc.sbj`, the canonical signed artefact; and `meta.jdat`, what the artefact must turn out to be:
985its address, the length of its payload region, and -- where the payload is a node tree -- its node
986count and its depth. The suite reads `doc.jdat`, signs it with the committed key, and requires the
987bytes it gets back to be `doc.sbj`, byte for byte.
988
989**Not every payload is a node tree.** The container carries any schema (§1.2), and the fixtures
990named `post_*`, `card_*` and `share_*` carry `daimond/post/0`, `daimond/card/0` and
991`daimond/share/0`, which are flat canonical maps rather than trees. Those declare no node count and
992no depth, because they have neither, and their `doc.jdat` is written in plain JDAT with none of the
993`sbj_` node labels below. Everything else about them is identical: the same header, the same
994envelope, the same address, the same signature, and every rule of §3.
995
996The `share_*` fixtures carry one rule the others do not, and it is the reason that schema exists:
997`code` is the sender's SIGNED statement about whether the share carries a program, and it is
998checked against the files both ways. `share_code_hidden` is a page under a claim of no code, and
999`share_code_claimed_without_code` is the opposite. A share is a COPY the receiver comes to own, so
1000there is no live view, nothing to revoke, and no third party in the middle of it.
1001
1002**Rejection** fixtures carry `doc.sbj`, the bad artefact, and `reject.jdat`, which declares the rule
1003broken, the step of §2 that must catch it, what the error must say, and the node or the byte it must
1004name. \"It was rejected\" is not the claim: the claim is that it was rejected for the right reason.
1005A rejection fixture also carries `doc.jdat` where the tree region is the encoding of a tree that can
1006be written down; where the fault is in the bytes themselves, there is no tree to write.
1007
1008Every rejection fixture past the header is correctly hashed and correctly signed, so that the
1009rejection can only have come from the rule the fixture breaks, and never from a signature that
1010happened not to check out.
1011
1012`{}` holds the fixed key every fixture is signed with, and a second key that signs nothing but the
1013fixture of a signature by the wrong hand. It is committed on purpose: a fixture signed by a fresh
1014key would be a different file on every run, and a suite that has to be regenerated to pass tests
1015nothing. It is a test key, published here, and signs nothing else.
1016
1017Node labels in `doc.jdat` carry an `sbj_` prefix, because two of the v0 kind labels, `box` and
1018`list`, are JDAT's own kind labels as well: `(box|{{..}})` would read back as a `Dat::Box`. None of
1019this reaches the wire, where BDAT carries the `u16` kind code and no label at all.
1020
1021The kind code {} appears in the `unknown_kind` and `unknown_kind_fallback` fixtures. It names no v0
1022node kind, which is the point of it: the first carries no fallback and is refused, and the second
1023carries a fallback of known nodes and is accepted (§4.5).
1024
1025The kind codes {} (`edit`) and {} (`surface`) appear in the three `reserved_*` fixtures. They are
1026not unknown: §4.2 reserves them to the chrome and to applications, and `oxeweb/doc/0` admits the
1027kinds 1 to 13 and no others. All three are refused, and the third carries a valid fallback and is
1028refused anyway, which is the point of it: a fallback admits a code the reader has never heard of,
1029and never one the reader knows a document may not carry.
1030", KEY_FILE, ALIEN_CODE, ReservedKind::Edit.code(), ReservedKind::Surface.code())
1031}
1032
1033
1034// ┌───────────────────────────────────────────────────────────────────────────┐
1035// │ THE SCHEMAS THAT ARE NOT NODE TREES │
1036// └───────────────────────────────────────────────────────────────────────────┘
1037//
1038// A post and a card are flat canonical maps, so none of §4 applies to them and the fixtures below
1039// carry no node count and no depth. Everything else is the same file in the same container, which
1040// is the point: the envelope, the address, the signature and every rule of §3 are the container's
1041// and do not change with the payload.
1042
1043/// A recipient's key, fixed so a fixture written twice is written the same.
1044const TO_KEY: [u8; post::limit::KEY_BYTES] = [0xA1; post::limit::KEY_BYTES];
1045
1046/// A message's nonce, fixed for the same reason.
1047const NONCE: [u8; post::limit::NONCE_BYTES] = [0xB2; post::limit::NONCE_BYTES];
1048
1049/// A sealing subkey, fixed for the same reason.
1050const ENC_KEY: [u8; card::limit::KEY_BYTES] = [0xE1; card::limit::KEY_BYTES];
1051
1052/// The smallest message that is still a message.
1053fn post_minimal() -> Post {
1054 Post {
1055 body: "The crop is in, and the second field can wait.".to_string(),
1056 to: TO_KEY.to_vec(),
1057 nonce: NONCE.to_vec(),
1058 reply_to: None,
1059 refs: Vec::new(),
1060 }
1061}
1062
1063/// The fixtures of the post and card schemas, and the count of them.
1064fn payloads(
1065 root: &Path,
1066 keys: &Keys,
1067)
1068 -> Outcome<usize>
1069{
1070 let author = res!(keys.author.signer());
1071
1072 // -- What a reader must accept. -------------------------------------------------------------
1073
1074 res!(accept_payload(root, &author, "post_minimal", &Payload::Post(post_minimal()),
1075 "The smallest message that is still one: a body, a recipient and a nonce. Neither optional \
1076 field is present, and neither is encoded as `none` — an absent field is omitted (§3 rule \
1077 4), because a message written two ways would be a message with two addresses."));
1078
1079 res!(accept_payload(root, &author, "post_every_target", &Payload::Post(Post {
1080 body: "All four of the things a message may point at, once each.".to_string(),
1081 to: TO_KEY.to_vec(),
1082 nonce: NONCE.to_vec(),
1083 reply_to: Some(vec![0xC3; post::limit::ADDR_BYTES]),
1084 refs: vec![
1085 Reference {
1086 target: Target::Proposal {
1087 account: "oxedyne".to_string(),
1088 repo: "daimond".to_string(),
1089 number: 17,
1090 },
1091 fallback: "the proposal about the panel showing nothing when signed out"
1092 .to_string(),
1093 },
1094 Reference {
1095 target: Target::Build { id: "f9f68b75c73b".to_string() },
1096 fallback: "the build this was fixed in".to_string(),
1097 },
1098 Reference {
1099 target: Target::Panel { name: "spend".to_string() },
1100 fallback: "the Spending panel".to_string(),
1101 },
1102 Reference {
1103 target: Target::Guide {
1104 page: "improve".to_string(),
1105 anchor: Some("voices".to_string()),
1106 },
1107 fallback: "the guide section on voices".to_string(),
1108 },
1109 ],
1110 }),
1111 "Every kind of reference once, at the limit of four, and a reply. All four referents are \
1112 PUBLIC anchors: each is named globally and can be resolved by anybody holding a session. A \
1113 reference to something only the sender can reach would draw a pressable chip that always \
1114 fails, which is why no such kind exists."));
1115
1116 res!(accept_payload(root, &author, "card_first", &Payload::Card(Card {
1117 label: "Jason".to_string(),
1118 enc: ENC_KEY.to_vec(),
1119 role: Role::Root,
1120 prev: None,
1121 }),
1122 "A first identity card: a display label, the sealing subkey, and the role. The SIGNING key \
1123 is not a field here — it is the envelope's `author`, so a card has exactly one place that \
1124 says which key composed it. Self-signed, which proves the holder of that key composed it \
1125 and proves nothing whatever about who the holder is."));
1126
1127 res!(accept_payload(root, &author, "card_rotated", &Payload::Card(Card {
1128 label: "Jason".to_string(),
1129 enc: ENC_KEY.to_vec(),
1130 role: Role::Root,
1131 prev: Some(vec![0xD4; card::limit::KEY_BYTES]),
1132 }),
1133 "A card naming the key it supersedes. It must not encode as `card_first` does: a rotated \
1134 key and a first key are different facts, and a reader that could not tell them apart could \
1135 not tell a replacement from a stranger."));
1136
1137 // -- What a reader must refuse. -------------------------------------------------------------
1138
1139 // The re-labelling attack, which is the whole reason §1.3 length-prefixes the schema. The bytes,
1140 // the hash and the signature are untouched; one word of the envelope is changed. Both payloads
1141 // are flat maps, so the card's decoder would happily be handed the post's bytes -- the envelope
1142 // is the only thing that says which this is, and the envelope is signed.
1143 let bytes = res!(post_minimal().encode());
1144 let mut env = res!(common::seal(&bytes, SCHEMA_POST, &author, TIME));
1145 env.schema = SCHEMA_CARD.to_string();
1146 res!(reject(root, "post_relabelled_as_card", &res!(common::assemble(&env, &bytes)), None,
1147 Reject {
1148 stage: Stage::Sig,
1149 rule: "SPEC.md §1.3: the schema is inside the signing input, and is preceded by its \
1150 length.".to_string(),
1151 says: "not a signature by the author".to_string(),
1152 node: None,
1153 offset: None,
1154 note: "A signed post whose envelope has been re-labelled `daimond/card/0` after \
1155 signing. Everything else is untouched: the payload bytes, the hash of them, and \
1156 the signature over that hash. The signature covers the schema as well as the \
1157 address, so the re-labelling is what breaks it. Without that, a payload could be \
1158 presented to whichever validator would accept it, and an author's signature would \
1159 vouch for a claim they never made.".to_string(),
1160 }));
1161
1162 // A body one byte past the limit. A rejection and never a truncation: a message silently cut
1163 // short is a message whose sender and reader disagree about what was said.
1164 let mut long = post_minimal();
1165 long.body = "x".repeat(post::limit::BODY_BYTES + 1);
1166 res!(payload_reject(root, &author, "post_body_over_limit", SCHEMA_POST,
1167 &res!(long.to_dat()),
1168 "exceeding the limit",
1169 "SPEC.md §5 and the post schema's own limits: a body is at most 8 KiB of UTF-8."
1170 .to_string(),
1171 "A body one byte past the ceiling. The number is revisable on evidence; that there is one \
1172 is not, since a body with no ceiling is a body that sets the relay's storage.".to_string()));
1173
1174 // A nonce of the wrong width. Not a shorter nonce: a different thing.
1175 let short_nonce = {
1176 let mut m = DaticleMap::new();
1177 m.insert(dat!(post::KEY_BODY), Dat::BU32(post_minimal().body.into_bytes()));
1178 m.insert(dat!(post::KEY_NONCE), Dat::BU8(vec![0xB2; post::limit::NONCE_BYTES - 1]));
1179 m.insert(dat!(post::KEY_TO), Dat::BU8(TO_KEY.to_vec()));
1180 Dat::Map(m)
1181 };
1182 res!(payload_reject(root, &author, "post_nonce_width", SCHEMA_POST, &short_nonce,
1183 "must carry exactly",
1184 "The post schema fixes the nonce at 16 bytes.".to_string(),
1185 "A nonce of fifteen bytes. A key or a nonce of the wrong width is not a shorter one; it \
1186 is a different thing, and admitting it would let a sender choose how much randomness a \
1187 message carried.".to_string()));
1188
1189 // A sealing subkey of the wrong width, for the same reason on the card's side.
1190 let short_enc = {
1191 let mut m = DaticleMap::new();
1192 m.insert(dat!(card::KEY_ENC), Dat::BU8(vec![0xE1; card::limit::KEY_BYTES - 1]));
1193 m.insert(dat!(card::KEY_LABEL), Dat::Str("Jason".to_string()));
1194 m.insert(dat!(card::KEY_ROLE), Dat::Str(Role::Root.as_str().to_string()));
1195 Dat::Map(m)
1196 };
1197 res!(payload_reject(root, &author, "card_enc_width", SCHEMA_CARD, &short_enc,
1198 "must carry exactly",
1199 "The card schema fixes the sealing subkey at 32 bytes.".to_string(),
1200 "A sealing subkey of thirty-one bytes. A card is what a correspondent reads a sealing key \
1201 OFF, so a key of the wrong width here is a key nothing can seal to.".to_string()));
1202
1203 // A list that is present and empty. Two encodings of one message, and so two addresses.
1204 let empty_refs = {
1205 let mut m = DaticleMap::new();
1206 m.insert(dat!(post::KEY_BODY), Dat::BU32(post_minimal().body.into_bytes()));
1207 m.insert(dat!(post::KEY_NONCE), Dat::BU8(NONCE.to_vec()));
1208 m.insert(dat!(post::KEY_REFS), Dat::List(Vec::new()));
1209 m.insert(dat!(post::KEY_TO), Dat::BU8(TO_KEY.to_vec()));
1210 Dat::Map(m)
1211 };
1212 res!(payload_reject(root, &author, "post_refs_empty_list", SCHEMA_POST, &empty_refs,
1213 "carries an empty \"refs\" list",
1214 "SPEC.md §3 rules 4 and 8: an absent optional field is omitted, never encoded as an empty \
1215 one.".to_string(),
1216 "A message carrying `refs` as an empty list. A message with no references and a message \
1217 with an empty list of them are the same message, so admitting both would give it two \
1218 encodings and therefore two addresses.".to_string()));
1219
1220 // A duplicate key, which survives only in the bytes: a decoding map collapses it into one entry,
1221 // so nothing but re-encoding and comparing can catch it.
1222 res!(reject(root, "post_duplicate_key",
1223 &res!(payload_file(&author, SCHEMA_POST, &res!(post_duplicate_key_bytes()))), None,
1224 Reject {
1225 stage: Stage::Decode,
1226 rule: "SPEC.md §3: a map carries each key once. A duplicate survives only in the \
1227 bytes.".to_string(),
1228 says: "not in canonical form".to_string(),
1229 node: None,
1230 offset: None,
1231 note: "A post whose map carries `to` twice on the wire. Both entries decode, and the \
1232 second overwrites the first, so the decoded value is indistinguishable from a \
1233 sound one: the fault exists in the bytes alone. It is caught by re-encoding what \
1234 was decoded and requiring the same bytes back, which is why that comparison is \
1235 not an optimisation to skip.".to_string(),
1236 }));
1237
1238 // A non-minimal length in the ENVELOPE, over a post. The envelope obeys §3 like everything else
1239 // the hash is read from, and `tree_len` is the one field written as a variable-width c64.
1240 res!(reject(root, "envelope_nonminimal_c64",
1241 &res!(nonminimal_tree_len(&author)), None,
1242 Reject {
1243 stage: Stage::Envelope,
1244 rule: "SPEC.md §1.2 and §3: the envelope is canonical, so a length is written in as \
1245 few bytes as it needs.".to_string(),
1246 says: "minimally encoded".to_string(),
1247 node: None,
1248 offset: None,
1249 note: "An envelope whose `tree_len` is written as a wider c64 than the value needs. \
1250 It decodes to the same number, so nothing about where the payload is or what it \
1251 says would change; only the bytes differ. Admitted, it would give one artefact \
1252 more than one envelope encoding, and an envelope is what a reader identifies an \
1253 artefact from. Caught by the BDAT decoder as it reads the length, which is earlier \
1254 and more precise than the envelope's own re-encode comparison -- that comparison \
1255 is the backstop for the faults a decode survives, such as a duplicate key, and \
1256 this is not one of them.".to_string(),
1257 }));
1258
1259 Ok(11)
1260}
1261
1262/// Writes an acceptance fixture for a payload that is not a node tree.
1263fn accept_payload(
1264 root: &Path,
1265 author: &SignatureScheme,
1266 name: &str,
1267 payload: &Payload,
1268 note: &str,
1269)
1270 -> Outcome<()>
1271{
1272 let dir = res!(fresh_dir(root, name));
1273 let buf = res!(doc::write_artefact(payload, author, TIME));
1274 let env = res!(doc::verify_only(&buf));
1275 let meta = Meta {
1276 schema: env.schema.clone(),
1277 time: env.time,
1278 hash: env.hash.clone(),
1279 tree_len: env.tree_len,
1280 // A flat record has no nodes and no depth, so it declares neither.
1281 nodes: None,
1282 depth: None,
1283 index: false,
1284 note: note.to_string(),
1285 };
1286 let as_dat = match payload {
1287 Payload::Post(p) => res!(p.to_dat()),
1288 Payload::Card(c) => res!(c.to_dat()),
1289 Payload::Share(s) => res!(s.to_dat()),
1290 Payload::Tree { .. } => return Err(err!(
1291 "The fixture '{}' is a node tree, which `accept` writes and this does not.", name;
1292 Bug, Invalid)),
1293 };
1294 res!(common::write_bytes(&dir.join(DOC_JDAT), res!(common::to_jdat_plain(&as_dat)).as_bytes()));
1295 res!(common::write_bytes(&dir.join(DOC_SBJ), &buf));
1296 res!(common::write_bytes(
1297 &dir.join(META_JDAT),
1298 res!(common::to_jdat_plain(&meta.to_dat())).as_bytes(),
1299 ));
1300 Ok(())
1301}
1302
1303/// Writes a rejection fixture whose payload is a canonical map breaking one of its schema's rules.
1304///
1305/// The map is encoded without being checked, so the fixture isolates the rule: the container is
1306/// sound, the bytes hash to what the envelope says, the signature verifies, and the payload's own
1307/// decoder is what refuses it.
1308fn payload_reject(
1309 root: &Path,
1310 author: &SignatureScheme,
1311 name: &str,
1312 schema: &str,
1313 payload: &Dat,
1314 says: &str,
1315 rule: String,
1316 note: String,
1317)
1318 -> Outcome<()>
1319{
1320 let bytes = res!(payload.to_bytes(Vec::new()));
1321 let bad = res!(payload_file(author, schema, &bytes));
1322 let dir = res!(fresh_dir(root, name));
1323 res!(common::write_bytes(&dir.join(DOC_SBJ), &bad));
1324 res!(common::write_bytes(
1325 &dir.join(REJECT_JDAT),
1326 res!(common::to_jdat_plain(&Reject {
1327 stage: Stage::Decode,
1328 rule,
1329 says: says.to_string(),
1330 node: None,
1331 offset: None,
1332 note,
1333 }.to_dat())).as_bytes(),
1334 ));
1335 // Written plain, with none of the `sbj_` node labels: there are no `usr` daticles in a record.
1336 res!(common::write_bytes(&dir.join(DOC_JDAT), res!(common::to_jdat_plain(payload)).as_bytes()));
1337 Ok(())
1338}
1339
1340/// A whole file around payload bytes, correctly hashed and correctly signed.
1341fn payload_file(
1342 author: &SignatureScheme,
1343 schema: &str,
1344 bytes: &[u8],
1345)
1346 -> Outcome<Vec<u8>>
1347{
1348 common::assemble(&res!(common::seal(bytes, schema, author, TIME)), bytes)
1349}
1350
1351/// The bytes of a post whose map carries the key `to` twice.
1352///
1353/// A map is a `BTreeMap` once decoded, so a duplicate key exists only on the wire. The bytes are
1354/// therefore written entry by entry, in the order a `BTreeMap` puts them in, with one written twice.
1355fn post_duplicate_key_bytes() -> Outcome<Vec<u8>> {
1356 let p = post_minimal();
1357 let mut inner = Vec::new();
1358 inner = res!(Dat::Str(post::KEY_BODY.to_string()).to_bytes(inner));
1359 inner = res!(Dat::BU32(p.body.into_bytes()).to_bytes(inner));
1360 inner = res!(Dat::Str(post::KEY_NONCE.to_string()).to_bytes(inner));
1361 inner = res!(Dat::BU8(NONCE.to_vec()).to_bytes(inner));
1362 for _ in 0..2 { // The duplicate.
1363 inner = res!(Dat::Str(post::KEY_TO.to_string()).to_bytes(inner));
1364 inner = res!(Dat::BU8(TO_KEY.to_vec()).to_bytes(inner));
1365 }
1366 map_bytes(&inner)
1367}
1368
1369/// A sound post in a file whose envelope writes `tree_len` in more bytes than it needs.
1370fn nonminimal_tree_len(author: &SignatureScheme) -> Outcome<Vec<u8>> {
1371 let bytes = res!(post_minimal().encode());
1372 let env = res!(common::seal(&bytes, SCHEMA_POST, author, TIME));
1373 let env_dat = res!(env.to_dat());
1374 let map = match &env_dat {
1375 Dat::Map(m) => m,
1376 other => return Err(err!(
1377 "An envelope encodes as a map, and this is a {:?}.", other.kind(); Bug, Invalid)),
1378 };
1379 // Every entry as the encoder writes it, except `tree_len`, which is written at a wider c64.
1380 let mut inner = Vec::new();
1381 for (k, v) in map.iter() {
1382 inner = res!(k.to_bytes(inner));
1383 if *k == dat!(envelope::KEY_TREE_LEN) {
1384 inner.extend_from_slice(&wide_c64(env.tree_len));
1385 } else {
1386 inner = res!(v.to_bytes(inner));
1387 }
1388 }
1389 let env_bytes = res!(map_bytes(&inner));
1390 let mut buf = res!(envelope::write_header(env_bytes.len()));
1391 buf.extend_from_slice(&env_bytes);
1392 buf.extend_from_slice(&bytes);
1393 Ok(buf)
1394}
1395
1396/// A BDAT map around already-encoded entries: the map code, the byte length, then the entries.
1397///
1398/// Written by hand because every fixture that reaches for it is a fixture whose entries a `Dat`
1399/// cannot hold -- a duplicate key, or a value written at a width the encoder would never choose.
1400fn map_bytes(inner: &[u8]) -> Outcome<Vec<u8>> {
1401 let mut buf = vec![Dat::MAP_CODE];
1402 buf = res!(Dat::C64(try_into!(u64, inner.len())).to_bytes(buf));
1403 buf.extend_from_slice(inner);
1404 Ok(buf)
1405}
1406
1407/// A `c64` written in one more byte than the value needs.
1408///
1409/// A `c64` is a code byte carrying the number of value bytes that follow, so the same number has as
1410/// many encodings as there are widths that hold it. Canonical form is the narrowest (§3); this is
1411/// the next one up, which decodes to exactly the same number and differs only in the bytes.
1412fn wide_c64(v: u64) -> Vec<u8> {
1413 let be = v.to_be_bytes();
1414 // The minimal width, then one more. A zero needs no value bytes, so the wide form of it is one.
1415 let narrow = be.iter().position(|b| *b != 0).map_or(0, |i| 8 - i);
1416 // `min(8)` rather than a check: eight is the widest a c64 has, so a value already at it has no
1417 // wider form and is written as it stands. No caller reaches that, since `tree_len` is capped at
1418 // four mebibytes by §5.
1419 let width = (narrow + 1).min(8);
1420 let mut out = vec![Dat::C64_CODE_START + width as u8];
1421 out.extend_from_slice(&be[8 - width..]);
1422 out
1423}
1424
1425
1426// ┌───────────────────────────────────────────────────────────────────────────┐
1427// │ THE SHARE SCHEMA │
1428// └───────────────────────────────────────────────────────────────────────────┘
1429//
1430// `daimond/share/0` is one person sending another a COPY of something they own, and it is the
1431// third flat record in this container. Its fixtures are here rather than among the post's because
1432// what they are teeth for is different: a post's rules are about one message having one address,
1433// and a share's are about that plus a consent bit, which is the one field in this format whose
1434// whole purpose is that a receiver can check what the SENDER marked before anything runs.
1435
1436/// A capp's page, standing in for the real thing: what makes a share carry code is the NAME.
1437const PAGE: &'static [u8] = b"<html><body><p>A page somebody else wrote.</p></body></html>";
1438
1439/// A share of data alone, fixed so a fixture written twice is written the same.
1440fn share_data() -> Share {
1441 Share::new(
1442 "Sourdough".to_string(),
1443 TO_KEY.to_vec(),
1444 NONCE.to_vec(),
1445 None,
1446 vec![
1447 share::File {
1448 path: "bakes/2026.jsonl".to_string(),
1449 body: b"{\"day\":1,\"loaves\":2}\n".to_vec(),
1450 },
1451 share::File {
1452 path: "crystal.json".to_string(),
1453 body: b"{\"starter\":\"fed Tuesday\"}".to_vec(),
1454 },
1455 ],
1456 )
1457}
1458
1459/// The same share, carrying a page, and therefore carrying a program.
1460fn share_capp() -> Share {
1461 let mut files = share_data().files;
1462 files.push(share::File { path: "crystal.html".to_string(), body: PAGE.to_vec() });
1463 Share::new(
1464 "Life log".to_string(),
1465 TO_KEY.to_vec(),
1466 NONCE.to_vec(),
1467 Some("The food log we talked about.".to_string()),
1468 files,
1469 )
1470}
1471
1472/// The share fixtures, and the count of them.
1473fn shares(
1474 root: &Path,
1475 keys: &Keys,
1476)
1477 -> Outcome<usize>
1478{
1479 let author = res!(keys.author.signer());
1480
1481 // -- What a reader must accept. -------------------------------------------------------------
1482
1483 res!(accept_payload(root, &author, "share_data", &Payload::Share(share_data()),
1484 "A share of DATA alone: two files, a display name, a recipient and a nonce, and `code` \
1485 written as false. The bit is present even though nothing here is code — an omitted false \
1486 and a sender whose build had never heard of the field are the same bytes, and those are \
1487 the two things a receiver must be able to tell apart. No note, and the absent one is \
1488 omitted rather than written empty (§3 rules 4 and 8). The files are in path order, which \
1489 is fixed, because a set of files written two ways would be one Diamond at two addresses."));
1490
1491 res!(accept_payload(root, &author, "share_capp", &Payload::Share(share_capp()),
1492 "A share carrying `crystal.html`, and therefore carrying a PROGRAM written by another \
1493 person. `code` is true, it is inside the payload, and the payload is what the envelope's \
1494 hash covers and the signature commits to — so a receiver can check that the SENDER marked \
1495 it, which is the whole point: a flag a relay could add or strip is not a consent flag. It \
1496 must not encode as `share_data` does, and it carries a covering note besides."));
1497
1498 // -- What a reader must refuse. -------------------------------------------------------------
1499
1500 // The central rejection of this schema. Everything else here is a canonicalisation rule; this
1501 // one is what the consent bit is for.
1502 let mut hidden = share_capp();
1503 hidden.code = false;
1504 res!(payload_reject(root, &author, "share_code_hidden", SCHEMA_SHARE,
1505 &res!(hidden.to_dat()),
1506 "crystal.html",
1507 "The share schema: `code` is the sender's signed claim, and it is checked against the \
1508 files.".to_string(),
1509 "A share carrying `crystal.html` under `code: false`. It is signed, correctly hashed and \
1510 correctly addressed, so nothing in the container catches it: the payload's own decoder \
1511 does, naming the file. Without that check the bit would be decoration — a sender could \
1512 ship a page as data and the receiving client, believing the claim, would mount somebody \
1513 else's program without asking. The refusal is what makes the claim worth reading."
1514 .to_string()));
1515
1516 // And the other direction, which is not symmetry for its own sake.
1517 let mut crying = share_data();
1518 crying.code = true;
1519 res!(payload_reject(root, &author, "share_code_claimed_without_code", SCHEMA_SHARE,
1520 &res!(crying.to_dat()),
1521 "carries none",
1522 "The share schema: `code` is checked against the files BOTH ways.".to_string(),
1523 "A share claiming code and carrying none. Refused rather than waved through as harmless \
1524 caution: a receiver asked to consent to a program that is not there is a receiver being \
1525 taught that the question does not mean anything, and the next time it is asked in earnest \
1526 they will answer the same way.".to_string()));
1527
1528 // The bit is REQUIRED. An absent one would be read as false by any reader generous enough to
1529 // default it, which is exactly the generosity a consent flag cannot afford.
1530 let no_bit = {
1531 let mut m = match res!(share_data().to_dat()) {
1532 Dat::Map(m) => m,
1533 other => return Err(err!(
1534 "A share encodes as a map, and this is a {:?}.", other.kind(); Bug, Invalid)),
1535 };
1536 m.remove(&dat!(share::KEY_CODE));
1537 Dat::Map(m)
1538 };
1539 res!(payload_reject(root, &author, "share_missing_code_bit", SCHEMA_SHARE, &no_bit,
1540 "missing the required key",
1541 "The share schema: `code` is required, and is written even when it is false.".to_string(),
1542 "A share with no `code` key at all. A reader that defaulted it to false would be reading \
1543 \"they did not say\" as \"they said there is nothing to worry about\", which is the one \
1544 reading a consent bit must never be given.".to_string()));
1545
1546 // One Diamond, two addresses: the rule a message's `refs` deliberately does not have, because
1547 // references are ordered by their author's meaning and a set of files is not.
1548 let unsorted = {
1549 let s = share_data();
1550 let mut m = match res!(s.to_dat()) {
1551 Dat::Map(m) => m,
1552 other => return Err(err!(
1553 "A share encodes as a map, and this is a {:?}.", other.kind(); Bug, Invalid)),
1554 };
1555 let mut list = Vec::new();
1556 for f in s.files.iter().rev() {
1557 list.push(res!(f.to_dat()));
1558 }
1559 m.insert(dat!(share::KEY_FILES), Dat::List(list));
1560 Dat::Map(m)
1561 };
1562 res!(payload_reject(root, &author, "share_files_out_of_order", SCHEMA_SHARE, &unsorted,
1563 "not in path order",
1564 "The share schema: the files are ordered by path, so that one set of files has one \
1565 encoding.".to_string(),
1566 "The same two files, listed the other way round. They decode to the same Diamond and hash \
1567 to a different address, so admitting both would give one share two addresses. It is \
1568 refused rather than sorted: sorting it would be accepting a second encoding and quietly \
1569 rewriting it, which is what §3 exists to stop.".to_string()));
1570
1571 // The three paths a share may not carry, one fixture each for the two that are about somebody
1572 // else's records.
1573 res!(payload_reject(root, &author, "share_carries_the_log", SCHEMA_SHARE,
1574 &res!(share_with_path(".daimond/log.jsonl")),
1575 ".daimond/",
1576 "The share schema: a share may not carry the sender's own `.daimond/` record."
1577 .to_string(),
1578 "A share carrying the sender's append-only log — the record of what agents did in THEIR \
1579 copy. Refused in the format rather than in a client, so that every implementation refuses \
1580 it: a person sending a recipe does not think to check what travels with it, and the \
1581 receiver's copy is new, so its record starts empty because nothing has happened in it yet."
1582 .to_string()));
1583
1584 res!(payload_reject(root, &author, "share_carries_capp_record", SCHEMA_SHARE,
1585 &res!(share_with_path("capp.json")),
1586 "capp.json",
1587 "The share schema: a share may not carry a capp delivery record.".to_string(),
1588 "A share carrying `capp.json`, which says which bytes were delivered to that instance and \
1589 at what template version, and decides which files a future fix may replace. The receiver \
1590 was never delivered to; they were handed a copy by a person. One carried across from \
1591 somebody else's machine would pin their copy against updates they never chose, and a \
1592 doctored one would do it on purpose. A copy with no record is a case the receiving client \
1593 already knows: it asks.".to_string()));
1594
1595 res!(payload_reject(root, &author, "share_path_walks", SCHEMA_SHARE,
1596 &res!(share_with_path("../../notes/private.md")),
1597 "segment",
1598 "The share schema: a path is refused rather than resolved, and never walks."
1599 .to_string(),
1600 "A share whose file path climbs out of the Diamond. Refused rather than normalised, which \
1601 is where this parts company with the client-side path guard it otherwise matches: that one \
1602 is handed an untrusted request and tidies it on the way to a real file, and this is \
1603 deciding what a SIGNED artefact means, where a path that needed tidying is a path with two \
1604 spellings.".to_string()));
1605
1606 // The re-labelling attack over the third schema. The reserved name became a real one, and this
1607 // is the fixture that shows nothing already signed was weakened by it.
1608 let bytes = res!(share_data().encode());
1609 let mut env = res!(common::seal(&bytes, SCHEMA_SHARE, &author, TIME));
1610 env.schema = SCHEMA_POST.to_string();
1611 res!(reject(root, "share_relabelled_as_post", &res!(common::assemble(&env, &bytes)), None,
1612 Reject {
1613 stage: Stage::Sig,
1614 rule: "SPEC.md §1.3: the schema is inside the signing input, and is preceded by its \
1615 length.".to_string(),
1616 says: "not a signature by the author".to_string(),
1617 node: None,
1618 offset: None,
1619 note: "A signed share whose envelope has been re-labelled `daimond/post/0` after \
1620 signing. The payload bytes, their hash and the signature over that hash are \
1621 untouched. This is the same claim as `post_relabelled_as_card`, made over the \
1622 schema name that was RESERVED when those were signed: the schema reaches the \
1623 signing input length-prefixed, so a third name coming to exist re-addressed \
1624 nothing, weakened nothing, and left every fixture already committed byte for byte \
1625 the file it was.".to_string(),
1626 }));
1627
1628 Ok(10)
1629}
1630
1631/// A share whose one file sits at `path`, for the fixtures about paths a share may not carry.
1632///
1633/// Built as a daticle rather than through `Share::new`, because the point of each is a path the
1634/// constructor's own validator would refuse, and a fixture that could not be written would prove
1635/// nothing about what a reader does with one that was.
1636fn share_with_path(path: &str) -> Outcome<Dat> {
1637 let mut file = DaticleMap::new();
1638 file.insert(dat!(share::KEY_BODY), Dat::BU32(b"whatever is in it".to_vec()));
1639 file.insert(dat!(share::KEY_PATH), Dat::Str(path.to_string()));
1640
1641 let mut m = DaticleMap::new();
1642 m.insert(dat!(share::KEY_CODE), Dat::Bool(false));
1643 m.insert(dat!(share::KEY_FILES), Dat::List(vec![Dat::Map(file)]));
1644 m.insert(dat!(share::KEY_NAME), Dat::Str("A share reaching too far".to_string()));
1645 m.insert(dat!(share::KEY_NONCE), Dat::BU8(NONCE.to_vec()));
1646 m.insert(dat!(share::KEY_TO), Dat::BU8(TO_KEY.to_vec()));
1647 Ok(Dat::Map(m))
1648}