Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_sbj/src/bin/sbj.rs

28.2 KiB, 1 run

created by r1870400018:22206, 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//! `sbj` -- the authoring toolchain for the SBJ document format.
2//!
3//! A document's source is its JDAT text form and its artefact is the signed binary file, and this is
4//! what turns one into the other. `compile` reads the text, validates it against the node schema,
5//! encodes it canonically, hashes it, signs the hash and writes the file; `import` does the same to
6//! a Markdown file, having first mapped the prose to the node vocabulary; `verify` runs the five
7//! steps of `SPEC.md` §2 that touch no content and reports what the envelope vouches for; `inspect`
8//! reads the document whole and says what is in it; and `dump` writes a document back out as text,
9//! so that a document can be read, edited and recompiled by whoever holds it.
10//!
11//! Run `sbj` with no arguments for the usage.
12
13#![forbid(unsafe_code)]
14
15use oxedyne_fe2o3_sbj::{
16 doc,
17 envelope::{
18 self,
19 Envelope,
20 },
21 import,
22 index,
23 key::{
24 self,
25 KeyPair,
26 },
27 kinds::{
28 NodeKind,
29 ReservedKind,
30 KEY_STYLES,
31 },
32 text::{
33 self,
34 KindDecl,
35 },
36 validate,
37 SCHEMA_DOC,
38};
39
40use oxedyne_fe2o3_core::prelude::*;
41use oxedyne_fe2o3_jdat::prelude::*;
42
43use std::{
44 collections::BTreeMap,
45 fs,
46 path::{
47 Path,
48 PathBuf,
49 },
50 process,
51 time::{
52 SystemTime,
53 UNIX_EPOCH,
54 },
55};
56
57/// The key file a compiler signs with when the author names none.
58const DEFAULT_KEY: &'static str = "key.jdat";
59
60/// The stack the work is done on.
61///
62/// The JDAT text decoder is recursive, and a document may legally nest to the depth limit of
63/// `SPEC.md` §5. A node costs about five text levels and the limit is 64, so the deepest legal
64/// document needs some 320 levels, at about 2.4 KB of stack each in a build with no optimisation:
65/// call it 800 KB. Eight mebibytes leaves a tenfold margin.
66///
67/// The limit is the format's and does not move to suit a tool, so the tool moves. The stack is
68/// reserved rather than committed, and a document that never nests deeply never touches it.
69const STACK_BYTES: usize = 8 * 1024 * 1024;
70
71/// What the tool was asked to do.
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73enum Cmd {
74 /// Read a document's text form and write the signed artefact.
75 Compile,
76 /// Read a Markdown file, map it to the node vocabulary, and write the signed artefact.
77 Import,
78 /// Run steps 1 to 5 of §2, which touch no content, and report what the envelope vouches for.
79 Verify,
80 /// Read a document whole and report what is in it.
81 Inspect,
82 /// Write a document back out in its text form.
83 Dump,
84}
85
86impl Cmd {
87
88 /// The subcommand a word names.
89 fn from_word(word: &str) -> Outcome<Self> {
90 match word {
91 "compile" => Ok(Self::Compile),
92 "import" => Ok(Self::Import),
93 "verify" => Ok(Self::Verify),
94 "inspect" => Ok(Self::Inspect),
95 "dump" => Ok(Self::Dump),
96 _ => Err(err!(
97 "'{}' is not an sbj subcommand. The subcommands are compile, import, verify, \
98 inspect and dump.", word;
99 Invalid, Input)),
100 }
101 }
102}
103
104/// The arguments a run was given.
105#[derive(Clone, Debug)]
106struct Args {
107 /// The subcommand.
108 cmd: Cmd,
109 /// The file to read: a `doc.jdat` to compile, or a `doc.sbj` to read.
110 input: PathBuf,
111 /// The file to write, if the subcommand writes one.
112 output: Option<PathBuf>,
113 /// The key file to sign with.
114 keyfile: PathBuf,
115 /// The entry of the key file to sign with, where the file holds several pairs.
116 entry: Option<String>,
117 /// The authoring time, in Unix milliseconds. The clock, when the author names none.
118 time: Option<u64>,
119 /// Whether to append the optional index of §1.4.
120 index: bool,
121 /// The kinds outside the v0 vocabulary the source names by a label of its own (§4.5).
122 kinds: Vec<KindDecl>,
123 /// The title an imported document carries. Its first level 1 heading, when the author names none.
124 title: Option<String>,
125 /// The language an imported document declares. [`import::DEFAULT_LANG`], when the author names
126 /// none.
127 lang: Option<String>,
128}
129
130fn main() {
131 // The work runs on a thread with a stack that can hold the deepest document the format permits.
132 let thread = match std::thread::Builder::new()
133 .name("sbj".to_string())
134 .stack_size(STACK_BYTES)
135 .spawn(run)
136 {
137 Ok(thread) => thread,
138 Err(e) => {
139 eprintln!("sbj: could not start: {}", e);
140 process::exit(1);
141 },
142 };
143 let outcome = match thread.join() {
144 Ok(outcome) => outcome,
145 Err(_) => {
146 eprintln!("sbj: the working thread did not return.");
147 process::exit(1);
148 },
149 };
150 match outcome {
151 Ok(()) => process::exit(0),
152 Err(e) => {
153 eprintln!("sbj: {}", e);
154 process::exit(1);
155 },
156 }
157}
158
159/// Reads the arguments and runs the subcommand they name.
160fn run() -> Outcome<()> {
161 let words: Vec<String> = std::env::args().skip(1).collect();
162 if words.is_empty() || words.iter().any(|w| w == "-h" || w == "--help") {
163 usage();
164 return Ok(());
165 }
166 let args = res!(parse(&words));
167 match args.cmd {
168 Cmd::Compile => compile(&args),
169 Cmd::Import => import(&args),
170 Cmd::Verify => verify(&args),
171 Cmd::Inspect => inspect(&args),
172 Cmd::Dump => dump(&args),
173 }
174}
175
176/// Prints what the tool does and how it is asked to do it.
177fn usage() {
178 println!("\
179sbj -- the authoring toolchain for SBJ, the oxeweb document format.
180
181Usage:
182 sbj compile <doc.jdat> -o <doc.sbj> [options] Validate, canonicalise, hash, sign, write.
183 sbj import <doc.md|.html> [-o <doc.sbj>] Map Markdown or HTML to a document, then compile it.
184 sbj verify <doc.sbj> Check the envelope without decoding the tree.
185 sbj inspect <doc.sbj> Read the document whole and say what is in it.
186 sbj dump <doc.sbj> [-o <doc.jdat>] Write the document back out as text.
187
188Options:
189 -o, --out <path> Where to write. Compile requires it; import writes beside its source;
190 dump writes to stdout without it.
191 -k, --key <path> The key file to sign with, generated and saved if absent [{key}].
192 --key-entry <name> The entry of the key file to sign with, where it holds several pairs.
193 -t, --time <ms> The authoring time, in Unix milliseconds [the clock].
194 --index Append the optional offset index of SPEC.md 1.4.
195 --kind <label>=<n> Declare a kind outside the v0 vocabulary, e.g. --kind sbj_alien=99.
196 A kind written (sbj_k<n>|{{..}}) needs no declaring.
197 --title <text> Import: the document's title [its first level 1 heading, or the file name].
198 --lang <tag> Import: the document's language, BCP-47 [{lang}].
199 -h, --help This.
200
201An import maps the prose and drops what the v0 vocabulary has no room for: a thematic break goes,
202an image becomes its alt text, since v0 addresses an image by content hash and Markdown gives a
203path, and an inline code span becomes its characters, since code is flow content and cannot sit in
204a paragraph.
205
206A document is written in JDAT text form, where a node is its kind label and its payload:
207
208 (sbj_doc|(map|{{
209 (str|\"title\"): (str|\"Style without a cascade\"),
210 (str|\"lang\"): (str|\"en\"),
211 (str|\"children\"): (list|[
212 (sbj_para|(map|{{
213 (str|\"children\"): (list|[(sbj_text|(str|\"A paragraph.\"))]),
214 }})),
215 ]),
216 }}))
217", key = DEFAULT_KEY, lang = import::DEFAULT_LANG);
218}
219
220/// Reads the arguments, refusing one that is not an argument and one that is missing its value.
221fn parse(words: &[String]) -> Outcome<Args> {
222
223 let cmd = res!(Cmd::from_word(&words[0]));
224
225 let mut input: Option<PathBuf> = None;
226 let mut output: Option<PathBuf> = None;
227 let mut keyfile: PathBuf = PathBuf::from(DEFAULT_KEY);
228 let mut entry: Option<String> = None;
229 let mut time: Option<u64> = None;
230 let mut index: bool = false;
231 let mut kinds: Vec<KindDecl> = Vec::new();
232 let mut title: Option<String> = None;
233 let mut lang: Option<String> = None;
234
235 let mut i = 1;
236 while i < words.len() {
237 let word = words[i].as_str();
238 match word {
239 "-o" | "--out" => {
240 output = Some(PathBuf::from(res!(value(words, &mut i, word))));
241 },
242 "-k" | "--key" => {
243 keyfile = PathBuf::from(res!(value(words, &mut i, word)));
244 },
245 "--key-entry" => {
246 entry = Some(res!(value(words, &mut i, word)));
247 },
248 "-t" | "--time" => {
249 let v = res!(value(words, &mut i, word));
250 time = Some(match v.parse::<u64>() {
251 Ok(ms) => ms,
252 Err(_) => return Err(err!(
253 "The time '{}' is not a number of Unix milliseconds.", v;
254 Invalid, Input)),
255 });
256 },
257 "--index" => index = true,
258 "--kind" => {
259 kinds.push(res!(kind_decl(&res!(value(words, &mut i, word)))));
260 },
261 "--title" => {
262 title = Some(res!(value(words, &mut i, word)));
263 },
264 "--lang" => {
265 lang = Some(res!(value(words, &mut i, word)));
266 },
267 other if other.starts_with('-') => return Err(err!(
268 "'{}' is not an sbj option. Run `sbj --help` for the ones there are.", other;
269 Invalid, Input, Unknown)),
270 other => {
271 if input.is_some() {
272 return Err(err!(
273 "sbj {} reads one file, and was given both '{}' and '{}'.",
274 words[0], input_name(&input), other;
275 Invalid, Input, Excessive));
276 }
277 input = Some(PathBuf::from(other));
278 },
279 }
280 i += 1;
281 }
282
283 let input = match input {
284 Some(input) => input,
285 None => return Err(err!(
286 "sbj {} needs a file to read.", words[0];
287 Invalid, Input, Missing)),
288 };
289 if cmd == Cmd::Compile && output.is_none() {
290 return Err(err!(
291 "sbj compile needs somewhere to write the document: `sbj compile {} -o <doc.sbj>`.",
292 input.display();
293 Invalid, Input, Missing));
294 }
295
296 Ok(Args {
297 cmd,
298 input,
299 output,
300 keyfile,
301 entry,
302 time,
303 index,
304 kinds,
305 title,
306 lang,
307 })
308}
309
310/// The value of an option, or an error naming the option that was given none.
311fn value(
312 words: &[String],
313 i: &mut usize,
314 opt: &str,
315)
316 -> Outcome<String>
317{
318 *i += 1;
319 match words.get(*i) {
320 Some(v) => Ok(v.clone()),
321 None => Err(err!(
322 "The option '{}' takes a value, and was given none.", opt;
323 Invalid, Input, Missing)),
324 }
325}
326
327/// Reads a `--kind <label>=<code>` declaration.
328fn kind_decl(s: &str) -> Outcome<KindDecl> {
329 let (label, code) = match s.split_once('=') {
330 Some((label, code)) => (label.trim(), code.trim()),
331 None => return Err(err!(
332 "The kind declaration '{}' is not of the form <label>=<code>, e.g. sbj_alien=99.", s;
333 Invalid, Input)),
334 };
335 if label.is_empty() {
336 return Err(err!(
337 "The kind declaration '{}' names no label.", s;
338 Invalid, Input, Missing));
339 }
340 let code = match code.parse::<u16>() {
341 Ok(code) => code,
342 Err(_) => return Err(err!(
343 "The kind declaration '{}' gives the code '{}', which is not a u16. A node kind code \
344 runs from 0 to {}.", s, code, u16::MAX;
345 Invalid, Input)),
346 };
347 Ok(KindDecl {
348 label: label.to_string(),
349 code,
350 })
351}
352
353/// The name of the input file, for an error message raised before it is known there is one.
354fn input_name(input: &Option<PathBuf>) -> String {
355 match input {
356 Some(path) => fmt!("{}", path.display()),
357 None => fmt!("nothing"),
358 }
359}
360
361// ┌───────────────────────────────────────────────────────────────────────────┐
362// │ COMPILE AND IMPORT │
363// └───────────────────────────────────────────────────────────────────────────┘
364
365/// Compiles a document: read the text, validate, canonically encode, hash, sign, write.
366fn compile(args: &Args) -> Outcome<()> {
367 let src = res!(read_text(&args.input));
368 let tree = match text::decode(&src, &args.kinds) {
369 Ok(tree) => tree,
370 Err(e) => return Err(err!(e,
371 "{} is not a readable document.", args.input.display();
372 Invalid, Input)),
373 };
374 let out = match &args.output {
375 Some(out) => out.clone(),
376 None => return Err(err!(
377 "sbj compile needs somewhere to write the document."; Bug, Missing)),
378 };
379 sign_and_write(args, &tree, &out, "Compiled")
380}
381
382/// Imports a Markdown file: read the prose, map it to the node vocabulary, and compile the tree.
383///
384/// Everything after the mapping is the compile path exactly, because by then it is a document like
385/// any other: an imported tree is held to the same schema, canonicalised the same way, and signed by
386/// the same hand as one written by an author who typed the JDAT out. The mapping, and the three
387/// things Markdown says that v0 has no room for, are [`import`](oxedyne_fe2o3_sbj::import)'s business.
388fn import(args: &Args) -> Outcome<()> {
389 let src = res!(read_text(&args.input));
390 let opts = import::Options {
391 title: args.title.clone(),
392 lang: match &args.lang {
393 Some(lang) => lang.clone(),
394 None => import::DEFAULT_LANG.to_string(),
395 },
396 stem: stem(&args.input),
397 };
398 // The form is read from the name, because the two forms are told apart by nothing else: HTML and
399 // Markdown are both text, and a file that is one is legal input to the reader for the other.
400 let form = Form::of(&args.input);
401 let tree = match form.read(&src, &opts) {
402 Ok(tree) => tree,
403 Err(e) => return Err(err!(e,
404 "{} is not readable {}.", args.input.display(), form.label();
405 Invalid, Input)),
406 };
407 // An import writes beside its source unless it is told otherwise, since an author who has a
408 // document to import has nowhere in mind to put it yet.
409 let out = match &args.output {
410 Some(out) => out.clone(),
411 None => args.input.with_extension("sbj"),
412 };
413 sign_and_write(args, &tree, &out, "Imported")
414}
415
416/// The form an imported source is written in.
417///
418/// Both forms reach the same tree and the same mapping; they differ only in the reader that gets
419/// them there. HTML earns its place by being what everything else exports: prose written in a form
420/// no reader here understands is often reachable through the tool that does understand it, with the
421/// author's own macros already resolved.
422#[derive(Clone, Copy, Debug, PartialEq, Eq)]
423enum Form {
424 /// Markdown, the form most existing prose is written in.
425 Markdown,
426 /// HTML, the form most other things export.
427 Html,
428}
429
430impl Form {
431
432 /// The form a path names, judged by its extension. Anything not plainly HTML is read as Markdown,
433 /// which is the form an author is likelier to have and the likelier thing to mean.
434 fn of(path: &Path) -> Self {
435 let ext = match path.extension() {
436 Some(ext) => ext.to_string_lossy().to_lowercase(),
437 None => return Self::Markdown,
438 };
439 match ext.as_str() {
440 "html" | "htm" => Self::Html,
441 _ => Self::Markdown,
442 }
443 }
444
445 /// Reads a source of this form into a document tree.
446 fn read(&self, src: &str, opts: &import::Options) -> Outcome<Dat> {
447 match self {
448 Self::Markdown => import::from_markdown(src, opts),
449 Self::Html => import::from_html(src, opts),
450 }
451 }
452
453 /// What this form is called, for saying which reader refused a file.
454 fn label(&self) -> &'static str {
455 match self {
456 Self::Markdown => "Markdown",
457 Self::Html => "HTML",
458 }
459 }
460}
461
462/// The name a source file is known by, which is an imported document's title of last resort.
463fn stem(path: &Path) -> String {
464 match path.file_stem() {
465 Some(stem) => stem.to_string_lossy().to_string(),
466 None => import::DEFAULT_STEM.to_string(),
467 }
468}
469
470/// Signs a tree and writes the document: the path a compile and an import share.
471///
472/// Nothing this crate would refuse to read is ever given a signature and an address, so a document
473/// that fails its schema is refused here rather than published and refused by every reader of it.
474/// It is one path deliberately: a tree that came from Markdown is a document by the time it arrives,
475/// and a second signing path for it would be a second place for the schema check to go missing.
476fn sign_and_write(
477 args: &Args,
478 tree: &Dat,
479 out: &Path,
480 verb: &str,
481)
482 -> Outcome<()>
483{
484 let (pair, made) = res!(signing_key(&args.keyfile, args.entry.as_deref()));
485 let signer = res!(pair.signer());
486 let time = match args.time {
487 Some(time) => time,
488 None => res!(now()),
489 };
490
491 let buf = if args.index {
492 res!(doc::write_with_index(tree, SCHEMA_DOC, &signer, time))
493 } else {
494 res!(doc::write(tree, SCHEMA_DOC, &signer, time))
495 };
496
497 // It must read back the way a reader would, or it is not a document, it is a file.
498 let read = res!(doc::read(&buf));
499 let stats = res!(validate::validate(read.tree(), &read.env().schema));
500
501 res!(write_bytes(out, &buf));
502
503 if made {
504 println!("Generated a signing key and saved it to {}.", args.keyfile.display());
505 }
506 println!("{} {}", verb, args.input.display());
507 res!(report_envelope(read.env()));
508 println!(" nodes {}, depth {}", stats.nodes, stats.depth);
509 println!(" wrote {} ({} bytes{})",
510 out.display(),
511 buf.len(),
512 if args.index { ", index appended" } else { "" },
513 );
514 Ok(())
515}
516
517/// The key a document is signed with, generated and saved if the key file is not there yet.
518///
519/// The second return says whether the key was made here, since an author who did not know they had
520/// no key should be told where the one they now have was put.
521fn signing_key(
522 path: &Path,
523 entry: Option<&str>,
524)
525 -> Outcome<(KeyPair, bool)>
526{
527 if path.is_file() {
528 return Ok((res!(key::load(path, entry)), false));
529 }
530 if entry.is_some() {
531 return Err(err!(
532 "There is no key file at {}, and an entry of it was named. A key file that holds \
533 several pairs is not one this tool would have written, so it is not generated here.",
534 path.display();
535 Invalid, Input, Missing));
536 }
537 let pair = res!(KeyPair::generate());
538 res!(key::save(&pair, path));
539 Ok((pair, true))
540}
541
542// ┌───────────────────────────────────────────────────────────────────────────┐
543// │ VERIFY │
544// └───────────────────────────────────────────────────────────────────────────┘
545
546/// Verifies a document without decoding its tree: steps 1 to 5 of §2, which touch no content.
547///
548/// A document that fails renders as an error card and is never partially displayed, so this says so
549/// and stops, and the shell hears about it.
550fn verify(args: &Args) -> Outcome<()> {
551 let buf = res!(read_bytes(&args.input));
552 let env = match doc::verify_only(&buf) {
553 Ok(env) => env,
554 Err(e) => return Err(err!(e,
555 "{} is not a document that verifies.", args.input.display();
556 Invalid, Input)),
557 };
558 println!("Verified {} ({} bytes)", args.input.display(), buf.len());
559 res!(report_envelope(&env));
560 println!(" signature good, over the signing input of SPEC.md §1.3");
561 println!(" tree {} bytes, not decoded", env.tree_len);
562 Ok(())
563}
564
565/// Reports what an envelope vouches for: the schema, the author, the schemes, the time, the address.
566fn report_envelope(env: &Envelope) -> Outcome<()> {
567 println!(" schema {}", env.schema);
568 println!(" author {}", hex(&env.author));
569 println!(" sig scheme {:#010X} ({})", env.sig_scheme, res!(sig_scheme_name(env.sig_scheme)));
570 println!(" hash scheme {:#010X} ({})", env.hash_scheme,
571 res!(hash_scheme_name(env.hash_scheme)));
572 println!(" time {} (Unix ms)", env.time);
573 println!(" address {}", hex(&env.hash));
574 Ok(())
575}
576
577/// The name of a signature scheme id, refusing one this version does not implement.
578fn sig_scheme_name(id: u32) -> Outcome<&'static str> {
579 match id {
580 envelope::SIG_SCHEME_ED25519 => Ok("ed25519"),
581 _ => Err(err!(
582 "The envelope names the signature scheme {:#010X}, which this version does not \
583 implement. v0 signs with Ed25519, whose scheme id is {:#010X}.",
584 id, envelope::SIG_SCHEME_ED25519;
585 Invalid, Input, Unimplemented)),
586 }
587}
588
589/// The name of a hash scheme id, refusing one this version does not implement.
590fn hash_scheme_name(id: u32) -> Outcome<&'static str> {
591 match id {
592 envelope::HASH_SCHEME_SHA3_256 => Ok("sha3-256"),
593 _ => Err(err!(
594 "The envelope names the hash scheme {:#010X}, which this version does not implement. \
595 v0 hashes with SHA3-256, whose scheme id is {:#010X}.",
596 id, envelope::HASH_SCHEME_SHA3_256;
597 Invalid, Input, Unimplemented)),
598 }
599}
600
601// ┌───────────────────────────────────────────────────────────────────────────┐
602// │ INSPECT │
603// └───────────────────────────────────────────────────────────────────────────┘
604
605/// Reads a document whole and reports what is in it: its shape, its vocabulary, its styles.
606fn inspect(args: &Args) -> Outcome<()> {
607 let buf = res!(read_bytes(&args.input));
608 let read = match doc::read(&buf) {
609 Ok(read) => read,
610 Err(e) => return Err(err!(e,
611 "{} is not a document that reads.", args.input.display();
612 Invalid, Input)),
613 };
614 let stats = res!(validate::validate(read.tree(), &read.env().schema));
615
616 println!("Inspected {} ({} bytes)", args.input.display(), buf.len());
617 res!(report_envelope(read.env()));
618 println!(" tree {} bytes", read.env().tree_len);
619 println!(" nodes {}, depth {}", stats.nodes, stats.depth);
620
621 let mut counts: BTreeMap<u16, usize> = BTreeMap::new();
622 count_kinds(read.tree(), &mut counts);
623 println!(" kinds {}", res!(kind_list(&counts)));
624
625 println!(" styles {}", res!(style_list(read.tree())));
626
627 // The index of §1.4 is derived data lying outside the hash, and is never trusted: what it says is
628 // checked against the tree it claims to describe before it is believed.
629 let rest = res!(doc::index_region(&buf));
630 if rest.is_empty() {
631 println!(" index none");
632 } else {
633 let idx = res!(index::parse(rest));
634 let (_, region) = res!(doc::verify(&buf));
635 match index::check(region, &idx) {
636 Ok(()) => println!(" index {} entries, {} bytes, checked against the tree",
637 idx.len(), rest.len()),
638 Err(e) => return Err(err!(e,
639 "The index of {} does not describe the tree it trails.", args.input.display();
640 Invalid, Input)),
641 }
642 }
643 Ok(())
644}
645
646/// Counts the nodes of each kind, descending into children and into the fallback of §4.5.
647fn count_kinds(
648 node: &Dat,
649 counts: &mut BTreeMap<u16, usize>,
650) {
651 let (uid, payload) = match node {
652 Dat::Usr(uid, Some(payload)) => (uid, payload.as_ref()),
653 _ => return,
654 };
655 *counts.entry(uid.code()).or_insert(0) += 1;
656 let map = match payload {
657 Dat::Map(map) => map,
658 _ => return,
659 };
660 for (_, v) in map {
661 if let Dat::List(list) = v {
662 for kid in list {
663 count_kinds(kid, counts);
664 }
665 }
666 }
667}
668
669/// The kinds a document uses, and how often, for the report.
670fn kind_list(counts: &BTreeMap<u16, usize>) -> Outcome<String> {
671 if counts.is_empty() {
672 return Ok(fmt!("none"));
673 }
674 let mut s = String::new();
675 for (code, n) in counts {
676 if !s.is_empty() {
677 s.push_str(", ");
678 }
679 let name = match NodeKind::from_code(*code) {
680 Ok(kind) => kind.label().to_string(),
681 Err(_) => match ReservedKind::from_code(*code) {
682 // A kind reserved to the chrome and to applications (§4.2). A document carrying one
683 // never reads, so this names a tree that came from somewhere other than a document.
684 Some(reserved) => fmt!("reserved kind {}", reserved.label()),
685 // A kind the vocabulary does not know, which §4.5 admits because it carries a
686 // fallback.
687 None => fmt!("unknown kind {}", code),
688 },
689 };
690 s.push_str(&fmt!("{} {}", name, n));
691 }
692 Ok(s)
693}
694
695/// The document's style table (§4.4), as the report spells it.
696fn style_list(tree: &Dat) -> Outcome<String> {
697 let map = match tree {
698 Dat::Usr(_, Some(payload)) => match payload.as_ref() {
699 Dat::Map(map) => map,
700 _ => return Ok(fmt!("none")),
701 },
702 _ => return Ok(fmt!("none")),
703 };
704 let table = match map.get(&dat!(KEY_STYLES)) {
705 Some(Dat::Map(table)) => table,
706 _ => return Ok(fmt!("none")),
707 };
708 let mut s = String::new();
709 for (name, record) in table {
710 if !s.is_empty() {
711 s.push_str("\n ");
712 }
713 let name = match name {
714 Dat::Str(name) => name.clone(),
715 d => return Err(err!(
716 "A style name is a str, found a {:?}.", d.kind();
717 Invalid, Input)),
718 };
719 s.push_str(&fmt!("{}: {}", name, res!(style_record(record))));
720 }
721 Ok(s)
722}
723
724/// One style record, as the report spells it.
725fn style_record(record: &Dat) -> Outcome<String> {
726 let map = match record {
727 Dat::Map(map) => map,
728 d => return Err(err!(
729 "A style record is a map, found a {:?}.", d.kind();
730 Invalid, Input)),
731 };
732 let mut s = String::new();
733 for (prop, v) in map {
734 if !s.is_empty() {
735 s.push_str(", ");
736 }
737 let prop = match prop {
738 Dat::Str(prop) => prop.clone(),
739 d => return Err(err!(
740 "A style property is a str, found a {:?}.", d.kind();
741 Invalid, Input)),
742 };
743 s.push_str(&fmt!("{} = {}", prop, style_value(v)));
744 }
745 Ok(s)
746}
747
748/// One style value, as the report spells it.
749fn style_value(v: &Dat) -> String {
750 match v {
751 Dat::Str(s) => s.clone(),
752 Dat::U8(n) => fmt!("{}", n),
753 Dat::I8(n) => fmt!("{}", n),
754 d => fmt!("a {:?}", d.kind()),
755 }
756}
757
758// ┌───────────────────────────────────────────────────────────────────────────┐
759// │ DUMP │
760// └───────────────────────────────────────────────────────────────────────────┘
761
762/// Writes a document back out in its text form, so that it can be read, edited and recompiled.
763///
764/// The document is verified and decoded first, so what is written is a document rather than whatever
765/// the bytes happened to hold, and what comes out compiles back to the bytes that went in.
766fn dump(args: &Args) -> Outcome<()> {
767 let buf = res!(read_bytes(&args.input));
768 let read = match doc::read(&buf) {
769 Ok(read) => read,
770 Err(e) => return Err(err!(e,
771 "{} is not a document that reads.", args.input.display();
772 Invalid, Input)),
773 };
774 let src = res!(text::encode(read.tree()));
775 match &args.output {
776 Some(out) => {
777 res!(write_bytes(out, src.as_bytes()));
778 println!("Wrote {} ({} bytes).", out.display(), src.len());
779 },
780 None => print!("{}", src),
781 }
782 Ok(())
783}
784
785// ┌───────────────────────────────────────────────────────────────────────────┐
786// │ FILES, TIME, BYTES │
787// └───────────────────────────────────────────────────────────────────────────┘
788
789/// Reads a file whole, naming it if it will not open.
790fn read_bytes(path: &Path) -> Outcome<Vec<u8>> {
791 match fs::read(path) {
792 Ok(byts) => Ok(byts),
793 Err(e) => Err(err!(e,
794 "Could not read {}.", path.display();
795 IO, File)),
796 }
797}
798
799/// Reads a text file whole, naming it if it will not open.
800fn read_text(path: &Path) -> Outcome<String> {
801 match fs::read_to_string(path) {
802 Ok(s) => Ok(s),
803 Err(e) => Err(err!(e,
804 "Could not read {}.", path.display();
805 IO, File)),
806 }
807}
808
809/// Writes a file whole, making the directory it goes in.
810fn write_bytes(
811 path: &Path,
812 byts: &[u8],
813)
814 -> Outcome<()>
815{
816 if let Some(dir) = path.parent() {
817 if !dir.as_os_str().is_empty() {
818 match fs::create_dir_all(dir) {
819 Ok(()) => (),
820 Err(e) => return Err(err!(e,
821 "Could not make the directory {}.", dir.display();
822 IO, File)),
823 }
824 }
825 }
826 match fs::write(path, byts) {
827 Ok(()) => Ok(()),
828 Err(e) => Err(err!(e,
829 "Could not write {}.", path.display();
830 IO, File)),
831 }
832}
833
834/// The time now, in Unix milliseconds, which is what an envelope carries.
835fn now() -> Outcome<u64> {
836 let since = match SystemTime::now().duration_since(UNIX_EPOCH) {
837 Ok(since) => since,
838 Err(e) => return Err(err!(e,
839 "The clock reads a time before the Unix epoch, which an envelope cannot carry.";
840 Invalid, Input)),
841 };
842 Ok(try_into!(u64, since.as_millis()))
843}
844
845/// Renders bytes as hexadecimal, for a report that must name what it read.
846fn hex(byts: &[u8]) -> String {
847 let mut s = String::new();
848 for b in byts {
849 s.push_str(&fmt!("{:02x}", b));
850 }
851 s
852}