Files
workinf_Blender_Wasm/tools/web/fuzz-case-runner.mjs
mes123456 380cbed4ff
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Checkpoint web parity through Chromium input tasks
2026-08-19 10:39:03 -04:00

109 lines
7.3 KiB
JavaScript

import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import { fileURLToPath, pathToFileURL } from "node:url";
import ts from "../../web/node_modules/typescript/lib/typescript.js";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const domain = process.argv[2];
const seed = Number.parseInt(process.argv[3] ?? "0", 10) >>> 0;
const iteration = Number.parseInt(process.argv[4] ?? "0", 10);
if (!["blend", "image", "font", "node", "manifest"].includes(domain) || !Number.isSafeInteger(iteration) || iteration < 0) process.exit(64);
let state = (seed ^ Math.imul(iteration + 1, 0x9e3779b1)) >>> 0;
const random = () => { state ^= state << 13; state ^= state >>> 17; state ^= state << 5; return state >>> 0; };
const mutate = (source) => {
let bytes = Buffer.from(source);
if (iteration % 7 === 0) bytes = bytes.subarray(0, Math.max(0, Math.min(bytes.length, random() % Math.max(1, bytes.length))));
else for (let index = 0; index < 1 + iteration % 8 && bytes.length > 0; index++) bytes[random() % bytes.length] ^= 1 << (random() % 8);
return bytes;
};
const digest = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), `m13-fuzz-${domain}-`));
const transpile = (name, replacements = []) => {
const sourcePath = path.join(root, "web/protocol", `${name}.ts`);
const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
if (result.diagnostics?.length) throw new Error(`TRANSPILE_FAILED:${name}`);
const output = replacements.reduce((value, [from, to]) => value.replaceAll(from, to), result.outputText);
fs.writeFileSync(path.join(temporary, `${name}.mjs`), output);
};
const result = (status, code) => process.stdout.write(`${JSON.stringify({ domain, seed, iteration, status, code })}\n`);
try {
if (domain === "blend") {
const [{ default: factory }, wasmBinary, source] = await Promise.all([
import(pathToFileURL(path.join(root, "web/app/src/vendor/blender/web_engine.js")).href),
fs.promises.readFile(path.join(root, "web/app/src/vendor/blender/web_engine.wasm")),
fs.promises.readFile(path.join(root, "tests/files/web/basic_scene.blend")),
]);
const bytes = mutate(source);
const engine = await factory({ wasmBinary });
const handle = engine._web_engine_create();
let pointer = 0;
try {
if (bytes.length) { pointer = engine._malloc(bytes.length); engine.HEAPU8.set(bytes, pointer); }
const code = engine._web_engine_open_blend(handle, pointer, bytes.length);
result(code === 0 ? "ACCEPTED" : "REJECTED", code === 0 ? "OK" : "BLEND_OPEN_INVALID");
} finally {
if (pointer) engine._free(pointer);
engine._web_engine_destroy(handle);
}
} else if (domain === "image") {
transpile("asset-preview");
transpile("asset-preview-decode", [['from "./asset-preview"', 'from "./asset-preview.mjs"']]);
const identityProtocol = await import(pathToFileURL(path.join(temporary, "asset-preview.mjs")).href);
const decode = await import(pathToFileURL(path.join(temporary, "asset-preview-decode.mjs")).href);
const source = fs.readFileSync(path.join(root, "tests/golden/M12-02B/preview.png"));
const bytes = mutate(source);
const original = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/identity.json"), "utf8"));
const value = { ...original, content: { ...original.content, byteLength: bytes.length, sha256: digest(bytes) } };
delete value.identitySha256;
const identity = await identityProtocol.createAssetPreviewIdentity(value);
await decode.planAssetPreviewDecode(identity, bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
result("ACCEPTED", "OK");
} else if (domain === "font") {
transpile("asset-path");
transpile("external-vfont", [['from "./asset-path"', 'from "./asset-path.mjs"']]);
const font = await import(pathToFileURL(path.join(temporary, "external-vfont.mjs")).href);
const bytes = mutate(fs.readFileSync(path.join(root, "blender-5.2.0/release/datafiles/bfont.pfb")));
const data = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
await font.validateExternalVFontImport({ sourcePath: "//fonts/fuzz.pfb", mimeType: "application/x-font-type1", byteLength: bytes.length, sha256: digest(bytes), data });
result("ACCEPTED", "OK");
} else if (domain === "node") {
transpile("shader-compiler");
const shader = await import(pathToFileURL(path.join(temporary, "shader-compiler.mjs")).href);
const material = { id: "material:fuzz", name: "fuzz", baseColor: [0.2, 0.3, 0.4, 1], roughness: 0.5, metallic: 0, emissionColor: [0, 0, 0, 1], alpha: 1, ior: 1.45, shaderGraphHash: "a".repeat(64), nodes: [{ id: "principled", type: "PRINCIPLED", name: "Principled" }, { id: "output", type: "OUTPUT", name: "Output" }], links: [{ fromNodeId: "principled", fromSocket: "BSDF", toNodeId: "output", toSocket: "Surface" }] };
switch (iteration % 6) {
case 0: material.nodes.push({ id: `unknown-${random()}`, type: "UNSUPPORTED", name: "Unknown" }); break;
case 1: material.nodes.push({ ...material.nodes[0] }); break;
case 2: material.links.push({ fromNodeId: "output", fromSocket: "Surface", toNodeId: "principled", toSocket: "Base Color" }); break;
case 3: material.shaderGraphHash = digest(Buffer.from(String(random()))).slice(1); break;
case 4: material.nodes[0].id = "x".repeat(4096); break;
default: material.roughness = Number.NaN;
}
const compiled = shader.compileMaterialGraph(material);
result(compiled.status === "BLOCKED" ? "REJECTED" : "ACCEPTED", compiled.issues?.[0]?.code ?? "OK");
} else {
for (const name of ["asset-path", "capability-gates", "scripting-platform"]) transpile(name, [['from "./asset-path"', 'from "./asset-path.mjs"'], ['from "./capability-gates"', 'from "./capability-gates.mjs"']]);
const protocol = await import(pathToFileURL(path.join(temporary, "scripting-platform.mjs")).href);
const script = { id: "fuzz", name: "fuzz", entryPath: "scripts/fuzz.py", sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "local", signature: "b".repeat(128), keyId: "key:local", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false };
const manifest = { schemaVersion: 1, scripts: [script] };
switch (iteration % 6) {
case 0: script.entryPath = `../${random()}.py`; break;
case 1: script.sourceByteLength = Number.MAX_SAFE_INTEGER; break;
case 2: script.permissions = ["UNKNOWN"]; break;
case 3: script.dependencies = [{ id: "fuzz", sourceSha256: "x", sourcePath: "../dep.py" }]; break;
case 4: manifest.schemaVersion = 2; break;
default: script.module = true;
}
protocol.parseScriptingManifest(manifest);
result("ACCEPTED", "OK");
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const code = error?.code ?? message.match(/^([A-Z][A-Z0-9_]+):/u)?.[1] ?? "STRUCTURED_REJECTION";
result("REJECTED", code);
} finally {
fs.rmSync(temporary, { recursive: true, force: true });
}