zig/lib/build-web/main.js
2025-08-09 18:48:15 -04:00

346 lines
12 KiB
JavaScript

const domConnectionStatus = document.getElementById("connectionStatus");
const domFirefoxWebSocketBullshitExplainer = document.getElementById("firefoxWebSocketBullshitExplainer");
const domMain = document.getElementsByTagName("main")[0];
const domSummary = {
stepCount: document.getElementById("summaryStepCount"),
status: document.getElementById("summaryStatus"),
};
const domButtonRebuild = document.getElementById("buttonRebuild");
const domStepList = document.getElementById("stepList");
let domSteps = [];
let wasm_promise = fetch("main.wasm");
let wasm_exports = null;
const text_decoder = new TextDecoder();
const text_encoder = new TextEncoder();
domButtonRebuild.addEventListener("click", () => wasm_exports.rebuild());
setConnectionStatus("Loading WebAssembly...", false);
WebAssembly.instantiateStreaming(wasm_promise, {
core: {
log: function(ptr, len) {
const msg = decodeString(ptr, len);
console.log(msg);
},
panic: function (ptr, len) {
const msg = decodeString(ptr, len);
throw new Error("panic: " + msg);
},
timestamp: function () {
return BigInt(new Date());
},
hello: hello,
updateBuildStatus: updateBuildStatus,
updateStepStatus: updateStepStatus,
sendWsMessage: (ptr, len) => ws.send(new Uint8Array(wasm_exports.memory.buffer, ptr, len)),
},
fuzz: {
requestSources: fuzzRequestSources,
ready: fuzzReady,
updateStats: fuzzUpdateStats,
updateEntryPoints: fuzzUpdateEntryPoints,
updateSource: fuzzUpdateSource,
updateCoverage: fuzzUpdateCoverage,
},
time_report: {
updateCompile: timeReportUpdateCompile,
updateGeneric: timeReportUpdateGeneric,
},
}).then(function(obj) {
setConnectionStatus("Connecting to WebSocket...", true);
connectWebSocket();
wasm_exports = obj.instance.exports;
window.wasm = obj; // for debugging
});
function connectWebSocket() {
const host = document.location.host;
const pathname = document.location.pathname;
const isHttps = document.location.protocol === 'https:';
const match = host.match(/^(.+):(\d+)$/);
const defaultPort = isHttps ? 443 : 80;
const port = match ? parseInt(match[2], 10) : defaultPort;
const hostName = match ? match[1] : host;
const wsProto = isHttps ? "wss:" : "ws:";
const wsUrl = wsProto + '//' + hostName + ':' + port + pathname;
ws = new WebSocket(wsUrl);
ws.binaryType = "arraybuffer";
ws.addEventListener('message', onWebSocketMessage, false);
ws.addEventListener('error', onWebSocketClose, false);
ws.addEventListener('close', onWebSocketClose, false);
ws.addEventListener('open', onWebSocketOpen, false);
}
function onWebSocketOpen() {
setConnectionStatus("Waiting for data...", false);
}
function onWebSocketMessage(ev) {
const jsArray = new Uint8Array(ev.data);
const ptr = wasm_exports.message_begin(jsArray.length);
const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, jsArray.length);
wasmArray.set(jsArray);
wasm_exports.message_end();
}
function onWebSocketClose() {
setConnectionStatus("WebSocket connection closed. Re-connecting...", true);
ws.removeEventListener('message', onWebSocketMessage, false);
ws.removeEventListener('error', onWebSocketClose, false);
ws.removeEventListener('close', onWebSocketClose, false);
ws.removeEventListener('open', onWebSocketOpen, false);
ws = null;
setTimeout(connectWebSocket, 1000);
}
function setConnectionStatus(msg, is_websocket_connect) {
domConnectionStatus.textContent = msg;
if (msg.length > 0) {
domConnectionStatus.classList.remove("hidden");
domMain.classList.add("hidden");
} else {
domConnectionStatus.classList.add("hidden");
domMain.classList.remove("hidden");
}
if (is_websocket_connect) {
domFirefoxWebSocketBullshitExplainer.classList.remove("hidden");
} else {
domFirefoxWebSocketBullshitExplainer.classList.add("hidden");
}
}
function hello(
steps_len,
build_status,
time_report,
) {
domSummary.stepCount.textContent = steps_len;
updateBuildStatus(build_status);
setConnectionStatus("", false);
{
let entries = [];
for (let i = 0; i < steps_len; i += 1) {
const step_name = unwrapString(wasm_exports.stepName(i));
const code = document.createElement("code");
code.textContent = step_name;
const li = document.createElement("li");
li.appendChild(code);
entries.push(li);
}
domStepList.replaceChildren(...entries);
for (let i = 0; i < steps_len; i += 1) {
updateStepStatus(i);
}
}
if (time_report) timeReportReset(steps_len);
fuzzReset();
}
function updateBuildStatus(s) {
let text;
let active = false;
let reset_time_reports = false;
if (s == 0) {
text = "Idle";
} else if (s == 1) {
text = "Watching for changes...";
} else if (s == 2) {
text = "Running...";
active = true;
reset_time_reports = true;
} else if (s == 3) {
text = "Starting fuzzer...";
active = true;
} else {
console.log(`bad build status: ${s}`);
}
domSummary.status.textContent = text;
if (active) {
domSummary.status.classList.add("status-running");
domSummary.status.classList.remove("status-idle");
domButtonRebuild.disabled = true;
} else {
domSummary.status.classList.remove("status-running");
domSummary.status.classList.add("status-idle");
domButtonRebuild.disabled = false;
}
if (reset_time_reports) {
// Grey out and collapse all the time reports
for (const time_report_host of domTimeReportList.children) {
const details = time_report_host.shadowRoot.querySelector(":host > details");
details.classList.add("pending");
details.open = false;
}
}
}
function updateStepStatus(step_idx) {
const li = domStepList.children[step_idx];
const step_status = wasm_exports.stepStatus(step_idx);
li.classList.remove("step-wip", "step-success", "step-failure");
if (step_status == 0) {
// pending
} else if (step_status == 1) {
li.classList.add("step-wip");
} else if (step_status == 2) {
li.classList.add("step-success");
} else if (step_status == 3) {
li.classList.add("step-failure");
} else {
console.log(`bad step status: ${step_status}`);
}
}
function decodeString(ptr, len) {
if (len === 0) return "";
return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
}
function getU32Array(ptr, len) {
if (len === 0) return new Uint32Array();
return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
}
function unwrapString(bigint) {
const ptr = Number(bigint & 0xffffffffn);
const len = Number(bigint >> 32n);
return decodeString(ptr, len);
}
const time_report_entry_template = document.getElementById("timeReportEntryTemplate").content;
const domTimeReport = document.getElementById("timeReport");
const domTimeReportList = document.getElementById("timeReportList");
function timeReportReset(steps_len) {
let entries = [];
for (let i = 0; i < steps_len; i += 1) {
const step_name = unwrapString(wasm_exports.stepName(i));
const host = document.createElement("div");
const shadow = host.attachShadow({ mode: "open" });
shadow.appendChild(time_report_entry_template.cloneNode(true));
shadow.querySelector(":host > details").classList.add("pending");
const slotted_name = document.createElement("code");
slotted_name.setAttribute("slot", "step-name");
slotted_name.textContent = step_name;
host.appendChild(slotted_name);
entries.push(host);
}
domTimeReportList.replaceChildren(...entries);
domTimeReport.classList.remove("hidden");
}
function timeReportUpdateCompile(
step_idx,
inner_html_ptr,
inner_html_len,
file_table_html_ptr,
file_table_html_len,
decl_table_html_ptr,
decl_table_html_len,
use_llvm,
) {
const inner_html = decodeString(inner_html_ptr, inner_html_len);
const file_table_html = decodeString(file_table_html_ptr, file_table_html_len);
const decl_table_html = decodeString(decl_table_html_ptr, decl_table_html_len);
const host = domTimeReportList.children.item(step_idx);
const shadow = host.shadowRoot;
shadow.querySelector(":host > details").classList.remove("pending", "no-llvm");
shadow.getElementById("genericReport").classList.add("hidden");
shadow.getElementById("compileReport").classList.remove("hidden");
if (!use_llvm) shadow.querySelector(":host > details").classList.add("no-llvm");
host.innerHTML = inner_html;
shadow.getElementById("fileTableBody").innerHTML = file_table_html;
shadow.getElementById("declTableBody").innerHTML = decl_table_html;
}
function timeReportUpdateGeneric(
step_idx,
inner_html_ptr,
inner_html_len,
) {
const inner_html = decodeString(inner_html_ptr, inner_html_len);
const host = domTimeReportList.children.item(step_idx);
const shadow = host.shadowRoot;
shadow.querySelector(":host > details").classList.remove("pending", "no-llvm");
shadow.getElementById("genericReport").classList.remove("hidden");
shadow.getElementById("compileReport").classList.add("hidden");
host.innerHTML = inner_html;
}
const fuzz_entry_template = document.getElementById("fuzzEntryTemplate").content;
const domFuzz = document.getElementById("fuzz");
const domFuzzStatus = document.getElementById("fuzzStatus");
const domFuzzEntries = document.getElementById("fuzzEntries");
let domFuzzInstance = null;
function fuzzRequestSources() {
domFuzzStatus.classList.remove("hidden");
domFuzzStatus.textContent = "Loading sources tarball...";
fetch("sources.tar").then(function(response) {
if (!response.ok) throw new Error("unable to download sources");
domFuzzStatus.textContent = "Parsing fuzz test sources...";
return response.arrayBuffer();
}).then(function(buffer) {
if (buffer.length === 0) throw new Error("sources.tar was empty");
const js_array = new Uint8Array(buffer);
const ptr = wasm_exports.alloc(js_array.length);
const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
wasm_array.set(js_array);
wasm_exports.fuzzUnpackSources(ptr, js_array.length);
domFuzzStatus.textContent = "";
domFuzzStatus.classList.add("hidden");
});
}
function fuzzReady() {
domFuzz.classList.remove("hidden");
// TODO: multiple fuzzer instances
if (domFuzzInstance !== null) return;
const host = document.createElement("div");
const shadow = host.attachShadow({ mode: "open" });
shadow.appendChild(fuzz_entry_template.cloneNode(true));
domFuzzInstance = host;
domFuzzEntries.appendChild(host);
}
function fuzzReset() {
domFuzz.classList.add("hidden");
domFuzzEntries.replaceChildren();
domFuzzInstance = null;
}
function fuzzUpdateStats(stats_html_ptr, stats_html_len) {
if (domFuzzInstance === null) throw new Error("fuzzUpdateStats called when fuzzer inactive");
const stats_html = decodeString(stats_html_ptr, stats_html_len);
const host = domFuzzInstance;
host.innerHTML = stats_html;
}
function fuzzUpdateEntryPoints(entry_points_html_ptr, entry_points_html_len) {
if (domFuzzInstance === null) throw new Error("fuzzUpdateEntryPoints called when fuzzer inactive");
const entry_points_html = decodeString(entry_points_html_ptr, entry_points_html_len);
const domEntryPointList = domFuzzInstance.shadowRoot.getElementById("entryPointList");
domEntryPointList.innerHTML = entry_points_html;
}
function fuzzUpdateSource(source_html_ptr, source_html_len) {
if (domFuzzInstance === null) throw new Error("fuzzUpdateSource called when fuzzer inactive");
const source_html = decodeString(source_html_ptr, source_html_len);
const domSourceText = domFuzzInstance.shadowRoot.getElementById("sourceText");
domSourceText.innerHTML = source_html;
domFuzzInstance.shadowRoot.getElementById("source").classList.remove("hidden");
}
function fuzzUpdateCoverage(covered_ptr, covered_len) {
if (domFuzzInstance === null) throw new Error("fuzzUpdateCoverage called when fuzzer inactive");
const shadow = domFuzzInstance.shadowRoot;
const domSourceText = shadow.getElementById("sourceText");
const covered = getU32Array(covered_ptr, covered_len);
for (let i = 0; i < domSourceText.children.length; i += 1) {
const childDom = domSourceText.children[i];
if (childDom.id != null && childDom.id[0] == "l") {
childDom.classList.add("l");
childDom.classList.remove("c");
}
}
for (const sli of covered) {
shadow.getElementById(`l${sli}`).classList.add("c");
}
}