Files
regex-tools/scripts/pcre2-engine-pack.mjs

1019 lines
32 KiB
JavaScript

import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import {
access,
constants as fsConstants,
copyFile,
lstat,
mkdir,
mkdtemp,
readFile,
readdir,
realpath,
rename,
rm,
stat,
writeFile,
} from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
export const PCRE2_LOCK = Object.freeze({
bridgeAbi: 3,
maximumPatternBytes: 1_048_576,
maximumSubjectBytes: 16_777_216,
maximumReplacementBytes: 262_144,
maximumMatches: 10_000,
maximumCaptureRows: 100_000,
maximumCaptureGroups: 1_000,
maximumTraceEvents: 50_000,
maximumTraceBytes: 10_485_760,
maximumTraceMarkBytes: 1_024,
sourceDateEpoch: 1_760_997_684,
source: Object.freeze({
repository: "https://github.com/PCRE2Project/pcre2.git",
tag: "pcre2-10.47",
tagObject: "cd007b4466798f66d479d1442a407099e7c40050",
commit: "f454e231fe5006dd7ff8f4693fd2b8eb94333429",
tree: "81a83a3552bd68d0ea7b7004f8bb6e7892f583ba",
version: "10.47 2025-10-21",
license: "BSD-3-Clause WITH PCRE2-exception",
criticalFiles: Object.freeze({
"CMakeLists.txt":
"74f65935e2b1120e7d7aeb4be81755e0f7a875732467ae64a9cc6ee7174fa5ba",
"LICENCE.md":
"197d8a73ffee0d6b09adba2f9c677b5f5aede24edf89258a68e48248d010d811",
"src/pcre2.h.generic":
"058462dc030e845ee627e19ac36347f561f5cc44d89eaf1c0010f2b363ab68d5",
}),
}),
emscripten: Object.freeze({
version: "6.0.4",
compilerRevision: "fe5be6afdff43ad58860d821fcc8572a23f92d19",
repositoryCommit: "224ec5f9f2f72f09f9ce0e26d66bae7dbd8b692f",
emccSha256:
"498eba9b6aacffca1c358ba340fc7b37015d7867cb211c7f2d45784ee9310f03",
emcmakeSha256:
"18c8397cf60835533e0db88d1172e04fa0b753dea68e978149d923a9e6378af3",
}),
cmakeVersion: "4.3.4",
ninjaVersion: "1.13.2",
});
const PACK_FILES = Object.freeze([
"LICENSE.txt",
"SHA256SUMS",
"engine-metadata.json",
"pcre2.mjs",
"pcre2.wasm",
]);
const CHECKSUM_FILES = Object.freeze([
"LICENSE.txt",
"engine-metadata.json",
"pcre2.mjs",
"pcre2.wasm",
]);
const BRIDGE_SOURCE_FILES = Object.freeze([
"engines/pcre2/CMakeLists.txt",
"engines/pcre2/pcre2_bridge.c",
"engines/pcre2/pcre2_bridge.h",
]);
const EXPORTED_FUNCTIONS = Object.freeze([
"_free",
"_malloc",
"_regex_pcre2_bridge_abi_version",
"_regex_pcre2_compile_probe",
"_regex_pcre2_config_flags",
"_regex_pcre2_error_message",
"_regex_pcre2_execute",
"_regex_pcre2_self_test",
"_regex_pcre2_substitute",
"_regex_pcre2_trace",
"_regex_pcre2_version",
]);
function sha256(data) {
return createHash("sha256").update(data).digest("hex");
}
async function sha256File(file) {
return sha256(await readFile(file));
}
function commandLabel(command, arguments_) {
return [command, ...arguments_]
.map((part) =>
/^[A-Za-z0-9_./:=,+-]+$/u.test(part) ? part : JSON.stringify(part),
)
.join(" ");
}
async function run(command, arguments_, options = {}) {
const child = spawn(command, arguments_, {
cwd: options.cwd,
env: options.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stdout = [];
const stderr = [];
child.stdout.on("data", (chunk) => stdout.push(chunk));
child.stderr.on("data", (chunk) => stderr.push(chunk));
const result = await new Promise((resolve, reject) => {
child.once("error", reject);
child.once("close", (code, signal) => resolve({ code, signal }));
});
const output = Buffer.concat(stdout).toString("utf8");
const errorOutput = Buffer.concat(stderr).toString("utf8");
if (result.code !== 0) {
const detail = [output, errorOutput].filter(Boolean).join("\n").trim();
throw new Error(
`${commandLabel(command, arguments_)} failed with ${
result.signal ? `signal ${result.signal}` : `exit code ${result.code}`
}${detail ? `:\n${detail}` : "."}`,
);
}
return { stdout: output, stderr: errorOutput };
}
async function assertRealDirectory(candidate, label) {
const details = await lstat(candidate).catch(() => null);
if (!details?.isDirectory() || details.isSymbolicLink()) {
throw new Error(`${label} must be a real directory: ${candidate}`);
}
return realpath(candidate);
}
async function assertRegularFile(candidate, label) {
const details = await lstat(candidate).catch(() => null);
if (!details?.isFile() || details.isSymbolicLink()) {
throw new Error(
`${label} must be a regular, non-symlink file: ${candidate}`,
);
}
return realpath(candidate);
}
async function assertHash(file, expected, label) {
const actual = await sha256File(file);
if (actual !== expected) {
throw new Error(
`${label} SHA-256 mismatch: expected ${expected}, got ${actual}.`,
);
}
}
async function resolveExecutable(name, environment = process.env) {
const searchPath = environment.PATH ?? "";
for (const directory of searchPath.split(path.delimiter)) {
if (!directory) continue;
const candidate = path.join(directory, name);
try {
await access(candidate, fsConstants.X_OK);
const details = await stat(candidate);
if (details.isFile()) return realpath(candidate);
} catch {
// Continue through PATH.
}
}
throw new Error(`${name} is required on PATH for the explicit PCRE2 build.`);
}
function singleVersionLine(output) {
return output.trim().split(/\r?\n/u)[0] ?? "";
}
async function verifyPcre2Source(sourceDirectory) {
const source = await assertRealDirectory(sourceDirectory, "PCRE2 source");
const git = await resolveExecutable("git");
const status = await run(
git,
["status", "--porcelain=v1", "--untracked-files=all"],
{ cwd: source },
);
if (status.stdout !== "") {
throw new Error("The PCRE2 source checkout must be completely clean.");
}
const checks = [
[["rev-parse", "HEAD"], PCRE2_LOCK.source.commit, "PCRE2 commit"],
[["rev-parse", "HEAD^{tree}"], PCRE2_LOCK.source.tree, "PCRE2 source tree"],
[
["rev-parse", `refs/tags/${PCRE2_LOCK.source.tag}^{tag}`],
PCRE2_LOCK.source.tagObject,
"PCRE2 signed tag object",
],
[
["rev-parse", `refs/tags/${PCRE2_LOCK.source.tag}^{commit}`],
PCRE2_LOCK.source.commit,
"PCRE2 tag target",
],
];
for (const [arguments_, expected, label] of checks) {
const { stdout } = await run(git, arguments_, { cwd: source });
const actual = stdout.trim();
if (actual !== expected) {
throw new Error(
`${label} mismatch: expected ${expected}, got ${actual}.`,
);
}
}
for (const [relativePath, expected] of Object.entries(
PCRE2_LOCK.source.criticalFiles,
)) {
await assertHash(
path.join(source, relativePath),
expected,
`PCRE2 ${relativePath}`,
);
}
return source;
}
async function verifyEmscripten(emccFile) {
const emcc = await assertRegularFile(emccFile, "emcc");
const emcmake = await assertRegularFile(
path.join(path.dirname(emcc), "emcmake"),
"emcmake",
);
await assertHash(emcc, PCRE2_LOCK.emscripten.emccSha256, "emcc");
await assertHash(emcmake, PCRE2_LOCK.emscripten.emcmakeSha256, "emcmake");
const version = await run(emcc, ["--version"]);
const expectedVersion =
`emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) ` +
`${PCRE2_LOCK.emscripten.version} (${PCRE2_LOCK.emscripten.compilerRevision})`;
if (singleVersionLine(version.stdout) !== expectedVersion) {
throw new Error(
`Emscripten identity mismatch: expected ${expectedVersion}, got ${singleVersionLine(
version.stdout,
)}.`,
);
}
const git = await resolveExecutable("git");
const repository = (
await run(git, ["rev-parse", "--show-toplevel"], {
cwd: path.dirname(emcc),
})
).stdout.trim();
const commit = (
await run(git, ["rev-parse", "HEAD"], { cwd: repository })
).stdout.trim();
if (commit !== PCRE2_LOCK.emscripten.repositoryCommit) {
throw new Error(
`Emscripten repository mismatch: expected ${PCRE2_LOCK.emscripten.repositoryCommit}, got ${commit}.`,
);
}
const tagTarget = (
await run(
git,
["rev-parse", `refs/tags/${PCRE2_LOCK.emscripten.version}^{commit}`],
{
cwd: repository,
},
)
).stdout.trim();
if (tagTarget !== PCRE2_LOCK.emscripten.repositoryCommit) {
throw new Error(
"The Emscripten 6.0.4 tag does not resolve to the pinned commit.",
);
}
const status = await run(
git,
["status", "--porcelain=v1", "--untracked-files=all"],
{ cwd: repository },
);
if (status.stdout !== "") {
throw new Error("The Emscripten checkout must be completely clean.");
}
return { emcc, emcmake };
}
async function verifyNativeBuildTools() {
const cmake = await resolveExecutable("cmake");
const ninja = await resolveExecutable("ninja");
const cmakeOutput = await run(cmake, ["--version"]);
const ninjaOutput = await run(ninja, ["--version"]);
if (
singleVersionLine(cmakeOutput.stdout) !==
`cmake version ${PCRE2_LOCK.cmakeVersion}`
) {
throw new Error(
`The PCRE2 pack requires CMake ${PCRE2_LOCK.cmakeVersion}.`,
);
}
if (ninjaOutput.stdout.trim() !== PCRE2_LOCK.ninjaVersion) {
throw new Error(
`The PCRE2 pack requires Ninja ${PCRE2_LOCK.ninjaVersion}.`,
);
}
return { cmake, ninja };
}
function parseValue(argv, index, option) {
const value = argv[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`${option} requires a path.`);
}
return value;
}
export function parsePcre2BuildArguments(argv) {
if (argv.length === 0) return { mode: "ecmascript" };
if (argv.length === 1 && argv[0] === "--help") return { mode: "help" };
let pcre2 = false;
let sourceDirectory;
let emccFile;
let force = false;
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === "--pcre2") {
if (pcre2) throw new Error("--pcre2 may be supplied only once.");
pcre2 = true;
} else if (argument === "--source-dir") {
if (sourceDirectory)
throw new Error("--source-dir may be supplied only once.");
sourceDirectory = parseValue(argv, index, argument);
index += 1;
} else if (argument === "--emcc") {
if (emccFile) throw new Error("--emcc may be supplied only once.");
emccFile = parseValue(argv, index, argument);
index += 1;
} else if (argument === "--force") {
if (force) throw new Error("--force may be supplied only once.");
force = true;
} else {
throw new Error(`Unknown engine-build argument: ${argument}`);
}
}
if (!pcre2) {
throw new Error(
"External engine options require the explicit --pcre2 gate.",
);
}
if (!sourceDirectory || !emccFile) {
throw new Error(
"The offline PCRE2 build requires both --source-dir and --emcc; it never downloads them.",
);
}
return { mode: "pcre2", sourceDirectory, emccFile, force };
}
async function bridgeSourceFiles(root) {
return Promise.all(
BRIDGE_SOURCE_FILES.map(async (relativePath) => {
const contents = await readFile(path.join(root, relativePath));
return {
path: relativePath,
sha256: sha256(contents),
bytes: contents.byteLength,
};
}),
);
}
async function describedFile(directory, name) {
const contents = await readFile(path.join(directory, name));
return { path: name, sha256: sha256(contents), bytes: contents.byteLength };
}
async function expectedMetadata(root, packDirectory) {
const files = await Promise.all(
["LICENSE.txt", "pcre2.mjs", "pcre2.wasm"].map((name) =>
describedFile(packDirectory, name),
),
);
return {
schemaVersion: 1,
engine: "pcre2",
engineVersion: PCRE2_LOCK.source.version,
status: "production-runtime",
bridge: {
abiVersion: PCRE2_LOCK.bridgeAbi,
maximumPatternBytes: PCRE2_LOCK.maximumPatternBytes,
maximumSubjectBytes: PCRE2_LOCK.maximumSubjectBytes,
maximumReplacementBytes: PCRE2_LOCK.maximumReplacementBytes,
maximumMatches: PCRE2_LOCK.maximumMatches,
maximumCaptureRows: PCRE2_LOCK.maximumCaptureRows,
maximumCaptureGroups: PCRE2_LOCK.maximumCaptureGroups,
maximumTraceEvents: PCRE2_LOCK.maximumTraceEvents,
maximumTraceBytes: PCRE2_LOCK.maximumTraceBytes,
maximumTraceMarkBytes: PCRE2_LOCK.maximumTraceMarkBytes,
sourceFiles: await bridgeSourceFiles(root),
exports: [...EXPORTED_FUNCTIONS],
},
source: {
repository: PCRE2_LOCK.source.repository,
tag: PCRE2_LOCK.source.tag,
tagObjectSha1: PCRE2_LOCK.source.tagObject,
commitSha1: PCRE2_LOCK.source.commit,
treeSha1: PCRE2_LOCK.source.tree,
license: PCRE2_LOCK.source.license,
},
toolchain: {
emscriptenVersion: PCRE2_LOCK.emscripten.version,
emscriptenCompilerRevision: PCRE2_LOCK.emscripten.compilerRevision,
emsdkCommitSha1: PCRE2_LOCK.emscripten.repositoryCommit,
cmakeVersion: PCRE2_LOCK.cmakeVersion,
ninjaVersion: PCRE2_LOCK.ninjaVersion,
},
configuration: {
codeUnitWidth: 8,
unicode: true,
jit: false,
threads: false,
filesystem: false,
initialMemoryBytes: 16_777_216,
maximumMemoryBytes: 268_435_456,
},
sourceDateEpoch: PCRE2_LOCK.sourceDateEpoch,
files,
};
}
function deterministicJson(value) {
return `${JSON.stringify(value, null, 2)}\n`;
}
async function expectedChecksums(packDirectory) {
const entries = await Promise.all(
CHECKSUM_FILES.map(
async (name) =>
`${await sha256File(path.join(packDirectory, name))} ${name}`,
),
);
return `${entries.join("\n")}\n`;
}
function equalJson(left, right) {
return JSON.stringify(left) === JSON.stringify(right);
}
async function smokePcre2Pack(packDirectory) {
const wasm = await readFile(path.join(packDirectory, "pcre2.wasm"));
if (
wasm.byteLength < 8 ||
!wasm.subarray(0, 4).equals(Buffer.from([0x00, 0x61, 0x73, 0x6d])) ||
!WebAssembly.validate(wasm)
) {
throw new Error("pcre2.wasm is not a valid WebAssembly module.");
}
const moduleUrl = `${pathToFileURL(path.join(packDirectory, "pcre2.mjs")).href}?sha256=${sha256(
await readFile(path.join(packDirectory, "pcre2.mjs")),
)}`;
const imported = await import(moduleUrl);
if (typeof imported.default !== "function") {
throw new Error("pcre2.mjs does not export its Emscripten factory.");
}
const diagnostics = [];
const module = await imported.default({
locateFile(name) {
return path.join(packDirectory, name);
},
print() {},
printErr(line) {
diagnostics.push(String(line));
},
});
if (module._regex_pcre2_bridge_abi_version() !== PCRE2_LOCK.bridgeAbi) {
throw new Error("The WebAssembly bridge ABI version is incorrect.");
}
if (module._regex_pcre2_config_flags() !== 1) {
throw new Error(
"The WebAssembly pack must have Unicode enabled and JIT disabled.",
);
}
if (
module.UTF8ToString(module._regex_pcre2_version()) !==
PCRE2_LOCK.source.version
) {
throw new Error("The WebAssembly PCRE2 version is incorrect.");
}
if (module._regex_pcre2_self_test() !== 0) {
throw new Error(
`The WebAssembly bridge self-test failed: ${diagnostics.join("\n")}`,
);
}
const pattern = new TextEncoder().encode("(?<word>\\p{L}+)-(?<number>\\d+)");
const patternPointer = module._malloc(pattern.byteLength);
const resultPointer = module._malloc(20);
if (!patternPointer || !resultPointer) {
if (patternPointer) module._free(patternPointer);
if (resultPointer) module._free(resultPointer);
throw new Error("The WebAssembly smoke test could not allocate memory.");
}
try {
module.HEAPU8.set(pattern, patternPointer);
module.HEAPU8.fill(0, resultPointer, resultPointer + 20);
const status = module._regex_pcre2_compile_probe(
patternPointer,
pattern.byteLength,
0x20 | 0x40,
resultPointer,
);
const result = new DataView(module.HEAPU8.buffer, resultPointer, 20);
if (
status !== 0 ||
result.getInt32(0, true) !== 0 ||
result.getUint32(8, true) !== 2 ||
result.getUint32(12, true) !== 2
) {
throw new Error("The WebAssembly compile-probe result is incorrect.");
}
module.HEAPU8.fill(0, resultPointer, resultPointer + 20);
const unsupportedStatus = module._regex_pcre2_compile_probe(
patternPointer,
pattern.byteLength,
0x80000000,
resultPointer,
);
if (unsupportedStatus !== -10002 || result.getInt32(0, true) !== -10002) {
throw new Error("The WebAssembly bridge accepted an unknown option bit.");
}
module.HEAPU8[patternPointer] = 0x28;
module.HEAPU8.fill(0, resultPointer, resultPointer + 20);
const invalidStatus = module._regex_pcre2_compile_probe(
patternPointer,
1,
0,
resultPointer,
);
if (
invalidStatus <= 0 ||
result.getInt32(0, true) !== invalidStatus ||
result.getUint32(4, true) > 1
) {
throw new Error(
"The WebAssembly bridge did not preserve the compile error.",
);
}
if (
module._regex_pcre2_compile_probe(0, 1, 0, resultPointer) !== -10001 ||
module._regex_pcre2_compile_probe(
patternPointer,
PCRE2_LOCK.maximumPatternBytes + 1,
0,
resultPointer,
) !== -10003
) {
throw new Error("The WebAssembly bridge input limits are not enforced.");
}
} finally {
module._free(resultPointer);
module._free(patternPointer);
}
const runPattern = new TextEncoder().encode("(?<emoji>😀)|(?<word>\\p{L}+)");
const runSubject = new TextEncoder().encode("x😀 Grüße");
const runPointers = [];
const allocate = (bytes) => {
const pointer = module._malloc(Math.max(1, bytes));
if (!pointer) throw new Error("The PCRE2 runtime smoke allocation failed.");
runPointers.push(pointer);
return pointer;
};
try {
const runPatternPointer = allocate(runPattern.byteLength);
const runSubjectPointer = allocate(runSubject.byteLength);
const limitsPointer = allocate(20);
const recordsPointer = allocate(20 * 16);
const namesPointer = allocate(10 * 12);
const nameBytesPointer = allocate(256);
const runResultPointer = allocate(60);
module.HEAPU8.set(runPattern, runPatternPointer);
module.HEAPU8.set(runSubject, runSubjectPointer);
let view = new DataView(module.HEAPU8.buffer);
view.setUint32(limitsPointer, 10, true);
view.setUint32(limitsPointer + 4, 100, true);
view.setUint32(limitsPointer + 8, 1_000_000, true);
view.setUint32(limitsPointer + 12, 1_000, true);
view.setUint32(limitsPointer + 16, 32_768, true);
module.HEAPU8.fill(0, runResultPointer, runResultPointer + 60);
const runStatus = module._regex_pcre2_execute(
runPatternPointer,
runPattern.byteLength,
runSubjectPointer,
runSubject.byteLength,
0x20 | 0x40 | 0x100,
limitsPointer,
recordsPointer,
20,
namesPointer,
10,
nameBytesPointer,
256,
runResultPointer,
);
view = new DataView(module.HEAPU8.buffer);
if (
runStatus !== 0 ||
view.getInt32(runResultPointer, true) !== 0 ||
view.getUint32(runResultPointer + 12, true) !== 2 ||
view.getUint32(runResultPointer + 20, true) !== 3 ||
view.getUint32(runResultPointer + 24, true) !== 9 ||
view.getUint32(recordsPointer + 3 * 16 + 8, true) !== 1 ||
view.getUint32(recordsPointer + 3 * 16 + 12, true) !== 5
) {
throw new Error("The WebAssembly bounded-match smoke result is wrong.");
}
const replacementPattern = new TextEncoder().encode("(?<word>\\p{L}+)");
const replacementSubject = new TextEncoder().encode("Grüße 42");
const replacement = new TextEncoder().encode("${word}!");
const replacementPatternPointer = allocate(replacementPattern.byteLength);
const replacementSubjectPointer = allocate(replacementSubject.byteLength);
const replacementPointer = allocate(replacement.byteLength);
const outputPointer = allocate(65);
module.HEAPU8.set(replacementPattern, replacementPatternPointer);
module.HEAPU8.set(replacementSubject, replacementSubjectPointer);
module.HEAPU8.set(replacement, replacementPointer);
module.HEAPU8.fill(0, runResultPointer, runResultPointer + 60);
const substituteStatus = module._regex_pcre2_substitute(
replacementPatternPointer,
replacementPattern.byteLength,
replacementSubjectPointer,
replacementSubject.byteLength,
replacementPointer,
replacement.byteLength,
0x20 | 0x40 | 0x100,
limitsPointer,
recordsPointer,
20,
namesPointer,
10,
nameBytesPointer,
256,
outputPointer,
64,
runResultPointer,
);
view = new DataView(module.HEAPU8.buffer);
const outputLength = view.getUint32(runResultPointer + 48, true);
const output = new TextDecoder().decode(
module.HEAPU8.slice(outputPointer, outputPointer + outputLength),
);
if (
substituteStatus !== 0 ||
output !== "Grüße! 42" ||
view.getUint32(runResultPointer + 52, true) !== 1 ||
view.getUint32(runResultPointer + 56, true) !== 0
) {
throw new Error(
"The WebAssembly bounded-substitution smoke result is wrong.",
);
}
module.HEAPU8.fill(0, runResultPointer, runResultPointer + 60);
const missingOutputStatus = module._regex_pcre2_substitute(
replacementPatternPointer,
replacementPattern.byteLength,
replacementSubjectPointer,
replacementSubject.byteLength,
replacementPointer,
replacement.byteLength,
0x20 | 0x40,
limitsPointer,
recordsPointer,
20,
namesPointer,
10,
nameBytesPointer,
256,
0,
0,
runResultPointer,
);
if (
missingOutputStatus !== -10001 ||
view.getInt32(runResultPointer, true) !== -10001
) {
throw new Error(
"The WebAssembly substitution bridge accepted a missing NUL byte.",
);
}
const traceEventsPointer = allocate(64 * 32);
const traceMarksPointer = allocate(256);
const traceResultPointer = allocate(52);
view.setUint32(limitsPointer, 64, true);
view.setUint32(limitsPointer + 4, 4_096, true);
module.HEAPU8.fill(0, traceResultPointer, traceResultPointer + 52);
const traceStatus = module._regex_pcre2_trace(
runPatternPointer,
runPattern.byteLength,
runSubjectPointer,
runSubject.byteLength,
0x20 | 0x40,
limitsPointer,
traceEventsPointer,
64,
traceMarksPointer,
256,
traceResultPointer,
);
view = new DataView(module.HEAPU8.buffer);
const traceEventCount = view.getUint32(traceResultPointer + 20, true);
if (
traceStatus !== 0 ||
view.getInt32(traceResultPointer, true) !== 0 ||
view.getInt32(traceResultPointer + 12, true) <= 0 ||
traceEventCount === 0 ||
view.getUint32(traceResultPointer + 24, true) !== traceEventCount ||
view.getUint32(traceResultPointer + 36, true) !== 0 ||
view.getUint32(traceResultPointer + 44, true) !== traceEventCount - 1 ||
view.getUint32(traceResultPointer + 48, true) !== 1
) {
throw new Error("The WebAssembly automatic-callout trace is wrong.");
}
view.setUint32(limitsPointer, 1, true);
module.HEAPU8.fill(0, traceResultPointer, traceResultPointer + 52);
const boundedTraceStatus = module._regex_pcre2_trace(
runPatternPointer,
runPattern.byteLength,
runSubjectPointer,
runSubject.byteLength,
0x20 | 0x40,
limitsPointer,
traceEventsPointer,
64,
traceMarksPointer,
256,
traceResultPointer,
);
if (
boundedTraceStatus !== 0 ||
view.getInt32(traceResultPointer, true) !== 0 ||
view.getInt32(traceResultPointer + 12, true) !== -37 ||
view.getUint32(traceResultPointer + 20, true) !== 1 ||
view.getUint32(traceResultPointer + 24, true) !== 2 ||
view.getUint32(traceResultPointer + 36, true) !== 1 ||
view.getUint32(traceResultPointer + 44, true) !== 0
) {
throw new Error("The WebAssembly trace cap is not authoritative.");
}
} finally {
for (const pointer of runPointers.reverse()) module._free(pointer);
}
}
export async function verifyPcre2Pack(packDirectory, root) {
const repositoryRoot = await assertRealDirectory(
root,
"Regex Tools repository",
);
const pack = await assertRealDirectory(packDirectory, "PCRE2 pack");
const entries = (await readdir(pack, { withFileTypes: true })).sort(
(left, right) =>
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
);
if (
entries.length !== PACK_FILES.length ||
entries.some(
(entry, index) =>
entry.name !== PACK_FILES[index] ||
!entry.isFile() ||
entry.isSymbolicLink(),
)
) {
throw new Error(
`The staged PCRE2 pack must contain only: ${PACK_FILES.join(", ")}.`,
);
}
await assertHash(
path.join(pack, "LICENSE.txt"),
PCRE2_LOCK.source.criticalFiles["LICENCE.md"],
"staged PCRE2 licence",
);
const moduleText = await readFile(path.join(pack, "pcre2.mjs"), "utf8");
if (
moduleText.includes("sourceMappingURL") ||
moduleText.includes("/mnt/") ||
moduleText.includes("/home/")
) {
throw new Error("pcre2.mjs leaks a build path or source-map reference.");
}
let metadata;
try {
metadata = JSON.parse(
await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
);
} catch (error) {
throw new Error("engine-metadata.json is not valid JSON.", {
cause: error,
});
}
const expected = await expectedMetadata(repositoryRoot, pack);
if (!equalJson(metadata, expected)) {
throw new Error(
"The staged PCRE2 metadata does not match the pinned build contract.",
);
}
const checksums = await readFile(path.join(pack, "SHA256SUMS"), "utf8");
if (checksums !== (await expectedChecksums(pack))) {
throw new Error("The staged PCRE2 SHA256SUMS file is incorrect.");
}
await smokePcre2Pack(pack);
return metadata;
}
export async function installPcre2Pack(packDirectory, root) {
const repositoryRoot = await assertRealDirectory(
root,
"Regex Tools repository",
);
const sourcePack = await assertRealDirectory(
packDirectory,
"verified PCRE2 pack",
);
const metadata = await verifyPcre2Pack(sourcePack, repositoryRoot);
const engineRoot = await assertRealDirectory(
path.join(repositoryRoot, "public", "engines"),
"public engine directory",
);
const target = path.join(engineRoot, "pcre2");
const stage = await mkdtemp(path.join(engineRoot, ".pcre2-install-"));
const targetDetails = await lstat(target).catch(() => null);
if (
targetDetails &&
(!targetDetails.isDirectory() || targetDetails.isSymbolicLink())
) {
await rm(stage, { recursive: true, force: true });
throw new Error("public/engines/pcre2 must be a real directory.");
}
try {
for (const name of PACK_FILES) {
await copyFile(
path.join(sourcePack, name),
path.join(stage, name),
fsConstants.COPYFILE_EXCL,
);
}
await verifyPcre2Pack(stage, repositoryRoot);
if (targetDetails) {
const backup = path.join(engineRoot, `.pcre2-replaced-${process.pid}`);
await rename(target, backup);
try {
await rename(stage, target);
} catch (error) {
await rename(backup, target);
throw error;
}
await rm(backup, { recursive: true });
} else {
await rename(stage, target);
}
return { output: target, metadata };
} finally {
await rm(stage, { recursive: true, force: true });
}
}
function cleanBuildEnvironment() {
const environment = { ...process.env };
for (const variable of [
"CFLAGS",
"CPPFLAGS",
"CXXFLAGS",
"DESTDIR",
"EMCC_CFLAGS",
"EMMAKEN_CFLAGS",
"LDFLAGS",
]) {
delete environment[variable];
}
environment.LANG = "C";
environment.LC_ALL = "C";
environment.SOURCE_DATE_EPOCH = String(PCRE2_LOCK.sourceDateEpoch);
environment.TZ = "UTC";
environment.ZERO_AR_DATE = "1";
return environment;
}
async function ensurePrivateBuildRoot(root) {
const buildRoot = path.join(root, ".engine-build");
const existing = await lstat(buildRoot).catch(() => null);
if (existing && (!existing.isDirectory() || existing.isSymbolicLink())) {
throw new Error(".engine-build must be a real directory.");
}
await mkdir(buildRoot, { recursive: true });
return realpath(buildRoot);
}
function assertGeneratedChild(buildRoot, candidate) {
const relative = path.relative(buildRoot, candidate);
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
throw new Error(
`Refusing to remove a path outside .engine-build: ${candidate}`,
);
}
}
export async function buildPcre2Pack(options, root) {
const repositoryRoot = await assertRealDirectory(
root,
"Regex Tools repository",
);
const source = await verifyPcre2Source(options.sourceDirectory);
const { emcmake } = await verifyEmscripten(options.emccFile);
const { cmake, ninja } = await verifyNativeBuildTools();
const buildRoot = await ensurePrivateBuildRoot(repositoryRoot);
const output = path.join(buildRoot, "pcre2");
const outputDetails = await lstat(output).catch(() => null);
if (outputDetails && !options.force) {
throw new Error(
`${output} already exists; pass --force to replace that exact staged pack.`,
);
}
const work = await mkdtemp(path.join(buildRoot, ".pcre2-work-"));
const stage = await mkdtemp(path.join(buildRoot, ".pcre2-stage-"));
assertGeneratedChild(buildRoot, work);
assertGeneratedChild(buildRoot, stage);
const environment = cleanBuildEnvironment();
try {
await run(
emcmake,
[
cmake,
"-S",
path.join(repositoryRoot, "engines", "pcre2"),
"-B",
work,
"-G",
"Ninja",
"-DCMAKE_BUILD_TYPE=Release",
`-DCMAKE_MAKE_PROGRAM=${ninja}`,
`-DPCRE2_SOURCE_DIR=${source}`,
`-DREGEX_PCRE2_OUTPUT_DIR=${stage}`,
],
{ cwd: repositoryRoot, env: environment },
);
await run(
cmake,
["--build", work, "--target", "regex-pcre2-pack", "--parallel", "1"],
{
cwd: repositoryRoot,
env: environment,
},
);
const generatedEntries = (await readdir(stage)).sort();
if (
generatedEntries.length !== 2 ||
generatedEntries[0] !== "pcre2.mjs" ||
generatedEntries[1] !== "pcre2.wasm"
) {
throw new Error(
`Emscripten produced unexpected files: ${generatedEntries.join(", ") || "(none)"}.`,
);
}
await copyFile(
path.join(source, "LICENCE.md"),
path.join(stage, "LICENSE.txt"),
fsConstants.COPYFILE_EXCL,
);
const metadata = await expectedMetadata(repositoryRoot, stage);
await writeFile(
path.join(stage, "engine-metadata.json"),
deterministicJson(metadata),
{ encoding: "utf8", flag: "wx" },
);
await writeFile(
path.join(stage, "SHA256SUMS"),
await expectedChecksums(stage),
{ encoding: "utf8", flag: "wx" },
);
await verifyPcre2Pack(stage, repositoryRoot);
if (outputDetails) {
const backup = path.join(buildRoot, `.pcre2-replaced-${process.pid}`);
assertGeneratedChild(buildRoot, backup);
await rename(output, backup);
try {
await rename(stage, output);
} catch (error) {
await rename(backup, output);
throw error;
}
await rm(backup, { recursive: true });
} else {
await rename(stage, output);
}
return { output, metadata };
} finally {
await rm(work, { recursive: true, force: true });
await rm(stage, { recursive: true, force: true });
}
}