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 | |
| 17 | use 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 | |
| 34 | use oxedyne_fe2o3_core::prelude::*; |
| 35 | use oxedyne_fe2o3_crypto::sign::SignatureScheme; |
| 36 | use oxedyne_fe2o3_iop_crypto::{ |
| 37 | keys::KeyManager, |
| 38 | sign::Signer, |
| 39 | }; |
| 40 | use 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 | |
| 54 | use 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. |
| 64 | pub 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. |
| 76 | pub const STACK_BYTES: usize = 8 * 1024 * 1024; |
| 77 | |
| 78 | /// The file holding the fixed key the fixtures are signed with. |
| 79 | pub const KEY_FILE: &'static str = "key.jdat"; |
| 80 | /// The file explaining the fixture directory to a reader. |
| 81 | pub const README_FILE: &'static str = "README.md"; |
| 82 | /// The document, in JDAT text form: the source of truth for a fixture. |
| 83 | pub const DOC_JDAT: &'static str = "doc.jdat"; |
| 84 | /// The signed binary artefact. |
| 85 | pub const DOC_SBJ: &'static str = "doc.sbj"; |
| 86 | /// The expectations of an acceptance fixture. |
| 87 | pub const META_JDAT: &'static str = "meta.jdat"; |
| 88 | /// The declared failure of a rejection fixture. |
| 89 | pub const REJECT_JDAT: &'static str = "reject.jdat"; |
| 90 | |
| 91 | /// Every v0 node kind, in code order. |
| 92 | pub 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. |
| 109 | pub const ALIEN_CODE: u16 = 99; |
| 110 | |
| 111 | /// The label the alien kind carries in the JDAT text form. |
| 112 | pub 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. |
| 119 | pub 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. |
| 122 | pub 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. |
| 130 | pub 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. |
| 141 | pub 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. |
| 155 | pub 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. |
| 165 | pub 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. |
| 175 | pub 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. |
| 183 | pub 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. |
| 192 | pub 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. |
| 205 | pub 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. |
| 215 | pub 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`. |
| 226 | pub 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. |
| 235 | pub 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. |
| 240 | pub fn text(s: &str) -> Dat { |
| 241 | node(NodeKind::Text, Dat::Str(s.to_string())) |
| 242 | } |
| 243 | |
| 244 | /// A payload map, from string keys. |
| 245 | pub 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)] |
| 256 | pub 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)] |
| 265 | pub 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 | |
| 272 | impl 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 | |
| 296 | impl 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. |
| 346 | fn 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. |
| 366 | pub 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. |
| 394 | pub 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. |
| 405 | pub 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. |
| 419 | pub 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. |
| 433 | pub 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. |
| 439 | pub 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)] |
| 451 | pub 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 | |
| 468 | impl 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)] |
| 507 | pub 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 | |
| 530 | impl 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)] |
| 574 | pub 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 | |
| 589 | impl 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. |
| 622 | fn 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. |
| 637 | fn 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. |
| 647 | fn 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. |
| 657 | fn 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. |
| 667 | fn 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. |
| 678 | fn 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. |
| 688 | fn 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. |
| 698 | pub fn fixtures_dir() -> PathBuf { |
| 699 | PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures") |
| 700 | } |
| 701 | |
| 702 | /// Reads a file whole. |
| 703 | pub 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. |
| 713 | pub 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. |
| 723 | pub 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. |
| 733 | pub 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. |
| 1076 | pub fn schema() -> &'static str { |
| 1077 | SCHEMA_DOC |
| 1078 | } |
| 1079 | |
| 1080 | /// The node depth limit, quoted where a fixture needs it. |
| 1081 | pub fn depth_limit() -> usize { |
| 1082 | limit::DEPTH |
| 1083 | } |
| 1084 | |
| 1085 | /// The tree region size limit, quoted where a fixture needs it. |
| 1086 | pub fn tree_limit() -> usize { |
| 1087 | limit::TREE_BYTES |
| 1088 | } |
| 1089 | |
| 1090 | /// Encodes a tree canonically, refusing one that is not canonical. |
| 1091 | pub 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. |
| 1096 | pub fn encode_unchecked(tree: &Dat) -> Outcome<Vec<u8>> { |
| 1097 | Ok(res!(tree.to_bytes(Vec::new()))) |
| 1098 | } |