master
1const domConnectionStatus = document.getElementById("connectionStatus");
2const domFirefoxWebSocketBullshitExplainer = document.getElementById("firefoxWebSocketBullshitExplainer");
3
4const domMain = document.getElementsByTagName("main")[0];
5const domSummary = {
6 stepCount: document.getElementById("summaryStepCount"),
7 status: document.getElementById("summaryStatus"),
8};
9const domButtonRebuild = document.getElementById("buttonRebuild");
10const domStepList = document.getElementById("stepList");
11let domSteps = [];
12
13let wasm_promise = fetch("main.wasm");
14let wasm_exports = null;
15
16const text_decoder = new TextDecoder();
17const text_encoder = new TextEncoder();
18
19domButtonRebuild.addEventListener("click", () => wasm_exports.rebuild());
20
21setConnectionStatus("Loading WebAssembly...", false);
22WebAssembly.instantiateStreaming(wasm_promise, {
23 core: {
24 log: function(ptr, len) {
25 const msg = decodeString(ptr, len);
26 console.log(msg);
27 },
28 panic: function (ptr, len) {
29 const msg = decodeString(ptr, len);
30 throw new Error("panic: " + msg);
31 },
32 timestamp: function () {
33 return BigInt(new Date());
34 },
35 hello: hello,
36 updateBuildStatus: updateBuildStatus,
37 updateStepStatus: updateStepStatus,
38 sendWsMessage: (ptr, len) => ws.send(new Uint8Array(wasm_exports.memory.buffer, ptr, len)),
39 },
40 fuzz: {
41 requestSources: fuzzRequestSources,
42 ready: fuzzReady,
43 updateStats: fuzzUpdateStats,
44 updateEntryPoints: fuzzUpdateEntryPoints,
45 updateSource: fuzzUpdateSource,
46 updateCoverage: fuzzUpdateCoverage,
47 },
48 time_report: {
49 updateGeneric: timeReportUpdateGeneric,
50 updateCompile: timeReportUpdateCompile,
51 updateRunTest: timeReportUpdateRunTest,
52 },
53}).then(function(obj) {
54 setConnectionStatus("Connecting to WebSocket...", true);
55 connectWebSocket();
56
57 wasm_exports = obj.instance.exports;
58 window.wasm = obj; // for debugging
59});
60
61function connectWebSocket() {
62 const host = document.location.host;
63 const pathname = document.location.pathname;
64 const isHttps = document.location.protocol === 'https:';
65 const match = host.match(/^(.+):(\d+)$/);
66 const defaultPort = isHttps ? 443 : 80;
67 const port = match ? parseInt(match[2], 10) : defaultPort;
68 const hostName = match ? match[1] : host;
69 const wsProto = isHttps ? "wss:" : "ws:";
70 const wsUrl = wsProto + '//' + hostName + ':' + port + pathname;
71 ws = new WebSocket(wsUrl);
72 ws.binaryType = "arraybuffer";
73 ws.addEventListener('message', onWebSocketMessage, false);
74 ws.addEventListener('error', onWebSocketClose, false);
75 ws.addEventListener('close', onWebSocketClose, false);
76 ws.addEventListener('open', onWebSocketOpen, false);
77}
78function onWebSocketOpen() {
79 setConnectionStatus("Waiting for data...", false);
80}
81function onWebSocketMessage(ev) {
82 const jsArray = new Uint8Array(ev.data);
83 const ptr = wasm_exports.message_begin(jsArray.length);
84 const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, jsArray.length);
85 wasmArray.set(jsArray);
86 wasm_exports.message_end();
87}
88function onWebSocketClose() {
89 setConnectionStatus("WebSocket connection closed. Re-connecting...", true);
90 ws.removeEventListener('message', onWebSocketMessage, false);
91 ws.removeEventListener('error', onWebSocketClose, false);
92 ws.removeEventListener('close', onWebSocketClose, false);
93 ws.removeEventListener('open', onWebSocketOpen, false);
94 ws = null;
95 setTimeout(connectWebSocket, 1000);
96}
97
98function setConnectionStatus(msg, is_websocket_connect) {
99 domConnectionStatus.textContent = msg;
100 if (msg.length > 0) {
101 domConnectionStatus.classList.remove("hidden");
102 domMain.classList.add("hidden");
103 } else {
104 domConnectionStatus.classList.add("hidden");
105 domMain.classList.remove("hidden");
106 }
107 if (is_websocket_connect) {
108 domFirefoxWebSocketBullshitExplainer.classList.remove("hidden");
109 } else {
110 domFirefoxWebSocketBullshitExplainer.classList.add("hidden");
111 }
112}
113
114function hello(
115 steps_len,
116 build_status,
117 time_report,
118) {
119 domSummary.stepCount.textContent = steps_len;
120 updateBuildStatus(build_status);
121 setConnectionStatus("", false);
122
123 {
124 let entries = [];
125 for (let i = 0; i < steps_len; i += 1) {
126 const step_name = unwrapString(wasm_exports.stepName(i));
127 const code = document.createElement("code");
128 code.textContent = step_name;
129 const li = document.createElement("li");
130 li.appendChild(code);
131 entries.push(li);
132 }
133 domStepList.replaceChildren(...entries);
134 for (let i = 0; i < steps_len; i += 1) {
135 updateStepStatus(i);
136 }
137 }
138
139 if (time_report) timeReportReset(steps_len);
140 fuzzReset();
141}
142
143function updateBuildStatus(s) {
144 let text;
145 let active = false;
146 let reset_time_reports = false;
147 if (s == 0) {
148 text = "Idle";
149 } else if (s == 1) {
150 text = "Watching for changes...";
151 } else if (s == 2) {
152 text = "Running...";
153 active = true;
154 reset_time_reports = true;
155 } else if (s == 3) {
156 text = "Starting fuzzer...";
157 active = true;
158 } else {
159 console.log(`bad build status: ${s}`);
160 }
161 domSummary.status.textContent = text;
162 if (active) {
163 domSummary.status.classList.add("status-running");
164 domSummary.status.classList.remove("status-idle");
165 domButtonRebuild.disabled = true;
166 } else {
167 domSummary.status.classList.remove("status-running");
168 domSummary.status.classList.add("status-idle");
169 domButtonRebuild.disabled = false;
170 }
171 if (reset_time_reports) {
172 // Grey out and collapse all the time reports
173 for (const time_report_host of domTimeReportList.children) {
174 const details = time_report_host.shadowRoot.querySelector(":host > details");
175 details.classList.add("pending");
176 details.open = false;
177 }
178 }
179}
180function updateStepStatus(step_idx) {
181 const li = domStepList.children[step_idx];
182 const step_status = wasm_exports.stepStatus(step_idx);
183 li.classList.remove("step-wip", "step-success", "step-failure");
184 if (step_status == 0) {
185 // pending
186 } else if (step_status == 1) {
187 li.classList.add("step-wip");
188 } else if (step_status == 2) {
189 li.classList.add("step-success");
190 } else if (step_status == 3) {
191 li.classList.add("step-failure");
192 } else {
193 console.log(`bad step status: ${step_status}`);
194 }
195}
196
197function decodeString(ptr, len) {
198 if (len === 0) return "";
199 return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
200}
201function getU32Array(ptr, len) {
202 if (len === 0) return new Uint32Array();
203 return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
204}
205function unwrapString(bigint) {
206 const ptr = Number(bigint & 0xffffffffn);
207 const len = Number(bigint >> 32n);
208 return decodeString(ptr, len);
209}
210
211const time_report_entry_template = document.getElementById("timeReportEntryTemplate").content;
212const domTimeReport = document.getElementById("timeReport");
213const domTimeReportList = document.getElementById("timeReportList");
214function timeReportReset(steps_len) {
215 let entries = [];
216 for (let i = 0; i < steps_len; i += 1) {
217 const step_name = unwrapString(wasm_exports.stepName(i));
218 const host = document.createElement("div");
219 const shadow = host.attachShadow({ mode: "open" });
220 shadow.appendChild(time_report_entry_template.cloneNode(true));
221 shadow.querySelector(":host > details").classList.add("pending");
222 const slotted_name = document.createElement("code");
223 slotted_name.setAttribute("slot", "step-name");
224 slotted_name.textContent = step_name;
225 host.appendChild(slotted_name);
226 entries.push(host);
227 }
228 domTimeReportList.replaceChildren(...entries);
229 domTimeReport.classList.remove("hidden");
230}
231function timeReportUpdateCompile(
232 step_idx,
233 inner_html_ptr,
234 inner_html_len,
235 file_table_html_ptr,
236 file_table_html_len,
237 decl_table_html_ptr,
238 decl_table_html_len,
239 use_llvm,
240) {
241 const inner_html = decodeString(inner_html_ptr, inner_html_len);
242 const file_table_html = decodeString(file_table_html_ptr, file_table_html_len);
243 const decl_table_html = decodeString(decl_table_html_ptr, decl_table_html_len);
244
245 const host = domTimeReportList.children.item(step_idx);
246 const shadow = host.shadowRoot;
247
248 shadow.querySelector(":host > details").classList.remove("pending", "no-llvm");
249
250 shadow.getElementById("genericReport").classList.add("hidden");
251 shadow.getElementById("compileReport").classList.remove("hidden");
252 shadow.getElementById("runTestReport").classList.add("hidden");
253
254 if (!use_llvm) shadow.querySelector(":host > details").classList.add("no-llvm");
255 host.innerHTML = inner_html;
256 shadow.getElementById("fileTableBody").innerHTML = file_table_html;
257 shadow.getElementById("declTableBody").innerHTML = decl_table_html;
258}
259function timeReportUpdateGeneric(
260 step_idx,
261 inner_html_ptr,
262 inner_html_len,
263) {
264 const inner_html = decodeString(inner_html_ptr, inner_html_len);
265 const host = domTimeReportList.children.item(step_idx);
266 const shadow = host.shadowRoot;
267 shadow.querySelector(":host > details").classList.remove("pending", "no-llvm");
268 shadow.getElementById("genericReport").classList.remove("hidden");
269 shadow.getElementById("compileReport").classList.add("hidden");
270 shadow.getElementById("runTestReport").classList.add("hidden");
271 host.innerHTML = inner_html;
272}
273function timeReportUpdateRunTest(
274 step_idx,
275 table_html_ptr,
276 table_html_len,
277) {
278 const table_html = decodeString(table_html_ptr, table_html_len);
279 const host = domTimeReportList.children.item(step_idx);
280 const shadow = host.shadowRoot;
281
282 shadow.querySelector(":host > details").classList.remove("pending", "no-llvm");
283
284 shadow.getElementById("genericReport").classList.add("hidden");
285 shadow.getElementById("compileReport").classList.add("hidden");
286 shadow.getElementById("runTestReport").classList.remove("hidden");
287
288 shadow.getElementById("runTestTableBody").innerHTML = table_html;
289}
290
291const fuzz_entry_template = document.getElementById("fuzzEntryTemplate").content;
292const domFuzz = document.getElementById("fuzz");
293const domFuzzStatus = document.getElementById("fuzzStatus");
294const domFuzzEntries = document.getElementById("fuzzEntries");
295let domFuzzInstance = null;
296function fuzzRequestSources() {
297 domFuzzStatus.classList.remove("hidden");
298 domFuzzStatus.textContent = "Loading sources tarball...";
299 fetch("sources.tar").then(function(response) {
300 if (!response.ok) throw new Error("unable to download sources");
301 domFuzzStatus.textContent = "Parsing fuzz test sources...";
302 return response.arrayBuffer();
303 }).then(function(buffer) {
304 if (buffer.length === 0) throw new Error("sources.tar was empty");
305 const js_array = new Uint8Array(buffer);
306 const ptr = wasm_exports.alloc(js_array.length);
307 const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
308 wasm_array.set(js_array);
309 wasm_exports.fuzzUnpackSources(ptr, js_array.length);
310 domFuzzStatus.textContent = "";
311 domFuzzStatus.classList.add("hidden");
312 });
313}
314function fuzzReady() {
315 domFuzz.classList.remove("hidden");
316
317 // TODO: multiple fuzzer instances
318 if (domFuzzInstance !== null) return;
319
320 const host = document.createElement("div");
321 const shadow = host.attachShadow({ mode: "open" });
322 shadow.appendChild(fuzz_entry_template.cloneNode(true));
323
324 domFuzzInstance = host;
325 domFuzzEntries.appendChild(host);
326}
327function fuzzReset() {
328 domFuzz.classList.add("hidden");
329 domFuzzEntries.replaceChildren();
330 domFuzzInstance = null;
331}
332function fuzzUpdateStats(stats_html_ptr, stats_html_len) {
333 if (domFuzzInstance === null) throw new Error("fuzzUpdateStats called when fuzzer inactive");
334 const stats_html = decodeString(stats_html_ptr, stats_html_len);
335 const host = domFuzzInstance;
336 host.innerHTML = stats_html;
337}
338function fuzzUpdateEntryPoints(entry_points_html_ptr, entry_points_html_len) {
339 if (domFuzzInstance === null) throw new Error("fuzzUpdateEntryPoints called when fuzzer inactive");
340 const entry_points_html = decodeString(entry_points_html_ptr, entry_points_html_len);
341 const domEntryPointList = domFuzzInstance.shadowRoot.getElementById("entryPointList");
342 domEntryPointList.innerHTML = entry_points_html;
343}
344function fuzzUpdateSource(source_html_ptr, source_html_len) {
345 if (domFuzzInstance === null) throw new Error("fuzzUpdateSource called when fuzzer inactive");
346 const source_html = decodeString(source_html_ptr, source_html_len);
347 const domSourceText = domFuzzInstance.shadowRoot.getElementById("sourceText");
348 domSourceText.innerHTML = source_html;
349 domFuzzInstance.shadowRoot.getElementById("source").classList.remove("hidden");
350}
351function fuzzUpdateCoverage(covered_ptr, covered_len) {
352 if (domFuzzInstance === null) throw new Error("fuzzUpdateCoverage called when fuzzer inactive");
353 const shadow = domFuzzInstance.shadowRoot;
354 const domSourceText = shadow.getElementById("sourceText");
355 const covered = getU32Array(covered_ptr, covered_len);
356 for (let i = 0; i < domSourceText.children.length; i += 1) {
357 const childDom = domSourceText.children[i];
358 if (childDom.id != null && childDom.id[0] == "l") {
359 childDom.classList.add("l");
360 childDom.classList.remove("c");
361 }
362 }
363 for (const sli of covered) {
364 shadow.getElementById(`l${sli}`).classList.add("c");
365 }
366}