mirror of
https://github.com/ziglang/zig.git
synced 2026-01-04 20:43:19 +00:00
Merge pull request #20958 from ziglang/fuzz
introduce a fuzz testing web interface
This commit is contained in:
commit
0e99f517f2
@ -13,11 +13,9 @@ Documentation** corresponding to the version of Zig that you are using by
|
|||||||
following the appropriate link on the
|
following the appropriate link on the
|
||||||
[download page](https://ziglang.org/download).
|
[download page](https://ziglang.org/download).
|
||||||
|
|
||||||
Otherwise, you're looking at a release of Zig, and you can find documentation
|
Otherwise, you're looking at a release of Zig, so you can find the language
|
||||||
here:
|
reference at `doc/langref.html`, and the standard library documentation by
|
||||||
|
running `zig std`, which will open a browser tab.
|
||||||
* doc/langref.html
|
|
||||||
* doc/std/index.html
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,12 @@ const runner = @This();
|
|||||||
pub const root = @import("@build");
|
pub const root = @import("@build");
|
||||||
pub const dependencies = @import("@dependencies");
|
pub const dependencies = @import("@dependencies");
|
||||||
|
|
||||||
|
pub const std_options: std.Options = .{
|
||||||
|
.side_channels_mitigations = .none,
|
||||||
|
.http_disable_tls = true,
|
||||||
|
.crypto_fork_safety = false,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
// Here we use an ArenaAllocator backed by a page allocator because a build is a short-lived,
|
// Here we use an ArenaAllocator backed by a page allocator because a build is a short-lived,
|
||||||
// one shot program. We don't need to waste time freeing memory and finding places to squish
|
// 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 {
|
|||||||
var watch = false;
|
var watch = false;
|
||||||
var fuzz = false;
|
var fuzz = false;
|
||||||
var debounce_interval_ms: u16 = 50;
|
var debounce_interval_ms: u16 = 50;
|
||||||
|
var listen_port: u16 = 0;
|
||||||
|
|
||||||
while (nextArg(args, &arg_idx)) |arg| {
|
while (nextArg(args, &arg_idx)) |arg| {
|
||||||
if (mem.startsWith(u8, arg, "-Z")) {
|
if (mem.startsWith(u8, arg, "-Z")) {
|
||||||
@ -203,6 +210,14 @@ pub fn main() !void {
|
|||||||
next_arg, @errorName(err),
|
next_arg, @errorName(err),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
} else if (mem.eql(u8, arg, "--port")) {
|
||||||
|
const next_arg = nextArg(args, &arg_idx) orelse
|
||||||
|
fatalWithHint("expected u16 after '{s}'", .{arg});
|
||||||
|
listen_port = std.fmt.parseUnsigned(u16, next_arg, 10) catch |err| {
|
||||||
|
fatal("unable to parse port '{s}' as unsigned 16-bit integer: {s}\n", .{
|
||||||
|
next_arg, @errorName(err),
|
||||||
|
});
|
||||||
|
};
|
||||||
} else if (mem.eql(u8, arg, "--debug-log")) {
|
} else if (mem.eql(u8, arg, "--debug-log")) {
|
||||||
const next_arg = nextArgOrFatal(args, &arg_idx);
|
const next_arg = nextArgOrFatal(args, &arg_idx);
|
||||||
try debug_log_scopes.append(next_arg);
|
try debug_log_scopes.append(next_arg);
|
||||||
@ -403,7 +418,27 @@ pub fn main() !void {
|
|||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
if (fuzz) {
|
if (fuzz) {
|
||||||
Fuzz.start(&run.thread_pool, run.step_stack.keys(), run.ttyconf, main_progress_node);
|
switch (builtin.os.tag) {
|
||||||
|
// Current implementation depends on two things that need to be ported to Windows:
|
||||||
|
// * Memory-mapping to share data between the fuzzer and build runner.
|
||||||
|
// * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
|
||||||
|
// many addresses to source locations).
|
||||||
|
.windows => fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
const listen_address = std.net.Address.parseIp("127.0.0.1", listen_port) catch unreachable;
|
||||||
|
try Fuzz.start(
|
||||||
|
gpa,
|
||||||
|
arena,
|
||||||
|
global_cache_directory,
|
||||||
|
zig_lib_directory,
|
||||||
|
zig_exe,
|
||||||
|
&run.thread_pool,
|
||||||
|
run.step_stack.keys(),
|
||||||
|
run.ttyconf,
|
||||||
|
listen_address,
|
||||||
|
main_progress_node,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!watch) return cleanExit();
|
if (!watch) return cleanExit();
|
||||||
|
|||||||
@ -275,10 +275,6 @@ fn buildWasmBinary(
|
|||||||
) ![]const u8 {
|
) ![]const u8 {
|
||||||
const gpa = context.gpa;
|
const gpa = context.gpa;
|
||||||
|
|
||||||
const main_src_path = try std.fs.path.join(arena, &.{
|
|
||||||
context.zig_lib_directory, "docs", "wasm", "main.zig",
|
|
||||||
});
|
|
||||||
|
|
||||||
var argv: std.ArrayListUnmanaged([]const u8) = .{};
|
var argv: std.ArrayListUnmanaged([]const u8) = .{};
|
||||||
|
|
||||||
try argv.appendSlice(arena, &.{
|
try argv.appendSlice(arena, &.{
|
||||||
@ -298,7 +294,10 @@ fn buildWasmBinary(
|
|||||||
"--name",
|
"--name",
|
||||||
"autodoc",
|
"autodoc",
|
||||||
"-rdynamic",
|
"-rdynamic",
|
||||||
main_src_path,
|
"--dep",
|
||||||
|
"Walk",
|
||||||
|
try std.fmt.allocPrint(arena, "-Mroot={s}/docs/wasm/main.zig", .{context.zig_lib_directory}),
|
||||||
|
try std.fmt.allocPrint(arena, "-MWalk={s}/docs/wasm/Walk.zig", .{context.zig_lib_directory}),
|
||||||
"--listen=-",
|
"--listen=-",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
//! Default test runner for unit tests.
|
//! Default test runner for unit tests.
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const io = std.io;
|
const io = std.io;
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
pub const std_options = .{
|
pub const std_options = .{
|
||||||
.logFn = log,
|
.logFn = log,
|
||||||
@ -28,6 +30,7 @@ pub fn main() void {
|
|||||||
@panic("unable to parse command line args");
|
@panic("unable to parse command line args");
|
||||||
|
|
||||||
var listen = false;
|
var listen = false;
|
||||||
|
var opt_cache_dir: ?[]const u8 = null;
|
||||||
|
|
||||||
for (args[1..]) |arg| {
|
for (args[1..]) |arg| {
|
||||||
if (std.mem.eql(u8, arg, "--listen=-")) {
|
if (std.mem.eql(u8, arg, "--listen=-")) {
|
||||||
@ -35,12 +38,18 @@ pub fn main() void {
|
|||||||
} else if (std.mem.startsWith(u8, arg, "--seed=")) {
|
} else if (std.mem.startsWith(u8, arg, "--seed=")) {
|
||||||
testing.random_seed = std.fmt.parseUnsigned(u32, arg["--seed=".len..], 0) catch
|
testing.random_seed = std.fmt.parseUnsigned(u32, arg["--seed=".len..], 0) catch
|
||||||
@panic("unable to parse --seed command line argument");
|
@panic("unable to parse --seed command line argument");
|
||||||
|
} else if (std.mem.startsWith(u8, arg, "--cache-dir")) {
|
||||||
|
opt_cache_dir = arg["--cache-dir=".len..];
|
||||||
} else {
|
} else {
|
||||||
@panic("unrecognized command line argument");
|
@panic("unrecognized command line argument");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fba.reset();
|
fba.reset();
|
||||||
|
if (builtin.fuzz) {
|
||||||
|
const cache_dir = opt_cache_dir orelse @panic("missing --cache-dir=[path] argument");
|
||||||
|
fuzzer_init(FuzzerSlice.fromSlice(cache_dir));
|
||||||
|
}
|
||||||
|
|
||||||
if (listen) {
|
if (listen) {
|
||||||
return mainServer() catch @panic("internal test runner failure");
|
return mainServer() catch @panic("internal test runner failure");
|
||||||
@ -59,6 +68,11 @@ fn mainServer() !void {
|
|||||||
});
|
});
|
||||||
defer server.deinit();
|
defer server.deinit();
|
||||||
|
|
||||||
|
if (builtin.fuzz) {
|
||||||
|
const coverage_id = fuzzer_coverage_id();
|
||||||
|
try server.serveU64Message(.coverage_id, coverage_id);
|
||||||
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const hdr = try server.receiveMessage();
|
const hdr = try server.receiveMessage();
|
||||||
switch (hdr.tag) {
|
switch (hdr.tag) {
|
||||||
@ -129,7 +143,9 @@ fn mainServer() !void {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
.start_fuzzing => {
|
.start_fuzzing => {
|
||||||
|
if (!builtin.fuzz) unreachable;
|
||||||
const index = try server.receiveBody_u32();
|
const index = try server.receiveBody_u32();
|
||||||
|
var first = true;
|
||||||
const test_fn = builtin.test_functions[index];
|
const test_fn = builtin.test_functions[index];
|
||||||
while (true) {
|
while (true) {
|
||||||
testing.allocator_instance = .{};
|
testing.allocator_instance = .{};
|
||||||
@ -148,6 +164,10 @@ fn mainServer() !void {
|
|||||||
};
|
};
|
||||||
if (!is_fuzz_test) @panic("missed call to std.testing.fuzzInput");
|
if (!is_fuzz_test) @panic("missed call to std.testing.fuzzInput");
|
||||||
if (log_err_count != 0) @panic("error logs detected");
|
if (log_err_count != 0) @panic("error logs detected");
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
try server.serveU64Message(.fuzz_start_addr, entry_addr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -315,20 +335,32 @@ const FuzzerSlice = extern struct {
|
|||||||
ptr: [*]const u8,
|
ptr: [*]const u8,
|
||||||
len: usize,
|
len: usize,
|
||||||
|
|
||||||
|
/// Inline to avoid fuzzer instrumentation.
|
||||||
inline fn toSlice(s: FuzzerSlice) []const u8 {
|
inline fn toSlice(s: FuzzerSlice) []const u8 {
|
||||||
return s.ptr[0..s.len];
|
return s.ptr[0..s.len];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inline to avoid fuzzer instrumentation.
|
||||||
|
inline fn fromSlice(s: []const u8) FuzzerSlice {
|
||||||
|
return .{ .ptr = s.ptr, .len = s.len };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var is_fuzz_test: bool = undefined;
|
var is_fuzz_test: bool = undefined;
|
||||||
|
var entry_addr: usize = 0;
|
||||||
|
|
||||||
extern fn fuzzer_next() FuzzerSlice;
|
extern fn fuzzer_next() FuzzerSlice;
|
||||||
|
extern fn fuzzer_init(cache_dir: FuzzerSlice) void;
|
||||||
|
extern fn fuzzer_coverage_id() u64;
|
||||||
|
|
||||||
pub fn fuzzInput(options: testing.FuzzInputOptions) []const u8 {
|
pub fn fuzzInput(options: testing.FuzzInputOptions) []const u8 {
|
||||||
@disableInstrumentation();
|
@disableInstrumentation();
|
||||||
if (crippled) return "";
|
if (crippled) return "";
|
||||||
is_fuzz_test = true;
|
is_fuzz_test = true;
|
||||||
if (builtin.fuzz) return fuzzer_next().toSlice();
|
if (builtin.fuzz) {
|
||||||
|
if (entry_addr == 0) entry_addr = @returnAddress();
|
||||||
|
return fuzzer_next().toSlice();
|
||||||
|
}
|
||||||
if (options.corpus.len == 0) return "";
|
if (options.corpus.len == 0) return "";
|
||||||
var prng = std.Random.DefaultPrng.init(testing.random_seed);
|
var prng = std.Random.DefaultPrng.init(testing.random_seed);
|
||||||
const random = prng.random();
|
const random = prng.random();
|
||||||
|
|||||||
@ -1,3 +1,12 @@
|
|||||||
|
const Decl = @This();
|
||||||
|
const std = @import("std");
|
||||||
|
const Ast = std.zig.Ast;
|
||||||
|
const Walk = @import("Walk.zig");
|
||||||
|
const gpa = std.heap.wasm_allocator;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const log = std.log;
|
||||||
|
const Oom = error{OutOfMemory};
|
||||||
|
|
||||||
ast_node: Ast.Node.Index,
|
ast_node: Ast.Node.Index,
|
||||||
file: Walk.File.Index,
|
file: Walk.File.Index,
|
||||||
/// The decl whose namespace this is in.
|
/// The decl whose namespace this is in.
|
||||||
@ -215,12 +224,3 @@ pub fn find(search_string: []const u8) Decl.Index {
|
|||||||
}
|
}
|
||||||
return current_decl_index;
|
return current_decl_index;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Decl = @This();
|
|
||||||
const std = @import("std");
|
|
||||||
const Ast = std.zig.Ast;
|
|
||||||
const Walk = @import("Walk.zig");
|
|
||||||
const gpa = std.heap.wasm_allocator;
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
const log = std.log;
|
|
||||||
const Oom = error{OutOfMemory};
|
|
||||||
|
|||||||
@ -1,4 +1,15 @@
|
|||||||
//! Find and annotate identifiers with links to their declarations.
|
//! Find and annotate identifiers with links to their declarations.
|
||||||
|
|
||||||
|
const Walk = @This();
|
||||||
|
const std = @import("std");
|
||||||
|
const Ast = std.zig.Ast;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const log = std.log;
|
||||||
|
const gpa = std.heap.wasm_allocator;
|
||||||
|
const Oom = error{OutOfMemory};
|
||||||
|
|
||||||
|
pub const Decl = @import("Decl.zig");
|
||||||
|
|
||||||
pub var files: std.StringArrayHashMapUnmanaged(File) = .{};
|
pub var files: std.StringArrayHashMapUnmanaged(File) = .{};
|
||||||
pub var decls: std.ArrayListUnmanaged(Decl) = .{};
|
pub var decls: std.ArrayListUnmanaged(Decl) = .{};
|
||||||
pub var modules: std.StringArrayHashMapUnmanaged(File.Index) = .{};
|
pub var modules: std.StringArrayHashMapUnmanaged(File.Index) = .{};
|
||||||
@ -1120,15 +1131,6 @@ pub fn isPrimitiveNonType(name: []const u8) bool {
|
|||||||
// try w.root();
|
// try w.root();
|
||||||
//}
|
//}
|
||||||
|
|
||||||
const Walk = @This();
|
|
||||||
const std = @import("std");
|
|
||||||
const Ast = std.zig.Ast;
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
const Decl = @import("Decl.zig");
|
|
||||||
const log = std.log;
|
|
||||||
const gpa = std.heap.wasm_allocator;
|
|
||||||
const Oom = error{OutOfMemory};
|
|
||||||
|
|
||||||
fn shrinkToFit(m: anytype) void {
|
fn shrinkToFit(m: anytype) void {
|
||||||
m.shrinkAndFree(gpa, m.entries.len);
|
m.shrinkAndFree(gpa, m.entries.len);
|
||||||
}
|
}
|
||||||
|
|||||||
412
lib/docs/wasm/html_render.zig
Normal file
412
lib/docs/wasm/html_render.zig
Normal file
@ -0,0 +1,412 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Ast = std.zig.Ast;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const Walk = @import("Walk");
|
||||||
|
const Decl = Walk.Decl;
|
||||||
|
|
||||||
|
const gpa = std.heap.wasm_allocator;
|
||||||
|
const Oom = error{OutOfMemory};
|
||||||
|
|
||||||
|
/// Delete this to find out where URL escaping needs to be added.
|
||||||
|
pub const missing_feature_url_escape = true;
|
||||||
|
|
||||||
|
pub const RenderSourceOptions = struct {
|
||||||
|
skip_doc_comments: bool = false,
|
||||||
|
skip_comments: bool = false,
|
||||||
|
collapse_whitespace: bool = false,
|
||||||
|
fn_link: Decl.Index = .none,
|
||||||
|
/// Assumed to be sorted ascending.
|
||||||
|
source_location_annotations: []const Annotation = &.{},
|
||||||
|
/// Concatenated with dom_id.
|
||||||
|
annotation_prefix: []const u8 = "l",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Annotation = struct {
|
||||||
|
file_byte_offset: u32,
|
||||||
|
/// Concatenated with annotation_prefix.
|
||||||
|
dom_id: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn fileSourceHtml(
|
||||||
|
file_index: Walk.File.Index,
|
||||||
|
out: *std.ArrayListUnmanaged(u8),
|
||||||
|
root_node: Ast.Node.Index,
|
||||||
|
options: RenderSourceOptions,
|
||||||
|
) !void {
|
||||||
|
const ast = file_index.get_ast();
|
||||||
|
const file = file_index.get();
|
||||||
|
|
||||||
|
const g = struct {
|
||||||
|
var field_access_buffer: std.ArrayListUnmanaged(u8) = .{};
|
||||||
|
};
|
||||||
|
|
||||||
|
const token_tags = ast.tokens.items(.tag);
|
||||||
|
const token_starts = ast.tokens.items(.start);
|
||||||
|
const main_tokens = ast.nodes.items(.main_token);
|
||||||
|
|
||||||
|
const start_token = ast.firstToken(root_node);
|
||||||
|
const end_token = ast.lastToken(root_node) + 1;
|
||||||
|
|
||||||
|
var cursor: usize = token_starts[start_token];
|
||||||
|
|
||||||
|
var indent: usize = 0;
|
||||||
|
if (std.mem.lastIndexOf(u8, ast.source[0..cursor], "\n")) |newline_index| {
|
||||||
|
for (ast.source[newline_index + 1 .. cursor]) |c| {
|
||||||
|
if (c == ' ') {
|
||||||
|
indent += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var next_annotate_index: usize = 0;
|
||||||
|
|
||||||
|
for (
|
||||||
|
token_tags[start_token..end_token],
|
||||||
|
token_starts[start_token..end_token],
|
||||||
|
start_token..,
|
||||||
|
) |tag, start, token_index| {
|
||||||
|
const between = ast.source[cursor..start];
|
||||||
|
if (std.mem.trim(u8, between, " \t\r\n").len > 0) {
|
||||||
|
if (!options.skip_comments) {
|
||||||
|
try out.appendSlice(gpa, "<span class=\"tok-comment\">");
|
||||||
|
try appendUnindented(out, between, indent);
|
||||||
|
try out.appendSlice(gpa, "</span>");
|
||||||
|
}
|
||||||
|
} else if (between.len > 0) {
|
||||||
|
if (options.collapse_whitespace) {
|
||||||
|
if (out.items.len > 0 and out.items[out.items.len - 1] != ' ')
|
||||||
|
try out.append(gpa, ' ');
|
||||||
|
} else {
|
||||||
|
try appendUnindented(out, between, indent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tag == .eof) break;
|
||||||
|
const slice = ast.tokenSlice(token_index);
|
||||||
|
cursor = start + slice.len;
|
||||||
|
|
||||||
|
// Insert annotations.
|
||||||
|
while (true) {
|
||||||
|
if (next_annotate_index >= options.source_location_annotations.len) break;
|
||||||
|
const next_annotation = options.source_location_annotations[next_annotate_index];
|
||||||
|
if (cursor <= next_annotation.file_byte_offset) break;
|
||||||
|
try out.writer(gpa).print("<span id=\"{s}{d}\"></span>", .{
|
||||||
|
options.annotation_prefix, next_annotation.dom_id,
|
||||||
|
});
|
||||||
|
next_annotate_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (tag) {
|
||||||
|
.eof => unreachable,
|
||||||
|
|
||||||
|
.keyword_addrspace,
|
||||||
|
.keyword_align,
|
||||||
|
.keyword_and,
|
||||||
|
.keyword_asm,
|
||||||
|
.keyword_async,
|
||||||
|
.keyword_await,
|
||||||
|
.keyword_break,
|
||||||
|
.keyword_catch,
|
||||||
|
.keyword_comptime,
|
||||||
|
.keyword_const,
|
||||||
|
.keyword_continue,
|
||||||
|
.keyword_defer,
|
||||||
|
.keyword_else,
|
||||||
|
.keyword_enum,
|
||||||
|
.keyword_errdefer,
|
||||||
|
.keyword_error,
|
||||||
|
.keyword_export,
|
||||||
|
.keyword_extern,
|
||||||
|
.keyword_for,
|
||||||
|
.keyword_if,
|
||||||
|
.keyword_inline,
|
||||||
|
.keyword_noalias,
|
||||||
|
.keyword_noinline,
|
||||||
|
.keyword_nosuspend,
|
||||||
|
.keyword_opaque,
|
||||||
|
.keyword_or,
|
||||||
|
.keyword_orelse,
|
||||||
|
.keyword_packed,
|
||||||
|
.keyword_anyframe,
|
||||||
|
.keyword_pub,
|
||||||
|
.keyword_resume,
|
||||||
|
.keyword_return,
|
||||||
|
.keyword_linksection,
|
||||||
|
.keyword_callconv,
|
||||||
|
.keyword_struct,
|
||||||
|
.keyword_suspend,
|
||||||
|
.keyword_switch,
|
||||||
|
.keyword_test,
|
||||||
|
.keyword_threadlocal,
|
||||||
|
.keyword_try,
|
||||||
|
.keyword_union,
|
||||||
|
.keyword_unreachable,
|
||||||
|
.keyword_usingnamespace,
|
||||||
|
.keyword_var,
|
||||||
|
.keyword_volatile,
|
||||||
|
.keyword_allowzero,
|
||||||
|
.keyword_while,
|
||||||
|
.keyword_anytype,
|
||||||
|
.keyword_fn,
|
||||||
|
=> {
|
||||||
|
try out.appendSlice(gpa, "<span class=\"tok-kw\">");
|
||||||
|
try appendEscaped(out, slice);
|
||||||
|
try out.appendSlice(gpa, "</span>");
|
||||||
|
},
|
||||||
|
|
||||||
|
.string_literal,
|
||||||
|
.char_literal,
|
||||||
|
.multiline_string_literal_line,
|
||||||
|
=> {
|
||||||
|
try out.appendSlice(gpa, "<span class=\"tok-str\">");
|
||||||
|
try appendEscaped(out, slice);
|
||||||
|
try out.appendSlice(gpa, "</span>");
|
||||||
|
},
|
||||||
|
|
||||||
|
.builtin => {
|
||||||
|
try out.appendSlice(gpa, "<span class=\"tok-builtin\">");
|
||||||
|
try appendEscaped(out, slice);
|
||||||
|
try out.appendSlice(gpa, "</span>");
|
||||||
|
},
|
||||||
|
|
||||||
|
.doc_comment,
|
||||||
|
.container_doc_comment,
|
||||||
|
=> {
|
||||||
|
if (!options.skip_doc_comments) {
|
||||||
|
try out.appendSlice(gpa, "<span class=\"tok-comment\">");
|
||||||
|
try appendEscaped(out, slice);
|
||||||
|
try out.appendSlice(gpa, "</span>");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
.identifier => i: {
|
||||||
|
if (options.fn_link != .none) {
|
||||||
|
const fn_link = options.fn_link.get();
|
||||||
|
const fn_token = main_tokens[fn_link.ast_node];
|
||||||
|
if (token_index == fn_token + 1) {
|
||||||
|
try out.appendSlice(gpa, "<a class=\"tok-fn\" href=\"#");
|
||||||
|
_ = missing_feature_url_escape;
|
||||||
|
try fn_link.fqn(out);
|
||||||
|
try out.appendSlice(gpa, "\">");
|
||||||
|
try appendEscaped(out, slice);
|
||||||
|
try out.appendSlice(gpa, "</a>");
|
||||||
|
break :i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token_index > 0 and token_tags[token_index - 1] == .keyword_fn) {
|
||||||
|
try out.appendSlice(gpa, "<span class=\"tok-fn\">");
|
||||||
|
try appendEscaped(out, slice);
|
||||||
|
try out.appendSlice(gpa, "</span>");
|
||||||
|
break :i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Walk.isPrimitiveNonType(slice)) {
|
||||||
|
try out.appendSlice(gpa, "<span class=\"tok-null\">");
|
||||||
|
try appendEscaped(out, slice);
|
||||||
|
try out.appendSlice(gpa, "</span>");
|
||||||
|
break :i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.zig.primitives.isPrimitive(slice)) {
|
||||||
|
try out.appendSlice(gpa, "<span class=\"tok-type\">");
|
||||||
|
try appendEscaped(out, slice);
|
||||||
|
try out.appendSlice(gpa, "</span>");
|
||||||
|
break :i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.token_parents.get(token_index)) |field_access_node| {
|
||||||
|
g.field_access_buffer.clearRetainingCapacity();
|
||||||
|
try walkFieldAccesses(file_index, &g.field_access_buffer, field_access_node);
|
||||||
|
if (g.field_access_buffer.items.len > 0) {
|
||||||
|
try out.appendSlice(gpa, "<a href=\"#");
|
||||||
|
_ = missing_feature_url_escape;
|
||||||
|
try out.appendSlice(gpa, g.field_access_buffer.items);
|
||||||
|
try out.appendSlice(gpa, "\">");
|
||||||
|
try appendEscaped(out, slice);
|
||||||
|
try out.appendSlice(gpa, "</a>");
|
||||||
|
} else {
|
||||||
|
try appendEscaped(out, slice);
|
||||||
|
}
|
||||||
|
break :i;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
g.field_access_buffer.clearRetainingCapacity();
|
||||||
|
try resolveIdentLink(file_index, &g.field_access_buffer, token_index);
|
||||||
|
if (g.field_access_buffer.items.len > 0) {
|
||||||
|
try out.appendSlice(gpa, "<a href=\"#");
|
||||||
|
_ = missing_feature_url_escape;
|
||||||
|
try out.appendSlice(gpa, g.field_access_buffer.items);
|
||||||
|
try out.appendSlice(gpa, "\">");
|
||||||
|
try appendEscaped(out, slice);
|
||||||
|
try out.appendSlice(gpa, "</a>");
|
||||||
|
break :i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try appendEscaped(out, slice);
|
||||||
|
},
|
||||||
|
|
||||||
|
.number_literal => {
|
||||||
|
try out.appendSlice(gpa, "<span class=\"tok-number\">");
|
||||||
|
try appendEscaped(out, slice);
|
||||||
|
try out.appendSlice(gpa, "</span>");
|
||||||
|
},
|
||||||
|
|
||||||
|
.bang,
|
||||||
|
.pipe,
|
||||||
|
.pipe_pipe,
|
||||||
|
.pipe_equal,
|
||||||
|
.equal,
|
||||||
|
.equal_equal,
|
||||||
|
.equal_angle_bracket_right,
|
||||||
|
.bang_equal,
|
||||||
|
.l_paren,
|
||||||
|
.r_paren,
|
||||||
|
.semicolon,
|
||||||
|
.percent,
|
||||||
|
.percent_equal,
|
||||||
|
.l_brace,
|
||||||
|
.r_brace,
|
||||||
|
.l_bracket,
|
||||||
|
.r_bracket,
|
||||||
|
.period,
|
||||||
|
.period_asterisk,
|
||||||
|
.ellipsis2,
|
||||||
|
.ellipsis3,
|
||||||
|
.caret,
|
||||||
|
.caret_equal,
|
||||||
|
.plus,
|
||||||
|
.plus_plus,
|
||||||
|
.plus_equal,
|
||||||
|
.plus_percent,
|
||||||
|
.plus_percent_equal,
|
||||||
|
.plus_pipe,
|
||||||
|
.plus_pipe_equal,
|
||||||
|
.minus,
|
||||||
|
.minus_equal,
|
||||||
|
.minus_percent,
|
||||||
|
.minus_percent_equal,
|
||||||
|
.minus_pipe,
|
||||||
|
.minus_pipe_equal,
|
||||||
|
.asterisk,
|
||||||
|
.asterisk_equal,
|
||||||
|
.asterisk_asterisk,
|
||||||
|
.asterisk_percent,
|
||||||
|
.asterisk_percent_equal,
|
||||||
|
.asterisk_pipe,
|
||||||
|
.asterisk_pipe_equal,
|
||||||
|
.arrow,
|
||||||
|
.colon,
|
||||||
|
.slash,
|
||||||
|
.slash_equal,
|
||||||
|
.comma,
|
||||||
|
.ampersand,
|
||||||
|
.ampersand_equal,
|
||||||
|
.question_mark,
|
||||||
|
.angle_bracket_left,
|
||||||
|
.angle_bracket_left_equal,
|
||||||
|
.angle_bracket_angle_bracket_left,
|
||||||
|
.angle_bracket_angle_bracket_left_equal,
|
||||||
|
.angle_bracket_angle_bracket_left_pipe,
|
||||||
|
.angle_bracket_angle_bracket_left_pipe_equal,
|
||||||
|
.angle_bracket_right,
|
||||||
|
.angle_bracket_right_equal,
|
||||||
|
.angle_bracket_angle_bracket_right,
|
||||||
|
.angle_bracket_angle_bracket_right_equal,
|
||||||
|
.tilde,
|
||||||
|
=> try appendEscaped(out, slice),
|
||||||
|
|
||||||
|
.invalid, .invalid_periodasterisks => return error.InvalidToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn appendUnindented(out: *std.ArrayListUnmanaged(u8), s: []const u8, indent: usize) !void {
|
||||||
|
var it = std.mem.splitScalar(u8, s, '\n');
|
||||||
|
var is_first_line = true;
|
||||||
|
while (it.next()) |line| {
|
||||||
|
if (is_first_line) {
|
||||||
|
try appendEscaped(out, line);
|
||||||
|
is_first_line = false;
|
||||||
|
} else {
|
||||||
|
try out.appendSlice(gpa, "\n");
|
||||||
|
try appendEscaped(out, unindent(line, indent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn appendEscaped(out: *std.ArrayListUnmanaged(u8), s: []const u8) !void {
|
||||||
|
for (s) |c| {
|
||||||
|
try out.ensureUnusedCapacity(gpa, 6);
|
||||||
|
switch (c) {
|
||||||
|
'&' => out.appendSliceAssumeCapacity("&"),
|
||||||
|
'<' => out.appendSliceAssumeCapacity("<"),
|
||||||
|
'>' => out.appendSliceAssumeCapacity(">"),
|
||||||
|
'"' => out.appendSliceAssumeCapacity("""),
|
||||||
|
else => out.appendAssumeCapacity(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walkFieldAccesses(
|
||||||
|
file_index: Walk.File.Index,
|
||||||
|
out: *std.ArrayListUnmanaged(u8),
|
||||||
|
node: Ast.Node.Index,
|
||||||
|
) Oom!void {
|
||||||
|
const ast = file_index.get_ast();
|
||||||
|
const node_tags = ast.nodes.items(.tag);
|
||||||
|
assert(node_tags[node] == .field_access);
|
||||||
|
const node_datas = ast.nodes.items(.data);
|
||||||
|
const main_tokens = ast.nodes.items(.main_token);
|
||||||
|
const object_node = node_datas[node].lhs;
|
||||||
|
const dot_token = main_tokens[node];
|
||||||
|
const field_ident = dot_token + 1;
|
||||||
|
switch (node_tags[object_node]) {
|
||||||
|
.identifier => {
|
||||||
|
const lhs_ident = main_tokens[object_node];
|
||||||
|
try resolveIdentLink(file_index, out, lhs_ident);
|
||||||
|
},
|
||||||
|
.field_access => {
|
||||||
|
try walkFieldAccesses(file_index, out, object_node);
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
if (out.items.len > 0) {
|
||||||
|
try out.append(gpa, '.');
|
||||||
|
try out.appendSlice(gpa, ast.tokenSlice(field_ident));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolveIdentLink(
|
||||||
|
file_index: Walk.File.Index,
|
||||||
|
out: *std.ArrayListUnmanaged(u8),
|
||||||
|
ident_token: Ast.TokenIndex,
|
||||||
|
) Oom!void {
|
||||||
|
const decl_index = file_index.get().lookup_token(ident_token);
|
||||||
|
if (decl_index == .none) return;
|
||||||
|
try resolveDeclLink(decl_index, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unindent(s: []const u8, indent: usize) []const u8 {
|
||||||
|
var indent_idx: usize = 0;
|
||||||
|
for (s) |c| {
|
||||||
|
if (c == ' ' and indent_idx < indent) {
|
||||||
|
indent_idx += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s[indent_idx..];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolveDeclLink(decl_index: Decl.Index, out: *std.ArrayListUnmanaged(u8)) Oom!void {
|
||||||
|
const decl = decl_index.get();
|
||||||
|
switch (decl.categorize()) {
|
||||||
|
.alias => |alias_decl| try alias_decl.get().fqn(out),
|
||||||
|
else => try decl.fqn(out),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +1,17 @@
|
|||||||
/// Delete this to find out where URL escaping needs to be added.
|
|
||||||
const missing_feature_url_escape = true;
|
|
||||||
|
|
||||||
const gpa = std.heap.wasm_allocator;
|
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const log = std.log;
|
const log = std.log;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Ast = std.zig.Ast;
|
const Ast = std.zig.Ast;
|
||||||
const Walk = @import("Walk.zig");
|
const Walk = @import("Walk");
|
||||||
const markdown = @import("markdown.zig");
|
const markdown = @import("markdown.zig");
|
||||||
const Decl = @import("Decl.zig");
|
const Decl = Walk.Decl;
|
||||||
|
|
||||||
|
const fileSourceHtml = @import("html_render.zig").fileSourceHtml;
|
||||||
|
const appendEscaped = @import("html_render.zig").appendEscaped;
|
||||||
|
const resolveDeclLink = @import("html_render.zig").resolveDeclLink;
|
||||||
|
const missing_feature_url_escape = @import("html_render.zig").missing_feature_url_escape;
|
||||||
|
|
||||||
|
const gpa = std.heap.wasm_allocator;
|
||||||
|
|
||||||
const js = struct {
|
const js = struct {
|
||||||
extern "js" fn log(ptr: [*]const u8, len: usize) void;
|
extern "js" fn log(ptr: [*]const u8, len: usize) void;
|
||||||
@ -53,7 +55,7 @@ export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
|
|||||||
const tar_bytes = tar_ptr[0..tar_len];
|
const tar_bytes = tar_ptr[0..tar_len];
|
||||||
//log.debug("received {d} bytes of tar file", .{tar_bytes.len});
|
//log.debug("received {d} bytes of tar file", .{tar_bytes.len});
|
||||||
|
|
||||||
unpack_inner(tar_bytes) catch |err| {
|
unpackInner(tar_bytes) catch |err| {
|
||||||
fatal("unable to unpack tar: {s}", .{@errorName(err)});
|
fatal("unable to unpack tar: {s}", .{@errorName(err)});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -439,7 +441,7 @@ fn decl_field_html_fallible(
|
|||||||
const decl = decl_index.get();
|
const decl = decl_index.get();
|
||||||
const ast = decl.file.get_ast();
|
const ast = decl.file.get_ast();
|
||||||
try out.appendSlice(gpa, "<pre><code>");
|
try out.appendSlice(gpa, "<pre><code>");
|
||||||
try file_source_html(decl.file, out, field_node, .{});
|
try fileSourceHtml(decl.file, out, field_node, .{});
|
||||||
try out.appendSlice(gpa, "</code></pre>");
|
try out.appendSlice(gpa, "</code></pre>");
|
||||||
|
|
||||||
const field = ast.fullContainerField(field_node).?;
|
const field = ast.fullContainerField(field_node).?;
|
||||||
@ -478,7 +480,7 @@ fn decl_param_html_fallible(
|
|||||||
try out.appendSlice(gpa, "<pre><code>");
|
try out.appendSlice(gpa, "<pre><code>");
|
||||||
try appendEscaped(out, name);
|
try appendEscaped(out, name);
|
||||||
try out.appendSlice(gpa, ": ");
|
try out.appendSlice(gpa, ": ");
|
||||||
try file_source_html(decl.file, out, param_node, .{});
|
try fileSourceHtml(decl.file, out, param_node, .{});
|
||||||
try out.appendSlice(gpa, "</code></pre>");
|
try out.appendSlice(gpa, "</code></pre>");
|
||||||
|
|
||||||
if (ast.tokens.items(.tag)[first_doc_comment] == .doc_comment) {
|
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
|
|||||||
};
|
};
|
||||||
|
|
||||||
string_result.clearRetainingCapacity();
|
string_result.clearRetainingCapacity();
|
||||||
file_source_html(decl.file, &string_result, proto_node, .{
|
fileSourceHtml(decl.file, &string_result, proto_node, .{
|
||||||
.skip_doc_comments = true,
|
.skip_doc_comments = true,
|
||||||
.skip_comments = true,
|
.skip_comments = true,
|
||||||
.collapse_whitespace = true,
|
.collapse_whitespace = true,
|
||||||
@ -521,7 +523,7 @@ export fn decl_source_html(decl_index: Decl.Index) String {
|
|||||||
const decl = decl_index.get();
|
const decl = decl_index.get();
|
||||||
|
|
||||||
string_result.clearRetainingCapacity();
|
string_result.clearRetainingCapacity();
|
||||||
file_source_html(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
|
fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
|
||||||
fatal("unable to render source: {s}", .{@errorName(err)});
|
fatal("unable to render source: {s}", .{@errorName(err)});
|
||||||
};
|
};
|
||||||
return String.init(string_result.items);
|
return String.init(string_result.items);
|
||||||
@ -533,7 +535,7 @@ export fn decl_doctest_html(decl_index: Decl.Index) String {
|
|||||||
return String.init("");
|
return String.init("");
|
||||||
|
|
||||||
string_result.clearRetainingCapacity();
|
string_result.clearRetainingCapacity();
|
||||||
file_source_html(decl.file, &string_result, doctest_ast_node, .{}) catch |err| {
|
fileSourceHtml(decl.file, &string_result, doctest_ast_node, .{}) catch |err| {
|
||||||
fatal("unable to render source: {s}", .{@errorName(err)});
|
fatal("unable to render source: {s}", .{@errorName(err)});
|
||||||
};
|
};
|
||||||
return String.init(string_result.items);
|
return String.init(string_result.items);
|
||||||
@ -691,7 +693,7 @@ fn render_docs(
|
|||||||
const content = doc.string(data.text.content);
|
const content = doc.string(data.text.content);
|
||||||
if (resolve_decl_path(r.context, content)) |resolved_decl_index| {
|
if (resolve_decl_path(r.context, content)) |resolved_decl_index| {
|
||||||
g.link_buffer.clearRetainingCapacity();
|
g.link_buffer.clearRetainingCapacity();
|
||||||
try resolve_decl_link(resolved_decl_index, &g.link_buffer);
|
try resolveDeclLink(resolved_decl_index, &g.link_buffer);
|
||||||
|
|
||||||
try writer.writeAll("<a href=\"#");
|
try writer.writeAll("<a href=\"#");
|
||||||
_ = missing_feature_url_escape;
|
_ = missing_feature_url_escape;
|
||||||
@ -734,7 +736,7 @@ export fn decl_type_html(decl_index: Decl.Index) String {
|
|||||||
if (ast.fullVarDecl(decl.ast_node)) |var_decl| {
|
if (ast.fullVarDecl(decl.ast_node)) |var_decl| {
|
||||||
if (var_decl.ast.type_node != 0) {
|
if (var_decl.ast.type_node != 0) {
|
||||||
string_result.appendSlice(gpa, "<code>") catch @panic("OOM");
|
string_result.appendSlice(gpa, "<code>") catch @panic("OOM");
|
||||||
file_source_html(decl.file, &string_result, var_decl.ast.type_node, .{
|
fileSourceHtml(decl.file, &string_result, var_decl.ast.type_node, .{
|
||||||
.skip_comments = true,
|
.skip_comments = true,
|
||||||
.collapse_whitespace = true,
|
.collapse_whitespace = true,
|
||||||
}) catch |e| {
|
}) catch |e| {
|
||||||
@ -750,7 +752,7 @@ export fn decl_type_html(decl_index: Decl.Index) String {
|
|||||||
|
|
||||||
const Oom = error{OutOfMemory};
|
const Oom = error{OutOfMemory};
|
||||||
|
|
||||||
fn unpack_inner(tar_bytes: []u8) !void {
|
fn unpackInner(tar_bytes: []u8) !void {
|
||||||
var fbs = std.io.fixedBufferStream(tar_bytes);
|
var fbs = std.io.fixedBufferStream(tar_bytes);
|
||||||
var file_name_buffer: [1024]u8 = undefined;
|
var file_name_buffer: [1024]u8 = undefined;
|
||||||
var link_name_buffer: [1024]u8 = undefined;
|
var link_name_buffer: [1024]u8 = undefined;
|
||||||
@ -902,382 +904,6 @@ export fn namespace_members(parent: Decl.Index, include_private: bool) Slice(Dec
|
|||||||
return Slice(Decl.Index).init(g.members.items);
|
return Slice(Decl.Index).init(g.members.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
const RenderSourceOptions = struct {
|
|
||||||
skip_doc_comments: bool = false,
|
|
||||||
skip_comments: bool = false,
|
|
||||||
collapse_whitespace: bool = false,
|
|
||||||
fn_link: Decl.Index = .none,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn file_source_html(
|
|
||||||
file_index: Walk.File.Index,
|
|
||||||
out: *std.ArrayListUnmanaged(u8),
|
|
||||||
root_node: Ast.Node.Index,
|
|
||||||
options: RenderSourceOptions,
|
|
||||||
) !void {
|
|
||||||
const ast = file_index.get_ast();
|
|
||||||
const file = file_index.get();
|
|
||||||
|
|
||||||
const g = struct {
|
|
||||||
var field_access_buffer: std.ArrayListUnmanaged(u8) = .{};
|
|
||||||
};
|
|
||||||
|
|
||||||
const token_tags = ast.tokens.items(.tag);
|
|
||||||
const token_starts = ast.tokens.items(.start);
|
|
||||||
const main_tokens = ast.nodes.items(.main_token);
|
|
||||||
|
|
||||||
const start_token = ast.firstToken(root_node);
|
|
||||||
const end_token = ast.lastToken(root_node) + 1;
|
|
||||||
|
|
||||||
var cursor: usize = token_starts[start_token];
|
|
||||||
|
|
||||||
var indent: usize = 0;
|
|
||||||
if (std.mem.lastIndexOf(u8, ast.source[0..cursor], "\n")) |newline_index| {
|
|
||||||
for (ast.source[newline_index + 1 .. cursor]) |c| {
|
|
||||||
if (c == ' ') {
|
|
||||||
indent += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (
|
|
||||||
token_tags[start_token..end_token],
|
|
||||||
token_starts[start_token..end_token],
|
|
||||||
start_token..,
|
|
||||||
) |tag, start, token_index| {
|
|
||||||
const between = ast.source[cursor..start];
|
|
||||||
if (std.mem.trim(u8, between, " \t\r\n").len > 0) {
|
|
||||||
if (!options.skip_comments) {
|
|
||||||
try out.appendSlice(gpa, "<span class=\"tok-comment\">");
|
|
||||||
try appendUnindented(out, between, indent);
|
|
||||||
try out.appendSlice(gpa, "</span>");
|
|
||||||
}
|
|
||||||
} else if (between.len > 0) {
|
|
||||||
if (options.collapse_whitespace) {
|
|
||||||
if (out.items.len > 0 and out.items[out.items.len - 1] != ' ')
|
|
||||||
try out.append(gpa, ' ');
|
|
||||||
} else {
|
|
||||||
try appendUnindented(out, between, indent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (tag == .eof) break;
|
|
||||||
const slice = ast.tokenSlice(token_index);
|
|
||||||
cursor = start + slice.len;
|
|
||||||
switch (tag) {
|
|
||||||
.eof => unreachable,
|
|
||||||
|
|
||||||
.keyword_addrspace,
|
|
||||||
.keyword_align,
|
|
||||||
.keyword_and,
|
|
||||||
.keyword_asm,
|
|
||||||
.keyword_async,
|
|
||||||
.keyword_await,
|
|
||||||
.keyword_break,
|
|
||||||
.keyword_catch,
|
|
||||||
.keyword_comptime,
|
|
||||||
.keyword_const,
|
|
||||||
.keyword_continue,
|
|
||||||
.keyword_defer,
|
|
||||||
.keyword_else,
|
|
||||||
.keyword_enum,
|
|
||||||
.keyword_errdefer,
|
|
||||||
.keyword_error,
|
|
||||||
.keyword_export,
|
|
||||||
.keyword_extern,
|
|
||||||
.keyword_for,
|
|
||||||
.keyword_if,
|
|
||||||
.keyword_inline,
|
|
||||||
.keyword_noalias,
|
|
||||||
.keyword_noinline,
|
|
||||||
.keyword_nosuspend,
|
|
||||||
.keyword_opaque,
|
|
||||||
.keyword_or,
|
|
||||||
.keyword_orelse,
|
|
||||||
.keyword_packed,
|
|
||||||
.keyword_anyframe,
|
|
||||||
.keyword_pub,
|
|
||||||
.keyword_resume,
|
|
||||||
.keyword_return,
|
|
||||||
.keyword_linksection,
|
|
||||||
.keyword_callconv,
|
|
||||||
.keyword_struct,
|
|
||||||
.keyword_suspend,
|
|
||||||
.keyword_switch,
|
|
||||||
.keyword_test,
|
|
||||||
.keyword_threadlocal,
|
|
||||||
.keyword_try,
|
|
||||||
.keyword_union,
|
|
||||||
.keyword_unreachable,
|
|
||||||
.keyword_usingnamespace,
|
|
||||||
.keyword_var,
|
|
||||||
.keyword_volatile,
|
|
||||||
.keyword_allowzero,
|
|
||||||
.keyword_while,
|
|
||||||
.keyword_anytype,
|
|
||||||
.keyword_fn,
|
|
||||||
=> {
|
|
||||||
try out.appendSlice(gpa, "<span class=\"tok-kw\">");
|
|
||||||
try appendEscaped(out, slice);
|
|
||||||
try out.appendSlice(gpa, "</span>");
|
|
||||||
},
|
|
||||||
|
|
||||||
.string_literal,
|
|
||||||
.char_literal,
|
|
||||||
.multiline_string_literal_line,
|
|
||||||
=> {
|
|
||||||
try out.appendSlice(gpa, "<span class=\"tok-str\">");
|
|
||||||
try appendEscaped(out, slice);
|
|
||||||
try out.appendSlice(gpa, "</span>");
|
|
||||||
},
|
|
||||||
|
|
||||||
.builtin => {
|
|
||||||
try out.appendSlice(gpa, "<span class=\"tok-builtin\">");
|
|
||||||
try appendEscaped(out, slice);
|
|
||||||
try out.appendSlice(gpa, "</span>");
|
|
||||||
},
|
|
||||||
|
|
||||||
.doc_comment,
|
|
||||||
.container_doc_comment,
|
|
||||||
=> {
|
|
||||||
if (!options.skip_doc_comments) {
|
|
||||||
try out.appendSlice(gpa, "<span class=\"tok-comment\">");
|
|
||||||
try appendEscaped(out, slice);
|
|
||||||
try out.appendSlice(gpa, "</span>");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
.identifier => i: {
|
|
||||||
if (options.fn_link != .none) {
|
|
||||||
const fn_link = options.fn_link.get();
|
|
||||||
const fn_token = main_tokens[fn_link.ast_node];
|
|
||||||
if (token_index == fn_token + 1) {
|
|
||||||
try out.appendSlice(gpa, "<a class=\"tok-fn\" href=\"#");
|
|
||||||
_ = missing_feature_url_escape;
|
|
||||||
try fn_link.fqn(out);
|
|
||||||
try out.appendSlice(gpa, "\">");
|
|
||||||
try appendEscaped(out, slice);
|
|
||||||
try out.appendSlice(gpa, "</a>");
|
|
||||||
break :i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token_index > 0 and token_tags[token_index - 1] == .keyword_fn) {
|
|
||||||
try out.appendSlice(gpa, "<span class=\"tok-fn\">");
|
|
||||||
try appendEscaped(out, slice);
|
|
||||||
try out.appendSlice(gpa, "</span>");
|
|
||||||
break :i;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Walk.isPrimitiveNonType(slice)) {
|
|
||||||
try out.appendSlice(gpa, "<span class=\"tok-null\">");
|
|
||||||
try appendEscaped(out, slice);
|
|
||||||
try out.appendSlice(gpa, "</span>");
|
|
||||||
break :i;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.zig.primitives.isPrimitive(slice)) {
|
|
||||||
try out.appendSlice(gpa, "<span class=\"tok-type\">");
|
|
||||||
try appendEscaped(out, slice);
|
|
||||||
try out.appendSlice(gpa, "</span>");
|
|
||||||
break :i;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.token_parents.get(token_index)) |field_access_node| {
|
|
||||||
g.field_access_buffer.clearRetainingCapacity();
|
|
||||||
try walk_field_accesses(file_index, &g.field_access_buffer, field_access_node);
|
|
||||||
if (g.field_access_buffer.items.len > 0) {
|
|
||||||
try out.appendSlice(gpa, "<a href=\"#");
|
|
||||||
_ = missing_feature_url_escape;
|
|
||||||
try out.appendSlice(gpa, g.field_access_buffer.items);
|
|
||||||
try out.appendSlice(gpa, "\">");
|
|
||||||
try appendEscaped(out, slice);
|
|
||||||
try out.appendSlice(gpa, "</a>");
|
|
||||||
} else {
|
|
||||||
try appendEscaped(out, slice);
|
|
||||||
}
|
|
||||||
break :i;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
g.field_access_buffer.clearRetainingCapacity();
|
|
||||||
try resolve_ident_link(file_index, &g.field_access_buffer, token_index);
|
|
||||||
if (g.field_access_buffer.items.len > 0) {
|
|
||||||
try out.appendSlice(gpa, "<a href=\"#");
|
|
||||||
_ = missing_feature_url_escape;
|
|
||||||
try out.appendSlice(gpa, g.field_access_buffer.items);
|
|
||||||
try out.appendSlice(gpa, "\">");
|
|
||||||
try appendEscaped(out, slice);
|
|
||||||
try out.appendSlice(gpa, "</a>");
|
|
||||||
break :i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try appendEscaped(out, slice);
|
|
||||||
},
|
|
||||||
|
|
||||||
.number_literal => {
|
|
||||||
try out.appendSlice(gpa, "<span class=\"tok-number\">");
|
|
||||||
try appendEscaped(out, slice);
|
|
||||||
try out.appendSlice(gpa, "</span>");
|
|
||||||
},
|
|
||||||
|
|
||||||
.bang,
|
|
||||||
.pipe,
|
|
||||||
.pipe_pipe,
|
|
||||||
.pipe_equal,
|
|
||||||
.equal,
|
|
||||||
.equal_equal,
|
|
||||||
.equal_angle_bracket_right,
|
|
||||||
.bang_equal,
|
|
||||||
.l_paren,
|
|
||||||
.r_paren,
|
|
||||||
.semicolon,
|
|
||||||
.percent,
|
|
||||||
.percent_equal,
|
|
||||||
.l_brace,
|
|
||||||
.r_brace,
|
|
||||||
.l_bracket,
|
|
||||||
.r_bracket,
|
|
||||||
.period,
|
|
||||||
.period_asterisk,
|
|
||||||
.ellipsis2,
|
|
||||||
.ellipsis3,
|
|
||||||
.caret,
|
|
||||||
.caret_equal,
|
|
||||||
.plus,
|
|
||||||
.plus_plus,
|
|
||||||
.plus_equal,
|
|
||||||
.plus_percent,
|
|
||||||
.plus_percent_equal,
|
|
||||||
.plus_pipe,
|
|
||||||
.plus_pipe_equal,
|
|
||||||
.minus,
|
|
||||||
.minus_equal,
|
|
||||||
.minus_percent,
|
|
||||||
.minus_percent_equal,
|
|
||||||
.minus_pipe,
|
|
||||||
.minus_pipe_equal,
|
|
||||||
.asterisk,
|
|
||||||
.asterisk_equal,
|
|
||||||
.asterisk_asterisk,
|
|
||||||
.asterisk_percent,
|
|
||||||
.asterisk_percent_equal,
|
|
||||||
.asterisk_pipe,
|
|
||||||
.asterisk_pipe_equal,
|
|
||||||
.arrow,
|
|
||||||
.colon,
|
|
||||||
.slash,
|
|
||||||
.slash_equal,
|
|
||||||
.comma,
|
|
||||||
.ampersand,
|
|
||||||
.ampersand_equal,
|
|
||||||
.question_mark,
|
|
||||||
.angle_bracket_left,
|
|
||||||
.angle_bracket_left_equal,
|
|
||||||
.angle_bracket_angle_bracket_left,
|
|
||||||
.angle_bracket_angle_bracket_left_equal,
|
|
||||||
.angle_bracket_angle_bracket_left_pipe,
|
|
||||||
.angle_bracket_angle_bracket_left_pipe_equal,
|
|
||||||
.angle_bracket_right,
|
|
||||||
.angle_bracket_right_equal,
|
|
||||||
.angle_bracket_angle_bracket_right,
|
|
||||||
.angle_bracket_angle_bracket_right_equal,
|
|
||||||
.tilde,
|
|
||||||
=> try appendEscaped(out, slice),
|
|
||||||
|
|
||||||
.invalid, .invalid_periodasterisks => return error.InvalidToken,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unindent(s: []const u8, indent: usize) []const u8 {
|
|
||||||
var indent_idx: usize = 0;
|
|
||||||
for (s) |c| {
|
|
||||||
if (c == ' ' and indent_idx < indent) {
|
|
||||||
indent_idx += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s[indent_idx..];
|
|
||||||
}
|
|
||||||
|
|
||||||
fn appendUnindented(out: *std.ArrayListUnmanaged(u8), s: []const u8, indent: usize) !void {
|
|
||||||
var it = std.mem.splitScalar(u8, s, '\n');
|
|
||||||
var is_first_line = true;
|
|
||||||
while (it.next()) |line| {
|
|
||||||
if (is_first_line) {
|
|
||||||
try appendEscaped(out, line);
|
|
||||||
is_first_line = false;
|
|
||||||
} else {
|
|
||||||
try out.appendSlice(gpa, "\n");
|
|
||||||
try appendEscaped(out, unindent(line, indent));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_ident_link(
|
|
||||||
file_index: Walk.File.Index,
|
|
||||||
out: *std.ArrayListUnmanaged(u8),
|
|
||||||
ident_token: Ast.TokenIndex,
|
|
||||||
) Oom!void {
|
|
||||||
const decl_index = file_index.get().lookup_token(ident_token);
|
|
||||||
if (decl_index == .none) return;
|
|
||||||
try resolve_decl_link(decl_index, out);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_decl_link(decl_index: Decl.Index, out: *std.ArrayListUnmanaged(u8)) Oom!void {
|
|
||||||
const decl = decl_index.get();
|
|
||||||
switch (decl.categorize()) {
|
|
||||||
.alias => |alias_decl| try alias_decl.get().fqn(out),
|
|
||||||
else => try decl.fqn(out),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn walk_field_accesses(
|
|
||||||
file_index: Walk.File.Index,
|
|
||||||
out: *std.ArrayListUnmanaged(u8),
|
|
||||||
node: Ast.Node.Index,
|
|
||||||
) Oom!void {
|
|
||||||
const ast = file_index.get_ast();
|
|
||||||
const node_tags = ast.nodes.items(.tag);
|
|
||||||
assert(node_tags[node] == .field_access);
|
|
||||||
const node_datas = ast.nodes.items(.data);
|
|
||||||
const main_tokens = ast.nodes.items(.main_token);
|
|
||||||
const object_node = node_datas[node].lhs;
|
|
||||||
const dot_token = main_tokens[node];
|
|
||||||
const field_ident = dot_token + 1;
|
|
||||||
switch (node_tags[object_node]) {
|
|
||||||
.identifier => {
|
|
||||||
const lhs_ident = main_tokens[object_node];
|
|
||||||
try resolve_ident_link(file_index, out, lhs_ident);
|
|
||||||
},
|
|
||||||
.field_access => {
|
|
||||||
try walk_field_accesses(file_index, out, object_node);
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
if (out.items.len > 0) {
|
|
||||||
try out.append(gpa, '.');
|
|
||||||
try out.appendSlice(gpa, ast.tokenSlice(field_ident));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn appendEscaped(out: *std.ArrayListUnmanaged(u8), s: []const u8) !void {
|
|
||||||
for (s) |c| {
|
|
||||||
try out.ensureUnusedCapacity(gpa, 6);
|
|
||||||
switch (c) {
|
|
||||||
'&' => out.appendSliceAssumeCapacity("&"),
|
|
||||||
'<' => out.appendSliceAssumeCapacity("<"),
|
|
||||||
'>' => out.appendSliceAssumeCapacity(">"),
|
|
||||||
'"' => out.appendSliceAssumeCapacity("""),
|
|
||||||
else => out.appendAssumeCapacity(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn count_scalar(haystack: []const u8, needle: u8) usize {
|
fn count_scalar(haystack: []const u8, needle: u8) usize {
|
||||||
var total: usize = 0;
|
var total: usize = 0;
|
||||||
for (haystack) |elem| {
|
for (haystack) |elem| {
|
||||||
|
|||||||
245
lib/fuzzer.zig
245
lib/fuzzer.zig
@ -2,6 +2,8 @@ const builtin = @import("builtin");
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
const fatal = std.process.fatal;
|
||||||
|
const SeenPcsHeader = std.Build.Fuzz.abi.SeenPcsHeader;
|
||||||
|
|
||||||
pub const std_options = .{
|
pub const std_options = .{
|
||||||
.logFn = logOverride,
|
.logFn = logOverride,
|
||||||
@ -15,9 +17,9 @@ fn logOverride(
|
|||||||
comptime format: []const u8,
|
comptime format: []const u8,
|
||||||
args: anytype,
|
args: anytype,
|
||||||
) void {
|
) void {
|
||||||
if (builtin.mode != .Debug) return;
|
|
||||||
const f = if (log_file) |f| f else f: {
|
const f = if (log_file) |f| f else f: {
|
||||||
const f = std.fs.cwd().createFile("libfuzzer.log", .{}) catch @panic("failed to open fuzzer log file");
|
const f = fuzzer.cache_dir.createFile("tmp/libfuzzer.log", .{}) catch
|
||||||
|
@panic("failed to open fuzzer log file");
|
||||||
log_file = f;
|
log_file = f;
|
||||||
break :f f;
|
break :f f;
|
||||||
};
|
};
|
||||||
@ -26,18 +28,19 @@ fn logOverride(
|
|||||||
f.writer().print(prefix1 ++ prefix2 ++ format ++ "\n", args) catch @panic("failed to write to fuzzer log");
|
f.writer().print(prefix1 ++ prefix2 ++ format ++ "\n", args) catch @panic("failed to write to fuzzer log");
|
||||||
}
|
}
|
||||||
|
|
||||||
export threadlocal var __sancov_lowest_stack: usize = 0;
|
export threadlocal var __sancov_lowest_stack: usize = std.math.maxInt(usize);
|
||||||
|
|
||||||
export fn __sanitizer_cov_8bit_counters_init(start: [*]u8, stop: [*]u8) void {
|
var module_count_8bc: usize = 0;
|
||||||
std.log.debug("__sanitizer_cov_8bit_counters_init start={*}, stop={*}", .{ start, stop });
|
var module_count_pcs: usize = 0;
|
||||||
|
|
||||||
|
export fn __sanitizer_cov_8bit_counters_init(start: [*]u8, end: [*]u8) void {
|
||||||
|
assert(@atomicRmw(usize, &module_count_8bc, .Add, 1, .monotonic) == 0);
|
||||||
|
fuzzer.pc_counters = start[0 .. end - start];
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn __sanitizer_cov_pcs_init(pc_start: [*]const usize, pc_end: [*]const usize) void {
|
export fn __sanitizer_cov_pcs_init(start: [*]const Fuzzer.FlaggedPc, end: [*]const Fuzzer.FlaggedPc) void {
|
||||||
std.log.debug("__sanitizer_cov_pcs_init pc_start={*}, pc_end={*}", .{ pc_start, pc_end });
|
assert(@atomicRmw(usize, &module_count_pcs, .Add, 1, .monotonic) == 0);
|
||||||
fuzzer.pc_range = .{
|
fuzzer.flagged_pcs = start[0 .. end - start];
|
||||||
.start = @intFromPtr(pc_start),
|
|
||||||
.end = @intFromPtr(pc_start),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn __sanitizer_cov_trace_const_cmp1(arg1: u8, arg2: u8) void {
|
export fn __sanitizer_cov_trace_const_cmp1(arg1: u8, arg2: u8) void {
|
||||||
@ -102,11 +105,21 @@ const Fuzzer = struct {
|
|||||||
gpa: Allocator,
|
gpa: Allocator,
|
||||||
rng: std.Random.DefaultPrng,
|
rng: std.Random.DefaultPrng,
|
||||||
input: std.ArrayListUnmanaged(u8),
|
input: std.ArrayListUnmanaged(u8),
|
||||||
pc_range: PcRange,
|
flagged_pcs: []const FlaggedPc,
|
||||||
count: usize,
|
pc_counters: []u8,
|
||||||
|
n_runs: usize,
|
||||||
recent_cases: RunMap,
|
recent_cases: RunMap,
|
||||||
deduplicated_runs: usize,
|
/// Data collected from code coverage instrumentation from one execution of
|
||||||
|
/// the test function.
|
||||||
coverage: Coverage,
|
coverage: Coverage,
|
||||||
|
/// Tracks which PCs have been seen across all runs that do not crash the fuzzer process.
|
||||||
|
/// Stored in a memory-mapped file so that it can be shared with other
|
||||||
|
/// processes and viewed while the fuzzer is running.
|
||||||
|
seen_pcs: MemoryMappedList,
|
||||||
|
cache_dir: std.fs.Dir,
|
||||||
|
/// Identifies the file name that will be used to store coverage
|
||||||
|
/// information, available to other processes.
|
||||||
|
coverage_id: u64,
|
||||||
|
|
||||||
const RunMap = std.ArrayHashMapUnmanaged(Run, void, Run.HashContext, false);
|
const RunMap = std.ArrayHashMapUnmanaged(Run, void, Run.HashContext, false);
|
||||||
|
|
||||||
@ -161,9 +174,12 @@ const Fuzzer = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const PcRange = struct {
|
const FlaggedPc = extern struct {
|
||||||
start: usize,
|
addr: usize,
|
||||||
end: usize,
|
flags: packed struct(usize) {
|
||||||
|
entry: bool,
|
||||||
|
_: @Type(.{ .Int = .{ .signedness = .unsigned, .bits = @bitSizeOf(usize) - 1 } }),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Analysis = struct {
|
const Analysis = struct {
|
||||||
@ -171,6 +187,72 @@ const Fuzzer = struct {
|
|||||||
id: Run.Id,
|
id: Run.Id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn init(f: *Fuzzer, cache_dir: std.fs.Dir) !void {
|
||||||
|
const flagged_pcs = f.flagged_pcs;
|
||||||
|
|
||||||
|
f.cache_dir = cache_dir;
|
||||||
|
|
||||||
|
// Choose a file name for the coverage based on a hash of the PCs that will be stored within.
|
||||||
|
const pc_digest = d: {
|
||||||
|
var hasher = std.hash.Wyhash.init(0);
|
||||||
|
for (flagged_pcs) |flagged_pc| {
|
||||||
|
hasher.update(std.mem.asBytes(&flagged_pc.addr));
|
||||||
|
}
|
||||||
|
break :d f.coverage.run_id_hasher.final();
|
||||||
|
};
|
||||||
|
f.coverage_id = pc_digest;
|
||||||
|
const hex_digest = std.fmt.hex(pc_digest);
|
||||||
|
const coverage_file_path = "v/" ++ hex_digest;
|
||||||
|
|
||||||
|
// Layout of this file:
|
||||||
|
// - Header
|
||||||
|
// - list of PC addresses (usize elements)
|
||||||
|
// - list of hit flag, 1 bit per address (stored in u8 elements)
|
||||||
|
const coverage_file = createFileBail(cache_dir, coverage_file_path, .{
|
||||||
|
.read = true,
|
||||||
|
.truncate = false,
|
||||||
|
});
|
||||||
|
defer coverage_file.close();
|
||||||
|
const n_bitset_elems = (flagged_pcs.len + 7) / 8;
|
||||||
|
const bytes_len = @sizeOf(SeenPcsHeader) + flagged_pcs.len * @sizeOf(usize) + n_bitset_elems;
|
||||||
|
const existing_len = coverage_file.getEndPos() catch |err| {
|
||||||
|
fatal("unable to check len of coverage file: {s}", .{@errorName(err)});
|
||||||
|
};
|
||||||
|
if (existing_len == 0) {
|
||||||
|
coverage_file.setEndPos(bytes_len) catch |err| {
|
||||||
|
fatal("unable to set len of coverage file: {s}", .{@errorName(err)});
|
||||||
|
};
|
||||||
|
} else if (existing_len != bytes_len) {
|
||||||
|
fatal("incompatible existing coverage file (differing lengths)", .{});
|
||||||
|
}
|
||||||
|
f.seen_pcs = MemoryMappedList.init(coverage_file, existing_len, bytes_len) catch |err| {
|
||||||
|
fatal("unable to init coverage memory map: {s}", .{@errorName(err)});
|
||||||
|
};
|
||||||
|
if (existing_len != 0) {
|
||||||
|
const existing_pcs_bytes = f.seen_pcs.items[@sizeOf(SeenPcsHeader)..][0 .. flagged_pcs.len * @sizeOf(usize)];
|
||||||
|
const existing_pcs = std.mem.bytesAsSlice(usize, existing_pcs_bytes);
|
||||||
|
for (existing_pcs, flagged_pcs, 0..) |old, new, i| {
|
||||||
|
if (old != new.addr) {
|
||||||
|
fatal("incompatible existing coverage file (differing PC at index {d}: {x} != {x})", .{
|
||||||
|
i, old, new.addr,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const header: SeenPcsHeader = .{
|
||||||
|
.n_runs = 0,
|
||||||
|
.unique_runs = 0,
|
||||||
|
.pcs_len = flagged_pcs.len,
|
||||||
|
.lowest_stack = std.math.maxInt(usize),
|
||||||
|
};
|
||||||
|
f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&header));
|
||||||
|
for (flagged_pcs) |flagged_pc| {
|
||||||
|
f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&flagged_pc.addr));
|
||||||
|
}
|
||||||
|
f.seen_pcs.appendNTimesAssumeCapacity(0, n_bitset_elems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn analyzeLastRun(f: *Fuzzer) Analysis {
|
fn analyzeLastRun(f: *Fuzzer) Analysis {
|
||||||
return .{
|
return .{
|
||||||
.id = f.coverage.run_id_hasher.final(),
|
.id = f.coverage.run_id_hasher.final(),
|
||||||
@ -194,7 +276,7 @@ const Fuzzer = struct {
|
|||||||
.score = 0,
|
.score = 0,
|
||||||
}, {});
|
}, {});
|
||||||
} else {
|
} else {
|
||||||
if (f.count % 1000 == 0) f.dumpStats();
|
if (f.n_runs % 10000 == 0) f.dumpStats();
|
||||||
|
|
||||||
const analysis = f.analyzeLastRun();
|
const analysis = f.analyzeLastRun();
|
||||||
const gop = f.recent_cases.getOrPutAssumeCapacity(.{
|
const gop = f.recent_cases.getOrPutAssumeCapacity(.{
|
||||||
@ -204,7 +286,6 @@ const Fuzzer = struct {
|
|||||||
});
|
});
|
||||||
if (gop.found_existing) {
|
if (gop.found_existing) {
|
||||||
//std.log.info("duplicate analysis: score={d} id={d}", .{ analysis.score, analysis.id });
|
//std.log.info("duplicate analysis: score={d} id={d}", .{ analysis.score, analysis.id });
|
||||||
f.deduplicated_runs += 1;
|
|
||||||
if (f.input.items.len < gop.key_ptr.input.len or gop.key_ptr.score == 0) {
|
if (f.input.items.len < gop.key_ptr.input.len or gop.key_ptr.score == 0) {
|
||||||
gpa.free(gop.key_ptr.input);
|
gpa.free(gop.key_ptr.input);
|
||||||
gop.key_ptr.input = try gpa.dupe(u8, f.input.items);
|
gop.key_ptr.input = try gpa.dupe(u8, f.input.items);
|
||||||
@ -217,6 +298,28 @@ const Fuzzer = struct {
|
|||||||
.input = try gpa.dupe(u8, f.input.items),
|
.input = try gpa.dupe(u8, f.input.items),
|
||||||
.score = analysis.score,
|
.score = analysis.score,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Track code coverage from all runs.
|
||||||
|
{
|
||||||
|
const seen_pcs = f.seen_pcs.items[@sizeOf(SeenPcsHeader) + f.flagged_pcs.len * @sizeOf(usize) ..];
|
||||||
|
for (seen_pcs, 0..) |*elem, i| {
|
||||||
|
const byte_i = i * 8;
|
||||||
|
const mask: u8 =
|
||||||
|
(@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 0] != 0)) << 0) |
|
||||||
|
(@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 1] != 0)) << 1) |
|
||||||
|
(@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 2] != 0)) << 2) |
|
||||||
|
(@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 3] != 0)) << 3) |
|
||||||
|
(@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 4] != 0)) << 4) |
|
||||||
|
(@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 5] != 0)) << 5) |
|
||||||
|
(@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 6] != 0)) << 6) |
|
||||||
|
(@as(u8, @intFromBool(f.pc_counters.ptr[byte_i + 7] != 0)) << 7);
|
||||||
|
|
||||||
|
_ = @atomicRmw(u8, elem, .Or, mask, .monotonic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);
|
||||||
|
_ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (f.recent_cases.entries.len >= 100) {
|
if (f.recent_cases.entries.len >= 100) {
|
||||||
@ -244,8 +347,12 @@ const Fuzzer = struct {
|
|||||||
f.input.appendSliceAssumeCapacity(run.input);
|
f.input.appendSliceAssumeCapacity(run.input);
|
||||||
try f.mutate();
|
try f.mutate();
|
||||||
|
|
||||||
|
f.n_runs += 1;
|
||||||
|
const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);
|
||||||
|
_ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);
|
||||||
|
_ = @atomicRmw(usize, &header.lowest_stack, .Min, __sancov_lowest_stack, .monotonic);
|
||||||
|
@memset(f.pc_counters, 0);
|
||||||
f.coverage.reset();
|
f.coverage.reset();
|
||||||
f.count += 1;
|
|
||||||
return f.input.items;
|
return f.input.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,10 +363,6 @@ const Fuzzer = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn dumpStats(f: *Fuzzer) void {
|
fn dumpStats(f: *Fuzzer) void {
|
||||||
std.log.info("stats: runs={d} deduplicated={d}", .{
|
|
||||||
f.count,
|
|
||||||
f.deduplicated_runs,
|
|
||||||
});
|
|
||||||
for (f.recent_cases.keys()[0..@min(f.recent_cases.entries.len, 5)], 0..) |run, i| {
|
for (f.recent_cases.keys()[0..@min(f.recent_cases.entries.len, 5)], 0..) |run, i| {
|
||||||
std.log.info("best[{d}] id={x} score={d} input: '{}'", .{
|
std.log.info("best[{d}] id={x} score={d} input: '{}'", .{
|
||||||
i, run.id, run.score, std.zig.fmtEscapes(run.input),
|
i, run.id, run.score, std.zig.fmtEscapes(run.input),
|
||||||
@ -291,6 +394,21 @@ const Fuzzer = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn createFileBail(dir: std.fs.Dir, sub_path: []const u8, flags: std.fs.File.CreateFlags) std.fs.File {
|
||||||
|
return dir.createFile(sub_path, flags) catch |err| switch (err) {
|
||||||
|
error.FileNotFound => {
|
||||||
|
const dir_name = std.fs.path.dirname(sub_path).?;
|
||||||
|
dir.makePath(dir_name) catch |e| {
|
||||||
|
fatal("unable to make path '{s}': {s}", .{ dir_name, @errorName(e) });
|
||||||
|
};
|
||||||
|
return dir.createFile(sub_path, flags) catch |e| {
|
||||||
|
fatal("unable to create file '{s}': {s}", .{ sub_path, @errorName(e) });
|
||||||
|
};
|
||||||
|
},
|
||||||
|
else => fatal("unable to create file '{s}': {s}", .{ sub_path, @errorName(err) }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn oom(err: anytype) noreturn {
|
fn oom(err: anytype) noreturn {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
error.OutOfMemory => @panic("out of memory"),
|
error.OutOfMemory => @panic("out of memory"),
|
||||||
@ -303,15 +421,88 @@ var fuzzer: Fuzzer = .{
|
|||||||
.gpa = general_purpose_allocator.allocator(),
|
.gpa = general_purpose_allocator.allocator(),
|
||||||
.rng = std.Random.DefaultPrng.init(0),
|
.rng = std.Random.DefaultPrng.init(0),
|
||||||
.input = .{},
|
.input = .{},
|
||||||
.pc_range = .{ .start = 0, .end = 0 },
|
.flagged_pcs = undefined,
|
||||||
.count = 0,
|
.pc_counters = undefined,
|
||||||
.deduplicated_runs = 0,
|
.n_runs = 0,
|
||||||
.recent_cases = .{},
|
.recent_cases = .{},
|
||||||
.coverage = undefined,
|
.coverage = undefined,
|
||||||
|
.cache_dir = undefined,
|
||||||
|
.seen_pcs = undefined,
|
||||||
|
.coverage_id = undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Invalid until `fuzzer_init` is called.
|
||||||
|
export fn fuzzer_coverage_id() u64 {
|
||||||
|
return fuzzer.coverage_id;
|
||||||
|
}
|
||||||
|
|
||||||
export fn fuzzer_next() Fuzzer.Slice {
|
export fn fuzzer_next() Fuzzer.Slice {
|
||||||
return Fuzzer.Slice.fromZig(fuzzer.next() catch |err| switch (err) {
|
return Fuzzer.Slice.fromZig(fuzzer.next() catch |err| switch (err) {
|
||||||
error.OutOfMemory => @panic("out of memory"),
|
error.OutOfMemory => @panic("out of memory"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {
|
||||||
|
if (module_count_8bc == 0) fatal("__sanitizer_cov_8bit_counters_init was never called", .{});
|
||||||
|
if (module_count_pcs == 0) fatal("__sanitizer_cov_pcs_init was never called", .{});
|
||||||
|
|
||||||
|
const cache_dir_path = cache_dir_struct.toZig();
|
||||||
|
const cache_dir = if (cache_dir_path.len == 0)
|
||||||
|
std.fs.cwd()
|
||||||
|
else
|
||||||
|
std.fs.cwd().makeOpenPath(cache_dir_path, .{ .iterate = true }) catch |err| {
|
||||||
|
fatal("unable to open fuzz directory '{s}': {s}", .{ cache_dir_path, @errorName(err) });
|
||||||
|
};
|
||||||
|
|
||||||
|
fuzzer.init(cache_dir) catch |err| fatal("unable to init fuzzer: {s}", .{@errorName(err)});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping.
|
||||||
|
pub const MemoryMappedList = struct {
|
||||||
|
/// Contents of the list.
|
||||||
|
///
|
||||||
|
/// Pointers to elements in this slice are invalidated by various functions
|
||||||
|
/// of this ArrayList in accordance with the respective documentation. In
|
||||||
|
/// all cases, "invalidated" means that the memory has been passed to this
|
||||||
|
/// allocator's resize or free function.
|
||||||
|
items: []align(std.mem.page_size) volatile u8,
|
||||||
|
/// How many bytes this list can hold without allocating additional memory.
|
||||||
|
capacity: usize,
|
||||||
|
|
||||||
|
pub fn init(file: std.fs.File, length: usize, capacity: usize) !MemoryMappedList {
|
||||||
|
const ptr = try std.posix.mmap(
|
||||||
|
null,
|
||||||
|
capacity,
|
||||||
|
std.posix.PROT.READ | std.posix.PROT.WRITE,
|
||||||
|
.{ .TYPE = .SHARED },
|
||||||
|
file.handle,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
return .{
|
||||||
|
.items = ptr[0..length],
|
||||||
|
.capacity = capacity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append the slice of items to the list.
|
||||||
|
/// Asserts that the list can hold the additional items.
|
||||||
|
pub fn appendSliceAssumeCapacity(l: *MemoryMappedList, items: []const u8) void {
|
||||||
|
const old_len = l.items.len;
|
||||||
|
const new_len = old_len + items.len;
|
||||||
|
assert(new_len <= l.capacity);
|
||||||
|
l.items.len = new_len;
|
||||||
|
@memcpy(l.items[old_len..][0..items.len], items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append a value to the list `n` times.
|
||||||
|
/// Never invalidates element pointers.
|
||||||
|
/// The function is inline so that a comptime-known `value` parameter will
|
||||||
|
/// have better memset codegen in case it has a repeated byte pattern.
|
||||||
|
/// Asserts that the list can hold the additional items.
|
||||||
|
pub inline fn appendNTimesAssumeCapacity(l: *MemoryMappedList, value: u8, n: usize) void {
|
||||||
|
const new_len = l.items.len + n;
|
||||||
|
assert(new_len <= l.capacity);
|
||||||
|
@memset(l.items.ptr[l.items.len..new_len], value);
|
||||||
|
l.items.len = new_len;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
161
lib/fuzzer/index.html
Normal file
161
lib/fuzzer/index.html
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Zig Build System Interface</title>
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, Roboto, "Segoe UI", sans-serif;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #2A6286;
|
||||||
|
}
|
||||||
|
pre{
|
||||||
|
font-family:"Source Code Pro",monospace;
|
||||||
|
font-size:1em;
|
||||||
|
background-color:#F5F5F5;
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
:not(pre) > code {
|
||||||
|
white-space: break-spaces;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family:"Source Code Pro",monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
code a {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
kbd {
|
||||||
|
color: #000;
|
||||||
|
background-color: #fafbfc;
|
||||||
|
border-color: #d1d5da;
|
||||||
|
border-bottom-color: #c6cbd1;
|
||||||
|
box-shadow-color: #c6cbd1;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.3em 0.2em;
|
||||||
|
font: 1.2em monospace;
|
||||||
|
line-height: 0.8em;
|
||||||
|
vertical-align: middle;
|
||||||
|
border: solid 1px;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: inset 0 -1px 0;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.l {
|
||||||
|
display: inline-block;
|
||||||
|
background: red;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
.c {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tok-kw {
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.tok-str {
|
||||||
|
color: #d14;
|
||||||
|
}
|
||||||
|
.tok-builtin {
|
||||||
|
color: #0086b3;
|
||||||
|
}
|
||||||
|
.tok-comment {
|
||||||
|
color: #777;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.tok-fn {
|
||||||
|
color: #900;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.tok-null {
|
||||||
|
color: #008080;
|
||||||
|
}
|
||||||
|
.tok-number {
|
||||||
|
color: #008080;
|
||||||
|
}
|
||||||
|
.tok-type {
|
||||||
|
color: #458;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: #111;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: #222;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #88f;
|
||||||
|
}
|
||||||
|
code a {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.l {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
.c {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
.tok-kw {
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
.tok-str {
|
||||||
|
color: #2e5;
|
||||||
|
}
|
||||||
|
.tok-builtin {
|
||||||
|
color: #ff894c;
|
||||||
|
}
|
||||||
|
.tok-comment {
|
||||||
|
color: #aa7;
|
||||||
|
}
|
||||||
|
.tok-fn {
|
||||||
|
color: #B1A0F8;
|
||||||
|
}
|
||||||
|
.tok-null {
|
||||||
|
color: #ff8080;
|
||||||
|
}
|
||||||
|
.tok-number {
|
||||||
|
color: #ff8080;
|
||||||
|
}
|
||||||
|
.tok-type {
|
||||||
|
color: #68f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p id="status">Loading JavaScript...</p>
|
||||||
|
<div id="sectStats" class="hidden">
|
||||||
|
<ul>
|
||||||
|
<li>Total Runs: <span id="statTotalRuns"></span></li>
|
||||||
|
<li>Unique Runs: <span id="statUniqueRuns"></span></li>
|
||||||
|
<li>Coverage: <span id="statCoverage"></span></li>
|
||||||
|
<li>Lowest Stack: <span id="statLowestStack"></span></li>
|
||||||
|
<li>Entry Points: <ul id="entryPointsList"></ul></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="sectSource" class="hidden">
|
||||||
|
<h2>Source Code</h2>
|
||||||
|
<pre><code id="sourceText"></code></pre>
|
||||||
|
</div>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
249
lib/fuzzer/main.js
Normal file
249
lib/fuzzer/main.js
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
(function() {
|
||||||
|
const domStatus = document.getElementById("status");
|
||||||
|
const domSectSource = document.getElementById("sectSource");
|
||||||
|
const domSectStats = document.getElementById("sectStats");
|
||||||
|
const domSourceText = document.getElementById("sourceText");
|
||||||
|
const domStatTotalRuns = document.getElementById("statTotalRuns");
|
||||||
|
const domStatUniqueRuns = document.getElementById("statUniqueRuns");
|
||||||
|
const domStatCoverage = document.getElementById("statCoverage");
|
||||||
|
const domStatLowestStack = document.getElementById("statLowestStack");
|
||||||
|
const domEntryPointsList = document.getElementById("entryPointsList");
|
||||||
|
|
||||||
|
let wasm_promise = fetch("main.wasm");
|
||||||
|
let sources_promise = fetch("sources.tar").then(function(response) {
|
||||||
|
if (!response.ok) throw new Error("unable to download sources");
|
||||||
|
return response.arrayBuffer();
|
||||||
|
});
|
||||||
|
var wasm_exports = null;
|
||||||
|
var curNavSearch = null;
|
||||||
|
var curNavLocation = null;
|
||||||
|
|
||||||
|
const text_decoder = new TextDecoder();
|
||||||
|
const text_encoder = new TextEncoder();
|
||||||
|
|
||||||
|
domStatus.textContent = "Loading WebAssembly...";
|
||||||
|
WebAssembly.instantiateStreaming(wasm_promise, {
|
||||||
|
js: {
|
||||||
|
log: function(ptr, len) {
|
||||||
|
const msg = decodeString(ptr, len);
|
||||||
|
console.log(msg);
|
||||||
|
},
|
||||||
|
panic: function (ptr, len) {
|
||||||
|
const msg = decodeString(ptr, len);
|
||||||
|
throw new Error("panic: " + msg);
|
||||||
|
},
|
||||||
|
emitSourceIndexChange: onSourceIndexChange,
|
||||||
|
emitCoverageUpdate: onCoverageUpdate,
|
||||||
|
emitEntryPointsUpdate: renderStats,
|
||||||
|
},
|
||||||
|
}).then(function(obj) {
|
||||||
|
wasm_exports = obj.instance.exports;
|
||||||
|
window.wasm = obj; // for debugging
|
||||||
|
domStatus.textContent = "Loading sources tarball...";
|
||||||
|
|
||||||
|
sources_promise.then(function(buffer) {
|
||||||
|
domStatus.textContent = "Parsing sources...";
|
||||||
|
const js_array = new Uint8Array(buffer);
|
||||||
|
const ptr = wasm_exports.alloc(js_array.length);
|
||||||
|
const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
|
||||||
|
wasm_array.set(js_array);
|
||||||
|
wasm_exports.unpack(ptr, js_array.length);
|
||||||
|
|
||||||
|
window.addEventListener('popstate', onPopState, false);
|
||||||
|
onHashChange(null);
|
||||||
|
|
||||||
|
domStatus.textContent = "Waiting for server to send source location metadata...";
|
||||||
|
connectWebSocket();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function onPopState(ev) {
|
||||||
|
onHashChange(ev.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHashChange(state) {
|
||||||
|
history.replaceState({}, "");
|
||||||
|
navigate(location.hash);
|
||||||
|
if (state == null) window.scrollTo({top: 0});
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigate(location_hash) {
|
||||||
|
domSectSource.classList.add("hidden");
|
||||||
|
|
||||||
|
curNavLocation = null;
|
||||||
|
curNavSearch = null;
|
||||||
|
|
||||||
|
if (location_hash.length > 1 && location_hash[0] === '#') {
|
||||||
|
const query = location_hash.substring(1);
|
||||||
|
const qpos = query.indexOf("?");
|
||||||
|
let nonSearchPart;
|
||||||
|
if (qpos === -1) {
|
||||||
|
nonSearchPart = query;
|
||||||
|
} else {
|
||||||
|
nonSearchPart = query.substring(0, qpos);
|
||||||
|
curNavSearch = decodeURIComponent(query.substring(qpos + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nonSearchPart[0] == "l") {
|
||||||
|
curNavLocation = +nonSearchPart.substring(1);
|
||||||
|
renderSource(curNavLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWebSocket() {
|
||||||
|
const host = document.location.host;
|
||||||
|
const pathname = document.location.pathname;
|
||||||
|
const isHttps = document.location.protocol === 'https:';
|
||||||
|
const match = host.match(/^(.+):(\d+)$/);
|
||||||
|
const defaultPort = isHttps ? 443 : 80;
|
||||||
|
const port = match ? parseInt(match[2], 10) : defaultPort;
|
||||||
|
const hostName = match ? match[1] : host;
|
||||||
|
const wsProto = isHttps ? "wss:" : "ws:";
|
||||||
|
const wsUrl = wsProto + '//' + hostName + ':' + port + pathname;
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
ws.binaryType = "arraybuffer";
|
||||||
|
ws.addEventListener('message', onWebSocketMessage, false);
|
||||||
|
ws.addEventListener('error', timeoutThenCreateNew, false);
|
||||||
|
ws.addEventListener('close', timeoutThenCreateNew, false);
|
||||||
|
ws.addEventListener('open', onWebSocketOpen, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWebSocketOpen() {
|
||||||
|
//console.log("web socket opened");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWebSocketMessage(ev) {
|
||||||
|
wasmOnMessage(ev.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeoutThenCreateNew() {
|
||||||
|
ws.removeEventListener('message', onWebSocketMessage, false);
|
||||||
|
ws.removeEventListener('error', timeoutThenCreateNew, false);
|
||||||
|
ws.removeEventListener('close', timeoutThenCreateNew, false);
|
||||||
|
ws.removeEventListener('open', onWebSocketOpen, false);
|
||||||
|
ws = null;
|
||||||
|
setTimeout(connectWebSocket, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wasmOnMessage(data) {
|
||||||
|
const jsArray = new Uint8Array(data);
|
||||||
|
const ptr = wasm_exports.message_begin(jsArray.length);
|
||||||
|
const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, jsArray.length);
|
||||||
|
wasmArray.set(jsArray);
|
||||||
|
wasm_exports.message_end();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSourceIndexChange() {
|
||||||
|
render();
|
||||||
|
if (curNavLocation != null) renderSource(curNavLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCoverageUpdate() {
|
||||||
|
renderStats();
|
||||||
|
renderCoverage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
domStatus.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStats() {
|
||||||
|
const totalRuns = wasm_exports.totalRuns();
|
||||||
|
const uniqueRuns = wasm_exports.uniqueRuns();
|
||||||
|
const totalSourceLocations = wasm_exports.totalSourceLocations();
|
||||||
|
const coveredSourceLocations = wasm_exports.coveredSourceLocations();
|
||||||
|
domStatTotalRuns.innerText = totalRuns;
|
||||||
|
domStatUniqueRuns.innerText = uniqueRuns + " (" + percent(uniqueRuns, totalRuns) + "%)";
|
||||||
|
domStatCoverage.innerText = coveredSourceLocations + " / " + totalSourceLocations + " (" + percent(coveredSourceLocations, totalSourceLocations) + "%)";
|
||||||
|
domStatLowestStack.innerText = unwrapString(wasm_exports.lowestStack());
|
||||||
|
|
||||||
|
const entryPoints = unwrapInt32Array(wasm_exports.entryPoints());
|
||||||
|
resizeDomList(domEntryPointsList, entryPoints.length, "<li></li>");
|
||||||
|
for (let i = 0; i < entryPoints.length; i += 1) {
|
||||||
|
const liDom = domEntryPointsList.children[i];
|
||||||
|
liDom.innerHTML = unwrapString(wasm_exports.sourceLocationLinkHtml(entryPoints[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
domSectStats.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCoverage() {
|
||||||
|
if (curNavLocation == null) return;
|
||||||
|
const sourceLocationIndex = curNavLocation;
|
||||||
|
|
||||||
|
for (let i = 0; i < domSourceText.children.length; i += 1) {
|
||||||
|
const childDom = domSourceText.children[i];
|
||||||
|
if (childDom.id != null && childDom.id[0] == "l") {
|
||||||
|
childDom.classList.add("l");
|
||||||
|
childDom.classList.remove("c");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const coveredList = unwrapInt32Array(wasm_exports.sourceLocationFileCoveredList(sourceLocationIndex));
|
||||||
|
for (let i = 0; i < coveredList.length; i += 1) {
|
||||||
|
document.getElementById("l" + coveredList[i]).classList.add("c");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeDomList(listDom, desiredLen, templateHtml) {
|
||||||
|
for (let i = listDom.childElementCount; i < desiredLen; i += 1) {
|
||||||
|
listDom.insertAdjacentHTML('beforeend', templateHtml);
|
||||||
|
}
|
||||||
|
while (desiredLen < listDom.childElementCount) {
|
||||||
|
listDom.removeChild(listDom.lastChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function percent(a, b) {
|
||||||
|
return ((Number(a) / Number(b)) * 100).toFixed(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSource(sourceLocationIndex) {
|
||||||
|
const pathName = unwrapString(wasm_exports.sourceLocationPath(sourceLocationIndex));
|
||||||
|
if (pathName.length === 0) return;
|
||||||
|
|
||||||
|
const h2 = domSectSource.children[0];
|
||||||
|
h2.innerText = pathName;
|
||||||
|
domSourceText.innerHTML = unwrapString(wasm_exports.sourceLocationFileHtml(sourceLocationIndex));
|
||||||
|
|
||||||
|
domSectSource.classList.remove("hidden");
|
||||||
|
|
||||||
|
// Empirically, Firefox needs this requestAnimationFrame in order for the scrollIntoView to work.
|
||||||
|
requestAnimationFrame(function() {
|
||||||
|
const slDom = document.getElementById("l" + sourceLocationIndex);
|
||||||
|
if (slDom != null) slDom.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "center",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeString(ptr, len) {
|
||||||
|
if (len === 0) return "";
|
||||||
|
return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapInt32Array(bigint) {
|
||||||
|
const ptr = Number(bigint & 0xffffffffn);
|
||||||
|
const len = Number(bigint >> 32n);
|
||||||
|
if (len === 0) return new Uint32Array();
|
||||||
|
return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInputString(s) {
|
||||||
|
const jsArray = text_encoder.encode(s);
|
||||||
|
const len = jsArray.length;
|
||||||
|
const ptr = wasm_exports.set_input_string(len);
|
||||||
|
const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, len);
|
||||||
|
wasmArray.set(jsArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapString(bigint) {
|
||||||
|
const ptr = Number(bigint & 0xffffffffn);
|
||||||
|
const len = Number(bigint >> 32n);
|
||||||
|
return decodeString(ptr, len);
|
||||||
|
}
|
||||||
|
})();
|
||||||
424
lib/fuzzer/wasm/main.zig
Normal file
424
lib/fuzzer/wasm/main.zig
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const abi = std.Build.Fuzz.abi;
|
||||||
|
const gpa = std.heap.wasm_allocator;
|
||||||
|
const log = std.log;
|
||||||
|
const Coverage = std.debug.Coverage;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const Walk = @import("Walk");
|
||||||
|
const Decl = Walk.Decl;
|
||||||
|
const html_render = @import("html_render");
|
||||||
|
|
||||||
|
const js = struct {
|
||||||
|
extern "js" fn log(ptr: [*]const u8, len: usize) void;
|
||||||
|
extern "js" fn panic(ptr: [*]const u8, len: usize) noreturn;
|
||||||
|
extern "js" fn emitSourceIndexChange() void;
|
||||||
|
extern "js" fn emitCoverageUpdate() void;
|
||||||
|
extern "js" fn emitEntryPointsUpdate() void;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const std_options: std.Options = .{
|
||||||
|
.logFn = logFn,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn panic(msg: []const u8, st: ?*std.builtin.StackTrace, addr: ?usize) noreturn {
|
||||||
|
_ = st;
|
||||||
|
_ = addr;
|
||||||
|
log.err("panic: {s}", .{msg});
|
||||||
|
@trap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logFn(
|
||||||
|
comptime message_level: log.Level,
|
||||||
|
comptime scope: @TypeOf(.enum_literal),
|
||||||
|
comptime format: []const u8,
|
||||||
|
args: anytype,
|
||||||
|
) void {
|
||||||
|
const level_txt = comptime message_level.asText();
|
||||||
|
const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
|
||||||
|
var buf: [500]u8 = undefined;
|
||||||
|
const line = std.fmt.bufPrint(&buf, level_txt ++ prefix2 ++ format, args) catch l: {
|
||||||
|
buf[buf.len - 3 ..][0..3].* = "...".*;
|
||||||
|
break :l &buf;
|
||||||
|
};
|
||||||
|
js.log(line.ptr, line.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn alloc(n: usize) [*]u8 {
|
||||||
|
const slice = gpa.alloc(u8, n) catch @panic("OOM");
|
||||||
|
return slice.ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
var message_buffer: std.ArrayListAlignedUnmanaged(u8, @alignOf(u64)) = .{};
|
||||||
|
|
||||||
|
/// Resizes the message buffer to be the correct length; returns the pointer to
|
||||||
|
/// the query string.
|
||||||
|
export fn message_begin(len: usize) [*]u8 {
|
||||||
|
message_buffer.resize(gpa, len) catch @panic("OOM");
|
||||||
|
return message_buffer.items.ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn message_end() void {
|
||||||
|
const msg_bytes = message_buffer.items;
|
||||||
|
|
||||||
|
const tag: abi.ToClientTag = @enumFromInt(msg_bytes[0]);
|
||||||
|
switch (tag) {
|
||||||
|
.source_index => return sourceIndexMessage(msg_bytes) catch @panic("OOM"),
|
||||||
|
.coverage_update => return coverageUpdateMessage(msg_bytes) catch @panic("OOM"),
|
||||||
|
.entry_points => return entryPointsMessage(msg_bytes) catch @panic("OOM"),
|
||||||
|
_ => unreachable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn unpack(tar_ptr: [*]u8, tar_len: usize) void {
|
||||||
|
const tar_bytes = tar_ptr[0..tar_len];
|
||||||
|
log.debug("received {d} bytes of tar file", .{tar_bytes.len});
|
||||||
|
|
||||||
|
unpackInner(tar_bytes) catch |err| {
|
||||||
|
fatal("unable to unpack tar: {s}", .{@errorName(err)});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set by `set_input_string`.
|
||||||
|
var input_string: std.ArrayListUnmanaged(u8) = .{};
|
||||||
|
var string_result: std.ArrayListUnmanaged(u8) = .{};
|
||||||
|
|
||||||
|
export fn set_input_string(len: usize) [*]u8 {
|
||||||
|
input_string.resize(gpa, len) catch @panic("OOM");
|
||||||
|
return input_string.items.ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Looks up the root struct decl corresponding to a file by path.
|
||||||
|
/// Uses `input_string`.
|
||||||
|
export fn find_file_root() Decl.Index {
|
||||||
|
const file: Walk.File.Index = @enumFromInt(Walk.files.getIndex(input_string.items) orelse return .none);
|
||||||
|
return file.findRootDecl();
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn decl_source_html(decl_index: Decl.Index) String {
|
||||||
|
const decl = decl_index.get();
|
||||||
|
|
||||||
|
string_result.clearRetainingCapacity();
|
||||||
|
html_render.fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
|
||||||
|
fatal("unable to render source: {s}", .{@errorName(err)});
|
||||||
|
};
|
||||||
|
return String.init(string_result.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn lowestStack() String {
|
||||||
|
const header: *abi.CoverageUpdateHeader = @ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]);
|
||||||
|
string_result.clearRetainingCapacity();
|
||||||
|
string_result.writer(gpa).print("0x{d}", .{header.lowest_stack}) catch @panic("OOM");
|
||||||
|
return String.init(string_result.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn totalSourceLocations() usize {
|
||||||
|
return coverage_source_locations.items.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn coveredSourceLocations() usize {
|
||||||
|
const covered_bits = recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..];
|
||||||
|
var count: usize = 0;
|
||||||
|
for (covered_bits) |byte| count += @popCount(byte);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn totalRuns() u64 {
|
||||||
|
const header: *abi.CoverageUpdateHeader = @ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]);
|
||||||
|
return header.n_runs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn uniqueRuns() u64 {
|
||||||
|
const header: *abi.CoverageUpdateHeader = @ptrCast(recent_coverage_update.items[0..@sizeOf(abi.CoverageUpdateHeader)]);
|
||||||
|
return header.unique_runs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const String = Slice(u8);
|
||||||
|
|
||||||
|
fn Slice(T: type) type {
|
||||||
|
return packed struct(u64) {
|
||||||
|
ptr: u32,
|
||||||
|
len: u32,
|
||||||
|
|
||||||
|
fn init(s: []const T) @This() {
|
||||||
|
return .{
|
||||||
|
.ptr = @intFromPtr(s.ptr),
|
||||||
|
.len = s.len,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unpackInner(tar_bytes: []u8) !void {
|
||||||
|
var fbs = std.io.fixedBufferStream(tar_bytes);
|
||||||
|
var file_name_buffer: [1024]u8 = undefined;
|
||||||
|
var link_name_buffer: [1024]u8 = undefined;
|
||||||
|
var it = std.tar.iterator(fbs.reader(), .{
|
||||||
|
.file_name_buffer = &file_name_buffer,
|
||||||
|
.link_name_buffer = &link_name_buffer,
|
||||||
|
});
|
||||||
|
while (try it.next()) |tar_file| {
|
||||||
|
switch (tar_file.kind) {
|
||||||
|
.file => {
|
||||||
|
if (tar_file.size == 0 and tar_file.name.len == 0) break;
|
||||||
|
if (std.mem.endsWith(u8, tar_file.name, ".zig")) {
|
||||||
|
log.debug("found file: '{s}'", .{tar_file.name});
|
||||||
|
const file_name = try gpa.dupe(u8, tar_file.name);
|
||||||
|
if (std.mem.indexOfScalar(u8, file_name, '/')) |pkg_name_end| {
|
||||||
|
const pkg_name = file_name[0..pkg_name_end];
|
||||||
|
const gop = try Walk.modules.getOrPut(gpa, pkg_name);
|
||||||
|
const file: Walk.File.Index = @enumFromInt(Walk.files.entries.len);
|
||||||
|
if (!gop.found_existing or
|
||||||
|
std.mem.eql(u8, file_name[pkg_name_end..], "/root.zig") or
|
||||||
|
std.mem.eql(u8, file_name[pkg_name_end + 1 .. file_name.len - ".zig".len], pkg_name))
|
||||||
|
{
|
||||||
|
gop.value_ptr.* = file;
|
||||||
|
}
|
||||||
|
const file_bytes = tar_bytes[fbs.pos..][0..@intCast(tar_file.size)];
|
||||||
|
assert(file == try Walk.add_file(file_name, file_bytes));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("skipping: '{s}' - the tar creation should have done that", .{tar_file.name});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fatal(comptime format: []const u8, args: anytype) noreturn {
|
||||||
|
var buf: [500]u8 = undefined;
|
||||||
|
const line = std.fmt.bufPrint(&buf, format, args) catch l: {
|
||||||
|
buf[buf.len - 3 ..][0..3].* = "...".*;
|
||||||
|
break :l &buf;
|
||||||
|
};
|
||||||
|
js.panic(line.ptr, line.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sourceIndexMessage(msg_bytes: []u8) error{OutOfMemory}!void {
|
||||||
|
const Header = abi.SourceIndexHeader;
|
||||||
|
const header: Header = @bitCast(msg_bytes[0..@sizeOf(Header)].*);
|
||||||
|
|
||||||
|
const directories_start = @sizeOf(Header);
|
||||||
|
const directories_end = directories_start + header.directories_len * @sizeOf(Coverage.String);
|
||||||
|
const files_start = directories_end;
|
||||||
|
const files_end = files_start + header.files_len * @sizeOf(Coverage.File);
|
||||||
|
const source_locations_start = files_end;
|
||||||
|
const source_locations_end = source_locations_start + header.source_locations_len * @sizeOf(Coverage.SourceLocation);
|
||||||
|
const string_bytes = msg_bytes[source_locations_end..][0..header.string_bytes_len];
|
||||||
|
|
||||||
|
const directories: []const Coverage.String = @alignCast(std.mem.bytesAsSlice(Coverage.String, msg_bytes[directories_start..directories_end]));
|
||||||
|
const files: []const Coverage.File = @alignCast(std.mem.bytesAsSlice(Coverage.File, msg_bytes[files_start..files_end]));
|
||||||
|
const source_locations: []const Coverage.SourceLocation = @alignCast(std.mem.bytesAsSlice(Coverage.SourceLocation, msg_bytes[source_locations_start..source_locations_end]));
|
||||||
|
|
||||||
|
try updateCoverage(directories, files, source_locations, string_bytes);
|
||||||
|
js.emitSourceIndexChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn coverageUpdateMessage(msg_bytes: []u8) error{OutOfMemory}!void {
|
||||||
|
recent_coverage_update.clearRetainingCapacity();
|
||||||
|
recent_coverage_update.appendSlice(gpa, msg_bytes) catch @panic("OOM");
|
||||||
|
js.emitCoverageUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry_points: std.ArrayListUnmanaged(u32) = .{};
|
||||||
|
|
||||||
|
fn entryPointsMessage(msg_bytes: []u8) error{OutOfMemory}!void {
|
||||||
|
const header: abi.EntryPointHeader = @bitCast(msg_bytes[0..@sizeOf(abi.EntryPointHeader)].*);
|
||||||
|
entry_points.resize(gpa, header.flags.locs_len) catch @panic("OOM");
|
||||||
|
@memcpy(entry_points.items, std.mem.bytesAsSlice(u32, msg_bytes[@sizeOf(abi.EntryPointHeader)..]));
|
||||||
|
js.emitEntryPointsUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn entryPoints() Slice(u32) {
|
||||||
|
return Slice(u32).init(entry_points.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Index into `coverage_source_locations`.
|
||||||
|
const SourceLocationIndex = enum(u32) {
|
||||||
|
_,
|
||||||
|
|
||||||
|
fn haveCoverage(sli: SourceLocationIndex) bool {
|
||||||
|
return @intFromEnum(sli) < coverage_source_locations.items.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ptr(sli: SourceLocationIndex) *Coverage.SourceLocation {
|
||||||
|
return &coverage_source_locations.items[@intFromEnum(sli)];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sourceLocationLinkHtml(
|
||||||
|
sli: SourceLocationIndex,
|
||||||
|
out: *std.ArrayListUnmanaged(u8),
|
||||||
|
) Allocator.Error!void {
|
||||||
|
const sl = sli.ptr();
|
||||||
|
try out.writer(gpa).print("<a href=\"#l{d}\">", .{@intFromEnum(sli)});
|
||||||
|
try sli.appendPath(out);
|
||||||
|
try out.writer(gpa).print(":{d}:{d}</a>", .{ sl.line, sl.column });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn appendPath(sli: SourceLocationIndex, out: *std.ArrayListUnmanaged(u8)) Allocator.Error!void {
|
||||||
|
const sl = sli.ptr();
|
||||||
|
const file = coverage.fileAt(sl.file);
|
||||||
|
const file_name = coverage.stringAt(file.basename);
|
||||||
|
const dir_name = coverage.stringAt(coverage.directories.keys()[file.directory_index]);
|
||||||
|
try html_render.appendEscaped(out, dir_name);
|
||||||
|
try out.appendSlice(gpa, "/");
|
||||||
|
try html_render.appendEscaped(out, file_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toWalkFile(sli: SourceLocationIndex) ?Walk.File.Index {
|
||||||
|
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||||
|
defer buf.deinit(gpa);
|
||||||
|
sli.appendPath(&buf) catch @panic("OOM");
|
||||||
|
return @enumFromInt(Walk.files.getIndex(buf.items) orelse return null);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fileHtml(
|
||||||
|
sli: SourceLocationIndex,
|
||||||
|
out: *std.ArrayListUnmanaged(u8),
|
||||||
|
) error{ OutOfMemory, SourceUnavailable }!void {
|
||||||
|
const walk_file_index = sli.toWalkFile() orelse return error.SourceUnavailable;
|
||||||
|
const root_node = walk_file_index.findRootDecl().get().ast_node;
|
||||||
|
var annotations: std.ArrayListUnmanaged(html_render.Annotation) = .{};
|
||||||
|
defer annotations.deinit(gpa);
|
||||||
|
try computeSourceAnnotations(sli.ptr().file, walk_file_index, &annotations, coverage_source_locations.items);
|
||||||
|
html_render.fileSourceHtml(walk_file_index, out, root_node, .{
|
||||||
|
.source_location_annotations = annotations.items,
|
||||||
|
}) catch |err| {
|
||||||
|
fatal("unable to render source: {s}", .{@errorName(err)});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn computeSourceAnnotations(
|
||||||
|
cov_file_index: Coverage.File.Index,
|
||||||
|
walk_file_index: Walk.File.Index,
|
||||||
|
annotations: *std.ArrayListUnmanaged(html_render.Annotation),
|
||||||
|
source_locations: []const Coverage.SourceLocation,
|
||||||
|
) !void {
|
||||||
|
// Collect all the source locations from only this file into this array
|
||||||
|
// first, then sort by line, col, so that we can collect annotations with
|
||||||
|
// O(N) time complexity.
|
||||||
|
var locs: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
|
||||||
|
defer locs.deinit(gpa);
|
||||||
|
|
||||||
|
for (source_locations, 0..) |sl, sli_usize| {
|
||||||
|
if (sl.file != cov_file_index) continue;
|
||||||
|
const sli: SourceLocationIndex = @enumFromInt(sli_usize);
|
||||||
|
try locs.append(gpa, sli);
|
||||||
|
}
|
||||||
|
|
||||||
|
std.mem.sortUnstable(SourceLocationIndex, locs.items, {}, struct {
|
||||||
|
pub fn lessThan(context: void, lhs: SourceLocationIndex, rhs: SourceLocationIndex) bool {
|
||||||
|
_ = context;
|
||||||
|
const lhs_ptr = lhs.ptr();
|
||||||
|
const rhs_ptr = rhs.ptr();
|
||||||
|
if (lhs_ptr.line < rhs_ptr.line) return true;
|
||||||
|
if (lhs_ptr.line > rhs_ptr.line) return false;
|
||||||
|
return lhs_ptr.column < rhs_ptr.column;
|
||||||
|
}
|
||||||
|
}.lessThan);
|
||||||
|
|
||||||
|
const source = walk_file_index.get_ast().source;
|
||||||
|
var line: usize = 1;
|
||||||
|
var column: usize = 1;
|
||||||
|
var next_loc_index: usize = 0;
|
||||||
|
for (source, 0..) |byte, offset| {
|
||||||
|
if (byte == '\n') {
|
||||||
|
line += 1;
|
||||||
|
column = 1;
|
||||||
|
} else {
|
||||||
|
column += 1;
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
if (next_loc_index >= locs.items.len) return;
|
||||||
|
const next_sli = locs.items[next_loc_index];
|
||||||
|
const next_sl = next_sli.ptr();
|
||||||
|
if (next_sl.line > line or (next_sl.line == line and next_sl.column > column)) break;
|
||||||
|
try annotations.append(gpa, .{
|
||||||
|
.file_byte_offset = offset,
|
||||||
|
.dom_id = @intFromEnum(next_sli),
|
||||||
|
});
|
||||||
|
next_loc_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var coverage = Coverage.init;
|
||||||
|
/// Index of type `SourceLocationIndex`.
|
||||||
|
var coverage_source_locations: std.ArrayListUnmanaged(Coverage.SourceLocation) = .{};
|
||||||
|
/// Contains the most recent coverage update message, unmodified.
|
||||||
|
var recent_coverage_update: std.ArrayListUnmanaged(u8) = .{};
|
||||||
|
|
||||||
|
fn updateCoverage(
|
||||||
|
directories: []const Coverage.String,
|
||||||
|
files: []const Coverage.File,
|
||||||
|
source_locations: []const Coverage.SourceLocation,
|
||||||
|
string_bytes: []const u8,
|
||||||
|
) !void {
|
||||||
|
coverage.directories.clearRetainingCapacity();
|
||||||
|
coverage.files.clearRetainingCapacity();
|
||||||
|
coverage.string_bytes.clearRetainingCapacity();
|
||||||
|
coverage_source_locations.clearRetainingCapacity();
|
||||||
|
|
||||||
|
try coverage_source_locations.appendSlice(gpa, source_locations);
|
||||||
|
try coverage.string_bytes.appendSlice(gpa, string_bytes);
|
||||||
|
|
||||||
|
try coverage.files.entries.resize(gpa, files.len);
|
||||||
|
@memcpy(coverage.files.entries.items(.key), files);
|
||||||
|
try coverage.files.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
|
||||||
|
|
||||||
|
try coverage.directories.entries.resize(gpa, directories.len);
|
||||||
|
@memcpy(coverage.directories.entries.items(.key), directories);
|
||||||
|
try coverage.directories.reIndexContext(gpa, .{ .string_bytes = coverage.string_bytes.items });
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn sourceLocationLinkHtml(index: SourceLocationIndex) String {
|
||||||
|
string_result.clearRetainingCapacity();
|
||||||
|
index.sourceLocationLinkHtml(&string_result) catch @panic("OOM");
|
||||||
|
return String.init(string_result.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns empty string if coverage metadata is not available for this source location.
|
||||||
|
export fn sourceLocationPath(sli: SourceLocationIndex) String {
|
||||||
|
string_result.clearRetainingCapacity();
|
||||||
|
if (sli.haveCoverage()) sli.appendPath(&string_result) catch @panic("OOM");
|
||||||
|
return String.init(string_result.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn sourceLocationFileHtml(sli: SourceLocationIndex) String {
|
||||||
|
string_result.clearRetainingCapacity();
|
||||||
|
sli.fileHtml(&string_result) catch |err| switch (err) {
|
||||||
|
error.OutOfMemory => @panic("OOM"),
|
||||||
|
error.SourceUnavailable => {},
|
||||||
|
};
|
||||||
|
return String.init(string_result.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn sourceLocationFileCoveredList(sli_file: SourceLocationIndex) Slice(SourceLocationIndex) {
|
||||||
|
const global = struct {
|
||||||
|
var result: std.ArrayListUnmanaged(SourceLocationIndex) = .{};
|
||||||
|
fn add(i: u32, want_file: Coverage.File.Index) void {
|
||||||
|
const src_loc_index: SourceLocationIndex = @enumFromInt(i);
|
||||||
|
if (src_loc_index.ptr().file == want_file) result.appendAssumeCapacity(src_loc_index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const want_file = sli_file.ptr().file;
|
||||||
|
global.result.clearRetainingCapacity();
|
||||||
|
const covered_bits = recent_coverage_update.items[@sizeOf(abi.CoverageUpdateHeader)..];
|
||||||
|
var sli: u32 = 0;
|
||||||
|
for (covered_bits) |byte| {
|
||||||
|
global.result.ensureUnusedCapacity(gpa, 8) catch @panic("OOM");
|
||||||
|
if ((byte & 0b0000_0001) != 0) global.add(sli + 0, want_file);
|
||||||
|
if ((byte & 0b0000_0010) != 0) global.add(sli + 1, want_file);
|
||||||
|
if ((byte & 0b0000_0100) != 0) global.add(sli + 2, want_file);
|
||||||
|
if ((byte & 0b0000_1000) != 0) global.add(sli + 3, want_file);
|
||||||
|
if ((byte & 0b0001_0000) != 0) global.add(sli + 4, want_file);
|
||||||
|
if ((byte & 0b0010_0000) != 0) global.add(sli + 5, want_file);
|
||||||
|
if ((byte & 0b0100_0000) != 0) global.add(sli + 6, want_file);
|
||||||
|
if ((byte & 0b1000_0000) != 0) global.add(sli + 7, want_file);
|
||||||
|
sli += 8;
|
||||||
|
}
|
||||||
|
return Slice(SourceLocationIndex).init(global.result.items);
|
||||||
|
}
|
||||||
@ -2300,22 +2300,26 @@ pub const LazyPath = union(enum) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn path(lazy_path: LazyPath, b: *Build, sub_path: []const u8) LazyPath {
|
pub fn path(lazy_path: LazyPath, b: *Build, sub_path: []const u8) LazyPath {
|
||||||
|
return lazy_path.join(b.allocator, sub_path) catch @panic("OOM");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn join(lazy_path: LazyPath, arena: Allocator, sub_path: []const u8) Allocator.Error!LazyPath {
|
||||||
return switch (lazy_path) {
|
return switch (lazy_path) {
|
||||||
.src_path => |src| .{ .src_path = .{
|
.src_path => |src| .{ .src_path = .{
|
||||||
.owner = src.owner,
|
.owner = src.owner,
|
||||||
.sub_path = b.pathResolve(&.{ src.sub_path, sub_path }),
|
.sub_path = try fs.path.resolve(arena, &.{ src.sub_path, sub_path }),
|
||||||
} },
|
} },
|
||||||
.generated => |gen| .{ .generated = .{
|
.generated => |gen| .{ .generated = .{
|
||||||
.file = gen.file,
|
.file = gen.file,
|
||||||
.up = gen.up,
|
.up = gen.up,
|
||||||
.sub_path = b.pathResolve(&.{ gen.sub_path, sub_path }),
|
.sub_path = try fs.path.resolve(arena, &.{ gen.sub_path, sub_path }),
|
||||||
} },
|
} },
|
||||||
.cwd_relative => |cwd_relative| .{
|
.cwd_relative => |cwd_relative| .{
|
||||||
.cwd_relative = b.pathResolve(&.{ cwd_relative, sub_path }),
|
.cwd_relative = try fs.path.resolve(arena, &.{ cwd_relative, sub_path }),
|
||||||
},
|
},
|
||||||
.dependency => |dep| .{ .dependency = .{
|
.dependency => |dep| .{ .dependency = .{
|
||||||
.dependency = dep.dependency,
|
.dependency = dep.dependency,
|
||||||
.sub_path = b.pathResolve(&.{ dep.sub_path, sub_path }),
|
.sub_path = try fs.path.resolve(arena, &.{ dep.sub_path, sub_path }),
|
||||||
} },
|
} },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,16 +32,16 @@ pub fn resolvePosix(p: Path, arena: Allocator, sub_path: []const u8) Allocator.E
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn joinString(p: Path, allocator: Allocator, sub_path: []const u8) Allocator.Error![]u8 {
|
pub fn joinString(p: Path, gpa: Allocator, sub_path: []const u8) Allocator.Error![]u8 {
|
||||||
const parts: []const []const u8 =
|
const parts: []const []const u8 =
|
||||||
if (p.sub_path.len == 0) &.{sub_path} else &.{ p.sub_path, sub_path };
|
if (p.sub_path.len == 0) &.{sub_path} else &.{ p.sub_path, sub_path };
|
||||||
return p.root_dir.join(allocator, parts);
|
return p.root_dir.join(gpa, parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn joinStringZ(p: Path, allocator: Allocator, sub_path: []const u8) Allocator.Error![:0]u8 {
|
pub fn joinStringZ(p: Path, gpa: Allocator, sub_path: []const u8) Allocator.Error![:0]u8 {
|
||||||
const parts: []const []const u8 =
|
const parts: []const []const u8 =
|
||||||
if (p.sub_path.len == 0) &.{sub_path} else &.{ p.sub_path, sub_path };
|
if (p.sub_path.len == 0) &.{sub_path} else &.{ p.sub_path, sub_path };
|
||||||
return p.root_dir.joinZ(allocator, parts);
|
return p.root_dir.joinZ(gpa, parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn openFile(
|
pub fn openFile(
|
||||||
|
|||||||
@ -1,57 +1,102 @@
|
|||||||
|
const builtin = @import("builtin");
|
||||||
const std = @import("../std.zig");
|
const std = @import("../std.zig");
|
||||||
const Fuzz = @This();
|
const Build = std.Build;
|
||||||
const Step = std.Build.Step;
|
const Step = std.Build.Step;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const fatal = std.process.fatal;
|
const fatal = std.process.fatal;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const log = std.log;
|
||||||
|
|
||||||
|
const Fuzz = @This();
|
||||||
const build_runner = @import("root");
|
const build_runner = @import("root");
|
||||||
|
|
||||||
|
pub const WebServer = @import("Fuzz/WebServer.zig");
|
||||||
|
pub const abi = @import("Fuzz/abi.zig");
|
||||||
|
|
||||||
pub fn start(
|
pub fn start(
|
||||||
|
gpa: Allocator,
|
||||||
|
arena: Allocator,
|
||||||
|
global_cache_directory: Build.Cache.Directory,
|
||||||
|
zig_lib_directory: Build.Cache.Directory,
|
||||||
|
zig_exe_path: []const u8,
|
||||||
thread_pool: *std.Thread.Pool,
|
thread_pool: *std.Thread.Pool,
|
||||||
all_steps: []const *Step,
|
all_steps: []const *Step,
|
||||||
ttyconf: std.io.tty.Config,
|
ttyconf: std.io.tty.Config,
|
||||||
|
listen_address: std.net.Address,
|
||||||
prog_node: std.Progress.Node,
|
prog_node: std.Progress.Node,
|
||||||
) void {
|
) Allocator.Error!void {
|
||||||
const count = block: {
|
const fuzz_run_steps = block: {
|
||||||
const rebuild_node = prog_node.start("Rebuilding Unit Tests", 0);
|
const rebuild_node = prog_node.start("Rebuilding Unit Tests", 0);
|
||||||
defer rebuild_node.end();
|
defer rebuild_node.end();
|
||||||
var count: usize = 0;
|
|
||||||
var wait_group: std.Thread.WaitGroup = .{};
|
var wait_group: std.Thread.WaitGroup = .{};
|
||||||
defer wait_group.wait();
|
defer wait_group.wait();
|
||||||
|
var fuzz_run_steps: std.ArrayListUnmanaged(*Step.Run) = .{};
|
||||||
|
defer fuzz_run_steps.deinit(gpa);
|
||||||
for (all_steps) |step| {
|
for (all_steps) |step| {
|
||||||
const run = step.cast(Step.Run) orelse continue;
|
const run = step.cast(Step.Run) orelse continue;
|
||||||
if (run.fuzz_tests.items.len > 0 and run.producer != null) {
|
if (run.fuzz_tests.items.len > 0 and run.producer != null) {
|
||||||
thread_pool.spawnWg(&wait_group, rebuildTestsWorkerRun, .{ run, ttyconf, rebuild_node });
|
thread_pool.spawnWg(&wait_group, rebuildTestsWorkerRun, .{ run, ttyconf, rebuild_node });
|
||||||
count += 1;
|
try fuzz_run_steps.append(gpa, run);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (count == 0) fatal("no fuzz tests found", .{});
|
if (fuzz_run_steps.items.len == 0) fatal("no fuzz tests found", .{});
|
||||||
rebuild_node.setEstimatedTotalItems(count);
|
rebuild_node.setEstimatedTotalItems(fuzz_run_steps.items.len);
|
||||||
break :block count;
|
break :block try arena.dupe(*Step.Run, fuzz_run_steps.items);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Detect failure.
|
// Detect failure.
|
||||||
for (all_steps) |step| {
|
for (fuzz_run_steps) |run| {
|
||||||
const run = step.cast(Step.Run) orelse continue;
|
assert(run.fuzz_tests.items.len > 0);
|
||||||
if (run.fuzz_tests.items.len > 0 and run.rebuilt_executable == null)
|
if (run.rebuilt_executable == null)
|
||||||
fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{});
|
fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var web_server: WebServer = .{
|
||||||
|
.gpa = gpa,
|
||||||
|
.global_cache_directory = global_cache_directory,
|
||||||
|
.zig_lib_directory = zig_lib_directory,
|
||||||
|
.zig_exe_path = zig_exe_path,
|
||||||
|
.listen_address = listen_address,
|
||||||
|
.fuzz_run_steps = fuzz_run_steps,
|
||||||
|
|
||||||
|
.msg_queue = .{},
|
||||||
|
.mutex = .{},
|
||||||
|
.condition = .{},
|
||||||
|
|
||||||
|
.coverage_files = .{},
|
||||||
|
.coverage_mutex = .{},
|
||||||
|
.coverage_condition = .{},
|
||||||
|
};
|
||||||
|
|
||||||
|
// For accepting HTTP connections.
|
||||||
|
const web_server_thread = std.Thread.spawn(.{}, WebServer.run, .{&web_server}) catch |err| {
|
||||||
|
fatal("unable to spawn web server thread: {s}", .{@errorName(err)});
|
||||||
|
};
|
||||||
|
defer web_server_thread.join();
|
||||||
|
|
||||||
|
// For polling messages and sending updates to subscribers.
|
||||||
|
const coverage_thread = std.Thread.spawn(.{}, WebServer.coverageRun, .{&web_server}) catch |err| {
|
||||||
|
fatal("unable to spawn coverage thread: {s}", .{@errorName(err)});
|
||||||
|
};
|
||||||
|
defer coverage_thread.join();
|
||||||
|
|
||||||
{
|
{
|
||||||
const fuzz_node = prog_node.start("Fuzzing", count);
|
const fuzz_node = prog_node.start("Fuzzing", fuzz_run_steps.len);
|
||||||
defer fuzz_node.end();
|
defer fuzz_node.end();
|
||||||
var wait_group: std.Thread.WaitGroup = .{};
|
var wait_group: std.Thread.WaitGroup = .{};
|
||||||
defer wait_group.wait();
|
defer wait_group.wait();
|
||||||
|
|
||||||
for (all_steps) |step| {
|
for (fuzz_run_steps) |run| {
|
||||||
const run = step.cast(Step.Run) orelse continue;
|
|
||||||
for (run.fuzz_tests.items) |unit_test_index| {
|
for (run.fuzz_tests.items) |unit_test_index| {
|
||||||
assert(run.rebuilt_executable != null);
|
assert(run.rebuilt_executable != null);
|
||||||
thread_pool.spawnWg(&wait_group, fuzzWorkerRun, .{ run, unit_test_index, ttyconf, fuzz_node });
|
thread_pool.spawnWg(&wait_group, fuzzWorkerRun, .{
|
||||||
|
run, &web_server, unit_test_index, ttyconf, fuzz_node,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fatal("all fuzz workers crashed", .{});
|
log.err("all fuzz workers crashed", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog_node: std.Progress.Node) void {
|
fn 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
|
|||||||
build_runner.printErrorMessages(gpa, &compile.step, ttyconf, stderr, false) catch {};
|
build_runner.printErrorMessages(gpa, &compile.step, ttyconf, stderr, false) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result) |rebuilt_bin_path| {
|
const rebuilt_bin_path = result catch |err| switch (err) {
|
||||||
run.rebuilt_executable = rebuilt_bin_path;
|
error.MakeFailed => return,
|
||||||
} else |err| switch (err) {
|
|
||||||
error.MakeFailed => {},
|
|
||||||
else => {
|
else => {
|
||||||
std.debug.print("step '{s}': failed to rebuild in fuzz mode: {s}\n", .{
|
log.err("step '{s}': failed to rebuild in fuzz mode: {s}", .{
|
||||||
compile.step.name, @errorName(err),
|
compile.step.name, @errorName(err),
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
run.rebuilt_executable = rebuilt_bin_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fuzzWorkerRun(
|
fn fuzzWorkerRun(
|
||||||
run: *Step.Run,
|
run: *Step.Run,
|
||||||
|
web_server: *WebServer,
|
||||||
unit_test_index: u32,
|
unit_test_index: u32,
|
||||||
ttyconf: std.io.tty.Config,
|
ttyconf: std.io.tty.Config,
|
||||||
parent_prog_node: std.Progress.Node,
|
parent_prog_node: std.Progress.Node,
|
||||||
@ -98,17 +144,19 @@ fn fuzzWorkerRun(
|
|||||||
const prog_node = parent_prog_node.start(test_name, 0);
|
const prog_node = parent_prog_node.start(test_name, 0);
|
||||||
defer prog_node.end();
|
defer prog_node.end();
|
||||||
|
|
||||||
run.rerunInFuzzMode(unit_test_index, prog_node) catch |err| switch (err) {
|
run.rerunInFuzzMode(web_server, unit_test_index, prog_node) catch |err| switch (err) {
|
||||||
error.MakeFailed => {
|
error.MakeFailed => {
|
||||||
const stderr = std.io.getStdErr();
|
const stderr = std.io.getStdErr();
|
||||||
std.debug.lockStdErr();
|
std.debug.lockStdErr();
|
||||||
defer std.debug.unlockStdErr();
|
defer std.debug.unlockStdErr();
|
||||||
build_runner.printErrorMessages(gpa, &run.step, ttyconf, stderr, false) catch {};
|
build_runner.printErrorMessages(gpa, &run.step, ttyconf, stderr, false) catch {};
|
||||||
|
return;
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
std.debug.print("step '{s}': failed to rebuild '{s}' in fuzz mode: {s}\n", .{
|
log.err("step '{s}': failed to rerun '{s}' in fuzz mode: {s}", .{
|
||||||
run.step.name, test_name, @errorName(err),
|
run.step.name, test_name, @errorName(err),
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
679
lib/std/Build/Fuzz/WebServer.zig
Normal file
679
lib/std/Build/Fuzz/WebServer.zig
Normal file
@ -0,0 +1,679 @@
|
|||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
const std = @import("../../std.zig");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const Build = std.Build;
|
||||||
|
const Step = std.Build.Step;
|
||||||
|
const Coverage = std.debug.Coverage;
|
||||||
|
const abi = std.Build.Fuzz.abi;
|
||||||
|
const log = std.log;
|
||||||
|
|
||||||
|
const WebServer = @This();
|
||||||
|
|
||||||
|
gpa: Allocator,
|
||||||
|
global_cache_directory: Build.Cache.Directory,
|
||||||
|
zig_lib_directory: Build.Cache.Directory,
|
||||||
|
zig_exe_path: []const u8,
|
||||||
|
listen_address: std.net.Address,
|
||||||
|
fuzz_run_steps: []const *Step.Run,
|
||||||
|
|
||||||
|
/// Messages from fuzz workers. Protected by mutex.
|
||||||
|
msg_queue: std.ArrayListUnmanaged(Msg),
|
||||||
|
/// Protects `msg_queue` only.
|
||||||
|
mutex: std.Thread.Mutex,
|
||||||
|
/// Signaled when there is a message in `msg_queue`.
|
||||||
|
condition: std.Thread.Condition,
|
||||||
|
|
||||||
|
coverage_files: std.AutoArrayHashMapUnmanaged(u64, CoverageMap),
|
||||||
|
/// Protects `coverage_files` only.
|
||||||
|
coverage_mutex: std.Thread.Mutex,
|
||||||
|
/// Signaled when `coverage_files` changes.
|
||||||
|
coverage_condition: std.Thread.Condition,
|
||||||
|
|
||||||
|
const CoverageMap = struct {
|
||||||
|
mapped_memory: []align(std.mem.page_size) const u8,
|
||||||
|
coverage: Coverage,
|
||||||
|
source_locations: []Coverage.SourceLocation,
|
||||||
|
/// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested.
|
||||||
|
entry_points: std.ArrayListUnmanaged(u32),
|
||||||
|
|
||||||
|
fn deinit(cm: *CoverageMap, gpa: Allocator) void {
|
||||||
|
std.posix.munmap(cm.mapped_memory);
|
||||||
|
cm.coverage.deinit(gpa);
|
||||||
|
cm.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Msg = union(enum) {
|
||||||
|
coverage: struct {
|
||||||
|
id: u64,
|
||||||
|
run: *Step.Run,
|
||||||
|
},
|
||||||
|
entry_point: struct {
|
||||||
|
coverage_id: u64,
|
||||||
|
addr: u64,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn run(ws: *WebServer) void {
|
||||||
|
var http_server = ws.listen_address.listen(.{
|
||||||
|
.reuse_address = true,
|
||||||
|
}) catch |err| {
|
||||||
|
log.err("failed to listen to port {d}: {s}", .{ ws.listen_address.in.getPort(), @errorName(err) });
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const port = http_server.listen_address.in.getPort();
|
||||||
|
log.info("web interface listening at http://127.0.0.1:{d}/", .{port});
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const connection = http_server.accept() catch |err| {
|
||||||
|
log.err("failed to accept connection: {s}", .{@errorName(err)});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
_ = std.Thread.spawn(.{}, accept, .{ ws, connection }) catch |err| {
|
||||||
|
log.err("unable to spawn connection thread: {s}", .{@errorName(err)});
|
||||||
|
connection.stream.close();
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept(ws: *WebServer, connection: std.net.Server.Connection) void {
|
||||||
|
defer connection.stream.close();
|
||||||
|
|
||||||
|
var read_buffer: [0x4000]u8 = undefined;
|
||||||
|
var server = std.http.Server.init(connection, &read_buffer);
|
||||||
|
var web_socket: std.http.WebSocket = undefined;
|
||||||
|
var send_buffer: [0x4000]u8 = undefined;
|
||||||
|
var ws_recv_buffer: [0x4000]u8 align(4) = undefined;
|
||||||
|
while (server.state == .ready) {
|
||||||
|
var request = server.receiveHead() catch |err| switch (err) {
|
||||||
|
error.HttpConnectionClosing => return,
|
||||||
|
else => {
|
||||||
|
log.err("closing http connection: {s}", .{@errorName(err)});
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (web_socket.init(&request, &send_buffer, &ws_recv_buffer) catch |err| {
|
||||||
|
log.err("initializing web socket: {s}", .{@errorName(err)});
|
||||||
|
return;
|
||||||
|
}) {
|
||||||
|
serveWebSocket(ws, &web_socket) catch |err| {
|
||||||
|
log.err("unable to serve web socket connection: {s}", .{@errorName(err)});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
serveRequest(ws, &request) catch |err| switch (err) {
|
||||||
|
error.AlreadyReported => return,
|
||||||
|
else => |e| {
|
||||||
|
log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(e) });
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serveRequest(ws: *WebServer, request: *std.http.Server.Request) !void {
|
||||||
|
if (std.mem.eql(u8, request.head.target, "/") or
|
||||||
|
std.mem.eql(u8, request.head.target, "/debug") or
|
||||||
|
std.mem.eql(u8, request.head.target, "/debug/"))
|
||||||
|
{
|
||||||
|
try serveFile(ws, request, "fuzzer/index.html", "text/html");
|
||||||
|
} else if (std.mem.eql(u8, request.head.target, "/main.js") or
|
||||||
|
std.mem.eql(u8, request.head.target, "/debug/main.js"))
|
||||||
|
{
|
||||||
|
try serveFile(ws, request, "fuzzer/main.js", "application/javascript");
|
||||||
|
} else if (std.mem.eql(u8, request.head.target, "/main.wasm")) {
|
||||||
|
try serveWasm(ws, request, .ReleaseFast);
|
||||||
|
} else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) {
|
||||||
|
try serveWasm(ws, request, .Debug);
|
||||||
|
} else if (std.mem.eql(u8, request.head.target, "/sources.tar") or
|
||||||
|
std.mem.eql(u8, request.head.target, "/debug/sources.tar"))
|
||||||
|
{
|
||||||
|
try serveSourcesTar(ws, request);
|
||||||
|
} else {
|
||||||
|
try request.respond("not found", .{
|
||||||
|
.status = .not_found,
|
||||||
|
.extra_headers = &.{
|
||||||
|
.{ .name = "content-type", .value = "text/plain" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serveFile(
|
||||||
|
ws: *WebServer,
|
||||||
|
request: *std.http.Server.Request,
|
||||||
|
name: []const u8,
|
||||||
|
content_type: []const u8,
|
||||||
|
) !void {
|
||||||
|
const gpa = ws.gpa;
|
||||||
|
// The desired API is actually sendfile, which will require enhancing std.http.Server.
|
||||||
|
// We load the file with every request so that the user can make changes to the file
|
||||||
|
// and refresh the HTML page without restarting this server.
|
||||||
|
const file_contents = ws.zig_lib_directory.handle.readFileAlloc(gpa, name, 10 * 1024 * 1024) catch |err| {
|
||||||
|
log.err("failed to read '{}{s}': {s}", .{ ws.zig_lib_directory, name, @errorName(err) });
|
||||||
|
return error.AlreadyReported;
|
||||||
|
};
|
||||||
|
defer gpa.free(file_contents);
|
||||||
|
try request.respond(file_contents, .{
|
||||||
|
.extra_headers = &.{
|
||||||
|
.{ .name = "content-type", .value = content_type },
|
||||||
|
cache_control_header,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serveWasm(
|
||||||
|
ws: *WebServer,
|
||||||
|
request: *std.http.Server.Request,
|
||||||
|
optimize_mode: std.builtin.OptimizeMode,
|
||||||
|
) !void {
|
||||||
|
const gpa = ws.gpa;
|
||||||
|
|
||||||
|
var arena_instance = std.heap.ArenaAllocator.init(gpa);
|
||||||
|
defer arena_instance.deinit();
|
||||||
|
const arena = arena_instance.allocator();
|
||||||
|
|
||||||
|
// Do the compilation every request, so that the user can edit the files
|
||||||
|
// and see the changes without restarting the server.
|
||||||
|
const wasm_binary_path = try buildWasmBinary(ws, arena, optimize_mode);
|
||||||
|
// std.http.Server does not have a sendfile API yet.
|
||||||
|
const file_contents = try std.fs.cwd().readFileAlloc(gpa, wasm_binary_path, 10 * 1024 * 1024);
|
||||||
|
defer gpa.free(file_contents);
|
||||||
|
try request.respond(file_contents, .{
|
||||||
|
.extra_headers = &.{
|
||||||
|
.{ .name = "content-type", .value = "application/wasm" },
|
||||||
|
cache_control_header,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buildWasmBinary(
|
||||||
|
ws: *WebServer,
|
||||||
|
arena: Allocator,
|
||||||
|
optimize_mode: std.builtin.OptimizeMode,
|
||||||
|
) ![]const u8 {
|
||||||
|
const gpa = ws.gpa;
|
||||||
|
|
||||||
|
const main_src_path: Build.Cache.Path = .{
|
||||||
|
.root_dir = ws.zig_lib_directory,
|
||||||
|
.sub_path = "fuzzer/wasm/main.zig",
|
||||||
|
};
|
||||||
|
const walk_src_path: Build.Cache.Path = .{
|
||||||
|
.root_dir = ws.zig_lib_directory,
|
||||||
|
.sub_path = "docs/wasm/Walk.zig",
|
||||||
|
};
|
||||||
|
const html_render_src_path: Build.Cache.Path = .{
|
||||||
|
.root_dir = ws.zig_lib_directory,
|
||||||
|
.sub_path = "docs/wasm/html_render.zig",
|
||||||
|
};
|
||||||
|
|
||||||
|
var argv: std.ArrayListUnmanaged([]const u8) = .{};
|
||||||
|
|
||||||
|
try argv.appendSlice(arena, &.{
|
||||||
|
ws.zig_exe_path, "build-exe", //
|
||||||
|
"-fno-entry", //
|
||||||
|
"-O", @tagName(optimize_mode), //
|
||||||
|
"-target", "wasm32-freestanding", //
|
||||||
|
"-mcpu", "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext", //
|
||||||
|
"--cache-dir", ws.global_cache_directory.path orelse ".", //
|
||||||
|
"--global-cache-dir", ws.global_cache_directory.path orelse ".", //
|
||||||
|
"--name", "fuzzer", //
|
||||||
|
"-rdynamic", //
|
||||||
|
"-fsingle-threaded", //
|
||||||
|
"--dep", "Walk", //
|
||||||
|
"--dep", "html_render", //
|
||||||
|
try std.fmt.allocPrint(arena, "-Mroot={}", .{main_src_path}), //
|
||||||
|
try std.fmt.allocPrint(arena, "-MWalk={}", .{walk_src_path}), //
|
||||||
|
"--dep", "Walk", //
|
||||||
|
try std.fmt.allocPrint(arena, "-Mhtml_render={}", .{html_render_src_path}), //
|
||||||
|
"--listen=-",
|
||||||
|
});
|
||||||
|
|
||||||
|
var child = std.process.Child.init(argv.items, gpa);
|
||||||
|
child.stdin_behavior = .Pipe;
|
||||||
|
child.stdout_behavior = .Pipe;
|
||||||
|
child.stderr_behavior = .Pipe;
|
||||||
|
try child.spawn();
|
||||||
|
|
||||||
|
var poller = std.io.poll(gpa, enum { stdout, stderr }, .{
|
||||||
|
.stdout = child.stdout.?,
|
||||||
|
.stderr = child.stderr.?,
|
||||||
|
});
|
||||||
|
defer poller.deinit();
|
||||||
|
|
||||||
|
try sendMessage(child.stdin.?, .update);
|
||||||
|
try sendMessage(child.stdin.?, .exit);
|
||||||
|
|
||||||
|
const Header = std.zig.Server.Message.Header;
|
||||||
|
var result: ?[]const u8 = null;
|
||||||
|
var result_error_bundle = std.zig.ErrorBundle.empty;
|
||||||
|
|
||||||
|
const stdout = poller.fifo(.stdout);
|
||||||
|
|
||||||
|
poll: while (true) {
|
||||||
|
while (stdout.readableLength() < @sizeOf(Header)) {
|
||||||
|
if (!(try poller.poll())) break :poll;
|
||||||
|
}
|
||||||
|
const header = stdout.reader().readStruct(Header) catch unreachable;
|
||||||
|
while (stdout.readableLength() < header.bytes_len) {
|
||||||
|
if (!(try poller.poll())) break :poll;
|
||||||
|
}
|
||||||
|
const body = stdout.readableSliceOfLen(header.bytes_len);
|
||||||
|
|
||||||
|
switch (header.tag) {
|
||||||
|
.zig_version => {
|
||||||
|
if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
|
||||||
|
return error.ZigProtocolVersionMismatch;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.error_bundle => {
|
||||||
|
const EbHdr = std.zig.Server.Message.ErrorBundle;
|
||||||
|
const eb_hdr = @as(*align(1) const EbHdr, @ptrCast(body));
|
||||||
|
const extra_bytes =
|
||||||
|
body[@sizeOf(EbHdr)..][0 .. @sizeOf(u32) * eb_hdr.extra_len];
|
||||||
|
const string_bytes =
|
||||||
|
body[@sizeOf(EbHdr) + extra_bytes.len ..][0..eb_hdr.string_bytes_len];
|
||||||
|
// TODO: use @ptrCast when the compiler supports it
|
||||||
|
const unaligned_extra = std.mem.bytesAsSlice(u32, extra_bytes);
|
||||||
|
const extra_array = try arena.alloc(u32, unaligned_extra.len);
|
||||||
|
@memcpy(extra_array, unaligned_extra);
|
||||||
|
result_error_bundle = .{
|
||||||
|
.string_bytes = try arena.dupe(u8, string_bytes),
|
||||||
|
.extra = extra_array,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
.emit_bin_path => {
|
||||||
|
const EbpHdr = std.zig.Server.Message.EmitBinPath;
|
||||||
|
const ebp_hdr = @as(*align(1) const EbpHdr, @ptrCast(body));
|
||||||
|
if (!ebp_hdr.flags.cache_hit) {
|
||||||
|
log.info("source changes detected; rebuilt wasm component", .{});
|
||||||
|
}
|
||||||
|
result = try arena.dupe(u8, body[@sizeOf(EbpHdr)..]);
|
||||||
|
},
|
||||||
|
else => {}, // ignore other messages
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.discard(body.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stderr = poller.fifo(.stderr);
|
||||||
|
if (stderr.readableLength() > 0) {
|
||||||
|
const owned_stderr = try stderr.toOwnedSlice();
|
||||||
|
defer gpa.free(owned_stderr);
|
||||||
|
std.debug.print("{s}", .{owned_stderr});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send EOF to stdin.
|
||||||
|
child.stdin.?.close();
|
||||||
|
child.stdin = null;
|
||||||
|
|
||||||
|
switch (try child.wait()) {
|
||||||
|
.Exited => |code| {
|
||||||
|
if (code != 0) {
|
||||||
|
log.err(
|
||||||
|
"the following command exited with error code {d}:\n{s}",
|
||||||
|
.{ code, try Build.Step.allocPrintCmd(arena, null, argv.items) },
|
||||||
|
);
|
||||||
|
return error.WasmCompilationFailed;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Signal, .Stopped, .Unknown => {
|
||||||
|
log.err(
|
||||||
|
"the following command terminated unexpectedly:\n{s}",
|
||||||
|
.{try Build.Step.allocPrintCmd(arena, null, argv.items)},
|
||||||
|
);
|
||||||
|
return error.WasmCompilationFailed;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result_error_bundle.errorMessageCount() > 0) {
|
||||||
|
const color = std.zig.Color.auto;
|
||||||
|
result_error_bundle.renderToStdErr(color.renderOptions());
|
||||||
|
log.err("the following command failed with {d} compilation errors:\n{s}", .{
|
||||||
|
result_error_bundle.errorMessageCount(),
|
||||||
|
try Build.Step.allocPrintCmd(arena, null, argv.items),
|
||||||
|
});
|
||||||
|
return error.WasmCompilationFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result orelse {
|
||||||
|
log.err("child process failed to report result\n{s}", .{
|
||||||
|
try Build.Step.allocPrintCmd(arena, null, argv.items),
|
||||||
|
});
|
||||||
|
return error.WasmCompilationFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sendMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag) !void {
|
||||||
|
const header: std.zig.Client.Message.Header = .{
|
||||||
|
.tag = tag,
|
||||||
|
.bytes_len = 0,
|
||||||
|
};
|
||||||
|
try file.writeAll(std.mem.asBytes(&header));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serveWebSocket(ws: *WebServer, web_socket: *std.http.WebSocket) !void {
|
||||||
|
ws.coverage_mutex.lock();
|
||||||
|
defer ws.coverage_mutex.unlock();
|
||||||
|
|
||||||
|
// On first connection, the client needs all the coverage information
|
||||||
|
// so that subsequent updates can contain only the updated bits.
|
||||||
|
var prev_unique_runs: usize = 0;
|
||||||
|
var prev_entry_points: usize = 0;
|
||||||
|
try sendCoverageContext(ws, web_socket, &prev_unique_runs, &prev_entry_points);
|
||||||
|
while (true) {
|
||||||
|
ws.coverage_condition.timedWait(&ws.coverage_mutex, std.time.ns_per_ms * 500) catch {};
|
||||||
|
try sendCoverageContext(ws, web_socket, &prev_unique_runs, &prev_entry_points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sendCoverageContext(
|
||||||
|
ws: *WebServer,
|
||||||
|
web_socket: *std.http.WebSocket,
|
||||||
|
prev_unique_runs: *usize,
|
||||||
|
prev_entry_points: *usize,
|
||||||
|
) !void {
|
||||||
|
const coverage_maps = ws.coverage_files.values();
|
||||||
|
if (coverage_maps.len == 0) return;
|
||||||
|
// TODO: make each events URL correspond to one coverage map
|
||||||
|
const coverage_map = &coverage_maps[0];
|
||||||
|
const cov_header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
|
||||||
|
const seen_pcs = coverage_map.mapped_memory[@sizeOf(abi.SeenPcsHeader) + coverage_map.source_locations.len * @sizeOf(usize) ..];
|
||||||
|
const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic);
|
||||||
|
const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic);
|
||||||
|
const lowest_stack = @atomicLoad(usize, &cov_header.lowest_stack, .monotonic);
|
||||||
|
if (prev_unique_runs.* != unique_runs) {
|
||||||
|
// There has been an update.
|
||||||
|
if (prev_unique_runs.* == 0) {
|
||||||
|
// We need to send initial context.
|
||||||
|
const header: abi.SourceIndexHeader = .{
|
||||||
|
.flags = .{},
|
||||||
|
.directories_len = @intCast(coverage_map.coverage.directories.entries.len),
|
||||||
|
.files_len = @intCast(coverage_map.coverage.files.entries.len),
|
||||||
|
.source_locations_len = @intCast(coverage_map.source_locations.len),
|
||||||
|
.string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len),
|
||||||
|
};
|
||||||
|
const iovecs: [5]std.posix.iovec_const = .{
|
||||||
|
makeIov(std.mem.asBytes(&header)),
|
||||||
|
makeIov(std.mem.sliceAsBytes(coverage_map.coverage.directories.keys())),
|
||||||
|
makeIov(std.mem.sliceAsBytes(coverage_map.coverage.files.keys())),
|
||||||
|
makeIov(std.mem.sliceAsBytes(coverage_map.source_locations)),
|
||||||
|
makeIov(coverage_map.coverage.string_bytes.items),
|
||||||
|
};
|
||||||
|
try web_socket.writeMessagev(&iovecs, .binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
const header: abi.CoverageUpdateHeader = .{
|
||||||
|
.n_runs = n_runs,
|
||||||
|
.unique_runs = unique_runs,
|
||||||
|
.lowest_stack = lowest_stack,
|
||||||
|
};
|
||||||
|
const iovecs: [2]std.posix.iovec_const = .{
|
||||||
|
makeIov(std.mem.asBytes(&header)),
|
||||||
|
makeIov(seen_pcs),
|
||||||
|
};
|
||||||
|
try web_socket.writeMessagev(&iovecs, .binary);
|
||||||
|
|
||||||
|
prev_unique_runs.* = unique_runs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev_entry_points.* != coverage_map.entry_points.items.len) {
|
||||||
|
const header: abi.EntryPointHeader = .{
|
||||||
|
.flags = .{
|
||||||
|
.locs_len = @intCast(coverage_map.entry_points.items.len),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const iovecs: [2]std.posix.iovec_const = .{
|
||||||
|
makeIov(std.mem.asBytes(&header)),
|
||||||
|
makeIov(std.mem.sliceAsBytes(coverage_map.entry_points.items)),
|
||||||
|
};
|
||||||
|
try web_socket.writeMessagev(&iovecs, .binary);
|
||||||
|
|
||||||
|
prev_entry_points.* = coverage_map.entry_points.items.len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void {
|
||||||
|
const gpa = ws.gpa;
|
||||||
|
|
||||||
|
var arena_instance = std.heap.ArenaAllocator.init(gpa);
|
||||||
|
defer arena_instance.deinit();
|
||||||
|
const arena = arena_instance.allocator();
|
||||||
|
|
||||||
|
var send_buffer: [0x4000]u8 = undefined;
|
||||||
|
var response = request.respondStreaming(.{
|
||||||
|
.send_buffer = &send_buffer,
|
||||||
|
.respond_options = .{
|
||||||
|
.extra_headers = &.{
|
||||||
|
.{ .name = "content-type", .value = "application/x-tar" },
|
||||||
|
cache_control_header,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const w = response.writer();
|
||||||
|
|
||||||
|
const DedupeTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false);
|
||||||
|
var dedupe_table: DedupeTable = .{};
|
||||||
|
defer dedupe_table.deinit(gpa);
|
||||||
|
|
||||||
|
for (ws.fuzz_run_steps) |run_step| {
|
||||||
|
const compile_step_inputs = run_step.producer.?.step.inputs.table;
|
||||||
|
for (compile_step_inputs.keys(), compile_step_inputs.values()) |dir_path, *file_list| {
|
||||||
|
try dedupe_table.ensureUnusedCapacity(gpa, file_list.items.len);
|
||||||
|
for (file_list.items) |sub_path| {
|
||||||
|
// Special file "." means the entire directory.
|
||||||
|
if (std.mem.eql(u8, sub_path, ".")) continue;
|
||||||
|
const joined_path = try dir_path.join(arena, sub_path);
|
||||||
|
_ = dedupe_table.getOrPutAssumeCapacity(joined_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deduped_paths = dedupe_table.keys();
|
||||||
|
const SortContext = struct {
|
||||||
|
pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool {
|
||||||
|
_ = this;
|
||||||
|
return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) {
|
||||||
|
.lt => true,
|
||||||
|
.gt => false,
|
||||||
|
.eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan);
|
||||||
|
|
||||||
|
var cwd_cache: ?[]const u8 = null;
|
||||||
|
|
||||||
|
for (deduped_paths) |joined_path| {
|
||||||
|
var file = joined_path.root_dir.handle.openFile(joined_path.sub_path, .{}) catch |err| {
|
||||||
|
log.err("failed to open {}: {s}", .{ joined_path, @errorName(err) });
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
const stat = file.stat() catch |err| {
|
||||||
|
log.err("failed to stat {}: {s}", .{ joined_path, @errorName(err) });
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if (stat.kind != .file)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const padding = p: {
|
||||||
|
const remainder = stat.size % 512;
|
||||||
|
break :p if (remainder > 0) 512 - remainder else 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
var file_header = std.tar.output.Header.init();
|
||||||
|
file_header.typeflag = .regular;
|
||||||
|
try file_header.setPath(
|
||||||
|
joined_path.root_dir.path orelse try memoizedCwd(arena, &cwd_cache),
|
||||||
|
joined_path.sub_path,
|
||||||
|
);
|
||||||
|
try file_header.setSize(stat.size);
|
||||||
|
try file_header.updateChecksum();
|
||||||
|
try w.writeAll(std.mem.asBytes(&file_header));
|
||||||
|
try w.writeFile(file);
|
||||||
|
try w.writeByteNTimes(0, padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// intentionally omitting the pointless trailer
|
||||||
|
//try w.writeByteNTimes(0, 512 * 2);
|
||||||
|
try response.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memoizedCwd(arena: Allocator, opt_ptr: *?[]const u8) ![]const u8 {
|
||||||
|
if (opt_ptr.*) |cached| return cached;
|
||||||
|
const result = try std.process.getCwdAlloc(arena);
|
||||||
|
opt_ptr.* = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache_control_header: std.http.Header = .{
|
||||||
|
.name = "cache-control",
|
||||||
|
.value = "max-age=0, must-revalidate",
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn coverageRun(ws: *WebServer) void {
|
||||||
|
ws.mutex.lock();
|
||||||
|
defer ws.mutex.unlock();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
ws.condition.wait(&ws.mutex);
|
||||||
|
for (ws.msg_queue.items) |msg| switch (msg) {
|
||||||
|
.coverage => |coverage| prepareTables(ws, coverage.run, coverage.id) catch |err| switch (err) {
|
||||||
|
error.AlreadyReported => continue,
|
||||||
|
else => |e| log.err("failed to prepare code coverage tables: {s}", .{@errorName(e)}),
|
||||||
|
},
|
||||||
|
.entry_point => |entry_point| addEntryPoint(ws, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) {
|
||||||
|
error.AlreadyReported => continue,
|
||||||
|
else => |e| log.err("failed to prepare code coverage tables: {s}", .{@errorName(e)}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ws.msg_queue.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepareTables(
|
||||||
|
ws: *WebServer,
|
||||||
|
run_step: *Step.Run,
|
||||||
|
coverage_id: u64,
|
||||||
|
) error{ OutOfMemory, AlreadyReported }!void {
|
||||||
|
const gpa = ws.gpa;
|
||||||
|
|
||||||
|
ws.coverage_mutex.lock();
|
||||||
|
defer ws.coverage_mutex.unlock();
|
||||||
|
|
||||||
|
const gop = try ws.coverage_files.getOrPut(gpa, coverage_id);
|
||||||
|
if (gop.found_existing) {
|
||||||
|
// We are fuzzing the same executable with multiple threads.
|
||||||
|
// Perhaps the same unit test; perhaps a different one. In any
|
||||||
|
// case, since the coverage file is the same, we only have to
|
||||||
|
// notice changes to that one file in order to learn coverage for
|
||||||
|
// this particular executable.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
errdefer _ = ws.coverage_files.pop();
|
||||||
|
|
||||||
|
gop.value_ptr.* = .{
|
||||||
|
.coverage = std.debug.Coverage.init,
|
||||||
|
.mapped_memory = undefined, // populated below
|
||||||
|
.source_locations = undefined, // populated below
|
||||||
|
.entry_points = .{},
|
||||||
|
};
|
||||||
|
errdefer gop.value_ptr.coverage.deinit(gpa);
|
||||||
|
|
||||||
|
const rebuilt_exe_path: Build.Cache.Path = .{
|
||||||
|
.root_dir = Build.Cache.Directory.cwd(),
|
||||||
|
.sub_path = run_step.rebuilt_executable.?,
|
||||||
|
};
|
||||||
|
var debug_info = std.debug.Info.load(gpa, rebuilt_exe_path, &gop.value_ptr.coverage) catch |err| {
|
||||||
|
log.err("step '{s}': failed to load debug information for '{}': {s}", .{
|
||||||
|
run_step.step.name, rebuilt_exe_path, @errorName(err),
|
||||||
|
});
|
||||||
|
return error.AlreadyReported;
|
||||||
|
};
|
||||||
|
defer debug_info.deinit(gpa);
|
||||||
|
|
||||||
|
const coverage_file_path: Build.Cache.Path = .{
|
||||||
|
.root_dir = run_step.step.owner.cache_root,
|
||||||
|
.sub_path = "v/" ++ std.fmt.hex(coverage_id),
|
||||||
|
};
|
||||||
|
var coverage_file = coverage_file_path.root_dir.handle.openFile(coverage_file_path.sub_path, .{}) catch |err| {
|
||||||
|
log.err("step '{s}': failed to load coverage file '{}': {s}", .{
|
||||||
|
run_step.step.name, coverage_file_path, @errorName(err),
|
||||||
|
});
|
||||||
|
return error.AlreadyReported;
|
||||||
|
};
|
||||||
|
defer coverage_file.close();
|
||||||
|
|
||||||
|
const file_size = coverage_file.getEndPos() catch |err| {
|
||||||
|
log.err("unable to check len of coverage file '{}': {s}", .{ coverage_file_path, @errorName(err) });
|
||||||
|
return error.AlreadyReported;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapped_memory = std.posix.mmap(
|
||||||
|
null,
|
||||||
|
file_size,
|
||||||
|
std.posix.PROT.READ,
|
||||||
|
.{ .TYPE = .SHARED },
|
||||||
|
coverage_file.handle,
|
||||||
|
0,
|
||||||
|
) catch |err| {
|
||||||
|
log.err("failed to map coverage file '{}': {s}", .{ coverage_file_path, @errorName(err) });
|
||||||
|
return error.AlreadyReported;
|
||||||
|
};
|
||||||
|
gop.value_ptr.mapped_memory = mapped_memory;
|
||||||
|
|
||||||
|
const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
|
||||||
|
const pcs_bytes = mapped_memory[@sizeOf(abi.SeenPcsHeader)..][0 .. header.pcs_len * @sizeOf(usize)];
|
||||||
|
const pcs = std.mem.bytesAsSlice(usize, pcs_bytes);
|
||||||
|
const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len);
|
||||||
|
errdefer gpa.free(source_locations);
|
||||||
|
debug_info.resolveAddresses(gpa, pcs, source_locations) catch |err| {
|
||||||
|
log.err("failed to resolve addresses to source locations: {s}", .{@errorName(err)});
|
||||||
|
return error.AlreadyReported;
|
||||||
|
};
|
||||||
|
gop.value_ptr.source_locations = source_locations;
|
||||||
|
|
||||||
|
ws.coverage_condition.broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addEntryPoint(ws: *WebServer, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory }!void {
|
||||||
|
ws.coverage_mutex.lock();
|
||||||
|
defer ws.coverage_mutex.unlock();
|
||||||
|
|
||||||
|
const coverage_map = ws.coverage_files.getPtr(coverage_id).?;
|
||||||
|
const ptr = coverage_map.mapped_memory;
|
||||||
|
const pcs_bytes = ptr[@sizeOf(abi.SeenPcsHeader)..][0 .. coverage_map.source_locations.len * @sizeOf(usize)];
|
||||||
|
const pcs: []const usize = @alignCast(std.mem.bytesAsSlice(usize, pcs_bytes));
|
||||||
|
const index = std.sort.upperBound(usize, pcs, addr, struct {
|
||||||
|
fn order(context: usize, item: usize) std.math.Order {
|
||||||
|
return std.math.order(item, context);
|
||||||
|
}
|
||||||
|
}.order);
|
||||||
|
if (index >= pcs.len) {
|
||||||
|
log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{
|
||||||
|
addr, pcs[0], pcs[pcs.len - 1],
|
||||||
|
});
|
||||||
|
return error.AlreadyReported;
|
||||||
|
}
|
||||||
|
if (false) {
|
||||||
|
const sl = coverage_map.source_locations[index];
|
||||||
|
const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename);
|
||||||
|
log.debug("server found entry point {s}:{d}:{d}", .{
|
||||||
|
file_name, sl.line, sl.column,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const gpa = ws.gpa;
|
||||||
|
try coverage_map.entry_points.append(gpa, @intCast(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn makeIov(s: []const u8) std.posix.iovec_const {
|
||||||
|
return .{
|
||||||
|
.base = s.ptr,
|
||||||
|
.len = s.len,
|
||||||
|
};
|
||||||
|
}
|
||||||
69
lib/std/Build/Fuzz/abi.zig
Normal file
69
lib/std/Build/Fuzz/abi.zig
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
//! This file is shared among Zig code running in wildly different contexts:
|
||||||
|
//! libfuzzer, compiled alongside unit tests, the build runner, running on the
|
||||||
|
//! host computer, and the fuzzing web interface webassembly code running in
|
||||||
|
//! the browser. All of these components interface to some degree via an ABI.
|
||||||
|
|
||||||
|
/// libfuzzer uses this and its usize is the one that counts. To match the ABI,
|
||||||
|
/// make the ints be the size of the target used with libfuzzer.
|
||||||
|
///
|
||||||
|
/// Trailing:
|
||||||
|
/// * pc_addr: usize for each pcs_len
|
||||||
|
/// * 1 bit per pc_addr, usize elements
|
||||||
|
pub const SeenPcsHeader = extern struct {
|
||||||
|
n_runs: usize,
|
||||||
|
unique_runs: usize,
|
||||||
|
pcs_len: usize,
|
||||||
|
lowest_stack: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ToClientTag = enum(u8) {
|
||||||
|
source_index,
|
||||||
|
coverage_update,
|
||||||
|
entry_points,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Sent to the fuzzer web client on first connection to the websocket URL.
|
||||||
|
///
|
||||||
|
/// Trailing:
|
||||||
|
/// * std.debug.Coverage.String for each directories_len
|
||||||
|
/// * std.debug.Coverage.File for each files_len
|
||||||
|
/// * std.debug.Coverage.SourceLocation for each source_locations_len
|
||||||
|
/// * u8 for each string_bytes_len
|
||||||
|
pub const SourceIndexHeader = extern struct {
|
||||||
|
flags: Flags,
|
||||||
|
directories_len: u32,
|
||||||
|
files_len: u32,
|
||||||
|
source_locations_len: u32,
|
||||||
|
string_bytes_len: u32,
|
||||||
|
|
||||||
|
pub const Flags = packed struct(u32) {
|
||||||
|
tag: ToClientTag = .source_index,
|
||||||
|
_: u24 = 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Sent to the fuzzer web client whenever the set of covered source locations
|
||||||
|
/// changes.
|
||||||
|
///
|
||||||
|
/// Trailing:
|
||||||
|
/// * one bit per source_locations_len, contained in u8 elements
|
||||||
|
pub const CoverageUpdateHeader = extern struct {
|
||||||
|
tag: ToClientTag = .coverage_update,
|
||||||
|
n_runs: u64 align(1),
|
||||||
|
unique_runs: u64 align(1),
|
||||||
|
lowest_stack: u64 align(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Sent to the fuzzer web client when the set of entry points is updated.
|
||||||
|
///
|
||||||
|
/// Trailing:
|
||||||
|
/// * one u32 index of source_locations per locs_len
|
||||||
|
pub const EntryPointHeader = extern struct {
|
||||||
|
flags: Flags,
|
||||||
|
|
||||||
|
pub const Flags = packed struct(u32) {
|
||||||
|
tag: ToClientTag = .entry_points,
|
||||||
|
locs_len: u24,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -559,7 +559,8 @@ fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool) !?[]const u8 {
|
|||||||
},
|
},
|
||||||
.zig_lib => zl: {
|
.zig_lib => zl: {
|
||||||
if (s.cast(Step.Compile)) |compile| {
|
if (s.cast(Step.Compile)) |compile| {
|
||||||
if (compile.zig_lib_dir) |lp| {
|
if (compile.zig_lib_dir) |zig_lib_dir| {
|
||||||
|
const lp = try zig_lib_dir.join(arena, sub_path);
|
||||||
try addWatchInput(s, lp);
|
try addWatchInput(s, lp);
|
||||||
break :zl;
|
break :zl;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -205,6 +205,7 @@ pub fn enableTestRunnerMode(run: *Run) void {
|
|||||||
run.stdio = .zig_test;
|
run.stdio = .zig_test;
|
||||||
run.addArgs(&.{
|
run.addArgs(&.{
|
||||||
std.fmt.allocPrint(arena, "--seed=0x{x}", .{b.graph.random_seed}) catch @panic("OOM"),
|
std.fmt.allocPrint(arena, "--seed=0x{x}", .{b.graph.random_seed}) catch @panic("OOM"),
|
||||||
|
std.fmt.allocPrint(arena, "--cache-dir={s}", .{b.cache_root.path orelse ""}) catch @panic("OOM"),
|
||||||
"--listen=-",
|
"--listen=-",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -845,7 +846,12 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rerunInFuzzMode(run: *Run, unit_test_index: u32, prog_node: std.Progress.Node) !void {
|
pub fn rerunInFuzzMode(
|
||||||
|
run: *Run,
|
||||||
|
web_server: *std.Build.Fuzz.WebServer,
|
||||||
|
unit_test_index: u32,
|
||||||
|
prog_node: std.Progress.Node,
|
||||||
|
) !void {
|
||||||
const step = &run.step;
|
const step = &run.step;
|
||||||
const b = step.owner;
|
const b = step.owner;
|
||||||
const arena = b.allocator;
|
const arena = b.allocator;
|
||||||
@ -877,7 +883,10 @@ pub fn rerunInFuzzMode(run: *Run, unit_test_index: u32, prog_node: std.Progress.
|
|||||||
const has_side_effects = false;
|
const has_side_effects = false;
|
||||||
const rand_int = std.crypto.random.int(u64);
|
const rand_int = std.crypto.random.int(u64);
|
||||||
const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
|
const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
|
||||||
try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, prog_node, unit_test_index);
|
try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, prog_node, .{
|
||||||
|
.unit_test_index = unit_test_index,
|
||||||
|
.web_server = web_server,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn populateGeneratedPaths(
|
fn populateGeneratedPaths(
|
||||||
@ -952,13 +961,18 @@ fn termMatches(expected: ?std.process.Child.Term, actual: std.process.Child.Term
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FuzzContext = struct {
|
||||||
|
web_server: *std.Build.Fuzz.WebServer,
|
||||||
|
unit_test_index: u32,
|
||||||
|
};
|
||||||
|
|
||||||
fn runCommand(
|
fn runCommand(
|
||||||
run: *Run,
|
run: *Run,
|
||||||
argv: []const []const u8,
|
argv: []const []const u8,
|
||||||
has_side_effects: bool,
|
has_side_effects: bool,
|
||||||
output_dir_path: []const u8,
|
output_dir_path: []const u8,
|
||||||
prog_node: std.Progress.Node,
|
prog_node: std.Progress.Node,
|
||||||
fuzz_unit_test_index: ?u32,
|
fuzz_context: ?FuzzContext,
|
||||||
) !void {
|
) !void {
|
||||||
const step = &run.step;
|
const step = &run.step;
|
||||||
const b = step.owner;
|
const b = step.owner;
|
||||||
@ -977,7 +991,7 @@ fn runCommand(
|
|||||||
var interp_argv = std.ArrayList([]const u8).init(b.allocator);
|
var interp_argv = std.ArrayList([]const u8).init(b.allocator);
|
||||||
defer interp_argv.deinit();
|
defer interp_argv.deinit();
|
||||||
|
|
||||||
const result = spawnChildAndCollect(run, argv, has_side_effects, prog_node, fuzz_unit_test_index) catch |err| term: {
|
const result = spawnChildAndCollect(run, argv, has_side_effects, prog_node, fuzz_context) catch |err| term: {
|
||||||
// InvalidExe: cpu arch mismatch
|
// InvalidExe: cpu arch mismatch
|
||||||
// FileNotFound: can happen with a wrong dynamic linker path
|
// FileNotFound: can happen with a wrong dynamic linker path
|
||||||
if (err == error.InvalidExe or err == error.FileNotFound) interpret: {
|
if (err == error.InvalidExe or err == error.FileNotFound) interpret: {
|
||||||
@ -1113,7 +1127,7 @@ fn runCommand(
|
|||||||
|
|
||||||
try Step.handleVerbose2(step.owner, cwd, run.env_map, interp_argv.items);
|
try Step.handleVerbose2(step.owner, cwd, run.env_map, interp_argv.items);
|
||||||
|
|
||||||
break :term spawnChildAndCollect(run, interp_argv.items, has_side_effects, prog_node, fuzz_unit_test_index) catch |e| {
|
break :term spawnChildAndCollect(run, interp_argv.items, has_side_effects, prog_node, fuzz_context) catch |e| {
|
||||||
if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;
|
if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;
|
||||||
|
|
||||||
return step.fail("unable to spawn interpreter {s}: {s}", .{
|
return step.fail("unable to spawn interpreter {s}: {s}", .{
|
||||||
@ -1133,7 +1147,7 @@ fn runCommand(
|
|||||||
|
|
||||||
const final_argv = if (interp_argv.items.len == 0) argv else interp_argv.items;
|
const final_argv = if (interp_argv.items.len == 0) argv else interp_argv.items;
|
||||||
|
|
||||||
if (fuzz_unit_test_index != null) {
|
if (fuzz_context != null) {
|
||||||
try step.handleChildProcessTerm(result.term, cwd, final_argv);
|
try step.handleChildProcessTerm(result.term, cwd, final_argv);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1298,12 +1312,12 @@ fn spawnChildAndCollect(
|
|||||||
argv: []const []const u8,
|
argv: []const []const u8,
|
||||||
has_side_effects: bool,
|
has_side_effects: bool,
|
||||||
prog_node: std.Progress.Node,
|
prog_node: std.Progress.Node,
|
||||||
fuzz_unit_test_index: ?u32,
|
fuzz_context: ?FuzzContext,
|
||||||
) !ChildProcResult {
|
) !ChildProcResult {
|
||||||
const b = run.step.owner;
|
const b = run.step.owner;
|
||||||
const arena = b.allocator;
|
const arena = b.allocator;
|
||||||
|
|
||||||
if (fuzz_unit_test_index != null) {
|
if (fuzz_context != null) {
|
||||||
assert(!has_side_effects);
|
assert(!has_side_effects);
|
||||||
assert(run.stdio == .zig_test);
|
assert(run.stdio == .zig_test);
|
||||||
}
|
}
|
||||||
@ -1357,7 +1371,7 @@ fn spawnChildAndCollect(
|
|||||||
var timer = try std.time.Timer.start();
|
var timer = try std.time.Timer.start();
|
||||||
|
|
||||||
const result = if (run.stdio == .zig_test)
|
const result = if (run.stdio == .zig_test)
|
||||||
evalZigTest(run, &child, prog_node, fuzz_unit_test_index)
|
evalZigTest(run, &child, prog_node, fuzz_context)
|
||||||
else
|
else
|
||||||
evalGeneric(run, &child);
|
evalGeneric(run, &child);
|
||||||
|
|
||||||
@ -1383,7 +1397,7 @@ fn evalZigTest(
|
|||||||
run: *Run,
|
run: *Run,
|
||||||
child: *std.process.Child,
|
child: *std.process.Child,
|
||||||
prog_node: std.Progress.Node,
|
prog_node: std.Progress.Node,
|
||||||
fuzz_unit_test_index: ?u32,
|
fuzz_context: ?FuzzContext,
|
||||||
) !StdIoResult {
|
) !StdIoResult {
|
||||||
const gpa = run.step.owner.allocator;
|
const gpa = run.step.owner.allocator;
|
||||||
const arena = run.step.owner.allocator;
|
const arena = run.step.owner.allocator;
|
||||||
@ -1394,8 +1408,8 @@ fn evalZigTest(
|
|||||||
});
|
});
|
||||||
defer poller.deinit();
|
defer poller.deinit();
|
||||||
|
|
||||||
if (fuzz_unit_test_index) |index| {
|
if (fuzz_context) |fuzz| {
|
||||||
try sendRunTestMessage(child.stdin.?, .start_fuzzing, index);
|
try sendRunTestMessage(child.stdin.?, .start_fuzzing, fuzz.unit_test_index);
|
||||||
} else {
|
} else {
|
||||||
run.fuzz_tests.clearRetainingCapacity();
|
run.fuzz_tests.clearRetainingCapacity();
|
||||||
try sendMessage(child.stdin.?, .query_test_metadata);
|
try sendMessage(child.stdin.?, .query_test_metadata);
|
||||||
@ -1413,6 +1427,7 @@ fn evalZigTest(
|
|||||||
var log_err_count: u32 = 0;
|
var log_err_count: u32 = 0;
|
||||||
|
|
||||||
var metadata: ?TestMetadata = null;
|
var metadata: ?TestMetadata = null;
|
||||||
|
var coverage_id: ?u64 = null;
|
||||||
|
|
||||||
var sub_prog_node: ?std.Progress.Node = null;
|
var sub_prog_node: ?std.Progress.Node = null;
|
||||||
defer if (sub_prog_node) |n| n.end();
|
defer if (sub_prog_node) |n| n.end();
|
||||||
@ -1437,7 +1452,7 @@ fn evalZigTest(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
.test_metadata => {
|
.test_metadata => {
|
||||||
assert(fuzz_unit_test_index == null);
|
assert(fuzz_context == null);
|
||||||
const TmHdr = std.zig.Server.Message.TestMetadata;
|
const TmHdr = std.zig.Server.Message.TestMetadata;
|
||||||
const tm_hdr = @as(*align(1) const TmHdr, @ptrCast(body));
|
const tm_hdr = @as(*align(1) const TmHdr, @ptrCast(body));
|
||||||
test_count = tm_hdr.tests_len;
|
test_count = tm_hdr.tests_len;
|
||||||
@ -1466,7 +1481,7 @@ fn evalZigTest(
|
|||||||
try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);
|
try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);
|
||||||
},
|
},
|
||||||
.test_results => {
|
.test_results => {
|
||||||
assert(fuzz_unit_test_index == null);
|
assert(fuzz_context == null);
|
||||||
const md = metadata.?;
|
const md = metadata.?;
|
||||||
|
|
||||||
const TrHdr = std.zig.Server.Message.TestResults;
|
const TrHdr = std.zig.Server.Message.TestResults;
|
||||||
@ -1500,6 +1515,34 @@ fn evalZigTest(
|
|||||||
|
|
||||||
try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);
|
try requestNextTest(child.stdin.?, &metadata.?, &sub_prog_node);
|
||||||
},
|
},
|
||||||
|
.coverage_id => {
|
||||||
|
const web_server = fuzz_context.?.web_server;
|
||||||
|
const msg_ptr: *align(1) const u64 = @ptrCast(body);
|
||||||
|
coverage_id = msg_ptr.*;
|
||||||
|
{
|
||||||
|
web_server.mutex.lock();
|
||||||
|
defer web_server.mutex.unlock();
|
||||||
|
try web_server.msg_queue.append(web_server.gpa, .{ .coverage = .{
|
||||||
|
.id = coverage_id.?,
|
||||||
|
.run = run,
|
||||||
|
} });
|
||||||
|
web_server.condition.signal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.fuzz_start_addr => {
|
||||||
|
const web_server = fuzz_context.?.web_server;
|
||||||
|
const msg_ptr: *align(1) const u64 = @ptrCast(body);
|
||||||
|
const addr = msg_ptr.*;
|
||||||
|
{
|
||||||
|
web_server.mutex.lock();
|
||||||
|
defer web_server.mutex.unlock();
|
||||||
|
try web_server.msg_queue.append(web_server.gpa, .{ .entry_point = .{
|
||||||
|
.addr = addr,
|
||||||
|
.coverage_id = coverage_id.?,
|
||||||
|
} });
|
||||||
|
web_server.condition.signal();
|
||||||
|
}
|
||||||
|
},
|
||||||
else => {}, // ignore other messages
|
else => {}, // ignore other messages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,9 +14,12 @@ const native_os = builtin.os.tag;
|
|||||||
const native_endian = native_arch.endian();
|
const native_endian = native_arch.endian();
|
||||||
|
|
||||||
pub const MemoryAccessor = @import("debug/MemoryAccessor.zig");
|
pub const MemoryAccessor = @import("debug/MemoryAccessor.zig");
|
||||||
|
pub const FixedBufferReader = @import("debug/FixedBufferReader.zig");
|
||||||
pub const Dwarf = @import("debug/Dwarf.zig");
|
pub const Dwarf = @import("debug/Dwarf.zig");
|
||||||
pub const Pdb = @import("debug/Pdb.zig");
|
pub const Pdb = @import("debug/Pdb.zig");
|
||||||
pub const SelfInfo = @import("debug/SelfInfo.zig");
|
pub const SelfInfo = @import("debug/SelfInfo.zig");
|
||||||
|
pub const Info = @import("debug/Info.zig");
|
||||||
|
pub const Coverage = @import("debug/Coverage.zig");
|
||||||
|
|
||||||
/// Unresolved source locations can be represented with a single `usize` that
|
/// Unresolved source locations can be represented with a single `usize` that
|
||||||
/// corresponds to a virtual memory address of the program counter. Combined
|
/// corresponds to a virtual memory address of the program counter. Combined
|
||||||
@ -26,6 +29,18 @@ pub const SourceLocation = struct {
|
|||||||
line: u64,
|
line: u64,
|
||||||
column: u64,
|
column: u64,
|
||||||
file_name: []const u8,
|
file_name: []const u8,
|
||||||
|
|
||||||
|
pub const invalid: SourceLocation = .{
|
||||||
|
.line = 0,
|
||||||
|
.column = 0,
|
||||||
|
.file_name = &.{},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Symbol = struct {
|
||||||
|
name: []const u8 = "???",
|
||||||
|
compile_unit_name: []const u8 = "???",
|
||||||
|
source_location: ?SourceLocation = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Deprecated because it returns the optimization mode of the standard
|
/// Deprecated because it returns the optimization mode of the standard
|
||||||
@ -748,7 +763,7 @@ pub fn writeCurrentStackTrace(
|
|||||||
// an overflow. We do not need to signal `StackIterator` as it will correctly detect this
|
// an overflow. We do not need to signal `StackIterator` as it will correctly detect this
|
||||||
// condition on the subsequent iteration and return `null` thus terminating the loop.
|
// condition on the subsequent iteration and return `null` thus terminating the loop.
|
||||||
// same behaviour for x86-windows-msvc
|
// same behaviour for x86-windows-msvc
|
||||||
const address = if (return_address == 0) return_address else return_address - 1;
|
const address = return_address -| 1;
|
||||||
try printSourceAtAddress(debug_info, out_stream, address, tty_config);
|
try printSourceAtAddress(debug_info, out_stream, address, tty_config);
|
||||||
} else printLastUnwindError(&it, debug_info, out_stream, tty_config);
|
} else printLastUnwindError(&it, debug_info, out_stream, tty_config);
|
||||||
}
|
}
|
||||||
@ -871,13 +886,13 @@ pub fn printSourceAtAddress(debug_info: *SelfInfo, out_stream: anytype, address:
|
|||||||
error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config),
|
error.MissingDebugInfo, error.InvalidDebugInfo => return printUnknownSource(debug_info, out_stream, address, tty_config),
|
||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
defer symbol_info.deinit(debug_info.allocator);
|
defer if (symbol_info.source_location) |sl| debug_info.allocator.free(sl.file_name);
|
||||||
|
|
||||||
return printLineInfo(
|
return printLineInfo(
|
||||||
out_stream,
|
out_stream,
|
||||||
symbol_info.line_info,
|
symbol_info.source_location,
|
||||||
address,
|
address,
|
||||||
symbol_info.symbol_name,
|
symbol_info.name,
|
||||||
symbol_info.compile_unit_name,
|
symbol_info.compile_unit_name,
|
||||||
tty_config,
|
tty_config,
|
||||||
printLineFromFileAnyOs,
|
printLineFromFileAnyOs,
|
||||||
@ -886,7 +901,7 @@ pub fn printSourceAtAddress(debug_info: *SelfInfo, out_stream: anytype, address:
|
|||||||
|
|
||||||
fn printLineInfo(
|
fn printLineInfo(
|
||||||
out_stream: anytype,
|
out_stream: anytype,
|
||||||
line_info: ?SourceLocation,
|
source_location: ?SourceLocation,
|
||||||
address: usize,
|
address: usize,
|
||||||
symbol_name: []const u8,
|
symbol_name: []const u8,
|
||||||
compile_unit_name: []const u8,
|
compile_unit_name: []const u8,
|
||||||
@ -896,8 +911,8 @@ fn printLineInfo(
|
|||||||
nosuspend {
|
nosuspend {
|
||||||
try tty_config.setColor(out_stream, .bold);
|
try tty_config.setColor(out_stream, .bold);
|
||||||
|
|
||||||
if (line_info) |*li| {
|
if (source_location) |*sl| {
|
||||||
try out_stream.print("{s}:{d}:{d}", .{ li.file_name, li.line, li.column });
|
try out_stream.print("{s}:{d}:{d}", .{ sl.file_name, sl.line, sl.column });
|
||||||
} else {
|
} else {
|
||||||
try out_stream.writeAll("???:?:?");
|
try out_stream.writeAll("???:?:?");
|
||||||
}
|
}
|
||||||
@ -910,11 +925,11 @@ fn printLineInfo(
|
|||||||
try out_stream.writeAll("\n");
|
try out_stream.writeAll("\n");
|
||||||
|
|
||||||
// Show the matching source code line if possible
|
// Show the matching source code line if possible
|
||||||
if (line_info) |li| {
|
if (source_location) |sl| {
|
||||||
if (printLineFromFile(out_stream, li)) {
|
if (printLineFromFile(out_stream, sl)) {
|
||||||
if (li.column > 0) {
|
if (sl.column > 0) {
|
||||||
// The caret already takes one char
|
// The caret already takes one char
|
||||||
const space_needed = @as(usize, @intCast(li.column - 1));
|
const space_needed = @as(usize, @intCast(sl.column - 1));
|
||||||
|
|
||||||
try out_stream.writeByteNTimes(' ', space_needed);
|
try out_stream.writeByteNTimes(' ', space_needed);
|
||||||
try tty_config.setColor(out_stream, .green);
|
try tty_config.setColor(out_stream, .green);
|
||||||
@ -932,10 +947,10 @@ fn printLineInfo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn printLineFromFileAnyOs(out_stream: anytype, line_info: SourceLocation) !void {
|
fn printLineFromFileAnyOs(out_stream: anytype, source_location: SourceLocation) !void {
|
||||||
// Need this to always block even in async I/O mode, because this could potentially
|
// Need this to always block even in async I/O mode, because this could potentially
|
||||||
// be called from e.g. the event loop code crashing.
|
// be called from e.g. the event loop code crashing.
|
||||||
var f = try fs.cwd().openFile(line_info.file_name, .{});
|
var f = try fs.cwd().openFile(source_location.file_name, .{});
|
||||||
defer f.close();
|
defer f.close();
|
||||||
// TODO fstat and make sure that the file has the correct size
|
// TODO fstat and make sure that the file has the correct size
|
||||||
|
|
||||||
@ -944,7 +959,7 @@ fn printLineFromFileAnyOs(out_stream: anytype, line_info: SourceLocation) !void
|
|||||||
const line_start = seek: {
|
const line_start = seek: {
|
||||||
var current_line_start: usize = 0;
|
var current_line_start: usize = 0;
|
||||||
var next_line: usize = 1;
|
var next_line: usize = 1;
|
||||||
while (next_line != line_info.line) {
|
while (next_line != source_location.line) {
|
||||||
const slice = buf[current_line_start..amt_read];
|
const slice = buf[current_line_start..amt_read];
|
||||||
if (mem.indexOfScalar(u8, slice, '\n')) |pos| {
|
if (mem.indexOfScalar(u8, slice, '\n')) |pos| {
|
||||||
next_line += 1;
|
next_line += 1;
|
||||||
@ -1481,99 +1496,6 @@ pub const SafetyLock = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Deprecated. Don't use this, just read from your memory directly.
|
|
||||||
///
|
|
||||||
/// This only exists because someone was too lazy to rework logic that used to
|
|
||||||
/// operate on an open file to operate on a memory buffer instead.
|
|
||||||
pub const DeprecatedFixedBufferReader = struct {
|
|
||||||
buf: []const u8,
|
|
||||||
pos: usize = 0,
|
|
||||||
endian: std.builtin.Endian,
|
|
||||||
|
|
||||||
pub const Error = error{ EndOfBuffer, Overflow, InvalidBuffer };
|
|
||||||
|
|
||||||
pub fn seekTo(fbr: *DeprecatedFixedBufferReader, pos: u64) Error!void {
|
|
||||||
if (pos > fbr.buf.len) return error.EndOfBuffer;
|
|
||||||
fbr.pos = @intCast(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn seekForward(fbr: *DeprecatedFixedBufferReader, amount: u64) Error!void {
|
|
||||||
if (fbr.buf.len - fbr.pos < amount) return error.EndOfBuffer;
|
|
||||||
fbr.pos += @intCast(amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub inline fn readByte(fbr: *DeprecatedFixedBufferReader) Error!u8 {
|
|
||||||
if (fbr.pos >= fbr.buf.len) return error.EndOfBuffer;
|
|
||||||
defer fbr.pos += 1;
|
|
||||||
return fbr.buf[fbr.pos];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readByteSigned(fbr: *DeprecatedFixedBufferReader) Error!i8 {
|
|
||||||
return @bitCast(try fbr.readByte());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readInt(fbr: *DeprecatedFixedBufferReader, comptime T: type) Error!T {
|
|
||||||
const size = @divExact(@typeInfo(T).Int.bits, 8);
|
|
||||||
if (fbr.buf.len - fbr.pos < size) return error.EndOfBuffer;
|
|
||||||
defer fbr.pos += size;
|
|
||||||
return std.mem.readInt(T, fbr.buf[fbr.pos..][0..size], fbr.endian);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readIntChecked(
|
|
||||||
fbr: *DeprecatedFixedBufferReader,
|
|
||||||
comptime T: type,
|
|
||||||
ma: *MemoryAccessor,
|
|
||||||
) Error!T {
|
|
||||||
if (ma.load(T, @intFromPtr(fbr.buf[fbr.pos..].ptr)) == null)
|
|
||||||
return error.InvalidBuffer;
|
|
||||||
|
|
||||||
return fbr.readInt(T);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readUleb128(fbr: *DeprecatedFixedBufferReader, comptime T: type) Error!T {
|
|
||||||
return std.leb.readUleb128(T, fbr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readIleb128(fbr: *DeprecatedFixedBufferReader, comptime T: type) Error!T {
|
|
||||||
return std.leb.readIleb128(T, fbr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readAddress(fbr: *DeprecatedFixedBufferReader, format: std.dwarf.Format) Error!u64 {
|
|
||||||
return switch (format) {
|
|
||||||
.@"32" => try fbr.readInt(u32),
|
|
||||||
.@"64" => try fbr.readInt(u64),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readAddressChecked(
|
|
||||||
fbr: *DeprecatedFixedBufferReader,
|
|
||||||
format: std.dwarf.Format,
|
|
||||||
ma: *MemoryAccessor,
|
|
||||||
) Error!u64 {
|
|
||||||
return switch (format) {
|
|
||||||
.@"32" => try fbr.readIntChecked(u32, ma),
|
|
||||||
.@"64" => try fbr.readIntChecked(u64, ma),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readBytes(fbr: *DeprecatedFixedBufferReader, len: usize) Error![]const u8 {
|
|
||||||
if (fbr.buf.len - fbr.pos < len) return error.EndOfBuffer;
|
|
||||||
defer fbr.pos += len;
|
|
||||||
return fbr.buf[fbr.pos..][0..len];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn readBytesTo(fbr: *DeprecatedFixedBufferReader, comptime sentinel: u8) Error![:sentinel]const u8 {
|
|
||||||
const end = @call(.always_inline, std.mem.indexOfScalarPos, .{
|
|
||||||
u8,
|
|
||||||
fbr.buf,
|
|
||||||
fbr.pos,
|
|
||||||
sentinel,
|
|
||||||
}) orelse return error.EndOfBuffer;
|
|
||||||
defer fbr.pos = end + 1;
|
|
||||||
return fbr.buf[fbr.pos..end :sentinel];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Detect whether the program is being executed in the Valgrind virtual machine.
|
/// Detect whether the program is being executed in the Valgrind virtual machine.
|
||||||
///
|
///
|
||||||
/// When Valgrind integrations are disabled, this returns comptime-known false.
|
/// When Valgrind integrations are disabled, this returns comptime-known false.
|
||||||
@ -1587,6 +1509,7 @@ pub inline fn inValgrind() bool {
|
|||||||
test {
|
test {
|
||||||
_ = &Dwarf;
|
_ = &Dwarf;
|
||||||
_ = &MemoryAccessor;
|
_ = &MemoryAccessor;
|
||||||
|
_ = &FixedBufferReader;
|
||||||
_ = &Pdb;
|
_ = &Pdb;
|
||||||
_ = &SelfInfo;
|
_ = &SelfInfo;
|
||||||
_ = &dumpHex;
|
_ = &dumpHex;
|
||||||
|
|||||||
244
lib/std/debug/Coverage.zig
Normal file
244
lib/std/debug/Coverage.zig
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
const std = @import("../std.zig");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const Hash = std.hash.Wyhash;
|
||||||
|
const Dwarf = std.debug.Dwarf;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const Coverage = @This();
|
||||||
|
|
||||||
|
/// Provides a globally-scoped integer index for directories.
|
||||||
|
///
|
||||||
|
/// As opposed to, for example, a directory index that is compilation-unit
|
||||||
|
/// scoped inside a single ELF module.
|
||||||
|
///
|
||||||
|
/// String memory references the memory-mapped debug information.
|
||||||
|
///
|
||||||
|
/// Protected by `mutex`.
|
||||||
|
directories: std.ArrayHashMapUnmanaged(String, void, String.MapContext, false),
|
||||||
|
/// Provides a globally-scoped integer index for files.
|
||||||
|
///
|
||||||
|
/// String memory references the memory-mapped debug information.
|
||||||
|
///
|
||||||
|
/// Protected by `mutex`.
|
||||||
|
files: std.ArrayHashMapUnmanaged(File, void, File.MapContext, false),
|
||||||
|
string_bytes: std.ArrayListUnmanaged(u8),
|
||||||
|
/// Protects the other fields.
|
||||||
|
mutex: std.Thread.Mutex,
|
||||||
|
|
||||||
|
pub const init: Coverage = .{
|
||||||
|
.directories = .{},
|
||||||
|
.files = .{},
|
||||||
|
.mutex = .{},
|
||||||
|
.string_bytes = .{},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const String = enum(u32) {
|
||||||
|
_,
|
||||||
|
|
||||||
|
pub const MapContext = struct {
|
||||||
|
string_bytes: []const u8,
|
||||||
|
|
||||||
|
pub fn eql(self: @This(), a: String, b: String, b_index: usize) bool {
|
||||||
|
_ = b_index;
|
||||||
|
const a_slice = span(self.string_bytes[@intFromEnum(a)..]);
|
||||||
|
const b_slice = span(self.string_bytes[@intFromEnum(b)..]);
|
||||||
|
return std.mem.eql(u8, a_slice, b_slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash(self: @This(), a: String) u32 {
|
||||||
|
return @truncate(Hash.hash(0, span(self.string_bytes[@intFromEnum(a)..])));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const SliceAdapter = struct {
|
||||||
|
string_bytes: []const u8,
|
||||||
|
|
||||||
|
pub fn eql(self: @This(), a_slice: []const u8, b: String, b_index: usize) bool {
|
||||||
|
_ = b_index;
|
||||||
|
const b_slice = span(self.string_bytes[@intFromEnum(b)..]);
|
||||||
|
return std.mem.eql(u8, a_slice, b_slice);
|
||||||
|
}
|
||||||
|
pub fn hash(self: @This(), a: []const u8) u32 {
|
||||||
|
_ = self;
|
||||||
|
return @truncate(Hash.hash(0, a));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const SourceLocation = extern struct {
|
||||||
|
file: File.Index,
|
||||||
|
line: u32,
|
||||||
|
column: u32,
|
||||||
|
|
||||||
|
pub const invalid: SourceLocation = .{
|
||||||
|
.file = .invalid,
|
||||||
|
.line = 0,
|
||||||
|
.column = 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const File = extern struct {
|
||||||
|
directory_index: u32,
|
||||||
|
basename: String,
|
||||||
|
|
||||||
|
pub const Index = enum(u32) {
|
||||||
|
invalid = std.math.maxInt(u32),
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const MapContext = struct {
|
||||||
|
string_bytes: []const u8,
|
||||||
|
|
||||||
|
pub fn hash(self: MapContext, a: File) u32 {
|
||||||
|
const a_basename = span(self.string_bytes[@intFromEnum(a.basename)..]);
|
||||||
|
return @truncate(Hash.hash(a.directory_index, a_basename));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(self: MapContext, a: File, b: File, b_index: usize) bool {
|
||||||
|
_ = b_index;
|
||||||
|
if (a.directory_index != b.directory_index) return false;
|
||||||
|
const a_basename = span(self.string_bytes[@intFromEnum(a.basename)..]);
|
||||||
|
const b_basename = span(self.string_bytes[@intFromEnum(b.basename)..]);
|
||||||
|
return std.mem.eql(u8, a_basename, b_basename);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const SliceAdapter = struct {
|
||||||
|
string_bytes: []const u8,
|
||||||
|
|
||||||
|
pub const Entry = struct {
|
||||||
|
directory_index: u32,
|
||||||
|
basename: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn hash(self: @This(), a: Entry) u32 {
|
||||||
|
_ = self;
|
||||||
|
return @truncate(Hash.hash(a.directory_index, a.basename));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(self: @This(), a: Entry, b: File, b_index: usize) bool {
|
||||||
|
_ = b_index;
|
||||||
|
if (a.directory_index != b.directory_index) return false;
|
||||||
|
const b_basename = span(self.string_bytes[@intFromEnum(b.basename)..]);
|
||||||
|
return std.mem.eql(u8, a.basename, b_basename);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn deinit(cov: *Coverage, gpa: Allocator) void {
|
||||||
|
cov.directories.deinit(gpa);
|
||||||
|
cov.files.deinit(gpa);
|
||||||
|
cov.string_bytes.deinit(gpa);
|
||||||
|
cov.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fileAt(cov: *Coverage, index: File.Index) *File {
|
||||||
|
return &cov.files.keys()[@intFromEnum(index)];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stringAt(cov: *Coverage, index: String) [:0]const u8 {
|
||||||
|
return span(cov.string_bytes.items[@intFromEnum(index)..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ResolveAddressesDwarfError = Dwarf.ScanError;
|
||||||
|
|
||||||
|
pub fn resolveAddressesDwarf(
|
||||||
|
cov: *Coverage,
|
||||||
|
gpa: Allocator,
|
||||||
|
sorted_pc_addrs: []const u64,
|
||||||
|
/// Asserts its length equals length of `sorted_pc_addrs`.
|
||||||
|
output: []SourceLocation,
|
||||||
|
d: *Dwarf,
|
||||||
|
) ResolveAddressesDwarfError!void {
|
||||||
|
assert(sorted_pc_addrs.len == output.len);
|
||||||
|
assert(d.compile_units_sorted);
|
||||||
|
|
||||||
|
var cu_i: usize = 0;
|
||||||
|
var line_table_i: usize = 0;
|
||||||
|
var cu: *Dwarf.CompileUnit = &d.compile_unit_list.items[0];
|
||||||
|
var range = cu.pc_range.?;
|
||||||
|
// Protects directories and files tables from other threads.
|
||||||
|
cov.mutex.lock();
|
||||||
|
defer cov.mutex.unlock();
|
||||||
|
next_pc: for (sorted_pc_addrs, output) |pc, *out| {
|
||||||
|
while (pc >= range.end) {
|
||||||
|
cu_i += 1;
|
||||||
|
if (cu_i >= d.compile_unit_list.items.len) {
|
||||||
|
out.* = SourceLocation.invalid;
|
||||||
|
continue :next_pc;
|
||||||
|
}
|
||||||
|
cu = &d.compile_unit_list.items[cu_i];
|
||||||
|
line_table_i = 0;
|
||||||
|
range = cu.pc_range orelse {
|
||||||
|
out.* = SourceLocation.invalid;
|
||||||
|
continue :next_pc;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (pc < range.start) {
|
||||||
|
out.* = SourceLocation.invalid;
|
||||||
|
continue :next_pc;
|
||||||
|
}
|
||||||
|
if (line_table_i == 0) {
|
||||||
|
line_table_i = 1;
|
||||||
|
cov.mutex.unlock();
|
||||||
|
defer cov.mutex.lock();
|
||||||
|
d.populateSrcLocCache(gpa, cu) catch |err| switch (err) {
|
||||||
|
error.MissingDebugInfo, error.InvalidDebugInfo => {
|
||||||
|
out.* = SourceLocation.invalid;
|
||||||
|
cu_i += 1;
|
||||||
|
if (cu_i < d.compile_unit_list.items.len) {
|
||||||
|
cu = &d.compile_unit_list.items[cu_i];
|
||||||
|
line_table_i = 0;
|
||||||
|
if (cu.pc_range) |r| range = r;
|
||||||
|
}
|
||||||
|
continue :next_pc;
|
||||||
|
},
|
||||||
|
else => |e| return e,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const slc = &cu.src_loc_cache.?;
|
||||||
|
const table_addrs = slc.line_table.keys();
|
||||||
|
while (line_table_i < table_addrs.len and table_addrs[line_table_i] < pc) line_table_i += 1;
|
||||||
|
|
||||||
|
const entry = slc.line_table.values()[line_table_i - 1];
|
||||||
|
const corrected_file_index = entry.file - @intFromBool(slc.version < 5);
|
||||||
|
const file_entry = slc.files[corrected_file_index];
|
||||||
|
const dir_path = slc.directories[file_entry.dir_index].path;
|
||||||
|
try cov.string_bytes.ensureUnusedCapacity(gpa, dir_path.len + file_entry.path.len + 2);
|
||||||
|
const dir_gop = try cov.directories.getOrPutContextAdapted(gpa, dir_path, String.SliceAdapter{
|
||||||
|
.string_bytes = cov.string_bytes.items,
|
||||||
|
}, String.MapContext{
|
||||||
|
.string_bytes = cov.string_bytes.items,
|
||||||
|
});
|
||||||
|
if (!dir_gop.found_existing)
|
||||||
|
dir_gop.key_ptr.* = addStringAssumeCapacity(cov, dir_path);
|
||||||
|
const file_gop = try cov.files.getOrPutContextAdapted(gpa, File.SliceAdapter.Entry{
|
||||||
|
.directory_index = @intCast(dir_gop.index),
|
||||||
|
.basename = file_entry.path,
|
||||||
|
}, File.SliceAdapter{
|
||||||
|
.string_bytes = cov.string_bytes.items,
|
||||||
|
}, File.MapContext{
|
||||||
|
.string_bytes = cov.string_bytes.items,
|
||||||
|
});
|
||||||
|
if (!file_gop.found_existing) file_gop.key_ptr.* = .{
|
||||||
|
.directory_index = @intCast(dir_gop.index),
|
||||||
|
.basename = addStringAssumeCapacity(cov, file_entry.path),
|
||||||
|
};
|
||||||
|
out.* = .{
|
||||||
|
.file = @enumFromInt(file_gop.index),
|
||||||
|
.line = entry.line,
|
||||||
|
.column = entry.column,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addStringAssumeCapacity(cov: *Coverage, s: []const u8) String {
|
||||||
|
const result: String = @enumFromInt(cov.string_bytes.items.len);
|
||||||
|
cov.string_bytes.appendSliceAssumeCapacity(s);
|
||||||
|
cov.string_bytes.appendAssumeCapacity(0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span(s: []const u8) [:0]const u8 {
|
||||||
|
return std.mem.sliceTo(@as([:0]const u8, @ptrCast(s)), 0);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
93
lib/std/debug/FixedBufferReader.zig
Normal file
93
lib/std/debug/FixedBufferReader.zig
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
//! Optimized for performance in debug builds.
|
||||||
|
|
||||||
|
const std = @import("../std.zig");
|
||||||
|
const MemoryAccessor = std.debug.MemoryAccessor;
|
||||||
|
|
||||||
|
const FixedBufferReader = @This();
|
||||||
|
|
||||||
|
buf: []const u8,
|
||||||
|
pos: usize = 0,
|
||||||
|
endian: std.builtin.Endian,
|
||||||
|
|
||||||
|
pub const Error = error{ EndOfBuffer, Overflow, InvalidBuffer };
|
||||||
|
|
||||||
|
pub fn seekTo(fbr: *FixedBufferReader, pos: u64) Error!void {
|
||||||
|
if (pos > fbr.buf.len) return error.EndOfBuffer;
|
||||||
|
fbr.pos = @intCast(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seekForward(fbr: *FixedBufferReader, amount: u64) Error!void {
|
||||||
|
if (fbr.buf.len - fbr.pos < amount) return error.EndOfBuffer;
|
||||||
|
fbr.pos += @intCast(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn readByte(fbr: *FixedBufferReader) Error!u8 {
|
||||||
|
if (fbr.pos >= fbr.buf.len) return error.EndOfBuffer;
|
||||||
|
defer fbr.pos += 1;
|
||||||
|
return fbr.buf[fbr.pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readByteSigned(fbr: *FixedBufferReader) Error!i8 {
|
||||||
|
return @bitCast(try fbr.readByte());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readInt(fbr: *FixedBufferReader, comptime T: type) Error!T {
|
||||||
|
const size = @divExact(@typeInfo(T).Int.bits, 8);
|
||||||
|
if (fbr.buf.len - fbr.pos < size) return error.EndOfBuffer;
|
||||||
|
defer fbr.pos += size;
|
||||||
|
return std.mem.readInt(T, fbr.buf[fbr.pos..][0..size], fbr.endian);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readIntChecked(
|
||||||
|
fbr: *FixedBufferReader,
|
||||||
|
comptime T: type,
|
||||||
|
ma: *MemoryAccessor,
|
||||||
|
) Error!T {
|
||||||
|
if (ma.load(T, @intFromPtr(fbr.buf[fbr.pos..].ptr)) == null)
|
||||||
|
return error.InvalidBuffer;
|
||||||
|
|
||||||
|
return fbr.readInt(T);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readUleb128(fbr: *FixedBufferReader, comptime T: type) Error!T {
|
||||||
|
return std.leb.readUleb128(T, fbr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readIleb128(fbr: *FixedBufferReader, comptime T: type) Error!T {
|
||||||
|
return std.leb.readIleb128(T, fbr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readAddress(fbr: *FixedBufferReader, format: std.dwarf.Format) Error!u64 {
|
||||||
|
return switch (format) {
|
||||||
|
.@"32" => try fbr.readInt(u32),
|
||||||
|
.@"64" => try fbr.readInt(u64),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readAddressChecked(
|
||||||
|
fbr: *FixedBufferReader,
|
||||||
|
format: std.dwarf.Format,
|
||||||
|
ma: *MemoryAccessor,
|
||||||
|
) Error!u64 {
|
||||||
|
return switch (format) {
|
||||||
|
.@"32" => try fbr.readIntChecked(u32, ma),
|
||||||
|
.@"64" => try fbr.readIntChecked(u64, ma),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readBytes(fbr: *FixedBufferReader, len: usize) Error![]const u8 {
|
||||||
|
if (fbr.buf.len - fbr.pos < len) return error.EndOfBuffer;
|
||||||
|
defer fbr.pos += len;
|
||||||
|
return fbr.buf[fbr.pos..][0..len];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readBytesTo(fbr: *FixedBufferReader, comptime sentinel: u8) Error![:sentinel]const u8 {
|
||||||
|
const end = @call(.always_inline, std.mem.indexOfScalarPos, .{
|
||||||
|
u8,
|
||||||
|
fbr.buf,
|
||||||
|
fbr.pos,
|
||||||
|
sentinel,
|
||||||
|
}) orelse return error.EndOfBuffer;
|
||||||
|
defer fbr.pos = end + 1;
|
||||||
|
return fbr.buf[fbr.pos..end :sentinel];
|
||||||
|
}
|
||||||
62
lib/std/debug/Info.zig
Normal file
62
lib/std/debug/Info.zig
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
//! Cross-platform abstraction for loading debug information into an in-memory
|
||||||
|
//! format that supports queries such as "what is the source location of this
|
||||||
|
//! virtual memory address?"
|
||||||
|
//!
|
||||||
|
//! Unlike `std.debug.SelfInfo`, this API does not assume the debug information
|
||||||
|
//! in question happens to match the host CPU architecture, OS, or other target
|
||||||
|
//! properties.
|
||||||
|
|
||||||
|
const std = @import("../std.zig");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const Path = std.Build.Cache.Path;
|
||||||
|
const Dwarf = std.debug.Dwarf;
|
||||||
|
const page_size = std.mem.page_size;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Coverage = std.debug.Coverage;
|
||||||
|
const SourceLocation = std.debug.Coverage.SourceLocation;
|
||||||
|
|
||||||
|
const Info = @This();
|
||||||
|
|
||||||
|
/// Sorted by key, ascending.
|
||||||
|
address_map: std.AutoArrayHashMapUnmanaged(u64, Dwarf.ElfModule),
|
||||||
|
/// Externally managed, outlives this `Info` instance.
|
||||||
|
coverage: *Coverage,
|
||||||
|
|
||||||
|
pub const LoadError = Dwarf.ElfModule.LoadError;
|
||||||
|
|
||||||
|
pub fn load(gpa: Allocator, path: Path, coverage: *Coverage) LoadError!Info {
|
||||||
|
var sections: Dwarf.SectionArray = Dwarf.null_section_array;
|
||||||
|
var elf_module = try Dwarf.ElfModule.loadPath(gpa, path, null, null, §ions, null);
|
||||||
|
try elf_module.dwarf.sortCompileUnits();
|
||||||
|
var info: Info = .{
|
||||||
|
.address_map = .{},
|
||||||
|
.coverage = coverage,
|
||||||
|
};
|
||||||
|
try info.address_map.put(gpa, elf_module.base_address, elf_module);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(info: *Info, gpa: Allocator) void {
|
||||||
|
for (info.address_map.values()) |*elf_module| {
|
||||||
|
elf_module.dwarf.deinit(gpa);
|
||||||
|
}
|
||||||
|
info.address_map.deinit(gpa);
|
||||||
|
info.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ResolveAddressesError = Coverage.ResolveAddressesDwarfError;
|
||||||
|
|
||||||
|
/// Given an array of virtual memory addresses, sorted ascending, outputs a
|
||||||
|
/// corresponding array of source locations.
|
||||||
|
pub fn resolveAddresses(
|
||||||
|
info: *Info,
|
||||||
|
gpa: Allocator,
|
||||||
|
sorted_pc_addrs: []const u64,
|
||||||
|
/// Asserts its length equals length of `sorted_pc_addrs`.
|
||||||
|
output: []SourceLocation,
|
||||||
|
) ResolveAddressesError!void {
|
||||||
|
assert(sorted_pc_addrs.len == output.len);
|
||||||
|
if (info.address_map.entries.len != 1) @panic("TODO");
|
||||||
|
const elf_module = &info.address_map.values()[0];
|
||||||
|
return info.coverage.resolveAddressesDwarf(gpa, sorted_pc_addrs, output, &elf_module.dwarf);
|
||||||
|
}
|
||||||
@ -587,7 +587,7 @@ pub const Module = switch (native_os) {
|
|||||||
}
|
}
|
||||||
if (section_index == null) continue;
|
if (section_index == null) continue;
|
||||||
|
|
||||||
const section_bytes = try chopSlice(mapped_mem, sect.offset, sect.size);
|
const section_bytes = try Dwarf.chopSlice(mapped_mem, sect.offset, sect.size);
|
||||||
sections[section_index.?] = .{
|
sections[section_index.?] = .{
|
||||||
.data = section_bytes,
|
.data = section_bytes,
|
||||||
.virtual_address = sect.addr,
|
.virtual_address = sect.addr,
|
||||||
@ -602,10 +602,11 @@ pub const Module = switch (native_os) {
|
|||||||
sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
|
sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
|
||||||
if (missing_debug_info) return error.MissingDebugInfo;
|
if (missing_debug_info) return error.MissingDebugInfo;
|
||||||
|
|
||||||
var di = Dwarf{
|
var di: Dwarf = .{
|
||||||
.endian = .little,
|
.endian = .little,
|
||||||
.sections = sections,
|
.sections = sections,
|
||||||
.is_macho = true,
|
.is_macho = true,
|
||||||
|
.compile_units_sorted = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
try Dwarf.open(&di, allocator);
|
try Dwarf.open(&di, allocator);
|
||||||
@ -622,7 +623,7 @@ pub const Module = switch (native_os) {
|
|||||||
return result.value_ptr;
|
return result.value_ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !SymbolInfo {
|
pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !std.debug.Symbol {
|
||||||
nosuspend {
|
nosuspend {
|
||||||
const result = try self.getOFileInfoForAddress(allocator, address);
|
const result = try self.getOFileInfoForAddress(allocator, address);
|
||||||
if (result.symbol == null) return .{};
|
if (result.symbol == null) return .{};
|
||||||
@ -630,19 +631,19 @@ pub const Module = switch (native_os) {
|
|||||||
// Take the symbol name from the N_FUN STAB entry, we're going to
|
// Take the symbol name from the N_FUN STAB entry, we're going to
|
||||||
// use it if we fail to find the DWARF infos
|
// use it if we fail to find the DWARF infos
|
||||||
const stab_symbol = mem.sliceTo(self.strings[result.symbol.?.strx..], 0);
|
const stab_symbol = mem.sliceTo(self.strings[result.symbol.?.strx..], 0);
|
||||||
if (result.o_file_info == null) return .{ .symbol_name = stab_symbol };
|
if (result.o_file_info == null) return .{ .name = stab_symbol };
|
||||||
|
|
||||||
// Translate again the address, this time into an address inside the
|
// Translate again the address, this time into an address inside the
|
||||||
// .o file
|
// .o file
|
||||||
const relocated_address_o = result.o_file_info.?.addr_table.get(stab_symbol) orelse return .{
|
const relocated_address_o = result.o_file_info.?.addr_table.get(stab_symbol) orelse return .{
|
||||||
.symbol_name = "???",
|
.name = "???",
|
||||||
};
|
};
|
||||||
|
|
||||||
const addr_off = result.relocated_address - result.symbol.?.addr;
|
const addr_off = result.relocated_address - result.symbol.?.addr;
|
||||||
const o_file_di = &result.o_file_info.?.di;
|
const o_file_di = &result.o_file_info.?.di;
|
||||||
if (o_file_di.findCompileUnit(relocated_address_o)) |compile_unit| {
|
if (o_file_di.findCompileUnit(relocated_address_o)) |compile_unit| {
|
||||||
return SymbolInfo{
|
return .{
|
||||||
.symbol_name = o_file_di.getSymbolName(relocated_address_o) orelse "???",
|
.name = o_file_di.getSymbolName(relocated_address_o) orelse "???",
|
||||||
.compile_unit_name = compile_unit.die.getAttrString(
|
.compile_unit_name = compile_unit.die.getAttrString(
|
||||||
o_file_di,
|
o_file_di,
|
||||||
std.dwarf.AT.name,
|
std.dwarf.AT.name,
|
||||||
@ -651,9 +652,9 @@ pub const Module = switch (native_os) {
|
|||||||
) catch |err| switch (err) {
|
) catch |err| switch (err) {
|
||||||
error.MissingDebugInfo, error.InvalidDebugInfo => "???",
|
error.MissingDebugInfo, error.InvalidDebugInfo => "???",
|
||||||
},
|
},
|
||||||
.line_info = o_file_di.getLineNumberInfo(
|
.source_location = o_file_di.getLineNumberInfo(
|
||||||
allocator,
|
allocator,
|
||||||
compile_unit.*,
|
compile_unit,
|
||||||
relocated_address_o + addr_off,
|
relocated_address_o + addr_off,
|
||||||
) catch |err| switch (err) {
|
) catch |err| switch (err) {
|
||||||
error.MissingDebugInfo, error.InvalidDebugInfo => null,
|
error.MissingDebugInfo, error.InvalidDebugInfo => null,
|
||||||
@ -662,7 +663,7 @@ pub const Module = switch (native_os) {
|
|||||||
};
|
};
|
||||||
} else |err| switch (err) {
|
} else |err| switch (err) {
|
||||||
error.MissingDebugInfo, error.InvalidDebugInfo => {
|
error.MissingDebugInfo, error.InvalidDebugInfo => {
|
||||||
return SymbolInfo{ .symbol_name = stab_symbol };
|
return .{ .name = stab_symbol };
|
||||||
},
|
},
|
||||||
else => return err,
|
else => return err,
|
||||||
}
|
}
|
||||||
@ -729,7 +730,7 @@ pub const Module = switch (native_os) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getSymbolFromPdb(self: *@This(), relocated_address: usize) !?SymbolInfo {
|
fn getSymbolFromPdb(self: *@This(), relocated_address: usize) !?std.debug.Symbol {
|
||||||
var coff_section: *align(1) const coff.SectionHeader = undefined;
|
var coff_section: *align(1) const coff.SectionHeader = undefined;
|
||||||
const mod_index = for (self.pdb.?.sect_contribs) |sect_contrib| {
|
const mod_index = for (self.pdb.?.sect_contribs) |sect_contrib| {
|
||||||
if (sect_contrib.Section > self.coff_section_headers.len) continue;
|
if (sect_contrib.Section > self.coff_section_headers.len) continue;
|
||||||
@ -759,14 +760,14 @@ pub const Module = switch (native_os) {
|
|||||||
relocated_address - coff_section.virtual_address,
|
relocated_address - coff_section.virtual_address,
|
||||||
);
|
);
|
||||||
|
|
||||||
return SymbolInfo{
|
return .{
|
||||||
.symbol_name = symbol_name,
|
.name = symbol_name,
|
||||||
.compile_unit_name = obj_basename,
|
.compile_unit_name = obj_basename,
|
||||||
.line_info = opt_line_info,
|
.source_location = opt_line_info,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !SymbolInfo {
|
pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !std.debug.Symbol {
|
||||||
// Translate the VA into an address into this object
|
// Translate the VA into an address into this object
|
||||||
const relocated_address = address - self.base_address;
|
const relocated_address = address - self.base_address;
|
||||||
|
|
||||||
@ -776,10 +777,10 @@ pub const Module = switch (native_os) {
|
|||||||
|
|
||||||
if (self.dwarf) |*dwarf| {
|
if (self.dwarf) |*dwarf| {
|
||||||
const dwarf_address = relocated_address + self.coff_image_base;
|
const dwarf_address = relocated_address + self.coff_image_base;
|
||||||
return getSymbolFromDwarf(allocator, dwarf_address, dwarf);
|
return dwarf.getSymbol(allocator, dwarf_address);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SymbolInfo{};
|
return .{};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {
|
pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {
|
||||||
@ -792,41 +793,18 @@ pub const Module = switch (native_os) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.linux, .netbsd, .freebsd, .dragonfly, .openbsd, .haiku, .solaris, .illumos => struct {
|
.linux, .netbsd, .freebsd, .dragonfly, .openbsd, .haiku, .solaris, .illumos => Dwarf.ElfModule,
|
||||||
base_address: usize,
|
|
||||||
dwarf: Dwarf,
|
|
||||||
mapped_memory: []align(mem.page_size) const u8,
|
|
||||||
external_mapped_memory: ?[]align(mem.page_size) const u8,
|
|
||||||
|
|
||||||
pub fn deinit(self: *@This(), allocator: Allocator) void {
|
|
||||||
self.dwarf.deinit(allocator);
|
|
||||||
posix.munmap(self.mapped_memory);
|
|
||||||
if (self.external_mapped_memory) |m| posix.munmap(m);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !SymbolInfo {
|
|
||||||
// Translate the VA into an address into this object
|
|
||||||
const relocated_address = address - self.base_address;
|
|
||||||
return getSymbolFromDwarf(allocator, relocated_address, &self.dwarf);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {
|
|
||||||
_ = allocator;
|
|
||||||
_ = address;
|
|
||||||
return &self.dwarf;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.wasi, .emscripten => struct {
|
.wasi, .emscripten => struct {
|
||||||
pub fn deinit(self: *@This(), allocator: Allocator) void {
|
pub fn deinit(self: *@This(), allocator: Allocator) void {
|
||||||
_ = self;
|
_ = self;
|
||||||
_ = allocator;
|
_ = allocator;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !SymbolInfo {
|
pub fn getSymbolAtAddress(self: *@This(), allocator: Allocator, address: usize) !std.debug.Symbol {
|
||||||
_ = self;
|
_ = self;
|
||||||
_ = allocator;
|
_ = allocator;
|
||||||
_ = address;
|
_ = address;
|
||||||
return SymbolInfo{};
|
return .{};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {
|
pub fn getDwarfInfoForAddress(self: *@This(), allocator: Allocator, address: usize) !?*const Dwarf {
|
||||||
@ -1014,10 +992,11 @@ fn readCoffDebugInfo(allocator: Allocator, coff_obj: *coff.Coff) !Module {
|
|||||||
} else null;
|
} else null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var dwarf = Dwarf{
|
var dwarf: Dwarf = .{
|
||||||
.endian = native_endian,
|
.endian = native_endian,
|
||||||
.sections = sections,
|
.sections = sections,
|
||||||
.is_macho = false,
|
.is_macho = false,
|
||||||
|
.compile_units_sorted = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
try Dwarf.open(&dwarf, allocator);
|
try Dwarf.open(&dwarf, allocator);
|
||||||
@ -1068,7 +1047,7 @@ pub fn readElfDebugInfo(
|
|||||||
expected_crc: ?u32,
|
expected_crc: ?u32,
|
||||||
parent_sections: *Dwarf.SectionArray,
|
parent_sections: *Dwarf.SectionArray,
|
||||||
parent_mapped_mem: ?[]align(mem.page_size) const u8,
|
parent_mapped_mem: ?[]align(mem.page_size) const u8,
|
||||||
) !Module {
|
) !Dwarf.ElfModule {
|
||||||
nosuspend {
|
nosuspend {
|
||||||
const elf_file = (if (elf_filename) |filename| blk: {
|
const elf_file = (if (elf_filename) |filename| blk: {
|
||||||
break :blk fs.cwd().openFile(filename, .{});
|
break :blk fs.cwd().openFile(filename, .{});
|
||||||
@ -1078,176 +1057,15 @@ pub fn readElfDebugInfo(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mapped_mem = try mapWholeFile(elf_file);
|
const mapped_mem = try mapWholeFile(elf_file);
|
||||||
if (expected_crc) |crc| if (crc != std.hash.crc.Crc32.hash(mapped_mem)) return error.InvalidDebugInfo;
|
return Dwarf.ElfModule.load(
|
||||||
|
allocator,
|
||||||
const hdr: *const elf.Ehdr = @ptrCast(&mapped_mem[0]);
|
mapped_mem,
|
||||||
if (!mem.eql(u8, hdr.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic;
|
build_id,
|
||||||
if (hdr.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion;
|
expected_crc,
|
||||||
|
parent_sections,
|
||||||
const endian: std.builtin.Endian = switch (hdr.e_ident[elf.EI_DATA]) {
|
parent_mapped_mem,
|
||||||
elf.ELFDATA2LSB => .little,
|
elf_filename,
|
||||||
elf.ELFDATA2MSB => .big,
|
);
|
||||||
else => return error.InvalidElfEndian,
|
|
||||||
};
|
|
||||||
assert(endian == native_endian); // this is our own debug info
|
|
||||||
|
|
||||||
const shoff = hdr.e_shoff;
|
|
||||||
const str_section_off = shoff + @as(u64, hdr.e_shentsize) * @as(u64, hdr.e_shstrndx);
|
|
||||||
const str_shdr: *const elf.Shdr = @ptrCast(@alignCast(&mapped_mem[math.cast(usize, str_section_off) orelse return error.Overflow]));
|
|
||||||
const header_strings = mapped_mem[str_shdr.sh_offset..][0..str_shdr.sh_size];
|
|
||||||
const shdrs = @as(
|
|
||||||
[*]const elf.Shdr,
|
|
||||||
@ptrCast(@alignCast(&mapped_mem[shoff])),
|
|
||||||
)[0..hdr.e_shnum];
|
|
||||||
|
|
||||||
var sections: Dwarf.SectionArray = Dwarf.null_section_array;
|
|
||||||
|
|
||||||
// Combine section list. This takes ownership over any owned sections from the parent scope.
|
|
||||||
for (parent_sections, §ions) |*parent, *section| {
|
|
||||||
if (parent.*) |*p| {
|
|
||||||
section.* = p.*;
|
|
||||||
p.owned = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errdefer for (sections) |section| if (section) |s| if (s.owned) allocator.free(s.data);
|
|
||||||
|
|
||||||
var separate_debug_filename: ?[]const u8 = null;
|
|
||||||
var separate_debug_crc: ?u32 = null;
|
|
||||||
|
|
||||||
for (shdrs) |*shdr| {
|
|
||||||
if (shdr.sh_type == elf.SHT_NULL or shdr.sh_type == elf.SHT_NOBITS) continue;
|
|
||||||
const name = mem.sliceTo(header_strings[shdr.sh_name..], 0);
|
|
||||||
|
|
||||||
if (mem.eql(u8, name, ".gnu_debuglink")) {
|
|
||||||
const gnu_debuglink = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
|
|
||||||
const debug_filename = mem.sliceTo(@as([*:0]const u8, @ptrCast(gnu_debuglink.ptr)), 0);
|
|
||||||
const crc_offset = mem.alignForward(usize, @intFromPtr(&debug_filename[debug_filename.len]) + 1, 4) - @intFromPtr(gnu_debuglink.ptr);
|
|
||||||
const crc_bytes = gnu_debuglink[crc_offset..][0..4];
|
|
||||||
separate_debug_crc = mem.readInt(u32, crc_bytes, native_endian);
|
|
||||||
separate_debug_filename = debug_filename;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var section_index: ?usize = null;
|
|
||||||
inline for (@typeInfo(Dwarf.Section.Id).Enum.fields, 0..) |section, i| {
|
|
||||||
if (mem.eql(u8, "." ++ section.name, name)) section_index = i;
|
|
||||||
}
|
|
||||||
if (section_index == null) continue;
|
|
||||||
if (sections[section_index.?] != null) continue;
|
|
||||||
|
|
||||||
const section_bytes = try chopSlice(mapped_mem, shdr.sh_offset, shdr.sh_size);
|
|
||||||
sections[section_index.?] = if ((shdr.sh_flags & elf.SHF_COMPRESSED) > 0) blk: {
|
|
||||||
var section_stream = std.io.fixedBufferStream(section_bytes);
|
|
||||||
var section_reader = section_stream.reader();
|
|
||||||
const chdr = section_reader.readStruct(elf.Chdr) catch continue;
|
|
||||||
if (chdr.ch_type != .ZLIB) continue;
|
|
||||||
|
|
||||||
var zlib_stream = std.compress.zlib.decompressor(section_stream.reader());
|
|
||||||
|
|
||||||
const decompressed_section = try allocator.alloc(u8, chdr.ch_size);
|
|
||||||
errdefer allocator.free(decompressed_section);
|
|
||||||
|
|
||||||
const read = zlib_stream.reader().readAll(decompressed_section) catch continue;
|
|
||||||
assert(read == decompressed_section.len);
|
|
||||||
|
|
||||||
break :blk .{
|
|
||||||
.data = decompressed_section,
|
|
||||||
.virtual_address = shdr.sh_addr,
|
|
||||||
.owned = true,
|
|
||||||
};
|
|
||||||
} else .{
|
|
||||||
.data = section_bytes,
|
|
||||||
.virtual_address = shdr.sh_addr,
|
|
||||||
.owned = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const missing_debug_info =
|
|
||||||
sections[@intFromEnum(Dwarf.Section.Id.debug_info)] == null or
|
|
||||||
sections[@intFromEnum(Dwarf.Section.Id.debug_abbrev)] == null or
|
|
||||||
sections[@intFromEnum(Dwarf.Section.Id.debug_str)] == null or
|
|
||||||
sections[@intFromEnum(Dwarf.Section.Id.debug_line)] == null;
|
|
||||||
|
|
||||||
// Attempt to load debug info from an external file
|
|
||||||
// See: https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html
|
|
||||||
if (missing_debug_info) {
|
|
||||||
|
|
||||||
// Only allow one level of debug info nesting
|
|
||||||
if (parent_mapped_mem) |_| {
|
|
||||||
return error.MissingDebugInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const global_debug_directories = [_][]const u8{
|
|
||||||
"/usr/lib/debug",
|
|
||||||
};
|
|
||||||
|
|
||||||
// <global debug directory>/.build-id/<2-character id prefix>/<id remainder>.debug
|
|
||||||
if (build_id) |id| blk: {
|
|
||||||
if (id.len < 3) break :blk;
|
|
||||||
|
|
||||||
// Either md5 (16 bytes) or sha1 (20 bytes) are used here in practice
|
|
||||||
const extension = ".debug";
|
|
||||||
var id_prefix_buf: [2]u8 = undefined;
|
|
||||||
var filename_buf: [38 + extension.len]u8 = undefined;
|
|
||||||
|
|
||||||
_ = std.fmt.bufPrint(&id_prefix_buf, "{s}", .{std.fmt.fmtSliceHexLower(id[0..1])}) catch unreachable;
|
|
||||||
const filename = std.fmt.bufPrint(
|
|
||||||
&filename_buf,
|
|
||||||
"{s}" ++ extension,
|
|
||||||
.{std.fmt.fmtSliceHexLower(id[1..])},
|
|
||||||
) catch break :blk;
|
|
||||||
|
|
||||||
for (global_debug_directories) |global_directory| {
|
|
||||||
const path = try fs.path.join(allocator, &.{ global_directory, ".build-id", &id_prefix_buf, filename });
|
|
||||||
defer allocator.free(path);
|
|
||||||
|
|
||||||
return readElfDebugInfo(allocator, path, null, separate_debug_crc, §ions, mapped_mem) catch continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// use the path from .gnu_debuglink, in the same search order as gdb
|
|
||||||
if (separate_debug_filename) |separate_filename| blk: {
|
|
||||||
if (elf_filename != null and mem.eql(u8, elf_filename.?, separate_filename)) return error.MissingDebugInfo;
|
|
||||||
|
|
||||||
// <cwd>/<gnu_debuglink>
|
|
||||||
if (readElfDebugInfo(allocator, separate_filename, null, separate_debug_crc, §ions, mapped_mem)) |debug_info| return debug_info else |_| {}
|
|
||||||
|
|
||||||
// <cwd>/.debug/<gnu_debuglink>
|
|
||||||
{
|
|
||||||
const path = try fs.path.join(allocator, &.{ ".debug", separate_filename });
|
|
||||||
defer allocator.free(path);
|
|
||||||
|
|
||||||
if (readElfDebugInfo(allocator, path, null, separate_debug_crc, §ions, mapped_mem)) |debug_info| return debug_info else |_| {}
|
|
||||||
}
|
|
||||||
|
|
||||||
var cwd_buf: [fs.max_path_bytes]u8 = undefined;
|
|
||||||
const cwd_path = posix.realpath(".", &cwd_buf) catch break :blk;
|
|
||||||
|
|
||||||
// <global debug directory>/<absolute folder of current binary>/<gnu_debuglink>
|
|
||||||
for (global_debug_directories) |global_directory| {
|
|
||||||
const path = try fs.path.join(allocator, &.{ global_directory, cwd_path, separate_filename });
|
|
||||||
defer allocator.free(path);
|
|
||||||
if (readElfDebugInfo(allocator, path, null, separate_debug_crc, §ions, mapped_mem)) |debug_info| return debug_info else |_| {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.MissingDebugInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
var di = Dwarf{
|
|
||||||
.endian = endian,
|
|
||||||
.sections = sections,
|
|
||||||
.is_macho = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
try Dwarf.open(&di, allocator);
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.base_address = undefined,
|
|
||||||
.dwarf = di,
|
|
||||||
.mapped_memory = parent_mapped_mem orelse mapped_mem,
|
|
||||||
.external_mapped_memory = if (parent_mapped_mem != null) mapped_mem else null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1289,22 +1107,6 @@ fn mapWholeFile(file: File) ![]align(mem.page_size) const u8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn chopSlice(ptr: []const u8, offset: u64, size: u64) error{Overflow}![]const u8 {
|
|
||||||
const start = math.cast(usize, offset) orelse return error.Overflow;
|
|
||||||
const end = start + (math.cast(usize, size) orelse return error.Overflow);
|
|
||||||
return ptr[start..end];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const SymbolInfo = struct {
|
|
||||||
symbol_name: []const u8 = "???",
|
|
||||||
compile_unit_name: []const u8 = "???",
|
|
||||||
line_info: ?std.debug.SourceLocation = null,
|
|
||||||
|
|
||||||
pub fn deinit(self: SymbolInfo, allocator: Allocator) void {
|
|
||||||
if (self.line_info) |li| allocator.free(li.file_name);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn machoSearchSymbols(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol {
|
fn machoSearchSymbols(symbols: []const MachoSymbol, address: usize) ?*const MachoSymbol {
|
||||||
var min: usize = 0;
|
var min: usize = 0;
|
||||||
var max: usize = symbols.len - 1;
|
var max: usize = symbols.len - 1;
|
||||||
@ -1350,26 +1152,6 @@ test machoSearchSymbols {
|
|||||||
try testing.expectEqual(&symbols[2], machoSearchSymbols(&symbols, 5000).?);
|
try testing.expectEqual(&symbols[2], machoSearchSymbols(&symbols, 5000).?);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getSymbolFromDwarf(allocator: Allocator, address: u64, di: *Dwarf) !SymbolInfo {
|
|
||||||
if (nosuspend di.findCompileUnit(address)) |compile_unit| {
|
|
||||||
return SymbolInfo{
|
|
||||||
.symbol_name = nosuspend di.getSymbolName(address) orelse "???",
|
|
||||||
.compile_unit_name = compile_unit.die.getAttrString(di, std.dwarf.AT.name, di.section(.debug_str), compile_unit.*) catch |err| switch (err) {
|
|
||||||
error.MissingDebugInfo, error.InvalidDebugInfo => "???",
|
|
||||||
},
|
|
||||||
.line_info = nosuspend di.getLineNumberInfo(allocator, compile_unit.*, address) catch |err| switch (err) {
|
|
||||||
error.MissingDebugInfo, error.InvalidDebugInfo => null,
|
|
||||||
else => return err,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else |err| switch (err) {
|
|
||||||
error.MissingDebugInfo, error.InvalidDebugInfo => {
|
|
||||||
return SymbolInfo{};
|
|
||||||
},
|
|
||||||
else => return err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unwind a frame using MachO compact unwind info (from __unwind_info).
|
/// Unwind a frame using MachO compact unwind info (from __unwind_info).
|
||||||
/// If the compact encoding can't encode a way to unwind a frame, it will
|
/// If the compact encoding can't encode a way to unwind a frame, it will
|
||||||
/// defer unwinding to DWARF, in which case `.eh_frame` will be used if available.
|
/// defer unwinding to DWARF, in which case `.eh_frame` will be used if available.
|
||||||
@ -1796,7 +1578,7 @@ pub fn unwindFrameDwarf(
|
|||||||
const frame_section = di.section(dwarf_section) orelse return error.MissingFDE;
|
const frame_section = di.section(dwarf_section) orelse return error.MissingFDE;
|
||||||
if (fde_offset >= frame_section.len) return error.MissingFDE;
|
if (fde_offset >= frame_section.len) return error.MissingFDE;
|
||||||
|
|
||||||
var fbr: std.debug.DeprecatedFixedBufferReader = .{
|
var fbr: std.debug.FixedBufferReader = .{
|
||||||
.buf = frame_section,
|
.buf = frame_section,
|
||||||
.pos = fde_offset,
|
.pos = fde_offset,
|
||||||
.endian = di.endian,
|
.endian = di.endian,
|
||||||
@ -2028,6 +1810,7 @@ fn unwindFrameMachODwarf(
|
|||||||
var di: Dwarf = .{
|
var di: Dwarf = .{
|
||||||
.endian = native_endian,
|
.endian = native_endian,
|
||||||
.is_macho = true,
|
.is_macho = true,
|
||||||
|
.compile_units_sorted = false,
|
||||||
};
|
};
|
||||||
defer di.deinit(context.allocator);
|
defer di.deinit(context.allocator);
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ pub const protocol = @import("http/protocol.zig");
|
|||||||
pub const HeadParser = @import("http/HeadParser.zig");
|
pub const HeadParser = @import("http/HeadParser.zig");
|
||||||
pub const ChunkParser = @import("http/ChunkParser.zig");
|
pub const ChunkParser = @import("http/ChunkParser.zig");
|
||||||
pub const HeaderIterator = @import("http/HeaderIterator.zig");
|
pub const HeaderIterator = @import("http/HeaderIterator.zig");
|
||||||
|
pub const WebSocket = @import("http/WebSocket.zig");
|
||||||
|
|
||||||
pub const Version = enum {
|
pub const Version = enum {
|
||||||
@"HTTP/1.0",
|
@"HTTP/1.0",
|
||||||
@ -318,6 +319,7 @@ test {
|
|||||||
_ = Status;
|
_ = Status;
|
||||||
_ = HeadParser;
|
_ = HeadParser;
|
||||||
_ = ChunkParser;
|
_ = ChunkParser;
|
||||||
|
_ = WebSocket;
|
||||||
_ = @import("http/test.zig");
|
_ = @import("http/test.zig");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
243
lib/std/http/WebSocket.zig
Normal file
243
lib/std/http/WebSocket.zig
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
//! See https://tools.ietf.org/html/rfc6455
|
||||||
|
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const std = @import("std");
|
||||||
|
const WebSocket = @This();
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const native_endian = builtin.cpu.arch.endian();
|
||||||
|
|
||||||
|
key: []const u8,
|
||||||
|
request: *std.http.Server.Request,
|
||||||
|
recv_fifo: std.fifo.LinearFifo(u8, .Slice),
|
||||||
|
reader: std.io.AnyReader,
|
||||||
|
response: std.http.Server.Response,
|
||||||
|
/// Number of bytes that have been peeked but not discarded yet.
|
||||||
|
outstanding_len: usize,
|
||||||
|
|
||||||
|
pub const InitError = error{WebSocketUpgradeMissingKey} ||
|
||||||
|
std.http.Server.Request.ReaderError;
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
ws: *WebSocket,
|
||||||
|
request: *std.http.Server.Request,
|
||||||
|
send_buffer: []u8,
|
||||||
|
recv_buffer: []align(4) u8,
|
||||||
|
) InitError!bool {
|
||||||
|
var sec_websocket_key: ?[]const u8 = null;
|
||||||
|
var upgrade_websocket: bool = false;
|
||||||
|
var it = request.iterateHeaders();
|
||||||
|
while (it.next()) |header| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(header.name, "sec-websocket-key")) {
|
||||||
|
sec_websocket_key = header.value;
|
||||||
|
} else if (std.ascii.eqlIgnoreCase(header.name, "upgrade")) {
|
||||||
|
if (!std.mem.eql(u8, header.value, "websocket"))
|
||||||
|
return false;
|
||||||
|
upgrade_websocket = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!upgrade_websocket)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const key = sec_websocket_key orelse return error.WebSocketUpgradeMissingKey;
|
||||||
|
|
||||||
|
var sha1 = std.crypto.hash.Sha1.init(.{});
|
||||||
|
sha1.update(key);
|
||||||
|
sha1.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
||||||
|
var digest: [std.crypto.hash.Sha1.digest_length]u8 = undefined;
|
||||||
|
sha1.final(&digest);
|
||||||
|
var base64_digest: [28]u8 = undefined;
|
||||||
|
assert(std.base64.standard.Encoder.encode(&base64_digest, &digest).len == base64_digest.len);
|
||||||
|
|
||||||
|
request.head.content_length = std.math.maxInt(u64);
|
||||||
|
|
||||||
|
ws.* = .{
|
||||||
|
.key = key,
|
||||||
|
.recv_fifo = std.fifo.LinearFifo(u8, .Slice).init(recv_buffer),
|
||||||
|
.reader = try request.reader(),
|
||||||
|
.response = request.respondStreaming(.{
|
||||||
|
.send_buffer = send_buffer,
|
||||||
|
.respond_options = .{
|
||||||
|
.status = .switching_protocols,
|
||||||
|
.extra_headers = &.{
|
||||||
|
.{ .name = "upgrade", .value = "websocket" },
|
||||||
|
.{ .name = "connection", .value = "upgrade" },
|
||||||
|
.{ .name = "sec-websocket-accept", .value = &base64_digest },
|
||||||
|
},
|
||||||
|
.transfer_encoding = .none,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
.request = request,
|
||||||
|
.outstanding_len = 0,
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Header0 = packed struct(u8) {
|
||||||
|
opcode: Opcode,
|
||||||
|
rsv3: u1 = 0,
|
||||||
|
rsv2: u1 = 0,
|
||||||
|
rsv1: u1 = 0,
|
||||||
|
fin: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Header1 = packed struct(u8) {
|
||||||
|
payload_len: enum(u7) {
|
||||||
|
len16 = 126,
|
||||||
|
len64 = 127,
|
||||||
|
_,
|
||||||
|
},
|
||||||
|
mask: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Opcode = enum(u4) {
|
||||||
|
continuation = 0,
|
||||||
|
text = 1,
|
||||||
|
binary = 2,
|
||||||
|
connection_close = 8,
|
||||||
|
ping = 9,
|
||||||
|
/// "A Pong frame MAY be sent unsolicited. This serves as a unidirectional
|
||||||
|
/// heartbeat. A response to an unsolicited Pong frame is not expected."
|
||||||
|
pong = 10,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ReadSmallTextMessageError = error{
|
||||||
|
ConnectionClose,
|
||||||
|
UnexpectedOpCode,
|
||||||
|
MessageTooBig,
|
||||||
|
MissingMaskBit,
|
||||||
|
} || RecvError;
|
||||||
|
|
||||||
|
pub const SmallMessage = struct {
|
||||||
|
/// Can be text, binary, or ping.
|
||||||
|
opcode: Opcode,
|
||||||
|
data: []u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Reads the next message from the WebSocket stream, failing if the message does not fit
|
||||||
|
/// into `recv_buffer`.
|
||||||
|
pub fn readSmallMessage(ws: *WebSocket) ReadSmallTextMessageError!SmallMessage {
|
||||||
|
while (true) {
|
||||||
|
const header_bytes = (try recv(ws, 2))[0..2];
|
||||||
|
const h0: Header0 = @bitCast(header_bytes[0]);
|
||||||
|
const h1: Header1 = @bitCast(header_bytes[1]);
|
||||||
|
|
||||||
|
switch (h0.opcode) {
|
||||||
|
.text, .binary, .pong, .ping => {},
|
||||||
|
.connection_close => return error.ConnectionClose,
|
||||||
|
.continuation => return error.UnexpectedOpCode,
|
||||||
|
_ => return error.UnexpectedOpCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!h0.fin) return error.MessageTooBig;
|
||||||
|
if (!h1.mask) return error.MissingMaskBit;
|
||||||
|
|
||||||
|
const len: usize = switch (h1.payload_len) {
|
||||||
|
.len16 => try recvReadInt(ws, u16),
|
||||||
|
.len64 => std.math.cast(usize, try recvReadInt(ws, u64)) orelse return error.MessageTooBig,
|
||||||
|
else => @intFromEnum(h1.payload_len),
|
||||||
|
};
|
||||||
|
if (len > ws.recv_fifo.buf.len) return error.MessageTooBig;
|
||||||
|
|
||||||
|
const mask: u32 = @bitCast((try recv(ws, 4))[0..4].*);
|
||||||
|
const payload = try recv(ws, len);
|
||||||
|
|
||||||
|
// Skip pongs.
|
||||||
|
if (h0.opcode == .pong) continue;
|
||||||
|
|
||||||
|
// The last item may contain a partial word of unused data.
|
||||||
|
const floored_len = (payload.len / 4) * 4;
|
||||||
|
const u32_payload: []align(1) u32 = @alignCast(std.mem.bytesAsSlice(u32, payload[0..floored_len]));
|
||||||
|
for (u32_payload) |*elem| elem.* ^= mask;
|
||||||
|
const mask_bytes = std.mem.asBytes(&mask)[0 .. payload.len - floored_len];
|
||||||
|
for (payload[floored_len..], mask_bytes) |*leftover, m| leftover.* ^= m;
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.opcode = h0.opcode,
|
||||||
|
.data = payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecvError = std.http.Server.Request.ReadError || error{EndOfStream};
|
||||||
|
|
||||||
|
fn recv(ws: *WebSocket, len: usize) RecvError![]u8 {
|
||||||
|
ws.recv_fifo.discard(ws.outstanding_len);
|
||||||
|
assert(len <= ws.recv_fifo.buf.len);
|
||||||
|
if (len > ws.recv_fifo.count) {
|
||||||
|
const small_buf = ws.recv_fifo.writableSlice(0);
|
||||||
|
const needed = len - ws.recv_fifo.count;
|
||||||
|
const buf = if (small_buf.len >= needed) small_buf else b: {
|
||||||
|
ws.recv_fifo.realign();
|
||||||
|
break :b ws.recv_fifo.writableSlice(0);
|
||||||
|
};
|
||||||
|
const n = try @as(RecvError!usize, @errorCast(ws.reader.readAtLeast(buf, needed)));
|
||||||
|
if (n < needed) return error.EndOfStream;
|
||||||
|
ws.recv_fifo.update(n);
|
||||||
|
}
|
||||||
|
ws.outstanding_len = len;
|
||||||
|
// TODO: improve the std lib API so this cast isn't necessary.
|
||||||
|
return @constCast(ws.recv_fifo.readableSliceOfLen(len));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recvReadInt(ws: *WebSocket, comptime I: type) !I {
|
||||||
|
const unswapped: I = @bitCast((try recv(ws, @sizeOf(I)))[0..@sizeOf(I)].*);
|
||||||
|
return switch (native_endian) {
|
||||||
|
.little => @byteSwap(unswapped),
|
||||||
|
.big => unswapped,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const WriteError = std.http.Server.Response.WriteError;
|
||||||
|
|
||||||
|
pub fn writeMessage(ws: *WebSocket, message: []const u8, opcode: Opcode) WriteError!void {
|
||||||
|
const iovecs: [1]std.posix.iovec_const = .{
|
||||||
|
.{ .base = message.ptr, .len = message.len },
|
||||||
|
};
|
||||||
|
return writeMessagev(ws, &iovecs, opcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeMessagev(ws: *WebSocket, message: []const std.posix.iovec_const, opcode: Opcode) WriteError!void {
|
||||||
|
const total_len = l: {
|
||||||
|
var total_len: u64 = 0;
|
||||||
|
for (message) |iovec| total_len += iovec.len;
|
||||||
|
break :l total_len;
|
||||||
|
};
|
||||||
|
|
||||||
|
var header_buf: [2 + 8]u8 = undefined;
|
||||||
|
header_buf[0] = @bitCast(@as(Header0, .{
|
||||||
|
.opcode = opcode,
|
||||||
|
.fin = true,
|
||||||
|
}));
|
||||||
|
const header = switch (total_len) {
|
||||||
|
0...125 => blk: {
|
||||||
|
header_buf[1] = @bitCast(@as(Header1, .{
|
||||||
|
.payload_len = @enumFromInt(total_len),
|
||||||
|
.mask = false,
|
||||||
|
}));
|
||||||
|
break :blk header_buf[0..2];
|
||||||
|
},
|
||||||
|
126...0xffff => blk: {
|
||||||
|
header_buf[1] = @bitCast(@as(Header1, .{
|
||||||
|
.payload_len = .len16,
|
||||||
|
.mask = false,
|
||||||
|
}));
|
||||||
|
std.mem.writeInt(u16, header_buf[2..4], @intCast(total_len), .big);
|
||||||
|
break :blk header_buf[0..4];
|
||||||
|
},
|
||||||
|
else => blk: {
|
||||||
|
header_buf[1] = @bitCast(@as(Header1, .{
|
||||||
|
.payload_len = .len64,
|
||||||
|
.mask = false,
|
||||||
|
}));
|
||||||
|
std.mem.writeInt(u64, header_buf[2..10], total_len, .big);
|
||||||
|
break :blk header_buf[0..10];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = &ws.response;
|
||||||
|
try response.writeAll(header);
|
||||||
|
for (message) |iovec|
|
||||||
|
try response.writeAll(iovec.base[0..iovec.len]);
|
||||||
|
try response.flush();
|
||||||
|
}
|
||||||
@ -47,6 +47,11 @@ else switch (native_os) {
|
|||||||
.plan9 => std.os.plan9,
|
.plan9 => std.os.plan9,
|
||||||
else => struct {
|
else => struct {
|
||||||
pub const ucontext_t = void;
|
pub const ucontext_t = void;
|
||||||
|
pub const pid_t = void;
|
||||||
|
pub const pollfd = void;
|
||||||
|
pub const fd_t = void;
|
||||||
|
pub const uid_t = void;
|
||||||
|
pub const gid_t = void;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,14 @@ pub const Message = struct {
|
|||||||
/// The remaining bytes is the file path relative to that prefix.
|
/// The remaining bytes is the file path relative to that prefix.
|
||||||
/// The prefixes are hard-coded in Compilation.create (cwd, zig lib dir, local cache dir)
|
/// The prefixes are hard-coded in Compilation.create (cwd, zig lib dir, local cache dir)
|
||||||
file_system_inputs,
|
file_system_inputs,
|
||||||
|
/// Body is a u64le that indicates the file path within the cache used
|
||||||
|
/// to store coverage information. The integer is a hash of the PCs
|
||||||
|
/// stored within that file.
|
||||||
|
coverage_id,
|
||||||
|
/// Body is a u64le that indicates the function pointer virtual memory
|
||||||
|
/// address of the fuzz unit test. This is used to provide a starting
|
||||||
|
/// point to view coverage.
|
||||||
|
fuzz_start_addr,
|
||||||
|
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
@ -180,6 +188,14 @@ pub fn serveMessage(
|
|||||||
try s.out.writevAll(iovecs[0 .. bufs.len + 1]);
|
try s.out.writevAll(iovecs[0 .. bufs.len + 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn serveU64Message(s: *Server, tag: OutMessage.Tag, int: u64) !void {
|
||||||
|
const msg_le = bswap(int);
|
||||||
|
return s.serveMessage(.{
|
||||||
|
.tag = tag,
|
||||||
|
.bytes_len = @sizeOf(u64),
|
||||||
|
}, &.{std.mem.asBytes(&msg_le)});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn serveEmitBinPath(
|
pub fn serveEmitBinPath(
|
||||||
s: *Server,
|
s: *Server,
|
||||||
fs_path: []const u8,
|
fs_path: []const u8,
|
||||||
@ -187,7 +203,7 @@ pub fn serveEmitBinPath(
|
|||||||
) !void {
|
) !void {
|
||||||
try s.serveMessage(.{
|
try s.serveMessage(.{
|
||||||
.tag = .emit_bin_path,
|
.tag = .emit_bin_path,
|
||||||
.bytes_len = @as(u32, @intCast(fs_path.len + @sizeOf(OutMessage.EmitBinPath))),
|
.bytes_len = @intCast(fs_path.len + @sizeOf(OutMessage.EmitBinPath)),
|
||||||
}, &.{
|
}, &.{
|
||||||
std.mem.asBytes(&header),
|
std.mem.asBytes(&header),
|
||||||
fs_path,
|
fs_path,
|
||||||
@ -201,7 +217,7 @@ pub fn serveTestResults(
|
|||||||
const msg_le = bswap(msg);
|
const msg_le = bswap(msg);
|
||||||
try s.serveMessage(.{
|
try s.serveMessage(.{
|
||||||
.tag = .test_results,
|
.tag = .test_results,
|
||||||
.bytes_len = @as(u32, @intCast(@sizeOf(OutMessage.TestResults))),
|
.bytes_len = @intCast(@sizeOf(OutMessage.TestResults)),
|
||||||
}, &.{
|
}, &.{
|
||||||
std.mem.asBytes(&msg_le),
|
std.mem.asBytes(&msg_le),
|
||||||
});
|
});
|
||||||
@ -209,14 +225,14 @@ pub fn serveTestResults(
|
|||||||
|
|
||||||
pub fn serveErrorBundle(s: *Server, error_bundle: std.zig.ErrorBundle) !void {
|
pub fn serveErrorBundle(s: *Server, error_bundle: std.zig.ErrorBundle) !void {
|
||||||
const eb_hdr: OutMessage.ErrorBundle = .{
|
const eb_hdr: OutMessage.ErrorBundle = .{
|
||||||
.extra_len = @as(u32, @intCast(error_bundle.extra.len)),
|
.extra_len = @intCast(error_bundle.extra.len),
|
||||||
.string_bytes_len = @as(u32, @intCast(error_bundle.string_bytes.len)),
|
.string_bytes_len = @intCast(error_bundle.string_bytes.len),
|
||||||
};
|
};
|
||||||
const bytes_len = @sizeOf(OutMessage.ErrorBundle) +
|
const bytes_len = @sizeOf(OutMessage.ErrorBundle) +
|
||||||
4 * error_bundle.extra.len + error_bundle.string_bytes.len;
|
4 * error_bundle.extra.len + error_bundle.string_bytes.len;
|
||||||
try s.serveMessage(.{
|
try s.serveMessage(.{
|
||||||
.tag = .error_bundle,
|
.tag = .error_bundle,
|
||||||
.bytes_len = @as(u32, @intCast(bytes_len)),
|
.bytes_len = @intCast(bytes_len),
|
||||||
}, &.{
|
}, &.{
|
||||||
std.mem.asBytes(&eb_hdr),
|
std.mem.asBytes(&eb_hdr),
|
||||||
// TODO: implement @ptrCast between slices changing the length
|
// TODO: implement @ptrCast between slices changing the length
|
||||||
@ -251,7 +267,7 @@ pub fn serveTestMetadata(s: *Server, test_metadata: TestMetadata) !void {
|
|||||||
|
|
||||||
return s.serveMessage(.{
|
return s.serveMessage(.{
|
||||||
.tag = .test_metadata,
|
.tag = .test_metadata,
|
||||||
.bytes_len = @as(u32, @intCast(bytes_len)),
|
.bytes_len = @intCast(bytes_len),
|
||||||
}, &.{
|
}, &.{
|
||||||
std.mem.asBytes(&header),
|
std.mem.asBytes(&header),
|
||||||
// TODO: implement @ptrCast between slices changing the length
|
// TODO: implement @ptrCast between slices changing the length
|
||||||
|
|||||||
@ -1840,3 +1840,48 @@ fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !v
|
|||||||
try std.testing.expectEqual(source.len, last_token.loc.start);
|
try std.testing.expectEqual(source.len, last_token.loc.start);
|
||||||
try std.testing.expectEqual(source.len, last_token.loc.end);
|
try std.testing.expectEqual(source.len, last_token.loc.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "fuzzable properties upheld" {
|
||||||
|
const source = std.testing.fuzzInput(.{});
|
||||||
|
const source0 = try std.testing.allocator.dupeZ(u8, source);
|
||||||
|
defer std.testing.allocator.free(source0);
|
||||||
|
var tokenizer = Tokenizer.init(source0);
|
||||||
|
var tokenization_failed = false;
|
||||||
|
while (true) {
|
||||||
|
const token = tokenizer.next();
|
||||||
|
|
||||||
|
// Property: token end location after start location (or equal)
|
||||||
|
try std.testing.expect(token.loc.end >= token.loc.start);
|
||||||
|
|
||||||
|
switch (token.tag) {
|
||||||
|
.invalid => {
|
||||||
|
tokenization_failed = true;
|
||||||
|
|
||||||
|
// Property: invalid token always ends at newline or eof
|
||||||
|
try std.testing.expect(source0[token.loc.end] == '\n' or source0[token.loc.end] == 0);
|
||||||
|
},
|
||||||
|
.eof => {
|
||||||
|
// Property: EOF token is always 0-length at end of source.
|
||||||
|
try std.testing.expectEqual(source0.len, token.loc.start);
|
||||||
|
try std.testing.expectEqual(source0.len, token.loc.end);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
else => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source0.len > 0) for (source0, source0[1..][0..source0.len]) |cur, next| {
|
||||||
|
// Property: No null byte allowed except at end.
|
||||||
|
if (cur == 0) {
|
||||||
|
try std.testing.expect(tokenization_failed);
|
||||||
|
}
|
||||||
|
// Property: No ASCII control characters other than \n and \t are allowed.
|
||||||
|
if (std.ascii.isControl(cur) and cur != '\n' and cur != '\t') {
|
||||||
|
try std.testing.expect(tokenization_failed);
|
||||||
|
}
|
||||||
|
// Property: All '\r' must be followed by '\n'.
|
||||||
|
if (cur == '\r' and next != '\n') {
|
||||||
|
try std.testing.expect(tokenization_failed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -4201,10 +4201,11 @@ fn workerDocsWasm(comp: *Compilation, parent_prog_node: std.Progress.Node) void
|
|||||||
const prog_node = parent_prog_node.start("Compile Autodocs", 0);
|
const prog_node = parent_prog_node.start("Compile Autodocs", 0);
|
||||||
defer prog_node.end();
|
defer prog_node.end();
|
||||||
|
|
||||||
workerDocsWasmFallible(comp, prog_node) catch |err| {
|
workerDocsWasmFallible(comp, prog_node) catch |err| switch (err) {
|
||||||
comp.lockAndSetMiscFailure(.docs_wasm, "unable to build autodocs: {s}", .{
|
error.SubCompilationFailed => return, // error reported already
|
||||||
|
else => comp.lockAndSetMiscFailure(.docs_wasm, "unable to build autodocs: {s}", .{
|
||||||
@errorName(err),
|
@errorName(err),
|
||||||
});
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4274,8 +4275,29 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) anye
|
|||||||
.cc_argv = &.{},
|
.cc_argv = &.{},
|
||||||
.parent = null,
|
.parent = null,
|
||||||
.builtin_mod = null,
|
.builtin_mod = null,
|
||||||
.builtin_modules = null, // there is only one module in this compilation
|
.builtin_modules = null,
|
||||||
});
|
});
|
||||||
|
const walk_mod = try Package.Module.create(arena, .{
|
||||||
|
.global_cache_directory = comp.global_cache_directory,
|
||||||
|
.paths = .{
|
||||||
|
.root = .{
|
||||||
|
.root_dir = comp.zig_lib_directory,
|
||||||
|
.sub_path = "docs/wasm",
|
||||||
|
},
|
||||||
|
.root_src_path = "Walk.zig",
|
||||||
|
},
|
||||||
|
.fully_qualified_name = "Walk",
|
||||||
|
.inherited = .{
|
||||||
|
.resolved_target = resolved_target,
|
||||||
|
.optimize_mode = optimize_mode,
|
||||||
|
},
|
||||||
|
.global = config,
|
||||||
|
.cc_argv = &.{},
|
||||||
|
.parent = root_mod,
|
||||||
|
.builtin_mod = root_mod.getBuiltinDependency(),
|
||||||
|
.builtin_modules = null, // `builtin_mod` is set
|
||||||
|
});
|
||||||
|
try root_mod.deps.put(arena, "Walk", walk_mod);
|
||||||
const bin_basename = try std.zig.binNameAlloc(arena, .{
|
const bin_basename = try std.zig.binNameAlloc(arena, .{
|
||||||
.root_name = root_name,
|
.root_name = root_name,
|
||||||
.target = resolved_target.result,
|
.target = resolved_target.result,
|
||||||
|
|||||||
@ -7,9 +7,10 @@ pub fn build(b: *std.Build) void {
|
|||||||
b.default_step = test_step;
|
b.default_step = test_step;
|
||||||
|
|
||||||
const optimize: std.builtin.OptimizeMode = .Debug;
|
const optimize: std.builtin.OptimizeMode = .Debug;
|
||||||
const target = b.standardTargetOptions(.{});
|
const target = if (builtin.os.tag == .windows)
|
||||||
|
b.standardTargetOptions(.{})
|
||||||
if (builtin.os.tag != .windows) return;
|
else
|
||||||
|
b.resolveTargetQuery(.{ .os_tag = .windows });
|
||||||
|
|
||||||
if (builtin.cpu.arch == .aarch64) {
|
if (builtin.cpu.arch == .aarch64) {
|
||||||
// https://github.com/ziglang/zig/issues/18427
|
// https://github.com/ziglang/zig/issues/18427
|
||||||
|
|||||||
@ -17,11 +17,11 @@ pub fn main() !void {
|
|||||||
|
|
||||||
const module = try debug_info.getModuleForAddress(add_addr);
|
const module = try debug_info.getModuleForAddress(add_addr);
|
||||||
const symbol = try module.getSymbolAtAddress(allocator, add_addr);
|
const symbol = try module.getSymbolAtAddress(allocator, add_addr);
|
||||||
defer symbol.deinit(allocator);
|
defer if (symbol.source_location) |sl| allocator.free(sl.file_name);
|
||||||
|
|
||||||
try testing.expectEqualStrings("add", symbol.symbol_name);
|
try testing.expectEqualStrings("add", symbol.name);
|
||||||
try testing.expect(symbol.line_info != null);
|
try testing.expect(symbol.source_location != null);
|
||||||
try testing.expectEqualStrings("shared_lib.c", std.fs.path.basename(symbol.line_info.?.file_name));
|
try testing.expectEqualStrings("shared_lib.c", std.fs.path.basename(symbol.source_location.?.file_name));
|
||||||
try testing.expectEqual(@as(u64, 3), symbol.line_info.?.line);
|
try testing.expectEqual(@as(u64, 3), symbol.source_location.?.line);
|
||||||
try testing.expectEqual(@as(u64, 0), symbol.line_info.?.column);
|
try testing.expectEqual(@as(u64, 0), symbol.source_location.?.column);
|
||||||
}
|
}
|
||||||
|
|||||||
79
tools/dump-cov.zig
Normal file
79
tools/dump-cov.zig
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
//! Reads a Zig coverage file and prints human-readable information to stdout,
|
||||||
|
//! including file:line:column information for each PC.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const fatal = std.process.fatal;
|
||||||
|
const Path = std.Build.Cache.Path;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .{};
|
||||||
|
defer _ = general_purpose_allocator.deinit();
|
||||||
|
const gpa = general_purpose_allocator.allocator();
|
||||||
|
|
||||||
|
var arena_instance = std.heap.ArenaAllocator.init(gpa);
|
||||||
|
defer arena_instance.deinit();
|
||||||
|
const arena = arena_instance.allocator();
|
||||||
|
|
||||||
|
const args = try std.process.argsAlloc(arena);
|
||||||
|
const exe_file_name = args[1];
|
||||||
|
const cov_file_name = args[2];
|
||||||
|
|
||||||
|
const exe_path: Path = .{
|
||||||
|
.root_dir = std.Build.Cache.Directory.cwd(),
|
||||||
|
.sub_path = exe_file_name,
|
||||||
|
};
|
||||||
|
const cov_path: Path = .{
|
||||||
|
.root_dir = std.Build.Cache.Directory.cwd(),
|
||||||
|
.sub_path = cov_file_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
var coverage = std.debug.Coverage.init;
|
||||||
|
defer coverage.deinit(gpa);
|
||||||
|
|
||||||
|
var debug_info = std.debug.Info.load(gpa, exe_path, &coverage) catch |err| {
|
||||||
|
fatal("failed to load debug info for {}: {s}", .{ exe_path, @errorName(err) });
|
||||||
|
};
|
||||||
|
defer debug_info.deinit(gpa);
|
||||||
|
|
||||||
|
const cov_bytes = cov_path.root_dir.handle.readFileAlloc(arena, cov_path.sub_path, 1 << 30) catch |err| {
|
||||||
|
fatal("failed to load coverage file {}: {s}", .{ cov_path, @errorName(err) });
|
||||||
|
};
|
||||||
|
|
||||||
|
var bw = std.io.bufferedWriter(std.io.getStdOut().writer());
|
||||||
|
const stdout = bw.writer();
|
||||||
|
|
||||||
|
const header: *align(1) SeenPcsHeader = @ptrCast(cov_bytes);
|
||||||
|
try stdout.print("{any}\n", .{header.*});
|
||||||
|
//const n_bitset_elems = (header.pcs_len + 7) / 8;
|
||||||
|
const pcs_bytes = cov_bytes[@sizeOf(SeenPcsHeader)..][0 .. header.pcs_len * @sizeOf(usize)];
|
||||||
|
const pcs = try arena.alloc(usize, header.pcs_len);
|
||||||
|
for (0..pcs_bytes.len / @sizeOf(usize), pcs) |i, *pc| {
|
||||||
|
pc.* = std.mem.readInt(usize, pcs_bytes[i * @sizeOf(usize) ..][0..@sizeOf(usize)], .little);
|
||||||
|
}
|
||||||
|
assert(std.sort.isSorted(usize, pcs, {}, std.sort.asc(usize)));
|
||||||
|
|
||||||
|
const seen_pcs = cov_bytes[@sizeOf(SeenPcsHeader) + pcs.len * @sizeOf(usize) ..];
|
||||||
|
|
||||||
|
const source_locations = try arena.alloc(std.debug.Coverage.SourceLocation, pcs.len);
|
||||||
|
try debug_info.resolveAddresses(gpa, pcs, source_locations);
|
||||||
|
|
||||||
|
for (pcs, source_locations, 0..) |pc, sl, i| {
|
||||||
|
const file = debug_info.coverage.fileAt(sl.file);
|
||||||
|
const dir_name = debug_info.coverage.directories.keys()[file.directory_index];
|
||||||
|
const dir_name_slice = debug_info.coverage.stringAt(dir_name);
|
||||||
|
const hit: u1 = @truncate(seen_pcs[i / 8] >> @intCast(i % 8));
|
||||||
|
try stdout.print("{c}{x}: {s}/{s}:{d}:{d}\n", .{
|
||||||
|
"-+"[hit], pc, dir_name_slice, debug_info.coverage.stringAt(file.basename), sl.line, sl.column,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try bw.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
const SeenPcsHeader = extern struct {
|
||||||
|
n_runs: usize,
|
||||||
|
deduplicated_runs: usize,
|
||||||
|
pcs_len: usize,
|
||||||
|
lowest_stack: usize,
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user