Files
FreeCADWasmDev/tools/run_web_upgrade_incompatible.mjs
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

97 lines
5.3 KiB
JavaScript

import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
const toolsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
const projectRoot = path.resolve(toolsRoot, "..");
const docsRoot = path.join(projectRoot, "docs");
const packageRoot = path.join(projectRoot, "web", "offline-package");
const fcstdSource = path.join(projectRoot, "freecad-source", "build-web", "configure", "bin", "WebBox.FCStd");
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-web-upgrade-incompatible-"));
const activeRoot = path.join(tempRoot, "active");
const candidateRoot = path.join(tempRoot, "candidate");
const opfsRoot = path.join(tempRoot, "opfs");
function canonical(value) { return `${JSON.stringify(value, null, 2)}\n`; }
function sha256(filePath) { return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex"); }
function updateManifest(packageDir) {
const manifestPath = path.join(packageDir, "manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const versionPath = path.join(packageDir, "version.json");
const versionEntry = manifest.files.find((entry) => entry.path === "version.json");
versionEntry.size = fs.statSync(versionPath).size;
versionEntry.sha256 = sha256(versionPath);
manifest.manifestFile.selfSha256 = null;
manifest.manifestFile.selfSha256 = crypto.createHash("sha256").update(canonical(manifest), "utf8").digest("hex");
fs.writeFileSync(manifestPath, canonical(manifest));
}
function validatePackage(packageDir) {
const manifest = JSON.parse(fs.readFileSync(path.join(packageDir, "manifest.json"), "utf8"));
const canonicalManifest = { ...manifest, manifestFile: { ...manifest.manifestFile, selfSha256: null } };
const selfHash = crypto.createHash("sha256").update(canonical(canonicalManifest), "utf8").digest("hex");
if (selfHash !== manifest.manifestFile.selfSha256) throw new Error("manifest self hash mismatch");
for (const entry of manifest.files) {
const file = path.join(packageDir, entry.path);
if (!fs.existsSync(file) || fs.statSync(file).size !== entry.size || sha256(file) !== entry.sha256) throw new Error(`manifest hash mismatch: ${entry.path}`);
}
return { manifest, version: JSON.parse(fs.readFileSync(path.join(packageDir, "version.json"), "utf8")) };
}
fs.cpSync(packageRoot, activeRoot, { recursive: true });
fs.cpSync(packageRoot, candidateRoot, { recursive: true });
const candidateVersionPath = path.join(candidateRoot, "version.json");
const candidateVersion = JSON.parse(fs.readFileSync(candidateVersionPath, "utf8"));
candidateVersion.packageVersion = "0.2.0";
candidateVersion.schema = "freecad-web-offline-package-version/v999";
fs.writeFileSync(candidateVersionPath, canonical(candidateVersion));
updateManifest(candidateRoot);
const active = validatePackage(activeRoot);
const candidate = validatePackage(candidateRoot);
fs.mkdirSync(opfsRoot, { recursive: true });
const documentPath = path.join(opfsRoot, "WebBox.FCStd");
fs.copyFileSync(fcstdSource, documentPath);
const before = {
activeVersion: active.version.packageVersion,
activeManifestSha256: sha256(path.join(activeRoot, "manifest.json")),
documentSha256: sha256(documentPath),
};
fs.writeFileSync(path.join(opfsRoot, "metadata.json"), canonical(before));
let rejected = false;
let rejectionReason = "";
try {
if (candidate.version.schema !== "freecad-web-offline-package-version/v1") throw new Error(`incompatible version schema: ${candidate.version.schema}`);
throw new Error("candidate unexpectedly accepted");
} catch (error) {
rejected = true;
rejectionReason = error instanceof Error ? error.message : String(error);
}
const after = {
activeVersion: active.version.packageVersion,
activeManifestSha256: sha256(path.join(activeRoot, "manifest.json")),
documentSha256: sha256(documentPath),
};
const result = {
schema: "freecad-web-upgrade-incompatible/v1",
browserPolicy: "Chrome only; no Firefox execution",
activePackage: { version: active.version.packageVersion, manifestSha256: before.activeManifestSha256, valid: true },
candidatePackage: { version: candidate.version.packageVersion, versionSchema: candidate.version.schema, manifestValid: true },
decision: { rejected, reason: rejectionReason, replacementAttempted: false },
opfsMetadata: { before, after },
fcstd: { sha256Before: before.documentSha256, sha256After: after.documentSha256, unchanged: before.documentSha256 === after.documentSha256 },
summary: {
rejectedBeforeReplacement: rejected,
activePackageUnchanged: before.activeManifestSha256 === after.activeManifestSha256,
documentUnchanged: before.documentSha256 === after.documentSha256,
result: rejected && before.activeManifestSha256 === after.activeManifestSha256 && before.documentSha256 === after.documentSha256
? "pass_incompatible_version_rejected_previous_preserved"
: "fail_incompatible_version_handling",
},
};
fs.writeFileSync(path.join(docsRoot, "freecad-web-upgrade-incompatible.json"), `${JSON.stringify(result, null, 2)}\n`);
console.log(`candidate ${candidate.version.packageVersion} rejected=${rejected}; active unchanged=${result.summary.activePackageUnchanged}`);
console.log(`result=${result.summary.result}`);
if (result.summary.result.startsWith("fail")) process.exitCode = 1;