authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-09-09 20:37:03-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-09-11 13:41:29-07:00
log0cdccff51912359b7ec5afa57fbbd5bb69d8f3a2
tree6221333ab3422901dc9e27e8745b7966bc47e377
parent9bc731b30a0be771a8128bab25d873f9212643a9

fuzzer: move web files into separate directory


7 files changed, 841 insertions(+), 841 deletions(-)

lib/fuzzer/index.html deleted-161
......@@ -1,161 +0,0 @@
1<!doctype html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Zig Build System Interface</title>
6 <style type="text/css">
7 body {
8 font-family: system-ui, -apple-system, Roboto, "Segoe UI", sans-serif;
9 color: #000000;
10 }
11 .hidden {
12 display: none;
13 }
14 table {
15 width: 100%;
16 }
17 a {
18 color: #2A6286;
19 }
20 pre{
21 font-family:"Source Code Pro",monospace;
22 font-size:1em;
23 background-color:#F5F5F5;
24 padding: 1em;
25 margin: 0;
26 overflow-x: auto;
27 }
28 :not(pre) > code {
29 white-space: break-spaces;
30 }
31 code {
32 font-family:"Source Code Pro",monospace;
33 font-size: 0.9em;
34 }
35 code a {
36 color: #000000;
37 }
38 kbd {
39 color: #000;
40 background-color: #fafbfc;
41 border-color: #d1d5da;
42 border-bottom-color: #c6cbd1;
43 box-shadow-color: #c6cbd1;
44 display: inline-block;
45 padding: 0.3em 0.2em;
46 font: 1.2em monospace;
47 line-height: 0.8em;
48 vertical-align: middle;
49 border: solid 1px;
50 border-radius: 3px;
51 box-shadow: inset 0 -1px 0;
52 cursor: default;
53 }
54
55 .l {
56 display: inline-block;
57 background: red;
58 width: 1em;
59 height: 1em;
60 border-radius: 1em;
61 }
62 .c {
63 background-color: green;
64 }
65
66 .tok-kw {
67 color: #333;
68 font-weight: bold;
69 }
70 .tok-str {
71 color: #d14;
72 }
73 .tok-builtin {
74 color: #0086b3;
75 }
76 .tok-comment {
77 color: #777;
78 font-style: italic;
79 }
80 .tok-fn {
81 color: #900;
82 font-weight: bold;
83 }
84 .tok-null {
85 color: #008080;
86 }
87 .tok-number {
88 color: #008080;
89 }
90 .tok-type {
91 color: #458;
92 font-weight: bold;
93 }
94
95 @media (prefers-color-scheme: dark) {
96 body {
97 background-color: #111;
98 color: #bbb;
99 }
100 pre {
101 background-color: #222;
102 color: #ccc;
103 }
104 a {
105 color: #88f;
106 }
107 code a {
108 color: #ccc;
109 }
110 .l {
111 background-color: red;
112 }
113 .c {
114 background-color: green;
115 }
116 .tok-kw {
117 color: #eee;
118 }
119 .tok-str {
120 color: #2e5;
121 }
122 .tok-builtin {
123 color: #ff894c;
124 }
125 .tok-comment {
126 color: #aa7;
127 }
128 .tok-fn {
129 color: #B1A0F8;
130 }
131 .tok-null {
132 color: #ff8080;
133 }
134 .tok-number {
135 color: #ff8080;
136 }
137 .tok-type {
138 color: #68f;
139 }
140 }
141 </style>
142 </head>
143 <body>
144 <p id="status">Loading JavaScript...</p>
145 <div id="sectStats" class="hidden">
146 <ul>
147 <li>Total Runs: <span id="statTotalRuns"></span></li>
148 <li>Unique Runs: <span id="statUniqueRuns"></span></li>
149 <li>Coverage: <span id="statCoverage"></span></li>
150 <li>Lowest Stack: <span id="statLowestStack"></span></li>
151 <li>Entry Points: <ul id="entryPointsList"></ul></li>
152 </ul>
153 </div>
154 <div id="sectSource" class="hidden">
155 <h2>Source Code</h2>
156 <pre><code id="sourceText"></code></pre>
157 </div>
158 <script src="main.js"></script>
159 </body>
160</html>
161
lib/fuzzer/main.js deleted-249
......@@ -1,249 +0,0 @@
1(function() {
2 const domStatus = document.getElementById("status");
3 const domSectSource = document.getElementById("sectSource");
4 const domSectStats = document.getElementById("sectStats");
5 const domSourceText = document.getElementById("sourceText");
6 const domStatTotalRuns = document.getElementById("statTotalRuns");
7 const domStatUniqueRuns = document.getElementById("statUniqueRuns");
8 const domStatCoverage = document.getElementById("statCoverage");
9 const domStatLowestStack = document.getElementById("statLowestStack");
10 const domEntryPointsList = document.getElementById("entryPointsList");
11
12 let wasm_promise = fetch("main.wasm");
13 let sources_promise = fetch("sources.tar").then(function(response) {
14 if (!response.ok) throw new Error("unable to download sources");
15 return response.arrayBuffer();
16 });
17 var wasm_exports = null;
18 var curNavSearch = null;
19 var curNavLocation = null;
20
21 const text_decoder = new TextDecoder();
22 const text_encoder = new TextEncoder();
23
24 domStatus.textContent = "Loading WebAssembly...";
25 WebAssembly.instantiateStreaming(wasm_promise, {
26 js: {
27 log: function(ptr, len) {
28 const msg = decodeString(ptr, len);
29 console.log(msg);
30 },
31 panic: function (ptr, len) {
32 const msg = decodeString(ptr, len);
33 throw new Error("panic: " + msg);
34 },
35 emitSourceIndexChange: onSourceIndexChange,
36 emitCoverageUpdate: onCoverageUpdate,
37 emitEntryPointsUpdate: renderStats,
38 },
39 }).then(function(obj) {
40 wasm_exports = obj.instance.exports;
41 window.wasm = obj; // for debugging
42 domStatus.textContent = "Loading sources tarball...";
43
44 sources_promise.then(function(buffer) {
45 domStatus.textContent = "Parsing sources...";
46 const js_array = new Uint8Array(buffer);
47 const ptr = wasm_exports.alloc(js_array.length);
48 const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
49 wasm_array.set(js_array);
50 wasm_exports.unpack(ptr, js_array.length);
51
52 window.addEventListener('popstate', onPopState, false);
53 onHashChange(null);
54
55 domStatus.textContent = "Waiting for server to send source location metadata...";
56 connectWebSocket();
57 });
58 });
59
60 function onPopState(ev) {
61 onHashChange(ev.state);
62 }
63
64 function onHashChange(state) {
65 history.replaceState({}, "");
66 navigate(location.hash);
67 if (state == null) window.scrollTo({top: 0});
68 }
69
70 function navigate(location_hash) {
71 domSectSource.classList.add("hidden");
72
73 curNavLocation = null;
74 curNavSearch = null;
75
76 if (location_hash.length > 1 && location_hash[0] === '#') {
77 const query = location_hash.substring(1);
78 const qpos = query.indexOf("?");
79 let nonSearchPart;
80 if (qpos === -1) {
81 nonSearchPart = query;
82 } else {
83 nonSearchPart = query.substring(0, qpos);
84 curNavSearch = decodeURIComponent(query.substring(qpos + 1));
85 }
86
87 if (nonSearchPart[0] == "l") {
88 curNavLocation = +nonSearchPart.substring(1);
89 renderSource(curNavLocation);
90 }
91 }
92
93 render();
94 }
95
96 function connectWebSocket() {
97 const host = document.location.host;
98 const pathname = document.location.pathname;
99 const isHttps = document.location.protocol === 'https:';
100 const match = host.match(/^(.+):(\d+)$/);
101 const defaultPort = isHttps ? 443 : 80;
102 const port = match ? parseInt(match[2], 10) : defaultPort;
103 const hostName = match ? match[1] : host;
104 const wsProto = isHttps ? "wss:" : "ws:";
105 const wsUrl = wsProto + '//' + hostName + ':' + port + pathname;
106 ws = new WebSocket(wsUrl);
107 ws.binaryType = "arraybuffer";
108 ws.addEventListener('message', onWebSocketMessage, false);
109 ws.addEventListener('error', timeoutThenCreateNew, false);
110 ws.addEventListener('close', timeoutThenCreateNew, false);
111 ws.addEventListener('open', onWebSocketOpen, false);
112 }
113
114 function onWebSocketOpen() {
115 //console.log("web socket opened");
116 }
117
118 function onWebSocketMessage(ev) {
119 wasmOnMessage(ev.data);
120 }
121
122 function timeoutThenCreateNew() {
123 ws.removeEventListener('message', onWebSocketMessage, false);
124 ws.removeEventListener('error', timeoutThenCreateNew, false);
125 ws.removeEventListener('close', timeoutThenCreateNew, false);
126 ws.removeEventListener('open', onWebSocketOpen, false);
127 ws = null;
128 setTimeout(connectWebSocket, 1000);
129 }
130
131 function wasmOnMessage(data) {
132 const jsArray = new Uint8Array(data);
133 const ptr = wasm_exports.message_begin(jsArray.length);
134 const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, jsArray.length);
135 wasmArray.set(jsArray);
136 wasm_exports.message_end();
137 }
138
139 function onSourceIndexChange() {
140 render();
141 if (curNavLocation != null) renderSource(curNavLocation);
142 }
143
144 function onCoverageUpdate() {
145 renderStats();
146 renderCoverage();
147 }
148
149 function render() {
150 domStatus.classList.add("hidden");
151 }
152
153 function renderStats() {
154 const totalRuns = wasm_exports.totalRuns();
155 const uniqueRuns = wasm_exports.uniqueRuns();
156 const totalSourceLocations = wasm_exports.totalSourceLocations();
157 const coveredSourceLocations = wasm_exports.coveredSourceLocations();
158 domStatTotalRuns.innerText = totalRuns;
159 domStatUniqueRuns.innerText = uniqueRuns + " (" + percent(uniqueRuns, totalRuns) + "%)";
160 domStatCoverage.innerText = coveredSourceLocations + " / " + totalSourceLocations + " (" + percent(coveredSourceLocations, totalSourceLocations) + "%)";
161 domStatLowestStack.innerText = unwrapString(wasm_exports.lowestStack());
162
163 const entryPoints = unwrapInt32Array(wasm_exports.entryPoints());
164 resizeDomList(domEntryPointsList, entryPoints.length, "<li></li>");
165 for (let i = 0; i < entryPoints.length; i += 1) {
166 const liDom = domEntryPointsList.children[i];
167 liDom.innerHTML = unwrapString(wasm_exports.sourceLocationLinkHtml(entryPoints[i]));
168 }
169
170
171 domSectStats.classList.remove("hidden");
172 }
173
174 function renderCoverage() {
175 if (curNavLocation == null) return;
176 const sourceLocationIndex = curNavLocation;
177
178 for (let i = 0; i < domSourceText.children.length; i += 1) {
179 const childDom = domSourceText.children[i];
180 if (childDom.id != null && childDom.id[0] == "l") {
181 childDom.classList.add("l");
182 childDom.classList.remove("c");
183 }
184 }
185 const coveredList = unwrapInt32Array(wasm_exports.sourceLocationFileCoveredList(sourceLocationIndex));
186 for (let i = 0; i < coveredList.length; i += 1) {
187 document.getElementById("l" + coveredList[i]).classList.add("c");
188 }
189 }
190
191 function resizeDomList(listDom, desiredLen, templateHtml) {
192 for (let i = listDom.childElementCount; i < desiredLen; i += 1) {
193 listDom.insertAdjacentHTML('beforeend', templateHtml);
194 }
195 while (desiredLen < listDom.childElementCount) {
196 listDom.removeChild(listDom.lastChild);
197 }
198 }
199
200 function percent(a, b) {
201 return ((Number(a) / Number(b)) * 100).toFixed(1);
202 }
203
204 function renderSource(sourceLocationIndex) {
205 const pathName = unwrapString(wasm_exports.sourceLocationPath(sourceLocationIndex));
206 if (pathName.length === 0) return;
207
208 const h2 = domSectSource.children[0];
209 h2.innerText = pathName;
210 domSourceText.innerHTML = unwrapString(wasm_exports.sourceLocationFileHtml(sourceLocationIndex));
211
212 domSectSource.classList.remove("hidden");
213
214 // Empirically, Firefox needs this requestAnimationFrame in order for the scrollIntoView to work.
215 requestAnimationFrame(function() {
216 const slDom = document.getElementById("l" + sourceLocationIndex);
217 if (slDom != null) slDom.scrollIntoView({
218 behavior: "smooth",
219 block: "center",
220 });
221 });
222 }
223
224 function decodeString(ptr, len) {
225 if (len === 0) return "";
226 return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
227 }
228
229 function unwrapInt32Array(bigint) {
230 const ptr = Number(bigint & 0xffffffffn);
231 const len = Number(bigint >> 32n);
232 if (len === 0) return new Uint32Array();
233 return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
234 }
235
236 function setInputString(s) {
237 const jsArray = text_encoder.encode(s);
238 const len = jsArray.length;
239 const ptr = wasm_exports.set_input_string(len);
240 const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
241 wasmArray.set(jsArray);
242 }
243
244 function unwrapString(bigint) {
245 const ptr = Number(bigint & 0xffffffffn);
246 const len = Number(bigint >> 32n);
247 return decodeString(ptr, len);
248 }
249})();
lib/fuzzer/wasm/main.zig deleted-428
......@@ -1,428 +0,0 @@
1const std = @import("std");
2const assert = std.debug.assert;
3const abi = std.Build.Fuzz.abi;
4const gpa = std.heap.wasm_allocator;
5const log = std.log;
6const Coverage = std.debug.Coverage;
7const Allocator = std.mem.Allocator;
8
9const Walk = @import("Walk");
10const Decl = Walk.Decl;
11const html_render = @import("html_render");
12
13const js = struct {
14 extern "js" fn log(ptr: [*]const u8, len: usize) void;
15 extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
16 extern "js" fn emitSourceIndexChange() void;
17 extern "js" fn emitCoverageUpdate() void;
18 extern "js" fn emitEntryPointsUpdate() void;
19};
20
21pub const std_options: std.Options = .{
22 .logFn = logFn,
23};
24
25pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn {
26 _ = st;
27 _ = addr;
28 log.err("panic: {s}", .{msg});
29 @trap();
30}
31
32fn logFn(
33 comptime message_level: log.Level,
34 comptime scope: @TypeOf(.enum_literal),
35 comptime format: []const u8,
36 args: anytype,
37) void {
38 const level_txt = comptime message_level.asText();
39 const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
40 var buf: [500]u8 = undefined;
41 const line = std.fmt.bufPrint(&buf, level_txt ++ prefix2 ++ format, args) catch l: {
42 buf[buf.len - 3 ..][0..3].* = "...".*;
43 break :l &buf;
44 };
45 js.log(line.ptr, line.len);
46}
47
48export fn alloc(n: usize) [*]u8 {
49 const slice = gpa.alloc(u8, n) catch @panic("OOM");
50 return slice.ptr;
51}
52
53var message_buffer: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
54
55/// Resizes the message buffer to be the correct length; returns the pointer to
56/// the query string.
57export fn message_begin(len: usize) [*]u8 {
58 message_buffer.resize(gpa, len) catch @panic("OOM");
59 return message_buffer.items.ptr;
60}
61
62export fn message_end() void {
63 const msg_bytes = message_buffer.items;
64
65 const tag: abi.ToClientTag = @enumFromInt(msg_bytes[0]);
66 switch (tag) {
67 .source_index => return sourceIndexMessage(msg_bytes) catch @panic("OOM"),
68 .coverage_update => return coverageUpdateMessage(msg_bytes) catch @panic("OOM"),
69 .entry_points => return entryPointsMessage(msg_bytes) catch @panic("OOM"),
70 _ => unreachable,
71 }
72}
73
74export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
75 const tar_bytes = tar_ptr[0..tar_len];
76 log.debug("received {d} bytes of tar file", .{tar_bytes.len});
77
78 unpackInner(tar_bytes) catch |err| {
79 fatal("unable to unpack tar: {s}", .{@errorName(err)});
80 };
81}
82
83/// Set by `set_input_string`.
84var input_string: std.ArrayListUnmanaged(u8) = .{};
85var string_result: std.ArrayListUnmanaged(u8) = .{};
86
87export fn set_input_string(len: usize) [*]u8 {
88 input_string.resize(gpa, len) catch @panic("OOM");
89 return input_string.items.ptr;
90}
91
92/// Looks up the root struct decl corresponding to a file by path.
93/// Uses `input_string`.
94export fn find_file_root() Decl.Index {
95 const file: Walk.File.Index = @enumFromInt(Walk.files.getIndex(input_string.items) orelse return .none);
96 return file.findRootDecl();
97}
98
99export fn decl_source_html(decl_index: Decl.Index) String {
100 const decl = decl_index.get();
101
102 string_result.clearRetainingCapacity();
103 html_render.fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
104 fatal("unable to render source: {s}", .{@errorName(err)});
105 };
106 return String.init(string_result.items);
107}
108
109export fn lowestStack() String {
110 const header: *abi.CoverageUpdateHeader = @ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]);
111 string_result.clearRetainingCapacity();
112 string_result.writer(gpa).print("0x{d}", .{header.lowest_stack}) catch @panic("OOM");
113 return String.init(string_result.items);
114}
115
116export fn totalSourceLocations() usize {
117 return coverage_source_locations.items.len;
118}
119
120export fn coveredSourceLocations() usize {
121 const covered_bits = recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..];
122 var count: usize = 0;
123 for (covered_bits) |byte| count += @popCount(byte);
124 return count;
125}
126
127export fn totalRuns() u64 {
128 const header: *abi.CoverageUpdateHeader = @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]));
129 return header.n_runs;
130}
131
132export fn uniqueRuns() u64 {
133 const header: *abi.CoverageUpdateHeader = @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]));
134 return header.unique_runs;
135}
136
137const String = Slice(u8);
138
139fn Slice(T: type) type {
140 return packed struct(u64) {
141 ptr: u32,
142 len: u32,
143
144 fn init(s: []const T) @This() {
145 return .{
146 .ptr = @intFromPtr(s.ptr),
147 .len = s.len,
148 };
149 }
150 };
151}
152
153fn unpackInner(tar_bytes: []u8) !void {
154 var fbs = std.io.fixedBufferStream(tar_bytes);
155 var file_name_buffer: [1024]u8 = undefined;
156 var link_name_buffer: [1024]u8 = undefined;
157 var it = std.tar.iterator(fbs.reader(), .{
158 .file_name_buffer = &file_name_buffer,
159 .link_name_buffer = &link_name_buffer,
160 });
161 while (try it.next()) |tar_file| {
162 switch (tar_file.kind) {
163 .file => {
164 if (tar_file.size == 0 and tar_file.name.len == 0) break;
165 if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
166 log.debug("found file: '{s}'", .{tar_file.name});
167 const file_name = try gpa.dupe(u8, tar_file.name);
168 if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| {
169 const pkg_name = file_name[0..pkg_name_end];
170 const gop = try Walk.modules.getOrPut(gpa, pkg_name);
171 const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
172 if (!gop.found_existing or
173 std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
174 std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
175 {
176 gop.value_ptr.* = file;
177 }
178 const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)];
179 assert(file == try Walk.add_file(file_name, file_bytes));
180 }
181 } else {
182 log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name});
183 }
184 },
185 else => continue,
186 }
187 }
188}
189
190fn fatal(comptime format: []const u8, args: anytype) noreturn {
191 var buf: [500]u8 = undefined;
192 const line = std.fmt.bufPrint(&buf, format, args) catch l: {
193 buf[buf.len - 3 ..][0..3].* = "...".*;
194 break :l &buf;
195 };
196 js.panic(line.ptr, line.len);
197}
198
199fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void {
200 const Header = abi.SourceIndexHeader;
201 const header: Header = @bitCast(msg_bytes[0..@sizeOf(Header)].*);
202
203 const directories_start = @sizeOf(Header);
204 const directories_end = directories_start + header.directories_len * @sizeOf(Coverage.String);
205 const files_start = directories_end;
206 const files_end = files_start + header.files_len * @sizeOf(Coverage.File);
207 const source_locations_start = files_end;
208 const source_locations_end = source_locations_start + header.source_locations_len * @sizeOf(Coverage.SourceLocation);
209 const string_bytes = msg_bytes[source_locations_end..][0..header.string_bytes_len];
210
211 const directories: []const Coverage.String = @alignCast(std.mem.bytesAsSlice(Coverage.String, msg_bytes[directories_start..directories_end]));
212 const files: []const Coverage.File = @alignCast(std.mem.bytesAsSlice(Coverage.File, msg_bytes[files_start..files_end]));
213 const source_locations: []const Coverage.SourceLocation = @alignCast(std.mem.bytesAsSlice(Coverage.SourceLocation, msg_bytes[source_locations_start..source_locations_end]));
214
215 try updateCoverage(directories, files, source_locations, string_bytes);
216 js.emitSourceIndexChange();
217}
218
219fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void {
220 recent_coverage_update.clearRetainingCapacity();
221 recent_coverage_update.appendSlice(gpa, msg_bytes) catch @panic("OOM");
222 js.emitCoverageUpdate();
223}
224
225var entry_points: std.ArrayListUnmanaged(u32) = .{};
226
227fn entryPointsMessage(msg_bytes: []u8) error{OutOfMemory}!void {
228 const header: abi.EntryPointHeader = @bitCast(msg_bytes[0..@sizeOf(abi.EntryPointHeader)].*);
229 entry_points.resize(gpa, header.flags.locs_len) catch @panic("OOM");
230 @memcpy(entry_points.items, std.mem.bytesAsSlice(u32, msg_bytes[@sizeOf(abi.EntryPointHeader)..]));
231 js.emitEntryPointsUpdate();
232}
233
234export fn entryPoints() Slice(u32) {
235 return Slice(u32).init(entry_points.items);
236}
237
238/// Index into `coverage_source_locations`.
239const SourceLocationIndex = enum(u32) {
240 _,
241
242 fn haveCoverage(sli: SourceLocationIndex) bool {
243 return @intFromEnum(sli) < coverage_source_locations.items.len;
244 }
245
246 fn ptr(sli: SourceLocationIndex) *Coverage.SourceLocation {
247 return &coverage_source_locations.items[@intFromEnum(sli)];
248 }
249
250 fn sourceLocationLinkHtml(
251 sli: SourceLocationIndex,
252 out: *std.ArrayListUnmanaged(u8),
253 ) Allocator.Error!void {
254 const sl = sli.ptr();
255 try out.writer(gpa).print("<a href=\"#l{d}\">", .{@intFromEnum(sli)});
256 try sli.appendPath(out);
257 try out.writer(gpa).print(":{d}:{d}</a>", .{ sl.line, sl.column });
258 }
259
260 fn appendPath(sli: SourceLocationIndex, out: *std.ArrayListUnmanaged(u8)) Allocator.Error!void {
261 const sl = sli.ptr();
262 const file = coverage.fileAt(sl.file);
263 const file_name = coverage.stringAt(file.basename);
264 const dir_name = coverage.stringAt(coverage.directories.keys()[file.directory_index]);
265 try html_render.appendEscaped(out, dir_name);
266 try out.appendSlice(gpa, "/");
267 try html_render.appendEscaped(out, file_name);
268 }
269
270 fn toWalkFile(sli: SourceLocationIndex) ?Walk.File.Index {
271 var buf: std.ArrayListUnmanaged(u8) = .{};
272 defer buf.deinit(gpa);
273 sli.appendPath(&buf) catch @panic("OOM");
274 return @enumFromInt(Walk.files.getIndex(buf.items) orelse return null);
275 }
276
277 fn fileHtml(
278 sli: SourceLocationIndex,
279 out: *std.ArrayListUnmanaged(u8),
280 ) error{ OutOfMemory, SourceUnavailable }!void {
281 const walk_file_index = sli.toWalkFile() orelse return error.SourceUnavailable;
282 const root_node = walk_file_index.findRootDecl().get().ast_node;
283 var annotations: std.ArrayListUnmanaged(html_render.Annotation) = .{};
284 defer annotations.deinit(gpa);
285 try computeSourceAnnotations(sli.ptr().file, walk_file_index, &annotations, coverage_source_locations.items);
286 html_render.fileSourceHtml(walk_file_index, out, root_node, .{
287 .source_location_annotations = annotations.items,
288 }) catch |err| {
289 fatal("unable to render source: {s}", .{@errorName(err)});
290 };
291 }
292};
293
294fn computeSourceAnnotations(
295 cov_file_index: Coverage.File.Index,
296 walk_file_index: Walk.File.Index,
297 annotations: *std.ArrayListUnmanaged(html_render.Annotation),
298 source_locations: []const Coverage.SourceLocation,
299) !void {
300 // Collect all the source locations from only this file into this array
301 // first, then sort by line, col, so that we can collect annotations with
302 // O(N) time complexity.
303 var locs: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
304 defer locs.deinit(gpa);
305
306 for (source_locations, 0..) |sl, sli_usize| {
307 if (sl.file != cov_file_index) continue;
308 const sli: SourceLocationIndex = @enumFromInt(sli_usize);
309 try locs.append(gpa, sli);
310 }
311
312 std.mem.sortUnstable(SourceLocationIndex, locs.items, {}, struct {
313 pub fn lessThan(context: void, lhs: SourceLocationIndex, rhs: SourceLocationIndex) bool {
314 _ = context;
315 const lhs_ptr = lhs.ptr();
316 const rhs_ptr = rhs.ptr();
317 if (lhs_ptr.line < rhs_ptr.line) return true;
318 if (lhs_ptr.line > rhs_ptr.line) return false;
319 return lhs_ptr.column < rhs_ptr.column;
320 }
321 }.lessThan);
322
323 const source = walk_file_index.get_ast().source;
324 var line: usize = 1;
325 var column: usize = 1;
326 var next_loc_index: usize = 0;
327 for (source, 0..) |byte, offset| {
328 if (byte == '\n') {
329 line += 1;
330 column = 1;
331 } else {
332 column += 1;
333 }
334 while (true) {
335 if (next_loc_index >= locs.items.len) return;
336 const next_sli = locs.items[next_loc_index];
337 const next_sl = next_sli.ptr();
338 if (next_sl.line > line or (next_sl.line == line and next_sl.column >= column)) break;
339 try annotations.append(gpa, .{
340 .file_byte_offset = offset,
341 .dom_id = @intFromEnum(next_sli),
342 });
343 next_loc_index += 1;
344 }
345 }
346}
347
348var coverage = Coverage.init;
349/// Index of type `SourceLocationIndex`.
350var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{};
351/// Contains the most recent coverage update message, unmodified.
352var recent_coverage_update: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
353
354fn updateCoverage(
355 directories: []const Coverage.String,
356 files: []const Coverage.File,
357 source_locations: []const Coverage.SourceLocation,
358 string_bytes: []const u8,
359) !void {
360 coverage.directories.clearRetainingCapacity();
361 coverage.files.clearRetainingCapacity();
362 coverage.string_bytes.clearRetainingCapacity();
363 coverage_source_locations.clearRetainingCapacity();
364
365 try coverage_source_locations.appendSlice(gpa, source_locations);
366 try coverage.string_bytes.appendSlice(gpa, string_bytes);
367
368 try coverage.files.entries.resize(gpa, files.len);
369 @memcpy(coverage.files.entries.items(.key), files);
370 try coverage.files.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
371
372 try coverage.directories.entries.resize(gpa, directories.len);
373 @memcpy(coverage.directories.entries.items(.key), directories);
374 try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
375}
376
377export fn sourceLocationLinkHtml(index: SourceLocationIndex) String {
378 string_result.clearRetainingCapacity();
379 index.sourceLocationLinkHtml(&string_result) catch @panic("OOM");
380 return String.init(string_result.items);
381}
382
383/// Returns empty string if coverage metadata is not available for this source location.
384export fn sourceLocationPath(sli: SourceLocationIndex) String {
385 string_result.clearRetainingCapacity();
386 if (sli.haveCoverage()) sli.appendPath(&string_result) catch @panic("OOM");
387 return String.init(string_result.items);
388}
389
390export fn sourceLocationFileHtml(sli: SourceLocationIndex) String {
391 string_result.clearRetainingCapacity();
392 sli.fileHtml(&string_result) catch |err| switch (err) {
393 error.OutOfMemory => @panic("OOM"),
394 error.SourceUnavailable => {},
395 };
396 return String.init(string_result.items);
397}
398
399export fn sourceLocationFileCoveredList(sli_file: SourceLocationIndex) Slice(SourceLocationIndex) {
400 const global = struct {
401 var result: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
402 fn add(i: u32, want_file: Coverage.File.Index) void {
403 const src_loc_index: SourceLocationIndex = @enumFromInt(i);
404 if (src_loc_index.ptr().file == want_file) result.appendAssumeCapacity(src_loc_index);
405 }
406 };
407 const want_file = sli_file.ptr().file;
408 global.result.clearRetainingCapacity();
409
410 // This code assumes 64-bit elements, which is incorrect if the executable
411 // being fuzzed is not a 64-bit CPU. It also assumes little-endian which
412 // can also be incorrect.
413 comptime assert(abi.CoverageUpdateHeader.trailing[0] == .pc_bits_usize);
414 const n_bitset_elems = (coverage_source_locations.items.len + @bitSizeOf(u64) - 1) / @bitSizeOf(u64);
415 const covered_bits = std.mem.bytesAsSlice(
416 u64,
417 recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..][0 .. n_bitset_elems * @sizeOf(u64)],
418 );
419 var sli: u32 = 0;
420 for (covered_bits) |elem| {
421 global.result.ensureUnusedCapacity(gpa, 64) catch @panic("OOM");
422 for (0..@bitSizeOf(u64)) |i| {
423 if ((elem & (@as(u64, 1) << @intCast(i))) != 0) global.add(sli, want_file);
424 sli += 1;
425 }
426 }
427 return Slice(SourceLocationIndex).init(global.result.items);
428}
lib/fuzzer/web/index.html created+161
......@@ -0,0 +1,161 @@
1<!doctype html>
2<html>
3 <head>
4 <meta charset="utf-8">
5 <title>Zig Build System Interface</title>
6 <style type="text/css">
7 body {
8 font-family: system-ui, -apple-system, Roboto, "Segoe UI", sans-serif;
9 color: #000000;
10 }
11 .hidden {
12 display: none;
13 }
14 table {
15 width: 100%;
16 }
17 a {
18 color: #2A6286;
19 }
20 pre{
21 font-family:"Source Code Pro",monospace;
22 font-size:1em;
23 background-color:#F5F5F5;
24 padding: 1em;
25 margin: 0;
26 overflow-x: auto;
27 }
28 :not(pre) > code {
29 white-space: break-spaces;
30 }
31 code {
32 font-family:"Source Code Pro",monospace;
33 font-size: 0.9em;
34 }
35 code a {
36 color: #000000;
37 }
38 kbd {
39 color: #000;
40 background-color: #fafbfc;
41 border-color: #d1d5da;
42 border-bottom-color: #c6cbd1;
43 box-shadow-color: #c6cbd1;
44 display: inline-block;
45 padding: 0.3em 0.2em;
46 font: 1.2em monospace;
47 line-height: 0.8em;
48 vertical-align: middle;
49 border: solid 1px;
50 border-radius: 3px;
51 box-shadow: inset 0 -1px 0;
52 cursor: default;
53 }
54
55 .l {
56 display: inline-block;
57 background: red;
58 width: 1em;
59 height: 1em;
60 border-radius: 1em;
61 }
62 .c {
63 background-color: green;
64 }
65
66 .tok-kw {
67 color: #333;
68 font-weight: bold;
69 }
70 .tok-str {
71 color: #d14;
72 }
73 .tok-builtin {
74 color: #0086b3;
75 }
76 .tok-comment {
77 color: #777;
78 font-style: italic;
79 }
80 .tok-fn {
81 color: #900;
82 font-weight: bold;
83 }
84 .tok-null {
85 color: #008080;
86 }
87 .tok-number {
88 color: #008080;
89 }
90 .tok-type {
91 color: #458;
92 font-weight: bold;
93 }
94
95 @media (prefers-color-scheme: dark) {
96 body {
97 background-color: #111;
98 color: #bbb;
99 }
100 pre {
101 background-color: #222;
102 color: #ccc;
103 }
104 a {
105 color: #88f;
106 }
107 code a {
108 color: #ccc;
109 }
110 .l {
111 background-color: red;
112 }
113 .c {
114 background-color: green;
115 }
116 .tok-kw {
117 color: #eee;
118 }
119 .tok-str {
120 color: #2e5;
121 }
122 .tok-builtin {
123 color: #ff894c;
124 }
125 .tok-comment {
126 color: #aa7;
127 }
128 .tok-fn {
129 color: #B1A0F8;
130 }
131 .tok-null {
132 color: #ff8080;
133 }
134 .tok-number {
135 color: #ff8080;
136 }
137 .tok-type {
138 color: #68f;
139 }
140 }
141 </style>
142 </head>
143 <body>
144 <p id="status">Loading JavaScript...</p>
145 <div id="sectStats" class="hidden">
146 <ul>
147 <li>Total Runs: <span id="statTotalRuns"></span></li>
148 <li>Unique Runs: <span id="statUniqueRuns"></span></li>
149 <li>Coverage: <span id="statCoverage"></span></li>
150 <li>Lowest Stack: <span id="statLowestStack"></span></li>
151 <li>Entry Points: <ul id="entryPointsList"></ul></li>
152 </ul>
153 </div>
154 <div id="sectSource" class="hidden">
155 <h2>Source Code</h2>
156 <pre><code id="sourceText"></code></pre>
157 </div>
158 <script src="main.js"></script>
159 </body>
160</html>
161
lib/fuzzer/web/main.js created+249
......@@ -0,0 +1,249 @@
1(function() {
2 const domStatus = document.getElementById("status");
3 const domSectSource = document.getElementById("sectSource");
4 const domSectStats = document.getElementById("sectStats");
5 const domSourceText = document.getElementById("sourceText");
6 const domStatTotalRuns = document.getElementById("statTotalRuns");
7 const domStatUniqueRuns = document.getElementById("statUniqueRuns");
8 const domStatCoverage = document.getElementById("statCoverage");
9 const domStatLowestStack = document.getElementById("statLowestStack");
10 const domEntryPointsList = document.getElementById("entryPointsList");
11
12 let wasm_promise = fetch("main.wasm");
13 let sources_promise = fetch("sources.tar").then(function(response) {
14 if (!response.ok) throw new Error("unable to download sources");
15 return response.arrayBuffer();
16 });
17 var wasm_exports = null;
18 var curNavSearch = null;
19 var curNavLocation = null;
20
21 const text_decoder = new TextDecoder();
22 const text_encoder = new TextEncoder();
23
24 domStatus.textContent = "Loading WebAssembly...";
25 WebAssembly.instantiateStreaming(wasm_promise, {
26 js: {
27 log: function(ptr, len) {
28 const msg = decodeString(ptr, len);
29 console.log(msg);
30 },
31 panic: function (ptr, len) {
32 const msg = decodeString(ptr, len);
33 throw new Error("panic: " + msg);
34 },
35 emitSourceIndexChange: onSourceIndexChange,
36 emitCoverageUpdate: onCoverageUpdate,
37 emitEntryPointsUpdate: renderStats,
38 },
39 }).then(function(obj) {
40 wasm_exports = obj.instance.exports;
41 window.wasm = obj; // for debugging
42 domStatus.textContent = "Loading sources tarball...";
43
44 sources_promise.then(function(buffer) {
45 domStatus.textContent = "Parsing sources...";
46 const js_array = new Uint8Array(buffer);
47 const ptr = wasm_exports.alloc(js_array.length);
48 const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
49 wasm_array.set(js_array);
50 wasm_exports.unpack(ptr, js_array.length);
51
52 window.addEventListener('popstate', onPopState, false);
53 onHashChange(null);
54
55 domStatus.textContent = "Waiting for server to send source location metadata...";
56 connectWebSocket();
57 });
58 });
59
60 function onPopState(ev) {
61 onHashChange(ev.state);
62 }
63
64 function onHashChange(state) {
65 history.replaceState({}, "");
66 navigate(location.hash);
67 if (state == null) window.scrollTo({top: 0});
68 }
69
70 function navigate(location_hash) {
71 domSectSource.classList.add("hidden");
72
73 curNavLocation = null;
74 curNavSearch = null;
75
76 if (location_hash.length > 1 && location_hash[0] === '#') {
77 const query = location_hash.substring(1);
78 const qpos = query.indexOf("?");
79 let nonSearchPart;
80 if (qpos === -1) {
81 nonSearchPart = query;
82 } else {
83 nonSearchPart = query.substring(0, qpos);
84 curNavSearch = decodeURIComponent(query.substring(qpos + 1));
85 }
86
87 if (nonSearchPart[0] == "l") {
88 curNavLocation = +nonSearchPart.substring(1);
89 renderSource(curNavLocation);
90 }
91 }
92
93 render();
94 }
95
96 function connectWebSocket() {
97 const host = document.location.host;
98 const pathname = document.location.pathname;
99 const isHttps = document.location.protocol === 'https:';
100 const match = host.match(/^(.+):(\d+)$/);
101 const defaultPort = isHttps ? 443 : 80;
102 const port = match ? parseInt(match[2], 10) : defaultPort;
103 const hostName = match ? match[1] : host;
104 const wsProto = isHttps ? "wss:" : "ws:";
105 const wsUrl = wsProto + '//' + hostName + ':' + port + pathname;
106 ws = new WebSocket(wsUrl);
107 ws.binaryType = "arraybuffer";
108 ws.addEventListener('message', onWebSocketMessage, false);
109 ws.addEventListener('error', timeoutThenCreateNew, false);
110 ws.addEventListener('close', timeoutThenCreateNew, false);
111 ws.addEventListener('open', onWebSocketOpen, false);
112 }
113
114 function onWebSocketOpen() {
115 //console.log("web socket opened");
116 }
117
118 function onWebSocketMessage(ev) {
119 wasmOnMessage(ev.data);
120 }
121
122 function timeoutThenCreateNew() {
123 ws.removeEventListener('message', onWebSocketMessage, false);
124 ws.removeEventListener('error', timeoutThenCreateNew, false);
125 ws.removeEventListener('close', timeoutThenCreateNew, false);
126 ws.removeEventListener('open', onWebSocketOpen, false);
127 ws = null;
128 setTimeout(connectWebSocket, 1000);
129 }
130
131 function wasmOnMessage(data) {
132 const jsArray = new Uint8Array(data);
133 const ptr = wasm_exports.message_begin(jsArray.length);
134 const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, jsArray.length);
135 wasmArray.set(jsArray);
136 wasm_exports.message_end();
137 }
138
139 function onSourceIndexChange() {
140 render();
141 if (curNavLocation != null) renderSource(curNavLocation);
142 }
143
144 function onCoverageUpdate() {
145 renderStats();
146 renderCoverage();
147 }
148
149 function render() {
150 domStatus.classList.add("hidden");
151 }
152
153 function renderStats() {
154 const totalRuns = wasm_exports.totalRuns();
155 const uniqueRuns = wasm_exports.uniqueRuns();
156 const totalSourceLocations = wasm_exports.totalSourceLocations();
157 const coveredSourceLocations = wasm_exports.coveredSourceLocations();
158 domStatTotalRuns.innerText = totalRuns;
159 domStatUniqueRuns.innerText = uniqueRuns + " (" + percent(uniqueRuns, totalRuns) + "%)";
160 domStatCoverage.innerText = coveredSourceLocations + " / " + totalSourceLocations + " (" + percent(coveredSourceLocations, totalSourceLocations) + "%)";
161 domStatLowestStack.innerText = unwrapString(wasm_exports.lowestStack());
162
163 const entryPoints = unwrapInt32Array(wasm_exports.entryPoints());
164 resizeDomList(domEntryPointsList, entryPoints.length, "<li></li>");
165 for (let i = 0; i < entryPoints.length; i += 1) {
166 const liDom = domEntryPointsList.children[i];
167 liDom.innerHTML = unwrapString(wasm_exports.sourceLocationLinkHtml(entryPoints[i]));
168 }
169
170
171 domSectStats.classList.remove("hidden");
172 }
173
174 function renderCoverage() {
175 if (curNavLocation == null) return;
176 const sourceLocationIndex = curNavLocation;
177
178 for (let i = 0; i < domSourceText.children.length; i += 1) {
179 const childDom = domSourceText.children[i];
180 if (childDom.id != null && childDom.id[0] == "l") {
181 childDom.classList.add("l");
182 childDom.classList.remove("c");
183 }
184 }
185 const coveredList = unwrapInt32Array(wasm_exports.sourceLocationFileCoveredList(sourceLocationIndex));
186 for (let i = 0; i < coveredList.length; i += 1) {
187 document.getElementById("l" + coveredList[i]).classList.add("c");
188 }
189 }
190
191 function resizeDomList(listDom, desiredLen, templateHtml) {
192 for (let i = listDom.childElementCount; i < desiredLen; i += 1) {
193 listDom.insertAdjacentHTML('beforeend', templateHtml);
194 }
195 while (desiredLen < listDom.childElementCount) {
196 listDom.removeChild(listDom.lastChild);
197 }
198 }
199
200 function percent(a, b) {
201 return ((Number(a) / Number(b)) * 100).toFixed(1);
202 }
203
204 function renderSource(sourceLocationIndex) {
205 const pathName = unwrapString(wasm_exports.sourceLocationPath(sourceLocationIndex));
206 if (pathName.length === 0) return;
207
208 const h2 = domSectSource.children[0];
209 h2.innerText = pathName;
210 domSourceText.innerHTML = unwrapString(wasm_exports.sourceLocationFileHtml(sourceLocationIndex));
211
212 domSectSource.classList.remove("hidden");
213
214 // Empirically, Firefox needs this requestAnimationFrame in order for the scrollIntoView to work.
215 requestAnimationFrame(function() {
216 const slDom = document.getElementById("l" + sourceLocationIndex);
217 if (slDom != null) slDom.scrollIntoView({
218 behavior: "smooth",
219 block: "center",
220 });
221 });
222 }
223
224 function decodeString(ptr, len) {
225 if (len === 0) return "";
226 return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
227 }
228
229 function unwrapInt32Array(bigint) {
230 const ptr = Number(bigint & 0xffffffffn);
231 const len = Number(bigint >> 32n);
232 if (len === 0) return new Uint32Array();
233 return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
234 }
235
236 function setInputString(s) {
237 const jsArray = text_encoder.encode(s);
238 const len = jsArray.length;
239 const ptr = wasm_exports.set_input_string(len);
240 const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
241 wasmArray.set(jsArray);
242 }
243
244 function unwrapString(bigint) {
245 const ptr = Number(bigint & 0xffffffffn);
246 const len = Number(bigint >> 32n);
247 return decodeString(ptr, len);
248 }
249})();
lib/fuzzer/web/main.zig created+428
......@@ -0,0 +1,428 @@
1const std = @import("std");
2const assert = std.debug.assert;
3const abi = std.Build.Fuzz.abi;
4const gpa = std.heap.wasm_allocator;
5const log = std.log;
6const Coverage = std.debug.Coverage;
7const Allocator = std.mem.Allocator;
8
9const Walk = @import("Walk");
10const Decl = Walk.Decl;
11const html_render = @import("html_render");
12
13const js = struct {
14 extern "js" fn log(ptr: [*]const u8, len: usize) void;
15 extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
16 extern "js" fn emitSourceIndexChange() void;
17 extern "js" fn emitCoverageUpdate() void;
18 extern "js" fn emitEntryPointsUpdate() void;
19};
20
21pub const std_options: std.Options = .{
22 .logFn = logFn,
23};
24
25pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn {
26 _ = st;
27 _ = addr;
28 log.err("panic: {s}", .{msg});
29 @trap();
30}
31
32fn logFn(
33 comptime message_level: log.Level,
34 comptime scope: @TypeOf(.enum_literal),
35 comptime format: []const u8,
36 args: anytype,
37) void {
38 const level_txt = comptime message_level.asText();
39 const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
40 var buf: [500]u8 = undefined;
41 const line = std.fmt.bufPrint(&buf, level_txt ++ prefix2 ++ format, args) catch l: {
42 buf[buf.len - 3 ..][0..3].* = "...".*;
43 break :l &buf;
44 };
45 js.log(line.ptr, line.len);
46}
47
48export fn alloc(n: usize) [*]u8 {
49 const slice = gpa.alloc(u8, n) catch @panic("OOM");
50 return slice.ptr;
51}
52
53var message_buffer: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
54
55/// Resizes the message buffer to be the correct length; returns the pointer to
56/// the query string.
57export fn message_begin(len: usize) [*]u8 {
58 message_buffer.resize(gpa, len) catch @panic("OOM");
59 return message_buffer.items.ptr;
60}
61
62export fn message_end() void {
63 const msg_bytes = message_buffer.items;
64
65 const tag: abi.ToClientTag = @enumFromInt(msg_bytes[0]);
66 switch (tag) {
67 .source_index => return sourceIndexMessage(msg_bytes) catch @panic("OOM"),
68 .coverage_update => return coverageUpdateMessage(msg_bytes) catch @panic("OOM"),
69 .entry_points => return entryPointsMessage(msg_bytes) catch @panic("OOM"),
70 _ => unreachable,
71 }
72}
73
74export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
75 const tar_bytes = tar_ptr[0..tar_len];
76 log.debug("received {d} bytes of tar file", .{tar_bytes.len});
77
78 unpackInner(tar_bytes) catch |err| {
79 fatal("unable to unpack tar: {s}", .{@errorName(err)});
80 };
81}
82
83/// Set by `set_input_string`.
84var input_string: std.ArrayListUnmanaged(u8) = .{};
85var string_result: std.ArrayListUnmanaged(u8) = .{};
86
87export fn set_input_string(len: usize) [*]u8 {
88 input_string.resize(gpa, len) catch @panic("OOM");
89 return input_string.items.ptr;
90}
91
92/// Looks up the root struct decl corresponding to a file by path.
93/// Uses `input_string`.
94export fn find_file_root() Decl.Index {
95 const file: Walk.File.Index = @enumFromInt(Walk.files.getIndex(input_string.items) orelse return .none);
96 return file.findRootDecl();
97}
98
99export fn decl_source_html(decl_index: Decl.Index) String {
100 const decl = decl_index.get();
101
102 string_result.clearRetainingCapacity();
103 html_render.fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
104 fatal("unable to render source: {s}", .{@errorName(err)});
105 };
106 return String.init(string_result.items);
107}
108
109export fn lowestStack() String {
110 const header: *abi.CoverageUpdateHeader = @ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]);
111 string_result.clearRetainingCapacity();
112 string_result.writer(gpa).print("0x{d}", .{header.lowest_stack}) catch @panic("OOM");
113 return String.init(string_result.items);
114}
115
116export fn totalSourceLocations() usize {
117 return coverage_source_locations.items.len;
118}
119
120export fn coveredSourceLocations() usize {
121 const covered_bits = recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..];
122 var count: usize = 0;
123 for (covered_bits) |byte| count += @popCount(byte);
124 return count;
125}
126
127export fn totalRuns() u64 {
128 const header: *abi.CoverageUpdateHeader = @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]));
129 return header.n_runs;
130}
131
132export fn uniqueRuns() u64 {
133 const header: *abi.CoverageUpdateHeader = @alignCast(@ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]));
134 return header.unique_runs;
135}
136
137const String = Slice(u8);
138
139fn Slice(T: type) type {
140 return packed struct(u64) {
141 ptr: u32,
142 len: u32,
143
144 fn init(s: []const T) @This() {
145 return .{
146 .ptr = @intFromPtr(s.ptr),
147 .len = s.len,
148 };
149 }
150 };
151}
152
153fn unpackInner(tar_bytes: []u8) !void {
154 var fbs = std.io.fixedBufferStream(tar_bytes);
155 var file_name_buffer: [1024]u8 = undefined;
156 var link_name_buffer: [1024]u8 = undefined;
157 var it = std.tar.iterator(fbs.reader(), .{
158 .file_name_buffer = &file_name_buffer,
159 .link_name_buffer = &link_name_buffer,
160 });
161 while (try it.next()) |tar_file| {
162 switch (tar_file.kind) {
163 .file => {
164 if (tar_file.size == 0 and tar_file.name.len == 0) break;
165 if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
166 log.debug("found file: '{s}'", .{tar_file.name});
167 const file_name = try gpa.dupe(u8, tar_file.name);
168 if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| {
169 const pkg_name = file_name[0..pkg_name_end];
170 const gop = try Walk.modules.getOrPut(gpa, pkg_name);
171 const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
172 if (!gop.found_existing or
173 std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
174 std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
175 {
176 gop.value_ptr.* = file;
177 }
178 const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)];
179 assert(file == try Walk.add_file(file_name, file_bytes));
180 }
181 } else {
182 log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name});
183 }
184 },
185 else => continue,
186 }
187 }
188}
189
190fn fatal(comptime format: []const u8, args: anytype) noreturn {
191 var buf: [500]u8 = undefined;
192 const line = std.fmt.bufPrint(&buf, format, args) catch l: {
193 buf[buf.len - 3 ..][0..3].* = "...".*;
194 break :l &buf;
195 };
196 js.panic(line.ptr, line.len);
197}
198
199fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void {
200 const Header = abi.SourceIndexHeader;
201 const header: Header = @bitCast(msg_bytes[0..@sizeOf(Header)].*);
202
203 const directories_start = @sizeOf(Header);
204 const directories_end = directories_start + header.directories_len * @sizeOf(Coverage.String);
205 const files_start = directories_end;
206 const files_end = files_start + header.files_len * @sizeOf(Coverage.File);
207 const source_locations_start = files_end;
208 const source_locations_end = source_locations_start + header.source_locations_len * @sizeOf(Coverage.SourceLocation);
209 const string_bytes = msg_bytes[source_locations_end..][0..header.string_bytes_len];
210
211 const directories: []const Coverage.String = @alignCast(std.mem.bytesAsSlice(Coverage.String, msg_bytes[directories_start..directories_end]));
212 const files: []const Coverage.File = @alignCast(std.mem.bytesAsSlice(Coverage.File, msg_bytes[files_start..files_end]));
213 const source_locations: []const Coverage.SourceLocation = @alignCast(std.mem.bytesAsSlice(Coverage.SourceLocation, msg_bytes[source_locations_start..source_locations_end]));
214
215 try updateCoverage(directories, files, source_locations, string_bytes);
216 js.emitSourceIndexChange();
217}
218
219fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void {
220 recent_coverage_update.clearRetainingCapacity();
221 recent_coverage_update.appendSlice(gpa, msg_bytes) catch @panic("OOM");
222 js.emitCoverageUpdate();
223}
224
225var entry_points: std.ArrayListUnmanaged(u32) = .{};
226
227fn entryPointsMessage(msg_bytes: []u8) error{OutOfMemory}!void {
228 const header: abi.EntryPointHeader = @bitCast(msg_bytes[0..@sizeOf(abi.EntryPointHeader)].*);
229 entry_points.resize(gpa, header.flags.locs_len) catch @panic("OOM");
230 @memcpy(entry_points.items, std.mem.bytesAsSlice(u32, msg_bytes[@sizeOf(abi.EntryPointHeader)..]));
231 js.emitEntryPointsUpdate();
232}
233
234export fn entryPoints() Slice(u32) {
235 return Slice(u32).init(entry_points.items);
236}
237
238/// Index into `coverage_source_locations`.
239const SourceLocationIndex = enum(u32) {
240 _,
241
242 fn haveCoverage(sli: SourceLocationIndex) bool {
243 return @intFromEnum(sli) < coverage_source_locations.items.len;
244 }
245
246 fn ptr(sli: SourceLocationIndex) *Coverage.SourceLocation {
247 return &coverage_source_locations.items[@intFromEnum(sli)];
248 }
249
250 fn sourceLocationLinkHtml(
251 sli: SourceLocationIndex,
252 out: *std.ArrayListUnmanaged(u8),
253 ) Allocator.Error!void {
254 const sl = sli.ptr();
255 try out.writer(gpa).print("<a href=\"#l{d}\">", .{@intFromEnum(sli)});
256 try sli.appendPath(out);
257 try out.writer(gpa).print(":{d}:{d}</a>", .{ sl.line, sl.column });
258 }
259
260 fn appendPath(sli: SourceLocationIndex, out: *std.ArrayListUnmanaged(u8)) Allocator.Error!void {
261 const sl = sli.ptr();
262 const file = coverage.fileAt(sl.file);
263 const file_name = coverage.stringAt(file.basename);
264 const dir_name = coverage.stringAt(coverage.directories.keys()[file.directory_index]);
265 try html_render.appendEscaped(out, dir_name);
266 try out.appendSlice(gpa, "/");
267 try html_render.appendEscaped(out, file_name);
268 }
269
270 fn toWalkFile(sli: SourceLocationIndex) ?Walk.File.Index {
271 var buf: std.ArrayListUnmanaged(u8) = .{};
272 defer buf.deinit(gpa);
273 sli.appendPath(&buf) catch @panic("OOM");
274 return @enumFromInt(Walk.files.getIndex(buf.items) orelse return null);
275 }
276
277 fn fileHtml(
278 sli: SourceLocationIndex,
279 out: *std.ArrayListUnmanaged(u8),
280 ) error{ OutOfMemory, SourceUnavailable }!void {
281 const walk_file_index = sli.toWalkFile() orelse return error.SourceUnavailable;
282 const root_node = walk_file_index.findRootDecl().get().ast_node;
283 var annotations: std.ArrayListUnmanaged(html_render.Annotation) = .{};
284 defer annotations.deinit(gpa);
285 try computeSourceAnnotations(sli.ptr().file, walk_file_index, &annotations, coverage_source_locations.items);
286 html_render.fileSourceHtml(walk_file_index, out, root_node, .{
287 .source_location_annotations = annotations.items,
288 }) catch |err| {
289 fatal("unable to render source: {s}", .{@errorName(err)});
290 };
291 }
292};
293
294fn computeSourceAnnotations(
295 cov_file_index: Coverage.File.Index,
296 walk_file_index: Walk.File.Index,
297 annotations: *std.ArrayListUnmanaged(html_render.Annotation),
298 source_locations: []const Coverage.SourceLocation,
299) !void {
300 // Collect all the source locations from only this file into this array
301 // first, then sort by line, col, so that we can collect annotations with
302 // O(N) time complexity.
303 var locs: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
304 defer locs.deinit(gpa);
305
306 for (source_locations, 0..) |sl, sli_usize| {
307 if (sl.file != cov_file_index) continue;
308 const sli: SourceLocationIndex = @enumFromInt(sli_usize);
309 try locs.append(gpa, sli);
310 }
311
312 std.mem.sortUnstable(SourceLocationIndex, locs.items, {}, struct {
313 pub fn lessThan(context: void, lhs: SourceLocationIndex, rhs: SourceLocationIndex) bool {
314 _ = context;
315 const lhs_ptr = lhs.ptr();
316 const rhs_ptr = rhs.ptr();
317 if (lhs_ptr.line < rhs_ptr.line) return true;
318 if (lhs_ptr.line > rhs_ptr.line) return false;
319 return lhs_ptr.column < rhs_ptr.column;
320 }
321 }.lessThan);
322
323 const source = walk_file_index.get_ast().source;
324 var line: usize = 1;
325 var column: usize = 1;
326 var next_loc_index: usize = 0;
327 for (source, 0..) |byte, offset| {
328 if (byte == '\n') {
329 line += 1;
330 column = 1;
331 } else {
332 column += 1;
333 }
334 while (true) {
335 if (next_loc_index >= locs.items.len) return;
336 const next_sli = locs.items[next_loc_index];
337 const next_sl = next_sli.ptr();
338 if (next_sl.line > line or (next_sl.line == line and next_sl.column >= column)) break;
339 try annotations.append(gpa, .{
340 .file_byte_offset = offset,
341 .dom_id = @intFromEnum(next_sli),
342 });
343 next_loc_index += 1;
344 }
345 }
346}
347
348var coverage = Coverage.init;
349/// Index of type `SourceLocationIndex`.
350var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{};
351/// Contains the most recent coverage update message, unmodified.
352var recent_coverage_update: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
353
354fn updateCoverage(
355 directories: []const Coverage.String,
356 files: []const Coverage.File,
357 source_locations: []const Coverage.SourceLocation,
358 string_bytes: []const u8,
359) !void {
360 coverage.directories.clearRetainingCapacity();
361 coverage.files.clearRetainingCapacity();
362 coverage.string_bytes.clearRetainingCapacity();
363 coverage_source_locations.clearRetainingCapacity();
364
365 try coverage_source_locations.appendSlice(gpa, source_locations);
366 try coverage.string_bytes.appendSlice(gpa, string_bytes);
367
368 try coverage.files.entries.resize(gpa, files.len);
369 @memcpy(coverage.files.entries.items(.key), files);
370 try coverage.files.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
371
372 try coverage.directories.entries.resize(gpa, directories.len);
373 @memcpy(coverage.directories.entries.items(.key), directories);
374 try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
375}
376
377export fn sourceLocationLinkHtml(index: SourceLocationIndex) String {
378 string_result.clearRetainingCapacity();
379 index.sourceLocationLinkHtml(&string_result) catch @panic("OOM");
380 return String.init(string_result.items);
381}
382
383/// Returns empty string if coverage metadata is not available for this source location.
384export fn sourceLocationPath(sli: SourceLocationIndex) String {
385 string_result.clearRetainingCapacity();
386 if (sli.haveCoverage()) sli.appendPath(&string_result) catch @panic("OOM");
387 return String.init(string_result.items);
388}
389
390export fn sourceLocationFileHtml(sli: SourceLocationIndex) String {
391 string_result.clearRetainingCapacity();
392 sli.fileHtml(&string_result) catch |err| switch (err) {
393 error.OutOfMemory => @panic("OOM"),
394 error.SourceUnavailable => {},
395 };
396 return String.init(string_result.items);
397}
398
399export fn sourceLocationFileCoveredList(sli_file: SourceLocationIndex) Slice(SourceLocationIndex) {
400 const global = struct {
401 var result: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
402 fn add(i: u32, want_file: Coverage.File.Index) void {
403 const src_loc_index: SourceLocationIndex = @enumFromInt(i);
404 if (src_loc_index.ptr().file == want_file) result.appendAssumeCapacity(src_loc_index);
405 }
406 };
407 const want_file = sli_file.ptr().file;
408 global.result.clearRetainingCapacity();
409
410 // This code assumes 64-bit elements, which is incorrect if the executable
411 // being fuzzed is not a 64-bit CPU. It also assumes little-endian which
412 // can also be incorrect.
413 comptime assert(abi.CoverageUpdateHeader.trailing[0] == .pc_bits_usize);
414 const n_bitset_elems = (coverage_source_locations.items.len + @bitSizeOf(u64) - 1) / @bitSizeOf(u64);
415 const covered_bits = std.mem.bytesAsSlice(
416 u64,
417 recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..][0 .. n_bitset_elems * @sizeOf(u64)],
418 );
419 var sli: u32 = 0;
420 for (covered_bits) |elem| {
421 global.result.ensureUnusedCapacity(gpa, 64) catch @panic("OOM");
422 for (0..@bitSizeOf(u64)) |i| {
423 if ((elem & (@as(u64, 1) << @intCast(i))) != 0) global.add(sli, want_file);
424 sli += 1;
425 }
426 }
427 return Slice(SourceLocationIndex).init(global.result.items);
428}
lib/std/Build/Fuzz/WebServer.zig+3-3
......@@ -128,11 +128,11 @@ fn serveRequest(ws: *WebServer, request: *std.http.Server.Request) !void {
128128 std.mem.eql(u8, request.head.target, "/debug") or
129129 std.mem.eql(u8, request.head.target, "/debug/"))
130130 {
131 try serveFile(ws, request, "fuzzer/index.html", "text/html");
131 try serveFile(ws, request, "fuzzer/web/index.html", "text/html");
132132 } else if (std.mem.eql(u8, request.head.target, "/main.js") or
133133 std.mem.eql(u8, request.head.target, "/debug/main.js"))
134134 {
135 try serveFile(ws, request, "fuzzer/main.js", "application/javascript");
135 try serveFile(ws, request, "fuzzer/web/main.js", "application/javascript");
136136 } else if (std.mem.eql(u8, request.head.target, "/main.wasm")) {
137137 try serveWasm(ws, request, .ReleaseFast);
138138 } else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) {
......@@ -217,7 +217,7 @@ fn buildWasmBinary(
217217
218218 const main_src_path: Build.Cache.Path = .{
219219 .root_dir = ws.zig_lib_directory,
220 .sub_path = "fuzzer/wasm/main.zig",
220 .sub_path = "fuzzer/web/main.zig",
221221 };
222222 const walk_src_path: Build.Cache.Path = .{
223223 .root_dir = ws.zig_lib_directory,