AtlatestRepositoryfolio
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` is4
// `define-native` over a vendored mbedTLS tree -- NATIVE ONLY -- so that client5
// cannot use it and must compute revisions with WebCrypto instead. This file is6
// that implementation, run under node, whose `crypto.subtle` is the same7
// WebCrypto surface the browser exposes.8
//9
// WHAT THIS IS AND IS NOT EVIDENCE FOR, STATED UP FRONT10
//11
// It IS a genuinely independent implementation of the contract: different12
// language, different SHA-256 code, different byte-handling primitives from13
// anything in the Sigil tree. A transcription error, a hex-case slip or an14
// encoding mistake on either side shows up here.15
//16
// It is NOT a browser. node's WebCrypto and a browser's are separate17
// implementations of one specification, so agreement here does not prove a18
// browser agrees -- it proves the CONTRACT is implementable outside the Sigil19
// tree and that folio's native answer matches an independent one. Running the20
// same vectors in a real browser is the remaining gap, and it is named in the21
// task note rather than papered over.22
//23
// TWO MODES, AND THE SECOND ONE IS THE POINT24
//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 rather30
// than merely to have never seen one. It is not a strawman: reading a File with31
// `FileReader.readAsText`, or a fetch Response with `.text()`, is the obvious32
// thing to write, and it is silently lossy -- every ill-formed byte becomes33
// U+FFFD. A client built that way agrees with the server on ordinary notes and34
// disagrees on exactly the files a knowledge base accumulates by accident, which35
// is the worst possible failure distribution: it passes every test anyone36
// thinks to write, then loses one user's writes on the one file with a stray37
// byte in it.39
const fs = require("fs");40
const path = require("path");42
const mode = process.argv[2];43
const dir = process.argv[3];45
if (!["--bytes", "--text"].includes(mode) || !dir) {46
console.error("usage: f2b-revision-webcrypto.js --bytes|--text <vector-dir>");47
process.exit(2);48
}50
async 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, no55
// re-encoding step.56
input = new Uint8Array(raw);57
} else {58
// The naive text path. `toString("utf8")` replaces every ill-formed byte59
// with U+FFFD exactly as a browser text read does, and TextEncoder then60
// encodes those replacement characters -- so the bytes hashed are NOT the61
// 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;69
}71
(async () => {72
// Sorted, so two runs of this harness emit lines in the same order and a diff73
// 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
});