mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
createHash, createHmac, crypto.hash, pbkdf2, pbkdf2Sync, hkdf and hkdfSync accept every digest crypto.getHashes() lists in Node 24: SHA-512/224, SHA-512/256, SHA-256/192, SHA3-224 to SHA3-512, Keccak-224 to Keccak-512, SHAKE128, SHAKE256, KECCAK-KMAC-128, KECCAK-KMAC-256, RIPEMD-160, BLAKE2b-512, BLAKE2s-256, SM3 and MD5-SHA1. The new runtime unit scr_hash.c implements Keccak, RIPEMD-160, BLAKE2b, BLAKE2s and SM3, and is registered in both runtime pack matrices and the C-compile path. Names resolve as Node 24.22 resolves them: case-insensitively, with OpenSSL's provider aliases and object identifiers, ending at the first NUL, and with trailing spaces allowed except on the RSA signature aliases. "dss1" and "DSS1" are SHA-1, and createHash and crypto.hash accept "dss1" in any case. The compiler no longer refuses literal digest names outside SHA-1/SHA-2 and MD5; unknown names throw Node's errors at run time. SHAKE and KMAC digests use their default output lengths. createHmac rejects them with Node's OpenSSL error, pbkdf2 with "xof digests not allowed", and hkdf with "Deriving bits failed". pbkdf2 with a key length of 0 now fails with "Deriving bits failed" as in Node 24.22, and the callback forms pass these failures to the callback. PBKDF2 computes the keyed HMAC states once instead of on every iteration, and digest contexts are cleared with memset and a compiler barrier instead of a byte loop. The Keccak permutation, the RIPEMD-160 and BLAKE2 compression functions and the SM3 rounds are unrolled with constant indices.
99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { mkdir } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { beforeAll, expect, test } from "vitest";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const testDir = import.meta.dirname;
|
|
const bin = join(testDir, "build", "test_url");
|
|
|
|
// Compiled once with ASan + the RC audit (leak/double-free coverage over
|
|
// every bridge call, thrown TypeErrors included).
|
|
beforeAll(async () => {
|
|
await mkdir(join(testDir, "build"), { recursive: true });
|
|
await execFileAsync("clang", [
|
|
"-std=c11",
|
|
"-O1",
|
|
"-Wall",
|
|
"-Wextra",
|
|
"-fsanitize=address",
|
|
"-DSCR_RC_AUDIT",
|
|
...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []),
|
|
"-o",
|
|
bin,
|
|
join(testDir, "test_url.c"),
|
|
join(testDir, "../src/scr_url.c"),
|
|
join(testDir, "../src/scr_url_params.c"),
|
|
join(testDir, "../src/scr_path.c"),
|
|
join(testDir, "../src/scr_string.c"),
|
|
join(testDir, "../src/scr_number.c"),
|
|
join(testDir, "../src/scr_array.c"),
|
|
// Checked-value collection equality shares the native JSON value runtime.
|
|
join(testDir, "../src/scr_json.c"),
|
|
join(testDir, "../src/scr_map.c"),
|
|
join(testDir, "../src/scr_union.c"),
|
|
join(testDir, "../src/scr_bigint.c"),
|
|
join(testDir, "../src/scr_closure.c"),
|
|
join(testDir, "../src/scr_bytes.c"),
|
|
join(testDir, "../src/scr_shared.c"),
|
|
join(testDir, "../src/scr_lib.c"),
|
|
join(testDir, "../src/scr_hash.c"),
|
|
join(testDir, "../src/scr_console.c"),
|
|
join(testDir, "../src/scr_error.c"),
|
|
join(testDir, "../src/scr_exception.c"),
|
|
join(testDir, "../src/scr_object.c"),
|
|
join(testDir, "../src/scr_cycle.c"),
|
|
join(testDir, "../src/scr_alloc.c"),
|
|
...(process.platform === "linux" ? ["-lm"] : []),
|
|
]);
|
|
});
|
|
|
|
// Runs against the committed case file (generated from Node v24's own
|
|
// fileURLToPath/pathToFileURL with { windows: true } — the exact code
|
|
// Windows Node dispatches to — via gen-url-cases.mjs; see that file for
|
|
// the excluded documented divergences). Covers the win32 arm's drive
|
|
// letters, UNC hosts, \\?\UNC\ prefixes, encoded-separator TypeErrors,
|
|
// and the UNC-malformation TypeErrors, plus posix-arm legs pinning the
|
|
// shared encoding rework.
|
|
// One committed case bakes the GENERATING platform into its bytes: Node's
|
|
// non-localhost file-URL host TypeError names the host OS, the oracle was
|
|
// generated on darwin, and scr_url.c mirrors Node by naming the RUNTIME
|
|
// platform. On Linux the native message is byte-exact against what Linux
|
|
// Node itself says — pin exactly that one darwin→linux substitution and
|
|
// nothing else.
|
|
const LINUX_PLATFORM_MESSAGE_CASES: ReadonlyMap<string, string> = new Map([
|
|
[
|
|
// u2p-posix("file://host/a")
|
|
"u2p-posix(66696c653a2f2f686f73742f61)",
|
|
Buffer.from('ERR:TypeError: File URL host must be "localhost" or empty on linux').toString(
|
|
"hex",
|
|
),
|
|
],
|
|
]);
|
|
|
|
test("file-URL bridge win32 arm matches Node on committed oracle cases", async () => {
|
|
const r = await execFileAsync(bin, [join(testDir, "url-cases.txt")]).then(
|
|
(v) => ({ stderr: v.stderr }),
|
|
(e: Error & { stderr?: string }) => ({ stderr: e.stderr ?? "" }),
|
|
);
|
|
const stderr = r.stderr.trim();
|
|
if (process.platform !== "linux") {
|
|
expect(stderr).toMatch(/^(\d+)\/\1 cases passed$/);
|
|
return;
|
|
}
|
|
const lines = stderr.split("\n");
|
|
const mismatches = lines.filter((l) => l.startsWith("MISMATCH "));
|
|
for (const line of mismatches) {
|
|
const m = /^MISMATCH (.+) expected=[0-9a-f]* got=([0-9a-f]*)$/.exec(line);
|
|
expect(m, `unparseable mismatch line: ${line}`).not.toBeNull();
|
|
expect(LINUX_PLATFORM_MESSAGE_CASES.get(m![1]!), line).toBe(m![2]);
|
|
}
|
|
// The pass-count tail proves nothing beyond the printed (allowlisted)
|
|
// set failed (the harness caps mismatch printing at 20 lines).
|
|
const t = /^(\d+)\/(\d+) cases passed$/.exec(lines.at(-1) ?? "");
|
|
expect(t, `missing pass-count line: ${lines.at(-1)}`).not.toBeNull();
|
|
expect(Number(t![2]) - Number(t![1]), "failed-case count").toBe(mismatches.length);
|
|
expect(mismatches.length).toBeLessThanOrEqual(LINUX_PLATFORM_MESSAGE_CASES.size);
|
|
});
|