807 lines
25 KiB
JavaScript
807 lines
25 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";
|
|
|
|
export const RUST_LOCK = Object.freeze({
|
|
bridgeAbi: 1,
|
|
engineVersion: "1.13.1",
|
|
rustcVersion: "1.97.1",
|
|
rustcIdentity: "rustc 1.97.1 (8bab26f4f 2026-07-14)",
|
|
rustcCommit: "8bab26f4f68e0e26f0bb7960be334d5b520ea452",
|
|
cargoIdentity: "cargo 1.97.1 (c980f4866 2026-06-30)",
|
|
llvmVersion: "22.1.6",
|
|
wasmBindgenVersion: "0.2.126",
|
|
wasmBindgenCrateChecksum:
|
|
"4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4",
|
|
wasmBindgenCliChecksum:
|
|
"8e2ebf6eef87c34e662347f1dddfbea541304bb7294711ed2c2acd8746cc5b50",
|
|
sourceDateEpoch: 1_784_146_753,
|
|
source: Object.freeze({
|
|
repository: "https://github.com/rust-lang/regex",
|
|
tag: "1.13.1",
|
|
tagObject: "7c28ee4dc4aef53571c96757f4825c51ac0c3d44",
|
|
commit: "2b527599eb9eea0dcc288c704584f242f26a5c61",
|
|
tree: "25bea9f6f55a4cf528afefaa00f5335c595c182c",
|
|
crateChecksum:
|
|
"f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d",
|
|
license: "MIT OR Apache-2.0",
|
|
}),
|
|
distribution: Object.freeze({
|
|
manifest: "https://static.rust-lang.org/dist/channel-rust-1.97.1.toml",
|
|
manifestSha256:
|
|
"03569b1886ceb5c05276b50c8431ab111de944cd6140fe1fa7d821dd8e0f29cf",
|
|
rustcArchiveSha256:
|
|
"1c441e430c1cca49dff54a8d59c41038bf6f79f7b8756596cb2f36511a015eba",
|
|
wasmStdArchiveSha256:
|
|
"13902d5573eeea50701d75acc774b6df2dfb4942ec88cdbe40bb07e448c307ea",
|
|
licenseHashes: Object.freeze({
|
|
apache:
|
|
"074e6e32c86a4c0ef8b3ed25b721ca23aca83df277cd88106ef7177c354615ff",
|
|
mit: "b85dcd3e453d05982552c52b5fc9e0bdd6d23c6f8e844b984a88af32570b0cc0",
|
|
unicode:
|
|
"f5062c9a188d81dfe66b56db4182dcf9e4b17c0d9b0d311a8e20b3a1b075c443",
|
|
stdlibCopyright:
|
|
"0a65bb747c49c7bb816cbc7188319bd6e4e8d08091c1190b8a3c0971c47968ed",
|
|
}),
|
|
}),
|
|
limits: Object.freeze({
|
|
maximumPatternBytes: 262_144,
|
|
maximumSubjectBytes: 16_777_216,
|
|
maximumReplacementBytes: 262_144,
|
|
maximumMatches: 10_000,
|
|
maximumCaptureRows: 100_000,
|
|
maximumCaptureGroups: 1_000,
|
|
maximumOutputBytes: 67_108_864,
|
|
maximumRequestJSONBytes: 103_874_560,
|
|
compiledSizeLimitBytes: 10_485_760,
|
|
dfaSizeLimitBytes: 2_097_152,
|
|
}),
|
|
});
|
|
|
|
const PACK_FILES = Object.freeze([
|
|
"LICENSE-Apache-2.0.txt",
|
|
"LICENSE-MIT.txt",
|
|
"LICENSE-Unicode-3.0.txt",
|
|
"RUST-STDLIB-COPYRIGHT.html",
|
|
"SHA256SUMS",
|
|
"THIRD_PARTY_LICENSES.txt",
|
|
"engine-metadata.json",
|
|
"rust-regex.mjs",
|
|
"rust-regex_bg.wasm",
|
|
]);
|
|
const CHECKSUM_FILES = Object.freeze(
|
|
PACK_FILES.filter((name) => name !== "SHA256SUMS"),
|
|
);
|
|
const SOURCE_FILES = Object.freeze([
|
|
"engines/rust/Cargo.lock",
|
|
"engines/rust/Cargo.toml",
|
|
"engines/rust/README.md",
|
|
"engines/rust/rust-toolchain.toml",
|
|
"engines/rust/src/lib.rs",
|
|
]);
|
|
|
|
function sha256(value) {
|
|
return createHash("sha256").update(value).digest("hex");
|
|
}
|
|
|
|
async function sha256File(file) {
|
|
return sha256(await readFile(file));
|
|
}
|
|
|
|
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 file: ${candidate}`);
|
|
}
|
|
return realpath(candidate);
|
|
}
|
|
|
|
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 };
|
|
}
|
|
|
|
function parseCargoLock(contents) {
|
|
const packages = [];
|
|
for (const block of contents.split("[[package]]").slice(1)) {
|
|
const field = (name) => {
|
|
const match = block.match(new RegExp(`^${name} = "([^"]+)"`, "mu"));
|
|
return match?.[1];
|
|
};
|
|
const name = field("name");
|
|
const version = field("version");
|
|
if (!name || !version) {
|
|
throw new Error("Cargo.lock contains a malformed package block.");
|
|
}
|
|
const source = field("source");
|
|
const checksum = field("checksum");
|
|
packages.push({
|
|
name,
|
|
version,
|
|
...(source ? { source } : {}),
|
|
...(checksum ? { checksum } : {}),
|
|
});
|
|
}
|
|
return packages;
|
|
}
|
|
|
|
async function findRegistryCrate(cargoHome, package_) {
|
|
const registrySource = path.join(cargoHome, "registry", "src");
|
|
const indexes = await readdir(registrySource, { withFileTypes: true });
|
|
const matches = [];
|
|
for (const index of indexes) {
|
|
if (!index.isDirectory() || index.isSymbolicLink()) continue;
|
|
const candidate = path.join(
|
|
registrySource,
|
|
index.name,
|
|
`${package_.name}-${package_.version}`,
|
|
);
|
|
const details = await lstat(candidate).catch(() => null);
|
|
if (details?.isDirectory() && !details.isSymbolicLink()) {
|
|
matches.push(await realpath(candidate));
|
|
}
|
|
}
|
|
if (matches.length !== 1) {
|
|
throw new Error(
|
|
`Expected one cached source directory for ${package_.name} ${package_.version}.`,
|
|
);
|
|
}
|
|
return matches[0];
|
|
}
|
|
|
|
async function thirdPartyLicenses(cargoHome, packages) {
|
|
const sections = [
|
|
"Regex Tools Rust engine third-party source notices",
|
|
"Generated from the checked-in Cargo.lock; package order is deterministic.",
|
|
"",
|
|
];
|
|
for (const package_ of packages.filter(({ source }) => source)) {
|
|
const crate = await findRegistryCrate(cargoHome, package_);
|
|
const entries = (await readdir(crate, { withFileTypes: true }))
|
|
.filter(
|
|
(entry) =>
|
|
entry.isFile() &&
|
|
!entry.isSymbolicLink() &&
|
|
/^(?:COPYING|COPYRIGHT|LICENSE|UNLICENSE)/iu.test(entry.name),
|
|
)
|
|
.sort((left, right) =>
|
|
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
|
|
);
|
|
sections.push(
|
|
`===============================================================================`,
|
|
`${package_.name} ${package_.version}`,
|
|
`Cargo source: ${package_.source}`,
|
|
`Cargo checksum: ${package_.checksum ?? "not recorded"}`,
|
|
);
|
|
if (entries.length === 0) {
|
|
sections.push(
|
|
"No root licence file was present in the packaged crate source.",
|
|
"",
|
|
);
|
|
continue;
|
|
}
|
|
for (const entry of entries) {
|
|
sections.push(
|
|
`-------------------------------------------------------------------------------`,
|
|
entry.name,
|
|
"",
|
|
(await readFile(path.join(crate, entry.name), "utf8")).trimEnd(),
|
|
"",
|
|
);
|
|
}
|
|
}
|
|
return `${sections.join("\n")}\n`;
|
|
}
|
|
|
|
async function verifyToolchain(toolchainRootDirectory) {
|
|
const root = await assertRealDirectory(
|
|
toolchainRootDirectory,
|
|
"Rust 1.97.1 toolchain",
|
|
);
|
|
const cargoHome = await assertRealDirectory(
|
|
path.join(root, "cargo"),
|
|
"Rust Cargo home",
|
|
);
|
|
const rustupHome = await assertRealDirectory(
|
|
path.join(root, "rustup"),
|
|
"Rustup home",
|
|
);
|
|
const toolchain = await assertRealDirectory(
|
|
path.join(rustupHome, "toolchains", "1.97.1-x86_64-unknown-linux-gnu"),
|
|
"Rust distribution toolchain",
|
|
);
|
|
const cargo = await assertRegularFile(
|
|
path.join(toolchain, "bin", "cargo"),
|
|
"cargo",
|
|
);
|
|
const rustc = await assertRegularFile(
|
|
path.join(toolchain, "bin", "rustc"),
|
|
"rustc",
|
|
);
|
|
const wasmBindgen = await assertRegularFile(
|
|
path.join(cargoHome, "bin", "wasm-bindgen"),
|
|
"wasm-bindgen",
|
|
);
|
|
for (const executable of [cargo, rustc, wasmBindgen]) {
|
|
await access(executable, fsConstants.X_OK);
|
|
}
|
|
const baseEnvironment = {
|
|
...process.env,
|
|
CARGO_HOME: cargoHome,
|
|
RUSTUP_HOME: rustupHome,
|
|
PATH: [
|
|
path.join(toolchain, "bin"),
|
|
path.join(cargoHome, "bin"),
|
|
process.env.PATH ?? "",
|
|
].join(path.delimiter),
|
|
};
|
|
const rustcVersion = await run(rustc, ["--version", "--verbose"], {
|
|
env: baseEnvironment,
|
|
});
|
|
if (
|
|
!rustcVersion.stdout.includes(`release: ${RUST_LOCK.rustcVersion}`) ||
|
|
!rustcVersion.stdout.includes(`commit-hash: ${RUST_LOCK.rustcCommit}`) ||
|
|
!rustcVersion.stdout.includes(`LLVM version: ${RUST_LOCK.llvmVersion}`)
|
|
) {
|
|
throw new Error("The Rust engine requires pinned rustc 1.97.1.");
|
|
}
|
|
const cargoVersion = await run(cargo, ["--version"], {
|
|
env: baseEnvironment,
|
|
});
|
|
if (cargoVersion.stdout.trim() !== RUST_LOCK.cargoIdentity) {
|
|
throw new Error("The Rust engine requires pinned Cargo 1.97.1.");
|
|
}
|
|
const bindgenVersion = await run(wasmBindgen, ["--version"], {
|
|
env: baseEnvironment,
|
|
});
|
|
if (
|
|
bindgenVersion.stdout.trim() !==
|
|
`wasm-bindgen ${RUST_LOCK.wasmBindgenVersion}`
|
|
) {
|
|
throw new Error("The Rust engine requires pinned wasm-bindgen 0.2.126.");
|
|
}
|
|
|
|
const docs = path.join(toolchain, "share", "doc", "rust");
|
|
const files = {
|
|
apache: await assertRegularFile(
|
|
path.join(docs, "licenses", "Apache-2.0.txt"),
|
|
"Rust Apache licence",
|
|
),
|
|
mit: await assertRegularFile(
|
|
path.join(docs, "licenses", "MIT.txt"),
|
|
"Rust MIT licence",
|
|
),
|
|
unicode: await assertRegularFile(
|
|
path.join(docs, "licenses", "Unicode-3.0.txt"),
|
|
"Rust Unicode licence",
|
|
),
|
|
stdlibCopyright: await assertRegularFile(
|
|
path.join(docs, "COPYRIGHT-library.html"),
|
|
"Rust standard-library copyright notice",
|
|
),
|
|
};
|
|
for (const [name, file] of Object.entries(files)) {
|
|
if (
|
|
(await sha256File(file)) !== RUST_LOCK.distribution.licenseHashes[name]
|
|
) {
|
|
throw new Error(`The pinned Rust ${name} notice does not match 1.97.1.`);
|
|
}
|
|
}
|
|
return {
|
|
root,
|
|
cargoHome,
|
|
rustupHome,
|
|
cargo,
|
|
rustc,
|
|
wasmBindgen,
|
|
baseEnvironment,
|
|
files,
|
|
};
|
|
}
|
|
|
|
function buildEnvironment(toolchain, repositoryRoot, targetDirectory) {
|
|
const environment = { ...toolchain.baseEnvironment };
|
|
for (const variable of [
|
|
"CARGO_BUILD_RUSTC",
|
|
"CARGO_ENCODED_RUSTFLAGS",
|
|
"CARGO_PROFILE_RELEASE_DEBUG",
|
|
"RUSTC",
|
|
"RUSTDOC",
|
|
"RUSTDOCFLAGS",
|
|
"RUSTFLAGS",
|
|
]) {
|
|
delete environment[variable];
|
|
}
|
|
environment.CARGO_INCREMENTAL = "0";
|
|
environment.CARGO_NET_OFFLINE = "true";
|
|
environment.CARGO_TARGET_DIR = targetDirectory;
|
|
environment.LANG = "C";
|
|
environment.LC_ALL = "C";
|
|
environment.REGEX_TOOLS_RUSTC_VERSION = RUST_LOCK.rustcIdentity;
|
|
environment.RUST_BACKTRACE = "0";
|
|
environment.RUSTUP_TOOLCHAIN = "1.97.1-x86_64-unknown-linux-gnu";
|
|
environment.RUSTFLAGS = [
|
|
`--remap-path-prefix=${repositoryRoot}=.`,
|
|
`--remap-path-prefix=${toolchain.cargoHome}=./cargo-home`,
|
|
].join(" ");
|
|
environment.SOURCE_DATE_EPOCH = String(RUST_LOCK.sourceDateEpoch);
|
|
environment.TZ = "UTC";
|
|
return environment;
|
|
}
|
|
|
|
async function sourceMetadata(root) {
|
|
return Promise.all(
|
|
SOURCE_FILES.map(async (relativePath) => {
|
|
const contents = await readFile(path.join(root, relativePath));
|
|
return {
|
|
path: relativePath,
|
|
sha256: sha256(contents),
|
|
bytes: contents.byteLength,
|
|
};
|
|
}),
|
|
);
|
|
}
|
|
|
|
async function fileMetadata(pack, name) {
|
|
const contents = await readFile(path.join(pack, name));
|
|
return { path: name, sha256: sha256(contents), bytes: contents.byteLength };
|
|
}
|
|
|
|
async function lockedPackages(root) {
|
|
const contents = await readFile(
|
|
path.join(root, "engines", "rust", "Cargo.lock"),
|
|
"utf8",
|
|
);
|
|
return parseCargoLock(contents)
|
|
.filter(({ source }) => source)
|
|
.map(({ name, version, checksum }) => ({ name, version, checksum }));
|
|
}
|
|
|
|
async function expectedMetadata(root, pack) {
|
|
return {
|
|
schemaVersion: 1,
|
|
engine: "rust",
|
|
engineName: "Rust regex crate",
|
|
engineVersion: RUST_LOCK.engineVersion,
|
|
semanticIdentity:
|
|
"Rust regex crate 1.13.1 compiled to WebAssembly with rustc 1.97.1",
|
|
status: "production-native-runtime",
|
|
bridge: {
|
|
abiVersion: RUST_LOCK.bridgeAbi,
|
|
offsetUnit: "utf8-byte",
|
|
requestEncoding:
|
|
"Exact UTF-8-bounded JSON values with worst-case escape allowance; fixed exports; base64 UTF-8 replacement output; no source evaluation",
|
|
supportedApplicationFlags: ["g", "i", "m", "s", "U", "u", "x", "R"],
|
|
unicode:
|
|
"Enabled by default, matching the Rust regex crate; u explicitly affirms that default.",
|
|
replacement:
|
|
"Bounded streaming expansion with regex::Captures::expand grammar, parity-checked against the native API.",
|
|
limits: { ...RUST_LOCK.limits },
|
|
sourceFiles: await sourceMetadata(root),
|
|
},
|
|
source: {
|
|
repository: RUST_LOCK.source.repository,
|
|
tag: RUST_LOCK.source.tag,
|
|
tagObjectSha1: RUST_LOCK.source.tagObject,
|
|
commitSha1: RUST_LOCK.source.commit,
|
|
treeSha1: RUST_LOCK.source.tree,
|
|
crateChecksumSha256: RUST_LOCK.source.crateChecksum,
|
|
license: RUST_LOCK.source.license,
|
|
},
|
|
toolchain: {
|
|
rustcVersion: RUST_LOCK.rustcVersion,
|
|
rustcIdentity: RUST_LOCK.rustcIdentity,
|
|
rustcCommitSha1: RUST_LOCK.rustcCommit,
|
|
llvmVersion: RUST_LOCK.llvmVersion,
|
|
cargoIdentity: RUST_LOCK.cargoIdentity,
|
|
target: "wasm32-unknown-unknown",
|
|
wasmBindgenVersion: RUST_LOCK.wasmBindgenVersion,
|
|
wasmBindgenCrateChecksumSha256: RUST_LOCK.wasmBindgenCrateChecksum,
|
|
wasmBindgenCliChecksumSha256: RUST_LOCK.wasmBindgenCliChecksum,
|
|
distributionManifest: RUST_LOCK.distribution.manifest,
|
|
distributionManifestSha256: RUST_LOCK.distribution.manifestSha256,
|
|
rustcArchiveSha256: RUST_LOCK.distribution.rustcArchiveSha256,
|
|
wasmStdArchiveSha256: RUST_LOCK.distribution.wasmStdArchiveSha256,
|
|
cargoLockPackages: await lockedPackages(root),
|
|
},
|
|
sourceDateEpoch: RUST_LOCK.sourceDateEpoch,
|
|
files: await Promise.all(
|
|
CHECKSUM_FILES.filter((name) => name !== "engine-metadata.json").map(
|
|
(name) => fileMetadata(pack, name),
|
|
),
|
|
),
|
|
};
|
|
}
|
|
|
|
async function checksumText(pack) {
|
|
const lines = await Promise.all(
|
|
CHECKSUM_FILES.map(
|
|
async (name) => `${await sha256File(path.join(pack, name))} ${name}`,
|
|
),
|
|
);
|
|
return `${lines.join("\n")}\n`;
|
|
}
|
|
|
|
function equalJson(left, right) {
|
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
}
|
|
|
|
async function smokePack(pack) {
|
|
const moduleUrl = new URL(`file://${path.join(pack, "rust-regex.mjs")}`).href;
|
|
const wasmPath = path.join(pack, "rust-regex_bg.wasm");
|
|
const smoke = `
|
|
import { readFile } from "node:fs/promises";
|
|
const engine = await import(process.argv[1]);
|
|
engine.initSync({
|
|
module: await WebAssembly.compile(await readFile(process.argv[2])),
|
|
});
|
|
const identity = JSON.parse(engine.identity());
|
|
if (
|
|
identity.abi !== 1 ||
|
|
identity.ok !== true ||
|
|
identity.identity.engineVersion !== "1.13.1" ||
|
|
identity.identity.rustcVersion !==
|
|
"rustc 1.97.1 (8bab26f4f 2026-07-14)" ||
|
|
identity.identity.selfTest !== true
|
|
) throw new Error("Rust identity self-test failed.");
|
|
const result = JSON.parse(engine.run(JSON.stringify({
|
|
abi: 1,
|
|
operation: "replace",
|
|
pattern: "(?P<word>😀+)",
|
|
flags: "g",
|
|
options: {},
|
|
subject: "x😀😀",
|
|
scanAll: false,
|
|
maximumMatches: 10,
|
|
maximumCaptureRows: 10,
|
|
replacement: "\${word}-$$",
|
|
maximumOutputBytes: 100,
|
|
})));
|
|
if (
|
|
result.ok !== true ||
|
|
result.matches[0].span[0] !== 1 ||
|
|
result.matches[0].span[1] !== 9 ||
|
|
Buffer.from(result.outputBase64, "base64").toString("utf8") !== "x😀😀-$"
|
|
) throw new Error("Rust execution self-test failed.");
|
|
`;
|
|
await run(process.execPath, [
|
|
"--input-type=module",
|
|
"--eval",
|
|
smoke,
|
|
moduleUrl,
|
|
wasmPath,
|
|
]);
|
|
}
|
|
|
|
export async function verifyRustPack(packDirectory, root) {
|
|
const repositoryRoot = await assertRealDirectory(
|
|
root,
|
|
"Regex Tools repository",
|
|
);
|
|
const pack = await assertRealDirectory(packDirectory, "Rust engine 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 Rust pack must contain only: ${PACK_FILES.join(", ")}.`,
|
|
);
|
|
}
|
|
const licenseFiles = {
|
|
"LICENSE-Apache-2.0.txt": RUST_LOCK.distribution.licenseHashes.apache,
|
|
"LICENSE-MIT.txt": RUST_LOCK.distribution.licenseHashes.mit,
|
|
"LICENSE-Unicode-3.0.txt": RUST_LOCK.distribution.licenseHashes.unicode,
|
|
"RUST-STDLIB-COPYRIGHT.html":
|
|
RUST_LOCK.distribution.licenseHashes.stdlibCopyright,
|
|
};
|
|
for (const [name, digest] of Object.entries(licenseFiles)) {
|
|
if ((await sha256File(path.join(pack, name))) !== digest) {
|
|
throw new Error(`${name} does not match the Rust 1.97.1 distribution.`);
|
|
}
|
|
}
|
|
const wasm = await readFile(path.join(pack, "rust-regex_bg.wasm"));
|
|
if (
|
|
wasm.byteLength < 100_000 ||
|
|
wasm.subarray(0, 4).toString("hex") !== "0061736d"
|
|
) {
|
|
throw new Error("The staged Rust WebAssembly module is invalid.");
|
|
}
|
|
const moduleText = await readFile(path.join(pack, "rust-regex.mjs"), "utf8");
|
|
if (
|
|
moduleText.includes("sourceMappingURL") ||
|
|
moduleText.includes("/mnt/") ||
|
|
moduleText.includes("/home/") ||
|
|
!moduleText.includes("export function identity") ||
|
|
!moduleText.includes("export function run") ||
|
|
!moduleText.includes("export { initSync")
|
|
) {
|
|
throw new Error(
|
|
"rust-regex.mjs leaks a build path, has a source map, or lacks bridge exports.",
|
|
);
|
|
}
|
|
const packages = await lockedPackages(repositoryRoot);
|
|
const notices = await readFile(
|
|
path.join(pack, "THIRD_PARTY_LICENSES.txt"),
|
|
"utf8",
|
|
);
|
|
for (const package_ of packages) {
|
|
if (
|
|
!notices.includes(`${package_.name} ${package_.version}\n`) ||
|
|
!notices.includes(`Cargo checksum: ${package_.checksum}`)
|
|
) {
|
|
throw new Error(
|
|
`Third-party notices omit ${package_.name} ${package_.version}.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
let metadata;
|
|
try {
|
|
metadata = JSON.parse(
|
|
await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
|
|
);
|
|
} catch (error) {
|
|
throw new Error("Rust engine-metadata.json is invalid JSON.", {
|
|
cause: error,
|
|
});
|
|
}
|
|
if (!equalJson(metadata, await expectedMetadata(repositoryRoot, pack))) {
|
|
throw new Error(
|
|
"The Rust metadata does not match the pinned build contract.",
|
|
);
|
|
}
|
|
if (
|
|
(await readFile(path.join(pack, "SHA256SUMS"), "utf8")) !==
|
|
(await checksumText(pack))
|
|
) {
|
|
throw new Error("The Rust SHA256SUMS file is incorrect.");
|
|
}
|
|
await smokePack(pack);
|
|
return metadata;
|
|
}
|
|
|
|
async function replaceDirectory(stage, target, label) {
|
|
const details = await lstat(target).catch(() => null);
|
|
if (details && (!details.isDirectory() || details.isSymbolicLink())) {
|
|
throw new Error(`${label} must be a real directory.`);
|
|
}
|
|
if (!details) {
|
|
await rename(stage, target);
|
|
return;
|
|
}
|
|
const backup = `${target}.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 });
|
|
}
|
|
|
|
async function bindgen(toolchain, rawWasm, output) {
|
|
await mkdir(output, { recursive: true });
|
|
await run(
|
|
toolchain.wasmBindgen,
|
|
[
|
|
"--target",
|
|
"web",
|
|
"--no-typescript",
|
|
"--out-name",
|
|
"rust-regex",
|
|
"--out-dir",
|
|
output,
|
|
rawWasm,
|
|
],
|
|
{ env: toolchain.baseEnvironment },
|
|
);
|
|
await rename(
|
|
path.join(output, "rust-regex.js"),
|
|
path.join(output, "rust-regex.mjs"),
|
|
);
|
|
}
|
|
|
|
export async function buildRustPack(
|
|
root,
|
|
toolchainRootDirectory,
|
|
outputDirectory,
|
|
) {
|
|
const repositoryRoot = await assertRealDirectory(
|
|
root,
|
|
"Regex Tools repository",
|
|
);
|
|
const engine = await assertRealDirectory(
|
|
path.join(repositoryRoot, "engines", "rust"),
|
|
"Rust engine source",
|
|
);
|
|
const toolchain = await verifyToolchain(toolchainRootDirectory);
|
|
const lockPackages = parseCargoLock(
|
|
await readFile(path.join(engine, "Cargo.lock"), "utf8"),
|
|
);
|
|
const regexPackage = lockPackages.find(
|
|
({ name, version }) => name === "regex" && version === "1.13.1",
|
|
);
|
|
const bindgenPackage = lockPackages.find(
|
|
({ name, version }) =>
|
|
name === "wasm-bindgen" && version === RUST_LOCK.wasmBindgenVersion,
|
|
);
|
|
if (
|
|
regexPackage?.checksum !== RUST_LOCK.source.crateChecksum ||
|
|
bindgenPackage?.checksum !== RUST_LOCK.wasmBindgenCrateChecksum
|
|
) {
|
|
throw new Error("Cargo.lock does not contain the pinned engine crates.");
|
|
}
|
|
|
|
const buildParent = path.join(repositoryRoot, ".engine-build");
|
|
await mkdir(buildParent, { recursive: true });
|
|
const temporary = await mkdtemp(path.join(buildParent, "rust-build-"));
|
|
try {
|
|
const generated = [];
|
|
for (const label of ["first", "second"]) {
|
|
const target = path.join(temporary, `${label}-target`);
|
|
const environment = buildEnvironment(toolchain, repositoryRoot, target);
|
|
await run(
|
|
toolchain.cargo,
|
|
[
|
|
"build",
|
|
"--locked",
|
|
"--offline",
|
|
"--release",
|
|
"--target",
|
|
"wasm32-unknown-unknown",
|
|
],
|
|
{ cwd: engine, env: environment },
|
|
);
|
|
const rawWasm = await assertRegularFile(
|
|
path.join(
|
|
target,
|
|
"wasm32-unknown-unknown",
|
|
"release",
|
|
"regex_tools_rust_engine.wasm",
|
|
),
|
|
"compiled Rust WebAssembly",
|
|
);
|
|
const output = path.join(temporary, `${label}-bindgen`);
|
|
await bindgen(toolchain, rawWasm, output);
|
|
generated.push(output);
|
|
}
|
|
for (const name of ["rust-regex.mjs", "rust-regex_bg.wasm"]) {
|
|
if (
|
|
(await sha256File(path.join(generated[0], name))) !==
|
|
(await sha256File(path.join(generated[1], name)))
|
|
) {
|
|
throw new Error(
|
|
`Two pinned Rust builds produced different ${name} hashes.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const target = path.resolve(
|
|
repositoryRoot,
|
|
outputDirectory ?? path.join(".engine-build", "rust"),
|
|
);
|
|
const stage = await mkdtemp(path.join(buildParent, "rust-pack-"));
|
|
await copyFile(
|
|
toolchain.files.apache,
|
|
path.join(stage, "LICENSE-Apache-2.0.txt"),
|
|
);
|
|
await copyFile(toolchain.files.mit, path.join(stage, "LICENSE-MIT.txt"));
|
|
await copyFile(
|
|
toolchain.files.unicode,
|
|
path.join(stage, "LICENSE-Unicode-3.0.txt"),
|
|
);
|
|
await copyFile(
|
|
toolchain.files.stdlibCopyright,
|
|
path.join(stage, "RUST-STDLIB-COPYRIGHT.html"),
|
|
);
|
|
await copyFile(
|
|
path.join(generated[0], "rust-regex.mjs"),
|
|
path.join(stage, "rust-regex.mjs"),
|
|
);
|
|
await copyFile(
|
|
path.join(generated[0], "rust-regex_bg.wasm"),
|
|
path.join(stage, "rust-regex_bg.wasm"),
|
|
);
|
|
await writeFile(
|
|
path.join(stage, "THIRD_PARTY_LICENSES.txt"),
|
|
await thirdPartyLicenses(toolchain.cargoHome, lockPackages),
|
|
"utf8",
|
|
);
|
|
const metadata = await expectedMetadata(repositoryRoot, stage);
|
|
await writeFile(
|
|
path.join(stage, "engine-metadata.json"),
|
|
`${JSON.stringify(metadata, null, 2)}\n`,
|
|
"utf8",
|
|
);
|
|
await writeFile(path.join(stage, "SHA256SUMS"), await checksumText(stage));
|
|
await verifyRustPack(stage, repositoryRoot);
|
|
await mkdir(path.dirname(target), { recursive: true });
|
|
await replaceDirectory(stage, target, "Rust staged pack");
|
|
return { metadata, output: target };
|
|
} finally {
|
|
await rm(temporary, { recursive: true });
|
|
}
|
|
}
|
|
|
|
export async function installRustPack(sourceDirectory, root) {
|
|
const repositoryRoot = await assertRealDirectory(
|
|
root,
|
|
"Regex Tools repository",
|
|
);
|
|
const source = await assertRealDirectory(sourceDirectory, "Rust staged pack");
|
|
const metadata = await verifyRustPack(source, repositoryRoot);
|
|
const engineParent = path.join(repositoryRoot, "public", "engines");
|
|
await mkdir(engineParent, { recursive: true });
|
|
const stage = await mkdtemp(path.join(engineParent, "rust-install-"));
|
|
for (const name of PACK_FILES) {
|
|
await copyFile(path.join(source, name), path.join(stage, name));
|
|
}
|
|
await verifyRustPack(stage, repositoryRoot);
|
|
const output = path.join(engineParent, "rust");
|
|
await replaceDirectory(stage, output, "Installed Rust engine pack");
|
|
return { metadata, output };
|
|
}
|