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 | |
| 15 | use 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 | |
| 40 | use oxedyne_fe2o3_core::prelude::*; |
| 41 | use oxedyne_fe2o3_jdat::prelude::*; |
| 42 | |
| 43 | use 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. |
| 58 | const 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. |
| 69 | const STACK_BYTES: usize = 8 * 1024 * 1024; |
| 70 | |
| 71 | /// What the tool was asked to do. |
| 72 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 73 | enum 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 | |
| 86 | impl 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)] |
| 106 | struct 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 | |
| 130 | fn 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. |
| 160 | fn 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. |
| 177 | fn usage() { |
| 178 | println!("\ |
| 179 | sbj -- the authoring toolchain for SBJ, the oxeweb document format. |
| 180 | |
| 181 | Usage: |
| 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 | |
| 188 | Options: |
| 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 | |
| 201 | An import maps the prose and drops what the v0 vocabulary has no room for: a thematic break goes, |
| 202 | an image becomes its alt text, since v0 addresses an image by content hash and Markdown gives a |
| 203 | path, and an inline code span becomes its characters, since code is flow content and cannot sit in |
| 204 | a paragraph. |
| 205 | |
| 206 | A 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. |
| 221 | fn 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. |
| 311 | fn 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. |
| 328 | fn 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. |
| 354 | fn 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. |
| 366 | fn 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. |
| 388 | fn 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)] |
| 423 | enum 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 | |
| 430 | impl 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. |
| 463 | fn 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. |
| 476 | fn 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. |
| 521 | fn 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. |
| 550 | fn 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. |
| 566 | fn 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. |
| 578 | fn 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. |
| 590 | fn 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. |
| 606 | fn 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. |
| 647 | fn 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. |
| 670 | fn 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. |
| 696 | fn 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. |
| 725 | fn 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. |
| 749 | fn 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. |
| 766 | fn 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. |
| 790 | fn 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. |
| 800 | fn 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. |
| 810 | fn 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. |
| 835 | fn 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. |
| 846 | fn 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 | } |