← Files ModRetro Chromatic PluginARCHIVED FILE
dist/bundled-template.js
5.18 KB · Oct 2, 2026 · 00:37 UTC
import { createHash } from "node:crypto";
import { constants } from "node:fs";
import { lstat, open, readdir, realpath } from "node:fs/promises";
import path from "node:path";
import { GameStudioProjectError } from "./project.js";
// This is the owner-delivered source snapshot, not a promised rebuilt ROM hash.
const MANIFEST = {
path: "MANIFEST.json", bytes: 228142,
sha256: "239d182ec218f5bc4145f58dc3c4ed3ec5ad3fc1425356c63c79269b659f9cdc",
};
const CHECKSUM = {
path: "MANIFEST.sha256", bytes: 80,
sha256: "1a8ee3c5d6b28fe2ea75f5d889c65c9e60161bf96d8dd52028f3812d1bf591b1",
};
const stable = (a, b) => a.dev === b.dev && a.ino === b.ino && a.mode === b.mode && a.nlink === b.nlink &&
a.uid === b.uid && a.gid === b.gid && a.size === b.size &&
a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
/** Capture the exact trusted bytes once; later copying never rereads a changed source. */
export async function readWrecklightTemplate(root) {
const files = new Map();
const identities = new Map();
const fail = (name) => {
throw new GameStudioProjectError("INVALID_PROJECT", `The Wrecklight template is missing, changed or unsafe: ${name}. Use the matching reviewed source template before creating a remix.`, path.join(root, name));
};
async function identity(name, directory) {
const filename = path.join(root, name);
const details = await lstat(filename);
if (await realpath(filename) !== filename || details.isSymbolicLink() ||
(directory ? !details.isDirectory() : !details.isFile() || details.nlink !== 1) ||
(typeof process.getuid === "function" && details.uid !== process.getuid()) ||
(process.platform !== "win32" && (directory ? (details.mode & 0o022) !== 0 : (details.mode & 0o777) !== 0o644)))
fail(name);
return details;
}
async function capture(member) {
const before = await identity(member.path, false);
identities.set(member.path, before);
if (before.size !== member.bytes || before.size > 16 * 1024 * 1024)
fail(member.path);
// A raced replacement with a FIFO must reach the identity check without waiting for a writer.
const handle = await open(path.join(root, member.path), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
try {
if (!stable(before, await handle.stat()))
fail(member.path);
// Bound the read itself, not just the size observed before opening.
const buffer = Buffer.alloc(member.bytes + 1);
let total = 0;
while (total < buffer.length) {
const { bytesRead } = await handle.read(buffer, total, buffer.length - total, null);
if (bytesRead === 0)
break;
total += bytesRead;
}
const bytes = buffer.subarray(0, total);
if (bytes.length !== member.bytes || createHash("sha256").update(bytes).digest("hex") !== member.sha256 ||
!stable(before, await handle.stat()))
fail(member.path);
files.set(member.path, bytes);
}
finally {
await handle.close();
}
}
identities.set("", await identity("", true));
await capture(MANIFEST);
const manifest = JSON.parse(files.get(MANIFEST.path).toString("utf8"));
if (manifest.schema !== "wrecklight-source-sample-v1" || manifest.fileCount !== 960 ||
manifest.totalBytes !== 11706594 || manifest.files.length !== 960)
fail(MANIFEST.path);
const members = new Map();
const directories = new Set([""]);
for (const member of [...manifest.files, MANIFEST, CHECKSUM]) {
if (members.has(member.path) || member.path.includes("\\") || member.path.split("/").some((part) => !part || part === "." || part === ".."))
fail(member.path);
members.set(member.path, member);
let parent = path.posix.dirname(member.path);
while (parent !== ".") {
directories.add(parent);
parent = path.posix.dirname(parent);
}
}
async function visit(relative) {
if (!identities.has(relative))
identities.set(relative, await identity(relative, true));
const entries = await readdir(path.join(root, relative), { withFileTypes: true });
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
const name = relative ? `${relative}/${entry.name}` : entry.name;
if (entry.isDirectory() && directories.has(name))
await visit(name);
else {
const member = members.get(name);
if (!member || !entry.isFile())
return fail(name);
if (!files.has(name))
await capture(member);
}
}
}
await visit("");
if (files.size !== members.size || identities.size !== members.size + directories.size)
fail("member inventory");
for (const [name, before] of identities) {
if (!stable(before, await identity(name, directories.has(name))))
fail(name);
}
return files;
}
//# sourceMappingURL=bundled-template.js.mapSHA-256: b2b5669438d1676ca871d7af480b232fe0328269abad968d447e234f5ca481c8