| 1 | const domConnectionStatus = document.getElementById("connectionStatus"); |
| 2 | const domFirefoxWebSocketBullshitExplainer = document.getElementById("firefoxWebSocketBullshitExplainer"); |
| 3 | |
| 4 | const domMain = document.getElementsByTagName("main")[0]; |
| 5 | const domSummary = { |
| 6 | stepCount: document.getElementById("summaryStepCount"), |
| 7 | status: document.getElementById("summaryStatus"), |
| 8 | }; |
| 9 | const domButtonRebuild = document.getElementById("buttonRebuild"); |
| 10 | const domStepList = document.getElementById("stepList"); |
| 11 | let domSteps = []; |
| 12 | |
| 13 | let wasm_promise = fetch("main.wasm"); |
| 14 | let wasm_exports = null; |
| 15 | |
| 16 | const text_decoder = new TextDecoder(); |
| 17 | const text_encoder = new TextEncoder(); |
| 18 | |
| 19 | domButtonRebuild.addEventListener("click", () => wasm_exports.rebuild()); |
| 20 | |
| 21 | setConnectionStatus("Loading WebAssembly...", false); |
| 22 | WebAssembly.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 | |
| 61 | function 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 | } |
| 78 | function onWebSocketOpen() { |
| 79 | setConnectionStatus("Waiting for data...", false); |
| 80 | } |
| 81 | function 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 | } |
| 88 | function 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 | |
| 98 | function 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 | |
| 114 | function 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 | |
| 143 | function 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 | } |
| 180 | function 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 | |
| 197 | function decodeString(ptr, len) { |
| 198 | if (len === 0) return ""; |
| 199 | return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len)); |
| 200 | } |
| 201 | function getU32Array(ptr, len) { |
| 202 | if (len === 0) return new Uint32Array(); |
| 203 | return new Uint32Array(wasm_exports.memory.buffer, ptr, len); |
| 204 | } |
| 205 | function unwrapString(bigint) { |
| 206 | const ptr = Number(bigint & 0xffffffffn); |
| 207 | const len = Number(bigint >> 32n); |
| 208 | return decodeString(ptr, len); |
| 209 | } |
| 210 | |
| 211 | const time_report_entry_template = document.getElementById("timeReportEntryTemplate").content; |
| 212 | const domTimeReport = document.getElementById("timeReport"); |
| 213 | const domTimeReportList = document.getElementById("timeReportList"); |
| 214 | function 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 | } |
| 231 | function 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 | } |
| 259 | function 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 | } |
| 273 | function 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 | |
| 291 | const fuzz_entry_template = document.getElementById("fuzzEntryTemplate").content; |
| 292 | const domFuzz = document.getElementById("fuzz"); |
| 293 | const domFuzzStatus = document.getElementById("fuzzStatus"); |
| 294 | const domFuzzEntries = document.getElementById("fuzzEntries"); |
| 295 | let domFuzzInstance = null; |
| 296 | function 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 | } |
| 314 | function 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 | } |
| 327 | function fuzzReset() { |
| 328 | domFuzz.classList.add("hidden"); |
| 329 | domFuzzEntries.replaceChildren(); |
| 330 | domFuzzInstance = null; |
| 331 | } |
| 332 | function 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 | } |
| 338 | function 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 | } |
| 344 | function 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 | } |
| 351 | function 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 | } |