authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-08-07 11:55:30-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2024-08-07 11:55:30-07:00
log0e99f517f2a1c8c19d7350a221539521b365230e
treef487716661a5771d7c41f45106bf264da1ea133f
parentf9f894200891c8af6ce3a3ad222cd0bf1ee15587
parentd721d9af69220c059a7be825d295a0b53081c4a0
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #20958 from ziglang/fuzz

introduce a fuzz testing web interface

34 files changed, 3921 insertions(+), 1078 deletions(-)

README.md+3-5
...@@ -13,11 +13,9 @@ Documentation** corresponding to the version of Zig that you are using by...@@ -13,11 +13,9 @@ Documentation** corresponding to the version of Zig that you are using by
13following the appropriate link on the13following the appropriate link on the
14[download page](https://ziglang.org/download).14[download page](https://ziglang.org/download).
1515
16Otherwise, you're looking at a release of Zig, and you can find documentation16Otherwise, you're looking at a release of Zig, so you can find the language
17here:17reference at `doc/langref.html`, and the standard library documentation by
1818running `zig std`, which will open a browser tab.
19 * doc/langref.html
20 * doc/std/index.html
2119
22## Installation20## Installation
2321
lib/compiler/build_runner.zig+36-1
...@@ -17,6 +17,12 @@ const runner = @This();...@@ -17,6 +17,12 @@ const runner = @This();
17pub const root = @import("@build");17pub const root = @import("@build");
18pub const dependencies = @import("@dependencies");18pub const dependencies = @import("@dependencies");
1919
20pub const std_options: std.Options = .{
21 .side_channels_mitigations = .none,
22 .http_disable_tls = true,
23 .crypto_fork_safety = false,
24};
25
20pub fn main() !void {26pub fn main() !void {
21 // Here we use an ArenaAllocator backed by a page allocator because a build is a short-lived,27 // Here we use an ArenaAllocator backed by a page allocator because a build is a short-lived,
22 // one shot program. We don't need to waste time freeing memory and finding places to squish28 // one shot program. We don't need to waste time freeing memory and finding places to squish
...@@ -106,6 +112,7 @@ pub fn main() !void {...@@ -106,6 +112,7 @@ pub fn main() !void {
106 var watch = false;112 var watch = false;
107 var fuzz = false;113 var fuzz = false;
108 var debounce_interval_ms: u16 = 50;114 var debounce_interval_ms: u16 = 50;
115 var listen_port: u16 = 0;
109116
110 while (nextArg(args, &arg_idx)) |arg| {117 while (nextArg(args, &arg_idx)) |arg| {
111 if (mem.startsWith(u8, arg, "-Z")) {118 if (mem.startsWith(u8, arg, "-Z")) {
...@@ -203,6 +210,14 @@ pub fn main() !void {...@@ -203,6 +210,14 @@ pub fn main() !void {
203 next_arg, @errorName(err),210 next_arg, @errorName(err),
204 });211 });
205 };212 };
213 } else if (mem.eql(u8, arg, "--port")) {
214 const next_arg = nextArg(args, &arg_idx) orelse
215 fatalWithHint("expected u16 after '{s}'", .{arg});
216 listen_port = std.fmt.parseUnsigned(u16, next_arg, 10) catch |err| {
217 fatal("unable to parse port '{s}' as unsigned 16-bit integer: {s}\n", .{
218 next_arg, @errorName(err),
219 });
220 };
206 } else if (mem.eql(u8, arg, "--debug-log")) {221 } else if (mem.eql(u8, arg, "--debug-log")) {
207 const next_arg = nextArgOrFatal(args, &arg_idx);222 const next_arg = nextArgOrFatal(args, &arg_idx);
208 try debug_log_scopes.append(next_arg);223 try debug_log_scopes.append(next_arg);
...@@ -403,7 +418,27 @@ pub fn main() !void {...@@ -403,7 +418,27 @@ pub fn main() !void {
403 else => return err,418 else => return err,
404 };419 };
405 if (fuzz) {420 if (fuzz) {
406 Fuzz.start(&run.thread_pool, run.step_stack.keys(), run.ttyconf, main_progress_node);421 switch (builtin.os.tag) {
422 // Current implementation depends on two things that need to be ported to Windows:
423 // * Memory-mapping to share data between the fuzzer and build runner.
424 // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
425 // many addresses to source locations).
426 .windows => fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}),
427 else => {},
428 }
429 const listen_address = std.net.Address.parseIp("127.0.0.1", listen_port) catch unreachable;
430 try Fuzz.start(
431 gpa,
432 arena,
433 global_cache_directory,
434 zig_lib_directory,
435 zig_exe,
436 &run.thread_pool,
437 run.step_stack.keys(),
438 run.ttyconf,
439 listen_address,
440 main_progress_node,
441 );
407 }442 }
408443
409 if (!watch) return cleanExit();444 if (!watch) return cleanExit();
lib/compiler/std-docs.zig+4-5
...@@ -275,10 +275,6 @@ fn buildWasmBinary(...@@ -275,10 +275,6 @@ fn buildWasmBinary(
275) ![]const u8 {275) ![]const u8 {
276 const gpa = context.gpa;276 const gpa = context.gpa;
277277
278 const main_src_path = try std.fs.path.join(arena, &.{
279 context.zig_lib_directory, "docs", "wasm", "main.zig",
280 });
281
282 var argv: std.ArrayListUnmanaged([]const u8) = .{};278 var argv: std.ArrayListUnmanaged([]const u8) = .{};
283279
284 try argv.appendSlice(arena, &.{280 try argv.appendSlice(arena, &.{
...@@ -298,7 +294,10 @@ fn buildWasmBinary(...@@ -298,7 +294,10 @@ fn buildWasmBinary(
298 "--name",294 "--name",
299 "autodoc",295 "autodoc",
300 "-rdynamic",296 "-rdynamic",
301 main_src_path,297 "--dep",
298 "Walk",
299 try std.fmt.allocPrint(arena, "-Mroot={s}/docs/wasm/main.zig", .{context.zig_lib_directory}),
300 try std.fmt.allocPrint(arena, "-MWalk={s}/docs/wasm/Walk.zig", .{context.zig_lib_directory}),
302 "--listen=-",301 "--listen=-",
303 });302 });
304303
lib/compiler/test_runner.zig+33-1
...@@ -1,8 +1,10 @@...@@ -1,8 +1,10 @@
1//! Default test runner for unit tests.1//! Default test runner for unit tests.
2const builtin = @import("builtin");2const builtin = @import("builtin");
3
3const std = @import("std");4const std = @import("std");
4const io = std.io;5const io = std.io;
5const testing = std.testing;6const testing = std.testing;
7const assert = std.debug.assert;
68
7pub const std_options = .{9pub const std_options = .{
8 .logFn = log,10 .logFn = log,
...@@ -28,6 +30,7 @@ pub fn main() void {...@@ -28,6 +30,7 @@ pub fn main() void {
28 @panic("unable to parse command line args");30 @panic("unable to parse command line args");
2931
30 var listen = false;32 var listen = false;
33 var opt_cache_dir: ?[]const u8 = null;
3134
32 for (args[1..]) |arg| {35 for (args[1..]) |arg| {
33 if (std.mem.eql(u8, arg, "--listen=-")) {36 if (std.mem.eql(u8, arg, "--listen=-")) {
...@@ -35,12 +38,18 @@ pub fn main() void {...@@ -35,12 +38,18 @@ pub fn main() void {
35 } else if (std.mem.startsWith(u8, arg, "--seed=")) {38 } else if (std.mem.startsWith(u8, arg, "--seed=")) {
36 testing.random_seed = std.fmt.parseUnsigned(u32, arg["--seed=".len..], 0) catch39 testing.random_seed = std.fmt.parseUnsigned(u32, arg["--seed=".len..], 0) catch
37 @panic("unable to parse --seed command line argument");40 @panic("unable to parse --seed command line argument");
41 } else if (std.mem.startsWith(u8, arg, "--cache-dir")) {
42 opt_cache_dir = arg["--cache-dir=".len..];
38 } else {43 } else {
39 @panic("unrecognized command line argument");44 @panic("unrecognized command line argument");
40 }45 }
41 }46 }
4247
43 fba.reset();48 fba.reset();
49 if (builtin.fuzz) {
50 const cache_dir = opt_cache_dir orelse @panic("missing --cache-dir=[path] argument");
51 fuzzer_init(FuzzerSlice.fromSlice(cache_dir));
52 }
4453
45 if (listen) {54 if (listen) {
46 return mainServer() catch @panic("internal test runner failure");55 return mainServer() catch @panic("internal test runner failure");
...@@ -59,6 +68,11 @@ fn mainServer() !void {...@@ -59,6 +68,11 @@ fn mainServer() !void {
59 });68 });
60 defer server.deinit();69 defer server.deinit();
6170
71 if (builtin.fuzz) {
72 const coverage_id = fuzzer_coverage_id();
73 try server.serveU64Message(.coverage_id, coverage_id);
74 }
75
62 while (true) {76 while (true) {
63 const hdr = try server.receiveMessage();77 const hdr = try server.receiveMessage();
64 switch (hdr.tag) {78 switch (hdr.tag) {
...@@ -129,7 +143,9 @@ fn mainServer() !void {...@@ -129,7 +143,9 @@ fn mainServer() !void {
129 });143 });
130 },144 },
131 .start_fuzzing => {145 .start_fuzzing => {
146 if (!builtin.fuzz) unreachable;
132 const index = try server.receiveBody_u32();147 const index = try server.receiveBody_u32();
148 var first = true;
133 const test_fn = builtin.test_functions[index];149 const test_fn = builtin.test_functions[index];
134 while (true) {150 while (true) {
135 testing.allocator_instance = .{};151 testing.allocator_instance = .{};
...@@ -148,6 +164,10 @@ fn mainServer() !void {...@@ -148,6 +164,10 @@ fn mainServer() !void {
148 };164 };
149 if (!is_fuzz_test) @panic("missed call to std.testing.fuzzInput");165 if (!is_fuzz_test) @panic("missed call to std.testing.fuzzInput");
150 if (log_err_count != 0) @panic("error logs detected");166 if (log_err_count != 0) @panic("error logs detected");
167 if (first) {
168 first = false;
169 try server.serveU64Message(.fuzz_start_addr, entry_addr);
170 }
151 }171 }
152 },172 },
153173
...@@ -315,20 +335,32 @@ const FuzzerSlice = extern struct {...@@ -315,20 +335,32 @@ const FuzzerSlice = extern struct {
315 ptr: [*]const u8,335 ptr: [*]const u8,
316 len: usize,336 len: usize,
317337
338 /// Inline to avoid fuzzer instrumentation.
318 inline fn toSlice(s: FuzzerSlice) []const u8 {339 inline fn toSlice(s: FuzzerSlice) []const u8 {
319 return s.ptr[0..s.len];340 return s.ptr[0..s.len];
320 }341 }
342
343 /// Inline to avoid fuzzer instrumentation.
344 inline fn fromSlice(s: []const u8) FuzzerSlice {
345 return .{ .ptr = s.ptr, .len = s.len };
346 }
321};347};
322348
323var is_fuzz_test: bool = undefined;349var is_fuzz_test: bool = undefined;
350var entry_addr: usize = 0;
324351
325extern fn fuzzer_next() FuzzerSlice;352extern fn fuzzer_next() FuzzerSlice;
353extern fn fuzzer_init(cache_dir: FuzzerSlice) void;
354extern fn fuzzer_coverage_id() u64;
326355
327pub fn fuzzInput(options: testing.FuzzInputOptions) []const u8 {356pub fn fuzzInput(options: testing.FuzzInputOptions) []const u8 {
328 @disableInstrumentation();357 @disableInstrumentation();
329 if (crippled) return "";358 if (crippled) return "";
330 is_fuzz_test = true;359 is_fuzz_test = true;
331 if (builtin.fuzz) return fuzzer_next().toSlice();360 if (builtin.fuzz) {
361 if (entry_addr == 0) entry_addr = @returnAddress();
362 return fuzzer_next().toSlice();
363 }
332 if (options.corpus.len == 0) return "";364 if (options.corpus.len == 0) return "";
333 var prng = std.Random.DefaultPrng.init(testing.random_seed);365 var prng = std.Random.DefaultPrng.init(testing.random_seed);
334 const random = prng.random();366 const random = prng.random();
lib/docs/wasm/Decl.zig+9-9
...@@ -1,3 +1,12 @@...@@ -1,3 +1,12 @@
1const Decl = @This();
2const std = @import("std");
3const Ast = std.zig.Ast;
4const Walk = @import("Walk.zig");
5const gpa = std.heap.wasm_allocator;
6const assert = std.debug.assert;
7const log = std.log;
8const Oom = error{OutOfMemory};
9
1ast_node: Ast.Node.Index,10ast_node: Ast.Node.Index,
2file: Walk.File.Index,11file: Walk.File.Index,
3/// The decl whose namespace this is in.12/// The decl whose namespace this is in.
...@@ -215,12 +224,3 @@ pub fn find(search_string: []const u8) Decl.Index {...@@ -215,12 +224,3 @@ pub fn find(search_string: []const u8) Decl.Index {
215 }224 }
216 return current_decl_index;225 return current_decl_index;
217}226}
218
219const Decl = @This();
220const std = @import("std");
221const Ast = std.zig.Ast;
222const Walk = @import("Walk.zig");
223const gpa = std.heap.wasm_allocator;
224const assert = std.debug.assert;
225const log = std.log;
226const Oom = error{OutOfMemory};
lib/docs/wasm/Walk.zig+11-9
...@@ -1,4 +1,15 @@...@@ -1,4 +1,15 @@
1//! Find and annotate identifiers with links to their declarations.1//! Find and annotate identifiers with links to their declarations.
2
3const Walk = @This();
4const std = @import("std");
5const Ast = std.zig.Ast;
6const assert = std.debug.assert;
7const log = std.log;
8const gpa = std.heap.wasm_allocator;
9const Oom = error{OutOfMemory};
10
11pub const Decl = @import("Decl.zig");
12
2pub var files: std.StringArrayHashMapUnmanaged(File) = .{};13pub var files: std.StringArrayHashMapUnmanaged(File) = .{};
3pub var decls: std.ArrayListUnmanaged(Decl) = .{};14pub var decls: std.ArrayListUnmanaged(Decl) = .{};
4pub var modules: std.StringArrayHashMapUnmanaged(File.Index) = .{};15pub var modules: std.StringArrayHashMapUnmanaged(File.Index) = .{};
...@@ -1120,15 +1131,6 @@ pub fn isPrimitiveNonType(name: []const u8) bool {...@@ -1120,15 +1131,6 @@ pub fn isPrimitiveNonType(name: []const u8) bool {
1120// try w.root();1131// try w.root();
1121//}1132//}
11221133
1123const Walk = @This();
1124const std = @import("std");
1125const Ast = std.zig.Ast;
1126const assert = std.debug.assert;
1127const Decl = @import("Decl.zig");
1128const log = std.log;
1129const gpa = std.heap.wasm_allocator;
1130const Oom = error{OutOfMemory};
1131
1132fn shrinkToFit(m: anytype) void {1134fn shrinkToFit(m: anytype) void {
1133 m.shrinkAndFree(gpa, m.entries.len);1135 m.shrinkAndFree(gpa, m.entries.len);
1134}1136}
lib/docs/wasm/html_render.zig created+412
...@@ -0,0 +1,412 @@
1const std = @import("std");
2const Ast = std.zig.Ast;
3const assert = std.debug.assert;
4
5const Walk = @import("Walk");
6const Decl = Walk.Decl;
7
8const gpa = std.heap.wasm_allocator;
9const Oom = error{OutOfMemory};
10
11/// Delete this to find out where URL escaping needs to be added.
12pub const missing_feature_url_escape = true;
13
14pub const RenderSourceOptions = struct {
15 skip_doc_comments: bool = false,
16 skip_comments: bool = false,
17 collapse_whitespace: bool = false,
18 fn_link: Decl.Index = .none,
19 /// Assumed to be sorted ascending.
20 source_location_annotations: []const Annotation = &.{},
21 /// Concatenated with dom_id.
22 annotation_prefix: []const u8 = "l",
23};
24
25pub const Annotation = struct {
26 file_byte_offset: u32,
27 /// Concatenated with annotation_prefix.
28 dom_id: u32,
29};
30
31pub fn fileSourceHtml(
32 file_index: Walk.File.Index,
33 out: *std.ArrayListUnmanaged(u8),
34 root_node: Ast.Node.Index,
35 options: RenderSourceOptions,
36) !void {
37 const ast = file_index.get_ast();
38 const file = file_index.get();
39
40 const g = struct {
41 var field_access_buffer: std.ArrayListUnmanaged(u8) = .{};
42 };
43
44 const token_tags = ast.tokens.items(.tag);
45 const token_starts = ast.tokens.items(.start);
46 const main_tokens = ast.nodes.items(.main_token);
47
48 const start_token = ast.firstToken(root_node);
49 const end_token = ast.lastToken(root_node) + 1;
50
51 var cursor: usize = token_starts[start_token];
52
53 var indent: usize = 0;
54 if (std.mem.lastIndexOf(u8, ast.source[0..cursor], "\n")) |newline_index| {
55 for (ast.source[newline_index + 1 .. cursor]) |c| {
56 if (c == ' ') {
57 indent += 1;
58 } else {
59 break;
60 }
61 }
62 }
63
64 var next_annotate_index: usize = 0;
65
66 for (
67 token_tags[start_token..end_token],
68 token_starts[start_token..end_token],
69 start_token..,
70 ) |tag, start, token_index| {
71 const between = ast.source[cursor..start];
72 if (std.mem.trim(u8, between, " \t\r\n").len > 0) {
73 if (!options.skip_comments) {
74 try out.appendSlice(gpa, "<span class=\"tok-comment\">");
75 try appendUnindented(out, between, indent);
76 try out.appendSlice(gpa, "</span>");
77 }
78 } else if (between.len > 0) {
79 if (options.collapse_whitespace) {
80 if (out.items.len > 0 and out.items[out.items.len - 1] != ' ')
81 try out.append(gpa, ' ');
82 } else {
83 try appendUnindented(out, between, indent);
84 }
85 }
86 if (tag == .eof) break;
87 const slice = ast.tokenSlice(token_index);
88 cursor = start + slice.len;
89
90 // Insert annotations.
91 while (true) {
92 if (next_annotate_index >= options.source_location_annotations.len) break;
93 const next_annotation = options.source_location_annotations[next_annotate_index];
94 if (cursor <= next_annotation.file_byte_offset) break;
95 try out.writer(gpa).print("<span id=\"{s}{d}\"></span>", .{
96 options.annotation_prefix, next_annotation.dom_id,
97 });
98 next_annotate_index += 1;
99 }
100
101 switch (tag) {
102 .eof => unreachable,
103
104 .keyword_addrspace,
105 .keyword_align,
106 .keyword_and,
107 .keyword_asm,
108 .keyword_async,
109 .keyword_await,
110 .keyword_break,
111 .keyword_catch,
112 .keyword_comptime,
113 .keyword_const,
114 .keyword_continue,
115 .keyword_defer,
116 .keyword_else,
117 .keyword_enum,
118 .keyword_errdefer,
119 .keyword_error,
120 .keyword_export,
121 .keyword_extern,
122 .keyword_for,
123 .keyword_if,
124 .keyword_inline,
125 .keyword_noalias,
126 .keyword_noinline,
127 .keyword_nosuspend,
128 .keyword_opaque,
129 .keyword_or,
130 .keyword_orelse,
131 .keyword_packed,
132 .keyword_anyframe,
133 .keyword_pub,
134 .keyword_resume,
135 .keyword_return,
136 .keyword_linksection,
137 .keyword_callconv,
138 .keyword_struct,
139 .keyword_suspend,
140 .keyword_switch,
141 .keyword_test,
142 .keyword_threadlocal,
143 .keyword_try,
144 .keyword_union,
145 .keyword_unreachable,
146 .keyword_usingnamespace,
147 .keyword_var,
148 .keyword_volatile,
149 .keyword_allowzero,
150 .keyword_while,
151 .keyword_anytype,
152 .keyword_fn,
153 => {
154 try out.appendSlice(gpa, "<span class=\"tok-kw\">");
155 try appendEscaped(out, slice);
156 try out.appendSlice(gpa, "</span>");
157 },
158
159 .string_literal,
160 .char_literal,
161 .multiline_string_literal_line,
162 => {
163 try out.appendSlice(gpa, "<span class=\"tok-str\">");
164 try appendEscaped(out, slice);
165 try out.appendSlice(gpa, "</span>");
166 },
167
168 .builtin => {
169 try out.appendSlice(gpa, "<span class=\"tok-builtin\">");
170 try appendEscaped(out, slice);
171 try out.appendSlice(gpa, "</span>");
172 },
173
174 .doc_comment,
175 .container_doc_comment,
176 => {
177 if (!options.skip_doc_comments) {
178 try out.appendSlice(gpa, "<span class=\"tok-comment\">");
179 try appendEscaped(out, slice);
180 try out.appendSlice(gpa, "</span>");
181 }
182 },
183
184 .identifier => i: {
185 if (options.fn_link != .none) {
186 const fn_link = options.fn_link.get();
187 const fn_token = main_tokens[fn_link.ast_node];
188 if (token_index == fn_token + 1) {
189 try out.appendSlice(gpa, "<a class=\"tok-fn\" href=\"#");
190 _ = missing_feature_url_escape;
191 try fn_link.fqn(out);
192 try out.appendSlice(gpa, "\">");
193 try appendEscaped(out, slice);
194 try out.appendSlice(gpa, "</a>");
195 break :i;
196 }
197 }
198
199 if (token_index > 0 and token_tags[token_index - 1] == .keyword_fn) {
200 try out.appendSlice(gpa, "<span class=\"tok-fn\">");
201 try appendEscaped(out, slice);
202 try out.appendSlice(gpa, "</span>");
203 break :i;
204 }
205
206 if (Walk.isPrimitiveNonType(slice)) {
207 try out.appendSlice(gpa, "<span class=\"tok-null\">");
208 try appendEscaped(out, slice);
209 try out.appendSlice(gpa, "</span>");
210 break :i;
211 }
212
213 if (std.zig.primitives.isPrimitive(slice)) {
214 try out.appendSlice(gpa, "<span class=\"tok-type\">");
215 try appendEscaped(out, slice);
216 try out.appendSlice(gpa, "</span>");
217 break :i;
218 }
219
220 if (file.token_parents.get(token_index)) |field_access_node| {
221 g.field_access_buffer.clearRetainingCapacity();
222 try walkFieldAccesses(file_index, &g.field_access_buffer, field_access_node);
223 if (g.field_access_buffer.items.len > 0) {
224 try out.appendSlice(gpa, "<a href=\"#");
225 _ = missing_feature_url_escape;
226 try out.appendSlice(gpa, g.field_access_buffer.items);
227 try out.appendSlice(gpa, "\">");
228 try appendEscaped(out, slice);
229 try out.appendSlice(gpa, "</a>");
230 } else {
231 try appendEscaped(out, slice);
232 }
233 break :i;
234 }
235
236 {
237 g.field_access_buffer.clearRetainingCapacity();
238 try resolveIdentLink(file_index, &g.field_access_buffer, token_index);
239 if (g.field_access_buffer.items.len > 0) {
240 try out.appendSlice(gpa, "<a href=\"#");
241 _ = missing_feature_url_escape;
242 try out.appendSlice(gpa, g.field_access_buffer.items);
243 try out.appendSlice(gpa, "\">");
244 try appendEscaped(out, slice);
245 try out.appendSlice(gpa, "</a>");
246 break :i;
247 }
248 }
249
250 try appendEscaped(out, slice);
251 },
252
253 .number_literal => {
254 try out.appendSlice(gpa, "<span class=\"tok-number\">");
255 try appendEscaped(out, slice);
256 try out.appendSlice(gpa, "</span>");
257 },
258
259 .bang,
260 .pipe,
261 .pipe_pipe,
262 .pipe_equal,
263 .equal,
264 .equal_equal,
265 .equal_angle_bracket_right,
266 .bang_equal,
267 .l_paren,
268 .r_paren,
269 .semicolon,
270 .percent,
271 .percent_equal,
272 .l_brace,
273 .r_brace,
274 .l_bracket,
275 .r_bracket,
276 .period,
277 .period_asterisk,
278 .ellipsis2,
279 .ellipsis3,
280 .caret,
281 .caret_equal,
282 .plus,
283 .plus_plus,
284 .plus_equal,
285 .plus_percent,
286 .plus_percent_equal,
287 .plus_pipe,
288 .plus_pipe_equal,
289 .minus,
290 .minus_equal,
291 .minus_percent,
292 .minus_percent_equal,
293 .minus_pipe,
294 .minus_pipe_equal,
295 .asterisk,
296 .asterisk_equal,
297 .asterisk_asterisk,
298 .asterisk_percent,
299 .asterisk_percent_equal,
300 .asterisk_pipe,
301 .asterisk_pipe_equal,
302 .arrow,
303 .colon,
304 .slash,
305 .slash_equal,
306 .comma,
307 .ampersand,
308 .ampersand_equal,
309 .question_mark,
310 .angle_bracket_left,
311 .angle_bracket_left_equal,
312 .angle_bracket_angle_bracket_left,
313 .angle_bracket_angle_bracket_left_equal,
314 .angle_bracket_angle_bracket_left_pipe,
315 .angle_bracket_angle_bracket_left_pipe_equal,
316 .angle_bracket_right,
317 .angle_bracket_right_equal,
318 .angle_bracket_angle_bracket_right,
319 .angle_bracket_angle_bracket_right_equal,
320 .tilde,
321 => try appendEscaped(out, slice),
322
323 .invalid, .invalid_periodasterisks => return error.InvalidToken,
324 }
325 }
326}
327
328fn appendUnindented(out: *std.ArrayListUnmanaged(u8), s: []const u8, indent: usize) !void {
329 var it = std.mem.splitScalar(u8, s, '\n');
330 var is_first_line = true;
331 while (it.next()) |line| {
332 if (is_first_line) {
333 try appendEscaped(out, line);
334 is_first_line = false;
335 } else {
336 try out.appendSlice(gpa, "\n");
337 try appendEscaped(out, unindent(line, indent));
338 }
339 }
340}
341
342pub fn appendEscaped(out: *std.ArrayListUnmanaged(u8), s: []const u8) !void {
343 for (s) |c| {
344 try out.ensureUnusedCapacity(gpa, 6);
345 switch (c) {
346 '&' => out.appendSliceAssumeCapacity("&amp;"),
347 '<' => out.appendSliceAssumeCapacity("&lt;"),
348 '>' => out.appendSliceAssumeCapacity("&gt;"),
349 '"' => out.appendSliceAssumeCapacity("&quot;"),
350 else => out.appendAssumeCapacity(c),
351 }
352 }
353}
354
355fn walkFieldAccesses(
356 file_index: Walk.File.Index,
357 out: *std.ArrayListUnmanaged(u8),
358 node: Ast.Node.Index,
359) Oom!void {
360 const ast = file_index.get_ast();
361 const node_tags = ast.nodes.items(.tag);
362 assert(node_tags[node] == .field_access);
363 const node_datas = ast.nodes.items(.data);
364 const main_tokens = ast.nodes.items(.main_token);
365 const object_node = node_datas[node].lhs;
366 const dot_token = main_tokens[node];
367 const field_ident = dot_token + 1;
368 switch (node_tags[object_node]) {
369 .identifier => {
370 const lhs_ident = main_tokens[object_node];
371 try resolveIdentLink(file_index, out, lhs_ident);
372 },
373 .field_access => {
374 try walkFieldAccesses(file_index, out, object_node);
375 },
376 else => {},
377 }
378 if (out.items.len > 0) {
379 try out.append(gpa, '.');
380 try out.appendSlice(gpa, ast.tokenSlice(field_ident));
381 }
382}
383
384fn resolveIdentLink(
385 file_index: Walk.File.Index,
386 out: *std.ArrayListUnmanaged(u8),
387 ident_token: Ast.TokenIndex,
388) Oom!void {
389 const decl_index = file_index.get().lookup_token(ident_token);
390 if (decl_index == .none) return;
391 try resolveDeclLink(decl_index, out);
392}
393
394fn unindent(s: []const u8, indent: usize) []const u8 {
395 var indent_idx: usize = 0;
396 for (s) |c| {
397 if (c == ' ' and indent_idx < indent) {
398 indent_idx += 1;
399 } else {
400 break;
401 }
402 }
403 return s[indent_idx..];
404}
405
406pub fn resolveDeclLink(decl_index: Decl.Index, out: *std.ArrayListUnmanaged(u8)) Oom!void {
407 const decl = decl_index.get();
408 switch (decl.categorize()) {
409 .alias => |alias_decl| try alias_decl.get().fqn(out),
410 else => try decl.fqn(out),
411 }
412}
lib/docs/wasm/main.zig+18-392
...@@ -1,15 +1,17 @@...@@ -1,15 +1,17 @@
1/// Delete this to find out where URL escaping needs to be added.
2const missing_feature_url_escape = true;
3
4const gpa = std.heap.wasm_allocator;
5
6const std = @import("std");1const std = @import("std");
7const log = std.log;2const log = std.log;
8const assert = std.debug.assert;3const assert = std.debug.assert;
9const Ast = std.zig.Ast;4const Ast = std.zig.Ast;
10const Walk = @import("Walk.zig");5const Walk = @import("Walk");
11const markdown = @import("markdown.zig");6const markdown = @import("markdown.zig");
12const Decl = @import("Decl.zig");7const Decl = Walk.Decl;
8
9const fileSourceHtml = @import("html_render.zig").fileSourceHtml;
10const appendEscaped = @import("html_render.zig").appendEscaped;
11const resolveDeclLink = @import("html_render.zig").resolveDeclLink;
12const missing_feature_url_escape = @import("html_render.zig").missing_feature_url_escape;
13
14const gpa = std.heap.wasm_allocator;
1315
14const js = struct {16const js = struct {
15 extern "js" fn log(ptr: [*]const u8, len: usize) void;17 extern "js" fn log(ptr: [*]const u8, len: usize) void;
...@@ -53,7 +55,7 @@ export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {...@@ -53,7 +55,7 @@ export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
53 const tar_bytes = tar_ptr[0..tar_len];55 const tar_bytes = tar_ptr[0..tar_len];
54 //log.debug("received {d} bytes of tar file", .{tar_bytes.len});56 //log.debug("received {d} bytes of tar file", .{tar_bytes.len});
5557
56 unpack_inner(tar_bytes) catch |err| {58 unpackInner(tar_bytes) catch |err| {
57 fatal("unable to unpack tar: {s}", .{@errorName(err)});59 fatal("unable to unpack tar: {s}", .{@errorName(err)});
58 };60 };
59}61}
...@@ -439,7 +441,7 @@ fn decl_field_html_fallible(...@@ -439,7 +441,7 @@ fn decl_field_html_fallible(
439 const decl = decl_index.get();441 const decl = decl_index.get();
440 const ast = decl.file.get_ast();442 const ast = decl.file.get_ast();
441 try out.appendSlice(gpa, "<pre><code>");443 try out.appendSlice(gpa, "<pre><code>");
442 try file_source_html(decl.file, out, field_node, .{});444 try fileSourceHtml(decl.file, out, field_node, .{});
443 try out.appendSlice(gpa, "</code></pre>");445 try out.appendSlice(gpa, "</code></pre>");
444446
445 const field = ast.fullContainerField(field_node).?;447 const field = ast.fullContainerField(field_node).?;
...@@ -478,7 +480,7 @@ fn decl_param_html_fallible(...@@ -478,7 +480,7 @@ fn decl_param_html_fallible(
478 try out.appendSlice(gpa, "<pre><code>");480 try out.appendSlice(gpa, "<pre><code>");
479 try appendEscaped(out, name);481 try appendEscaped(out, name);
480 try out.appendSlice(gpa, ": ");482 try out.appendSlice(gpa, ": ");
481 try file_source_html(decl.file, out, param_node, .{});483 try fileSourceHtml(decl.file, out, param_node, .{});
482 try out.appendSlice(gpa, "</code></pre>");484 try out.appendSlice(gpa, "</code></pre>");
483485
484 if (ast.tokens.items(.tag)[first_doc_comment] == .doc_comment) {486 if (ast.tokens.items(.tag)[first_doc_comment] == .doc_comment) {
...@@ -506,7 +508,7 @@ export fn decl_fn_proto_html(decl_index: Decl.Index, linkify_fn_name: bool) Stri...@@ -506,7 +508,7 @@ export fn decl_fn_proto_html(decl_index: Decl.Index, linkify_fn_name: bool) Stri
506 };508 };
507509
508 string_result.clearRetainingCapacity();510 string_result.clearRetainingCapacity();
509 file_source_html(decl.file, &string_result, proto_node, .{511 fileSourceHtml(decl.file, &string_result, proto_node, .{
510 .skip_doc_comments = true,512 .skip_doc_comments = true,
511 .skip_comments = true,513 .skip_comments = true,
512 .collapse_whitespace = true,514 .collapse_whitespace = true,
...@@ -521,7 +523,7 @@ export fn decl_source_html(decl_index: Decl.Index) String {...@@ -521,7 +523,7 @@ export fn decl_source_html(decl_index: Decl.Index) String {
521 const decl = decl_index.get();523 const decl = decl_index.get();
522524
523 string_result.clearRetainingCapacity();525 string_result.clearRetainingCapacity();
524 file_source_html(decl.file, &string_result, decl.ast_node, .{}) catch |err| {526 fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
525 fatal("unable to render source: {s}", .{@errorName(err)});527 fatal("unable to render source: {s}", .{@errorName(err)});
526 };528 };
527 return String.init(string_result.items);529 return String.init(string_result.items);
...@@ -533,7 +535,7 @@ export fn decl_doctest_html(decl_index: Decl.Index) String {...@@ -533,7 +535,7 @@ export fn decl_doctest_html(decl_index: Decl.Index) String {
533 return String.init("");535 return String.init("");
534536
535 string_result.clearRetainingCapacity();537 string_result.clearRetainingCapacity();
536 file_source_html(decl.file, &string_result, doctest_ast_node, .{}) catch |err| {538 fileSourceHtml(decl.file, &string_result, doctest_ast_node, .{}) catch |err| {
537 fatal("unable to render source: {s}", .{@errorName(err)});539 fatal("unable to render source: {s}", .{@errorName(err)});
538 };540 };
539 return String.init(string_result.items);541 return String.init(string_result.items);
...@@ -691,7 +693,7 @@ fn render_docs(...@@ -691,7 +693,7 @@ fn render_docs(
691 const content = doc.string(data.text.content);693 const content = doc.string(data.text.content);
692 if (resolve_decl_path(r.context, content)) |resolved_decl_index| {694 if (resolve_decl_path(r.context, content)) |resolved_decl_index| {
693 g.link_buffer.clearRetainingCapacity();695 g.link_buffer.clearRetainingCapacity();
694 try resolve_decl_link(resolved_decl_index, &g.link_buffer);696 try resolveDeclLink(resolved_decl_index, &g.link_buffer);
695697
696 try writer.writeAll("<a href=\"#");698 try writer.writeAll("<a href=\"#");
697 _ = missing_feature_url_escape;699 _ = missing_feature_url_escape;
...@@ -734,7 +736,7 @@ export fn decl_type_html(decl_index: Decl.Index) String {...@@ -734,7 +736,7 @@ export fn decl_type_html(decl_index: Decl.Index) String {
734 if (ast.fullVarDecl(decl.ast_node)) |var_decl| {736 if (ast.fullVarDecl(decl.ast_node)) |var_decl| {
735 if (var_decl.ast.type_node != 0) {737 if (var_decl.ast.type_node != 0) {
736 string_result.appendSlice(gpa, "<code>") catch @panic("OOM");738 string_result.appendSlice(gpa, "<code>") catch @panic("OOM");
737 file_source_html(decl.file, &string_result, var_decl.ast.type_node, .{739 fileSourceHtml(decl.file, &string_result, var_decl.ast.type_node, .{
738 .skip_comments = true,740 .skip_comments = true,
739 .collapse_whitespace = true,741 .collapse_whitespace = true,
740 }) catch |e| {742 }) catch |e| {
...@@ -750,7 +752,7 @@ export fn decl_type_html(decl_index: Decl.Index) String {...@@ -750,7 +752,7 @@ export fn decl_type_html(decl_index: Decl.Index) String {
750752
751const Oom = error{OutOfMemory};753const Oom = error{OutOfMemory};
752754
753fn unpack_inner(tar_bytes: []u8) !void {755fn unpackInner(tar_bytes: []u8) !void {
754 var fbs = std.io.fixedBufferStream(tar_bytes);756 var fbs = std.io.fixedBufferStream(tar_bytes);
755 var file_name_buffer: [1024]u8 = undefined;757 var file_name_buffer: [1024]u8 = undefined;
756 var link_name_buffer: [1024]u8 = undefined;758 var link_name_buffer: [1024]u8 = undefined;
...@@ -902,382 +904,6 @@ export fn namespace_members(parent: Decl.Index, include_private: bool) Slice(Dec...@@ -902,382 +904,6 @@ export fn namespace_members(parent: Decl.Index, include_private: bool) Slice(Dec
902 return Slice(Decl.Index).init(g.members.items);904 return Slice(Decl.Index).init(g.members.items);
903}905}
904906
905const RenderSourceOptions = struct {
906 skip_doc_comments: bool = false,
907 skip_comments: bool = false,
908 collapse_whitespace: bool = false,
909 fn_link: Decl.Index = .none,
910};
911
912fn file_source_html(
913 file_index: Walk.File.Index,
914 out: *std.ArrayListUnmanaged(u8),
915 root_node: Ast.Node.Index,
916 options: RenderSourceOptions,
917) !void {
918 const ast = file_index.get_ast();
919 const file = file_index.get();
920
921 const g = struct {
922 var field_access_buffer: std.ArrayListUnmanaged(u8) = .{};
923 };
924
925 const token_tags = ast.tokens.items(.tag);
926 const token_starts = ast.tokens.items(.start);
927 const main_tokens = ast.nodes.items(.main_token);
928
929 const start_token = ast.firstToken(root_node);
930 const end_token = ast.lastToken(root_node) + 1;
931
932 var cursor: usize = token_starts[start_token];
933
934 var indent: usize = 0;
935 if (std.mem.lastIndexOf(u8, ast.source[0..cursor], "\n")) |newline_index| {
936 for (ast.source[newline_index + 1 .. cursor]) |c| {
937 if (c == ' ') {
938 indent += 1;
939 } else {
940 break;
941 }
942 }
943 }
944
945 for (
946 token_tags[start_token..end_token],
947 token_starts[start_token..end_token],
948 start_token..,
949 ) |tag, start, token_index| {
950 const between = ast.source[cursor..start];
951 if (std.mem.trim(u8, between, " \t\r\n").len > 0) {
952 if (!options.skip_comments) {
953 try out.appendSlice(gpa, "<span class=\"tok-comment\">");
954 try appendUnindented(out, between, indent);
955 try out.appendSlice(gpa, "</span>");
956 }
957 } else if (between.len > 0) {
958 if (options.collapse_whitespace) {
959 if (out.items.len > 0 and out.items[out.items.len - 1] != ' ')
960 try out.append(gpa, ' ');
961 } else {
962 try appendUnindented(out, between, indent);
963 }
964 }
965 if (tag == .eof) break;
966 const slice = ast.tokenSlice(token_index);
967 cursor = start + slice.len;
968 switch (tag) {
969 .eof => unreachable,
970
971 .keyword_addrspace,
972 .keyword_align,
973 .keyword_and,
974 .keyword_asm,
975 .keyword_async,
976 .keyword_await,
977 .keyword_break,
978 .keyword_catch,
979 .keyword_comptime,
980 .keyword_const,
981 .keyword_continue,
982 .keyword_defer,
983 .keyword_else,
984 .keyword_enum,
985 .keyword_errdefer,
986 .keyword_error,
987 .keyword_export,
988 .keyword_extern,
989 .keyword_for,
990 .keyword_if,
991 .keyword_inline,
992 .keyword_noalias,
993 .keyword_noinline,
994 .keyword_nosuspend,
995 .keyword_opaque,
996 .keyword_or,
997 .keyword_orelse,
998 .keyword_packed,
999 .keyword_anyframe,
1000 .keyword_pub,
1001 .keyword_resume,
1002 .keyword_return,
1003 .keyword_linksection,
1004 .keyword_callconv,
1005 .keyword_struct,
1006 .keyword_suspend,
1007 .keyword_switch,
1008 .keyword_test,
1009 .keyword_threadlocal,
1010 .keyword_try,
1011 .keyword_union,
1012 .keyword_unreachable,
1013 .keyword_usingnamespace,
1014 .keyword_var,
1015 .keyword_volatile,
1016 .keyword_allowzero,
1017 .keyword_while,
1018 .keyword_anytype,
1019 .keyword_fn,
1020 => {
1021 try out.appendSlice(gpa, "<span class=\"tok-kw\">");
1022 try appendEscaped(out, slice);
1023 try out.appendSlice(gpa, "</span>");
1024 },
1025
1026 .string_literal,
1027 .char_literal,
1028 .multiline_string_literal_line,
1029 => {
1030 try out.appendSlice(gpa, "<span class=\"tok-str\">");
1031 try appendEscaped(out, slice);
1032 try out.appendSlice(gpa, "</span>");
1033 },
1034
1035 .builtin => {
1036 try out.appendSlice(gpa, "<span class=\"tok-builtin\">");
1037 try appendEscaped(out, slice);
1038 try out.appendSlice(gpa, "</span>");
1039 },
1040
1041 .doc_comment,
1042 .container_doc_comment,
1043 => {
1044 if (!options.skip_doc_comments) {
1045 try out.appendSlice(gpa, "<span class=\"tok-comment\">");
1046 try appendEscaped(out, slice);
1047 try out.appendSlice(gpa, "</span>");
1048 }
1049 },
1050
1051 .identifier => i: {
1052 if (options.fn_link != .none) {
1053 const fn_link = options.fn_link.get();
1054 const fn_token = main_tokens[fn_link.ast_node];
1055 if (token_index == fn_token + 1) {
1056 try out.appendSlice(gpa, "<a class=\"tok-fn\" href=\"#");
1057 _ = missing_feature_url_escape;
1058 try fn_link.fqn(out);
1059 try out.appendSlice(gpa, "\">");
1060 try appendEscaped(out, slice);
1061 try out.appendSlice(gpa, "</a>");
1062 break :i;
1063 }
1064 }
1065
1066 if (token_index > 0 and token_tags[token_index - 1] == .keyword_fn) {
1067 try out.appendSlice(gpa, "<span class=\"tok-fn\">");
1068 try appendEscaped(out, slice);
1069 try out.appendSlice(gpa, "</span>");
1070 break :i;
1071 }
1072
1073 if (Walk.isPrimitiveNonType(slice)) {
1074 try out.appendSlice(gpa, "<span class=\"tok-null\">");
1075 try appendEscaped(out, slice);
1076 try out.appendSlice(gpa, "</span>");
1077 break :i;
1078 }
1079
1080 if (std.zig.primitives.isPrimitive(slice)) {
1081 try out.appendSlice(gpa, "<span class=\"tok-type\">");
1082 try appendEscaped(out, slice);
1083 try out.appendSlice(gpa, "</span>");
1084 break :i;
1085 }
1086
1087 if (file.token_parents.get(token_index)) |field_access_node| {
1088 g.field_access_buffer.clearRetainingCapacity();
1089 try walk_field_accesses(file_index, &g.field_access_buffer, field_access_node);
1090 if (g.field_access_buffer.items.len > 0) {
1091 try out.appendSlice(gpa, "<a href=\"#");
1092 _ = missing_feature_url_escape;
1093 try out.appendSlice(gpa, g.field_access_buffer.items);
1094 try out.appendSlice(gpa, "\">");
1095 try appendEscaped(out, slice);
1096 try out.appendSlice(gpa, "</a>");
1097 } else {
1098 try appendEscaped(out, slice);
1099 }
1100 break :i;
1101 }
1102
1103 {
1104 g.field_access_buffer.clearRetainingCapacity();
1105 try resolve_ident_link(file_index, &g.field_access_buffer, token_index);
1106 if (g.field_access_buffer.items.len > 0) {
1107 try out.appendSlice(gpa, "<a href=\"#");
1108 _ = missing_feature_url_escape;
1109 try out.appendSlice(gpa, g.field_access_buffer.items);
1110 try out.appendSlice(gpa, "\">");
1111 try appendEscaped(out, slice);
1112 try out.appendSlice(gpa, "</a>");
1113 break :i;
1114 }
1115 }
1116
1117 try appendEscaped(out, slice);
1118 },
1119
1120 .number_literal => {
1121 try out.appendSlice(gpa, "<span class=\"tok-number\">");
1122 try appendEscaped(out, slice);
1123 try out.appendSlice(gpa, "</span>");
1124 },
1125
1126 .bang,
1127 .pipe,
1128 .pipe_pipe,
1129 .pipe_equal,
1130 .equal,
1131 .equal_equal,
1132 .equal_angle_bracket_right,
1133 .bang_equal,
1134 .l_paren,
1135 .r_paren,
1136 .semicolon,
1137 .percent,
1138 .percent_equal,
1139 .l_brace,
1140 .r_brace,
1141 .l_bracket,
1142 .r_bracket,
1143 .period,
1144 .period_asterisk,
1145 .ellipsis2,
1146 .ellipsis3,
1147 .caret,
1148 .caret_equal,
1149 .plus,
1150 .plus_plus,
1151 .plus_equal,
1152 .plus_percent,
1153 .plus_percent_equal,
1154 .plus_pipe,
1155 .plus_pipe_equal,
1156 .minus,
1157 .minus_equal,
1158 .minus_percent,
1159 .minus_percent_equal,
1160 .minus_pipe,
1161 .minus_pipe_equal,
1162 .asterisk,
1163 .asterisk_equal,
1164 .asterisk_asterisk,
1165 .asterisk_percent,
1166 .asterisk_percent_equal,
1167 .asterisk_pipe,
1168 .asterisk_pipe_equal,
1169 .arrow,
1170 .colon,
1171 .slash,
1172 .slash_equal,
1173 .comma,
1174 .ampersand,
1175 .ampersand_equal,
1176 .question_mark,
1177 .angle_bracket_left,
1178 .angle_bracket_left_equal,
1179 .angle_bracket_angle_bracket_left,
1180 .angle_bracket_angle_bracket_left_equal,
1181 .angle_bracket_angle_bracket_left_pipe,
1182 .angle_bracket_angle_bracket_left_pipe_equal,
1183 .angle_bracket_right,
1184 .angle_bracket_right_equal,
1185 .angle_bracket_angle_bracket_right,
1186 .angle_bracket_angle_bracket_right_equal,
1187 .tilde,
1188 => try appendEscaped(out, slice),
1189
1190 .invalid, .invalid_periodasterisks => return error.InvalidToken,
1191 }
1192 }
1193}
1194
1195fn unindent(s: []const u8, indent: usize) []const u8 {
1196 var indent_idx: usize = 0;
1197 for (s) |c| {
1198 if (c == ' ' and indent_idx < indent) {
1199 indent_idx += 1;
1200 } else {
1201 break;
1202 }
1203 }
1204 return s[indent_idx..];
1205}
1206
1207fn appendUnindented(out: *std.ArrayListUnmanaged(u8), s: []const u8, indent: usize) !void {
1208 var it = std.mem.splitScalar(u8, s, '\n');
1209 var is_first_line = true;
1210 while (it.next()) |line| {
1211 if (is_first_line) {
1212 try appendEscaped(out, line);
1213 is_first_line = false;
1214 } else {
1215 try out.appendSlice(gpa, "\n");
1216 try appendEscaped(out, unindent(line, indent));
1217 }
1218 }
1219}
1220
1221fn resolve_ident_link(
1222 file_index: Walk.File.Index,
1223 out: *std.ArrayListUnmanaged(u8),
1224 ident_token: Ast.TokenIndex,
1225) Oom!void {
1226 const decl_index = file_index.get().lookup_token(ident_token);
1227 if (decl_index == .none) return;
1228 try resolve_decl_link(decl_index, out);
1229}
1230
1231fn resolve_decl_link(decl_index: Decl.Index, out: *std.ArrayListUnmanaged(u8)) Oom!void {
1232 const decl = decl_index.get();
1233 switch (decl.categorize()) {
1234 .alias => |alias_decl| try alias_decl.get().fqn(out),
1235 else => try decl.fqn(out),
1236 }
1237}
1238
1239fn walk_field_accesses(
1240 file_index: Walk.File.Index,
1241 out: *std.ArrayListUnmanaged(u8),
1242 node: Ast.Node.Index,
1243) Oom!void {
1244 const ast = file_index.get_ast();
1245 const node_tags = ast.nodes.items(.tag);
1246 assert(node_tags[node] == .field_access);
1247 const node_datas = ast.nodes.items(.data);
1248 const main_tokens = ast.nodes.items(.main_token);
1249 const object_node = node_datas[node].lhs;
1250 const dot_token = main_tokens[node];
1251 const field_ident = dot_token + 1;
1252 switch (node_tags[object_node]) {
1253 .identifier => {
1254 const lhs_ident = main_tokens[object_node];
1255 try resolve_ident_link(file_index, out, lhs_ident);
1256 },
1257 .field_access => {
1258 try walk_field_accesses(file_index, out, object_node);
1259 },
1260 else => {},
1261 }
1262 if (out.items.len > 0) {
1263 try out.append(gpa, '.');
1264 try out.appendSlice(gpa, ast.tokenSlice(field_ident));
1265 }
1266}
1267
1268fn appendEscaped(out: *std.ArrayListUnmanaged(u8), s: []const u8) !void {
1269 for (s) |c| {
1270 try out.ensureUnusedCapacity(gpa, 6);
1271 switch (c) {
1272 '&' => out.appendSliceAssumeCapacity("&amp;"),
1273 '<' => out.appendSliceAssumeCapacity("&lt;"),
1274 '>' => out.appendSliceAssumeCapacity("&gt;"),
1275 '"' => out.appendSliceAssumeCapacity("&quot;"),
1276 else => out.appendAssumeCapacity(c),
1277 }
1278 }
1279}
1280
1281fn count_scalar(haystack: []const u8, needle: u8) usize {907fn count_scalar(haystack: []const u8, needle: u8) usize {
1282 var total: usize = 0;908 var total: usize = 0;
1283 for (haystack) |elem| {909 for (haystack) |elem| {
lib/fuzzer.zig+218-27
...@@ -2,6 +2,8 @@ const builtin = @import("builtin");...@@ -2,6 +2,8 @@ const builtin = @import("builtin");
2const std = @import("std");2const std = @import("std");
3const Allocator = std.mem.Allocator;3const Allocator = std.mem.Allocator;
4const assert = std.debug.assert;4const assert = std.debug.assert;
5const fatal = std.process.fatal;
6const SeenPcsHeader = std.Build.Fuzz.abi.SeenPcsHeader;
57
6pub const std_options = .{8pub const std_options = .{
7 .logFn = logOverride,9 .logFn = logOverride,
...@@ -15,9 +17,9 @@ fn logOverride(...@@ -15,9 +17,9 @@ fn logOverride(
15 comptime format: []const u8,17 comptime format: []const u8,
16 args: anytype,18 args: anytype,
17) void {19) void {
18 if (builtin.mode != .Debug) return;
19 const f = if (log_file) |f| f else f: {20 const f = if (log_file) |f| f else f: {
20 const f = std.fs.cwd().createFile("libfuzzer.log", .{}) catch @panic("failed to open fuzzer log file");21 const f = fuzzer.cache_dir.createFile("tmp/libfuzzer.log", .{}) catch
22 @panic("failed to open fuzzer log file");
21 log_file = f;23 log_file = f;
22 break :f f;24 break :f f;
23 };25 };
...@@ -26,18 +28,19 @@ fn logOverride(...@@ -26,18 +28,19 @@ fn logOverride(
26 f.writer().print(prefix1 ++ prefix2 ++ format ++ "\n", args) catch @panic("failed to write to fuzzer log");28 f.writer().print(prefix1 ++ prefix2 ++ format ++ "\n", args) catch @panic("failed to write to fuzzer log");
27}29}
2830
29export threadlocal var __sancov_lowest_stack: usize = 0;31export threadlocal var __sancov_lowest_stack: usize = std.math.maxInt(usize);
3032
31export fn __sanitizer_cov_8bit_counters_init(start: [*]u8, stop: [*]u8) void {33var module_count_8bc: usize = 0;
32 std.log.debug("__sanitizer_cov_8bit_counters_init start={*}, stop={*}", .{ start, stop });34var module_count_pcs: usize = 0;
35
36export fn __sanitizer_cov_8bit_counters_init(start: [*]u8, end: [*]u8) void {
37 assert(@atomicRmw(usize, &module_count_8bc, .Add, 1, .monotonic) == 0);
38 fuzzer.pc_counters = start[0 .. end - start];
33}39}
3440
35export fn __sanitizer_cov_pcs_init(pc_start: [*]const usize, pc_end: [*]const usize) void {41export fn __sanitizer_cov_pcs_init(start: [*]const Fuzzer.FlaggedPc, end: [*]const Fuzzer.FlaggedPc) void {
36 std.log.debug("__sanitizer_cov_pcs_init pc_start={*}, pc_end={*}", .{ pc_start, pc_end });42 assert(@atomicRmw(usize, &module_count_pcs, .Add, 1, .monotonic) == 0);
37 fuzzer.pc_range = .{43 fuzzer.flagged_pcs = start[0 .. end - start];
38 .start = @intFromPtr(pc_start),
39 .end = @intFromPtr(pc_start),
40 };
41}44}
4245
43export fn __sanitizer_cov_trace_const_cmp1(arg1: u8, arg2: u8) void {46export fn __sanitizer_cov_trace_const_cmp1(arg1: u8, arg2: u8) void {
...@@ -102,11 +105,21 @@ const Fuzzer = struct {...@@ -102,11 +105,21 @@ const Fuzzer = struct {
102 gpa: Allocator,105 gpa: Allocator,
103 rng: std.Random.DefaultPrng,106 rng: std.Random.DefaultPrng,
104 input: std.ArrayListUnmanaged(u8),107 input: std.ArrayListUnmanaged(u8),
105 pc_range: PcRange,108 flagged_pcs: []const FlaggedPc,
106 count: usize,109 pc_counters: []u8,
110 n_runs: usize,
107 recent_cases: RunMap,111 recent_cases: RunMap,
108 deduplicated_runs: usize,112 /// Data collected from code coverage instrumentation from one execution of
113 /// the test function.
109 coverage: Coverage,114 coverage: Coverage,
115 /// Tracks which PCs have been seen across all runs that do not crash the fuzzer process.
116 /// Stored in a memory-mapped file so that it can be shared with other
117 /// processes and viewed while the fuzzer is running.
118 seen_pcs: MemoryMappedList,
119 cache_dir: std.fs.Dir,
120 /// Identifies the file name that will be used to store coverage
121 /// information, available to other processes.
122 coverage_id: u64,
110123
111 const RunMap = std.ArrayHashMapUnmanaged(Run, void, Run.HashContext, false);124 const RunMap = std.ArrayHashMapUnmanaged(Run, void, Run.HashContext, false);
112125
...@@ -161,9 +174,12 @@ const Fuzzer = struct {...@@ -161,9 +174,12 @@ const Fuzzer = struct {
161 }174 }
162 };175 };
163176
164 const PcRange = struct {177 const FlaggedPc = extern struct {
165 start: usize,178 addr: usize,
166 end: usize,179 flags: packed struct(usize) {
180 entry: bool,
181 _: @Type(.{ .Int = .{ .signedness = .unsigned, .bits = @bitSizeOf(usize) - 1 } }),
182 },
167 };183 };
168184
169 const Analysis = struct {185 const Analysis = struct {
...@@ -171,6 +187,72 @@ const Fuzzer = struct {...@@ -171,6 +187,72 @@ const Fuzzer = struct {
171 id: Run.Id,187 id: Run.Id,
172 };188 };
173189
190 fn init(f: *Fuzzer, cache_dir: std.fs.Dir) !void {
191 const flagged_pcs = f.flagged_pcs;
192
193 f.cache_dir = cache_dir;
194
195 // Choose a file name for the coverage based on a hash of the PCs that will be stored within.
196 const pc_digest = d: {
197 var hasher = std.hash.Wyhash.init(0);
198 for (flagged_pcs) |flagged_pc| {
199 hasher.update(std.mem.asBytes(&flagged_pc.addr));
200 }
201 break :d f.coverage.run_id_hasher.final();
202 };
203 f.coverage_id = pc_digest;
204 const hex_digest = std.fmt.hex(pc_digest);
205 const coverage_file_path = "v/" ++ hex_digest;
206
207 // Layout of this file:
208 // - Header
209 // - list of PC addresses (usize elements)
210 // - list of hit flag, 1 bit per address (stored in u8 elements)
211 const coverage_file = createFileBail(cache_dir, coverage_file_path, .{
212 .read = true,
213 .truncate = false,
214 });
215 defer coverage_file.close();
216 const n_bitset_elems = (flagged_pcs.len + 7) / 8;
217 const bytes_len = @sizeOf(SeenPcsHeader) + flagged_pcs.len * @sizeOf(usize) + n_bitset_elems;
218 const existing_len = coverage_file.getEndPos() catch |err| {
219 fatal("unable to check len of coverage file: {s}", .{@errorName(err)});
220 };
221 if (existing_len == 0) {
222 coverage_file.setEndPos(bytes_len) catch |err| {
223 fatal("unable to set len of coverage file: {s}", .{@errorName(err)});
224 };
225 } else if (existing_len != bytes_len) {
226 fatal("incompatible existing coverage file (differing lengths)", .{});
227 }
228 f.seen_pcs = MemoryMappedList.init(coverage_file, existing_len, bytes_len) catch |err| {
229 fatal("unable to init coverage memory map: {s}", .{@errorName(err)});
230 };
231 if (existing_len != 0) {
232 const existing_pcs_bytes = f.seen_pcs.items[@sizeOf(SeenPcsHeader)..][0 .. flagged_pcs.len * @sizeOf(usize)];
233 const existing_pcs = std.mem.bytesAsSlice(usize, existing_pcs_bytes);
234 for (existing_pcs, flagged_pcs, 0..) |old, new, i| {
235 if (old != new.addr) {
236 fatal("incompatible existing coverage file (differing PC at index {d}: {x} != {x})", .{
237 i, old, new.addr,
238 });
239 }
240 }
241 } else {
242 const header: SeenPcsHeader = .{
243 .n_runs = 0,
244 .unique_runs = 0,
245 .pcs_len = flagged_pcs.len,
246 .lowest_stack = std.math.maxInt(usize),
247 };
248 f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&header));
249 for (flagged_pcs) |flagged_pc| {
250 f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&flagged_pc.addr));
251 }
252 f.seen_pcs.appendNTimesAssumeCapacity(0, n_bitset_elems);
253 }
254 }
255
174 fn analyzeLastRun(f: *Fuzzer) Analysis {256 fn analyzeLastRun(f: *Fuzzer) Analysis {
175 return .{257 return .{
176 .id = f.coverage.run_id_hasher.final(),258 .id = f.coverage.run_id_hasher.final(),
...@@ -194,7 +276,7 @@ const Fuzzer = struct {...@@ -194,7 +276,7 @@ const Fuzzer = struct {
194 .score = 0,276 .score = 0,
195 }, {});277 }, {});
196 } else {278 } else {
197 if (f.count % 1000 == 0) f.dumpStats();279 if (f.n_runs % 10000 == 0) f.dumpStats();
198280
199 const analysis = f.analyzeLastRun();281 const analysis = f.analyzeLastRun();
200 const gop = f.recent_cases.getOrPutAssumeCapacity(.{282 const gop = f.recent_cases.getOrPutAssumeCapacity(.{
...@@ -204,7 +286,6 @@ const Fuzzer = struct {...@@ -204,7 +286,6 @@ const Fuzzer = struct {
204 });286 });
205 if (gop.found_existing) {287 if (gop.found_existing) {
206 //std.log.info("duplicate analysis: score={d} id={d}", .{ analysis.score, analysis.id });288 //std.log.info("duplicate analysis: score={d} id={d}", .{ analysis.score, analysis.id });
207 f.deduplicated_runs += 1;
208 if (f.input.items.len < gop.key_ptr.input.len or gop.key_ptr.score == 0) {289 if (f.input.items.len < gop.key_ptr.input.len or gop.key_ptr.score == 0) {
209 gpa.free(gop.key_ptr.input);290 gpa.free(gop.key_ptr.input);
210 gop.key_ptr.input = try gpa.dupe(u8, f.input.items);291 gop.key_ptr.input = try gpa.dupe(u8, f.input.items);
...@@ -217,6 +298,28 @@ const Fuzzer = struct {...@@ -217,6 +298,28 @@ const Fuzzer = struct {
217 .input = try gpa.dupe(u8, f.input.items),298 .input = try gpa.dupe(u8, f.input.items),
218 .score = analysis.score,299 .score = analysis.score,
219 };300 };
301
302 // Track code coverage from all runs.
303 {
304 const seen_pcs = f.seen_pcs.items[@sizeOf(SeenPcsHeader) + f.flagged_pcs.len * @sizeOf(usize) ..];
305 for (seen_pcs, 0..) |*elem, i| {
306 const byte_i = i * 8;
307 const mask: u8 =
308 (@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 0] != 0)) << 0) |
309 (@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 1] != 0)) << 1) |
310 (@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 2] != 0)) << 2) |
311 (@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 3] != 0)) << 3) |
312 (@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 4] != 0)) << 4) |
313 (@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 5] != 0)) << 5) |
314 (@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 6] != 0)) << 6) |
315 (@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 7] != 0)) << 7);
316
317 _ = @atomicRmw(u8, elem, .Or, mask, .monotonic);
318 }
319 }
320
321 const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);
322 _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);
220 }323 }
221324
222 if (f.recent_cases.entries.len >= 100) {325 if (f.recent_cases.entries.len >= 100) {
...@@ -244,8 +347,12 @@ const Fuzzer = struct {...@@ -244,8 +347,12 @@ const Fuzzer = struct {
244 f.input.appendSliceAssumeCapacity(run.input);347 f.input.appendSliceAssumeCapacity(run.input);
245 try f.mutate();348 try f.mutate();
246349
350 f.n_runs += 1;
351 const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);
352 _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);
353 _ = @atomicRmw(usize, &header.lowest_stack, .Min, __sancov_lowest_stack, .monotonic);
354 @memset(f.pc_counters, 0);
247 f.coverage.reset();355 f.coverage.reset();
248 f.count += 1;
249 return f.input.items;356 return f.input.items;
250 }357 }
251358
...@@ -256,10 +363,6 @@ const Fuzzer = struct {...@@ -256,10 +363,6 @@ const Fuzzer = struct {
256 }363 }
257364
258 fn dumpStats(f: *Fuzzer) void {365 fn dumpStats(f: *Fuzzer) void {
259 std.log.info("stats: runs={d} deduplicated={d}", .{
260 f.count,
261 f.deduplicated_runs,
262 });
263 for (f.recent_cases.keys()[0..@min(f.recent_cases.entries.len, 5)], 0..) |run, i| {366 for (f.recent_cases.keys()[0..@min(f.recent_cases.entries.len, 5)], 0..) |run, i| {
264 std.log.info("best[{d}] id={x} score={d} input: '{}'", .{367 std.log.info("best[{d}] id={x} score={d} input: '{}'", .{
265 i, run.id, run.score, std.zig.fmtEscapes(run.input),368 i, run.id, run.score, std.zig.fmtEscapes(run.input),
...@@ -291,6 +394,21 @@ const Fuzzer = struct {...@@ -291,6 +394,21 @@ const Fuzzer = struct {
291 }394 }
292};395};
293396
397fn createFileBail(dir: std.fs.Dir, sub_path: []const u8, flags: std.fs.File.CreateFlags) std.fs.File {
398 return dir.createFile(sub_path, flags) catch |err| switch (err) {
399 error.FileNotFound => {
400 const dir_name = std.fs.path.dirname(sub_path).?;
401 dir.makePath(dir_name) catch |e| {
402 fatal("unable to make path '{s}': {s}", .{ dir_name, @errorName(e) });
403 };
404 return dir.createFile(sub_path, flags) catch |e| {
405 fatal("unable to create file '{s}': {s}", .{ sub_path, @errorName(e) });
406 };
407 },
408 else => fatal("unable to create file '{s}': {s}", .{ sub_path, @errorName(err) }),
409 };
410}
411
294fn oom(err: anytype) noreturn {412fn oom(err: anytype) noreturn {
295 switch (err) {413 switch (err) {
296 error.OutOfMemory => @panic("out of memory"),414 error.OutOfMemory => @panic("out of memory"),
...@@ -303,15 +421,88 @@ var fuzzer: Fuzzer = .{...@@ -303,15 +421,88 @@ var fuzzer: Fuzzer = .{
303 .gpa = general_purpose_allocator.allocator(),421 .gpa = general_purpose_allocator.allocator(),
304 .rng = std.Random.DefaultPrng.init(0),422 .rng = std.Random.DefaultPrng.init(0),
305 .input = .{},423 .input = .{},
306 .pc_range = .{ .start = 0, .end = 0 },424 .flagged_pcs = undefined,
307 .count = 0,425 .pc_counters = undefined,
308 .deduplicated_runs = 0,426 .n_runs = 0,
309 .recent_cases = .{},427 .recent_cases = .{},
310 .coverage = undefined,428 .coverage = undefined,
429 .cache_dir = undefined,
430 .seen_pcs = undefined,
431 .coverage_id = undefined,
311};432};
312433
434/// Invalid until `fuzzer_init` is called.
435export fn fuzzer_coverage_id() u64 {
436 return fuzzer.coverage_id;
437}
438
313export fn fuzzer_next() Fuzzer.Slice {439export fn fuzzer_next() Fuzzer.Slice {
314 return Fuzzer.Slice.fromZig(fuzzer.next() catch |err| switch (err) {440 return Fuzzer.Slice.fromZig(fuzzer.next() catch |err| switch (err) {
315 error.OutOfMemory => @panic("out of memory"),441 error.OutOfMemory => @panic("out of memory"),
316 });442 });
317}443}
444
445export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {
446 if (module_count_8bc == 0) fatal("__sanitizer_cov_8bit_counters_init was never called", .{});
447 if (module_count_pcs == 0) fatal("__sanitizer_cov_pcs_init was never called", .{});
448
449 const cache_dir_path = cache_dir_struct.toZig();
450 const cache_dir = if (cache_dir_path.len == 0)
451 std.fs.cwd()
452 else
453 std.fs.cwd().makeOpenPath(cache_dir_path, .{ .iterate = true }) catch |err| {
454 fatal("unable to open fuzz directory '{s}': {s}", .{ cache_dir_path, @errorName(err) });
455 };
456
457 fuzzer.init(cache_dir) catch |err| fatal("unable to init fuzzer: {s}", .{@errorName(err)});
458}
459
460/// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping.
461pub const MemoryMappedList = struct {
462 /// Contents of the list.
463 ///
464 /// Pointers to elements in this slice are invalidated by various functions
465 /// of this ArrayList in accordance with the respective documentation. In
466 /// all cases, "invalidated" means that the memory has been passed to this
467 /// allocator's resize or free function.
468 items: []align(std.mem.page_size) volatile u8,
469 /// How many bytes this list can hold without allocating additional memory.
470 capacity: usize,
471
472 pub fn init(file: std.fs.File, length: usize, capacity: usize) !MemoryMappedList {
473 const ptr = try std.posix.mmap(
474 null,
475 capacity,
476 std.posix.PROT.READ | std.posix.PROT.WRITE,
477 .{ .TYPE = .SHARED },
478 file.handle,
479 0,
480 );
481 return .{
482 .items = ptr[0..length],
483 .capacity = capacity,
484 };
485 }
486
487 /// Append the slice of items to the list.
488 /// Asserts that the list can hold the additional items.
489 pub fn appendSliceAssumeCapacity(l: *MemoryMappedList, items: []const u8) void {
490 const old_len = l.items.len;
491 const new_len = old_len + items.len;
492 assert(new_len <= l.capacity);
493 l.items.len = new_len;
494 @memcpy(l.items[old_len..][0..items.len], items);
495 }
496
497 /// Append a value to the list `n` times.
498 /// Never invalidates element pointers.
499 /// The function is inline so that a comptime-known `value` parameter will
500 /// have better memset codegen in case it has a repeated byte pattern.
501 /// Asserts that the list can hold the additional items.
502 pub inline fn appendNTimesAssumeCapacity(l: *MemoryMappedList, value: u8, n: usize) void {
503 const new_len = l.items.len + n;
504 assert(new_len <= l.capacity);
505 @memset(l.items.ptr[l.items.len..new_len], value);
506 l.items.len = new_len;
507 }
508};
lib/fuzzer/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/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/wasm/main.zig created+424
...@@ -0,0 +1,424 @@
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 = @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 = @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.ArrayListUnmanaged(u8) = .{};
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 const covered_bits = recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..];
410 var sli: u32 = 0;
411 for (covered_bits) |byte| {
412 global.result.ensureUnusedCapacity(gpa, 8) catch @panic("OOM");
413 if ((byte & 0b0000_0001) != 0) global.add(sli + 0, want_file);
414 if ((byte & 0b0000_0010) != 0) global.add(sli + 1, want_file);
415 if ((byte & 0b0000_0100) != 0) global.add(sli + 2, want_file);
416 if ((byte & 0b0000_1000) != 0) global.add(sli + 3, want_file);
417 if ((byte & 0b0001_0000) != 0) global.add(sli + 4, want_file);
418 if ((byte & 0b0010_0000) != 0) global.add(sli + 5, want_file);
419 if ((byte & 0b0100_0000) != 0) global.add(sli + 6, want_file);
420 if ((byte & 0b1000_0000) != 0) global.add(sli + 7, want_file);
421 sli += 8;
422 }
423 return Slice(SourceLocationIndex).init(global.result.items);
424}
lib/std/Build.zig+8-4
...@@ -2300,22 +2300,26 @@ pub const LazyPath = union(enum) {...@@ -2300,22 +2300,26 @@ pub const LazyPath = union(enum) {
2300 }2300 }
23012301
2302 pub fn path(lazy_path: LazyPath, b: *Build, sub_path: []const u8) LazyPath {2302 pub fn path(lazy_path: LazyPath, b: *Build, sub_path: []const u8) LazyPath {
2303 return lazy_path.join(b.allocator, sub_path) catch @panic("OOM");
2304 }
2305
2306 pub fn join(lazy_path: LazyPath, arena: Allocator, sub_path: []const u8) Allocator.Error!LazyPath {
2303 return switch (lazy_path) {2307 return switch (lazy_path) {
2304 .src_path => |src| .{ .src_path = .{2308 .src_path => |src| .{ .src_path = .{
2305 .owner = src.owner,2309 .owner = src.owner,
2306 .sub_path = b.pathResolve(&.{ src.sub_path, sub_path }),2310 .sub_path = try fs.path.resolve(arena, &.{ src.sub_path, sub_path }),
2307 } },2311 } },
2308 .generated => |gen| .{ .generated = .{2312 .generated => |gen| .{ .generated = .{
2309 .file = gen.file,2313 .file = gen.file,
2310 .up = gen.up,2314 .up = gen.up,
2311 .sub_path = b.pathResolve(&.{ gen.sub_path, sub_path }),2315 .sub_path = try fs.path.resolve(arena, &.{ gen.sub_path, sub_path }),
2312 } },2316 } },
2313 .cwd_relative => |cwd_relative| .{2317 .cwd_relative => |cwd_relative| .{
2314 .cwd_relative = b.pathResolve(&.{ cwd_relative, sub_path }),2318 .cwd_relative = try fs.path.resolve(arena, &.{ cwd_relative, sub_path }),
2315 },2319 },
2316 .dependency => |dep| .{ .dependency = .{2320 .dependency => |dep| .{ .dependency = .{
2317 .dependency = dep.dependency,2321 .dependency = dep.dependency,
2318 .sub_path = b.pathResolve(&.{ dep.sub_path, sub_path }),2322 .sub_path = try fs.path.resolve(arena, &.{ dep.sub_path, sub_path }),
2319 } },2323 } },
2320 };2324 };
2321 }2325 }
lib/std/Build/Cache/Path.zig+4-4
...@@ -32,16 +32,16 @@ pub fn resolvePosix(p: Path, arena: Allocator, sub_path: []const u8) Allocator.E...@@ -32,16 +32,16 @@ pub fn resolvePosix(p: Path, arena: Allocator, sub_path: []const u8) Allocator.E
32 };32 };
33}33}
3434
35pub fn joinString(p: Path, allocator: Allocator, sub_path: []const u8) Allocator.Error![]u8 {35pub fn joinString(p: Path, gpa: Allocator, sub_path: []const u8) Allocator.Error![]u8 {
36 const parts: []const []const u8 =36 const parts: []const []const u8 =
37 if (p.sub_path.len == 0) &.{sub_path} else &.{ p.sub_path, sub_path };37 if (p.sub_path.len == 0) &.{sub_path} else &.{ p.sub_path, sub_path };
38 return p.root_dir.join(allocator, parts);38 return p.root_dir.join(gpa, parts);
39}39}
4040
41pub fn joinStringZ(p: Path, allocator: Allocator, sub_path: []const u8) Allocator.Error![:0]u8 {41pub fn joinStringZ(p: Path, gpa: Allocator, sub_path: []const u8) Allocator.Error![:0]u8 {
42 const parts: []const []const u8 =42 const parts: []const []const u8 =
43 if (p.sub_path.len == 0) &.{sub_path} else &.{ p.sub_path, sub_path };43 if (p.sub_path.len == 0) &.{sub_path} else &.{ p.sub_path, sub_path };
44 return p.root_dir.joinZ(allocator, parts);44 return p.root_dir.joinZ(gpa, parts);
45}45}
4646
47pub fn openFile(47pub fn openFile(
lib/std/Build/Fuzz.zig+72-24
...@@ -1,57 +1,102 @@...@@ -1,57 +1,102 @@
1const builtin = @import("builtin");
1const std = @import("../std.zig");2const std = @import("../std.zig");
2const Fuzz = @This();3const Build = std.Build;
3const Step = std.Build.Step;4const Step = std.Build.Step;
4const assert = std.debug.assert;5const assert = std.debug.assert;
5const fatal = std.process.fatal;6const fatal = std.process.fatal;
7const Allocator = std.mem.Allocator;
8const log = std.log;
9
10const Fuzz = @This();
6const build_runner = @import("root");11const build_runner = @import("root");
712
13pub const WebServer = @import("Fuzz/WebServer.zig");
14pub const abi = @import("Fuzz/abi.zig");
15
8pub fn start(16pub fn start(
17 gpa: Allocator,
18 arena: Allocator,
19 global_cache_directory: Build.Cache.Directory,
20 zig_lib_directory: Build.Cache.Directory,
21 zig_exe_path: []const u8,
9 thread_pool: *std.Thread.Pool,22 thread_pool: *std.Thread.Pool,
10 all_steps: []const *Step,23 all_steps: []const *Step,
11 ttyconf: std.io.tty.Config,24 ttyconf: std.io.tty.Config,
25 listen_address: std.net.Address,
12 prog_node: std.Progress.Node,26 prog_node: std.Progress.Node,
13) void {27) Allocator.Error!void {
14 const count = block: {28 const fuzz_run_steps = block: {
15 const rebuild_node = prog_node.start("Rebuilding Unit Tests", 0);29 const rebuild_node = prog_node.start("Rebuilding Unit Tests", 0);
16 defer rebuild_node.end();30 defer rebuild_node.end();
17 var count: usize = 0;
18 var wait_group: std.Thread.WaitGroup = .{};31 var wait_group: std.Thread.WaitGroup = .{};
19 defer wait_group.wait();32 defer wait_group.wait();
33 var fuzz_run_steps: std.ArrayListUnmanaged(*Step.Run) = .{};
34 defer fuzz_run_steps.deinit(gpa);
20 for (all_steps) |step| {35 for (all_steps) |step| {
21 const run = step.cast(Step.Run) orelse continue;36 const run = step.cast(Step.Run) orelse continue;
22 if (run.fuzz_tests.items.len > 0 and run.producer != null) {37 if (run.fuzz_tests.items.len > 0 and run.producer != null) {
23 thread_pool.spawnWg(&wait_group, rebuildTestsWorkerRun, .{ run, ttyconf, rebuild_node });38 thread_pool.spawnWg(&wait_group, rebuildTestsWorkerRun, .{ run, ttyconf, rebuild_node });
24 count += 1;39 try fuzz_run_steps.append(gpa, run);
25 }40 }
26 }41 }
27 if (count == 0) fatal("no fuzz tests found", .{});42 if (fuzz_run_steps.items.len == 0) fatal("no fuzz tests found", .{});
28 rebuild_node.setEstimatedTotalItems(count);43 rebuild_node.setEstimatedTotalItems(fuzz_run_steps.items.len);
29 break :block count;44 break :block try arena.dupe(*Step.Run, fuzz_run_steps.items);
30 };45 };
3146
32 // Detect failure.47 // Detect failure.
33 for (all_steps) |step| {48 for (fuzz_run_steps) |run| {
34 const run = step.cast(Step.Run) orelse continue;49 assert(run.fuzz_tests.items.len > 0);
35 if (run.fuzz_tests.items.len > 0 and run.rebuilt_executable == null)50 if (run.rebuilt_executable == null)
36 fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{});51 fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{});
37 }52 }
3853
54 var web_server: WebServer = .{
55 .gpa = gpa,
56 .global_cache_directory = global_cache_directory,
57 .zig_lib_directory = zig_lib_directory,
58 .zig_exe_path = zig_exe_path,
59 .listen_address = listen_address,
60 .fuzz_run_steps = fuzz_run_steps,
61
62 .msg_queue = .{},
63 .mutex = .{},
64 .condition = .{},
65
66 .coverage_files = .{},
67 .coverage_mutex = .{},
68 .coverage_condition = .{},
69 };
70
71 // For accepting HTTP connections.
72 const web_server_thread = std.Thread.spawn(.{}, WebServer.run, .{&web_server}) catch |err| {
73 fatal("unable to spawn web server thread: {s}", .{@errorName(err)});
74 };
75 defer web_server_thread.join();
76
77 // For polling messages and sending updates to subscribers.
78 const coverage_thread = std.Thread.spawn(.{}, WebServer.coverageRun, .{&web_server}) catch |err| {
79 fatal("unable to spawn coverage thread: {s}", .{@errorName(err)});
80 };
81 defer coverage_thread.join();
82
39 {83 {
40 const fuzz_node = prog_node.start("Fuzzing", count);84 const fuzz_node = prog_node.start("Fuzzing", fuzz_run_steps.len);
41 defer fuzz_node.end();85 defer fuzz_node.end();
42 var wait_group: std.Thread.WaitGroup = .{};86 var wait_group: std.Thread.WaitGroup = .{};
43 defer wait_group.wait();87 defer wait_group.wait();
4488
45 for (all_steps) |step| {89 for (fuzz_run_steps) |run| {
46 const run = step.cast(Step.Run) orelse continue;
47 for (run.fuzz_tests.items) |unit_test_index| {90 for (run.fuzz_tests.items) |unit_test_index| {
48 assert(run.rebuilt_executable != null);91 assert(run.rebuilt_executable != null);
49 thread_pool.spawnWg(&wait_group, fuzzWorkerRun, .{ run, unit_test_index, ttyconf, fuzz_node });92 thread_pool.spawnWg(&wait_group, fuzzWorkerRun, .{
93 run, &web_server, unit_test_index, ttyconf, fuzz_node,
94 });
50 }95 }
51 }96 }
52 }97 }
5398
54 fatal("all fuzz workers crashed", .{});99 log.err("all fuzz workers crashed", .{});
55}100}
56101
57fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog_node: std.Progress.Node) void {102fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog_node: std.Progress.Node) void {
...@@ -74,20 +119,21 @@ fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog...@@ -74,20 +119,21 @@ fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog
74 build_runner.printErrorMessages(gpa, &compile.step, ttyconf, stderr, false) catch {};119 build_runner.printErrorMessages(gpa, &compile.step, ttyconf, stderr, false) catch {};
75 }120 }
76121
77 if (result) |rebuilt_bin_path| {122 const rebuilt_bin_path = result catch |err| switch (err) {
78 run.rebuilt_executable = rebuilt_bin_path;123 error.MakeFailed => return,
79 } else |err| switch (err) {
80 error.MakeFailed => {},
81 else => {124 else => {
82 std.debug.print("step '{s}': failed to rebuild in fuzz mode: {s}\n", .{125 log.err("step '{s}': failed to rebuild in fuzz mode: {s}", .{
83 compile.step.name, @errorName(err),126 compile.step.name, @errorName(err),
84 });127 });
128 return;
85 },129 },
86 }130 };
131 run.rebuilt_executable = rebuilt_bin_path;
87}132}
88133
89fn fuzzWorkerRun(134fn fuzzWorkerRun(
90 run: *Step.Run,135 run: *Step.Run,
136 web_server: *WebServer,
91 unit_test_index: u32,137 unit_test_index: u32,
92 ttyconf: std.io.tty.Config,138 ttyconf: std.io.tty.Config,
93 parent_prog_node: std.Progress.Node,139 parent_prog_node: std.Progress.Node,
...@@ -98,17 +144,19 @@ fn fuzzWorkerRun(...@@ -98,17 +144,19 @@ fn fuzzWorkerRun(
98 const prog_node = parent_prog_node.start(test_name, 0);144 const prog_node = parent_prog_node.start(test_name, 0);
99 defer prog_node.end();145 defer prog_node.end();
100146
101 run.rerunInFuzzMode(unit_test_index, prog_node) catch |err| switch (err) {147 run.rerunInFuzzMode(web_server, unit_test_index, prog_node) catch |err| switch (err) {
102 error.MakeFailed => {148 error.MakeFailed => {
103 const stderr = std.io.getStdErr();149 const stderr = std.io.getStdErr();
104 std.debug.lockStdErr();150 std.debug.lockStdErr();
105 defer std.debug.unlockStdErr();151 defer std.debug.unlockStdErr();
106 build_runner.printErrorMessages(gpa, &run.step, ttyconf, stderr, false) catch {};152 build_runner.printErrorMessages(gpa, &run.step, ttyconf, stderr, false) catch {};
153 return;
107 },154 },
108 else => {155 else => {
109 std.debug.print("step '{s}': failed to rebuild '{s}' in fuzz mode: {s}\n", .{156 log.err("step '{s}': failed to rerun '{s}' in fuzz mode: {s}", .{
110 run.step.name, test_name, @errorName(err),157 run.step.name, test_name, @errorName(err),
111 });158 });
159 return;
112 },160 },
113 };161 };
114}162}
lib/std/Build/Fuzz/WebServer.zig created+679
...@@ -0,0 +1,679 @@
1const builtin = @import("builtin");
2
3const std = @import("../../std.zig");
4const Allocator = std.mem.Allocator;
5const Build = std.Build;
6const Step = std.Build.Step;
7const Coverage = std.debug.Coverage;
8const abi = std.Build.Fuzz.abi;
9const log = std.log;
10
11const WebServer = @This();
12
13gpa: Allocator,
14global_cache_directory: Build.Cache.Directory,
15zig_lib_directory: Build.Cache.Directory,
16zig_exe_path: []const u8,
17listen_address: std.net.Address,
18fuzz_run_steps: []const *Step.Run,
19
20/// Messages from fuzz workers. Protected by mutex.
21msg_queue: std.ArrayListUnmanaged(Msg),
22/// Protects `msg_queue` only.
23mutex: std.Thread.Mutex,
24/// Signaled when there is a message in `msg_queue`.
25condition: std.Thread.Condition,
26
27coverage_files: std.AutoArrayHashMapUnmanaged(u64, CoverageMap),
28/// Protects `coverage_files` only.
29coverage_mutex: std.Thread.Mutex,
30/// Signaled when `coverage_files` changes.
31coverage_condition: std.Thread.Condition,
32
33const CoverageMap = struct {
34 mapped_memory: []align(std.mem.page_size) const u8,
35 coverage: Coverage,
36 source_locations: []Coverage.SourceLocation,
37 /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested.
38 entry_points: std.ArrayListUnmanaged(u32),
39
40 fn deinit(cm: *CoverageMap, gpa: Allocator) void {
41 std.posix.munmap(cm.mapped_memory);
42 cm.coverage.deinit(gpa);
43 cm.* = undefined;
44 }
45};
46
47const Msg = union(enum) {
48 coverage: struct {
49 id: u64,
50 run: *Step.Run,
51 },
52 entry_point: struct {
53 coverage_id: u64,
54 addr: u64,
55 },
56};
57
58pub fn run(ws: *WebServer) void {
59 var http_server = ws.listen_address.listen(.{
60 .reuse_address = true,
61 }) catch |err| {
62 log.err("failed to listen to port {d}: {s}", .{ ws.listen_address.in.getPort(), @errorName(err) });
63 return;
64 };
65 const port = http_server.listen_address.in.getPort();
66 log.info("web interface listening at http://127.0.0.1:{d}/", .{port});
67
68 while (true) {
69 const connection = http_server.accept() catch |err| {
70 log.err("failed to accept connection: {s}", .{@errorName(err)});
71 return;
72 };
73 _ = std.Thread.spawn(.{}, accept, .{ ws, connection }) catch |err| {
74 log.err("unable to spawn connection thread: {s}", .{@errorName(err)});
75 connection.stream.close();
76 continue;
77 };
78 }
79}
80
81fn accept(ws: *WebServer, connection: std.net.Server.Connection) void {
82 defer connection.stream.close();
83
84 var read_buffer: [0x4000]u8 = undefined;
85 var server = std.http.Server.init(connection, &read_buffer);
86 var web_socket: std.http.WebSocket = undefined;
87 var send_buffer: [0x4000]u8 = undefined;
88 var ws_recv_buffer: [0x4000]u8 align(4) = undefined;
89 while (server.state == .ready) {
90 var request = server.receiveHead() catch |err| switch (err) {
91 error.HttpConnectionClosing => return,
92 else => {
93 log.err("closing http connection: {s}", .{@errorName(err)});
94 return;
95 },
96 };
97 if (web_socket.init(&request, &send_buffer, &ws_recv_buffer) catch |err| {
98 log.err("initializing web socket: {s}", .{@errorName(err)});
99 return;
100 }) {
101 serveWebSocket(ws, &web_socket) catch |err| {
102 log.err("unable to serve web socket connection: {s}", .{@errorName(err)});
103 return;
104 };
105 } else {
106 serveRequest(ws, &request) catch |err| switch (err) {
107 error.AlreadyReported => return,
108 else => |e| {
109 log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(e) });
110 return;
111 },
112 };
113 }
114 }
115}
116
117fn serveRequest(ws: *WebServer, request: *std.http.Server.Request) !void {
118 if (std.mem.eql(u8, request.head.target, "/") or
119 std.mem.eql(u8, request.head.target, "/debug") or
120 std.mem.eql(u8, request.head.target, "/debug/"))
121 {
122 try serveFile(ws, request, "fuzzer/index.html", "text/html");
123 } else if (std.mem.eql(u8, request.head.target, "/main.js") or
124 std.mem.eql(u8, request.head.target, "/debug/main.js"))
125 {
126 try serveFile(ws, request, "fuzzer/main.js", "application/javascript");
127 } else if (std.mem.eql(u8, request.head.target, "/main.wasm")) {
128 try serveWasm(ws, request, .ReleaseFast);
129 } else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) {
130 try serveWasm(ws, request, .Debug);
131 } else if (std.mem.eql(u8, request.head.target, "/sources.tar") or
132 std.mem.eql(u8, request.head.target, "/debug/sources.tar"))
133 {
134 try serveSourcesTar(ws, request);
135 } else {
136 try request.respond("not found", .{
137 .status = .not_found,
138 .extra_headers = &.{
139 .{ .name = "content-type", .value = "text/plain" },
140 },
141 });
142 }
143}
144
145fn serveFile(
146 ws: *WebServer,
147 request: *std.http.Server.Request,
148 name: []const u8,
149 content_type: []const u8,
150) !void {
151 const gpa = ws.gpa;
152 // The desired API is actually sendfile, which will require enhancing std.http.Server.
153 // We load the file with every request so that the user can make changes to the file
154 // and refresh the HTML page without restarting this server.
155 const file_contents = ws.zig_lib_directory.handle.readFileAlloc(gpa, name, 10 * 1024 * 1024) catch |err| {
156 log.err("failed to read '{}{s}': {s}", .{ ws.zig_lib_directory, name, @errorName(err) });
157 return error.AlreadyReported;
158 };
159 defer gpa.free(file_contents);
160 try request.respond(file_contents, .{
161 .extra_headers = &.{
162 .{ .name = "content-type", .value = content_type },
163 cache_control_header,
164 },
165 });
166}
167
168fn serveWasm(
169 ws: *WebServer,
170 request: *std.http.Server.Request,
171 optimize_mode: std.builtin.OptimizeMode,
172) !void {
173 const gpa = ws.gpa;
174
175 var arena_instance = std.heap.ArenaAllocator.init(gpa);
176 defer arena_instance.deinit();
177 const arena = arena_instance.allocator();
178
179 // Do the compilation every request, so that the user can edit the files
180 // and see the changes without restarting the server.
181 const wasm_binary_path = try buildWasmBinary(ws, arena, optimize_mode);
182 // std.http.Server does not have a sendfile API yet.
183 const file_contents = try std.fs.cwd().readFileAlloc(gpa, wasm_binary_path, 10 * 1024 * 1024);
184 defer gpa.free(file_contents);
185 try request.respond(file_contents, .{
186 .extra_headers = &.{
187 .{ .name = "content-type", .value = "application/wasm" },
188 cache_control_header,
189 },
190 });
191}
192
193fn buildWasmBinary(
194 ws: *WebServer,
195 arena: Allocator,
196 optimize_mode: std.builtin.OptimizeMode,
197) ![]const u8 {
198 const gpa = ws.gpa;
199
200 const main_src_path: Build.Cache.Path = .{
201 .root_dir = ws.zig_lib_directory,
202 .sub_path = "fuzzer/wasm/main.zig",
203 };
204 const walk_src_path: Build.Cache.Path = .{
205 .root_dir = ws.zig_lib_directory,
206 .sub_path = "docs/wasm/Walk.zig",
207 };
208 const html_render_src_path: Build.Cache.Path = .{
209 .root_dir = ws.zig_lib_directory,
210 .sub_path = "docs/wasm/html_render.zig",
211 };
212
213 var argv: std.ArrayListUnmanaged([]const u8) = .{};
214
215 try argv.appendSlice(arena, &.{
216 ws.zig_exe_path, "build-exe", //
217 "-fno-entry", //
218 "-O", @tagName(optimize_mode), //
219 "-target", "wasm32-freestanding", //
220 "-mcpu", "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext", //
221 "--cache-dir", ws.global_cache_directory.path orelse ".", //
222 "--global-cache-dir", ws.global_cache_directory.path orelse ".", //
223 "--name", "fuzzer", //
224 "-rdynamic", //
225 "-fsingle-threaded", //
226 "--dep", "Walk", //
227 "--dep", "html_render", //
228 try std.fmt.allocPrint(arena, "-Mroot={}", .{main_src_path}), //
229 try std.fmt.allocPrint(arena, "-MWalk={}", .{walk_src_path}), //
230 "--dep", "Walk", //
231 try std.fmt.allocPrint(arena, "-Mhtml_render={}", .{html_render_src_path}), //
232 "--listen=-",
233 });
234
235 var child = std.process.Child.init(argv.items, gpa);
236 child.stdin_behavior = .Pipe;
237 child.stdout_behavior = .Pipe;
238 child.stderr_behavior = .Pipe;
239 try child.spawn();
240
241 var poller = std.io.poll(gpa, enum { stdout, stderr }, .{
242 .stdout = child.stdout.?,
243 .stderr = child.stderr.?,
244 });
245 defer poller.deinit();
246
247 try sendMessage(child.stdin.?, .update);
248 try sendMessage(child.stdin.?, .exit);
249
250 const Header = std.zig.Server.Message.Header;
251 var result: ?[]const u8 = null;
252 var result_error_bundle = std.zig.ErrorBundle.empty;
253
254 const stdout = poller.fifo(.stdout);
255
256 poll: while (true) {
257 while (stdout.readableLength() < @sizeOf(Header)) {
258 if (!(try poller.poll())) break :poll;
259 }
260 const header = stdout.reader().readStruct(Header) catch unreachable;
261 while (stdout.readableLength() < header.bytes_len) {
262 if (!(try poller.poll())) break :poll;
263 }
264 const body = stdout.readableSliceOfLen(header.bytes_len);
265
266 switch (header.tag) {
267 .zig_version => {
268 if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
269 return error.ZigProtocolVersionMismatch;
270 }
271 },
272 .error_bundle => {
273 const EbHdr = std.zig.Server.Message.ErrorBundle;
274 const eb_hdr = @as(*align(1) const EbHdr, @ptrCast(body));
275 const extra_bytes =
276 body[@sizeOf(EbHdr)..][0 .. @sizeOf(u32) * eb_hdr.extra_len];
277 const string_bytes =
278 body[@sizeOf(EbHdr) + extra_bytes.len ..][0..eb_hdr.string_bytes_len];
279 // TODO: use @ptrCast when the compiler supports it
280 const unaligned_extra = std.mem.bytesAsSlice(u32, extra_bytes);
281 const extra_array = try arena.alloc(u32, unaligned_extra.len);
282 @memcpy(extra_array, unaligned_extra);
283 result_error_bundle = .{
284 .string_bytes = try arena.dupe(u8, string_bytes),
285 .extra = extra_array,
286 };
287 },
288 .emit_bin_path => {
289 const EbpHdr = std.zig.Server.Message.EmitBinPath;
290 const ebp_hdr = @as(*align(1) const EbpHdr, @ptrCast(body));
291 if (!ebp_hdr.flags.cache_hit) {
292 log.info("source changes detected; rebuilt wasm component", .{});
293 }
294 result = try arena.dupe(u8, body[@sizeOf(EbpHdr)..]);
295 },
296 else => {}, // ignore other messages
297 }
298
299 stdout.discard(body.len);
300 }
301
302 const stderr = poller.fifo(.stderr);
303 if (stderr.readableLength() > 0) {
304 const owned_stderr = try stderr.toOwnedSlice();
305 defer gpa.free(owned_stderr);
306 std.debug.print("{s}", .{owned_stderr});
307 }
308
309 // Send EOF to stdin.
310 child.stdin.?.close();
311 child.stdin = null;
312
313 switch (try child.wait()) {
314 .Exited => |code| {
315 if (code != 0) {
316 log.err(
317 "the following command exited with error code {d}:\n{s}",
318 .{ code, try Build.Step.allocPrintCmd(arena, null, argv.items) },
319 );
320 return error.WasmCompilationFailed;
321 }
322 },
323 .Signal, .Stopped, .Unknown => {
324 log.err(
325 "the following command terminated unexpectedly:\n{s}",
326 .{try Build.Step.allocPrintCmd(arena, null, argv.items)},
327 );
328 return error.WasmCompilationFailed;
329 },
330 }
331
332 if (result_error_bundle.errorMessageCount() > 0) {
333 const color = std.zig.Color.auto;
334 result_error_bundle.renderToStdErr(color.renderOptions());
335 log.err("the following command failed with {d} compilation errors:\n{s}", .{
336 result_error_bundle.errorMessageCount(),
337 try Build.Step.allocPrintCmd(arena, null, argv.items),
338 });
339 return error.WasmCompilationFailed;
340 }
341
342 return result orelse {
343 log.err("child process failed to report result\n{s}", .{
344 try Build.Step.allocPrintCmd(arena, null, argv.items),
345 });
346 return error.WasmCompilationFailed;
347 };
348}
349
350fn sendMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag) !void {
351 const header: std.zig.Client.Message.Header = .{
352 .tag = tag,
353 .bytes_len = 0,
354 };
355 try file.writeAll(std.mem.asBytes(&header));
356}
357
358fn serveWebSocket(ws: *WebServer, web_socket: *std.http.WebSocket) !void {
359 ws.coverage_mutex.lock();
360 defer ws.coverage_mutex.unlock();
361
362 // On first connection, the client needs all the coverage information
363 // so that subsequent updates can contain only the updated bits.
364 var prev_unique_runs: usize = 0;
365 var prev_entry_points: usize = 0;
366 try sendCoverageContext(ws, web_socket, &prev_unique_runs, &prev_entry_points);
367 while (true) {
368 ws.coverage_condition.timedWait(&ws.coverage_mutex, std.time.ns_per_ms * 500) catch {};
369 try sendCoverageContext(ws, web_socket, &prev_unique_runs, &prev_entry_points);
370 }
371}
372
373fn sendCoverageContext(
374 ws: *WebServer,
375 web_socket: *std.http.WebSocket,
376 prev_unique_runs: *usize,
377 prev_entry_points: *usize,
378) !void {
379 const coverage_maps = ws.coverage_files.values();
380 if (coverage_maps.len == 0) return;
381 // TODO: make each events URL correspond to one coverage map
382 const coverage_map = &coverage_maps[0];
383 const cov_header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
384 const seen_pcs = coverage_map.mapped_memory[@sizeOf(abi.SeenPcsHeader) + coverage_map.source_locations.len * @sizeOf(usize) ..];
385 const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic);
386 const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic);
387 const lowest_stack = @atomicLoad(usize, &cov_header.lowest_stack, .monotonic);
388 if (prev_unique_runs.* != unique_runs) {
389 // There has been an update.
390 if (prev_unique_runs.* == 0) {
391 // We need to send initial context.
392 const header: abi.SourceIndexHeader = .{
393 .flags = .{},
394 .directories_len = @intCast(coverage_map.coverage.directories.entries.len),
395 .files_len = @intCast(coverage_map.coverage.files.entries.len),
396 .source_locations_len = @intCast(coverage_map.source_locations.len),
397 .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len),
398 };
399 const iovecs: [5]std.posix.iovec_const = .{
400 makeIov(std.mem.asBytes(&header)),
401 makeIov(std.mem.sliceAsBytes(coverage_map.coverage.directories.keys())),
402 makeIov(std.mem.sliceAsBytes(coverage_map.coverage.files.keys())),
403 makeIov(std.mem.sliceAsBytes(coverage_map.source_locations)),
404 makeIov(coverage_map.coverage.string_bytes.items),
405 };
406 try web_socket.writeMessagev(&iovecs, .binary);
407 }
408
409 const header: abi.CoverageUpdateHeader = .{
410 .n_runs = n_runs,
411 .unique_runs = unique_runs,
412 .lowest_stack = lowest_stack,
413 };
414 const iovecs: [2]std.posix.iovec_const = .{
415 makeIov(std.mem.asBytes(&header)),
416 makeIov(seen_pcs),
417 };
418 try web_socket.writeMessagev(&iovecs, .binary);
419
420 prev_unique_runs.* = unique_runs;
421 }
422
423 if (prev_entry_points.* != coverage_map.entry_points.items.len) {
424 const header: abi.EntryPointHeader = .{
425 .flags = .{
426 .locs_len = @intCast(coverage_map.entry_points.items.len),
427 },
428 };
429 const iovecs: [2]std.posix.iovec_const = .{
430 makeIov(std.mem.asBytes(&header)),
431 makeIov(std.mem.sliceAsBytes(coverage_map.entry_points.items)),
432 };
433 try web_socket.writeMessagev(&iovecs, .binary);
434
435 prev_entry_points.* = coverage_map.entry_points.items.len;
436 }
437}
438
439fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void {
440 const gpa = ws.gpa;
441
442 var arena_instance = std.heap.ArenaAllocator.init(gpa);
443 defer arena_instance.deinit();
444 const arena = arena_instance.allocator();
445
446 var send_buffer: [0x4000]u8 = undefined;
447 var response = request.respondStreaming(.{
448 .send_buffer = &send_buffer,
449 .respond_options = .{
450 .extra_headers = &.{
451 .{ .name = "content-type", .value = "application/x-tar" },
452 cache_control_header,
453 },
454 },
455 });
456 const w = response.writer();
457
458 const DedupeTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false);
459 var dedupe_table: DedupeTable = .{};
460 defer dedupe_table.deinit(gpa);
461
462 for (ws.fuzz_run_steps) |run_step| {
463 const compile_step_inputs = run_step.producer.?.step.inputs.table;
464 for (compile_step_inputs.keys(), compile_step_inputs.values()) |dir_path, *file_list| {
465 try dedupe_table.ensureUnusedCapacity(gpa, file_list.items.len);
466 for (file_list.items) |sub_path| {
467 // Special file "." means the entire directory.
468 if (std.mem.eql(u8, sub_path, ".")) continue;
469 const joined_path = try dir_path.join(arena, sub_path);
470 _ = dedupe_table.getOrPutAssumeCapacity(joined_path);
471 }
472 }
473 }
474
475 const deduped_paths = dedupe_table.keys();
476 const SortContext = struct {
477 pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool {
478 _ = this;
479 return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) {
480 .lt => true,
481 .gt => false,
482 .eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path),
483 };
484 }
485 };
486 std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan);
487
488 var cwd_cache: ?[]const u8 = null;
489
490 for (deduped_paths) |joined_path| {
491 var file = joined_path.root_dir.handle.openFile(joined_path.sub_path, .{}) catch |err| {
492 log.err("failed to open {}: {s}", .{ joined_path, @errorName(err) });
493 continue;
494 };
495 defer file.close();
496
497 const stat = file.stat() catch |err| {
498 log.err("failed to stat {}: {s}", .{ joined_path, @errorName(err) });
499 continue;
500 };
501 if (stat.kind != .file)
502 continue;
503
504 const padding = p: {
505 const remainder = stat.size % 512;
506 break :p if (remainder > 0) 512 - remainder else 0;
507 };
508
509 var file_header = std.tar.output.Header.init();
510 file_header.typeflag = .regular;
511 try file_header.setPath(
512 joined_path.root_dir.path orelse try memoizedCwd(arena, &cwd_cache),
513 joined_path.sub_path,
514 );
515 try file_header.setSize(stat.size);
516 try file_header.updateChecksum();
517 try w.writeAll(std.mem.asBytes(&file_header));
518 try w.writeFile(file);
519 try w.writeByteNTimes(0, padding);
520 }
521
522 // intentionally omitting the pointless trailer
523 //try w.writeByteNTimes(0, 512 * 2);
524 try response.end();
525}
526
527fn memoizedCwd(arena: Allocator, opt_ptr: *?[]const u8) ![]const u8 {
528 if (opt_ptr.*) |cached| return cached;
529 const result = try std.process.getCwdAlloc(arena);
530 opt_ptr.* = result;
531 return result;
532}
533
534const cache_control_header: std.http.Header = .{
535 .name = "cache-control",
536 .value = "max-age=0, must-revalidate",
537};
538
539pub fn coverageRun(ws: *WebServer) void {
540 ws.mutex.lock();
541 defer ws.mutex.unlock();
542
543 while (true) {
544 ws.condition.wait(&ws.mutex);
545 for (ws.msg_queue.items) |msg| switch (msg) {
546 .coverage => |coverage| prepareTables(ws, coverage.run, coverage.id) catch |err| switch (err) {
547 error.AlreadyReported => continue,
548 else => |e| log.err("failed to prepare code coverage tables: {s}", .{@errorName(e)}),
549 },
550 .entry_point => |entry_point| addEntryPoint(ws, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) {
551 error.AlreadyReported => continue,
552 else => |e| log.err("failed to prepare code coverage tables: {s}", .{@errorName(e)}),
553 },
554 };
555 ws.msg_queue.clearRetainingCapacity();
556 }
557}
558
559fn prepareTables(
560 ws: *WebServer,
561 run_step: *Step.Run,
562 coverage_id: u64,
563) error{ OutOfMemory, AlreadyReported }!void {
564 const gpa = ws.gpa;
565
566 ws.coverage_mutex.lock();
567 defer ws.coverage_mutex.unlock();
568
569 const gop = try ws.coverage_files.getOrPut(gpa, coverage_id);
570 if (gop.found_existing) {
571 // We are fuzzing the same executable with multiple threads.
572 // Perhaps the same unit test; perhaps a different one. In any
573 // case, since the coverage file is the same, we only have to
574 // notice changes to that one file in order to learn coverage for
575 // this particular executable.
576 return;
577 }
578 errdefer _ = ws.coverage_files.pop();
579
580 gop.value_ptr.* = .{
581 .coverage = std.debug.Coverage.init,
582 .mapped_memory = undefined, // populated below
583 .source_locations = undefined, // populated below
584 .entry_points = .{},
585 };
586 errdefer gop.value_ptr.coverage.deinit(gpa);
587
588 const rebuilt_exe_path: Build.Cache.Path = .{
589 .root_dir = Build.Cache.Directory.cwd(),
590 .sub_path = run_step.rebuilt_executable.?,
591 };
592 var debug_info = std.debug.Info.load(gpa, rebuilt_exe_path, &gop.value_ptr.coverage) catch |err| {
593 log.err("step '{s}': failed to load debug information for '{}': {s}", .{
594 run_step.step.name, rebuilt_exe_path, @errorName(err),
595 });
596 return error.AlreadyReported;
597 };
598 defer debug_info.deinit(gpa);
599
600 const coverage_file_path: Build.Cache.Path = .{
601 .root_dir = run_step.step.owner.cache_root,
602 .sub_path = "v/" ++ std.fmt.hex(coverage_id),
603 };
604 var coverage_file = coverage_file_path.root_dir.handle.openFile(coverage_file_path.sub_path, .{}) catch |err| {
605 log.err("step '{s}': failed to load coverage file '{}': {s}", .{
606 run_step.step.name, coverage_file_path, @errorName(err),
607 });
608 return error.AlreadyReported;
609 };
610 defer coverage_file.close();
611
612 const file_size = coverage_file.getEndPos() catch |err| {
613 log.err("unable to check len of coverage file '{}': {s}", .{ coverage_file_path, @errorName(err) });
614 return error.AlreadyReported;
615 };
616
617 const mapped_memory = std.posix.mmap(
618 null,
619 file_size,
620 std.posix.PROT.READ,
621 .{ .TYPE = .SHARED },
622 coverage_file.handle,
623 0,
624 ) catch |err| {
625 log.err("failed to map coverage file '{}': {s}", .{ coverage_file_path, @errorName(err) });
626 return error.AlreadyReported;
627 };
628 gop.value_ptr.mapped_memory = mapped_memory;
629
630 const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
631 const pcs_bytes = mapped_memory[@sizeOf(abi.SeenPcsHeader)..][0 .. header.pcs_len * @sizeOf(usize)];
632 const pcs = std.mem.bytesAsSlice(usize, pcs_bytes);
633 const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len);
634 errdefer gpa.free(source_locations);
635 debug_info.resolveAddresses(gpa, pcs, source_locations) catch |err| {
636 log.err("failed to resolve addresses to source locations: {s}", .{@errorName(err)});
637 return error.AlreadyReported;
638 };
639 gop.value_ptr.source_locations = source_locations;
640
641 ws.coverage_condition.broadcast();
642}
643
644fn addEntryPoint(ws: *WebServer, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory }!void {
645 ws.coverage_mutex.lock();
646 defer ws.coverage_mutex.unlock();
647
648 const coverage_map = ws.coverage_files.getPtr(coverage_id).?;
649 const ptr = coverage_map.mapped_memory;
650 const pcs_bytes = ptr[@sizeOf(abi.SeenPcsHeader)..][0 .. coverage_map.source_locations.len * @sizeOf(usize)];
651 const pcs: []const usize = @alignCast(std.mem.bytesAsSlice(usize, pcs_bytes));
652 const index = std.sort.upperBound(usize, pcs, addr, struct {
653 fn order(context: usize, item: usize) std.math.Order {
654 return std.math.order(item, context);
655 }
656 }.order);
657 if (index >= pcs.len) {
658 log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{
659 addr, pcs[0], pcs[pcs.len - 1],
660 });
661 return error.AlreadyReported;
662 }
663 if (false) {
664 const sl = coverage_map.source_locations[index];
665 const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename);
666 log.debug("server found entry point {s}:{d}:{d}", .{
667 file_name, sl.line, sl.column,
668 });
669 }
670 const gpa = ws.gpa;
671 try coverage_map.entry_points.append(gpa, @intCast(index));
672}
673
674fn makeIov(s: []const u8) std.posix.iovec_const {
675 return .{
676 .base = s.ptr,
677 .len = s.len,
678 };
679}
lib/std/Build/Fuzz/abi.zig created+69
...@@ -0,0 +1,69 @@
1//! This file is shared among Zig code running in wildly different contexts:
2//! libfuzzer, compiled alongside unit tests, the build runner, running on the
3//! host computer, and the fuzzing web interface webassembly code running in
4//! the browser. All of these components interface to some degree via an ABI.
5
6/// libfuzzer uses this and its usize is the one that counts. To match the ABI,
7/// make the ints be the size of the target used with libfuzzer.
8///
9/// Trailing:
10/// * pc_addr: usize for each pcs_len
11/// * 1 bit per pc_addr, usize elements
12pub const SeenPcsHeader = extern struct {
13 n_runs: usize,
14 unique_runs: usize,
15 pcs_len: usize,
16 lowest_stack: usize,
17};
18
19pub const ToClientTag = enum(u8) {
20 source_index,
21 coverage_update,
22 entry_points,
23 _,
24};
25
26/// Sent to the fuzzer web client on first connection to the websocket URL.
27///
28/// Trailing:
29/// * std.debug.Coverage.String for each directories_len
30/// * std.debug.Coverage.File for each files_len
31/// * std.debug.Coverage.SourceLocation for each source_locations_len
32/// * u8 for each string_bytes_len
33pub const SourceIndexHeader = extern struct {
34 flags: Flags,
35 directories_len: u32,
36 files_len: u32,
37 source_locations_len: u32,
38 string_bytes_len: u32,
39
40 pub const Flags = packed struct(u32) {
41 tag: ToClientTag = .source_index,
42 _: u24 = 0,
43 };
44};
45
46/// Sent to the fuzzer web client whenever the set of covered source locations
47/// changes.
48///
49/// Trailing:
50/// * one bit per source_locations_len, contained in u8 elements
51pub const CoverageUpdateHeader = extern struct {
52 tag: ToClientTag = .coverage_update,
53 n_runs: u64 align(1),
54 unique_runs: u64 align(1),
55 lowest_stack: u64 align(1),
56};
57
58/// Sent to the fuzzer web client when the set of entry points is updated.
59///
60/// Trailing:
61/// * one u32 index of source_locations per locs_len
62pub const EntryPointHeader = extern struct {
63 flags: Flags,
64
65 pub const Flags = packed struct(u32) {
66 tag: ToClientTag = .entry_points,
67 locs_len: u24,
68 };
69};
lib/std/Build/Step.zig+2-1
...@@ -559,7 +559,8 @@ fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool) !?[]const u8 {...@@ -559,7 +559,8 @@ fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool) !?[]const u8 {
559 },559 },
560 .zig_lib => zl: {560 .zig_lib => zl: {
561 if (s.cast(Step.Compile)) |compile| {561 if (s.cast(Step.Compile)) |compile| {
562 if (compile.zig_lib_dir) |lp| {562 if (compile.zig_lib_dir) |zig_lib_dir| {
563 const lp = try zig_lib_dir.join(arena, sub_path);
563 try addWatchInput(s, lp);564 try addWatchInput(s, lp);
564 break :zl;565 break :zl;
565 }566 }
lib/std/Build/Step/Run.zig+57-14
...@@ -205,6 +205,7 @@ pub fn enableTestRunnerMode(run: *Run) void {...@@ -205,6 +205,7 @@ pub fn enableTestRunnerMode(run: *Run) void {
205 run.stdio = .zig_test;205 run.stdio = .zig_test;
206 run.addArgs(&.{206 run.addArgs(&.{
207 std.fmt.allocPrint(arena, "--seed=0x{x}", .{b.graph.random_seed}) catch @panic("OOM"),207 std.fmt.allocPrint(arena, "--seed=0x{x}", .{b.graph.random_seed}) catch @panic("OOM"),
208 std.fmt.allocPrint(arena, "--cache-dir={s}", .{b.cache_root.path orelse ""}) catch @panic("OOM"),
208 "--listen=-",209 "--listen=-",
209 });210 });
210}211}
...@@ -845,7 +846,12 @@ fn make(step: *Step, options: Step.MakeOptions) !void {...@@ -845,7 +846,12 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
845 );846 );
846}847}
847848
848pub fn rerunInFuzzMode(run: *Run, unit_test_index: u32, prog_node: std.Progress.Node) !void {849pub fn rerunInFuzzMode(
850 run: *Run,
851 web_server: *std.Build.Fuzz.WebServer,
852 unit_test_index: u32,
853 prog_node: std.Progress.Node,
854) !void {
849 const step = &run.step;855 const step = &run.step;
850 const b = step.owner;856 const b = step.owner;
851 const arena = b.allocator;857 const arena = b.allocator;
...@@ -877,7 +883,10 @@ pub fn rerunInFuzzMode(run: *Run, unit_test_index: u32, prog_node: std.Progress....@@ -877,7 +883,10 @@ pub fn rerunInFuzzMode(run: *Run, unit_test_index: u32, prog_node: std.Progress.
877 const has_side_effects = false;883 const has_side_effects = false;
878 const rand_int = std.crypto.random.int(u64);884 const rand_int = std.crypto.random.int(u64);
879 const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);885 const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
880 try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, prog_node, unit_test_index);886 try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, prog_node, .{
887 .unit_test_index = unit_test_index,
888 .web_server = web_server,
889 });
881}890}
882891
883fn populateGeneratedPaths(892fn populateGeneratedPaths(
...@@ -952,13 +961,18 @@ fn termMatches(expected: ?std.process.Child.Term, actual: std.process.Child.Term...@@ -952,13 +961,18 @@ fn termMatches(expected: ?std.process.Child.Term, actual: std.process.Child.Term
952 };961 };
953}962}
954963
964const FuzzContext = struct {
965 web_server: *std.Build.Fuzz.WebServer,
966 unit_test_index: u32,
967};
968
955fn runCommand(969fn runCommand(
956 run: *Run,970 run: *Run,
957 argv: []const []const u8,971 argv: []const []const u8,
958 has_side_effects: bool,972 has_side_effects: bool,
959 output_dir_path: []const u8,973 output_dir_path: []const u8,
960 prog_node: std.Progress.Node,974 prog_node: std.Progress.Node,
961 fuzz_unit_test_index: ?u32,975 fuzz_context: ?FuzzContext,
962) !void {976) !void {
963 const step = &run.step;977 const step = &run.step;
964 const b = step.owner;978 const b = step.owner;
...@@ -977,7 +991,7 @@ fn runCommand(...@@ -977,7 +991,7 @@ fn runCommand(
977 var interp_argv = std.ArrayList([]const u8).init(b.allocator);991 var interp_argv = std.ArrayList([]const u8).init(b.allocator);
978 defer interp_argv.deinit();992 defer interp_argv.deinit();
979993
980 const result = spawnChildAndCollect(run, argv, has_side_effects, prog_node, fuzz_unit_test_index) catch |err| term: {994 const result = spawnChildAndCollect(run, argv, has_side_effects, prog_node, fuzz_context) catch |err| term: {
981 // InvalidExe: cpu arch mismatch995 // InvalidExe: cpu arch mismatch
982 // FileNotFound: can happen with a wrong dynamic linker path996 // FileNotFound: can happen with a wrong dynamic linker path
983 if (err == error.InvalidExe or err == error.FileNotFound) interpret: {997 if (err == error.InvalidExe or err == error.FileNotFound) interpret: {
...@@ -1113,7 +1127,7 @@ fn runCommand(...@@ -1113,7 +1127,7 @@ fn runCommand(
11131127
1114 try Step.handleVerbose2(step.owner, cwd, run.env_map, interp_argv.items);1128 try Step.handleVerbose2(step.owner, cwd, run.env_map, interp_argv.items);
11151129
1116 break :term spawnChildAndCollect(run, interp_argv.items, has_side_effects, prog_node, fuzz_unit_test_index) catch |e| {1130 break :term spawnChildAndCollect(run, interp_argv.items, has_side_effects, prog_node, fuzz_context) catch |e| {
1117 if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;1131 if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;
11181132
1119 return step.fail("unable to spawn interpreter {s}: {s}", .{1133 return step.fail("unable to spawn interpreter {s}: {s}", .{
...@@ -1133,7 +1147,7 @@ fn runCommand(...@@ -1133,7 +1147,7 @@ fn runCommand(
11331147
1134 const final_argv = if (interp_argv.items.len == 0) argv else interp_argv.items;1148 const final_argv = if (interp_argv.items.len == 0) argv else interp_argv.items;
11351149
1136 if (fuzz_unit_test_index != null) {1150 if (fuzz_context != null) {
1137 try step.handleChildProcessTerm(result.term, cwd, final_argv);1151 try step.handleChildProcessTerm(result.term, cwd, final_argv);
1138 return;1152 return;
1139 }1153 }
...@@ -1298,12 +1312,12 @@ fn spawnChildAndCollect(...@@ -1298,12 +1312,12 @@ fn spawnChildAndCollect(
1298 argv: []const []const u8,1312 argv: []const []const u8,
1299 has_side_effects: bool,1313 has_side_effects: bool,
1300 prog_node: std.Progress.Node,1314 prog_node: std.Progress.Node,
1301 fuzz_unit_test_index: ?u32,1315 fuzz_context: ?FuzzContext,
1302) !ChildProcResult {1316) !ChildProcResult {
1303 const b = run.step.owner;1317 const b = run.step.owner;
1304 const arena = b.allocator;1318 const arena = b.allocator;
13051319
1306 if (fuzz_unit_test_index != null) {1320 if (fuzz_context != null) {
1307 assert(!has_side_effects);1321 assert(!has_side_effects);
1308 assert(run.stdio == .zig_test);1322 assert(run.stdio == .zig_test);
1309 }1323 }
...@@ -1357,7 +1371,7 @@ fn spawnChildAndCollect(...@@ -1357,7 +1371,7 @@ fn spawnChildAndCollect(
1357 var timer = try std.time.Timer.start();1371 var timer = try std.time.Timer.start();
13581372
1359 const result = if (run.stdio == .zig_test)1373 const result = if (run.stdio == .zig_test)
1360 evalZigTest(run, &child, prog_node, fuzz_unit_test_index)1374 evalZigTest(run, &child, prog_node, fuzz_context)
1361 else1375 else
1362 evalGeneric(run, &child);1376 evalGeneric(run, &child);
13631377
...@@ -1383,7 +1397,7 @@ fn evalZigTest(...@@ -1383,7 +1397,7 @@ fn evalZigTest(
1383 run: *Run,1397 run: *Run,
1384 child: *std.process.Child,1398 child: *std.process.Child,
1385 prog_node: std.Progress.Node,1399 prog_node: std.Progress.Node,
1386 fuzz_unit_test_index: ?u32,1400 fuzz_context: ?FuzzContext,
1387) !StdIoResult {1401) !StdIoResult {
1388 const gpa = run.step.owner.allocator;1402 const gpa = run.step.owner.allocator;
1389 const arena = run.step.owner.allocator;1403 const arena = run.step.owner.allocator;
...@@ -1394,8 +1408,8 @@ fn evalZigTest(...@@ -1394,8 +1408,8 @@ fn evalZigTest(
1394 });1408 });
1395 defer poller.deinit();1409 defer poller.deinit();
13961410
1397 if (fuzz_unit_test_index) |index| {1411 if (fuzz_context) |fuzz| {
1398 try sendRunTestMessage(child.stdin.?, .start_fuzzing, index);1412 try sendRunTestMessage(child.stdin.?, .start_fuzzing, fuzz.unit_test_index);
1399 } else {1413 } else {
1400 run.fuzz_tests.clearRetainingCapacity();1414 run.fuzz_tests.clearRetainingCapacity();
1401 try sendMessage(child.stdin.?, .query_test_metadata);1415 try sendMessage(child.stdin.?, .query_test_metadata);
...@@ -1413,6 +1427,7 @@ fn evalZigTest(...@@ -1413,6 +1427,7 @@ fn evalZigTest(
1413 var log_err_count: u32 = 0;1427 var log_err_count: u32 = 0;
14141428
1415 var metadata: ?TestMetadata = null;1429 var metadata: ?TestMetadata = null;
1430 var coverage_id: ?u64 = null;
14161431
1417 var sub_prog_node: ?std.Progress.Node = null;1432 var sub_prog_node: ?std.Progress.Node = null;
1418 defer if (sub_prog_node) |n| n.end();1433 defer if (sub_prog_node) |n| n.end();
...@@ -1437,7 +1452,7 @@ fn evalZigTest(...@@ -1437,7 +1452,7 @@ fn evalZigTest(
1437 }1452 }
1438 },1453 },
1439 .test_metadata => {1454 .test_metadata => {
1440 assert(fuzz_unit_test_index == null);1455 assert(fuzz_context == null);
1441 const TmHdr = std.zig.Server.Message.TestMetadata;1456 const TmHdr = std.zig.Server.Message.TestMetadata;
1442 const tm_hdr = @as(*align(1) const TmHdr, @ptrCast(body));1457 const tm_hdr = @as(*align(1) const TmHdr, @ptrCast(body));
1443 test_count = tm_hdr.tests_len;1458 test_count = tm_hdr.tests_len;
...@@ -1466,7 +1481,7 @@ fn evalZigTest(...@@ -1466,7 +1481,7 @@ fn evalZigTest(
1466 try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);1481 try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);
1467 },1482 },
1468 .test_results => {1483 .test_results => {
1469 assert(fuzz_unit_test_index == null);1484 assert(fuzz_context == null);
1470 const md = metadata.?;1485 const md = metadata.?;
14711486
1472 const TrHdr = std.zig.Server.Message.TestResults;1487 const TrHdr = std.zig.Server.Message.TestResults;
...@@ -1500,6 +1515,34 @@ fn evalZigTest(...@@ -1500,6 +1515,34 @@ fn evalZigTest(
15001515
1501 try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);1516 try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);
1502 },1517 },
1518 .coverage_id => {
1519 const web_server = fuzz_context.?.web_server;
1520 const msg_ptr: *align(1) const u64 = @ptrCast(body);
1521 coverage_id = msg_ptr.*;
1522 {
1523 web_server.mutex.lock();
1524 defer web_server.mutex.unlock();
1525 try web_server.msg_queue.append(web_server.gpa, .{ .coverage = .{
1526 .id = coverage_id.?,
1527 .run = run,
1528 } });
1529 web_server.condition.signal();
1530 }
1531 },
1532 .fuzz_start_addr => {
1533 const web_server = fuzz_context.?.web_server;
1534 const msg_ptr: *align(1) const u64 = @ptrCast(body);
1535 const addr = msg_ptr.*;
1536 {
1537 web_server.mutex.lock();
1538 defer web_server.mutex.unlock();
1539 try web_server.msg_queue.append(web_server.gpa, .{ .entry_point = .{
1540 .addr = addr,
1541 .coverage_id = coverage_id.?,
1542 } });
1543 web_server.condition.signal();
1544 }
1545 },
1503 else => {}, // ignore other messages1546 else => {}, // ignore other messages
1504 }1547 }
15051548
lib/std/debug.zig+30-107
...@@ -14,9 +14,12 @@ const native_os = builtin.os.tag;...@@ -14,9 +14,12 @@ const native_os = builtin.os.tag;
14const native_endian = native_arch.endian();14const native_endian = native_arch.endian();
1515
16pub const MemoryAccessor = @import("debug/MemoryAccessor.zig");16pub const MemoryAccessor = @import("debug/MemoryAccessor.zig");
17pub const FixedBufferReader = @import("debug/FixedBufferReader.zig");
17pub const Dwarf = @import("debug/Dwarf.zig");18pub const Dwarf = @import("debug/Dwarf.zig");
18pub const Pdb = @import("debug/Pdb.zig");19pub const Pdb = @import("debug/Pdb.zig");
19pub const SelfInfo = @import("debug/SelfInfo.zig");20pub const SelfInfo = @import("debug/SelfInfo.zig");
21pub const Info = @import("debug/Info.zig");
22pub const Coverage = @import("debug/Coverage.zig");
2023
21/// Unresolved source locations can be represented with a single `usize` that24/// Unresolved source locations can be represented with a single `usize` that
22/// corresponds to a virtual memory address of the program counter. Combined25/// corresponds to a virtual memory address of the program counter. Combined
...@@ -26,6 +29,18 @@ pub const SourceLocation = struct {...@@ -26,6 +29,18 @@ pub const SourceLocation = struct {
26 line: u64,29 line: u64,
27 column: u64,30 column: u64,
28 file_name: []const u8,31 file_name: []const u8,
32
33 pub const invalid: SourceLocation = .{
34 .line = 0,
35 .column = 0,
36 .file_name = &.{},
37 };
38};
39
40pub const Symbol = struct {
41 name: []const u8 = "???",
42 compile_unit_name: []const u8 = "???",
43 source_location: ?SourceLocation = null,
29};44};
3045
31/// Deprecated because it returns the optimization mode of the standard46/// Deprecated because it returns the optimization mode of the standard
...@@ -748,7 +763,7 @@ pub fn writeCurrentStackTrace(...@@ -748,7 +763,7 @@ pub fn writeCurrentStackTrace(
748 // an overflow. We do not need to signal `StackIterator` as it will correctly detect this763 // an overflow. We do not need to signal `StackIterator` as it will correctly detect this
749 // condition on the subsequent iteration and return `null` thus terminating the loop.764 // condition on the subsequent iteration and return `null` thus terminating the loop.
750 // same behaviour for x86-windows-msvc765 // same behaviour for x86-windows-msvc
751 const address = if (return_address == 0) return_address else return_address - 1;766 const address = return_address -| 1;
752 try printSourceAtAddress(debug_info, out_stream, address, tty_config);767 try printSourceAtAddress(debug_info, out_stream, address, tty_config);
753 } else printLastUnwindError(&it, debug_info, out_stream, tty_config);768 } else printLastUnwindError(&it, debug_info, out_stream, tty_config);
754}769}
...@@ -871,13 +886,13 @@ pub fn printSourceAtAddress(debug_info: *SelfInfo, out_stream: anytype, address:...@@ -871,13 +886,13 @@ pub fn printSourceAtAddress(debug_info: *SelfInfo, out_stream: anytype, address:
871 error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config),886 error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config),
872 else => return err,887 else => return err,
873 };888 };
874 defer symbol_info.deinit(debug_info.allocator);889 defer if (symbol_info.source_location) |sl| debug_info.allocator.free(sl.file_name);
875890
876 return printLineInfo(891 return printLineInfo(
877 out_stream,892 out_stream,
878 symbol_info.line_info,893 symbol_info.source_location,
879 address,894 address,
880 symbol_info.symbol_name,895 symbol_info.name,
881 symbol_info.compile_unit_name,896 symbol_info.compile_unit_name,
882 tty_config,897 tty_config,
883 printLineFromFileAnyOs,898 printLineFromFileAnyOs,
...@@ -886,7 +901,7 @@ pub fn printSourceAtAddress(debug_info: *SelfInfo, out_stream: anytype, address:...@@ -886,7 +901,7 @@ pub fn printSourceAtAddress(debug_info: *SelfInfo, out_stream: anytype, address:
886901
887fn printLineInfo(902fn printLineInfo(
888 out_stream: anytype,903 out_stream: anytype,
889 line_info: ?SourceLocation,904 source_location: ?SourceLocation,
890 address: usize,905 address: usize,
891 symbol_name: []const u8,906 symbol_name: []const u8,
892 compile_unit_name: []const u8,907 compile_unit_name: []const u8,
...@@ -896,8 +911,8 @@ fn printLineInfo(...@@ -896,8 +911,8 @@ fn printLineInfo(
896 nosuspend {911 nosuspend {
897 try tty_config.setColor(out_stream, .bold);912 try tty_config.setColor(out_stream, .bold);
898913
899 if (line_info) |*li| {914 if (source_location) |*sl| {
900 try out_stream.print("{s}:{d}:{d}", .{ li.file_name, li.line, li.column });915 try out_stream.print("{s}:{d}:{d}", .{ sl.file_name, sl.line, sl.column });
901 } else {916 } else {
902 try out_stream.writeAll("???:?:?");917 try out_stream.writeAll("???:?:?");
903 }918 }
...@@ -910,11 +925,11 @@ fn printLineInfo(...@@ -910,11 +925,11 @@ fn printLineInfo(
910 try out_stream.writeAll("\n");925 try out_stream.writeAll("\n");
911926
912 // Show the matching source code line if possible927 // Show the matching source code line if possible
913 if (line_info) |li| {928 if (source_location) |sl| {
914 if (printLineFromFile(out_stream, li)) {929 if (printLineFromFile(out_stream, sl)) {
915 if (li.column > 0) {930 if (sl.column > 0) {
916 // The caret already takes one char931 // The caret already takes one char
917 const space_needed = @as(usize, @intCast(li.column - 1));932 const space_needed = @as(usize, @intCast(sl.column - 1));
918933
919 try out_stream.writeByteNTimes(' ', space_needed);934 try out_stream.writeByteNTimes(' ', space_needed);
920 try tty_config.setColor(out_stream, .green);935 try tty_config.setColor(out_stream, .green);
...@@ -932,10 +947,10 @@ fn printLineInfo(...@@ -932,10 +947,10 @@ fn printLineInfo(
932 }947 }
933}948}
934949
935fn printLineFromFileAnyOs(out_stream: anytype, line_info: SourceLocation) !void {950fn printLineFromFileAnyOs(out_stream: anytype, source_location: SourceLocation) !void {
936 // Need this to always block even in async I/O mode, because this could potentially951 // Need this to always block even in async I/O mode, because this could potentially
937 // be called from e.g. the event loop code crashing.952 // be called from e.g. the event loop code crashing.
938 var f = try fs.cwd().openFile(line_info.file_name, .{});953 var f = try fs.cwd().openFile(source_location.file_name, .{});
939 defer f.close();954 defer f.close();
940 // TODO fstat and make sure that the file has the correct size955 // TODO fstat and make sure that the file has the correct size
941956
...@@ -944,7 +959,7 @@ fn printLineFromFileAnyOs(out_stream: anytype, line_info: SourceLocation) !void...@@ -944,7 +959,7 @@ fn printLineFromFileAnyOs(out_stream: anytype, line_info: SourceLocation) !void
944 const line_start = seek: {959 const line_start = seek: {
945 var current_line_start: usize = 0;960 var current_line_start: usize = 0;
946 var next_line: usize = 1;961 var next_line: usize = 1;
947 while (next_line != line_info.line) {962 while (next_line != source_location.line) {
948 const slice = buf[current_line_start..amt_read];963 const slice = buf[current_line_start..amt_read];
949 if (mem.indexOfScalar(u8, slice, '\n')) |pos| {964 if (mem.indexOfScalar(u8, slice, '\n')) |pos| {
950 next_line += 1;965 next_line += 1;
...@@ -1481,99 +1496,6 @@ pub const SafetyLock = struct {...@@ -1481,99 +1496,6 @@ pub const SafetyLock = struct {
1481 }1496 }
1482};1497};
14831498
1484/// Deprecated. Don't use this, just read from your memory directly.
1485///
1486/// This only exists because someone was too lazy to rework logic that used to
1487/// operate on an open file to operate on a memory buffer instead.
1488pub const DeprecatedFixedBufferReader = struct {
1489 buf: []const u8,
1490 pos: usize = 0,
1491 endian: std.builtin.Endian,
1492
1493 pub const Error = error{ EndOfBuffer, Overflow, InvalidBuffer };
1494
1495 pub fn seekTo(fbr: *DeprecatedFixedBufferReader, pos: u64) Error!void {
1496 if (pos > fbr.buf.len) return error.EndOfBuffer;
1497 fbr.pos = @intCast(pos);
1498 }
1499
1500 pub fn seekForward(fbr: *DeprecatedFixedBufferReader, amount: u64) Error!void {
1501 if (fbr.buf.len - fbr.pos < amount) return error.EndOfBuffer;
1502 fbr.pos += @intCast(amount);
1503 }
1504
1505 pub inline fn readByte(fbr: *DeprecatedFixedBufferReader) Error!u8 {
1506 if (fbr.pos >= fbr.buf.len) return error.EndOfBuffer;
1507 defer fbr.pos += 1;
1508 return fbr.buf[fbr.pos];
1509 }
1510
1511 pub fn readByteSigned(fbr: *DeprecatedFixedBufferReader) Error!i8 {
1512 return @bitCast(try fbr.readByte());
1513 }
1514
1515 pub fn readInt(fbr: *DeprecatedFixedBufferReader, comptime T: type) Error!T {
1516 const size = @divExact(@typeInfo(T).Int.bits, 8);
1517 if (fbr.buf.len - fbr.pos < size) return error.EndOfBuffer;
1518 defer fbr.pos += size;
1519 return std.mem.readInt(T, fbr.buf[fbr.pos..][0..size], fbr.endian);
1520 }
1521
1522 pub fn readIntChecked(
1523 fbr: *DeprecatedFixedBufferReader,
1524 comptime T: type,
1525 ma: *MemoryAccessor,
1526 ) Error!T {
1527 if (ma.load(T, @intFromPtr(fbr.buf[fbr.pos..].ptr)) == null)
1528 return error.InvalidBuffer;
1529
1530 return fbr.readInt(T);
1531 }
1532
1533 pub fn readUleb128(fbr: *DeprecatedFixedBufferReader, comptime T: type) Error!T {
1534 return std.leb.readUleb128(T, fbr);
1535 }
1536
1537 pub fn readIleb128(fbr: *DeprecatedFixedBufferReader, comptime T: type) Error!T {
1538 return std.leb.readIleb128(T, fbr);
1539 }
1540
1541 pub fn readAddress(fbr: *DeprecatedFixedBufferReader, format: std.dwarf.Format) Error!u64 {
1542 return switch (format) {
1543 .@"32" => try fbr.readInt(u32),
1544 .@"64" => try fbr.readInt(u64),
1545 };
1546 }
1547
1548 pub fn readAddressChecked(
1549 fbr: *DeprecatedFixedBufferReader,
1550 format: std.dwarf.Format,
1551 ma: *MemoryAccessor,
1552 ) Error!u64 {
1553 return switch (format) {
1554 .@"32" => try fbr.readIntChecked(u32, ma),
1555 .@"64" => try fbr.readIntChecked(u64, ma),
1556 };
1557 }
1558
1559 pub fn readBytes(fbr: *DeprecatedFixedBufferReader, len: usize) Error![]const u8 {
1560 if (fbr.buf.len - fbr.pos < len) return error.EndOfBuffer;
1561 defer fbr.pos += len;
1562 return fbr.buf[fbr.pos..][0..len];
1563 }
1564
1565 pub fn readBytesTo(fbr: *DeprecatedFixedBufferReader, comptime sentinel: u8) Error![:sentinel]const u8 {
1566 const end = @call(.always_inline, std.mem.indexOfScalarPos, .{
1567 u8,
1568 fbr.buf,
1569 fbr.pos,
1570 sentinel,
1571 }) orelse return error.EndOfBuffer;
1572 defer fbr.pos = end + 1;
1573 return fbr.buf[fbr.pos..end :sentinel];
1574 }
1575};
1576
1577/// Detect whether the program is being executed in the Valgrind virtual machine.1499/// Detect whether the program is being executed in the Valgrind virtual machine.
1578///1500///
1579/// When Valgrind integrations are disabled, this returns comptime-known false.1501/// When Valgrind integrations are disabled, this returns comptime-known false.
...@@ -1587,6 +1509,7 @@ pub inline fn inValgrind() bool {...@@ -1587,6 +1509,7 @@ pub inline fn inValgrind() bool {
1587test {1509test {
1588 _ = &Dwarf;1510 _ = &Dwarf;
1589 _ = &MemoryAccessor;1511 _ = &MemoryAccessor;
1512 _ = &FixedBufferReader;
1590 _ = &Pdb;1513 _ = &Pdb;
1591 _ = &SelfInfo;1514 _ = &SelfInfo;
1592 _ = &dumpHex;1515 _ = &dumpHex;
lib/std/debug/Coverage.zig created+244
...@@ -0,0 +1,244 @@
1const std = @import("../std.zig");
2const Allocator = std.mem.Allocator;
3const Hash = std.hash.Wyhash;
4const Dwarf = std.debug.Dwarf;
5const assert = std.debug.assert;
6
7const Coverage = @This();
8
9/// Provides a globally-scoped integer index for directories.
10///
11/// As opposed to, for example, a directory index that is compilation-unit
12/// scoped inside a single ELF module.
13///
14/// String memory references the memory-mapped debug information.
15///
16/// Protected by `mutex`.
17directories: std.ArrayHashMapUnmanaged(String, void, String.MapContext, false),
18/// Provides a globally-scoped integer index for files.
19///
20/// String memory references the memory-mapped debug information.
21///
22/// Protected by `mutex`.
23files: std.ArrayHashMapUnmanaged(File, void, File.MapContext, false),
24string_bytes: std.ArrayListUnmanaged(u8),
25/// Protects the other fields.
26mutex: std.Thread.Mutex,
27
28pub const init: Coverage = .{
29 .directories = .{},
30 .files = .{},
31 .mutex = .{},
32 .string_bytes = .{},
33};
34
35pub const String = enum(u32) {
36 _,
37
38 pub const MapContext = struct {
39 string_bytes: []const u8,
40
41 pub fn eql(self: @This(), a: String, b: String, b_index: usize) bool {
42 _ = b_index;
43 const a_slice = span(self.string_bytes[@intFromEnum(a)..]);
44 const b_slice = span(self.string_bytes[@intFromEnum(b)..]);
45 return std.mem.eql(u8, a_slice, b_slice);
46 }
47
48 pub fn hash(self: @This(), a: String) u32 {
49 return @truncate(Hash.hash(0, span(self.string_bytes[@intFromEnum(a)..])));
50 }
51 };
52
53 pub const SliceAdapter = struct {
54 string_bytes: []const u8,
55
56 pub fn eql(self: @This(), a_slice: []const u8, b: String, b_index: usize) bool {
57 _ = b_index;
58 const b_slice = span(self.string_bytes[@intFromEnum(b)..]);
59 return std.mem.eql(u8, a_slice, b_slice);
60 }
61 pub fn hash(self: @This(), a: []const u8) u32 {
62 _ = self;
63 return @truncate(Hash.hash(0, a));
64 }
65 };
66};
67
68pub const SourceLocation = extern struct {
69 file: File.Index,
70 line: u32,
71 column: u32,
72
73 pub const invalid: SourceLocation = .{
74 .file = .invalid,
75 .line = 0,
76 .column = 0,
77 };
78};
79
80pub const File = extern struct {
81 directory_index: u32,
82 basename: String,
83
84 pub const Index = enum(u32) {
85 invalid = std.math.maxInt(u32),
86 _,
87 };
88
89 pub const MapContext = struct {
90 string_bytes: []const u8,
91
92 pub fn hash(self: MapContext, a: File) u32 {
93 const a_basename = span(self.string_bytes[@intFromEnum(a.basename)..]);
94 return @truncate(Hash.hash(a.directory_index, a_basename));
95 }
96
97 pub fn eql(self: MapContext, a: File, b: File, b_index: usize) bool {
98 _ = b_index;
99 if (a.directory_index != b.directory_index) return false;
100 const a_basename = span(self.string_bytes[@intFromEnum(a.basename)..]);
101 const b_basename = span(self.string_bytes[@intFromEnum(b.basename)..]);
102 return std.mem.eql(u8, a_basename, b_basename);
103 }
104 };
105
106 pub const SliceAdapter = struct {
107 string_bytes: []const u8,
108
109 pub const Entry = struct {
110 directory_index: u32,
111 basename: []const u8,
112 };
113
114 pub fn hash(self: @This(), a: Entry) u32 {
115 _ = self;
116 return @truncate(Hash.hash(a.directory_index, a.basename));
117 }
118
119 pub fn eql(self: @This(), a: Entry, b: File, b_index: usize) bool {
120 _ = b_index;
121 if (a.directory_index != b.directory_index) return false;
122 const b_basename = span(self.string_bytes[@intFromEnum(b.basename)..]);
123 return std.mem.eql(u8, a.basename, b_basename);
124 }
125 };
126};
127
128pub fn deinit(cov: *Coverage, gpa: Allocator) void {
129 cov.directories.deinit(gpa);
130 cov.files.deinit(gpa);
131 cov.string_bytes.deinit(gpa);
132 cov.* = undefined;
133}
134
135pub fn fileAt(cov: *Coverage, index: File.Index) *File {
136 return &cov.files.keys()[@intFromEnum(index)];
137}
138
139pub fn stringAt(cov: *Coverage, index: String) [:0]const u8 {
140 return span(cov.string_bytes.items[@intFromEnum(index)..]);
141}
142
143pub const ResolveAddressesDwarfError = Dwarf.ScanError;
144
145pub fn resolveAddressesDwarf(
146 cov: *Coverage,
147 gpa: Allocator,
148 sorted_pc_addrs: []const u64,
149 /// Asserts its length equals length of `sorted_pc_addrs`.
150 output: []SourceLocation,
151 d: *Dwarf,
152) ResolveAddressesDwarfError!void {
153 assert(sorted_pc_addrs.len == output.len);
154 assert(d.compile_units_sorted);
155
156 var cu_i: usize = 0;
157 var line_table_i: usize = 0;
158 var cu: *Dwarf.CompileUnit = &d.compile_unit_list.items[0];
159 var range = cu.pc_range.?;
160 // Protects directories and files tables from other threads.
161 cov.mutex.lock();
162 defer cov.mutex.unlock();
163 next_pc: for (sorted_pc_addrs, output) |pc, *out| {
164 while (pc >= range.end) {
165 cu_i += 1;
166 if (cu_i >= d.compile_unit_list.items.len) {
167 out.* = SourceLocation.invalid;
168 continue :next_pc;
169 }
170 cu = &d.compile_unit_list.items[cu_i];
171 line_table_i = 0;
172 range = cu.pc_range orelse {
173 out.* = SourceLocation.invalid;
174 continue :next_pc;
175 };
176 }
177 if (pc < range.start) {
178 out.* = SourceLocation.invalid;
179 continue :next_pc;
180 }
181 if (line_table_i == 0) {
182 line_table_i = 1;
183 cov.mutex.unlock();
184 defer cov.mutex.lock();
185 d.populateSrcLocCache(gpa, cu) catch |err| switch (err) {
186 error.MissingDebugInfo, error.InvalidDebugInfo => {
187 out.* = SourceLocation.invalid;
188 cu_i += 1;
189 if (cu_i < d.compile_unit_list.items.len) {
190 cu = &d.compile_unit_list.items[cu_i];
191 line_table_i = 0;
192 if (cu.pc_range) |r| range = r;
193 }
194 continue :next_pc;
195 },
196 else => |e| return e,
197 };
198 }
199 const slc = &cu.src_loc_cache.?;
200 const table_addrs = slc.line_table.keys();
201 while (line_table_i < table_addrs.len and table_addrs[line_table_i] < pc) line_table_i += 1;
202
203 const entry = slc.line_table.values()[line_table_i - 1];
204 const corrected_file_index = entry.file - @intFromBool(slc.version < 5);
205 const file_entry = slc.files[corrected_file_index];
206 const dir_path = slc.directories[file_entry.dir_index].path;
207 try cov.string_bytes.ensureUnusedCapacity(gpa, dir_path.len + file_entry.path.len + 2);
208 const dir_gop = try cov.directories.getOrPutContextAdapted(gpa, dir_path, String.SliceAdapter{
209 .string_bytes = cov.string_bytes.items,
210 }, String.MapContext{
211 .string_bytes = cov.string_bytes.items,
212 });
213 if (!dir_gop.found_existing)
214 dir_gop.key_ptr.* = addStringAssumeCapacity(cov, dir_path);
215 const file_gop = try cov.files.getOrPutContextAdapted(gpa, File.SliceAdapter.Entry{
216 .directory_index = @intCast(dir_gop.index),
217 .basename = file_entry.path,
218 }, File.SliceAdapter{
219 .string_bytes = cov.string_bytes.items,
220 }, File.MapContext{
221 .string_bytes = cov.string_bytes.items,
222 });
223 if (!file_gop.found_existing) file_gop.key_ptr.* = .{
224 .directory_index = @intCast(dir_gop.index),
225 .basename = addStringAssumeCapacity(cov, file_entry.path),
226 };
227 out.* = .{
228 .file = @enumFromInt(file_gop.index),
229 .line = entry.line,
230 .column = entry.column,
231 };
232 }
233}
234
235pub fn addStringAssumeCapacity(cov: *Coverage, s: []const u8) String {
236 const result: String = @enumFromInt(cov.string_bytes.items.len);
237 cov.string_bytes.appendSliceAssumeCapacity(s);
238 cov.string_bytes.appendAssumeCapacity(0);
239 return result;
240}
241
242fn span(s: []const u8) [:0]const u8 {
243 return std.mem.sliceTo(@as([:0]const u8, @ptrCast(s)), 0);
244}
lib/std/debug/Dwarf.zig+556-204
...@@ -12,6 +12,8 @@ const native_endian = builtin.cpu.arch.endian();...@@ -12,6 +12,8 @@ const native_endian = builtin.cpu.arch.endian();
1212
13const std = @import("../std.zig");13const std = @import("../std.zig");
14const Allocator = std.mem.Allocator;14const Allocator = std.mem.Allocator;
15const elf = std.elf;
16const mem = std.mem;
15const DW = std.dwarf;17const DW = std.dwarf;
16const AT = DW.AT;18const AT = DW.AT;
17const EH = DW.EH;19const EH = DW.EH;
...@@ -22,11 +24,10 @@ const UT = DW.UT;...@@ -22,11 +24,10 @@ const UT = DW.UT;
22const assert = std.debug.assert;24const assert = std.debug.assert;
23const cast = std.math.cast;25const cast = std.math.cast;
24const maxInt = std.math.maxInt;26const maxInt = std.math.maxInt;
25const readInt = std.mem.readInt;
26const MemoryAccessor = std.debug.MemoryAccessor;27const MemoryAccessor = std.debug.MemoryAccessor;
28const Path = std.Build.Cache.Path;
2729
28/// Did I mention this is deprecated?30const FixedBufferReader = std.debug.FixedBufferReader;
29const DeprecatedFixedBufferReader = std.debug.DeprecatedFixedBufferReader;
3031
31const Dwarf = @This();32const Dwarf = @This();
3233
...@@ -37,6 +38,7 @@ pub const call_frame = @import("Dwarf/call_frame.zig");...@@ -37,6 +38,7 @@ pub const call_frame = @import("Dwarf/call_frame.zig");
37endian: std.builtin.Endian,38endian: std.builtin.Endian,
38sections: SectionArray = null_section_array,39sections: SectionArray = null_section_array,
39is_macho: bool,40is_macho: bool,
41compile_units_sorted: bool,
4042
41// Filled later by the initializer43// Filled later by the initializer
42abbrev_table_list: std.ArrayListUnmanaged(Abbrev.Table) = .{},44abbrev_table_list: std.ArrayListUnmanaged(Abbrev.Table) = .{},
...@@ -136,6 +138,34 @@ pub const CompileUnit = struct {...@@ -136,6 +138,34 @@ pub const CompileUnit = struct {
136 rnglists_base: usize,138 rnglists_base: usize,
137 loclists_base: usize,139 loclists_base: usize,
138 frame_base: ?*const FormValue,140 frame_base: ?*const FormValue,
141
142 src_loc_cache: ?SrcLocCache,
143
144 pub const SrcLocCache = struct {
145 line_table: LineTable,
146 directories: []const FileEntry,
147 files: []FileEntry,
148 version: u16,
149
150 pub const LineTable = std.AutoArrayHashMapUnmanaged(u64, LineEntry);
151
152 pub const LineEntry = struct {
153 line: u32,
154 column: u32,
155 /// Offset by 1 depending on whether Dwarf version is >= 5.
156 file: u32,
157 };
158
159 pub fn findSource(slc: *const SrcLocCache, address: u64) !LineEntry {
160 const index = std.sort.upperBound(u64, slc.line_table.keys(), address, struct {
161 fn order(context: u64, item: u64) std.math.Order {
162 return std.math.order(item, context);
163 }
164 }.order);
165 if (index == 0) return missing();
166 return slc.line_table.values()[index - 1];
167 }
168 };
139};169};
140170
141pub const FormValue = union(enum) {171pub const FormValue = union(enum) {
...@@ -252,13 +282,13 @@ pub const Die = struct {...@@ -252,13 +282,13 @@ pub const Die = struct {
252 .@"32" => {282 .@"32" => {
253 const byte_offset = compile_unit.str_offsets_base + 4 * index;283 const byte_offset = compile_unit.str_offsets_base + 4 * index;
254 if (byte_offset + 4 > debug_str_offsets.len) return bad();284 if (byte_offset + 4 > debug_str_offsets.len) return bad();
255 const offset = readInt(u32, debug_str_offsets[byte_offset..][0..4], di.endian);285 const offset = mem.readInt(u32, debug_str_offsets[byte_offset..][0..4], di.endian);
256 return getStringGeneric(opt_str, offset);286 return getStringGeneric(opt_str, offset);
257 },287 },
258 .@"64" => {288 .@"64" => {
259 const byte_offset = compile_unit.str_offsets_base + 8 * index;289 const byte_offset = compile_unit.str_offsets_base + 8 * index;
260 if (byte_offset + 8 > debug_str_offsets.len) return bad();290 if (byte_offset + 8 > debug_str_offsets.len) return bad();
261 const offset = readInt(u64, debug_str_offsets[byte_offset..][0..8], di.endian);291 const offset = mem.readInt(u64, debug_str_offsets[byte_offset..][0..8], di.endian);
262 return getStringGeneric(opt_str, offset);292 return getStringGeneric(opt_str, offset);
263 },293 },
264 }294 }
...@@ -325,7 +355,7 @@ pub const ExceptionFrameHeader = struct {...@@ -325,7 +355,7 @@ pub const ExceptionFrameHeader = struct {
325 var left: usize = 0;355 var left: usize = 0;
326 var len: usize = self.fde_count;356 var len: usize = self.fde_count;
327357
328 var fbr: DeprecatedFixedBufferReader = .{ .buf = self.entries, .endian = native_endian };358 var fbr: FixedBufferReader = .{ .buf = self.entries, .endian = native_endian };
329359
330 while (len > 1) {360 while (len > 1) {
331 const mid = left + len / 2;361 const mid = left + len / 2;
...@@ -368,7 +398,7 @@ pub const ExceptionFrameHeader = struct {...@@ -368,7 +398,7 @@ pub const ExceptionFrameHeader = struct {
368 const eh_frame = @as([*]const u8, @ptrFromInt(self.eh_frame_ptr))[0 .. eh_frame_len orelse maxInt(u32)];398 const eh_frame = @as([*]const u8, @ptrFromInt(self.eh_frame_ptr))[0 .. eh_frame_len orelse maxInt(u32)];
369399
370 const fde_offset = fde_ptr - self.eh_frame_ptr;400 const fde_offset = fde_ptr - self.eh_frame_ptr;
371 var eh_frame_fbr: DeprecatedFixedBufferReader = .{401 var eh_frame_fbr: FixedBufferReader = .{
372 .buf = eh_frame,402 .buf = eh_frame,
373 .pos = fde_offset,403 .pos = fde_offset,
374 .endian = native_endian,404 .endian = native_endian,
...@@ -426,9 +456,9 @@ pub const EntryHeader = struct {...@@ -426,9 +456,9 @@ pub const EntryHeader = struct {
426 }456 }
427457
428 /// Reads a header for either an FDE or a CIE, then advances the fbr to the position after the trailing structure.458 /// Reads a header for either an FDE or a CIE, then advances the fbr to the position after the trailing structure.
429 /// `fbr` must be a DeprecatedFixedBufferReader backed by either the .eh_frame or .debug_frame sections.459 /// `fbr` must be a FixedBufferReader backed by either the .eh_frame or .debug_frame sections.
430 pub fn read(460 pub fn read(
431 fbr: *DeprecatedFixedBufferReader,461 fbr: *FixedBufferReader,
432 opt_ma: ?*MemoryAccessor,462 opt_ma: ?*MemoryAccessor,
433 dwarf_section: Section.Id,463 dwarf_section: Section.Id,
434 ) !EntryHeader {464 ) !EntryHeader {
...@@ -541,7 +571,7 @@ pub const CommonInformationEntry = struct {...@@ -541,7 +571,7 @@ pub const CommonInformationEntry = struct {
541 ) !CommonInformationEntry {571 ) !CommonInformationEntry {
542 if (addr_size_bytes > 8) return error.UnsupportedAddrSize;572 if (addr_size_bytes > 8) return error.UnsupportedAddrSize;
543573
544 var fbr: DeprecatedFixedBufferReader = .{ .buf = cie_bytes, .endian = endian };574 var fbr: FixedBufferReader = .{ .buf = cie_bytes, .endian = endian };
545575
546 const version = try fbr.readByte();576 const version = try fbr.readByte();
547 switch (dwarf_section) {577 switch (dwarf_section) {
...@@ -675,7 +705,7 @@ pub const FrameDescriptionEntry = struct {...@@ -675,7 +705,7 @@ pub const FrameDescriptionEntry = struct {
675 ) !FrameDescriptionEntry {705 ) !FrameDescriptionEntry {
676 if (addr_size_bytes > 8) return error.InvalidAddrSize;706 if (addr_size_bytes > 8) return error.InvalidAddrSize;
677707
678 var fbr: DeprecatedFixedBufferReader = .{ .buf = fde_bytes, .endian = endian };708 var fbr: FixedBufferReader = .{ .buf = fde_bytes, .endian = endian };
679709
680 const pc_begin = try readEhPointer(&fbr, cie.fde_pointer_enc, addr_size_bytes, .{710 const pc_begin = try readEhPointer(&fbr, cie.fde_pointer_enc, addr_size_bytes, .{
681 .pc_rel_base = try pcRelBase(@intFromPtr(&fde_bytes[fbr.pos]), pc_rel_offset),711 .pc_rel_base = try pcRelBase(@intFromPtr(&fde_bytes[fbr.pos]), pc_rel_offset),
...@@ -721,12 +751,14 @@ const num_sections = std.enums.directEnumArrayLen(Section.Id, 0);...@@ -721,12 +751,14 @@ const num_sections = std.enums.directEnumArrayLen(Section.Id, 0);
721pub const SectionArray = [num_sections]?Section;751pub const SectionArray = [num_sections]?Section;
722pub const null_section_array = [_]?Section{null} ** num_sections;752pub const null_section_array = [_]?Section{null} ** num_sections;
723753
754pub const OpenError = ScanError;
755
724/// Initialize DWARF info. The caller has the responsibility to initialize most756/// Initialize DWARF info. The caller has the responsibility to initialize most
725/// the `Dwarf` fields before calling. `binary_mem` is the raw bytes of the757/// the `Dwarf` fields before calling. `binary_mem` is the raw bytes of the
726/// main binary file (not the secondary debug info file).758/// main binary file (not the secondary debug info file).
727pub fn open(di: *Dwarf, allocator: Allocator) !void {759pub fn open(d: *Dwarf, gpa: Allocator) OpenError!void {
728 try di.scanAllFunctions(allocator);760 try d.scanAllFunctions(gpa);
729 try di.scanAllCompileUnits(allocator);761 try d.scanAllCompileUnits(gpa);
730}762}
731763
732const PcRange = struct {764const PcRange = struct {
...@@ -747,21 +779,26 @@ pub fn sectionVirtualOffset(di: Dwarf, dwarf_section: Section.Id, base_address:...@@ -747,21 +779,26 @@ pub fn sectionVirtualOffset(di: Dwarf, dwarf_section: Section.Id, base_address:
747 return if (di.sections[@intFromEnum(dwarf_section)]) |s| s.virtualOffset(base_address) else null;779 return if (di.sections[@intFromEnum(dwarf_section)]) |s| s.virtualOffset(base_address) else null;
748}780}
749781
750pub fn deinit(di: *Dwarf, allocator: Allocator) void {782pub fn deinit(di: *Dwarf, gpa: Allocator) void {
751 for (di.sections) |opt_section| {783 for (di.sections) |opt_section| {
752 if (opt_section) |s| if (s.owned) allocator.free(s.data);784 if (opt_section) |s| if (s.owned) gpa.free(s.data);
753 }785 }
754 for (di.abbrev_table_list.items) |*abbrev| {786 for (di.abbrev_table_list.items) |*abbrev| {
755 abbrev.deinit(allocator);787 abbrev.deinit(gpa);
756 }788 }
757 di.abbrev_table_list.deinit(allocator);789 di.abbrev_table_list.deinit(gpa);
758 for (di.compile_unit_list.items) |*cu| {790 for (di.compile_unit_list.items) |*cu| {
759 cu.die.deinit(allocator);791 if (cu.src_loc_cache) |*slc| {
792 slc.line_table.deinit(gpa);
793 gpa.free(slc.directories);
794 gpa.free(slc.files);
795 }
796 cu.die.deinit(gpa);
760 }797 }
761 di.compile_unit_list.deinit(allocator);798 di.compile_unit_list.deinit(gpa);
762 di.func_list.deinit(allocator);799 di.func_list.deinit(gpa);
763 di.cie_map.deinit(allocator);800 di.cie_map.deinit(gpa);
764 di.fde_list.deinit(allocator);801 di.fde_list.deinit(gpa);
765 di.* = undefined;802 di.* = undefined;
766}803}
767804
...@@ -777,8 +814,13 @@ pub fn getSymbolName(di: *Dwarf, address: u64) ?[]const u8 {...@@ -777,8 +814,13 @@ pub fn getSymbolName(di: *Dwarf, address: u64) ?[]const u8 {
777 return null;814 return null;
778}815}
779816
780fn scanAllFunctions(di: *Dwarf, allocator: Allocator) !void {817pub const ScanError = error{
781 var fbr: DeprecatedFixedBufferReader = .{ .buf = di.section(.debug_info).?, .endian = di.endian };818 InvalidDebugInfo,
819 MissingDebugInfo,
820} || Allocator.Error || std.debug.FixedBufferReader.Error;
821
822fn scanAllFunctions(di: *Dwarf, allocator: Allocator) ScanError!void {
823 var fbr: FixedBufferReader = .{ .buf = di.section(.debug_info).?, .endian = di.endian };
782 var this_unit_offset: u64 = 0;824 var this_unit_offset: u64 = 0;
783825
784 while (this_unit_offset < fbr.buf.len) {826 while (this_unit_offset < fbr.buf.len) {
...@@ -837,6 +879,7 @@ fn scanAllFunctions(di: *Dwarf, allocator: Allocator) !void {...@@ -837,6 +879,7 @@ fn scanAllFunctions(di: *Dwarf, allocator: Allocator) !void {
837 .rnglists_base = 0,879 .rnglists_base = 0,
838 .loclists_base = 0,880 .loclists_base = 0,
839 .frame_base = null,881 .frame_base = null,
882 .src_loc_cache = null,
840 };883 };
841884
842 while (true) {885 while (true) {
...@@ -964,8 +1007,8 @@ fn scanAllFunctions(di: *Dwarf, allocator: Allocator) !void {...@@ -964,8 +1007,8 @@ fn scanAllFunctions(di: *Dwarf, allocator: Allocator) !void {
964 }1007 }
965}1008}
9661009
967fn scanAllCompileUnits(di: *Dwarf, allocator: Allocator) !void {1010fn scanAllCompileUnits(di: *Dwarf, allocator: Allocator) ScanError!void {
968 var fbr: DeprecatedFixedBufferReader = .{ .buf = di.section(.debug_info).?, .endian = di.endian };1011 var fbr: FixedBufferReader = .{ .buf = di.section(.debug_info).?, .endian = di.endian };
969 var this_unit_offset: u64 = 0;1012 var this_unit_offset: u64 = 0;
9701013
971 var attrs_buf = std.ArrayList(Die.Attr).init(allocator);1014 var attrs_buf = std.ArrayList(Die.Attr).init(allocator);
...@@ -1023,6 +1066,7 @@ fn scanAllCompileUnits(di: *Dwarf, allocator: Allocator) !void {...@@ -1023,6 +1066,7 @@ fn scanAllCompileUnits(di: *Dwarf, allocator: Allocator) !void {
1023 .rnglists_base = if (compile_unit_die.getAttr(AT.rnglists_base)) |fv| try fv.getUInt(usize) else 0,1066 .rnglists_base = if (compile_unit_die.getAttr(AT.rnglists_base)) |fv| try fv.getUInt(usize) else 0,
1024 .loclists_base = if (compile_unit_die.getAttr(AT.loclists_base)) |fv| try fv.getUInt(usize) else 0,1067 .loclists_base = if (compile_unit_die.getAttr(AT.loclists_base)) |fv| try fv.getUInt(usize) else 0,
1025 .frame_base = compile_unit_die.getAttr(AT.frame_base),1068 .frame_base = compile_unit_die.getAttr(AT.frame_base),
1069 .src_loc_cache = null,
1026 };1070 };
10271071
1028 compile_unit.pc_range = x: {1072 compile_unit.pc_range = x: {
...@@ -1052,12 +1096,45 @@ fn scanAllCompileUnits(di: *Dwarf, allocator: Allocator) !void {...@@ -1052,12 +1096,45 @@ fn scanAllCompileUnits(di: *Dwarf, allocator: Allocator) !void {
1052 }1096 }
1053}1097}
10541098
1099/// Populate missing PC ranges in compilation units, and then sort them by start address.
1100/// Does not guarantee pc_range to be non-null because there could be missing debug info.
1101pub fn sortCompileUnits(d: *Dwarf) ScanError!void {
1102 assert(!d.compile_units_sorted);
1103
1104 for (d.compile_unit_list.items) |*cu| {
1105 if (cu.pc_range != null) continue;
1106 const ranges_value = cu.die.getAttr(AT.ranges) orelse continue;
1107 var iter = DebugRangeIterator.init(ranges_value, d, cu) catch continue;
1108 var start: u64 = maxInt(u64);
1109 var end: u64 = 0;
1110 while (try iter.next()) |range| {
1111 start = @min(start, range.start_addr);
1112 end = @max(end, range.end_addr);
1113 }
1114 if (end != 0) cu.pc_range = .{
1115 .start = start,
1116 .end = end,
1117 };
1118 }
1119
1120 std.mem.sortUnstable(CompileUnit, d.compile_unit_list.items, {}, struct {
1121 pub fn lessThan(ctx: void, a: CompileUnit, b: CompileUnit) bool {
1122 _ = ctx;
1123 const a_range = a.pc_range orelse return false;
1124 const b_range = b.pc_range orelse return true;
1125 return a_range.start < b_range.start;
1126 }
1127 }.lessThan);
1128
1129 d.compile_units_sorted = true;
1130}
1131
1055const DebugRangeIterator = struct {1132const DebugRangeIterator = struct {
1056 base_address: u64,1133 base_address: u64,
1057 section_type: Section.Id,1134 section_type: Section.Id,
1058 di: *const Dwarf,1135 di: *const Dwarf,
1059 compile_unit: *const CompileUnit,1136 compile_unit: *const CompileUnit,
1060 fbr: DeprecatedFixedBufferReader,1137 fbr: FixedBufferReader,
10611138
1062 pub fn init(ranges_value: *const FormValue, di: *const Dwarf, compile_unit: *const CompileUnit) !@This() {1139 pub fn init(ranges_value: *const FormValue, di: *const Dwarf, compile_unit: *const CompileUnit) !@This() {
1063 const section_type = if (compile_unit.version >= 5) Section.Id.debug_rnglists else Section.Id.debug_ranges;1140 const section_type = if (compile_unit.version >= 5) Section.Id.debug_rnglists else Section.Id.debug_ranges;
...@@ -1070,13 +1147,13 @@ const DebugRangeIterator = struct {...@@ -1070,13 +1147,13 @@ const DebugRangeIterator = struct {
1070 .@"32" => {1147 .@"32" => {
1071 const offset_loc = @as(usize, @intCast(compile_unit.rnglists_base + 4 * idx));1148 const offset_loc = @as(usize, @intCast(compile_unit.rnglists_base + 4 * idx));
1072 if (offset_loc + 4 > debug_ranges.len) return bad();1149 if (offset_loc + 4 > debug_ranges.len) return bad();
1073 const offset = readInt(u32, debug_ranges[offset_loc..][0..4], di.endian);1150 const offset = mem.readInt(u32, debug_ranges[offset_loc..][0..4], di.endian);
1074 break :off compile_unit.rnglists_base + offset;1151 break :off compile_unit.rnglists_base + offset;
1075 },1152 },
1076 .@"64" => {1153 .@"64" => {
1077 const offset_loc = @as(usize, @intCast(compile_unit.rnglists_base + 8 * idx));1154 const offset_loc = @as(usize, @intCast(compile_unit.rnglists_base + 8 * idx));
1078 if (offset_loc + 8 > debug_ranges.len) return bad();1155 if (offset_loc + 8 > debug_ranges.len) return bad();
1079 const offset = readInt(u64, debug_ranges[offset_loc..][0..8], di.endian);1156 const offset = mem.readInt(u64, debug_ranges[offset_loc..][0..8], di.endian);
1080 break :off compile_unit.rnglists_base + offset;1157 break :off compile_unit.rnglists_base + offset;
1081 },1158 },
1082 }1159 }
...@@ -1199,7 +1276,8 @@ const DebugRangeIterator = struct {...@@ -1199,7 +1276,8 @@ const DebugRangeIterator = struct {
1199 }1276 }
1200};1277};
12011278
1202pub fn findCompileUnit(di: *const Dwarf, target_address: u64) !*const CompileUnit {1279/// TODO: change this to binary searching the sorted compile unit list
1280pub fn findCompileUnit(di: *const Dwarf, target_address: u64) !*CompileUnit {
1203 for (di.compile_unit_list.items) |*compile_unit| {1281 for (di.compile_unit_list.items) |*compile_unit| {
1204 if (compile_unit.pc_range) |range| {1282 if (compile_unit.pc_range) |range| {
1205 if (target_address >= range.start and target_address < range.end) return compile_unit;1283 if (target_address >= range.start and target_address < range.end) return compile_unit;
...@@ -1231,7 +1309,7 @@ fn getAbbrevTable(di: *Dwarf, allocator: Allocator, abbrev_offset: u64) !*const...@@ -1231,7 +1309,7 @@ fn getAbbrevTable(di: *Dwarf, allocator: Allocator, abbrev_offset: u64) !*const
1231}1309}
12321310
1233fn parseAbbrevTable(di: *Dwarf, allocator: Allocator, offset: u64) !Abbrev.Table {1311fn parseAbbrevTable(di: *Dwarf, allocator: Allocator, offset: u64) !Abbrev.Table {
1234 var fbr: DeprecatedFixedBufferReader = .{1312 var fbr: FixedBufferReader = .{
1235 .buf = di.section(.debug_abbrev).?,1313 .buf = di.section(.debug_abbrev).?,
1236 .pos = cast(usize, offset) orelse return bad(),1314 .pos = cast(usize, offset) orelse return bad(),
1237 .endian = di.endian,1315 .endian = di.endian,
...@@ -1283,11 +1361,11 @@ fn parseAbbrevTable(di: *Dwarf, allocator: Allocator, offset: u64) !Abbrev.Table...@@ -1283,11 +1361,11 @@ fn parseAbbrevTable(di: *Dwarf, allocator: Allocator, offset: u64) !Abbrev.Table
1283}1361}
12841362
1285fn parseDie(1363fn parseDie(
1286 fbr: *DeprecatedFixedBufferReader,1364 fbr: *FixedBufferReader,
1287 attrs_buf: []Die.Attr,1365 attrs_buf: []Die.Attr,
1288 abbrev_table: *const Abbrev.Table,1366 abbrev_table: *const Abbrev.Table,
1289 format: Format,1367 format: Format,
1290) !?Die {1368) ScanError!?Die {
1291 const abbrev_code = try fbr.readUleb128(u64);1369 const abbrev_code = try fbr.readUleb128(u64);
1292 if (abbrev_code == 0) return null;1370 if (abbrev_code == 0) return null;
1293 const table_entry = abbrev_table.get(abbrev_code) orelse return bad();1371 const table_entry = abbrev_table.get(abbrev_code) orelse return bad();
...@@ -1309,34 +1387,36 @@ fn parseDie(...@@ -1309,34 +1387,36 @@ fn parseDie(
1309 };1387 };
1310}1388}
13111389
1312pub fn getLineNumberInfo(1390fn runLineNumberProgram(d: *Dwarf, gpa: Allocator, compile_unit: *CompileUnit) !CompileUnit.SrcLocCache {
1313 di: *Dwarf,1391 const compile_unit_cwd = try compile_unit.die.getAttrString(d, AT.comp_dir, d.section(.debug_line_str), compile_unit.*);
1314 allocator: Allocator,
1315 compile_unit: CompileUnit,
1316 target_address: u64,
1317) !std.debug.SourceLocation {
1318 const compile_unit_cwd = try compile_unit.die.getAttrString(di, AT.comp_dir, di.section(.debug_line_str), compile_unit);
1319 const line_info_offset = try compile_unit.die.getAttrSecOffset(AT.stmt_list);1392 const line_info_offset = try compile_unit.die.getAttrSecOffset(AT.stmt_list);
13201393
1321 var fbr: DeprecatedFixedBufferReader = .{ .buf = di.section(.debug_line).?, .endian = di.endian };1394 var fbr: FixedBufferReader = .{
1395 .buf = d.section(.debug_line).?,
1396 .endian = d.endian,
1397 };
1322 try fbr.seekTo(line_info_offset);1398 try fbr.seekTo(line_info_offset);
13231399
1324 const unit_header = try readUnitHeader(&fbr, null);1400 const unit_header = try readUnitHeader(&fbr, null);
1325 if (unit_header.unit_length == 0) return missing();1401 if (unit_header.unit_length == 0) return missing();
1402
1326 const next_offset = unit_header.header_length + unit_header.unit_length;1403 const next_offset = unit_header.header_length + unit_header.unit_length;
13271404
1328 const version = try fbr.readInt(u16);1405 const version = try fbr.readInt(u16);
1329 if (version < 2) return bad();1406 if (version < 2) return bad();
13301407
1331 var addr_size: u8 = switch (unit_header.format) {1408 const addr_size: u8, const seg_size: u8 = if (version >= 5) .{
1332 .@"32" => 4,1409 try fbr.readByte(),
1333 .@"64" => 8,1410 try fbr.readByte(),
1411 } else .{
1412 switch (unit_header.format) {
1413 .@"32" => 4,
1414 .@"64" => 8,
1415 },
1416 0,
1334 };1417 };
1335 var seg_size: u8 = 0;1418 _ = addr_size;
1336 if (version >= 5) {1419 _ = seg_size;
1337 addr_size = try fbr.readByte();
1338 seg_size = try fbr.readByte();
1339 }
13401420
1341 const prologue_length = try fbr.readAddress(unit_header.format);1421 const prologue_length = try fbr.readAddress(unit_header.format);
1342 const prog_start_offset = fbr.pos + prologue_length;1422 const prog_start_offset = fbr.pos + prologue_length;
...@@ -1345,8 +1425,8 @@ pub fn getLineNumberInfo(...@@ -1345,8 +1425,8 @@ pub fn getLineNumberInfo(
1345 if (minimum_instruction_length == 0) return bad();1425 if (minimum_instruction_length == 0) return bad();
13461426
1347 if (version >= 4) {1427 if (version >= 4) {
1348 // maximum_operations_per_instruction1428 const maximum_operations_per_instruction = try fbr.readByte();
1349 _ = try fbr.readByte();1429 _ = maximum_operations_per_instruction;
1350 }1430 }
13511431
1352 const default_is_stmt = (try fbr.readByte()) != 0;1432 const default_is_stmt = (try fbr.readByte()) != 0;
...@@ -1359,18 +1439,18 @@ pub fn getLineNumberInfo(...@@ -1359,18 +1439,18 @@ pub fn getLineNumberInfo(
13591439
1360 const standard_opcode_lengths = try fbr.readBytes(opcode_base - 1);1440 const standard_opcode_lengths = try fbr.readBytes(opcode_base - 1);
13611441
1362 var include_directories = std.ArrayList(FileEntry).init(allocator);1442 var directories: std.ArrayListUnmanaged(FileEntry) = .{};
1363 defer include_directories.deinit();1443 defer directories.deinit(gpa);
1364 var file_entries = std.ArrayList(FileEntry).init(allocator);1444 var file_entries: std.ArrayListUnmanaged(FileEntry) = .{};
1365 defer file_entries.deinit();1445 defer file_entries.deinit(gpa);
13661446
1367 if (version < 5) {1447 if (version < 5) {
1368 try include_directories.append(.{ .path = compile_unit_cwd });1448 try directories.append(gpa, .{ .path = compile_unit_cwd });
13691449
1370 while (true) {1450 while (true) {
1371 const dir = try fbr.readBytesTo(0);1451 const dir = try fbr.readBytesTo(0);
1372 if (dir.len == 0) break;1452 if (dir.len == 0) break;
1373 try include_directories.append(.{ .path = dir });1453 try directories.append(gpa, .{ .path = dir });
1374 }1454 }
13751455
1376 while (true) {1456 while (true) {
...@@ -1379,7 +1459,7 @@ pub fn getLineNumberInfo(...@@ -1379,7 +1459,7 @@ pub fn getLineNumberInfo(
1379 const dir_index = try fbr.readUleb128(u32);1459 const dir_index = try fbr.readUleb128(u32);
1380 const mtime = try fbr.readUleb128(u64);1460 const mtime = try fbr.readUleb128(u64);
1381 const size = try fbr.readUleb128(u64);1461 const size = try fbr.readUleb128(u64);
1382 try file_entries.append(.{1462 try file_entries.append(gpa, .{
1383 .path = file_name,1463 .path = file_name,
1384 .dir_index = dir_index,1464 .dir_index = dir_index,
1385 .mtime = mtime,1465 .mtime = mtime,
...@@ -1403,52 +1483,10 @@ pub fn getLineNumberInfo(...@@ -1403,52 +1483,10 @@ pub fn getLineNumberInfo(
1403 }1483 }
14041484
1405 const directories_count = try fbr.readUleb128(usize);1485 const directories_count = try fbr.readUleb128(usize);
1406 try include_directories.ensureUnusedCapacity(directories_count);
1407 {
1408 var i: usize = 0;
1409 while (i < directories_count) : (i += 1) {
1410 var e: FileEntry = .{ .path = &.{} };
1411 for (dir_ent_fmt_buf[0..directory_entry_format_count]) |ent_fmt| {
1412 const form_value = try parseFormValue(
1413 &fbr,
1414 ent_fmt.form_code,
1415 unit_header.format,
1416 null,
1417 );
1418 switch (ent_fmt.content_type_code) {
1419 DW.LNCT.path => e.path = try form_value.getString(di.*),
1420 DW.LNCT.directory_index => e.dir_index = try form_value.getUInt(u32),
1421 DW.LNCT.timestamp => e.mtime = try form_value.getUInt(u64),
1422 DW.LNCT.size => e.size = try form_value.getUInt(u64),
1423 DW.LNCT.MD5 => e.md5 = switch (form_value) {
1424 .data16 => |data16| data16.*,
1425 else => return bad(),
1426 },
1427 else => continue,
1428 }
1429 }
1430 include_directories.appendAssumeCapacity(e);
1431 }
1432 }
1433 }
1434
1435 var file_ent_fmt_buf: [10]FileEntFmt = undefined;
1436 const file_name_entry_format_count = try fbr.readByte();
1437 if (file_name_entry_format_count > file_ent_fmt_buf.len) return bad();
1438 for (file_ent_fmt_buf[0..file_name_entry_format_count]) |*ent_fmt| {
1439 ent_fmt.* = .{
1440 .content_type_code = try fbr.readUleb128(u8),
1441 .form_code = try fbr.readUleb128(u16),
1442 };
1443 }
14441486
1445 const file_names_count = try fbr.readUleb128(usize);1487 for (try directories.addManyAsSlice(gpa, directories_count)) |*e| {
1446 try file_entries.ensureUnusedCapacity(file_names_count);1488 e.* = .{ .path = &.{} };
1447 {1489 for (dir_ent_fmt_buf[0..directory_entry_format_count]) |ent_fmt| {
1448 var i: usize = 0;
1449 while (i < file_names_count) : (i += 1) {
1450 var e: FileEntry = .{ .path = &.{} };
1451 for (file_ent_fmt_buf[0..file_name_entry_format_count]) |ent_fmt| {
1452 const form_value = try parseFormValue(1490 const form_value = try parseFormValue(
1453 &fbr,1491 &fbr,
1454 ent_fmt.form_code,1492 ent_fmt.form_code,
...@@ -1456,7 +1494,7 @@ pub fn getLineNumberInfo(...@@ -1456,7 +1494,7 @@ pub fn getLineNumberInfo(
1456 null,1494 null,
1457 );1495 );
1458 switch (ent_fmt.content_type_code) {1496 switch (ent_fmt.content_type_code) {
1459 DW.LNCT.path => e.path = try form_value.getString(di.*),1497 DW.LNCT.path => e.path = try form_value.getString(d.*),
1460 DW.LNCT.directory_index => e.dir_index = try form_value.getUInt(u32),1498 DW.LNCT.directory_index => e.dir_index = try form_value.getUInt(u32),
1461 DW.LNCT.timestamp => e.mtime = try form_value.getUInt(u64),1499 DW.LNCT.timestamp => e.mtime = try form_value.getUInt(u64),
1462 DW.LNCT.size => e.size = try form_value.getUInt(u64),1500 DW.LNCT.size => e.size = try form_value.getUInt(u64),
...@@ -1467,17 +1505,49 @@ pub fn getLineNumberInfo(...@@ -1467,17 +1505,49 @@ pub fn getLineNumberInfo(
1467 else => continue,1505 else => continue,
1468 }1506 }
1469 }1507 }
1470 file_entries.appendAssumeCapacity(e);1508 }
1509 }
1510
1511 var file_ent_fmt_buf: [10]FileEntFmt = undefined;
1512 const file_name_entry_format_count = try fbr.readByte();
1513 if (file_name_entry_format_count > file_ent_fmt_buf.len) return bad();
1514 for (file_ent_fmt_buf[0..file_name_entry_format_count]) |*ent_fmt| {
1515 ent_fmt.* = .{
1516 .content_type_code = try fbr.readUleb128(u8),
1517 .form_code = try fbr.readUleb128(u16),
1518 };
1519 }
1520
1521 const file_names_count = try fbr.readUleb128(usize);
1522 try file_entries.ensureUnusedCapacity(gpa, file_names_count);
1523
1524 for (try file_entries.addManyAsSlice(gpa, file_names_count)) |*e| {
1525 e.* = .{ .path = &.{} };
1526 for (file_ent_fmt_buf[0..file_name_entry_format_count]) |ent_fmt| {
1527 const form_value = try parseFormValue(
1528 &fbr,
1529 ent_fmt.form_code,
1530 unit_header.format,
1531 null,
1532 );
1533 switch (ent_fmt.content_type_code) {
1534 DW.LNCT.path => e.path = try form_value.getString(d.*),
1535 DW.LNCT.directory_index => e.dir_index = try form_value.getUInt(u32),
1536 DW.LNCT.timestamp => e.mtime = try form_value.getUInt(u64),
1537 DW.LNCT.size => e.size = try form_value.getUInt(u64),
1538 DW.LNCT.MD5 => e.md5 = switch (form_value) {
1539 .data16 => |data16| data16.*,
1540 else => return bad(),
1541 },
1542 else => continue,
1543 }
1471 }1544 }
1472 }1545 }
1473 }1546 }
14741547
1475 var prog = LineNumberProgram.init(1548 var prog = LineNumberProgram.init(default_is_stmt, version);
1476 default_is_stmt,1549 var line_table: CompileUnit.SrcLocCache.LineTable = .{};
1477 include_directories.items,1550 errdefer line_table.deinit(gpa);
1478 target_address,
1479 version,
1480 );
14811551
1482 try fbr.seekTo(prog_start_offset);1552 try fbr.seekTo(prog_start_offset);
14831553
...@@ -1493,7 +1563,7 @@ pub fn getLineNumberInfo(...@@ -1493,7 +1563,7 @@ pub fn getLineNumberInfo(
1493 switch (sub_op) {1563 switch (sub_op) {
1494 DW.LNE.end_sequence => {1564 DW.LNE.end_sequence => {
1495 prog.end_sequence = true;1565 prog.end_sequence = true;
1496 if (try prog.checkLineMatch(allocator, file_entries.items)) |info| return info;1566 try prog.addRow(gpa, &line_table);
1497 prog.reset();1567 prog.reset();
1498 },1568 },
1499 DW.LNE.set_address => {1569 DW.LNE.set_address => {
...@@ -1505,7 +1575,7 @@ pub fn getLineNumberInfo(...@@ -1505,7 +1575,7 @@ pub fn getLineNumberInfo(
1505 const dir_index = try fbr.readUleb128(u32);1575 const dir_index = try fbr.readUleb128(u32);
1506 const mtime = try fbr.readUleb128(u64);1576 const mtime = try fbr.readUleb128(u64);
1507 const size = try fbr.readUleb128(u64);1577 const size = try fbr.readUleb128(u64);
1508 try file_entries.append(.{1578 try file_entries.append(gpa, .{
1509 .path = path,1579 .path = path,
1510 .dir_index = dir_index,1580 .dir_index = dir_index,
1511 .mtime = mtime,1581 .mtime = mtime,
...@@ -1521,12 +1591,12 @@ pub fn getLineNumberInfo(...@@ -1521,12 +1591,12 @@ pub fn getLineNumberInfo(
1521 const inc_line = @as(i32, line_base) + @as(i32, adjusted_opcode % line_range);1591 const inc_line = @as(i32, line_base) + @as(i32, adjusted_opcode % line_range);
1522 prog.line += inc_line;1592 prog.line += inc_line;
1523 prog.address += inc_addr;1593 prog.address += inc_addr;
1524 if (try prog.checkLineMatch(allocator, file_entries.items)) |info| return info;1594 try prog.addRow(gpa, &line_table);
1525 prog.basic_block = false;1595 prog.basic_block = false;
1526 } else {1596 } else {
1527 switch (opcode) {1597 switch (opcode) {
1528 DW.LNS.copy => {1598 DW.LNS.copy => {
1529 if (try prog.checkLineMatch(allocator, file_entries.items)) |info| return info;1599 try prog.addRow(gpa, &line_table);
1530 prog.basic_block = false;1600 prog.basic_block = false;
1531 },1601 },
1532 DW.LNS.advance_pc => {1602 DW.LNS.advance_pc => {
...@@ -1568,7 +1638,39 @@ pub fn getLineNumberInfo(...@@ -1568,7 +1638,39 @@ pub fn getLineNumberInfo(
1568 }1638 }
1569 }1639 }
15701640
1571 return missing();1641 return .{
1642 .line_table = line_table,
1643 .directories = try directories.toOwnedSlice(gpa),
1644 .files = try file_entries.toOwnedSlice(gpa),
1645 .version = version,
1646 };
1647}
1648
1649pub fn populateSrcLocCache(d: *Dwarf, gpa: Allocator, cu: *CompileUnit) ScanError!void {
1650 if (cu.src_loc_cache != null) return;
1651 cu.src_loc_cache = try runLineNumberProgram(d, gpa, cu);
1652}
1653
1654pub fn getLineNumberInfo(
1655 d: *Dwarf,
1656 gpa: Allocator,
1657 compile_unit: *CompileUnit,
1658 target_address: u64,
1659) !std.debug.SourceLocation {
1660 try populateSrcLocCache(d, gpa, compile_unit);
1661 const slc = &compile_unit.src_loc_cache.?;
1662 const entry = try slc.findSource(target_address);
1663 const file_index = entry.file - @intFromBool(slc.version < 5);
1664 if (file_index >= slc.files.len) return bad();
1665 const file_entry = &slc.files[file_index];
1666 if (file_entry.dir_index >= slc.directories.len) return bad();
1667 const dir_name = slc.directories[file_entry.dir_index].path;
1668 const file_name = try std.fs.path.join(gpa, &.{ dir_name, file_entry.path });
1669 return .{
1670 .line = entry.line,
1671 .column = entry.column,
1672 .file_name = file_name,
1673 };
1572}1674}
15731675
1574fn getString(di: Dwarf, offset: u64) ![:0]const u8 {1676fn getString(di: Dwarf, offset: u64) ![:0]const u8 {
...@@ -1588,7 +1690,7 @@ fn readDebugAddr(di: Dwarf, compile_unit: CompileUnit, index: u64) !u64 {...@@ -1588,7 +1690,7 @@ fn readDebugAddr(di: Dwarf, compile_unit: CompileUnit, index: u64) !u64 {
1588 // The header is 8 or 12 bytes depending on is_64.1690 // The header is 8 or 12 bytes depending on is_64.
1589 if (compile_unit.addr_base < 8) return bad();1691 if (compile_unit.addr_base < 8) return bad();
15901692
1591 const version = readInt(u16, debug_addr[compile_unit.addr_base - 4 ..][0..2], di.endian);1693 const version = mem.readInt(u16, debug_addr[compile_unit.addr_base - 4 ..][0..2], di.endian);
1592 if (version != 5) return bad();1694 if (version != 5) return bad();
15931695
1594 const addr_size = debug_addr[compile_unit.addr_base - 2];1696 const addr_size = debug_addr[compile_unit.addr_base - 2];
...@@ -1598,9 +1700,9 @@ fn readDebugAddr(di: Dwarf, compile_unit: CompileUnit, index: u64) !u64 {...@@ -1598,9 +1700,9 @@ fn readDebugAddr(di: Dwarf, compile_unit: CompileUnit, index: u64) !u64 {
1598 if (byte_offset + addr_size > debug_addr.len) return bad();1700 if (byte_offset + addr_size > debug_addr.len) return bad();
1599 return switch (addr_size) {1701 return switch (addr_size) {
1600 1 => debug_addr[byte_offset],1702 1 => debug_addr[byte_offset],
1601 2 => readInt(u16, debug_addr[byte_offset..][0..2], di.endian),1703 2 => mem.readInt(u16, debug_addr[byte_offset..][0..2], di.endian),
1602 4 => readInt(u32, debug_addr[byte_offset..][0..4], di.endian),1704 4 => mem.readInt(u32, debug_addr[byte_offset..][0..4], di.endian),
1603 8 => readInt(u64, debug_addr[byte_offset..][0..8], di.endian),1705 8 => mem.readInt(u64, debug_addr[byte_offset..][0..8], di.endian),
1604 else => bad(),1706 else => bad(),
1605 };1707 };
1606}1708}
...@@ -1611,7 +1713,7 @@ fn readDebugAddr(di: Dwarf, compile_unit: CompileUnit, index: u64) !u64 {...@@ -1611,7 +1713,7 @@ fn readDebugAddr(di: Dwarf, compile_unit: CompileUnit, index: u64) !u64 {
1611/// of FDEs is built for binary searching during unwinding.1713/// of FDEs is built for binary searching during unwinding.
1612pub fn scanAllUnwindInfo(di: *Dwarf, allocator: Allocator, base_address: usize) !void {1714pub fn scanAllUnwindInfo(di: *Dwarf, allocator: Allocator, base_address: usize) !void {
1613 if (di.section(.eh_frame_hdr)) |eh_frame_hdr| blk: {1715 if (di.section(.eh_frame_hdr)) |eh_frame_hdr| blk: {
1614 var fbr: DeprecatedFixedBufferReader = .{ .buf = eh_frame_hdr, .endian = native_endian };1716 var fbr: FixedBufferReader = .{ .buf = eh_frame_hdr, .endian = native_endian };
16151717
1616 const version = try fbr.readByte();1718 const version = try fbr.readByte();
1617 if (version != 1) break :blk;1719 if (version != 1) break :blk;
...@@ -1651,7 +1753,7 @@ pub fn scanAllUnwindInfo(di: *Dwarf, allocator: Allocator, base_address: usize)...@@ -1651,7 +1753,7 @@ pub fn scanAllUnwindInfo(di: *Dwarf, allocator: Allocator, base_address: usize)
1651 const frame_sections = [2]Section.Id{ .eh_frame, .debug_frame };1753 const frame_sections = [2]Section.Id{ .eh_frame, .debug_frame };
1652 for (frame_sections) |frame_section| {1754 for (frame_sections) |frame_section| {
1653 if (di.section(frame_section)) |section_data| {1755 if (di.section(frame_section)) |section_data| {
1654 var fbr: DeprecatedFixedBufferReader = .{ .buf = section_data, .endian = di.endian };1756 var fbr: FixedBufferReader = .{ .buf = section_data, .endian = di.endian };
1655 while (fbr.pos < fbr.buf.len) {1757 while (fbr.pos < fbr.buf.len) {
1656 const entry_header = try EntryHeader.read(&fbr, null, frame_section);1758 const entry_header = try EntryHeader.read(&fbr, null, frame_section);
1657 switch (entry_header.type) {1759 switch (entry_header.type) {
...@@ -1695,11 +1797,11 @@ pub fn scanAllUnwindInfo(di: *Dwarf, allocator: Allocator, base_address: usize)...@@ -1695,11 +1797,11 @@ pub fn scanAllUnwindInfo(di: *Dwarf, allocator: Allocator, base_address: usize)
1695}1797}
16961798
1697fn parseFormValue(1799fn parseFormValue(
1698 fbr: *DeprecatedFixedBufferReader,1800 fbr: *FixedBufferReader,
1699 form_id: u64,1801 form_id: u64,
1700 format: Format,1802 format: Format,
1701 implicit_const: ?i64,1803 implicit_const: ?i64,
1702) anyerror!FormValue {1804) ScanError!FormValue {
1703 return switch (form_id) {1805 return switch (form_id) {
1704 FORM.addr => .{ .addr = try fbr.readAddress(switch (@bitSizeOf(usize)) {1806 FORM.addr => .{ .addr = try fbr.readAddress(switch (@bitSizeOf(usize)) {
1705 32 => .@"32",1807 32 => .@"32",
...@@ -1783,17 +1885,6 @@ const LineNumberProgram = struct {...@@ -1783,17 +1885,6 @@ const LineNumberProgram = struct {
1783 end_sequence: bool,1885 end_sequence: bool,
17841886
1785 default_is_stmt: bool,1887 default_is_stmt: bool,
1786 target_address: u64,
1787 include_dirs: []const FileEntry,
1788
1789 prev_valid: bool,
1790 prev_address: u64,
1791 prev_file: usize,
1792 prev_line: i64,
1793 prev_column: u64,
1794 prev_is_stmt: bool,
1795 prev_basic_block: bool,
1796 prev_end_sequence: bool,
17971888
1798 // Reset the state machine following the DWARF specification1889 // Reset the state machine following the DWARF specification
1799 pub fn reset(self: *LineNumberProgram) void {1890 pub fn reset(self: *LineNumberProgram) void {
...@@ -1804,24 +1895,10 @@ const LineNumberProgram = struct {...@@ -1804,24 +1895,10 @@ const LineNumberProgram = struct {
1804 self.is_stmt = self.default_is_stmt;1895 self.is_stmt = self.default_is_stmt;
1805 self.basic_block = false;1896 self.basic_block = false;
1806 self.end_sequence = false;1897 self.end_sequence = false;
1807 // Invalidate all the remaining fields
1808 self.prev_valid = false;
1809 self.prev_address = 0;
1810 self.prev_file = undefined;
1811 self.prev_line = undefined;
1812 self.prev_column = undefined;
1813 self.prev_is_stmt = undefined;
1814 self.prev_basic_block = undefined;
1815 self.prev_end_sequence = undefined;
1816 }1898 }
18171899
1818 pub fn init(1900 pub fn init(is_stmt: bool, version: u16) LineNumberProgram {
1819 is_stmt: bool,1901 return .{
1820 include_dirs: []const FileEntry,
1821 target_address: u64,
1822 version: u16,
1823 ) LineNumberProgram {
1824 return LineNumberProgram{
1825 .address = 0,1902 .address = 0,
1826 .file = 1,1903 .file = 1,
1827 .line = 1,1904 .line = 1,
...@@ -1830,60 +1907,17 @@ const LineNumberProgram = struct {...@@ -1830,60 +1907,17 @@ const LineNumberProgram = struct {
1830 .is_stmt = is_stmt,1907 .is_stmt = is_stmt,
1831 .basic_block = false,1908 .basic_block = false,
1832 .end_sequence = false,1909 .end_sequence = false,
1833 .include_dirs = include_dirs,
1834 .default_is_stmt = is_stmt,1910 .default_is_stmt = is_stmt,
1835 .target_address = target_address,
1836 .prev_valid = false,
1837 .prev_address = 0,
1838 .prev_file = undefined,
1839 .prev_line = undefined,
1840 .prev_column = undefined,
1841 .prev_is_stmt = undefined,
1842 .prev_basic_block = undefined,
1843 .prev_end_sequence = undefined,
1844 };1911 };
1845 }1912 }
18461913
1847 pub fn checkLineMatch(1914 pub fn addRow(prog: *LineNumberProgram, gpa: Allocator, table: *CompileUnit.SrcLocCache.LineTable) !void {
1848 self: *LineNumberProgram,1915 if (prog.line == 0) return; // garbage data
1849 allocator: Allocator,1916 try table.put(gpa, prog.address, .{
1850 file_entries: []const FileEntry,1917 .line = cast(u32, prog.line) orelse maxInt(u32),
1851 ) !?std.debug.SourceLocation {1918 .column = cast(u32, prog.column) orelse maxInt(u32),
1852 if (self.prev_valid and1919 .file = cast(u32, prog.file) orelse return bad(),
1853 self.target_address >= self.prev_address and1920 });
1854 self.target_address < self.address)
1855 {
1856 const file_index = if (self.version >= 5) self.prev_file else i: {
1857 if (self.prev_file == 0) return missing();
1858 break :i self.prev_file - 1;
1859 };
1860
1861 if (file_index >= file_entries.len) return bad();
1862 const file_entry = &file_entries[file_index];
1863
1864 if (file_entry.dir_index >= self.include_dirs.len) return bad();
1865 const dir_name = self.include_dirs[file_entry.dir_index].path;
1866
1867 const file_name = try std.fs.path.join(allocator, &[_][]const u8{
1868 dir_name, file_entry.path,
1869 });
1870
1871 return std.debug.SourceLocation{
1872 .line = if (self.prev_line >= 0) @as(u64, @intCast(self.prev_line)) else 0,
1873 .column = self.prev_column,
1874 .file_name = file_name,
1875 };
1876 }
1877
1878 self.prev_valid = true;
1879 self.prev_address = self.address;
1880 self.prev_file = self.file;
1881 self.prev_line = self.line;
1882 self.prev_column = self.column;
1883 self.prev_is_stmt = self.is_stmt;
1884 self.prev_basic_block = self.basic_block;
1885 self.prev_end_sequence = self.end_sequence;
1886 return null;
1887 }1921 }
1888};1922};
18891923
...@@ -1892,7 +1926,8 @@ const UnitHeader = struct {...@@ -1892,7 +1926,8 @@ const UnitHeader = struct {
1892 header_length: u4,1926 header_length: u4,
1893 unit_length: u64,1927 unit_length: u64,
1894};1928};
1895fn readUnitHeader(fbr: *DeprecatedFixedBufferReader, opt_ma: ?*MemoryAccessor) !UnitHeader {1929
1930fn readUnitHeader(fbr: *FixedBufferReader, opt_ma: ?*MemoryAccessor) ScanError!UnitHeader {
1896 return switch (try if (opt_ma) |ma| fbr.readIntChecked(u32, ma) else fbr.readInt(u32)) {1931 return switch (try if (opt_ma) |ma| fbr.readIntChecked(u32, ma) else fbr.readInt(u32)) {
1897 0...0xfffffff0 - 1 => |unit_length| .{1932 0...0xfffffff0 - 1 => |unit_length| .{
1898 .format = .@"32",1933 .format = .@"32",
...@@ -1957,7 +1992,7 @@ const EhPointerContext = struct {...@@ -1957,7 +1992,7 @@ const EhPointerContext = struct {
1957 text_rel_base: ?u64 = null,1992 text_rel_base: ?u64 = null,
1958 function_rel_base: ?u64 = null,1993 function_rel_base: ?u64 = null,
1959};1994};
1960fn readEhPointer(fbr: *DeprecatedFixedBufferReader, enc: u8, addr_size_bytes: u8, ctx: EhPointerContext) !?u64 {1995fn readEhPointer(fbr: *FixedBufferReader, enc: u8, addr_size_bytes: u8, ctx: EhPointerContext) !?u64 {
1961 if (enc == EH.PE.omit) return null;1996 if (enc == EH.PE.omit) return null;
19621997
1963 const value: union(enum) {1998 const value: union(enum) {
...@@ -2023,3 +2058,320 @@ fn pcRelBase(field_ptr: usize, pc_rel_offset: i64) !usize {...@@ -2023,3 +2058,320 @@ fn pcRelBase(field_ptr: usize, pc_rel_offset: i64) !usize {
2023 return std.math.add(usize, field_ptr, @as(usize, @intCast(pc_rel_offset)));2058 return std.math.add(usize, field_ptr, @as(usize, @intCast(pc_rel_offset)));
2024 }2059 }
2025}2060}
2061
2062pub const ElfModule = struct {
2063 base_address: usize,
2064 dwarf: Dwarf,
2065 mapped_memory: []align(std.mem.page_size) const u8,
2066 external_mapped_memory: ?[]align(std.mem.page_size) const u8,
2067
2068 pub fn deinit(self: *@This(), allocator: Allocator) void {
2069 self.dwarf.deinit(allocator);
2070 std.posix.munmap(self.mapped_memory);
2071 if (self.external_mapped_memory) |m| std.posix.munmap(m);
2072 }
2073
2074 pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !std.debug.Symbol {
2075 // Translate the VA into an address into this object
2076 const relocated_address = address - self.base_address;
2077 return self.dwarf.getSymbol(allocator, relocated_address);
2078 }
2079
2080 pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {
2081 _ = allocator;
2082 _ = address;
2083 return &self.dwarf;
2084 }
2085
2086 pub const LoadError = error{
2087 InvalidDebugInfo,
2088 MissingDebugInfo,
2089 InvalidElfMagic,
2090 InvalidElfVersion,
2091 InvalidElfEndian,
2092 /// TODO: implement this and then remove this error code
2093 UnimplementedDwarfForeignEndian,
2094 /// The debug info may be valid but this implementation uses memory
2095 /// mapping which limits things to usize. If the target debug info is
2096 /// 64-bit and host is 32-bit, there may be debug info that is not
2097 /// supportable using this method.
2098 Overflow,
2099
2100 PermissionDenied,
2101 LockedMemoryLimitExceeded,
2102 MemoryMappingNotSupported,
2103 } || Allocator.Error || std.fs.File.OpenError || OpenError;
2104
2105 /// Reads debug info from an already mapped ELF file.
2106 ///
2107 /// If the required sections aren't present but a reference to external debug
2108 /// info is, then this this function will recurse to attempt to load the debug
2109 /// sections from an external file.
2110 pub fn load(
2111 gpa: Allocator,
2112 mapped_mem: []align(std.mem.page_size) const u8,
2113 build_id: ?[]const u8,
2114 expected_crc: ?u32,
2115 parent_sections: *Dwarf.SectionArray,
2116 parent_mapped_mem: ?[]align(std.mem.page_size) const u8,
2117 elf_filename: ?[]const u8,
2118 ) LoadError!Dwarf.ElfModule {
2119 if (expected_crc) |crc| if (crc != std.hash.crc.Crc32.hash(mapped_mem)) return error.InvalidDebugInfo;
2120
2121 const hdr: *const elf.Ehdr = @ptrCast(&mapped_mem[0]);
2122 if (!mem.eql(u8, hdr.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic;
2123 if (hdr.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion;
2124
2125 const endian: std.builtin.Endian = switch (hdr.e_ident[elf.EI_DATA]) {
2126 elf.ELFDATA2LSB => .little,
2127 elf.ELFDATA2MSB => .big,
2128 else => return error.InvalidElfEndian,
2129 };
2130 if (endian != native_endian) return error.UnimplementedDwarfForeignEndian;
2131
2132 const shoff = hdr.e_shoff;
2133 const str_section_off = shoff + @as(u64, hdr.e_shentsize) * @as(u64, hdr.e_shstrndx);
2134 const str_shdr: *const elf.Shdr = @ptrCast(@alignCast(&mapped_mem[cast(usize, str_section_off) orelse return error.Overflow]));
2135 const header_strings = mapped_mem[str_shdr.sh_offset..][0..str_shdr.sh_size];
2136 const shdrs = @as(
2137 [*]const elf.Shdr,
2138 @ptrCast(@alignCast(&mapped_mem[shoff])),
2139 )[0..hdr.e_shnum];
2140
2141 var sections: Dwarf.SectionArray = Dwarf.null_section_array;
2142
2143 // Combine section list. This takes ownership over any owned sections from the parent scope.
2144 for (parent_sections, &sections) |*parent, *section_elem| {
2145 if (parent.*) |*p| {
2146 section_elem.* = p.*;
2147 p.owned = false;
2148 }
2149 }
2150 errdefer for (sections) |opt_section| if (opt_section) |s| if (s.owned) gpa.free(s.data);
2151
2152 var separate_debug_filename: ?[]const u8 = null;
2153 var separate_debug_crc: ?u32 = null;
2154
2155 for (shdrs) |*shdr| {
2156 if (shdr.sh_type == elf.SHT_NULL or shdr.sh_type == elf.SHT_NOBITS) continue;
2157 const name = mem.sliceTo(header_strings[shdr.sh_name..], 0);
2158
2159 if (mem.eql(u8, name, ".gnu_debuglink")) {
2160 const gnu_debuglink = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
2161 const debug_filename = mem.sliceTo(@as([*:0]const u8, @ptrCast(gnu_debuglink.ptr)), 0);
2162 const crc_offset = mem.alignForward(usize, @intFromPtr(&debug_filename[debug_filename.len]) + 1, 4) - @intFromPtr(gnu_debuglink.ptr);
2163 const crc_bytes = gnu_debuglink[crc_offset..][0..4];
2164 separate_debug_crc = mem.readInt(u32, crc_bytes, native_endian);
2165 separate_debug_filename = debug_filename;
2166 continue;
2167 }
2168
2169 var section_index: ?usize = null;
2170 inline for (@typeInfo(Dwarf.Section.Id).Enum.fields, 0..) |sect, i| {
2171 if (mem.eql(u8, "." ++ sect.name, name)) section_index = i;
2172 }
2173 if (section_index == null) continue;
2174 if (sections[section_index.?] != null) continue;
2175
2176 const section_bytes = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
2177 sections[section_index.?] = if ((shdr.sh_flags & elf.SHF_COMPRESSED) > 0) blk: {
2178 var section_stream = std.io.fixedBufferStream(section_bytes);
2179 const section_reader = section_stream.reader();
2180 const chdr = section_reader.readStruct(elf.Chdr) catch continue;
2181 if (chdr.ch_type != .ZLIB) continue;
2182
2183 var zlib_stream = std.compress.zlib.decompressor(section_reader);
2184
2185 const decompressed_section = try gpa.alloc(u8, chdr.ch_size);
2186 errdefer gpa.free(decompressed_section);
2187
2188 const read = zlib_stream.reader().readAll(decompressed_section) catch continue;
2189 assert(read == decompressed_section.len);
2190
2191 break :blk .{
2192 .data = decompressed_section,
2193 .virtual_address = shdr.sh_addr,
2194 .owned = true,
2195 };
2196 } else .{
2197 .data = section_bytes,
2198 .virtual_address = shdr.sh_addr,
2199 .owned = false,
2200 };
2201 }
2202
2203 const missing_debug_info =
2204 sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or
2205 sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or
2206 sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or
2207 sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
2208
2209 // Attempt to load debug info from an external file
2210 // See: https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html
2211 if (missing_debug_info) {
2212
2213 // Only allow one level of debug info nesting
2214 if (parent_mapped_mem) |_| {
2215 return error.MissingDebugInfo;
2216 }
2217
2218 const global_debug_directories = [_][]const u8{
2219 "/usr/lib/debug",
2220 };
2221
2222 // <global debug directory>/.build-id/<2-character id prefix>/<id remainder>.debug
2223 if (build_id) |id| blk: {
2224 if (id.len < 3) break :blk;
2225
2226 // Either md5 (16 bytes) or sha1 (20 bytes) are used here in practice
2227 const extension = ".debug";
2228 var id_prefix_buf: [2]u8 = undefined;
2229 var filename_buf: [38 + extension.len]u8 = undefined;
2230
2231 _ = std.fmt.bufPrint(&id_prefix_buf, "{s}", .{std.fmt.fmtSliceHexLower(id[0..1])}) catch unreachable;
2232 const filename = std.fmt.bufPrint(
2233 &filename_buf,
2234 "{s}" ++ extension,
2235 .{std.fmt.fmtSliceHexLower(id[1..])},
2236 ) catch break :blk;
2237
2238 for (global_debug_directories) |global_directory| {
2239 const path: Path = .{
2240 .root_dir = std.Build.Cache.Directory.cwd(),
2241 .sub_path = try std.fs.path.join(gpa, &.{
2242 global_directory, ".build-id", &id_prefix_buf, filename,
2243 }),
2244 };
2245 defer gpa.free(path.sub_path);
2246
2247 return loadPath(gpa, path, null, separate_debug_crc, &sections, mapped_mem) catch continue;
2248 }
2249 }
2250
2251 // use the path from .gnu_debuglink, in the same search order as gdb
2252 if (separate_debug_filename) |separate_filename| blk: {
2253 if (elf_filename != null and mem.eql(u8, elf_filename.?, separate_filename))
2254 return error.MissingDebugInfo;
2255
2256 // <cwd>/<gnu_debuglink>
2257 if (loadPath(
2258 gpa,
2259 .{
2260 .root_dir = std.Build.Cache.Directory.cwd(),
2261 .sub_path = separate_filename,
2262 },
2263 null,
2264 separate_debug_crc,
2265 &sections,
2266 mapped_mem,
2267 )) |debug_info| {
2268 return debug_info;
2269 } else |_| {}
2270
2271 // <cwd>/.debug/<gnu_debuglink>
2272 {
2273 const path: Path = .{
2274 .root_dir = std.Build.Cache.Directory.cwd(),
2275 .sub_path = try std.fs.path.join(gpa, &.{ ".debug", separate_filename }),
2276 };
2277 defer gpa.free(path.sub_path);
2278
2279 if (loadPath(gpa, path, null, separate_debug_crc, &sections, mapped_mem)) |debug_info| return debug_info else |_| {}
2280 }
2281
2282 var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
2283 const cwd_path = std.posix.realpath(".", &cwd_buf) catch break :blk;
2284
2285 // <global debug directory>/<absolute folder of current binary>/<gnu_debuglink>
2286 for (global_debug_directories) |global_directory| {
2287 const path: Path = .{
2288 .root_dir = std.Build.Cache.Directory.cwd(),
2289 .sub_path = try std.fs.path.join(gpa, &.{ global_directory, cwd_path, separate_filename }),
2290 };
2291 defer gpa.free(path.sub_path);
2292 if (loadPath(gpa, path, null, separate_debug_crc, &sections, mapped_mem)) |debug_info| return debug_info else |_| {}
2293 }
2294 }
2295
2296 return error.MissingDebugInfo;
2297 }
2298
2299 var di: Dwarf = .{
2300 .endian = endian,
2301 .sections = sections,
2302 .is_macho = false,
2303 .compile_units_sorted = false,
2304 };
2305
2306 try Dwarf.open(&di, gpa);
2307
2308 return .{
2309 .base_address = 0,
2310 .dwarf = di,
2311 .mapped_memory = parent_mapped_mem orelse mapped_mem,
2312 .external_mapped_memory = if (parent_mapped_mem != null) mapped_mem else null,
2313 };
2314 }
2315
2316 pub fn loadPath(
2317 gpa: Allocator,
2318 elf_file_path: Path,
2319 build_id: ?[]const u8,
2320 expected_crc: ?u32,
2321 parent_sections: *Dwarf.SectionArray,
2322 parent_mapped_mem: ?[]align(std.mem.page_size) const u8,
2323 ) LoadError!Dwarf.ElfModule {
2324 const elf_file = elf_file_path.root_dir.handle.openFile(elf_file_path.sub_path, .{}) catch |err| switch (err) {
2325 error.FileNotFound => return missing(),
2326 else => return err,
2327 };
2328 defer elf_file.close();
2329
2330 const end_pos = elf_file.getEndPos() catch return bad();
2331 const file_len = cast(usize, end_pos) orelse return error.Overflow;
2332
2333 const mapped_mem = try std.posix.mmap(
2334 null,
2335 file_len,
2336 std.posix.PROT.READ,
2337 .{ .TYPE = .SHARED },
2338 elf_file.handle,
2339 0,
2340 );
2341 errdefer std.posix.munmap(mapped_mem);
2342
2343 return load(
2344 gpa,
2345 mapped_mem,
2346 build_id,
2347 expected_crc,
2348 parent_sections,
2349 parent_mapped_mem,
2350 elf_file_path.sub_path,
2351 );
2352 }
2353};
2354
2355pub fn getSymbol(di: *Dwarf, allocator: Allocator, address: u64) !std.debug.Symbol {
2356 if (di.findCompileUnit(address)) |compile_unit| {
2357 return .{
2358 .name = di.getSymbolName(address) orelse "???",
2359 .compile_unit_name = compile_unit.die.getAttrString(di, std.dwarf.AT.name, di.section(.debug_str), compile_unit.*) catch |err| switch (err) {
2360 error.MissingDebugInfo, error.InvalidDebugInfo => "???",
2361 },
2362 .source_location = di.getLineNumberInfo(allocator, compile_unit, address) catch |err| switch (err) {
2363 error.MissingDebugInfo, error.InvalidDebugInfo => null,
2364 else => return err,
2365 },
2366 };
2367 } else |err| switch (err) {
2368 error.MissingDebugInfo, error.InvalidDebugInfo => return .{},
2369 else => return err,
2370 }
2371}
2372
2373pub fn chopSlice(ptr: []const u8, offset: u64, size: u64) error{Overflow}![]const u8 {
2374 const start = cast(usize, offset) orelse return error.Overflow;
2375 const end = start + (cast(usize, size) orelse return error.Overflow);
2376 return ptr[start..end];
2377}
lib/std/debug/FixedBufferReader.zig created+93
...@@ -0,0 +1,93 @@
1//! Optimized for performance in debug builds.
2
3const std = @import("../std.zig");
4const MemoryAccessor = std.debug.MemoryAccessor;
5
6const FixedBufferReader = @This();
7
8buf: []const u8,
9pos: usize = 0,
10endian: std.builtin.Endian,
11
12pub const Error = error{ EndOfBuffer, Overflow, InvalidBuffer };
13
14pub fn seekTo(fbr: *FixedBufferReader, pos: u64) Error!void {
15 if (pos > fbr.buf.len) return error.EndOfBuffer;
16 fbr.pos = @intCast(pos);
17}
18
19pub fn seekForward(fbr: *FixedBufferReader, amount: u64) Error!void {
20 if (fbr.buf.len - fbr.pos < amount) return error.EndOfBuffer;
21 fbr.pos += @intCast(amount);
22}
23
24pub inline fn readByte(fbr: *FixedBufferReader) Error!u8 {
25 if (fbr.pos >= fbr.buf.len) return error.EndOfBuffer;
26 defer fbr.pos += 1;
27 return fbr.buf[fbr.pos];
28}
29
30pub fn readByteSigned(fbr: *FixedBufferReader) Error!i8 {
31 return @bitCast(try fbr.readByte());
32}
33
34pub fn readInt(fbr: *FixedBufferReader, comptime T: type) Error!T {
35 const size = @divExact(@typeInfo(T).Int.bits, 8);
36 if (fbr.buf.len - fbr.pos < size) return error.EndOfBuffer;
37 defer fbr.pos += size;
38 return std.mem.readInt(T, fbr.buf[fbr.pos..][0..size], fbr.endian);
39}
40
41pub fn readIntChecked(
42 fbr: *FixedBufferReader,
43 comptime T: type,
44 ma: *MemoryAccessor,
45) Error!T {
46 if (ma.load(T, @intFromPtr(fbr.buf[fbr.pos..].ptr)) == null)
47 return error.InvalidBuffer;
48
49 return fbr.readInt(T);
50}
51
52pub fn readUleb128(fbr: *FixedBufferReader, comptime T: type) Error!T {
53 return std.leb.readUleb128(T, fbr);
54}
55
56pub fn readIleb128(fbr: *FixedBufferReader, comptime T: type) Error!T {
57 return std.leb.readIleb128(T, fbr);
58}
59
60pub fn readAddress(fbr: *FixedBufferReader, format: std.dwarf.Format) Error!u64 {
61 return switch (format) {
62 .@"32" => try fbr.readInt(u32),
63 .@"64" => try fbr.readInt(u64),
64 };
65}
66
67pub fn readAddressChecked(
68 fbr: *FixedBufferReader,
69 format: std.dwarf.Format,
70 ma: *MemoryAccessor,
71) Error!u64 {
72 return switch (format) {
73 .@"32" => try fbr.readIntChecked(u32, ma),
74 .@"64" => try fbr.readIntChecked(u64, ma),
75 };
76}
77
78pub fn readBytes(fbr: *FixedBufferReader, len: usize) Error![]const u8 {
79 if (fbr.buf.len - fbr.pos < len) return error.EndOfBuffer;
80 defer fbr.pos += len;
81 return fbr.buf[fbr.pos..][0..len];
82}
83
84pub fn readBytesTo(fbr: *FixedBufferReader, comptime sentinel: u8) Error![:sentinel]const u8 {
85 const end = @call(.always_inline, std.mem.indexOfScalarPos, .{
86 u8,
87 fbr.buf,
88 fbr.pos,
89 sentinel,
90 }) orelse return error.EndOfBuffer;
91 defer fbr.pos = end + 1;
92 return fbr.buf[fbr.pos..end :sentinel];
93}
lib/std/debug/Info.zig created+62
...@@ -0,0 +1,62 @@
1//! Cross-platform abstraction for loading debug information into an in-memory
2//! format that supports queries such as "what is the source location of this
3//! virtual memory address?"
4//!
5//! Unlike `std.debug.SelfInfo`, this API does not assume the debug information
6//! in question happens to match the host CPU architecture, OS, or other target
7//! properties.
8
9const std = @import("../std.zig");
10const Allocator = std.mem.Allocator;
11const Path = std.Build.Cache.Path;
12const Dwarf = std.debug.Dwarf;
13const page_size = std.mem.page_size;
14const assert = std.debug.assert;
15const Coverage = std.debug.Coverage;
16const SourceLocation = std.debug.Coverage.SourceLocation;
17
18const Info = @This();
19
20/// Sorted by key, ascending.
21address_map: std.AutoArrayHashMapUnmanaged(u64, Dwarf.ElfModule),
22/// Externally managed, outlives this `Info` instance.
23coverage: *Coverage,
24
25pub const LoadError = Dwarf.ElfModule.LoadError;
26
27pub fn load(gpa: Allocator, path: Path, coverage: *Coverage) LoadError!Info {
28 var sections: Dwarf.SectionArray = Dwarf.null_section_array;
29 var elf_module = try Dwarf.ElfModule.loadPath(gpa, path, null, null, &sections, null);
30 try elf_module.dwarf.sortCompileUnits();
31 var info: Info = .{
32 .address_map = .{},
33 .coverage = coverage,
34 };
35 try info.address_map.put(gpa, elf_module.base_address, elf_module);
36 return info;
37}
38
39pub fn deinit(info: *Info, gpa: Allocator) void {
40 for (info.address_map.values()) |*elf_module| {
41 elf_module.dwarf.deinit(gpa);
42 }
43 info.address_map.deinit(gpa);
44 info.* = undefined;
45}
46
47pub const ResolveAddressesError = Coverage.ResolveAddressesDwarfError;
48
49/// Given an array of virtual memory addresses, sorted ascending, outputs a
50/// corresponding array of source locations.
51pub fn resolveAddresses(
52 info: *Info,
53 gpa: Allocator,
54 sorted_pc_addrs: []const u64,
55 /// Asserts its length equals length of `sorted_pc_addrs`.
56 output: []SourceLocation,
57) ResolveAddressesError!void {
58 assert(sorted_pc_addrs.len == output.len);
59 if (info.address_map.entries.len != 1) @panic("TODO");
60 const elf_module = &info.address_map.values()[0];
61 return info.coverage.resolveAddressesDwarf(gpa, sorted_pc_addrs, output, &elf_module.dwarf);
62}
lib/std/debug/SelfInfo.zig+35-252
...@@ -587,7 +587,7 @@ pub const Module = switch (native_os) {...@@ -587,7 +587,7 @@ pub const Module = switch (native_os) {
587 }587 }
588 if (section_index == null) continue;588 if (section_index == null) continue;
589589
590 const section_bytes = try chopSlice(mapped_mem, sect.offset, sect.size);590 const section_bytes = try Dwarf.chopSlice(mapped_mem, sect.offset, sect.size);
591 sections[section_index.?] = .{591 sections[section_index.?] = .{
592 .data = section_bytes,592 .data = section_bytes,
593 .virtual_address = sect.addr,593 .virtual_address = sect.addr,
...@@ -602,10 +602,11 @@ pub const Module = switch (native_os) {...@@ -602,10 +602,11 @@ pub const Module = switch (native_os) {
602 sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;602 sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
603 if (missing_debug_info) return error.MissingDebugInfo;603 if (missing_debug_info) return error.MissingDebugInfo;
604604
605 var di = Dwarf{605 var di: Dwarf = .{
606 .endian = .little,606 .endian = .little,
607 .sections = sections,607 .sections = sections,
608 .is_macho = true,608 .is_macho = true,
609 .compile_units_sorted = false,
609 };610 };
610611
611 try Dwarf.open(&di, allocator);612 try Dwarf.open(&di, allocator);
...@@ -622,7 +623,7 @@ pub const Module = switch (native_os) {...@@ -622,7 +623,7 @@ pub const Module = switch (native_os) {
622 return result.value_ptr;623 return result.value_ptr;
623 }624 }
624625
625 pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !SymbolInfo {626 pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !std.debug.Symbol {
626 nosuspend {627 nosuspend {
627 const result = try self.getOFileInfoForAddress(allocator, address);628 const result = try self.getOFileInfoForAddress(allocator, address);
628 if (result.symbol == null) return .{};629 if (result.symbol == null) return .{};
...@@ -630,19 +631,19 @@ pub const Module = switch (native_os) {...@@ -630,19 +631,19 @@ pub const Module = switch (native_os) {
630 // Take the symbol name from the N_FUN STAB entry, we're going to631 // Take the symbol name from the N_FUN STAB entry, we're going to
631 // use it if we fail to find the DWARF infos632 // use it if we fail to find the DWARF infos
632 const stab_symbol = mem.sliceTo(self.strings[result.symbol.?.strx..], 0);633 const stab_symbol = mem.sliceTo(self.strings[result.symbol.?.strx..], 0);
633 if (result.o_file_info == null) return .{ .symbol_name = stab_symbol };634 if (result.o_file_info == null) return .{ .name = stab_symbol };
634635
635 // Translate again the address, this time into an address inside the636 // Translate again the address, this time into an address inside the
636 // .o file637 // .o file
637 const relocated_address_o = result.o_file_info.?.addr_table.get(stab_symbol) orelse return .{638 const relocated_address_o = result.o_file_info.?.addr_table.get(stab_symbol) orelse return .{
638 .symbol_name = "???",639 .name = "???",
639 };640 };
640641
641 const addr_off = result.relocated_address - result.symbol.?.addr;642 const addr_off = result.relocated_address - result.symbol.?.addr;
642 const o_file_di = &result.o_file_info.?.di;643 const o_file_di = &result.o_file_info.?.di;
643 if (o_file_di.findCompileUnit(relocated_address_o)) |compile_unit| {644 if (o_file_di.findCompileUnit(relocated_address_o)) |compile_unit| {
644 return SymbolInfo{645 return .{
645 .symbol_name = o_file_di.getSymbolName(relocated_address_o) orelse "???",646 .name = o_file_di.getSymbolName(relocated_address_o) orelse "???",
646 .compile_unit_name = compile_unit.die.getAttrString(647 .compile_unit_name = compile_unit.die.getAttrString(
647 o_file_di,648 o_file_di,
648 std.dwarf.AT.name,649 std.dwarf.AT.name,
...@@ -651,9 +652,9 @@ pub const Module = switch (native_os) {...@@ -651,9 +652,9 @@ pub const Module = switch (native_os) {
651 ) catch |err| switch (err) {652 ) catch |err| switch (err) {
652 error.MissingDebugInfo, error.InvalidDebugInfo => "???",653 error.MissingDebugInfo, error.InvalidDebugInfo => "???",
653 },654 },
654 .line_info = o_file_di.getLineNumberInfo(655 .source_location = o_file_di.getLineNumberInfo(
655 allocator,656 allocator,
656 compile_unit.*,657 compile_unit,
657 relocated_address_o + addr_off,658 relocated_address_o + addr_off,
658 ) catch |err| switch (err) {659 ) catch |err| switch (err) {
659 error.MissingDebugInfo, error.InvalidDebugInfo => null,660 error.MissingDebugInfo, error.InvalidDebugInfo => null,
...@@ -662,7 +663,7 @@ pub const Module = switch (native_os) {...@@ -662,7 +663,7 @@ pub const Module = switch (native_os) {
662 };663 };
663 } else |err| switch (err) {664 } else |err| switch (err) {
664 error.MissingDebugInfo, error.InvalidDebugInfo => {665 error.MissingDebugInfo, error.InvalidDebugInfo => {
665 return SymbolInfo{ .symbol_name = stab_symbol };666 return .{ .name = stab_symbol };
666 },667 },
667 else => return err,668 else => return err,
668 }669 }
...@@ -729,7 +730,7 @@ pub const Module = switch (native_os) {...@@ -729,7 +730,7 @@ pub const Module = switch (native_os) {
729 }730 }
730 }731 }
731732
732 fn getSymbolFromPdb(self: *@This(), relocated_address: usize) !?SymbolInfo {733 fn getSymbolFromPdb(self: *@This(), relocated_address: usize) !?std.debug.Symbol {
733 var coff_section: *align(1) const coff.SectionHeader = undefined;734 var coff_section: *align(1) const coff.SectionHeader = undefined;
734 const mod_index = for (self.pdb.?.sect_contribs) |sect_contrib| {735 const mod_index = for (self.pdb.?.sect_contribs) |sect_contrib| {
735 if (sect_contrib.Section > self.coff_section_headers.len) continue;736 if (sect_contrib.Section > self.coff_section_headers.len) continue;
...@@ -759,14 +760,14 @@ pub const Module = switch (native_os) {...@@ -759,14 +760,14 @@ pub const Module = switch (native_os) {
759 relocated_address - coff_section.virtual_address,760 relocated_address - coff_section.virtual_address,
760 );761 );
761762
762 return SymbolInfo{763 return .{
763 .symbol_name = symbol_name,764 .name = symbol_name,
764 .compile_unit_name = obj_basename,765 .compile_unit_name = obj_basename,
765 .line_info = opt_line_info,766 .source_location = opt_line_info,
766 };767 };
767 }768 }
768769
769 pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !SymbolInfo {770 pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !std.debug.Symbol {
770 // Translate the VA into an address into this object771 // Translate the VA into an address into this object
771 const relocated_address = address - self.base_address;772 const relocated_address = address - self.base_address;
772773
...@@ -776,10 +777,10 @@ pub const Module = switch (native_os) {...@@ -776,10 +777,10 @@ pub const Module = switch (native_os) {
776777
777 if (self.dwarf) |*dwarf| {778 if (self.dwarf) |*dwarf| {
778 const dwarf_address = relocated_address + self.coff_image_base;779 const dwarf_address = relocated_address + self.coff_image_base;
779 return getSymbolFromDwarf(allocator, dwarf_address, dwarf);780 return dwarf.getSymbol(allocator, dwarf_address);
780 }781 }
781782
782 return SymbolInfo{};783 return .{};
783 }784 }
784785
785 pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {786 pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {
...@@ -792,41 +793,18 @@ pub const Module = switch (native_os) {...@@ -792,41 +793,18 @@ pub const Module = switch (native_os) {
792 };793 };
793 }794 }
794 },795 },
795 .linux, .netbsd, .freebsd, .dragonfly, .openbsd, .haiku, .solaris, .illumos => struct {796 .linux, .netbsd, .freebsd, .dragonfly, .openbsd, .haiku, .solaris, .illumos => Dwarf.ElfModule,
796 base_address: usize,
797 dwarf: Dwarf,
798 mapped_memory: []align(mem.page_size) const u8,
799 external_mapped_memory: ?[]align(mem.page_size) const u8,
800
801 pub fn deinit(self: *@This(), allocator: Allocator) void {
802 self.dwarf.deinit(allocator);
803 posix.munmap(self.mapped_memory);
804 if (self.external_mapped_memory) |m| posix.munmap(m);
805 }
806
807 pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !SymbolInfo {
808 // Translate the VA into an address into this object
809 const relocated_address = address - self.base_address;
810 return getSymbolFromDwarf(allocator, relocated_address, &self.dwarf);
811 }
812
813 pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {
814 _ = allocator;
815 _ = address;
816 return &self.dwarf;
817 }
818 },
819 .wasi, .emscripten => struct {797 .wasi, .emscripten => struct {
820 pub fn deinit(self: *@This(), allocator: Allocator) void {798 pub fn deinit(self: *@This(), allocator: Allocator) void {
821 _ = self;799 _ = self;
822 _ = allocator;800 _ = allocator;
823 }801 }
824802
825 pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !SymbolInfo {803 pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !std.debug.Symbol {
826 _ = self;804 _ = self;
827 _ = allocator;805 _ = allocator;
828 _ = address;806 _ = address;
829 return SymbolInfo{};807 return .{};
830 }808 }
831809
832 pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {810 pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {
...@@ -1014,10 +992,11 @@ fn readCoffDebugInfo(allocator: Allocator, coff_obj: *coff.Coff) !Module {...@@ -1014,10 +992,11 @@ fn readCoffDebugInfo(allocator: Allocator, coff_obj: *coff.Coff) !Module {
1014 } else null;992 } else null;
1015 }993 }
1016994
1017 var dwarf = Dwarf{995 var dwarf: Dwarf = .{
1018 .endian = native_endian,996 .endian = native_endian,
1019 .sections = sections,997 .sections = sections,
1020 .is_macho = false,998 .is_macho = false,
999 .compile_units_sorted = false,
1021 };1000 };
10221001
1023 try Dwarf.open(&dwarf, allocator);1002 try Dwarf.open(&dwarf, allocator);
...@@ -1068,7 +1047,7 @@ pub fn readElfDebugInfo(...@@ -1068,7 +1047,7 @@ pub fn readElfDebugInfo(
1068 expected_crc: ?u32,1047 expected_crc: ?u32,
1069 parent_sections: *Dwarf.SectionArray,1048 parent_sections: *Dwarf.SectionArray,
1070 parent_mapped_mem: ?[]align(mem.page_size) const u8,1049 parent_mapped_mem: ?[]align(mem.page_size) const u8,
1071) !Module {1050) !Dwarf.ElfModule {
1072 nosuspend {1051 nosuspend {
1073 const elf_file = (if (elf_filename) |filename| blk: {1052 const elf_file = (if (elf_filename) |filename| blk: {
1074 break :blk fs.cwd().openFile(filename, .{});1053 break :blk fs.cwd().openFile(filename, .{});
...@@ -1078,176 +1057,15 @@ pub fn readElfDebugInfo(...@@ -1078,176 +1057,15 @@ pub fn readElfDebugInfo(
1078 };1057 };
10791058
1080 const mapped_mem = try mapWholeFile(elf_file);1059 const mapped_mem = try mapWholeFile(elf_file);
1081 if (expected_crc) |crc| if (crc != std.hash.crc.Crc32.hash(mapped_mem)) return error.InvalidDebugInfo;1060 return Dwarf.ElfModule.load(
10821061 allocator,
1083 const hdr: *const elf.Ehdr = @ptrCast(&mapped_mem[0]);1062 mapped_mem,
1084 if (!mem.eql(u8, hdr.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic;1063 build_id,
1085 if (hdr.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion;1064 expected_crc,
10861065 parent_sections,
1087 const endian: std.builtin.Endian = switch (hdr.e_ident[elf.EI_DATA]) {1066 parent_mapped_mem,
1088 elf.ELFDATA2LSB => .little,1067 elf_filename,
1089 elf.ELFDATA2MSB => .big,1068 );
1090 else => return error.InvalidElfEndian,
1091 };
1092 assert(endian == native_endian); // this is our own debug info
1093
1094 const shoff = hdr.e_shoff;
1095 const str_section_off = shoff + @as(u64, hdr.e_shentsize) * @as(u64, hdr.e_shstrndx);
1096 const str_shdr: *const elf.Shdr = @ptrCast(@alignCast(&mapped_mem[math.cast(usize, str_section_off) orelse return error.Overflow]));
1097 const header_strings = mapped_mem[str_shdr.sh_offset..][0..str_shdr.sh_size];
1098 const shdrs = @as(
1099 [*]const elf.Shdr,
1100 @ptrCast(@alignCast(&mapped_mem[shoff])),
1101 )[0..hdr.e_shnum];
1102
1103 var sections: Dwarf.SectionArray = Dwarf.null_section_array;
1104
1105 // Combine section list. This takes ownership over any owned sections from the parent scope.
1106 for (parent_sections, &sections) |*parent, *section| {
1107 if (parent.*) |*p| {
1108 section.* = p.*;
1109 p.owned = false;
1110 }
1111 }
1112 errdefer for (sections) |section| if (section) |s| if (s.owned) allocator.free(s.data);
1113
1114 var separate_debug_filename: ?[]const u8 = null;
1115 var separate_debug_crc: ?u32 = null;
1116
1117 for (shdrs) |*shdr| {
1118 if (shdr.sh_type == elf.SHT_NULL or shdr.sh_type == elf.SHT_NOBITS) continue;
1119 const name = mem.sliceTo(header_strings[shdr.sh_name..], 0);
1120
1121 if (mem.eql(u8, name, ".gnu_debuglink")) {
1122 const gnu_debuglink = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
1123 const debug_filename = mem.sliceTo(@as([*:0]const u8, @ptrCast(gnu_debuglink.ptr)), 0);
1124 const crc_offset = mem.alignForward(usize, @intFromPtr(&debug_filename[debug_filename.len]) + 1, 4) - @intFromPtr(gnu_debuglink.ptr);
1125 const crc_bytes = gnu_debuglink[crc_offset..][0..4];
1126 separate_debug_crc = mem.readInt(u32, crc_bytes, native_endian);
1127 separate_debug_filename = debug_filename;
1128 continue;
1129 }
1130
1131 var section_index: ?usize = null;
1132 inline for (@typeInfo(Dwarf.Section.Id).Enum.fields, 0..) |section, i| {
1133 if (mem.eql(u8, "." ++ section.name, name)) section_index = i;
1134 }
1135 if (section_index == null) continue;
1136 if (sections[section_index.?] != null) continue;
1137
1138 const section_bytes = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
1139 sections[section_index.?] = if ((shdr.sh_flags & elf.SHF_COMPRESSED) > 0) blk: {
1140 var section_stream = std.io.fixedBufferStream(section_bytes);
1141 var section_reader = section_stream.reader();
1142 const chdr = section_reader.readStruct(elf.Chdr) catch continue;
1143 if (chdr.ch_type != .ZLIB) continue;
1144
1145 var zlib_stream = std.compress.zlib.decompressor(section_stream.reader());
1146
1147 const decompressed_section = try allocator.alloc(u8, chdr.ch_size);
1148 errdefer allocator.free(decompressed_section);
1149
1150 const read = zlib_stream.reader().readAll(decompressed_section) catch continue;
1151 assert(read == decompressed_section.len);
1152
1153 break :blk .{
1154 .data = decompressed_section,
1155 .virtual_address = shdr.sh_addr,
1156 .owned = true,
1157 };
1158 } else .{
1159 .data = section_bytes,
1160 .virtual_address = shdr.sh_addr,
1161 .owned = false,
1162 };
1163 }
1164
1165 const missing_debug_info =
1166 sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or
1167 sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or
1168 sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or
1169 sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
1170
1171 // Attempt to load debug info from an external file
1172 // See: https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html
1173 if (missing_debug_info) {
1174
1175 // Only allow one level of debug info nesting
1176 if (parent_mapped_mem) |_| {
1177 return error.MissingDebugInfo;
1178 }
1179
1180 const global_debug_directories = [_][]const u8{
1181 "/usr/lib/debug",
1182 };
1183
1184 // <global debug directory>/.build-id/<2-character id prefix>/<id remainder>.debug
1185 if (build_id) |id| blk: {
1186 if (id.len < 3) break :blk;
1187
1188 // Either md5 (16 bytes) or sha1 (20 bytes) are used here in practice
1189 const extension = ".debug";
1190 var id_prefix_buf: [2]u8 = undefined;
1191 var filename_buf: [38 + extension.len]u8 = undefined;
1192
1193 _ = std.fmt.bufPrint(&id_prefix_buf, "{s}", .{std.fmt.fmtSliceHexLower(id[0..1])}) catch unreachable;
1194 const filename = std.fmt.bufPrint(
1195 &filename_buf,
1196 "{s}" ++ extension,
1197 .{std.fmt.fmtSliceHexLower(id[1..])},
1198 ) catch break :blk;
1199
1200 for (global_debug_directories) |global_directory| {
1201 const path = try fs.path.join(allocator, &.{ global_directory, ".build-id", &id_prefix_buf, filename });
1202 defer allocator.free(path);
1203
1204 return readElfDebugInfo(allocator, path, null, separate_debug_crc, &sections, mapped_mem) catch continue;
1205 }
1206 }
1207
1208 // use the path from .gnu_debuglink, in the same search order as gdb
1209 if (separate_debug_filename) |separate_filename| blk: {
1210 if (elf_filename != null and mem.eql(u8, elf_filename.?, separate_filename)) return error.MissingDebugInfo;
1211
1212 // <cwd>/<gnu_debuglink>
1213 if (readElfDebugInfo(allocator, separate_filename, null, separate_debug_crc, &sections, mapped_mem)) |debug_info| return debug_info else |_| {}
1214
1215 // <cwd>/.debug/<gnu_debuglink>
1216 {
1217 const path = try fs.path.join(allocator, &.{ ".debug", separate_filename });
1218 defer allocator.free(path);
1219
1220 if (readElfDebugInfo(allocator, path, null, separate_debug_crc, &sections, mapped_mem)) |debug_info| return debug_info else |_| {}
1221 }
1222
1223 var cwd_buf: [fs.max_path_bytes]u8 = undefined;
1224 const cwd_path = posix.realpath(".", &cwd_buf) catch break :blk;
1225
1226 // <global debug directory>/<absolute folder of current binary>/<gnu_debuglink>
1227 for (global_debug_directories) |global_directory| {
1228 const path = try fs.path.join(allocator, &.{ global_directory, cwd_path, separate_filename });
1229 defer allocator.free(path);
1230 if (readElfDebugInfo(allocator, path, null, separate_debug_crc, &sections, mapped_mem)) |debug_info| return debug_info else |_| {}
1231 }
1232 }
1233
1234 return error.MissingDebugInfo;
1235 }
1236
1237 var di = Dwarf{
1238 .endian = endian,
1239 .sections = sections,
1240 .is_macho = false,
1241 };
1242
1243 try Dwarf.open(&di, allocator);
1244
1245 return .{
1246 .base_address = undefined,
1247 .dwarf = di,
1248 .mapped_memory = parent_mapped_mem orelse mapped_mem,
1249 .external_mapped_memory = if (parent_mapped_mem != null) mapped_mem else null,
1250 };
1251 }1069 }
1252}1070}
12531071
...@@ -1289,22 +1107,6 @@ fn mapWholeFile(file: File) ![]align(mem.page_size) const u8 {...@@ -1289,22 +1107,6 @@ fn mapWholeFile(file: File) ![]align(mem.page_size) const u8 {
1289 }1107 }
1290}1108}
12911109
1292fn chopSlice(ptr: []const u8, offset: u64, size: u64) error{Overflow}![]const u8 {
1293 const start = math.cast(usize, offset) orelse return error.Overflow;
1294 const end = start + (math.cast(usize, size) orelse return error.Overflow);
1295 return ptr[start..end];
1296}
1297
1298pub const SymbolInfo = struct {
1299 symbol_name: []const u8 = "???",
1300 compile_unit_name: []const u8 = "???",
1301 line_info: ?std.debug.SourceLocation = null,
1302
1303 pub fn deinit(self: SymbolInfo, allocator: Allocator) void {
1304 if (self.line_info) |li| allocator.free(li.file_name);
1305 }
1306};
1307
1308fn machoSearchSymbols(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol {1110fn machoSearchSymbols(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol {
1309 var min: usize = 0;1111 var min: usize = 0;
1310 var max: usize = symbols.len - 1;1112 var max: usize = symbols.len - 1;
...@@ -1350,26 +1152,6 @@ test machoSearchSymbols {...@@ -1350,26 +1152,6 @@ test machoSearchSymbols {
1350 try testing.expectEqual(&symbols[2], machoSearchSymbols(&symbols, 5000).?);1152 try testing.expectEqual(&symbols[2], machoSearchSymbols(&symbols, 5000).?);
1351}1153}
13521154
1353fn getSymbolFromDwarf(allocator: Allocator, address: u64, di: *Dwarf) !SymbolInfo {
1354 if (nosuspend di.findCompileUnit(address)) |compile_unit| {
1355 return SymbolInfo{
1356 .symbol_name = nosuspend di.getSymbolName(address) orelse "???",
1357 .compile_unit_name = compile_unit.die.getAttrString(di, std.dwarf.AT.name, di.section(.debug_str), compile_unit.*) catch |err| switch (err) {
1358 error.MissingDebugInfo, error.InvalidDebugInfo => "???",
1359 },
1360 .line_info = nosuspend di.getLineNumberInfo(allocator, compile_unit.*, address) catch |err| switch (err) {
1361 error.MissingDebugInfo, error.InvalidDebugInfo => null,
1362 else => return err,
1363 },
1364 };
1365 } else |err| switch (err) {
1366 error.MissingDebugInfo, error.InvalidDebugInfo => {
1367 return SymbolInfo{};
1368 },
1369 else => return err,
1370 }
1371}
1372
1373/// Unwind a frame using MachO compact unwind info (from __unwind_info).1155/// Unwind a frame using MachO compact unwind info (from __unwind_info).
1374/// If the compact encoding can't encode a way to unwind a frame, it will1156/// If the compact encoding can't encode a way to unwind a frame, it will
1375/// defer unwinding to DWARF, in which case `.eh_frame` will be used if available.1157/// defer unwinding to DWARF, in which case `.eh_frame` will be used if available.
...@@ -1796,7 +1578,7 @@ pub fn unwindFrameDwarf(...@@ -1796,7 +1578,7 @@ pub fn unwindFrameDwarf(
1796 const frame_section = di.section(dwarf_section) orelse return error.MissingFDE;1578 const frame_section = di.section(dwarf_section) orelse return error.MissingFDE;
1797 if (fde_offset >= frame_section.len) return error.MissingFDE;1579 if (fde_offset >= frame_section.len) return error.MissingFDE;
17981580
1799 var fbr: std.debug.DeprecatedFixedBufferReader = .{1581 var fbr: std.debug.FixedBufferReader = .{
1800 .buf = frame_section,1582 .buf = frame_section,
1801 .pos = fde_offset,1583 .pos = fde_offset,
1802 .endian = di.endian,1584 .endian = di.endian,
...@@ -2028,6 +1810,7 @@ fn unwindFrameMachODwarf(...@@ -2028,6 +1810,7 @@ fn unwindFrameMachODwarf(
2028 var di: Dwarf = .{1810 var di: Dwarf = .{
2029 .endian = native_endian,1811 .endian = native_endian,
2030 .is_macho = true,1812 .is_macho = true,
1813 .compile_units_sorted = false,
2031 };1814 };
2032 defer di.deinit(context.allocator);1815 defer di.deinit(context.allocator);
20331816
lib/std/http.zig+2
...@@ -4,6 +4,7 @@ pub const protocol = @import("http/protocol.zig");...@@ -4,6 +4,7 @@ pub const protocol = @import("http/protocol.zig");
4pub const HeadParser = @import("http/HeadParser.zig");4pub const HeadParser = @import("http/HeadParser.zig");
5pub const ChunkParser = @import("http/ChunkParser.zig");5pub const ChunkParser = @import("http/ChunkParser.zig");
6pub const HeaderIterator = @import("http/HeaderIterator.zig");6pub const HeaderIterator = @import("http/HeaderIterator.zig");
7pub const WebSocket = @import("http/WebSocket.zig");
78
8pub const Version = enum {9pub const Version = enum {
9 @"HTTP/1.0",10 @"HTTP/1.0",
...@@ -318,6 +319,7 @@ test {...@@ -318,6 +319,7 @@ test {
318 _ = Status;319 _ = Status;
319 _ = HeadParser;320 _ = HeadParser;
320 _ = ChunkParser;321 _ = ChunkParser;
322 _ = WebSocket;
321 _ = @import("http/test.zig");323 _ = @import("http/test.zig");
322 }324 }
323}325}
lib/std/http/WebSocket.zig created+243
...@@ -0,0 +1,243 @@
1//! See https://tools.ietf.org/html/rfc6455
2
3const builtin = @import("builtin");
4const std = @import("std");
5const WebSocket = @This();
6const assert = std.debug.assert;
7const native_endian = builtin.cpu.arch.endian();
8
9key: []const u8,
10request: *std.http.Server.Request,
11recv_fifo: std.fifo.LinearFifo(u8, .Slice),
12reader: std.io.AnyReader,
13response: std.http.Server.Response,
14/// Number of bytes that have been peeked but not discarded yet.
15outstanding_len: usize,
16
17pub const InitError = error{WebSocketUpgradeMissingKey} ||
18 std.http.Server.Request.ReaderError;
19
20pub fn init(
21 ws: *WebSocket,
22 request: *std.http.Server.Request,
23 send_buffer: []u8,
24 recv_buffer: []align(4) u8,
25) InitError!bool {
26 var sec_websocket_key: ?[]const u8 = null;
27 var upgrade_websocket: bool = false;
28 var it = request.iterateHeaders();
29 while (it.next()) |header| {
30 if (std.ascii.eqlIgnoreCase(header.name, "sec-websocket-key")) {
31 sec_websocket_key = header.value;
32 } else if (std.ascii.eqlIgnoreCase(header.name, "upgrade")) {
33 if (!std.mem.eql(u8, header.value, "websocket"))
34 return false;
35 upgrade_websocket = true;
36 }
37 }
38 if (!upgrade_websocket)
39 return false;
40
41 const key = sec_websocket_key orelse return error.WebSocketUpgradeMissingKey;
42
43 var sha1 = std.crypto.hash.Sha1.init(.{});
44 sha1.update(key);
45 sha1.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
46 var digest: [std.crypto.hash.Sha1.digest_length]u8 = undefined;
47 sha1.final(&digest);
48 var base64_digest: [28]u8 = undefined;
49 assert(std.base64.standard.Encoder.encode(&base64_digest, &digest).len == base64_digest.len);
50
51 request.head.content_length = std.math.maxInt(u64);
52
53 ws.* = .{
54 .key = key,
55 .recv_fifo = std.fifo.LinearFifo(u8, .Slice).init(recv_buffer),
56 .reader = try request.reader(),
57 .response = request.respondStreaming(.{
58 .send_buffer = send_buffer,
59 .respond_options = .{
60 .status = .switching_protocols,
61 .extra_headers = &.{
62 .{ .name = "upgrade", .value = "websocket" },
63 .{ .name = "connection", .value = "upgrade" },
64 .{ .name = "sec-websocket-accept", .value = &base64_digest },
65 },
66 .transfer_encoding = .none,
67 },
68 }),
69 .request = request,
70 .outstanding_len = 0,
71 };
72 return true;
73}
74
75pub const Header0 = packed struct(u8) {
76 opcode: Opcode,
77 rsv3: u1 = 0,
78 rsv2: u1 = 0,
79 rsv1: u1 = 0,
80 fin: bool,
81};
82
83pub const Header1 = packed struct(u8) {
84 payload_len: enum(u7) {
85 len16 = 126,
86 len64 = 127,
87 _,
88 },
89 mask: bool,
90};
91
92pub const Opcode = enum(u4) {
93 continuation = 0,
94 text = 1,
95 binary = 2,
96 connection_close = 8,
97 ping = 9,
98 /// "A Pong frame MAY be sent unsolicited. This serves as a unidirectional
99 /// heartbeat. A response to an unsolicited Pong frame is not expected."
100 pong = 10,
101 _,
102};
103
104pub const ReadSmallTextMessageError = error{
105 ConnectionClose,
106 UnexpectedOpCode,
107 MessageTooBig,
108 MissingMaskBit,
109} || RecvError;
110
111pub const SmallMessage = struct {
112 /// Can be text, binary, or ping.
113 opcode: Opcode,
114 data: []u8,
115};
116
117/// Reads the next message from the WebSocket stream, failing if the message does not fit
118/// into `recv_buffer`.
119pub fn readSmallMessage(ws: *WebSocket) ReadSmallTextMessageError!SmallMessage {
120 while (true) {
121 const header_bytes = (try recv(ws, 2))[0..2];
122 const h0: Header0 = @bitCast(header_bytes[0]);
123 const h1: Header1 = @bitCast(header_bytes[1]);
124
125 switch (h0.opcode) {
126 .text, .binary, .pong, .ping => {},
127 .connection_close => return error.ConnectionClose,
128 .continuation => return error.UnexpectedOpCode,
129 _ => return error.UnexpectedOpCode,
130 }
131
132 if (!h0.fin) return error.MessageTooBig;
133 if (!h1.mask) return error.MissingMaskBit;
134
135 const len: usize = switch (h1.payload_len) {
136 .len16 => try recvReadInt(ws, u16),
137 .len64 => std.math.cast(usize, try recvReadInt(ws, u64)) orelse return error.MessageTooBig,
138 else => @intFromEnum(h1.payload_len),
139 };
140 if (len > ws.recv_fifo.buf.len) return error.MessageTooBig;
141
142 const mask: u32 = @bitCast((try recv(ws, 4))[0..4].*);
143 const payload = try recv(ws, len);
144
145 // Skip pongs.
146 if (h0.opcode == .pong) continue;
147
148 // The last item may contain a partial word of unused data.
149 const floored_len = (payload.len / 4) * 4;
150 const u32_payload: []align(1) u32 = @alignCast(std.mem.bytesAsSlice(u32, payload[0..floored_len]));
151 for (u32_payload) |*elem| elem.* ^= mask;
152 const mask_bytes = std.mem.asBytes(&mask)[0 .. payload.len - floored_len];
153 for (payload[floored_len..], mask_bytes) |*leftover, m| leftover.* ^= m;
154
155 return .{
156 .opcode = h0.opcode,
157 .data = payload,
158 };
159 }
160}
161
162const RecvError = std.http.Server.Request.ReadError || error{EndOfStream};
163
164fn recv(ws: *WebSocket, len: usize) RecvError![]u8 {
165 ws.recv_fifo.discard(ws.outstanding_len);
166 assert(len <= ws.recv_fifo.buf.len);
167 if (len > ws.recv_fifo.count) {
168 const small_buf = ws.recv_fifo.writableSlice(0);
169 const needed = len - ws.recv_fifo.count;
170 const buf = if (small_buf.len >= needed) small_buf else b: {
171 ws.recv_fifo.realign();
172 break :b ws.recv_fifo.writableSlice(0);
173 };
174 const n = try @as(RecvError!usize, @errorCast(ws.reader.readAtLeast(buf, needed)));
175 if (n < needed) return error.EndOfStream;
176 ws.recv_fifo.update(n);
177 }
178 ws.outstanding_len = len;
179 // TODO: improve the std lib API so this cast isn't necessary.
180 return @constCast(ws.recv_fifo.readableSliceOfLen(len));
181}
182
183fn recvReadInt(ws: *WebSocket, comptime I: type) !I {
184 const unswapped: I = @bitCast((try recv(ws, @sizeOf(I)))[0..@sizeOf(I)].*);
185 return switch (native_endian) {
186 .little => @byteSwap(unswapped),
187 .big => unswapped,
188 };
189}
190
191pub const WriteError = std.http.Server.Response.WriteError;
192
193pub fn writeMessage(ws: *WebSocket, message: []const u8, opcode: Opcode) WriteError!void {
194 const iovecs: [1]std.posix.iovec_const = .{
195 .{ .base = message.ptr, .len = message.len },
196 };
197 return writeMessagev(ws, &iovecs, opcode);
198}
199
200pub fn writeMessagev(ws: *WebSocket, message: []const std.posix.iovec_const, opcode: Opcode) WriteError!void {
201 const total_len = l: {
202 var total_len: u64 = 0;
203 for (message) |iovec| total_len += iovec.len;
204 break :l total_len;
205 };
206
207 var header_buf: [2 + 8]u8 = undefined;
208 header_buf[0] = @bitCast(@as(Header0, .{
209 .opcode = opcode,
210 .fin = true,
211 }));
212 const header = switch (total_len) {
213 0...125 => blk: {
214 header_buf[1] = @bitCast(@as(Header1, .{
215 .payload_len = @enumFromInt(total_len),
216 .mask = false,
217 }));
218 break :blk header_buf[0..2];
219 },
220 126...0xffff => blk: {
221 header_buf[1] = @bitCast(@as(Header1, .{
222 .payload_len = .len16,
223 .mask = false,
224 }));
225 std.mem.writeInt(u16, header_buf[2..4], @intCast(total_len), .big);
226 break :blk header_buf[0..4];
227 },
228 else => blk: {
229 header_buf[1] = @bitCast(@as(Header1, .{
230 .payload_len = .len64,
231 .mask = false,
232 }));
233 std.mem.writeInt(u64, header_buf[2..10], total_len, .big);
234 break :blk header_buf[0..10];
235 },
236 };
237
238 const response = &ws.response;
239 try response.writeAll(header);
240 for (message) |iovec|
241 try response.writeAll(iovec.base[0..iovec.len]);
242 try response.flush();
243}
lib/std/posix.zig+5
...@@ -47,6 +47,11 @@ else switch (native_os) {...@@ -47,6 +47,11 @@ else switch (native_os) {
47 .plan9 => std.os.plan9,47 .plan9 => std.os.plan9,
48 else => struct {48 else => struct {
49 pub const ucontext_t = void;49 pub const ucontext_t = void;
50 pub const pid_t = void;
51 pub const pollfd = void;
52 pub const fd_t = void;
53 pub const uid_t = void;
54 pub const gid_t = void;
50 },55 },
51};56};
5257
lib/std/zig/Server.zig+22-6
...@@ -28,6 +28,14 @@ pub const Message = struct {...@@ -28,6 +28,14 @@ pub const Message = struct {
28 /// The remaining bytes is the file path relative to that prefix.28 /// The remaining bytes is the file path relative to that prefix.
29 /// The prefixes are hard-coded in Compilation.create (cwd, zig lib dir, local cache dir)29 /// The prefixes are hard-coded in Compilation.create (cwd, zig lib dir, local cache dir)
30 file_system_inputs,30 file_system_inputs,
31 /// Body is a u64le that indicates the file path within the cache used
32 /// to store coverage information. The integer is a hash of the PCs
33 /// stored within that file.
34 coverage_id,
35 /// Body is a u64le that indicates the function pointer virtual memory
36 /// address of the fuzz unit test. This is used to provide a starting
37 /// point to view coverage.
38 fuzz_start_addr,
3139
32 _,40 _,
33 };41 };
...@@ -180,6 +188,14 @@ pub fn serveMessage(...@@ -180,6 +188,14 @@ pub fn serveMessage(
180 try s.out.writevAll(iovecs[0 .. bufs.len + 1]);188 try s.out.writevAll(iovecs[0 .. bufs.len + 1]);
181}189}
182190
191pub fn serveU64Message(s: *Server, tag: OutMessage.Tag, int: u64) !void {
192 const msg_le = bswap(int);
193 return s.serveMessage(.{
194 .tag = tag,
195 .bytes_len = @sizeOf(u64),
196 }, &.{std.mem.asBytes(&msg_le)});
197}
198
183pub fn serveEmitBinPath(199pub fn serveEmitBinPath(
184 s: *Server,200 s: *Server,
185 fs_path: []const u8,201 fs_path: []const u8,
...@@ -187,7 +203,7 @@ pub fn serveEmitBinPath(...@@ -187,7 +203,7 @@ pub fn serveEmitBinPath(
187) !void {203) !void {
188 try s.serveMessage(.{204 try s.serveMessage(.{
189 .tag = .emit_bin_path,205 .tag = .emit_bin_path,
190 .bytes_len = @as(u32, @intCast(fs_path.len + @sizeOf(OutMessage.EmitBinPath))),206 .bytes_len = @intCast(fs_path.len + @sizeOf(OutMessage.EmitBinPath)),
191 }, &.{207 }, &.{
192 std.mem.asBytes(&header),208 std.mem.asBytes(&header),
193 fs_path,209 fs_path,
...@@ -201,7 +217,7 @@ pub fn serveTestResults(...@@ -201,7 +217,7 @@ pub fn serveTestResults(
201 const msg_le = bswap(msg);217 const msg_le = bswap(msg);
202 try s.serveMessage(.{218 try s.serveMessage(.{
203 .tag = .test_results,219 .tag = .test_results,
204 .bytes_len = @as(u32, @intCast(@sizeOf(OutMessage.TestResults))),220 .bytes_len = @intCast(@sizeOf(OutMessage.TestResults)),
205 }, &.{221 }, &.{
206 std.mem.asBytes(&msg_le),222 std.mem.asBytes(&msg_le),
207 });223 });
...@@ -209,14 +225,14 @@ pub fn serveTestResults(...@@ -209,14 +225,14 @@ pub fn serveTestResults(
209225
210pub fn serveErrorBundle(s: *Server, error_bundle: std.zig.ErrorBundle) !void {226pub fn serveErrorBundle(s: *Server, error_bundle: std.zig.ErrorBundle) !void {
211 const eb_hdr: OutMessage.ErrorBundle = .{227 const eb_hdr: OutMessage.ErrorBundle = .{
212 .extra_len = @as(u32, @intCast(error_bundle.extra.len)),228 .extra_len = @intCast(error_bundle.extra.len),
213 .string_bytes_len = @as(u32, @intCast(error_bundle.string_bytes.len)),229 .string_bytes_len = @intCast(error_bundle.string_bytes.len),
214 };230 };
215 const bytes_len = @sizeOf(OutMessage.ErrorBundle) +231 const bytes_len = @sizeOf(OutMessage.ErrorBundle) +
216 4 * error_bundle.extra.len + error_bundle.string_bytes.len;232 4 * error_bundle.extra.len + error_bundle.string_bytes.len;
217 try s.serveMessage(.{233 try s.serveMessage(.{
218 .tag = .error_bundle,234 .tag = .error_bundle,
219 .bytes_len = @as(u32, @intCast(bytes_len)),235 .bytes_len = @intCast(bytes_len),
220 }, &.{236 }, &.{
221 std.mem.asBytes(&eb_hdr),237 std.mem.asBytes(&eb_hdr),
222 // TODO: implement @ptrCast between slices changing the length238 // TODO: implement @ptrCast between slices changing the length
...@@ -251,7 +267,7 @@ pub fn serveTestMetadata(s: *Server, test_metadata: TestMetadata) !void {...@@ -251,7 +267,7 @@ pub fn serveTestMetadata(s: *Server, test_metadata: TestMetadata) !void {
251267
252 return s.serveMessage(.{268 return s.serveMessage(.{
253 .tag = .test_metadata,269 .tag = .test_metadata,
254 .bytes_len = @as(u32, @intCast(bytes_len)),270 .bytes_len = @intCast(bytes_len),
255 }, &.{271 }, &.{
256 std.mem.asBytes(&header),272 std.mem.asBytes(&header),
257 // TODO: implement @ptrCast between slices changing the length273 // TODO: implement @ptrCast between slices changing the length
lib/std/zig/tokenizer.zig+45
...@@ -1840,3 +1840,48 @@ fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !v...@@ -1840,3 +1840,48 @@ fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !v
1840 try std.testing.expectEqual(source.len, last_token.loc.start);1840 try std.testing.expectEqual(source.len, last_token.loc.start);
1841 try std.testing.expectEqual(source.len, last_token.loc.end);1841 try std.testing.expectEqual(source.len, last_token.loc.end);
1842}1842}
1843
1844test "fuzzable properties upheld" {
1845 const source = std.testing.fuzzInput(.{});
1846 const source0 = try std.testing.allocator.dupeZ(u8, source);
1847 defer std.testing.allocator.free(source0);
1848 var tokenizer = Tokenizer.init(source0);
1849 var tokenization_failed = false;
1850 while (true) {
1851 const token = tokenizer.next();
1852
1853 // Property: token end location after start location (or equal)
1854 try std.testing.expect(token.loc.end >= token.loc.start);
1855
1856 switch (token.tag) {
1857 .invalid => {
1858 tokenization_failed = true;
1859
1860 // Property: invalid token always ends at newline or eof
1861 try std.testing.expect(source0[token.loc.end] == '\n' or source0[token.loc.end] == 0);
1862 },
1863 .eof => {
1864 // Property: EOF token is always 0-length at end of source.
1865 try std.testing.expectEqual(source0.len, token.loc.start);
1866 try std.testing.expectEqual(source0.len, token.loc.end);
1867 break;
1868 },
1869 else => continue,
1870 }
1871 }
1872
1873 if (source0.len > 0) for (source0, source0[1..][0..source0.len]) |cur, next| {
1874 // Property: No null byte allowed except at end.
1875 if (cur == 0) {
1876 try std.testing.expect(tokenization_failed);
1877 }
1878 // Property: No ASCII control characters other than \n and \t are allowed.
1879 if (std.ascii.isControl(cur) and cur != '\n' and cur != '\t') {
1880 try std.testing.expect(tokenization_failed);
1881 }
1882 // Property: All '\r' must be followed by '\n'.
1883 if (cur == '\r' and next != '\n') {
1884 try std.testing.expect(tokenization_failed);
1885 }
1886 };
1887}
src/Compilation.zig+26-4
...@@ -4201,10 +4201,11 @@ fn workerDocsWasm(comp: *Compilation, parent_prog_node: std.Progress.Node) void...@@ -4201,10 +4201,11 @@ fn workerDocsWasm(comp: *Compilation, parent_prog_node: std.Progress.Node) void
4201 const prog_node = parent_prog_node.start("Compile Autodocs", 0);4201 const prog_node = parent_prog_node.start("Compile Autodocs", 0);
4202 defer prog_node.end();4202 defer prog_node.end();
42034203
4204 workerDocsWasmFallible(comp, prog_node) catch |err| {4204 workerDocsWasmFallible(comp, prog_node) catch |err| switch (err) {
4205 comp.lockAndSetMiscFailure(.docs_wasm, "unable to build autodocs: {s}", .{4205 error.SubCompilationFailed => return, // error reported already
4206 else => comp.lockAndSetMiscFailure(.docs_wasm, "unable to build autodocs: {s}", .{
4206 @errorName(err),4207 @errorName(err),
4207 });4208 }),
4208 };4209 };
4209}4210}
42104211
...@@ -4274,8 +4275,29 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) anye...@@ -4274,8 +4275,29 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) anye
4274 .cc_argv = &.{},4275 .cc_argv = &.{},
4275 .parent = null,4276 .parent = null,
4276 .builtin_mod = null,4277 .builtin_mod = null,
4277 .builtin_modules = null, // there is only one module in this compilation4278 .builtin_modules = null,
4279 });
4280 const walk_mod = try Package.Module.create(arena, .{
4281 .global_cache_directory = comp.global_cache_directory,
4282 .paths = .{
4283 .root = .{
4284 .root_dir = comp.zig_lib_directory,
4285 .sub_path = "docs/wasm",
4286 },
4287 .root_src_path = "Walk.zig",
4288 },
4289 .fully_qualified_name = "Walk",
4290 .inherited = .{
4291 .resolved_target = resolved_target,
4292 .optimize_mode = optimize_mode,
4293 },
4294 .global = config,
4295 .cc_argv = &.{},
4296 .parent = root_mod,
4297 .builtin_mod = root_mod.getBuiltinDependency(),
4298 .builtin_modules = null, // `builtin_mod` is set
4278 });4299 });
4300 try root_mod.deps.put(arena, "Walk", walk_mod);
4279 const bin_basename = try std.zig.binNameAlloc(arena, .{4301 const bin_basename = try std.zig.binNameAlloc(arena, .{
4280 .root_name = root_name,4302 .root_name = root_name,
4281 .target = resolved_target.result,4303 .target = resolved_target.result,
test/standalone/coff_dwarf/build.zig+4-3
...@@ -7,9 +7,10 @@ pub fn build(b: *std.Build) void {...@@ -7,9 +7,10 @@ pub fn build(b: *std.Build) void {
7 b.default_step = test_step;7 b.default_step = test_step;
88
9 const optimize: std.builtin.OptimizeMode = .Debug;9 const optimize: std.builtin.OptimizeMode = .Debug;
10 const target = b.standardTargetOptions(.{});10 const target = if (builtin.os.tag == .windows)
1111 b.standardTargetOptions(.{})
12 if (builtin.os.tag != .windows) return;12 else
13 b.resolveTargetQuery(.{ .os_tag = .windows });
1314
14 if (builtin.cpu.arch == .aarch64) {15 if (builtin.cpu.arch == .aarch64) {
15 // https://github.com/ziglang/zig/issues/1842716 // https://github.com/ziglang/zig/issues/18427
test/standalone/coff_dwarf/main.zig+6-6
...@@ -17,11 +17,11 @@ pub fn main() !void {...@@ -17,11 +17,11 @@ pub fn main() !void {
1717
18 const module = try debug_info.getModuleForAddress(add_addr);18 const module = try debug_info.getModuleForAddress(add_addr);
19 const symbol = try module.getSymbolAtAddress(allocator, add_addr);19 const symbol = try module.getSymbolAtAddress(allocator, add_addr);
20 defer symbol.deinit(allocator);20 defer if (symbol.source_location) |sl| allocator.free(sl.file_name);
2121
22 try testing.expectEqualStrings("add", symbol.symbol_name);22 try testing.expectEqualStrings("add", symbol.name);
23 try testing.expect(symbol.line_info != null);23 try testing.expect(symbol.source_location != null);
24 try testing.expectEqualStrings("shared_lib.c", std.fs.path.basename(symbol.line_info.?.file_name));24 try testing.expectEqualStrings("shared_lib.c", std.fs.path.basename(symbol.source_location.?.file_name));
25 try testing.expectEqual(@as(u64, 3), symbol.line_info.?.line);25 try testing.expectEqual(@as(u64, 3), symbol.source_location.?.line);
26 try testing.expectEqual(@as(u64, 0), symbol.line_info.?.column);26 try testing.expectEqual(@as(u64, 0), symbol.source_location.?.column);
27}27}
tools/dump-cov.zig created+79
...@@ -0,0 +1,79 @@
1//! Reads a Zig coverage file and prints human-readable information to stdout,
2//! including file:line:column information for each PC.
3
4const std = @import("std");
5const fatal = std.process.fatal;
6const Path = std.Build.Cache.Path;
7const assert = std.debug.assert;
8
9pub fn main() !void {
10 var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .{};
11 defer _ = general_purpose_allocator.deinit();
12 const gpa = general_purpose_allocator.allocator();
13
14 var arena_instance = std.heap.ArenaAllocator.init(gpa);
15 defer arena_instance.deinit();
16 const arena = arena_instance.allocator();
17
18 const args = try std.process.argsAlloc(arena);
19 const exe_file_name = args[1];
20 const cov_file_name = args[2];
21
22 const exe_path: Path = .{
23 .root_dir = std.Build.Cache.Directory.cwd(),
24 .sub_path = exe_file_name,
25 };
26 const cov_path: Path = .{
27 .root_dir = std.Build.Cache.Directory.cwd(),
28 .sub_path = cov_file_name,
29 };
30
31 var coverage = std.debug.Coverage.init;
32 defer coverage.deinit(gpa);
33
34 var debug_info = std.debug.Info.load(gpa, exe_path, &coverage) catch |err| {
35 fatal("failed to load debug info for {}: {s}", .{ exe_path, @errorName(err) });
36 };
37 defer debug_info.deinit(gpa);
38
39 const cov_bytes = cov_path.root_dir.handle.readFileAlloc(arena, cov_path.sub_path, 1 << 30) catch |err| {
40 fatal("failed to load coverage file {}: {s}", .{ cov_path, @errorName(err) });
41 };
42
43 var bw = std.io.bufferedWriter(std.io.getStdOut().writer());
44 const stdout = bw.writer();
45
46 const header: *align(1) SeenPcsHeader = @ptrCast(cov_bytes);
47 try stdout.print("{any}\n", .{header.*});
48 //const n_bitset_elems = (header.pcs_len + 7) / 8;
49 const pcs_bytes = cov_bytes[@sizeOf(SeenPcsHeader)..][0 .. header.pcs_len * @sizeOf(usize)];
50 const pcs = try arena.alloc(usize, header.pcs_len);
51 for (0..pcs_bytes.len / @sizeOf(usize), pcs) |i, *pc| {
52 pc.* = std.mem.readInt(usize, pcs_bytes[i * @sizeOf(usize) ..][0..@sizeOf(usize)], .little);
53 }
54 assert(std.sort.isSorted(usize, pcs, {}, std.sort.asc(usize)));
55
56 const seen_pcs = cov_bytes[@sizeOf(SeenPcsHeader) + pcs.len * @sizeOf(usize) ..];
57
58 const source_locations = try arena.alloc(std.debug.Coverage.SourceLocation, pcs.len);
59 try debug_info.resolveAddresses(gpa, pcs, source_locations);
60
61 for (pcs, source_locations, 0..) |pc, sl, i| {
62 const file = debug_info.coverage.fileAt(sl.file);
63 const dir_name = debug_info.coverage.directories.keys()[file.directory_index];
64 const dir_name_slice = debug_info.coverage.stringAt(dir_name);
65 const hit: u1 = @truncate(seen_pcs[i / 8] >> @intCast(i % 8));
66 try stdout.print("{c}{x}: {s}/{s}:{d}:{d}\n", .{
67 "-+"[hit], pc, dir_name_slice, debug_info.coverage.stringAt(file.basename), sl.line, sl.column,
68 });
69 }
70
71 try bw.flush();
72}
73
74const SeenPcsHeader = extern struct {
75 n_runs: usize,
76 deduplicated_runs: usize,
77 pcs_len: usize,
78 lowest_stack: usize,
79};