mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
refactor(windows): share native artifact validation
This commit is contained in:
1 parent
729684af9a
commit
3d565fa42a
8 files changed
+427
-82
No files matched your search
@@ -6,6 +6,13 @@ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { importVerifierSource } from "./lib/verifier-source-loader.mjs";
|
||||||
|
|
||||||
|
const { WINDOWS_NATIVE_ARTIFACT, createWindowsNativeManifest } =
|
||||||
|
await importVerifierSource(
|
||||||
|
new URL("../src/domain/windowsNativeArtifact.ts", import.meta.url).href,
|
||||||
|
import.meta.url,
|
||||||
|
);
|
||||||
|
|
||||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
const nodeVersion = "22.19.0";
|
const nodeVersion = "22.19.0";
|
||||||
@@ -87,7 +94,7 @@ if (!(await exists(join(headers, "node_api.h")))) {
|
|||||||
run("tar", ["xzf", archive, "-C", cache]);
|
run("tar", ["xzf", archive, "-C", cache]);
|
||||||
headers = join(cache, `node-v${nodeVersion}`, "include", "node");
|
headers = join(cache, `node-v${nodeVersion}`, "include", "node");
|
||||||
}
|
}
|
||||||
const artifact = "rea-windows-x64.node";
|
const artifact = WINDOWS_NATIVE_ARTIFACT.artifact;
|
||||||
// MSVC short-import libraries do not produce a correct Node import table with
|
// MSVC short-import libraries do not produce a correct Node import table with
|
||||||
// GNU ld. Generate a GNU delay-import library for the exact stable Node-API
|
// GNU ld. Generate a GNU delay-import library for the exact stable Node-API
|
||||||
// symbols; addon.cc binds it to the host executable, whatever its file name.
|
// symbols; addon.cc binds it to the host executable, whatever its file name.
|
||||||
@@ -116,7 +123,7 @@ run("x86_64-w64-mingw32-dlltool", ["-d", definition, "-y", library]);
|
|||||||
run(compiler, [
|
run(compiler, [
|
||||||
"-std=c++17",
|
"-std=c++17",
|
||||||
"-O2",
|
"-O2",
|
||||||
"-DNAPI_VERSION=8",
|
`-DNAPI_VERSION=${WINDOWS_NATIVE_ARTIFACT.nodeApiVersion}`,
|
||||||
"-D_WIN32_WINNT=0x0A00",
|
"-D_WIN32_WINNT=0x0A00",
|
||||||
"-shared",
|
"-shared",
|
||||||
"-static",
|
"-static",
|
||||||
@@ -140,15 +147,9 @@ const packageJson = JSON.parse(
|
|||||||
await readFile(join(root, "package.json"), "utf8"),
|
await readFile(join(root, "package.json"), "utf8"),
|
||||||
);
|
);
|
||||||
const bytes = await readFile(join(output, artifact));
|
const bytes = await readFile(join(output, artifact));
|
||||||
const metadata = {
|
const created = createWindowsNativeManifest(packageJson.version, bytes);
|
||||||
packageVersion: packageJson.version,
|
if (!created.ok) throw created.error;
|
||||||
platform: "win32",
|
const metadata = created.value;
|
||||||
architecture: "x64",
|
|
||||||
abiVersion: 1,
|
|
||||||
nodeApiVersion: 8,
|
|
||||||
artifact,
|
|
||||||
artifactSha256: createHash("sha256").update(bytes).digest("hex"),
|
|
||||||
};
|
|
||||||
await writeFile(
|
await writeFile(
|
||||||
join(output, "manifest.json"),
|
join(output, "manifest.json"),
|
||||||
`${JSON.stringify(metadata, null, 2)}\n`,
|
`${JSON.stringify(metadata, null, 2)}\n`,
|
||||||
|
|||||||
@@ -125,6 +125,11 @@ export const SCOPES = {
|
|||||||
"tests/boundary/filesystem/npmRelease.test.ts",
|
"tests/boundary/filesystem/npmRelease.test.ts",
|
||||||
"tests/boundary/process/releaseArtifactCanary.test.ts",
|
"tests/boundary/process/releaseArtifactCanary.test.ts",
|
||||||
"src/windows/**",
|
"src/windows/**",
|
||||||
|
"src/domain/windowsNativeArtifact*.ts",
|
||||||
|
"src/domain/peCoffHeader*.ts",
|
||||||
|
"src/domain/result.ts",
|
||||||
|
"scripts/lib/verifier-source-loader.mjs",
|
||||||
|
"tests/boundary/process/windowsNativeArtifactVerifier.test.ts",
|
||||||
"scripts/lib/windows-owned-command.mjs",
|
"scripts/lib/windows-owned-command.mjs",
|
||||||
"scripts/run-exclusive-command.mjs",
|
"scripts/run-exclusive-command.mjs",
|
||||||
"tests/process-global/runnerCancellation.test.ts",
|
"tests/process-global/runnerCancellation.test.ts",
|
||||||
|
|||||||
@@ -1,39 +1,42 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { createHash } from "node:crypto";
|
import { access, readFile } from "node:fs/promises";
|
||||||
import { readFile } from "node:fs/promises";
|
|
||||||
|
|
||||||
const root = new URL("../", import.meta.url);
|
const root = new URL("../", import.meta.url);
|
||||||
|
const source = new URL("src/domain/windowsNativeArtifact.ts", root);
|
||||||
|
let sourceExists = true;
|
||||||
|
try {
|
||||||
|
await access(source);
|
||||||
|
} catch (cause) {
|
||||||
|
if (cause?.code !== "ENOENT") throw cause;
|
||||||
|
sourceExists = false;
|
||||||
|
}
|
||||||
|
const owner = sourceExists
|
||||||
|
? await (
|
||||||
|
await import("./lib/verifier-source-loader.mjs")
|
||||||
|
).importVerifierSource(source.href, import.meta.url)
|
||||||
|
: await import(new URL("dist/domain/windowsNativeArtifact.js", root).href);
|
||||||
|
const {
|
||||||
|
WINDOWS_NATIVE_ARTIFACT,
|
||||||
|
parseWindowsNativeManifest,
|
||||||
|
validateWindowsNativeArtifact,
|
||||||
|
} = owner;
|
||||||
const packageJson = JSON.parse(
|
const packageJson = JSON.parse(
|
||||||
await readFile(new URL("package.json", root), "utf8"),
|
await readFile(new URL("package.json", root), "utf8"),
|
||||||
);
|
);
|
||||||
const manifest = JSON.parse(
|
const parsed = parseWindowsNativeManifest(
|
||||||
await readFile(new URL("native/windows/build/manifest.json", root), "utf8"),
|
JSON.parse(
|
||||||
|
await readFile(new URL("native/windows/build/manifest.json", root), "utf8"),
|
||||||
|
),
|
||||||
|
packageJson.version,
|
||||||
);
|
);
|
||||||
|
if (!parsed.ok) throw parsed.error;
|
||||||
|
const manifest = parsed.value;
|
||||||
const bytes = await readFile(
|
const bytes = await readFile(
|
||||||
new URL("native/windows/build/rea-windows-x64.node", root),
|
new URL(`native/windows/build/${WINDOWS_NATIVE_ARTIFACT.artifact}`, root),
|
||||||
);
|
);
|
||||||
if (
|
const valid = validateWindowsNativeArtifact(manifest, bytes);
|
||||||
manifest.packageVersion !== packageJson.version ||
|
if (!valid.ok) throw valid.error;
|
||||||
manifest.platform !== "win32" ||
|
|
||||||
manifest.architecture !== "x64" ||
|
|
||||||
manifest.abiVersion !== 1 ||
|
|
||||||
manifest.nodeApiVersion !== 8 ||
|
|
||||||
manifest.artifact !== "rea-windows-x64.node" ||
|
|
||||||
createHash("sha256").update(bytes).digest("hex") !== manifest.artifactSha256
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
"Windows native artifact metadata or SHA-256 does not match this package; run the Windows native build lane.",
|
|
||||||
);
|
|
||||||
const offset = bytes.length >= 64 ? bytes.readUInt32LE(0x3c) : bytes.length;
|
|
||||||
if (
|
|
||||||
bytes.toString("latin1", 0, 2) !== "MZ" ||
|
|
||||||
offset > bytes.length - 26 ||
|
|
||||||
bytes.toString("latin1", offset, offset + 4) !== "PE\0\0" ||
|
|
||||||
bytes.readUInt16LE(offset + 4) !== 0x8664 ||
|
|
||||||
bytes.readUInt16LE(offset + 24) !== 0x20b
|
|
||||||
)
|
|
||||||
throw new Error("Windows native artifact must be a Windows x64 PE32+ image.");
|
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`${JSON.stringify({ ok: true, ...manifest, artifactBytes: bytes.length })}\n`,
|
`${JSON.stringify({ ok: true, ...manifest, artifactBytes: bytes.length })}\n`,
|
||||||
);
|
);
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
createWindowsNativeManifest,
|
||||||
|
parseWindowsNativeManifest,
|
||||||
|
validateWindowsNativeArtifact,
|
||||||
|
} from "./windowsNativeArtifact.js";
|
||||||
|
|
||||||
|
const fixture = () => {
|
||||||
|
const bytes = Buffer.alloc(128);
|
||||||
|
bytes.write("MZ");
|
||||||
|
bytes.writeUInt32LE(64, 0x3c);
|
||||||
|
bytes.write("PE\0\0", 64, "ascii");
|
||||||
|
bytes.writeUInt16LE(0x8664, 68);
|
||||||
|
bytes.writeUInt16LE(0x20b, 88);
|
||||||
|
const manifest = {
|
||||||
|
packageVersion: "1.0.0",
|
||||||
|
platform: "win32",
|
||||||
|
architecture: "x64",
|
||||||
|
abiVersion: 1,
|
||||||
|
nodeApiVersion: 8,
|
||||||
|
artifact: "rea-windows-x64.node",
|
||||||
|
artifactSha256: createHash("sha256").update(bytes).digest("hex"),
|
||||||
|
};
|
||||||
|
return { bytes, manifest };
|
||||||
|
};
|
||||||
|
|
||||||
|
it("produces and admits the exact manifest/byte identity", () => {
|
||||||
|
const { bytes, manifest } = fixture();
|
||||||
|
const created = createWindowsNativeManifest("1.0.0", bytes);
|
||||||
|
expect(created).toEqual({ ok: true, value: manifest });
|
||||||
|
const parsed = parseWindowsNativeManifest(manifest, "1.0.0");
|
||||||
|
expect(parsed).toEqual(created);
|
||||||
|
if (!parsed.ok) throw parsed.error;
|
||||||
|
expect(validateWindowsNativeArtifact(parsed.value, bytes)).toEqual({
|
||||||
|
ok: true,
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
expect(parseWindowsNativeManifest(manifest, "2.0.0")).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { message: expect.stringContaining("another REA version") },
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
validateWindowsNativeArtifact(parsed.value, Buffer.alloc(128)),
|
||||||
|
).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { message: expect.stringContaining("SHA-256") },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([null, [], {}, "manifest"])(
|
||||||
|
"rejects malformed manifest %j",
|
||||||
|
(value) => {
|
||||||
|
expect(parseWindowsNativeManifest(value, "1.0.0")).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { _tag: "WindowsNativeArtifactError" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("rejects missing and unknown manifest fields", () => {
|
||||||
|
const { manifest } = fixture();
|
||||||
|
expect(
|
||||||
|
parseWindowsNativeManifest({ ...manifest, extra: true }, "1.0.0"),
|
||||||
|
).toMatchObject({ ok: false });
|
||||||
|
for (const key of Object.keys(manifest)) {
|
||||||
|
const incomplete: Record<string, unknown> = { ...manifest };
|
||||||
|
delete incomplete[key];
|
||||||
|
expect(parseWindowsNativeManifest(incomplete, "1.0.0")).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
parseWindowsNativeManifest({ ...manifest, [key]: null }, "1.0.0"),
|
||||||
|
).toMatchObject({ ok: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
"platform",
|
||||||
|
"architecture",
|
||||||
|
"abiVersion",
|
||||||
|
"nodeApiVersion",
|
||||||
|
"artifact",
|
||||||
|
"artifactSha256",
|
||||||
|
])("rejects unsupported %s", (key) => {
|
||||||
|
const { manifest } = fixture();
|
||||||
|
expect(
|
||||||
|
parseWindowsNativeManifest({ ...manifest, [key]: "unsupported" }, "1.0.0"),
|
||||||
|
).toMatchObject({ ok: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([0, 63, 64, 89, 128])(
|
||||||
|
"rejects invalid PE content in %i bytes",
|
||||||
|
(length) => {
|
||||||
|
expect(
|
||||||
|
createWindowsNativeManifest("1.0.0", Buffer.alloc(length)),
|
||||||
|
).toMatchObject({ ok: false });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(["offset", "signature", "machine", "optional magic"])(
|
||||||
|
"rejects invalid PE %s with a matching digest",
|
||||||
|
(field) => {
|
||||||
|
const { bytes } = fixture();
|
||||||
|
if (field === "offset") bytes.writeUInt32LE(0xffff_ffff, 0x3c);
|
||||||
|
if (field === "signature") bytes.write("BAD!", 64, "ascii");
|
||||||
|
if (field === "machine") bytes.writeUInt16LE(0x14c, 68);
|
||||||
|
if (field === "optional magic") bytes.writeUInt16LE(0x10b, 88);
|
||||||
|
const { manifest } = fixture();
|
||||||
|
manifest.artifactSha256 = createHash("sha256").update(bytes).digest("hex");
|
||||||
|
const parsed = parseWindowsNativeManifest(manifest, "1.0.0");
|
||||||
|
if (!parsed.ok) throw parsed.error;
|
||||||
|
expect(validateWindowsNativeArtifact(parsed.value, bytes)).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { message: expect.stringContaining("PE32+") },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { readPeCoffHeader } from "./peCoffHeader.js";
|
||||||
|
import { err, ok, type Result } from "./result.js";
|
||||||
|
|
||||||
|
/** Fixed compatibility contract shared by the artifact producer and readers. */
|
||||||
|
export const WINDOWS_NATIVE_ARTIFACT = {
|
||||||
|
platform: "win32",
|
||||||
|
architecture: "x64",
|
||||||
|
abiVersion: 1,
|
||||||
|
nodeApiVersion: 8,
|
||||||
|
artifact: "rea-windows-x64.node",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type WindowsNativeManifest = typeof WINDOWS_NATIVE_ARTIFACT & {
|
||||||
|
readonly packageVersion: string;
|
||||||
|
readonly artifactSha256: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class WindowsNativeArtifactError extends Error {
|
||||||
|
readonly _tag = "WindowsNativeArtifactError" as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse the exact package manifest before acquiring or loading its artifact. */
|
||||||
|
export const parseWindowsNativeManifest = (
|
||||||
|
value: unknown,
|
||||||
|
packageVersion: string,
|
||||||
|
): Result<WindowsNativeManifest, WindowsNativeArtifactError> => {
|
||||||
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
||||||
|
return err(
|
||||||
|
new WindowsNativeArtifactError(
|
||||||
|
"Windows native artifact manifest must be an object",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const manifest = value as Record<string, unknown>;
|
||||||
|
const keys = [
|
||||||
|
"packageVersion",
|
||||||
|
...Object.keys(WINDOWS_NATIVE_ARTIFACT),
|
||||||
|
"artifactSha256",
|
||||||
|
];
|
||||||
|
if (
|
||||||
|
Object.keys(manifest).length !== keys.length ||
|
||||||
|
keys.some((key) => !Object.hasOwn(manifest, key))
|
||||||
|
)
|
||||||
|
return err(
|
||||||
|
new WindowsNativeArtifactError(
|
||||||
|
"Windows native artifact manifest has missing or unknown fields",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for (const [key, expected] of Object.entries(WINDOWS_NATIVE_ARTIFACT))
|
||||||
|
if (manifest[key] !== expected)
|
||||||
|
return err(
|
||||||
|
new WindowsNativeArtifactError(
|
||||||
|
`Windows native artifact manifest has an invalid ${key}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (typeof manifest.packageVersion !== "string")
|
||||||
|
return err(
|
||||||
|
new WindowsNativeArtifactError(
|
||||||
|
"Windows native artifact manifest has an invalid packageVersion",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
typeof manifest.artifactSha256 !== "string" ||
|
||||||
|
!/^[a-f0-9]{64}$/u.test(manifest.artifactSha256)
|
||||||
|
)
|
||||||
|
return err(
|
||||||
|
new WindowsNativeArtifactError(
|
||||||
|
"Windows native artifact manifest has an invalid artifactSha256",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (manifest.packageVersion !== packageVersion)
|
||||||
|
return err(
|
||||||
|
new WindowsNativeArtifactError(
|
||||||
|
"Windows native artifact belongs to another REA version",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return ok({
|
||||||
|
...WINDOWS_NATIVE_ARTIFACT,
|
||||||
|
packageVersion: manifest.packageVersion,
|
||||||
|
artifactSha256: manifest.artifactSha256,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateWindowsX64Artifact = (
|
||||||
|
bytes: Buffer,
|
||||||
|
): Result<void, WindowsNativeArtifactError> => {
|
||||||
|
if (bytes.length < 64 || bytes.toString("latin1", 0, 2) !== "MZ")
|
||||||
|
return err(
|
||||||
|
new WindowsNativeArtifactError(
|
||||||
|
"Windows native artifact is not a PE image",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const offset = bytes.readUInt32LE(0x3c);
|
||||||
|
if (
|
||||||
|
offset > bytes.length - 26 ||
|
||||||
|
bytes.toString("latin1", offset, offset + 4) !== "PE\0\0" ||
|
||||||
|
bytes.readUInt16LE(offset + 24) !== 0x20b
|
||||||
|
)
|
||||||
|
return err(
|
||||||
|
new WindowsNativeArtifactError(
|
||||||
|
"Windows native artifact is not a Windows x64 PE32+ image",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const coff = readPeCoffHeader(bytes.subarray(offset + 4, offset + 24));
|
||||||
|
if (!coff.ok || coff.value.machine !== 0x8664)
|
||||||
|
return err(
|
||||||
|
new WindowsNativeArtifactError(
|
||||||
|
"Windows native artifact is not a Windows x64 PE32+ image",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return ok(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Bind the parsed manifest to its exact bytes before native code can load. */
|
||||||
|
export const validateWindowsNativeArtifact = (
|
||||||
|
manifest: WindowsNativeManifest,
|
||||||
|
bytes: Buffer,
|
||||||
|
): Result<void, WindowsNativeArtifactError> => {
|
||||||
|
if (
|
||||||
|
createHash("sha256").update(bytes).digest("hex") !== manifest.artifactSha256
|
||||||
|
)
|
||||||
|
return err(
|
||||||
|
new WindowsNativeArtifactError(
|
||||||
|
"Windows native artifact SHA-256 does not match package metadata",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return validateWindowsX64Artifact(bytes);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Produce metadata only for a Windows x64 PE32+ artifact. */
|
||||||
|
export const createWindowsNativeManifest = (
|
||||||
|
packageVersion: string,
|
||||||
|
bytes: Buffer,
|
||||||
|
): Result<WindowsNativeManifest, WindowsNativeArtifactError> => {
|
||||||
|
const valid = validateWindowsX64Artifact(bytes);
|
||||||
|
if (!valid.ok) return valid;
|
||||||
|
return parseWindowsNativeManifest(
|
||||||
|
{
|
||||||
|
...WINDOWS_NATIVE_ARTIFACT,
|
||||||
|
packageVersion,
|
||||||
|
artifactSha256: createHash("sha256").update(bytes).digest("hex"),
|
||||||
|
},
|
||||||
|
packageVersion,
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { readPeCoffHeader } from "../domain/peCoffHeader.js";
|
|
||||||
import { createHash } from "node:crypto";
|
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
@@ -7,6 +5,11 @@ import { fileURLToPath } from "node:url";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { PACKAGE_METADATA } from "../generatedPackageMetadata.js";
|
import { PACKAGE_METADATA } from "../generatedPackageMetadata.js";
|
||||||
|
import {
|
||||||
|
WINDOWS_NATIVE_ARTIFACT,
|
||||||
|
parseWindowsNativeManifest,
|
||||||
|
validateWindowsNativeArtifact,
|
||||||
|
} from "../domain/windowsNativeArtifact.js";
|
||||||
|
|
||||||
/** A native resource remains opaque until the addon validates its object tag. */
|
/** A native resource remains opaque until the addon validates its object tag. */
|
||||||
export const windowsHandleSchema = z.custom<object>(
|
export const windowsHandleSchema = z.custom<object>(
|
||||||
@@ -25,21 +28,10 @@ export const windowsFileIdentitySchema = z.strictObject({
|
|||||||
directory: z.boolean(),
|
directory: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Metadata bound to one packaged Windows x64 Node-API artifact. */
|
|
||||||
export const windowsNativeManifestSchema = z.strictObject({
|
|
||||||
packageVersion: z.string(),
|
|
||||||
platform: z.literal("win32"),
|
|
||||||
architecture: z.literal("x64"),
|
|
||||||
abiVersion: z.literal(1),
|
|
||||||
nodeApiVersion: z.literal(8),
|
|
||||||
artifact: z.literal("rea-windows-x64.node"),
|
|
||||||
artifactSha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
||||||
});
|
|
||||||
|
|
||||||
const inspectionSchema = z.strictObject({
|
const inspectionSchema = z.strictObject({
|
||||||
abiVersion: z.literal(1),
|
abiVersion: z.literal(WINDOWS_NATIVE_ARTIFACT.abiVersion),
|
||||||
nodeApiVersion: z.literal(8),
|
nodeApiVersion: z.literal(WINDOWS_NATIVE_ARTIFACT.nodeApiVersion),
|
||||||
architecture: z.literal("x64"),
|
architecture: z.literal(WINDOWS_NATIVE_ARTIFACT.architecture),
|
||||||
filesystem: windowsFileIdentitySchema,
|
filesystem: windowsFileIdentitySchema,
|
||||||
privateDacl: z.literal(true),
|
privateDacl: z.literal(true),
|
||||||
privateBridgeDescriptorAccess: z.literal(true),
|
privateBridgeDescriptorAccess: z.literal(true),
|
||||||
@@ -126,29 +118,27 @@ export const loadWindowsNativeAuthority = (
|
|||||||
reason: "REA Windows native controls require a Windows x64 host.",
|
reason: "REA Windows native controls require a Windows x64 host.",
|
||||||
};
|
};
|
||||||
const nodeApiVersion = Number(host.nodeApiVersion);
|
const nodeApiVersion = Number(host.nodeApiVersion);
|
||||||
if (!Number.isSafeInteger(nodeApiVersion) || nodeApiVersion < 8)
|
if (
|
||||||
|
!Number.isSafeInteger(nodeApiVersion) ||
|
||||||
|
nodeApiVersion < WINDOWS_NATIVE_ARTIFACT.nodeApiVersion
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
available: false,
|
available: false,
|
||||||
failureStage: "host",
|
failureStage: "host",
|
||||||
reason: "REA Windows native controls require Node-API 8 or newer.",
|
reason: "REA Windows native controls require Node-API 8 or newer.",
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const manifest = windowsNativeManifestSchema.parse(
|
const manifest = parseWindowsNativeManifest(
|
||||||
readPackagedWindowsNativeFile("manifest.json", () => host.readManifest()),
|
readPackagedWindowsNativeFile("manifest.json", () => host.readManifest()),
|
||||||
|
host.packageVersion,
|
||||||
);
|
);
|
||||||
if (manifest.packageVersion !== host.packageVersion)
|
if (!manifest.ok) throw manifest.error;
|
||||||
throw new Error("Windows native artifact belongs to another REA version");
|
const artifact = readPackagedWindowsNativeFile(
|
||||||
const artifact = readPackagedWindowsNativeFile("rea-windows-x64.node", () =>
|
WINDOWS_NATIVE_ARTIFACT.artifact,
|
||||||
host.readArtifact(),
|
() => host.readArtifact(),
|
||||||
);
|
);
|
||||||
if (
|
const valid = validateWindowsNativeArtifact(manifest.value, artifact);
|
||||||
createHash("sha256").update(artifact).digest("hex") !==
|
if (!valid.ok) throw valid.error;
|
||||||
manifest.artifactSha256
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
"Windows native artifact SHA-256 does not match package metadata",
|
|
||||||
);
|
|
||||||
assertWindowsX64Artifact(artifact);
|
|
||||||
let loadedModule: unknown;
|
let loadedModule: unknown;
|
||||||
try {
|
try {
|
||||||
loadedModule = host.loadArtifact();
|
loadedModule = host.loadArtifact();
|
||||||
@@ -189,21 +179,6 @@ export const loadWindowsNativeAuthority = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const assertWindowsX64Artifact = (bytes: Buffer): void => {
|
|
||||||
if (bytes.length < 64 || bytes.toString("latin1", 0, 2) !== "MZ")
|
|
||||||
throw new Error("Windows native artifact is not a PE image");
|
|
||||||
const offset = bytes.readUInt32LE(0x3c);
|
|
||||||
if (
|
|
||||||
offset > bytes.length - 26 ||
|
|
||||||
bytes.toString("latin1", offset, offset + 4) !== "PE\0\0" ||
|
|
||||||
bytes.readUInt16LE(offset + 24) !== 0x20b
|
|
||||||
)
|
|
||||||
throw new Error("Windows native artifact is not a Windows x64 PE32+ image");
|
|
||||||
const coff = readPeCoffHeader(bytes.subarray(offset + 4, offset + 24));
|
|
||||||
if (!coff.ok || coff.value.machine !== 0x8664)
|
|
||||||
throw new Error("Windows native artifact is not a Windows x64 PE32+ image");
|
|
||||||
};
|
|
||||||
|
|
||||||
let cached: WindowsNativeLoadResult | undefined;
|
let cached: WindowsNativeLoadResult | undefined;
|
||||||
|
|
||||||
/** Cache stable admission; let a later query retry failed owned inspection. */
|
/** Cache stable admission; let a later query retry failed owned inspection. */
|
||||||
|
|||||||
@@ -738,6 +738,19 @@ it.each([
|
|||||||
"src/windows/Example.ts",
|
"src/windows/Example.ts",
|
||||||
{ code: true, runtime: true, package: true, apple: false, evm: false },
|
{ code: true, runtime: true, package: true, apple: false, evm: false },
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"src/domain/windowsNativeArtifact.ts",
|
||||||
|
{ code: true, runtime: true, package: true },
|
||||||
|
],
|
||||||
|
["src/domain/peCoffHeader.ts", { code: true, runtime: true, package: true }],
|
||||||
|
[
|
||||||
|
"scripts/lib/verifier-source-loader.mjs",
|
||||||
|
{ code: true, runtime: true, package: true },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"tests/boundary/process/windowsNativeArtifactVerifier.test.ts",
|
||||||
|
{ code: true, package: true },
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"src/domain/javascript/javascriptRuntimeStaticCandidates.ts",
|
"src/domain/javascript/javascriptRuntimeStaticCandidates.ts",
|
||||||
{ code: true, runtime: true, package: true, javascript_recovery: true },
|
{ code: true, runtime: true, package: true, javascript_recovery: true },
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { expect } from "vitest";
|
||||||
|
import { processTest } from "../../support/process/processFixture.js";
|
||||||
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||||
|
|
||||||
|
const repository = fileURLToPath(new URL("../../..", import.meta.url));
|
||||||
|
|
||||||
|
for (const mode of ["preinstall source", "packed source-free"]) {
|
||||||
|
processTest(
|
||||||
|
`verifies native artifact identity from ${mode} without installed dependencies`,
|
||||||
|
async ({ processes }) => {
|
||||||
|
const root = await createTestTempDirectory(
|
||||||
|
"rea-native-artifact-verifier-",
|
||||||
|
);
|
||||||
|
const paths =
|
||||||
|
mode === "preinstall source"
|
||||||
|
? [
|
||||||
|
"scripts/lib/verifier-source-loader.mjs",
|
||||||
|
"src/domain/windowsNativeArtifact.ts",
|
||||||
|
"src/domain/peCoffHeader.ts",
|
||||||
|
"src/domain/result.ts",
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
"dist/domain/windowsNativeArtifact.js",
|
||||||
|
"dist/domain/peCoffHeader.js",
|
||||||
|
"dist/domain/result.js",
|
||||||
|
];
|
||||||
|
for (const path of [
|
||||||
|
"scripts/verify-windows-native-artifact.mjs",
|
||||||
|
...paths,
|
||||||
|
]) {
|
||||||
|
await mkdir(dirname(join(root, path)), { recursive: true });
|
||||||
|
await copyFile(join(repository, path), join(root, path));
|
||||||
|
}
|
||||||
|
await writeFile(
|
||||||
|
join(root, "package.json"),
|
||||||
|
JSON.stringify({ type: "module", version: "1.0.0" }),
|
||||||
|
);
|
||||||
|
const artifactRoot = join(root, "native/windows/build");
|
||||||
|
await mkdir(artifactRoot, { recursive: true });
|
||||||
|
// An inert PE header establishes artifact validation, never addon loading.
|
||||||
|
const bytes = Buffer.alloc(128);
|
||||||
|
bytes.write("MZ");
|
||||||
|
bytes.writeUInt32LE(64, 0x3c);
|
||||||
|
bytes.write("PE\0\0", 64, "ascii");
|
||||||
|
bytes.writeUInt16LE(0x8664, 68);
|
||||||
|
bytes.writeUInt16LE(0x20b, 88);
|
||||||
|
const manifest = {
|
||||||
|
packageVersion: "1.0.0",
|
||||||
|
platform: "win32",
|
||||||
|
architecture: "x64",
|
||||||
|
abiVersion: 1,
|
||||||
|
nodeApiVersion: 8,
|
||||||
|
artifact: "rea-windows-x64.node",
|
||||||
|
artifactSha256: createHash("sha256").update(bytes).digest("hex"),
|
||||||
|
};
|
||||||
|
await writeFile(join(artifactRoot, manifest.artifact), bytes);
|
||||||
|
const manifestPath = join(artifactRoot, "manifest.json");
|
||||||
|
await writeFile(manifestPath, JSON.stringify(manifest));
|
||||||
|
const run = () =>
|
||||||
|
processes.run(
|
||||||
|
process.execPath,
|
||||||
|
[join(root, "scripts/verify-windows-native-artifact.mjs")],
|
||||||
|
{ cwd: root, timeoutMs: 10_000 },
|
||||||
|
);
|
||||||
|
const valid = await run();
|
||||||
|
expect(valid.exitCode, valid.stderr).toBe(0);
|
||||||
|
expect(JSON.parse(valid.stdout)).toEqual({
|
||||||
|
ok: true,
|
||||||
|
...manifest,
|
||||||
|
artifactBytes: bytes.length,
|
||||||
|
});
|
||||||
|
await writeFile(
|
||||||
|
manifestPath,
|
||||||
|
JSON.stringify({ ...manifest, extra: true }),
|
||||||
|
);
|
||||||
|
const invalid = await run();
|
||||||
|
expect(invalid.exitCode).not.toBe(0);
|
||||||
|
expect(invalid.stderr).toContain("missing or unknown fields");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user