• Add File.sha256_hex / File.sha256_base64 (native streaming SHA-256)

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Tue Jul 21 19:57:46 2026
    open https://gitlab.synchro.net/main/sbbs/-/issues/1193

    ## Summary

    `File` exposes streaming, native hashes for MD5 and SHA-1 but nothing for SHA-256:

    | Property | Added | Notes |
    |---|---|---|
    | `File.md5_hex` / `File.md5_base64` | 3.11 | streams the file |
    | `File.sha1_hex` / `File.sha1_base64` | 3.19 | streams the file |
    | `File.sha256_hex` / `File.sha256_base64` | — | **missing** |

    The `md5_calc()` / `sha1_calc()` globals do not fill the gap, because they take a *string*: hashing a 600 MB disc image with one means holding the whole image in memory. There is no `sha256_calc()` either.

    SHA-256 is what upstream publishers actually distribute today — ScummVM ships a `.sha256` beside each archive, GitHub releases publish SHA-256 sums — so verifying a download against the publisher's own value currently requires hand-rolling the hash.

    ## Proposal

    Add `File.sha256_hex` and `File.sha256_base64` alongside the SHA-1 pair in `js_file.cpp`. The existing block already handles `FILE_PROP_MD5_HEX` / `FILE_PROP_MD5_B64` / `FILE_PROP_SHA1_HEX` / `FILE_PROP_SHA1_B64` through one open/read-loop/close path, so this is three more `case` labels plus two entries in the property table — the streaming behavior comes for free.

    A `sha256_calc(text[,hex])` global would round out the `md5_calc`/`sha1_calc` set, but it does **not** address the case above; if only one is added, the `File` properties are the useful one.

    Minor API note, worth considering only if the global is added: `md5_calc(text[,hex=false])` defaults to returning **raw digest bytes**. Nearly every caller wants hex, and raw bytes in a JS string invite avoidable byte-handling mistakes. A new `sha256_calc()` might reasonably default `hex=true` despite the asymmetry.

    ## Current workaround

    Ten `xtrn/` fetchers needed SHA-256 to verify downloaded game data against a pinned constant. They now share `exec/load/sha256.js`, which streams the file through cryptlib's `CryptContext(CryptContext.ALGO.SHA2)` in 64 KB chunks.

    That helper is written as a **polyfill**, so this issue resolving requires no call-site changes anywhere:

    ```js
    if (f.sha256_hex !== undefined)
    return String(f.sha256_hex).toLowerCase();
    // ...otherwise the CryptContext path
    ```

    Installs without the native property keep working; installs with it take the native path automatically.

    ## Notes gathered while writing that helper

    Two things that were misdocumented in the fetchers' comments, in case they are useful for the implementation or for JSDOCS:

    - `CryptContext.ALGO.SHA2` is **SHA-256** specifically — a fixed 256-bit digest, not a family selector. Confirmed against the NIST vectors for `"abc"` and `""`.
    - The idiomatic trailing `cc.encrypt("")` does **not** finalize the digest. Reading `.hashvalue` does: its getter calls `cryptEncrypt(p->ctx, p, 0)` before fetching the attribute. Hashing `"a"`,`"b"`,`"c"` with no final call yields the correct digest.

    Binary handling through `File.read()` in `"rb"` mode was verified correct against `sha256sum` for a file spanning every byte value 0x00-0xFF plus embedded NULs, CR/LF and `0x1A`, and digests are identical at 64 KB and 7-byte chunk sizes.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)