AtlatestRepositoryfolio

folio / tree / testf2b-revision-webcrypto.js

1// The WASM/browser half of the revision conformance pair.
2//
3// A folio client embedded in Slate runs in WASM. `sigil-crypto`'s `sha256` is
4// `define-native` over a vendored mbedTLS tree -- NATIVE ONLY -- so that client
5// cannot use it and must compute revisions with WebCrypto instead. This file is
6// that implementation, run under node, whose `crypto.subtle` is the same
7// WebCrypto surface the browser exposes.
8//
9// WHAT THIS IS AND IS NOT EVIDENCE FOR, STATED UP FRONT
10//
11// It IS a genuinely independent implementation of the contract: different
12// language, different SHA-256 code, different byte-handling primitives from
13// anything in the Sigil tree. A transcription error, a hex-case slip or an
14// encoding mistake on either side shows up here.
15//
16// It is NOT a browser. node's WebCrypto and a browser's are separate
17// implementations of one specification, so agreement here does not prove a
18// browser agrees -- it proves the CONTRACT is implementable outside the Sigil
19// tree and that folio's native answer matches an independent one. Running the
20// same vectors in a real browser is the remaining gap, and it is named in the
21// task note rather than papered over.
22//
23// TWO MODES, AND THE SECOND ONE IS THE POINT
24//
25// --bytes the CORRECT implementation: hash the raw file bytes.
26// --text the NAIVE implementation a browser client falls into by accident:
27// read the file as TEXT, then re-encode with TextEncoder.
28//
29// `--text` exists so the vector can be shown to DETECT a disagreement rather
30// than merely to have never seen one. It is not a strawman: reading a File with
31// `FileReader.readAsText`, or a fetch Response with `.text()`, is the obvious
32// thing to write, and it is silently lossy -- every ill-formed byte becomes
33// U+FFFD. A client built that way agrees with the server on ordinary notes and
34// disagrees on exactly the files a knowledge base accumulates by accident, which
35// is the worst possible failure distribution: it passes every test anyone
36// thinks to write, then loses one user's writes on the one file with a stray
37// byte in it.
39const fs = require("fs");
40const path = require("path");
42const mode = process.argv[2];
43const dir = process.argv[3];
45if (!["--bytes", "--text"].includes(mode) || !dir) {
46 console.error("usage: f2b-revision-webcrypto.js --bytes|--text <vector-dir>");
47 process.exit(2);
50async function revisionOf(file) {
51 const raw = fs.readFileSync(file);
52 let input;
53 if (mode === "--bytes") {
54 // The contract: SHA-256 over the RAW FILE BYTES, no normalisation, no
55 // re-encoding step.
56 input = new Uint8Array(raw);
57 } else {
58 // The naive text path. `toString("utf8")` replaces every ill-formed byte
59 // with U+FFFD exactly as a browser text read does, and TextEncoder then
60 // encodes those replacement characters -- so the bytes hashed are NOT the
61 // bytes on disk.
62 input = new TextEncoder().encode(raw.toString("utf8"));
63 }
64 const digest = await crypto.subtle.digest("SHA-256", input);
65 const hex = Array.from(new Uint8Array(digest))
66 .map((b) => b.toString(16).padStart(2, "0"))
67 .join("");
68 return "sha256:" + hex;
71(async () => {
72 // Sorted, so two runs of this harness emit lines in the same order and a diff
73 // between arms is a diff about revisions rather than about readdir order.
74 const files = fs.readdirSync(dir).filter((f) => f.endsWith(".sgd")).sort();
75 if (files.length === 0) {
76 console.error("f2b-revision-webcrypto.js: no .sgd vectors found in " + dir);
77 console.error("REFUSING to report success over an empty vector set: a run");
78 console.error("that hashed nothing agrees with everything.");
79 process.exit(3);
80 }
81 for (const f of files) {
82 console.log(f + "\t" + (await revisionOf(path.join(dir, f))));
83 }
84})().catch((e) => {
85 console.error("f2b-revision-webcrypto.js: " + e.stack);
86 process.exit(4);
87});