Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_sbj/tests/common/mod.rs

34.3 KiB, 11 runs

created by r1870400018:22232, 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//! Machinery shared by the fixture generator and the conformance suite.
2//!
3//! The generator (`examples/gen_fixtures.rs`) writes `fixtures/`, and the suite
4//! (`tests/conformance.rs`) reads it. Both need the same node builders, the same JDAT text codec,
5//! the same fixed key, and the same declaration formats, so both take them from here rather than
6//! from each other, and neither takes an expected error message from the implementation it is
7//! testing.
8//!
9//! The container is assembled here out of the crate's public API alone: `Envelope` says what an
10//! envelope is, `doc::hash_tree` hashes a region, a `Signer` signs the hash, and `write_header`
11//! writes the eight bytes in front. A rejection fixture must therefore carry a sound signature over
12//! whatever is wrong with it, so that the rejection can only have come from the rule the fixture
13//! breaks.
14
15#![allow(dead_code)] // Each of the two callers uses a part of this.
16
17use oxedyne_fe2o3_sbj::{
18 canon,
19 doc,
20 envelope::{
21 self,
22 Envelope,
23 },
24 kinds::{
25 NodeKind,
26 ReservedKind,
27 },
28 limit,
29 text,
30 HEADER_LEN,
31 SCHEMA_DOC,
32};
33
34use oxedyne_fe2o3_core::prelude::*;
35use oxedyne_fe2o3_crypto::sign::SignatureScheme;
36use oxedyne_fe2o3_iop_crypto::{
37 keys::KeyManager,
38 sign::Signer,
39};
40use oxedyne_fe2o3_jdat::{
41 prelude::*,
42 string::{
43 dec::DecoderConfig,
44 enc::EncoderConfig,
45 },
46 usr::{
47 UsrKind,
48 UsrKindCode,
49 UsrKindId,
50 UsrKinds,
51 },
52};
53
54use std::{
55 collections::BTreeMap,
56 fs,
57 path::{
58 Path,
59 PathBuf,
60 },
61};
62
63/// The authoring time every fixture is signed at, so that a fixture written twice is the same file.
64pub const TIME: u64 = 1_752_000_000_000;
65
66/// The stack a thread is given before it reads a document at the depth limit.
67///
68/// This was once 512 MiB, because the JDAT *text* decoder a fixture's `doc.jdat` goes through spent
69/// some 158 KB of stack on every level of a debug build, and the deepest legal fixture would not
70/// read in less. The decoder has since been given frame-splitting and costs about 2.4 KB a level.
71///
72/// The arithmetic now: a node costs about five text levels, and `SPEC.md` §5 permits 64, so the
73/// deepest legal document needs roughly 320 levels, or some 800 KB. Eight mebibytes leaves a
74/// tenfold margin. The limit is the format's, and it does not move to suit a test, so the test
75/// moves -- but it no longer has to move nearly so far.
76pub const STACK_BYTES: usize = 8 * 1024 * 1024;
77
78/// The file holding the fixed key the fixtures are signed with.
79pub const KEY_FILE: &'static str = "key.jdat";
80/// The file explaining the fixture directory to a reader.
81pub const README_FILE: &'static str = "README.md";
82/// The document, in JDAT text form: the source of truth for a fixture.
83pub const DOC_JDAT: &'static str = "doc.jdat";
84/// The signed binary artefact.
85pub const DOC_SBJ: &'static str = "doc.sbj";
86/// The expectations of an acceptance fixture.
87pub const META_JDAT: &'static str = "meta.jdat";
88/// The declared failure of a rejection fixture.
89pub const REJECT_JDAT: &'static str = "reject.jdat";
90
91/// Every v0 node kind, in code order.
92pub const KINDS: [NodeKind; 13] = [
93 NodeKind::Doc,
94 NodeKind::Section,
95 NodeKind::Para,
96 NodeKind::Heading,
97 NodeKind::List,
98 NodeKind::Item,
99 NodeKind::Boxx,
100 NodeKind::Image,
101 NodeKind::Text,
102 NodeKind::Emph,
103 NodeKind::Link,
104 NodeKind::Code,
105 NodeKind::Quote,
106];
107
108/// A kind code the v0 vocabulary does not know, for the fixture that carries an unknown node.
109pub const ALIEN_CODE: u16 = 99;
110
111/// The label the alien kind carries in the JDAT text form.
112pub const ALIEN_LABEL: &'static str = "sbj_alien";
113
114/// The reserved kinds of `SPEC.md` §4.2, which `oxeweb/doc/0` admits nowhere.
115///
116/// They are the engine's own: an editable text field, and a pane an application paints. A document
117/// carrying one is refused, fallback or no fallback, and the fixtures that carry them are what says
118/// so.
119pub const RESERVED: [ReservedKind; 2] = [ReservedKind::Edit, ReservedKind::Surface];
120
121/// The user kind registry the JDAT text codec needs to read and write node kinds.
122pub type Ukinds = UsrKinds<BTreeMap<UsrKindCode, UsrKind>, BTreeMap<String, UsrKindId>>;
123
124/// The label a node kind carries in the JDAT text form of a tree.
125///
126/// Two of the v0 kind labels, `box` and `list`, are also JDAT's own kind labels, so a node written
127/// as `(box|{..})` would read back as a `Dat::Box` and a node written as `(list|[..])` as a
128/// `Dat::List`. Every node label is therefore prefixed in the text form. Nothing of this reaches the
129/// wire: BDAT carries the `u16` kind code and no label at all, and `UsrKindId` compares by code.
130pub fn text_label(kind: NodeKind) -> String {
131 fmt!("sbj_{}", kind.label())
132}
133
134/// The user kind registry: one entry per v0 node kind, one for the kind that is not one, and one for
135/// each kind the document schema reserves.
136///
137/// The alien kind and the reserved kinds are registered so that the fixtures carrying them can still
138/// be read and written as text. The registry is the fixture suite's, not the format's: what SBJ
139/// admits in a document is `NodeKind`, and it refuses code 99 for want of a fallback and the codes 14
140/// and 15 whatever they carry.
141pub fn ukinds() -> Outcome<Ukinds> {
142 let mut uks = UsrKinds::new(BTreeMap::new(), BTreeMap::new());
143 for kind in KINDS {
144 res!(uks.add(ukid(kind)));
145 }
146 res!(uks.add(UsrKindId::new(ALIEN_CODE, Some(ALIEN_LABEL), Some(Kind::Map))));
147 for kind in RESERVED {
148 res!(uks.add(reserved_ukid(kind)));
149 }
150 Ok(uks)
151}
152
153/// The user kind id of a reserved kind, under the `sbj_k<code>` label the text form gives a kind the
154/// document vocabulary does not admit.
155pub fn reserved_ukid(kind: ReservedKind) -> UsrKindId {
156 UsrKindId::new(kind.code(), Some(&text::unknown_label(kind.code())), Some(Kind::Map))
157}
158
159/// The user kind id of a node kind, declaring the kind of payload it carries.
160///
161/// The payload kind is declared because the JDAT text decoder reads a user kind that declares one
162/// and drops the payload of one that does not. Nothing of it reaches the wire, where BDAT writes the
163/// `u16` code and nothing else, and `UsrKindId` compares by code, so a tree built here is the tree a
164/// decoder builds.
165pub fn ukid(kind: NodeKind) -> UsrKindId {
166 let payload = if kind.payload_is_str() {
167 Kind::Str
168 } else {
169 Kind::Map
170 };
171 UsrKindId::new(kind.code(), Some(&text_label(kind)), Some(payload))
172}
173
174/// A node of a kind the v0 vocabulary does not know.
175pub fn alien(payload: Dat) -> Dat {
176 Dat::Usr(
177 UsrKindId::new(ALIEN_CODE, Some(ALIEN_LABEL), Some(Kind::Map)),
178 Some(Box::new(payload)),
179 )
180}
181
182/// A node of a kind the document schema reserves (§4.2), which no document may carry.
183pub fn reserved(kind: ReservedKind, payload: Dat) -> Dat {
184 Dat::Usr(reserved_ukid(kind), Some(Box::new(payload)))
185}
186
187/// Writes a tree in JDAT text form, the form a fixture's `doc.jdat` carries.
188///
189/// Every kindicle is written out, including the ones JDAT would infer, so that the text says what
190/// the bytes say and nothing is left to a reader's guess: a `u8` reads as a `u8`, a list as a list,
191/// and a map as a map. It is what §3 asks of the bytes, asked of the text.
192pub fn to_jdat(dat: &Dat) -> Outcome<String> {
193 let cfg = EncoderConfig::jdat_full_to_lines(Some(res!(ukinds())), " ");
194 let mut s = res!(dat.encode_string_with_config(&cfg));
195 s.push('\n');
196 Ok(s)
197}
198
199/// Reads a tree from JDAT text form.
200///
201/// The crate's own text codec does the reading, under the limits of `SPEC.md` §5 rather than the
202/// decoder's defaults, since a document that nests to the ceiling §5 sets must read and one that
203/// nests past it must be refused for that reason. The alien kind is declared, since a label a
204/// document invents for a kind outside the vocabulary is declared rather than guessed.
205pub fn from_jdat(s: &str) -> Outcome<Dat> {
206 text::decode(s, &[
207 text::KindDecl {
208 label: ALIEN_LABEL.to_string(),
209 code: ALIEN_CODE,
210 },
211 ])
212}
213
214/// Writes a plain daticle, such as a `meta.jdat`, in JDAT text form.
215pub fn to_jdat_plain(dat: &Dat) -> Outcome<String> {
216 let cfg = EncoderConfig::<
217 BTreeMap<UsrKindCode, UsrKind>,
218 BTreeMap<String, UsrKindId>,
219 >::jdat_to_lines(None, " ");
220 let mut s = res!(dat.encode_string_with_config(&cfg));
221 s.push('\n');
222 Ok(s)
223}
224
225/// Reads a plain daticle, such as a `meta.jdat`.
226pub fn from_jdat_plain(s: &str) -> Outcome<Dat> {
227 let cfg = DecoderConfig::<
228 BTreeMap<UsrKindCode, UsrKind>,
229 BTreeMap<String, UsrKindId>,
230 >::jdat(None);
231 Dat::decode_string_with_config(s, &cfg)
232}
233
234/// Wraps a payload as a node of the given kind.
235pub fn node(kind: NodeKind, payload: Dat) -> Dat {
236 Dat::Usr(ukid(kind), Some(Box::new(payload)))
237}
238
239/// A text run, the one node whose payload is a bare string.
240pub fn text(s: &str) -> Dat {
241 node(NodeKind::Text, Dat::Str(s.to_string()))
242}
243
244/// A payload map, from string keys.
245pub fn map(kv: Vec<(&str, Dat)>) -> Dat {
246 create_dat_map(
247 kv.into_iter().map(|(k, v)| (Dat::Str(k.to_string()), v)).collect()
248 )
249}
250
251/// The fixed key pair a fixture is signed with, and the one it is not signed with.
252///
253/// Both are committed beside the fixtures, since a freshly generated key would give every artefact
254/// a new signature on every run, and a fixture that changes every run is not a fixture.
255#[derive(Clone, Debug)]
256pub struct Keys {
257 /// The author of every fixture.
258 pub author: KeyPair,
259 /// A key that is not the author, for the fixture signed by the wrong hand.
260 pub impostor: KeyPair,
261}
262
263/// An Ed25519 key pair, held as raw bytes.
264#[derive(Clone, Debug)]
265pub struct KeyPair {
266 /// The public key, which an envelope names as its author.
267 pub pk: Vec<u8>,
268 /// The secret key. This is a test key, published on purpose, and signs nothing else.
269 pub sk: Vec<u8>,
270}
271
272impl KeyPair {
273
274 /// A signer holding this pair.
275 pub fn signer(&self) -> Outcome<SignatureScheme> {
276 Ok(res!(SignatureScheme::empty_ed25519().clone_with_keys(Some(&self.pk), Some(&self.sk))))
277 }
278
279 /// This pair as a daticle, for the committed key file.
280 pub fn to_dat(&self) -> Dat {
281 map(vec![
282 ("pk", Dat::BU8(self.pk.clone())),
283 ("sk", Dat::BU8(self.sk.clone())),
284 ])
285 }
286
287 /// Reads a pair from the committed key file.
288 pub fn from_dat(d: &Dat) -> Outcome<Self> {
289 Ok(Self {
290 pk: res!(get_bytes(d, "pk")),
291 sk: res!(get_bytes(d, "sk")),
292 })
293 }
294}
295
296impl Keys {
297
298 /// Generates a fresh pair of key pairs. Called once, when the key file is first written.
299 pub fn generate() -> Outcome<Self> {
300 Ok(Self {
301 author: res!(fresh()),
302 impostor: res!(fresh()),
303 })
304 }
305
306 /// The keys as a daticle, for the committed key file.
307 pub fn to_dat(&self) -> Dat {
308 map(vec![
309 ("scheme", Dat::Str("ed25519".to_string())),
310 ("author", self.author.to_dat()),
311 ("impostor", self.impostor.to_dat()),
312 ])
313 }
314
315 /// Reads the keys from the committed key file.
316 pub fn from_dat(d: &Dat) -> Outcome<Self> {
317 let scheme = res!(get_str(d, "scheme"));
318 if scheme != "ed25519" {
319 return Err(err!(
320 "The fixture key file names the signature scheme '{}'; v0 signs with Ed25519.",
321 scheme;
322 Invalid, Input));
323 }
324 Ok(Self {
325 author: res!(KeyPair::from_dat(&res!(get(d, "author")))),
326 impostor: res!(KeyPair::from_dat(&res!(get(d, "impostor")))),
327 })
328 }
329
330 /// Reads the committed key file.
331 pub fn load(root: &Path) -> Outcome<Self> {
332 let path = root.join(KEY_FILE);
333 let s = res!(fs::read_to_string(&path), IO, File);
334 Self::from_dat(&res!(from_jdat_plain(&s)))
335 }
336
337 /// Writes the key file.
338 pub fn save(&self, root: &Path) -> Outcome<()> {
339 let path = root.join(KEY_FILE);
340 res!(fs::write(&path, res!(to_jdat_plain(&self.to_dat()))), IO, File);
341 Ok(())
342 }
343}
344
345/// A fresh Ed25519 key pair, in raw bytes.
346fn fresh() -> Outcome<KeyPair> {
347 let signer = SignatureScheme::new_ed25519();
348 let pk = match res!(signer.get_public_key()) {
349 Some(pk) => pk.to_vec(),
350 None => return Err(err!("A fresh Ed25519 signer holds no public key."; Bug, Missing)),
351 };
352 let sk = match res!(signer.get_secret_key()) {
353 Some(sk) => sk.to_vec(),
354 None => return Err(err!("A fresh Ed25519 signer holds no secret key."; Bug, Missing)),
355 };
356 Ok(KeyPair {
357 pk,
358 sk,
359 })
360}
361
362/// Builds the envelope for a tree region: hash the bytes, then sign the hash (`SPEC.md` §1.3).
363///
364/// This is the writer's half of the format, assembled from the crate's public API rather than
365/// borrowed from its private one, so that a fixture is a second opinion about what a file is.
366pub fn seal(
367 tree_bytes: &[u8],
368 schema: &str,
369 signer: &SignatureScheme,
370 time: u64,
371)
372 -> Outcome<Envelope>
373{
374 let author = match res!(signer.get_public_key()) {
375 Some(pk) => pk.to_vec(),
376 None => return Err(err!("The signer holds no public key."; Missing, Configuration)),
377 };
378 let hash_scheme = envelope::HASH_SCHEME_SHA3_256;
379 let mut env = Envelope {
380 schema: schema.to_string(),
381 author,
382 sig_scheme: envelope::SIG_SCHEME_ED25519,
383 hash_scheme,
384 time,
385 hash: res!(doc::hash_tree(hash_scheme, tree_bytes)),
386 sig: Vec::new(),
387 tree_len: try_into!(u64, tree_bytes.len()),
388 };
389 res!(resign(&mut env, signer));
390 Ok(env)
391}
392
393/// Signs the envelope's signing input afresh, after the envelope has been meddled with.
394pub fn resign(
395 env: &mut Envelope,
396 signer: &SignatureScheme,
397)
398 -> Outcome<()>
399{
400 env.sig = res!(signer.sign(&env.signing_input()));
401 Ok(())
402}
403
404/// Assembles a file: header, envelope, tree region.
405pub fn assemble(
406 env: &Envelope,
407 tree_bytes: &[u8],
408)
409 -> Outcome<Vec<u8>>
410{
411 let env_bytes = res!(env.encode());
412 let mut buf = res!(envelope::write_header(env_bytes.len()));
413 buf.extend_from_slice(&env_bytes);
414 buf.extend_from_slice(tree_bytes);
415 Ok(buf)
416}
417
418/// Assembles a file around an envelope map that is not an envelope, such as one missing a key.
419pub fn assemble_raw(
420 env_dat: &Dat,
421 tree_bytes: &[u8],
422)
423 -> Outcome<Vec<u8>>
424{
425 let env_bytes = res!(env_dat.to_bytes(Vec::new()));
426 let mut buf = res!(envelope::write_header(env_bytes.len()));
427 buf.extend_from_slice(&env_bytes);
428 buf.extend_from_slice(tree_bytes);
429 Ok(buf)
430}
431
432/// The offset at which the tree region of a file starts.
433pub fn tree_start(buf: &[u8]) -> Outcome<usize> {
434 let hdr = res!(envelope::read_header(buf));
435 Ok(HEADER_LEN + hdr.env_len as usize)
436}
437
438/// The first byte at which two byte strings differ, or `None` if one is a prefix of the other.
439pub fn first_diff(a: &[u8], b: &[u8]) -> Option<usize> {
440 a.iter().zip(b.iter()).position(|(x, y)| x != y)
441}
442
443/// The step of `SPEC.md` §2 at which a rejection fixture is refused.
444///
445/// The distinction that matters is between the steps that touch no content, which a caller may run
446/// and stop, and the steps that decode. A fixture declares which one refused it, and the suite holds
447/// the implementation to it: a document refused at `Decode` or `Validate` must pass verification
448/// first, or verification is not doing its job, and a document refused before that must never be
449/// decoded at all.
450#[derive(Clone, Copy, Debug, PartialEq, Eq)]
451pub enum Stage {
452 /// Step 1: the magic and the major version.
453 Header,
454 /// Step 2: the envelope map and its keys.
455 Envelope,
456 /// Step 3: the tree region, against the limit and against the bytes there are.
457 Region,
458 /// Step 4: the tree region hashes to what the envelope declares.
459 Hash,
460 /// Step 5: the author signed that hash.
461 Sig,
462 /// Step 6: the tree decodes, canonically, within the depth limit.
463 Decode,
464 /// Step 7: the tree obeys its schema and the remaining limits.
465 Validate,
466}
467
468impl Stage {
469
470 /// The label a `reject.jdat` names this step by.
471 pub fn label(&self) -> &'static str {
472 match self {
473 Self::Header => "header",
474 Self::Envelope => "envelope",
475 Self::Region => "region",
476 Self::Hash => "hash",
477 Self::Sig => "sig",
478 Self::Decode => "decode",
479 Self::Validate => "validate",
480 }
481 }
482
483 /// The step a label names.
484 pub fn from_label(s: &str) -> Outcome<Self> {
485 match s {
486 "header" => Ok(Self::Header),
487 "envelope" => Ok(Self::Envelope),
488 "region" => Ok(Self::Region),
489 "hash" => Ok(Self::Hash),
490 "sig" => Ok(Self::Sig),
491 "decode" => Ok(Self::Decode),
492 "validate" => Ok(Self::Validate),
493 _ => Err(err!(
494 "'{}' names no step of the verification order of SPEC.md §2.", s;
495 Invalid, Input)),
496 }
497 }
498
499 /// Whether steps 1 to 5 pass, so that the document is verified and only its content is wrong.
500 pub fn verifies(&self) -> bool {
501 matches!(self, Self::Decode | Self::Validate)
502 }
503}
504
505/// What an acceptance fixture expects: the address of the document, and its shape.
506#[derive(Clone, Debug)]
507pub struct Meta {
508 /// The schema the envelope declares.
509 pub schema: String,
510 /// The authoring time, in Unix milliseconds.
511 pub time: u64,
512 /// The hash of the tree region, which is the document's address.
513 pub hash: Vec<u8>,
514 /// The length of the tree region, in bytes.
515 pub tree_len: u64,
516 /// The number of nodes, for a payload that is a node tree.
517 ///
518 /// Absent for a payload that is not one. A post and a card are flat records, so a node count of
519 /// zero would be a measurement rather than the absence of one, and a fixture declaring it would
520 /// be asserting something about a shape it does not have.
521 pub nodes: Option<u64>,
522 /// The greatest nesting depth, the root alone being a depth of 1. Absent for the same reason.
523 pub depth: Option<u64>,
524 /// Whether the file carries the optional index of §1.4.
525 pub index: bool,
526 /// What the fixture is for.
527 pub note: String,
528}
529
530impl Meta {
531
532 /// The expectations as a daticle.
533 pub fn to_dat(&self) -> Dat {
534 let mut kv = vec![
535 ("schema", Dat::Str(self.schema.clone())),
536 ("time", Dat::U64(self.time)),
537 ("hash", Dat::BU8(self.hash.clone())),
538 ("tree_len", Dat::U64(self.tree_len)),
539 ];
540 // Omitted rather than written as zero, so a fixture for a payload that is not a tree makes
541 // no claim about a node count instead of making a false one.
542 if let Some(n) = self.nodes {
543 kv.push(("nodes", Dat::U64(n)));
544 }
545 if let Some(d) = self.depth {
546 kv.push(("depth", Dat::U64(d)));
547 }
548 kv.push(("index", Dat::Bool(self.index)));
549 kv.push(("note", Dat::Str(self.note.clone())));
550 map(kv)
551 }
552
553 /// Reads the expectations of a fixture.
554 pub fn from_dat(d: &Dat) -> Outcome<Self> {
555 Ok(Self {
556 schema: res!(get_str(d, "schema")),
557 time: res!(get_u64(d, "time")),
558 hash: res!(get_bytes(d, "hash")),
559 tree_len: res!(get_u64(d, "tree_len")),
560 nodes: res!(get_u64_opt(d, "nodes")),
561 depth: res!(get_u64_opt(d, "depth")),
562 index: res!(get_bool(d, "index")),
563 note: res!(get_str(d, "note")),
564 })
565 }
566}
567
568/// What a rejection fixture expects: the rule broken, and where.
569///
570/// "It was rejected" is not the claim. The claim is that it was rejected for this reason, at this
571/// step, naming this node or this byte, which is what stops a reader passing every rejection fixture
572/// by refusing everything.
573#[derive(Clone, Debug)]
574pub struct Reject {
575 /// The step of §2 that must refuse it.
576 pub stage: Stage,
577 /// The rule broken, as `SPEC.md` writes it.
578 pub rule: String,
579 /// What the rejection must say. `SPEC.md` §6: "Invalid document" is not an error message.
580 pub says: String,
581 /// The node the rejection must name, if the rule is a node's.
582 pub node: Option<u64>,
583 /// The byte the rejection must name, if the rule is a byte's.
584 pub offset: Option<u64>,
585 /// What is wrong with the file.
586 pub note: String,
587}
588
589impl Reject {
590
591 /// The declaration as a daticle.
592 pub fn to_dat(&self) -> Dat {
593 let mut kv = vec![
594 ("stage", Dat::Str(self.stage.label().to_string())),
595 ("rule", Dat::Str(self.rule.clone())),
596 ("says", Dat::Str(self.says.clone())),
597 ];
598 if let Some(id) = self.node {
599 kv.push(("node", Dat::U64(id)));
600 }
601 if let Some(off) = self.offset {
602 kv.push(("offset", Dat::U64(off)));
603 }
604 kv.push(("note", Dat::Str(self.note.clone())));
605 map(kv)
606 }
607
608 /// Reads the declaration of a fixture.
609 pub fn from_dat(d: &Dat) -> Outcome<Self> {
610 Ok(Self {
611 stage: res!(Stage::from_label(&res!(get_str(d, "stage")))),
612 rule: res!(get_str(d, "rule")),
613 says: res!(get_str(d, "says")),
614 node: res!(get_u64_opt(d, "node")),
615 offset: res!(get_u64_opt(d, "offset")),
616 note: res!(get_str(d, "note")),
617 })
618 }
619}
620
621/// The value under a key of a daticle map.
622fn get(d: &Dat, key: &str) -> Outcome<Dat> {
623 match d {
624 Dat::Map(m) => match m.get(&dat!(key)) {
625 Some(v) => Ok(v.clone()),
626 None => Err(err!(
627 "The declaration is missing the required key '{}'.", key;
628 Invalid, Input, Missing)),
629 },
630 d => Err(err!(
631 "A fixture declaration is a map, found a {:?}.", d.kind();
632 Invalid, Input)),
633 }
634}
635
636/// The value under an optional key of a daticle map.
637fn get_opt(d: &Dat, key: &str) -> Outcome<Option<Dat>> {
638 match d {
639 Dat::Map(m) => Ok(m.get(&dat!(key)).cloned()),
640 d => Err(err!(
641 "A fixture declaration is a map, found a {:?}.", d.kind();
642 Invalid, Input)),
643 }
644}
645
646/// A required string key.
647fn get_str(d: &Dat, key: &str) -> Outcome<String> {
648 match res!(get(d, key)) {
649 Dat::Str(s) => Ok(s),
650 v => Err(err!(
651 "The key '{}' carries a {:?}, but a str was expected.", key, v.kind();
652 Invalid, Input, Mismatch)),
653 }
654}
655
656/// A required unsigned integer key.
657fn get_u64(d: &Dat, key: &str) -> Outcome<u64> {
658 match res!(get(d, key)) {
659 Dat::U64(n) => Ok(n),
660 v => Err(err!(
661 "The key '{}' carries a {:?}, but a u64 was expected.", key, v.kind();
662 Invalid, Input, Mismatch)),
663 }
664}
665
666/// An optional unsigned integer key.
667fn get_u64_opt(d: &Dat, key: &str) -> Outcome<Option<u64>> {
668 match res!(get_opt(d, key)) {
669 None => Ok(None),
670 Some(Dat::U64(n)) => Ok(Some(n)),
671 Some(v) => Err(err!(
672 "The key '{}' carries a {:?}, but a u64 was expected.", key, v.kind();
673 Invalid, Input, Mismatch)),
674 }
675}
676
677/// A required boolean key.
678fn get_bool(d: &Dat, key: &str) -> Outcome<bool> {
679 match res!(get(d, key)) {
680 Dat::Bool(b) => Ok(b),
681 v => Err(err!(
682 "The key '{}' carries a {:?}, but a bool was expected.", key, v.kind();
683 Invalid, Input, Mismatch)),
684 }
685}
686
687/// A required byte string key.
688fn get_bytes(d: &Dat, key: &str) -> Outcome<Vec<u8>> {
689 match res!(get(d, key)) {
690 Dat::BU8(v) => Ok(v),
691 v => Err(err!(
692 "The key '{}' carries a {:?}, but a bu8 was expected.", key, v.kind();
693 Invalid, Input, Mismatch)),
694 }
695}
696
697/// The fixture directory.
698pub fn fixtures_dir() -> PathBuf {
699 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures")
700}
701
702/// Reads a file whole.
703pub fn read_bytes(path: &Path) -> Outcome<Vec<u8>> {
704 match fs::read(path) {
705 Ok(byts) => Ok(byts),
706 Err(e) => Err(err!(e,
707 "Could not read {}.", path.display();
708 IO, File)),
709 }
710}
711
712/// Reads a text file whole.
713pub fn read_text(path: &Path) -> Outcome<String> {
714 match fs::read_to_string(path) {
715 Ok(s) => Ok(s),
716 Err(e) => Err(err!(e,
717 "Could not read {}.", path.display();
718 IO, File)),
719 }
720}
721
722/// Writes a file whole.
723pub fn write_bytes(path: &Path, byts: &[u8]) -> Outcome<()> {
724 match fs::write(path, byts) {
725 Ok(()) => Ok(()),
726 Err(e) => Err(err!(e,
727 "Could not write {}.", path.display();
728 IO, File)),
729 }
730}
731
732/// The trees the fixtures are built from, and the ones they are built to break.
733pub mod tree {
734 use super::*;
735
736 /// A document with nothing in it.
737 pub fn empty() -> Dat {
738 node(NodeKind::Doc, map(vec![
739 ("title", Dat::Str("An empty document".to_string())),
740 ("lang", Dat::Str("en".to_string())),
741 ]))
742 }
743
744 /// A document of one paragraph.
745 pub fn one_para() -> Dat {
746 doc_of(vec![
747 node(NodeKind::Para, map(vec![
748 ("children", Dat::List(vec![
749 text("The web gave that up in 1993 and spent thirty years paying for it."),
750 ])),
751 ])),
752 ])
753 }
754
755 /// A document using every v0 node kind once, an optional field present, another absent, and a
756 /// node carrying no children at all.
757 ///
758 /// The thirteen kinds are the doc itself, a heading, a section, a paragraph, text, emphasis, a
759 /// link addressed by name, code, a quote, a list, an item, a box, and an image. The link carries
760 /// the typed address form of §4.3, a single-entry map, and the image references its content by a
761 /// `b32` content hash, both of which the old bare-string and short-byte forms are not.
762 pub fn every_kind() -> Dat {
763 node(NodeKind::Doc, map(vec![
764 ("title", Dat::Str("Style without a cascade".to_string())),
765 ("lang", Dat::Str("en".to_string())),
766 ("children", Dat::List(vec![
767 node(NodeKind::Heading, map(vec![
768 ("level", Dat::U8(2)),
769 ("children", Dat::List(vec![text("Style without a cascade")])),
770 ])),
771 node(NodeKind::Section, map(vec![
772 ("title", Dat::Str("A section".to_string())),
773 ("children", Dat::List(vec![
774 node(NodeKind::Para, map(vec![
775 ("children", Dat::List(vec![
776 text("A run\twith a tab\nand a newline, "),
777 node(NodeKind::Emph, map(vec![
778 ("strong", Dat::Bool(true)),
779 ("children", Dat::List(vec![text("loud")])),
780 ])),
781 node(NodeKind::Link, map(vec![
782 ("to", map(vec![
783 ("name", Dat::Str("news.cricket".to_string())),
784 ])),
785 ("children", Dat::List(vec![text("and a link")])),
786 ])),
787 ])),
788 ])),
789 // A paragraph with no children omits the key entirely (§3 rule 4).
790 node(NodeKind::Para, map(vec![])),
791 // A preserved run of source, whose text is a field rather than children.
792 node(NodeKind::Code, map(vec![
793 ("lang", Dat::Str("rust".to_string())),
794 ("text", Dat::Str("fn main() {}".to_string())),
795 ])),
796 // A block quotation, carrying flow content and an optional citation.
797 node(NodeKind::Quote, map(vec![
798 ("cite", Dat::Str("A. Author".to_string())),
799 ("children", Dat::List(vec![
800 node(NodeKind::Para, map(vec![
801 ("children", Dat::List(vec![text("A quoted line.")])),
802 ])),
803 ])),
804 ])),
805 node(NodeKind::List, map(vec![
806 ("ordered", Dat::Bool(false)),
807 ("children", Dat::List(vec![
808 node(NodeKind::Item, map(vec![
809 ("children", Dat::List(vec![
810 node(NodeKind::Para, map(vec![
811 ("children", Dat::List(vec![text("An item.")])),
812 ])),
813 ])),
814 ])),
815 ])),
816 ])),
817 // A box with its optional style absent, around an image with both of its
818 // optional fields present and a b32 content hash.
819 node(NodeKind::Boxx, map(vec![
820 ("children", Dat::List(vec![
821 node(NodeKind::Image, map(vec![
822 ("hash", Dat::from([0x01u8; 32])),
823 ("alt", Dat::Str("A diagram of a tree".to_string())),
824 ("w", Dat::U32(640)),
825 ("h", Dat::U32(480)),
826 ])),
827 ])),
828 ])),
829 ])),
830 ])),
831 ])),
832 ]))
833 }
834
835 /// A document exercising the style table of §4.4: an inherited property and a self-only one.
836 ///
837 /// The table defines two styles. `callout` carries the self-only `bg` and `pad`, and `lede`
838 /// carries the inherited `size`; a box names the first and a paragraph within it names the second,
839 /// so that both a style that inherits and one that does not are declared, named, and resolved.
840 pub fn styled() -> Dat {
841 node(NodeKind::Doc, map(vec![
842 ("title", Dat::Str("Style without a cascade".to_string())),
843 ("lang", Dat::Str("en".to_string())),
844 ("styles", map(vec![
845 ("callout", map(vec![
846 ("bg", Dat::Str("muted".to_string())), // Self-only (§4.4).
847 ("pad", Dat::U8(3)), // Self-only.
848 ])),
849 ("lede", map(vec![
850 ("size", Dat::I8(1)), // Inherited.
851 ])),
852 ])),
853 ("children", Dat::List(vec![
854 node(NodeKind::Boxx, map(vec![
855 ("style", Dat::Str("callout".to_string())),
856 ("children", Dat::List(vec![
857 node(NodeKind::Para, map(vec![
858 ("style", Dat::Str("lede".to_string())),
859 ("children", Dat::List(vec![
860 text("A lede paragraph in a callout box."),
861 ])),
862 ])),
863 ])),
864 ])),
865 ])),
866 ]))
867 }
868
869 /// A document where a box names an alignment, and the paragraphs inside it do not (§4.4).
870 ///
871 /// `align` is self-only, so it aligns the lines of the node that named it and of nothing within
872 /// it. The box has no lines of its own, so this document must read exactly as it would with no
873 /// style at all. It is here because the property inherits in CSS and does not in the format, so a
874 /// reader built on a browser will carry the box's alignment down into its paragraphs unless it is
875 /// stopped, and then one document says two things.
876 pub fn align_is_local() -> Dat {
877 node(NodeKind::Doc, map(vec![
878 ("title", Dat::Str("An alignment that stays where it is put".to_string())),
879 ("lang", Dat::Str("en".to_string())),
880 ("styles", map(vec![
881 ("flush", map(vec![
882 ("align", Dat::Str("justify".to_string())), // Self-only (§4.4).
883 ])),
884 ])),
885 ("children", Dat::List(vec![
886 node(NodeKind::Boxx, map(vec![
887 ("style", Dat::Str("flush".to_string())),
888 ("children", Dat::List(vec![
889 node(NodeKind::Para, map(vec![
890 ("children", Dat::List(vec![
891 // Long enough to take several lines, since a promise about how lines
892 // are aligned cannot be tested on a document with one line.
893 text("The oxeweb replaces the web's cascade with locality, so that a \
894 style error cannot escape the node that made it, and no rule \
895 reaches across a document to touch what it never named."),
896 ])),
897 ])),
898 ])),
899 ])),
900 ])),
901 ]))
902 }
903
904 /// A document whose one paragraph carries a link addressed by NAMES name (§4.3).
905 pub fn link_by_name() -> Dat {
906 doc_of(vec![
907 node(NodeKind::Para, map(vec![
908 ("children", Dat::List(vec![
909 node(NodeKind::Link, map(vec![
910 ("to", map(vec![
911 ("name", Dat::Str("news.cricket".to_string())),
912 ])),
913 ("children", Dat::List(vec![text("the cricket news")])),
914 ])),
915 ])),
916 ])),
917 ])
918 }
919
920 /// A document whose one paragraph carries a link addressed by content hash (§4.3).
921 pub fn link_by_hash() -> Dat {
922 doc_of(vec![
923 node(NodeKind::Para, map(vec![
924 ("children", Dat::List(vec![
925 node(NodeKind::Link, map(vec![
926 ("to", map(vec![
927 ("hash", Dat::from([0x9fu8; 32])),
928 ])),
929 ("children", Dat::List(vec![text("a document by address")])),
930 ])),
931 ])),
932 ])),
933 ])
934 }
935
936 /// A document whose one child is a kind this version does not know, carrying a valid fallback.
937 ///
938 /// The fallback is a list of known nodes that stand in for the unknown kind (§4.5), and the
939 /// unknown node also carries an uninterpreted field, which a reader that knew the kind would use
940 /// and which this one holds only to the canonical encoding rules of §3. A reader that does not
941 /// know the kind renders and validates the fallback, so the whole document is accepted.
942 pub fn unknown_fallback() -> Dat {
943 node(NodeKind::Doc, map(vec![
944 ("title", Dat::Str("A document with an unknown kind".to_string())),
945 ("lang", Dat::Str("en".to_string())),
946 ("children", Dat::List(vec![
947 alien(map(vec![
948 ("fallback", Dat::List(vec![
949 node(NodeKind::List, map(vec![
950 ("ordered", Dat::Bool(false)),
951 ("children", Dat::List(vec![
952 node(NodeKind::Item, map(vec![
953 ("children", Dat::List(vec![
954 node(NodeKind::Para, map(vec![
955 ("children", Dat::List(vec![
956 text("Q1 revenue: 1.2M"),
957 ])),
958 ])),
959 ])),
960 ])),
961 node(NodeKind::Item, map(vec![
962 ("children", Dat::List(vec![
963 node(NodeKind::Para, map(vec![
964 ("children", Dat::List(vec![
965 text("Q2 revenue: 1.5M"),
966 ])),
967 ])),
968 ])),
969 ])),
970 ])),
971 ])),
972 ])),
973 // A field only a reader that knows the kind interprets.
974 ("rows", Dat::Str("held to §3, not read as a document".to_string())),
975 ])),
976 ])),
977 ]))
978 }
979
980 /// A document whose deepest node sits at `depth`, counting the root as 1.
981 ///
982 /// The chain is boxes, since a box takes flow content and is therefore the cheapest way to nest.
983 pub fn chain(depth: usize) -> Outcome<Dat> {
984 if depth < 2 {
985 return Err(err!(
986 "A chain of depth {} has no room for the doc at its head.", depth;
987 Invalid, Input));
988 }
989 let mut inner = node(NodeKind::Boxx, map(vec![]));
990 for _ in 0..(depth - 2) {
991 inner = node(NodeKind::Boxx, map(vec![
992 ("children", Dat::List(vec![inner])),
993 ]));
994 }
995 Ok(doc_of(vec![inner]))
996 }
997
998 /// A document whose canonical encoding is exactly `target` bytes.
999 ///
1000 /// The tree is one paragraph of one text run, padded until the bytes come out to the byte the
1001 /// caller asked for, since the size limit of §5 is a limit on the encoded region and a fixture
1002 /// at the limit must land on it exactly.
1003 pub fn sized(target: usize) -> Outcome<Dat> {
1004 // The encoding grows by one byte for each character of the pad, except where the length in
1005 // front of a compound needs another byte, so search for the longest pad that fits and then
1006 // walk up from it.
1007 let mut lo = 0;
1008 let mut hi = target;
1009 while lo < hi {
1010 let mid = lo + (hi - lo + 1) / 2;
1011 if res!(sized_len(mid)) <= target {
1012 lo = mid;
1013 } else {
1014 hi = mid - 1;
1015 }
1016 }
1017 for n in lo..=(lo + 8) {
1018 if res!(sized_len(n)) == target {
1019 return Ok(padded(n));
1020 }
1021 }
1022 Err(err!(
1023 "No padding gives a tree of exactly {} bytes; the nearest is {} bytes.",
1024 target, res!(sized_len(lo));
1025 Invalid, Input, Bug))
1026 }
1027
1028 /// The encoded length of the padded document with a pad of `n` characters.
1029 fn sized_len(n: usize) -> Outcome<usize> {
1030 Ok(res!(padded(n).to_bytes(Vec::new())).len())
1031 }
1032
1033 /// The padded document, with a pad of `n` characters of prose.
1034 fn padded(n: usize) -> Dat {
1035 doc_of(vec![
1036 node(NodeKind::Para, map(vec![
1037 ("children", Dat::List(vec![text(&prose(n))])),
1038 ])),
1039 ])
1040 }
1041
1042 /// `n` characters of ASCII prose, so that a character is a byte.
1043 fn prose(n: usize) -> String {
1044 const LINE: &'static str =
1045 "The hash is the address, and the address is the hash. ";
1046 let mut s = String::with_capacity(n + LINE.len());
1047 while s.len() < n {
1048 s.push_str(LINE);
1049 }
1050 s.truncate(n);
1051 s
1052 }
1053
1054 /// A document whose children are the given flow nodes.
1055 pub fn doc_of(kids: Vec<Dat>) -> Dat {
1056 node(NodeKind::Doc, map(vec![
1057 ("title", Dat::Str("A document".to_string())),
1058 ("lang", Dat::Str("en".to_string())),
1059 ("children", Dat::List(kids)),
1060 ]))
1061 }
1062
1063 /// A document whose one child is a heading carrying the given payload, which is node 1.
1064 pub fn doc_with_heading(payload: Dat) -> Dat {
1065 node(NodeKind::Doc, map(vec![
1066 ("title", Dat::Str("A document".to_string())),
1067 ("lang", Dat::Str("en".to_string())),
1068 ("children", Dat::List(vec![
1069 Dat::Usr(ukid(NodeKind::Heading), Some(Box::new(payload))),
1070 ])),
1071 ]))
1072 }
1073}
1074
1075/// The schema every fixture but one declares.
1076pub fn schema() -> &'static str {
1077 SCHEMA_DOC
1078}
1079
1080/// The node depth limit, quoted where a fixture needs it.
1081pub fn depth_limit() -> usize {
1082 limit::DEPTH
1083}
1084
1085/// The tree region size limit, quoted where a fixture needs it.
1086pub fn tree_limit() -> usize {
1087 limit::TREE_BYTES
1088}
1089
1090/// Encodes a tree canonically, refusing one that is not canonical.
1091pub fn encode(tree: &Dat) -> Outcome<Vec<u8>> {
1092 canon::encode(tree)
1093}
1094
1095/// Encodes a tree without checking it, which is how a fixture that breaks a rule is written.
1096pub fn encode_unchecked(tree: &Dat) -> Outcome<Vec<u8>> {
1097 Ok(res!(tree.to_bytes(Vec::new())))
1098}