fuzzer.zig (26234B) - Raw
1 const builtin = @import("builtin"); 2 const std = @import("std"); 3 const Allocator = std.mem.Allocator; 4 const assert = std.debug.assert; 5 const fatal = std.process.fatal; 6 const SeenPcsHeader = std.Build.abi.fuzz.SeenPcsHeader; 7 8 pub const std_options = std.Options{ 9 .logFn = logOverride, 10 }; 11 12 var log_file_buffer: [256]u8 = undefined; 13 var log_file_writer: ?std.fs.File.Writer = null; 14 15 fn logOverride( 16 comptime level: std.log.Level, 17 comptime scope: @Type(.enum_literal), 18 comptime format: []const u8, 19 args: anytype, 20 ) void { 21 const fw = if (log_file_writer) |*f| f else f: { 22 const f = fuzzer.cache_dir.createFile("tmp/libfuzzer.log", .{}) catch 23 @panic("failed to open fuzzer log file"); 24 log_file_writer = f.writer(&log_file_buffer); 25 break :f &log_file_writer.?; 26 }; 27 const prefix1 = comptime level.asText(); 28 const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; 29 fw.interface.print(prefix1 ++ prefix2 ++ format ++ "\n", args) catch 30 @panic("failed to write to fuzzer log"); 31 fw.interface.flush() catch @panic("failed to flush fuzzer log"); 32 } 33 34 /// Helps determine run uniqueness in the face of recursion. 35 export threadlocal var __sancov_lowest_stack: usize = 0; 36 37 export fn __sanitizer_cov_trace_const_cmp1(arg1: u8, arg2: u8) void { 38 handleCmp(@returnAddress(), arg1, arg2); 39 } 40 41 export fn __sanitizer_cov_trace_cmp1(arg1: u8, arg2: u8) void { 42 handleCmp(@returnAddress(), arg1, arg2); 43 } 44 45 export fn __sanitizer_cov_trace_const_cmp2(arg1: u16, arg2: u16) void { 46 handleCmp(@returnAddress(), arg1, arg2); 47 } 48 49 export fn __sanitizer_cov_trace_cmp2(arg1: u16, arg2: u16) void { 50 handleCmp(@returnAddress(), arg1, arg2); 51 } 52 53 export fn __sanitizer_cov_trace_const_cmp4(arg1: u32, arg2: u32) void { 54 handleCmp(@returnAddress(), arg1, arg2); 55 } 56 57 export fn __sanitizer_cov_trace_cmp4(arg1: u32, arg2: u32) void { 58 handleCmp(@returnAddress(), arg1, arg2); 59 } 60 61 export fn __sanitizer_cov_trace_const_cmp8(arg1: u64, arg2: u64) void { 62 handleCmp(@returnAddress(), arg1, arg2); 63 } 64 65 export fn __sanitizer_cov_trace_cmp8(arg1: u64, arg2: u64) void { 66 handleCmp(@returnAddress(), arg1, arg2); 67 } 68 69 export fn __sanitizer_cov_trace_switch(val: u64, cases_ptr: [*]u64) void { 70 const pc = @returnAddress(); 71 const len = cases_ptr[0]; 72 const val_size_in_bits = cases_ptr[1]; 73 const cases = cases_ptr[2..][0..len]; 74 fuzzer.traceValue(pc ^ val); 75 _ = val_size_in_bits; 76 _ = cases; 77 //std.log.debug("0x{x}: switch on value {d} ({d} bits) with {d} cases", .{ 78 // pc, val, val_size_in_bits, cases.len, 79 //}); 80 } 81 82 export fn __sanitizer_cov_trace_pc_indir(callee: usize) void { 83 // Not valuable because we already have pc tracing via 8bit counters. 84 _ = callee; 85 //const pc = @returnAddress(); 86 //fuzzer.traceValue(pc ^ callee); 87 //std.log.debug("0x{x}: indirect call to 0x{x}", .{ pc, callee }); 88 } 89 export fn __sanitizer_cov_8bit_counters_init(start: usize, end: usize) void { 90 // clang will emit a call to this function when compiling with code coverage instrumentation. 91 // however fuzzer_init() does not need this information, since it directly reads from the symbol table. 92 _ = start; 93 _ = end; 94 } 95 export fn __sanitizer_cov_pcs_init(start: usize, end: usize) void { 96 // clang will emit a call to this function when compiling with code coverage instrumentation. 97 // however fuzzer_init() does not need this information, since it directly reads from the symbol table. 98 _ = start; 99 _ = end; 100 } 101 102 fn handleCmp(pc: usize, arg1: u64, arg2: u64) void { 103 fuzzer.traceValue(pc ^ arg1 ^ arg2); 104 //std.log.debug("0x{x}: comparison of {d} and {d}", .{ pc, arg1, arg2 }); 105 } 106 107 const Fuzzer = struct { 108 rng: std.Random.DefaultPrng, 109 pcs: []const usize, 110 pc_counters: []u8, 111 n_runs: usize, 112 traced_comparisons: std.AutoArrayHashMapUnmanaged(usize, void), 113 /// Tracks which PCs have been seen across all runs that do not crash the fuzzer process. 114 /// Stored in a memory-mapped file so that it can be shared with other 115 /// processes and viewed while the fuzzer is running. 116 seen_pcs: MemoryMappedList, 117 cache_dir: std.fs.Dir, 118 /// Identifies the file name that will be used to store coverage 119 /// information, available to other processes. 120 coverage_id: u64, 121 unit_test_name: []const u8, 122 123 /// The index corresponds to the file name within the f/ subdirectory. 124 /// The string is the input. 125 /// This data is read-only; it caches what is on the filesystem. 126 corpus: std.ArrayListUnmanaged(Input), 127 corpus_directory: std.Build.Cache.Directory, 128 129 /// The next input that will be given to the testOne function. When the 130 /// current process crashes, this memory-mapped file is used to recover the 131 /// input. 132 /// 133 /// The file size corresponds to the capacity. The length is not stored 134 /// and that is the next thing to work on! 135 input: MemoryMappedList, 136 137 const Input = struct { 138 bytes: []u8, 139 last_traced_comparison: usize, 140 }; 141 142 const Slice = extern struct { 143 ptr: [*]const u8, 144 len: usize, 145 146 fn toZig(s: Slice) []const u8 { 147 return s.ptr[0..s.len]; 148 } 149 150 fn fromZig(s: []const u8) Slice { 151 return .{ 152 .ptr = s.ptr, 153 .len = s.len, 154 }; 155 } 156 }; 157 158 fn init(f: *Fuzzer, cache_dir: std.fs.Dir, pc_counters: []u8, pcs: []const usize) !void { 159 f.cache_dir = cache_dir; 160 f.pc_counters = pc_counters; 161 f.pcs = pcs; 162 163 // Choose a file name for the coverage based on a hash of the PCs that will be stored within. 164 const pc_digest = std.hash.Wyhash.hash(0, std.mem.sliceAsBytes(pcs)); 165 f.coverage_id = pc_digest; 166 const hex_digest = std.fmt.hex(pc_digest); 167 const coverage_file_path = "v/" ++ hex_digest; 168 169 // Layout of this file: 170 // - Header 171 // - list of PC addresses (usize elements) 172 // - list of hit flag, 1 bit per address (stored in u8 elements) 173 const coverage_file = createFileBail(cache_dir, coverage_file_path, .{ 174 .read = true, 175 .truncate = false, 176 }); 177 const n_bitset_elems = (pcs.len + @bitSizeOf(usize) - 1) / @bitSizeOf(usize); 178 comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize); 179 comptime assert(SeenPcsHeader.trailing[1] == .pc_addr); 180 const bytes_len = @sizeOf(SeenPcsHeader) + 181 n_bitset_elems * @sizeOf(usize) + 182 pcs.len * @sizeOf(usize); 183 const existing_len = coverage_file.getEndPos() catch |err| { 184 fatal("unable to check len of coverage file: {s}", .{@errorName(err)}); 185 }; 186 if (existing_len == 0) { 187 coverage_file.setEndPos(bytes_len) catch |err| { 188 fatal("unable to set len of coverage file: {s}", .{@errorName(err)}); 189 }; 190 } else if (existing_len != bytes_len) { 191 fatal("incompatible existing coverage file (differing lengths)", .{}); 192 } 193 f.seen_pcs = MemoryMappedList.init(coverage_file, existing_len, bytes_len) catch |err| { 194 fatal("unable to init coverage memory map: {s}", .{@errorName(err)}); 195 }; 196 if (existing_len != 0) { 197 const existing_pcs_bytes = f.seen_pcs.items[@sizeOf(SeenPcsHeader) + @sizeOf(usize) * n_bitset_elems ..][0 .. pcs.len * @sizeOf(usize)]; 198 const existing_pcs = std.mem.bytesAsSlice(usize, existing_pcs_bytes); 199 for (existing_pcs, pcs, 0..) |old, new, i| { 200 if (old != new) { 201 fatal("incompatible existing coverage file (differing PC at index {d}: {x} != {x})", .{ 202 i, old, new, 203 }); 204 } 205 } 206 } else { 207 const header: SeenPcsHeader = .{ 208 .n_runs = 0, 209 .unique_runs = 0, 210 .pcs_len = pcs.len, 211 }; 212 f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&header)); 213 f.seen_pcs.appendNTimesAssumeCapacity(0, n_bitset_elems * @sizeOf(usize)); 214 f.seen_pcs.appendSliceAssumeCapacity(std.mem.sliceAsBytes(pcs)); 215 } 216 } 217 218 fn initNextInput(f: *Fuzzer) void { 219 while (true) { 220 const i = f.corpus.items.len; 221 var buf: [30]u8 = undefined; 222 const input_sub_path = std.fmt.bufPrint(&buf, "{d}", .{i}) catch unreachable; 223 const input = f.corpus_directory.handle.readFileAlloc(gpa, input_sub_path, 1 << 31) catch |err| switch (err) { 224 error.FileNotFound => { 225 // Make this one the next input. 226 const input_file = f.corpus_directory.handle.createFile(input_sub_path, .{ 227 .exclusive = true, 228 .truncate = false, 229 .read = true, 230 }) catch |e| switch (e) { 231 error.PathAlreadyExists => continue, 232 else => fatal("unable to create '{f}{d}: {s}", .{ f.corpus_directory, i, @errorName(err) }), 233 }; 234 errdefer input_file.close(); 235 // Initialize the mmap for the current input. 236 f.input = MemoryMappedList.create(input_file, 0, std.heap.page_size_max) catch |e| { 237 fatal("unable to init memory map for input at '{f}{d}': {s}", .{ 238 f.corpus_directory, i, @errorName(e), 239 }); 240 }; 241 break; 242 }, 243 else => fatal("unable to read '{f}{d}': {s}", .{ f.corpus_directory, i, @errorName(err) }), 244 }; 245 errdefer gpa.free(input); 246 f.corpus.append(gpa, .{ 247 .bytes = input, 248 .last_traced_comparison = 0, 249 }) catch |err| oom(err); 250 } 251 } 252 253 fn addCorpusElem(f: *Fuzzer, input: []const u8) !void { 254 try f.corpus.append(gpa, .{ 255 .bytes = try gpa.dupe(u8, input), 256 .last_traced_comparison = 0, 257 }); 258 } 259 260 fn start(f: *Fuzzer) !void { 261 const rng = fuzzer.rng.random(); 262 263 // Grab the corpus which is namespaced based on `unit_test_name`. 264 { 265 if (f.unit_test_name.len == 0) fatal("test runner never set unit test name", .{}); 266 const sub_path = try std.fmt.allocPrint(gpa, "f/{s}", .{f.unit_test_name}); 267 f.corpus_directory = .{ 268 .handle = f.cache_dir.makeOpenPath(sub_path, .{}) catch |err| 269 fatal("unable to open corpus directory 'f/{s}': {t}", .{ sub_path, err }), 270 .path = sub_path, 271 }; 272 initNextInput(f); 273 } 274 275 assert(f.n_runs == 0); 276 277 // If the corpus is empty, synthesize one input. 278 if (f.corpus.items.len == 0) { 279 const len = rng.uintLessThanBiased(usize, 200); 280 const slice = try gpa.alloc(u8, len); 281 rng.bytes(slice); 282 f.input.appendSliceAssumeCapacity(slice); 283 try f.corpus.append(gpa, .{ 284 .bytes = slice, 285 .last_traced_comparison = 0, 286 }); 287 runOne(f, 0); 288 } 289 290 while (true) { 291 const chosen_index = rng.uintLessThanBiased(usize, f.corpus.items.len); 292 const modification = rng.enumValue(Mutation); 293 f.mutateAndRunOne(chosen_index, modification); 294 } 295 } 296 297 /// `x` represents a possible branch. It is the PC address of the possible 298 /// branch site, hashed together with the value(s) used that determine to 299 /// where it branches. 300 fn traceValue(f: *Fuzzer, x: usize) void { 301 errdefer |err| oom(err); 302 try f.traced_comparisons.put(gpa, x, {}); 303 } 304 305 const Mutation = enum { 306 remove_byte, 307 modify_byte, 308 add_byte, 309 }; 310 311 fn mutateAndRunOne(f: *Fuzzer, corpus_index: usize, mutation: Mutation) void { 312 const rng = fuzzer.rng.random(); 313 f.input.clearRetainingCapacity(); 314 const old_input = f.corpus.items[corpus_index].bytes; 315 f.input.ensureTotalCapacity(old_input.len + 1) catch @panic("mmap file resize failed"); 316 switch (mutation) { 317 .remove_byte => { 318 const omitted_index = rng.uintLessThanBiased(usize, old_input.len); 319 f.input.appendSliceAssumeCapacity(old_input[0..omitted_index]); 320 f.input.appendSliceAssumeCapacity(old_input[omitted_index + 1 ..]); 321 }, 322 .modify_byte => { 323 const modified_index = rng.uintLessThanBiased(usize, old_input.len); 324 f.input.appendSliceAssumeCapacity(old_input); 325 f.input.items[modified_index] = rng.int(u8); 326 }, 327 .add_byte => { 328 const modified_index = rng.uintLessThanBiased(usize, old_input.len); 329 f.input.appendSliceAssumeCapacity(old_input[0..modified_index]); 330 f.input.appendAssumeCapacity(rng.int(u8)); 331 f.input.appendSliceAssumeCapacity(old_input[modified_index..]); 332 }, 333 } 334 runOne(f, corpus_index); 335 } 336 337 fn runOne(f: *Fuzzer, corpus_index: usize) void { 338 const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]); 339 340 f.traced_comparisons.clearRetainingCapacity(); 341 @memset(f.pc_counters, 0); 342 __sancov_lowest_stack = std.math.maxInt(usize); 343 344 fuzzer_one(@volatileCast(f.input.items.ptr), f.input.items.len); 345 346 f.n_runs += 1; 347 _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic); 348 349 // Track code coverage from all runs. 350 comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize); 351 const header_end_ptr: [*]volatile usize = @ptrCast(f.seen_pcs.items[@sizeOf(SeenPcsHeader)..]); 352 const remainder = f.pcs.len % @bitSizeOf(usize); 353 const aligned_len = f.pcs.len - remainder; 354 const seen_pcs = header_end_ptr[0..aligned_len]; 355 const pc_counters = std.mem.bytesAsSlice([@bitSizeOf(usize)]u8, f.pc_counters[0..aligned_len]); 356 const V = @Vector(@bitSizeOf(usize), u8); 357 const zero_v: V = @splat(0); 358 var fresh = false; 359 var superset = true; 360 361 for (header_end_ptr[0..pc_counters.len], pc_counters) |*elem, *array| { 362 const v: V = array.*; 363 const mask: usize = @bitCast(v != zero_v); 364 const prev = @atomicRmw(usize, elem, .Or, mask, .monotonic); 365 fresh = fresh or (prev | mask) != prev; 366 superset = superset and (prev | mask) != mask; 367 } 368 if (remainder > 0) { 369 const i = pc_counters.len; 370 const elem = &seen_pcs[i]; 371 var mask: usize = 0; 372 for (f.pc_counters[i * @bitSizeOf(usize) ..][0..remainder], 0..) |byte, bit_index| { 373 mask |= @as(usize, @intFromBool(byte != 0)) << @intCast(bit_index); 374 } 375 const prev = @atomicRmw(usize, elem, .Or, mask, .monotonic); 376 fresh = fresh or (prev | mask) != prev; 377 superset = superset and (prev | mask) != mask; 378 } 379 380 // First check if this is a better version of an already existing 381 // input, replacing that input. 382 if (superset or f.traced_comparisons.entries.len >= f.corpus.items[corpus_index].last_traced_comparison) { 383 const new_input = gpa.realloc(f.corpus.items[corpus_index].bytes, f.input.items.len) catch |err| oom(err); 384 f.corpus.items[corpus_index] = .{ 385 .bytes = new_input, 386 .last_traced_comparison = f.traced_comparisons.count(), 387 }; 388 @memcpy(new_input, @volatileCast(f.input.items)); 389 _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic); 390 return; 391 } 392 393 if (!fresh) return; 394 395 // Input is already committed to the file system, we just need to open a new file 396 // for the next input. 397 // Pre-add it to the corpus list so that it does not get redundantly picked up. 398 f.corpus.append(gpa, .{ 399 .bytes = gpa.dupe(u8, @volatileCast(f.input.items)) catch |err| oom(err), 400 .last_traced_comparison = f.traced_comparisons.entries.len, 401 }) catch |err| oom(err); 402 f.input.deinit(); 403 initNextInput(f); 404 405 // TODO: also mark input as "hot" so it gets prioritized for checking mutations above others. 406 407 _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic); 408 } 409 }; 410 411 fn createFileBail(dir: std.fs.Dir, sub_path: []const u8, flags: std.fs.File.CreateFlags) std.fs.File { 412 return dir.createFile(sub_path, flags) catch |err| switch (err) { 413 error.FileNotFound => { 414 const dir_name = std.fs.path.dirname(sub_path).?; 415 dir.makePath(dir_name) catch |e| { 416 fatal("unable to make path '{s}': {s}", .{ dir_name, @errorName(e) }); 417 }; 418 return dir.createFile(sub_path, flags) catch |e| { 419 fatal("unable to create file '{s}': {s}", .{ sub_path, @errorName(e) }); 420 }; 421 }, 422 else => fatal("unable to create file '{s}': {s}", .{ sub_path, @errorName(err) }), 423 }; 424 } 425 426 fn oom(err: anytype) noreturn { 427 switch (err) { 428 error.OutOfMemory => @panic("out of memory"), 429 } 430 } 431 432 var debug_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init; 433 434 const gpa = switch (builtin.mode) { 435 .Debug => debug_allocator.allocator(), 436 .ReleaseFast, .ReleaseSmall, .ReleaseSafe => std.heap.smp_allocator, 437 }; 438 439 var fuzzer: Fuzzer = .{ 440 .rng = std.Random.DefaultPrng.init(0), 441 .input = undefined, 442 .pcs = undefined, 443 .pc_counters = undefined, 444 .n_runs = 0, 445 .cache_dir = undefined, 446 .seen_pcs = undefined, 447 .coverage_id = undefined, 448 .unit_test_name = &.{}, 449 .corpus = .empty, 450 .corpus_directory = undefined, 451 .traced_comparisons = .empty, 452 }; 453 454 /// Invalid until `fuzzer_init` is called. 455 export fn fuzzer_coverage_id() u64 { 456 return fuzzer.coverage_id; 457 } 458 459 var fuzzer_one: *const fn (input_ptr: [*]const u8, input_len: usize) callconv(.c) void = undefined; 460 461 export fn fuzzer_start(testOne: @TypeOf(fuzzer_one)) void { 462 fuzzer_one = testOne; 463 fuzzer.start() catch |err| oom(err); 464 } 465 466 export fn fuzzer_set_name(name_ptr: [*]const u8, name_len: usize) void { 467 fuzzer.unit_test_name = name_ptr[0..name_len]; 468 } 469 470 export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void { 471 // Linkers are expected to automatically add `__start_<section>` and 472 // `__stop_<section>` symbols when section names are valid C identifiers. 473 474 const ofmt = builtin.object_format; 475 476 const start_symbol_prefix: []const u8 = if (ofmt == .macho) 477 "\x01section$start$__DATA$__" 478 else 479 "__start___"; 480 const end_symbol_prefix: []const u8 = if (ofmt == .macho) 481 "\x01section$end$__DATA$__" 482 else 483 "__stop___"; 484 485 const pc_counters_start_name = start_symbol_prefix ++ "sancov_cntrs"; 486 const pc_counters_start = @extern([*]u8, .{ 487 .name = pc_counters_start_name, 488 .linkage = .weak, 489 }) orelse fatal("missing {s} symbol", .{pc_counters_start_name}); 490 491 const pc_counters_end_name = end_symbol_prefix ++ "sancov_cntrs"; 492 const pc_counters_end = @extern([*]u8, .{ 493 .name = pc_counters_end_name, 494 .linkage = .weak, 495 }) orelse fatal("missing {s} symbol", .{pc_counters_end_name}); 496 497 const pc_counters = pc_counters_start[0 .. pc_counters_end - pc_counters_start]; 498 499 const pcs_start_name = start_symbol_prefix ++ "sancov_pcs1"; 500 const pcs_start = @extern([*]usize, .{ 501 .name = pcs_start_name, 502 .linkage = .weak, 503 }) orelse fatal("missing {s} symbol", .{pcs_start_name}); 504 505 const pcs_end_name = end_symbol_prefix ++ "sancov_pcs1"; 506 const pcs_end = @extern([*]usize, .{ 507 .name = pcs_end_name, 508 .linkage = .weak, 509 }) orelse fatal("missing {s} symbol", .{pcs_end_name}); 510 511 const pcs = pcs_start[0 .. pcs_end - pcs_start]; 512 513 const cache_dir_path = cache_dir_struct.toZig(); 514 const cache_dir = if (cache_dir_path.len == 0) 515 std.fs.cwd() 516 else 517 std.fs.cwd().makeOpenPath(cache_dir_path, .{ .iterate = true }) catch |err| { 518 fatal("unable to open fuzz directory '{s}': {s}", .{ cache_dir_path, @errorName(err) }); 519 }; 520 521 fuzzer.init(cache_dir, pc_counters, pcs) catch |err| 522 fatal("unable to init fuzzer: {s}", .{@errorName(err)}); 523 } 524 525 export fn fuzzer_init_corpus_elem(input_ptr: [*]const u8, input_len: usize) void { 526 fuzzer.addCorpusElem(input_ptr[0..input_len]) catch |err| 527 fatal("failed to add corpus element: {s}", .{@errorName(err)}); 528 } 529 530 /// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping. 531 pub const MemoryMappedList = struct { 532 /// Contents of the list. 533 /// 534 /// Pointers to elements in this slice are invalidated by various functions 535 /// of this ArrayList in accordance with the respective documentation. In 536 /// all cases, "invalidated" means that the memory has been passed to this 537 /// allocator's resize or free function. 538 items: []align(std.heap.page_size_min) volatile u8, 539 /// How many bytes this list can hold without allocating additional memory. 540 capacity: usize, 541 /// The file is kept open so that it can be resized. 542 file: std.fs.File, 543 544 pub fn init(file: std.fs.File, length: usize, capacity: usize) !MemoryMappedList { 545 const ptr = try std.posix.mmap( 546 null, 547 capacity, 548 std.posix.PROT.READ | std.posix.PROT.WRITE, 549 .{ .TYPE = .SHARED }, 550 file.handle, 551 0, 552 ); 553 return .{ 554 .file = file, 555 .items = ptr[0..length], 556 .capacity = capacity, 557 }; 558 } 559 560 pub fn create(file: std.fs.File, length: usize, capacity: usize) !MemoryMappedList { 561 try file.setEndPos(capacity); 562 return init(file, length, capacity); 563 } 564 565 pub fn deinit(l: *MemoryMappedList) void { 566 l.file.close(); 567 std.posix.munmap(@volatileCast(l.items.ptr[0..l.capacity])); 568 l.* = undefined; 569 } 570 571 /// Modify the array so that it can hold at least `additional_count` **more** items. 572 /// Invalidates element pointers if additional memory is needed. 573 pub fn ensureUnusedCapacity(l: *MemoryMappedList, additional_count: usize) !void { 574 return l.ensureTotalCapacity(l.items.len + additional_count); 575 } 576 577 /// If the current capacity is less than `new_capacity`, this function will 578 /// modify the array so that it can hold at least `new_capacity` items. 579 /// Invalidates element pointers if additional memory is needed. 580 pub fn ensureTotalCapacity(l: *MemoryMappedList, new_capacity: usize) !void { 581 if (l.capacity >= new_capacity) return; 582 583 const better_capacity = growCapacity(l.capacity, new_capacity); 584 return l.ensureTotalCapacityPrecise(better_capacity); 585 } 586 587 pub fn ensureTotalCapacityPrecise(l: *MemoryMappedList, new_capacity: usize) !void { 588 if (l.capacity >= new_capacity) return; 589 590 std.posix.munmap(@volatileCast(l.items.ptr[0..l.capacity])); 591 try l.file.setEndPos(new_capacity); 592 l.* = try init(l.file, l.items.len, new_capacity); 593 } 594 595 /// Invalidates all element pointers. 596 pub fn clearRetainingCapacity(l: *MemoryMappedList) void { 597 l.items.len = 0; 598 } 599 600 /// Append the slice of items to the list. 601 /// Asserts that the list can hold the additional items. 602 pub fn appendSliceAssumeCapacity(l: *MemoryMappedList, items: []const u8) void { 603 const old_len = l.items.len; 604 const new_len = old_len + items.len; 605 assert(new_len <= l.capacity); 606 l.items.len = new_len; 607 @memcpy(l.items[old_len..][0..items.len], items); 608 } 609 610 /// Extends the list by 1 element. 611 /// Never invalidates element pointers. 612 /// Asserts that the list can hold one additional item. 613 pub fn appendAssumeCapacity(l: *MemoryMappedList, item: u8) void { 614 const new_item_ptr = l.addOneAssumeCapacity(); 615 new_item_ptr.* = item; 616 } 617 618 /// Increase length by 1, returning pointer to the new item. 619 /// The returned pointer becomes invalid when the list is resized. 620 /// Never invalidates element pointers. 621 /// Asserts that the list can hold one additional item. 622 pub fn addOneAssumeCapacity(l: *MemoryMappedList) *volatile u8 { 623 assert(l.items.len < l.capacity); 624 l.items.len += 1; 625 return &l.items[l.items.len - 1]; 626 } 627 628 /// Append a value to the list `n` times. 629 /// Never invalidates element pointers. 630 /// The function is inline so that a comptime-known `value` parameter will 631 /// have better memset codegen in case it has a repeated byte pattern. 632 /// Asserts that the list can hold the additional items. 633 pub inline fn appendNTimesAssumeCapacity(l: *MemoryMappedList, value: u8, n: usize) void { 634 const new_len = l.items.len + n; 635 assert(new_len <= l.capacity); 636 @memset(l.items.ptr[l.items.len..new_len], value); 637 l.items.len = new_len; 638 } 639 640 /// Resize the array, adding `n` new elements, which have `undefined` values. 641 /// The return value is a slice pointing to the newly allocated elements. 642 /// Never invalidates element pointers. 643 /// The returned pointer becomes invalid when the list is resized. 644 /// Asserts that the list can hold the additional items. 645 pub fn addManyAsSliceAssumeCapacity(l: *MemoryMappedList, n: usize) []volatile u8 { 646 assert(l.items.len + n <= l.capacity); 647 const prev_len = l.items.len; 648 l.items.len += n; 649 return l.items[prev_len..][0..n]; 650 } 651 652 /// Called when memory growth is necessary. Returns a capacity larger than 653 /// minimum that grows super-linearly. 654 fn growCapacity(current: usize, minimum: usize) usize { 655 var new = current; 656 while (true) { 657 new = std.mem.alignForward(usize, new + new / 2, std.heap.page_size_max); 658 if (new >= minimum) return new; 659 } 660 } 661 };