| ... | ... | @@ -145,6 +145,8 @@ pub const bin_digest_len = 16; |
| 145 | 145 | pub const hex_digest_len = bin_digest_len * 2; |
| 146 | 146 | pub const BinDigest = [bin_digest_len]u8; |
| 147 | 147 | |
| 148 | /// This is currently just an arbitrary non-empty string that can't match another manifest line. |
| 149 | const manifest_header = "0"; |
| 148 | 150 | const manifest_file_size_max = 50 * 1024 * 1024; |
| 149 | 151 | |
| 150 | 152 | /// The type used for hashing file contents. Currently, this is SipHash128(1, 3), because it |
| ... | ... | @@ -152,8 +154,15 @@ const manifest_file_size_max = 50 * 1024 * 1024; |
| 152 | 154 | /// fastest options right now. |
| 153 | 155 | pub const Hasher = crypto.auth.siphash.SipHash128(1, 3); |
| 154 | 156 | |
| 155 | | /// Initial state, that can be copied. |
| 156 | | pub const hasher_init: Hasher = Hasher.init(&[_]u8{0} ** Hasher.key_length); |
| 157 | /// Initial state with random bytes, that can be copied. |
| 158 | /// Refresh this with new random bytes when the manifest |
| 159 | /// format is modified in a non-backwards-compatible way. |
| 160 | pub const hasher_init: Hasher = Hasher.init(&[_]u8{ |
| 161 | 0x33, 0x52, 0xa2, 0x84, |
| 162 | 0xcf, 0x17, 0x56, 0x57, |
| 163 | 0x01, 0xbb, 0xcd, 0xe4, |
| 164 | 0x77, 0xd6, 0xf0, 0x60, |
| 165 | }); |
| 157 | 166 | |
| 158 | 167 | pub const File = struct { |
| 159 | 168 | prefixed_path: ?PrefixedPath, |
| ... | ... | @@ -391,208 +400,179 @@ pub const Manifest = struct { |
| 391 | 400 | @memcpy(manifest_file_path[0..self.hex_digest.len], &self.hex_digest); |
| 392 | 401 | manifest_file_path[hex_digest_len..][0..ext.len].* = ext.*; |
| 393 | 402 | |
| 394 | | if (self.files.items.len == 0) { |
| 395 | | // If there are no file inputs, we check if the manifest file exists instead of |
| 396 | | // comparing the hashes on the files used for the cached item |
| 397 | | while (true) { |
| 398 | | if (self.cache.manifest_dir.openFile(&manifest_file_path, .{ |
| 399 | | .mode = .read_write, |
| 400 | | .lock = .Exclusive, |
| 401 | | .lock_nonblocking = self.want_shared_lock, |
| 402 | | })) |manifest_file| { |
| 403 | | self.manifest_file = manifest_file; |
| 404 | | self.have_exclusive_lock = true; |
| 405 | | break; |
| 406 | | } else |open_err| switch (open_err) { |
| 407 | | error.WouldBlock => { |
| 408 | | self.manifest_file = try self.cache.manifest_dir.openFile(&manifest_file_path, .{ |
| 409 | | .lock = .Shared, |
| 410 | | }); |
| 411 | | break; |
| 412 | | }, |
| 413 | | error.FileNotFound => { |
| 414 | | if (self.cache.manifest_dir.createFile(&manifest_file_path, .{ |
| 415 | | .read = true, |
| 416 | | .truncate = false, |
| 417 | | .lock = .Exclusive, |
| 418 | | .lock_nonblocking = self.want_shared_lock, |
| 419 | | })) |manifest_file| { |
| 420 | | self.manifest_file = manifest_file; |
| 421 | | self.manifest_dirty = true; |
| 422 | | self.have_exclusive_lock = true; |
| 423 | | return false; // cache miss; exclusive lock already held |
| 424 | | } else |err| switch (err) { |
| 425 | | // There are no dir components, so you would think |
| 426 | | // that this was unreachable, however we have |
| 427 | | // observed on macOS two processes racing to do |
| 428 | | // openat() with O_CREAT manifest in ENOENT. |
| 429 | | error.WouldBlock, error.FileNotFound => continue, |
| 430 | | else => |e| return e, |
| 431 | | } |
| 432 | | }, |
| 433 | | else => |e| return e, |
| 434 | | } |
| 435 | | } |
| 436 | | } else { |
| 437 | | while (true) { |
| 438 | | if (self.cache.manifest_dir.createFile(&manifest_file_path, .{ |
| 439 | | .read = true, |
| 440 | | .truncate = false, |
| 441 | | .lock = .Exclusive, |
| 442 | | .lock_nonblocking = self.want_shared_lock, |
| 443 | | })) |manifest_file| { |
| 444 | | self.manifest_file = manifest_file; |
| 445 | | self.have_exclusive_lock = true; |
| 403 | while (true) { |
| 404 | if (self.cache.manifest_dir.createFile(&manifest_file_path, .{ |
| 405 | .read = true, |
| 406 | .truncate = false, |
| 407 | .lock = .Exclusive, |
| 408 | .lock_nonblocking = self.want_shared_lock, |
| 409 | })) |manifest_file| { |
| 410 | self.manifest_file = manifest_file; |
| 411 | self.have_exclusive_lock = true; |
| 412 | break; |
| 413 | } else |err| switch (err) { |
| 414 | error.WouldBlock => { |
| 415 | self.manifest_file = try self.cache.manifest_dir.openFile(&manifest_file_path, .{ |
| 416 | .mode = .read_write, |
| 417 | .lock = .Shared, |
| 418 | }); |
| 446 | 419 | break; |
| 447 | | } else |err| switch (err) { |
| 448 | | error.WouldBlock => { |
| 449 | | self.manifest_file = try self.cache.manifest_dir.openFile(&manifest_file_path, .{ |
| 450 | | .lock = .Shared, |
| 451 | | }); |
| 452 | | break; |
| 453 | | }, |
| 454 | | // There are no dir components, so you would think that this was |
| 455 | | // unreachable, however we have observed on macOS two processes racing |
| 456 | | // to do openat() with O_CREAT manifest in ENOENT. |
| 457 | | error.FileNotFound => continue, |
| 458 | | else => |e| return e, |
| 459 | | } |
| 420 | }, |
| 421 | // There are no dir components, so you would think that this was |
| 422 | // unreachable, however we have observed on macOS two processes racing |
| 423 | // to do openat() with O_CREAT manifest in ENOENT. |
| 424 | error.FileNotFound => continue, |
| 425 | else => |e| return e, |
| 460 | 426 | } |
| 461 | 427 | } |
| 462 | 428 | |
| 463 | 429 | self.want_refresh_timestamp = true; |
| 464 | 430 | |
| 465 | | const file_contents = try self.manifest_file.?.reader().readAllAlloc(gpa, manifest_file_size_max); |
| 466 | | defer gpa.free(file_contents); |
| 467 | | |
| 468 | | const input_file_count = self.files.items.len; |
| 469 | | var any_file_changed = false; |
| 470 | | var line_iter = mem.tokenize(u8, file_contents, "\n"); |
| 471 | | var idx: usize = 0; |
| 472 | | while (line_iter.next()) |line| { |
| 473 | | defer idx += 1; |
| 474 | | |
| 475 | | const cache_hash_file = if (idx < input_file_count) &self.files.items[idx] else blk: { |
| 476 | | const new = try self.files.addOne(gpa); |
| 477 | | new.* = .{ |
| 478 | | .prefixed_path = null, |
| 479 | | .contents = null, |
| 480 | | .max_file_size = null, |
| 481 | | .stat = undefined, |
| 482 | | .bin_digest = undefined, |
| 483 | | }; |
| 484 | | break :blk new; |
| 485 | | }; |
| 486 | | |
| 487 | | var iter = mem.tokenize(u8, line, " "); |
| 488 | | const size = iter.next() orelse return error.InvalidFormat; |
| 489 | | const inode = iter.next() orelse return error.InvalidFormat; |
| 490 | | const mtime_nsec_str = iter.next() orelse return error.InvalidFormat; |
| 491 | | const digest_str = iter.next() orelse return error.InvalidFormat; |
| 492 | | const prefix_str = iter.next() orelse return error.InvalidFormat; |
| 493 | | const file_path = iter.rest(); |
| 494 | | |
| 495 | | cache_hash_file.stat.size = fmt.parseInt(u64, size, 10) catch return error.InvalidFormat; |
| 496 | | cache_hash_file.stat.inode = fmt.parseInt(fs.File.INode, inode, 10) catch return error.InvalidFormat; |
| 497 | | cache_hash_file.stat.mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat; |
| 498 | | _ = fmt.hexToBytes(&cache_hash_file.bin_digest, digest_str) catch return error.InvalidFormat; |
| 499 | | const prefix = fmt.parseInt(u8, prefix_str, 10) catch return error.InvalidFormat; |
| 500 | | if (prefix >= self.cache.prefixes_len) return error.InvalidFormat; |
| 501 | | |
| 502 | | if (file_path.len == 0) { |
| 503 | | return error.InvalidFormat; |
| 504 | | } |
| 505 | | if (cache_hash_file.prefixed_path) |pp| { |
| 506 | | if (pp.prefix != prefix or !mem.eql(u8, file_path, pp.sub_path)) { |
| 507 | | return error.InvalidFormat; |
| 431 | while (true) { |
| 432 | const file_contents = try self.manifest_file.?.reader().readAllAlloc(gpa, manifest_file_size_max); |
| 433 | defer gpa.free(file_contents); |
| 434 | |
| 435 | const input_file_count = self.files.items.len; |
| 436 | var any_file_changed = false; |
| 437 | var line_iter = mem.tokenize(u8, file_contents, "\n"); |
| 438 | var idx: usize = 0; |
| 439 | if (if (line_iter.next()) |line| !std.mem.eql(u8, line, manifest_header) else true) { |
| 440 | if (try self.upgradeToExclusiveLock()) continue; |
| 441 | self.manifest_dirty = true; |
| 442 | while (idx < input_file_count) : (idx += 1) { |
| 443 | const ch_file = &self.files.items[idx]; |
| 444 | self.populateFileHash(ch_file) catch |err| { |
| 445 | self.failed_file_index = idx; |
| 446 | return err; |
| 447 | }; |
| 508 | 448 | } |
| 449 | return false; |
| 509 | 450 | } |
| 510 | | |
| 511 | | if (cache_hash_file.prefixed_path == null) { |
| 512 | | cache_hash_file.prefixed_path = .{ |
| 513 | | .prefix = prefix, |
| 514 | | .sub_path = try gpa.dupe(u8, file_path), |
| 451 | while (line_iter.next()) |line| { |
| 452 | defer idx += 1; |
| 453 | |
| 454 | const cache_hash_file = if (idx < input_file_count) &self.files.items[idx] else blk: { |
| 455 | const new = try self.files.addOne(gpa); |
| 456 | new.* = .{ |
| 457 | .prefixed_path = null, |
| 458 | .contents = null, |
| 459 | .max_file_size = null, |
| 460 | .stat = undefined, |
| 461 | .bin_digest = undefined, |
| 462 | }; |
| 463 | break :blk new; |
| 515 | 464 | }; |
| 516 | | } |
| 517 | | |
| 518 | | const pp = cache_hash_file.prefixed_path.?; |
| 519 | | const dir = self.cache.prefixes()[pp.prefix].handle; |
| 520 | | const this_file = dir.openFile(pp.sub_path, .{ .mode = .read_only }) catch |err| switch (err) { |
| 521 | | error.FileNotFound => { |
| 522 | | try self.upgradeToExclusiveLock(); |
| 523 | | return false; |
| 524 | | }, |
| 525 | | else => return error.CacheUnavailable, |
| 526 | | }; |
| 527 | | defer this_file.close(); |
| 528 | 465 | |
| 529 | | const actual_stat = this_file.stat() catch |err| { |
| 530 | | self.failed_file_index = idx; |
| 531 | | return err; |
| 532 | | }; |
| 533 | | const size_match = actual_stat.size == cache_hash_file.stat.size; |
| 534 | | const mtime_match = actual_stat.mtime == cache_hash_file.stat.mtime; |
| 535 | | const inode_match = actual_stat.inode == cache_hash_file.stat.inode; |
| 466 | var iter = mem.tokenize(u8, line, " "); |
| 467 | const size = iter.next() orelse return error.InvalidFormat; |
| 468 | const inode = iter.next() orelse return error.InvalidFormat; |
| 469 | const mtime_nsec_str = iter.next() orelse return error.InvalidFormat; |
| 470 | const digest_str = iter.next() orelse return error.InvalidFormat; |
| 471 | const prefix_str = iter.next() orelse return error.InvalidFormat; |
| 472 | const file_path = iter.rest(); |
| 473 | |
| 474 | cache_hash_file.stat.size = fmt.parseInt(u64, size, 10) catch return error.InvalidFormat; |
| 475 | cache_hash_file.stat.inode = fmt.parseInt(fs.File.INode, inode, 10) catch return error.InvalidFormat; |
| 476 | cache_hash_file.stat.mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat; |
| 477 | _ = fmt.hexToBytes(&cache_hash_file.bin_digest, digest_str) catch return error.InvalidFormat; |
| 478 | const prefix = fmt.parseInt(u8, prefix_str, 10) catch return error.InvalidFormat; |
| 479 | if (prefix >= self.cache.prefixes_len) return error.InvalidFormat; |
| 480 | |
| 481 | if (file_path.len == 0) { |
| 482 | return error.InvalidFormat; |
| 483 | } |
| 484 | if (cache_hash_file.prefixed_path) |pp| { |
| 485 | if (pp.prefix != prefix or !mem.eql(u8, file_path, pp.sub_path)) { |
| 486 | return error.InvalidFormat; |
| 487 | } |
| 488 | } |
| 536 | 489 | |
| 537 | | if (!size_match or !mtime_match or !inode_match) { |
| 538 | | self.manifest_dirty = true; |
| 490 | if (cache_hash_file.prefixed_path == null) { |
| 491 | cache_hash_file.prefixed_path = .{ |
| 492 | .prefix = prefix, |
| 493 | .sub_path = try gpa.dupe(u8, file_path), |
| 494 | }; |
| 495 | } |
| 539 | 496 | |
| 540 | | cache_hash_file.stat = .{ |
| 541 | | .size = actual_stat.size, |
| 542 | | .mtime = actual_stat.mtime, |
| 543 | | .inode = actual_stat.inode, |
| 497 | const pp = cache_hash_file.prefixed_path.?; |
| 498 | const dir = self.cache.prefixes()[pp.prefix].handle; |
| 499 | const this_file = dir.openFile(pp.sub_path, .{ .mode = .read_only }) catch |err| switch (err) { |
| 500 | error.FileNotFound => { |
| 501 | if (try self.upgradeToExclusiveLock()) continue; |
| 502 | return false; |
| 503 | }, |
| 504 | else => return error.CacheUnavailable, |
| 544 | 505 | }; |
| 506 | defer this_file.close(); |
| 545 | 507 | |
| 546 | | if (self.isProblematicTimestamp(cache_hash_file.stat.mtime)) { |
| 547 | | // The actual file has an unreliable timestamp, force it to be hashed |
| 548 | | cache_hash_file.stat.mtime = 0; |
| 549 | | cache_hash_file.stat.inode = 0; |
| 550 | | } |
| 551 | | |
| 552 | | var actual_digest: BinDigest = undefined; |
| 553 | | hashFile(this_file, &actual_digest) catch |err| { |
| 508 | const actual_stat = this_file.stat() catch |err| { |
| 554 | 509 | self.failed_file_index = idx; |
| 555 | 510 | return err; |
| 556 | 511 | }; |
| 512 | const size_match = actual_stat.size == cache_hash_file.stat.size; |
| 513 | const mtime_match = actual_stat.mtime == cache_hash_file.stat.mtime; |
| 514 | const inode_match = actual_stat.inode == cache_hash_file.stat.inode; |
| 515 | |
| 516 | if (!size_match or !mtime_match or !inode_match) { |
| 517 | self.manifest_dirty = true; |
| 518 | |
| 519 | cache_hash_file.stat = .{ |
| 520 | .size = actual_stat.size, |
| 521 | .mtime = actual_stat.mtime, |
| 522 | .inode = actual_stat.inode, |
| 523 | }; |
| 524 | |
| 525 | if (self.isProblematicTimestamp(cache_hash_file.stat.mtime)) { |
| 526 | // The actual file has an unreliable timestamp, force it to be hashed |
| 527 | cache_hash_file.stat.mtime = 0; |
| 528 | cache_hash_file.stat.inode = 0; |
| 529 | } |
| 530 | |
| 531 | var actual_digest: BinDigest = undefined; |
| 532 | hashFile(this_file, &actual_digest) catch |err| { |
| 533 | self.failed_file_index = idx; |
| 534 | return err; |
| 535 | }; |
| 536 | |
| 537 | if (!mem.eql(u8, &cache_hash_file.bin_digest, &actual_digest)) { |
| 538 | cache_hash_file.bin_digest = actual_digest; |
| 539 | // keep going until we have the input file digests |
| 540 | any_file_changed = true; |
| 541 | } |
| 542 | } |
| 557 | 543 | |
| 558 | | if (!mem.eql(u8, &cache_hash_file.bin_digest, &actual_digest)) { |
| 559 | | cache_hash_file.bin_digest = actual_digest; |
| 560 | | // keep going until we have the input file digests |
| 561 | | any_file_changed = true; |
| 544 | if (!any_file_changed) { |
| 545 | self.hash.hasher.update(&cache_hash_file.bin_digest); |
| 562 | 546 | } |
| 563 | 547 | } |
| 564 | 548 | |
| 565 | | if (!any_file_changed) { |
| 566 | | self.hash.hasher.update(&cache_hash_file.bin_digest); |
| 549 | if (any_file_changed) { |
| 550 | if (try self.upgradeToExclusiveLock()) continue; |
| 551 | // cache miss |
| 552 | // keep the manifest file open |
| 553 | self.unhit(bin_digest, input_file_count); |
| 554 | return false; |
| 567 | 555 | } |
| 568 | | } |
| 569 | 556 | |
| 570 | | if (any_file_changed) { |
| 571 | | // cache miss |
| 572 | | // keep the manifest file open |
| 573 | | self.unhit(bin_digest, input_file_count); |
| 574 | | try self.upgradeToExclusiveLock(); |
| 575 | | return false; |
| 576 | | } |
| 557 | if (idx < input_file_count) { |
| 558 | if (try self.upgradeToExclusiveLock()) continue; |
| 559 | self.manifest_dirty = true; |
| 560 | while (idx < input_file_count) : (idx += 1) { |
| 561 | const ch_file = &self.files.items[idx]; |
| 562 | self.populateFileHash(ch_file) catch |err| { |
| 563 | self.failed_file_index = idx; |
| 564 | return err; |
| 565 | }; |
| 566 | } |
| 567 | return false; |
| 568 | } |
| 577 | 569 | |
| 578 | | if (idx < input_file_count) { |
| 579 | | self.manifest_dirty = true; |
| 580 | | while (idx < input_file_count) : (idx += 1) { |
| 581 | | const ch_file = &self.files.items[idx]; |
| 582 | | self.populateFileHash(ch_file) catch |err| { |
| 583 | | self.failed_file_index = idx; |
| 584 | | return err; |
| 585 | | }; |
| 570 | if (self.want_shared_lock) { |
| 571 | try self.downgradeToSharedLock(); |
| 586 | 572 | } |
| 587 | | try self.upgradeToExclusiveLock(); |
| 588 | | return false; |
| 589 | | } |
| 590 | 573 | |
| 591 | | if (self.want_shared_lock) { |
| 592 | | try self.downgradeToSharedLock(); |
| 574 | return true; |
| 593 | 575 | } |
| 594 | | |
| 595 | | return true; |
| 596 | 576 | } |
| 597 | 577 | |
| 598 | 578 | pub fn unhit(self: *Manifest, bin_digest: BinDigest, input_file_count: usize) void { |
| ... | ... | @@ -854,19 +834,13 @@ pub const Manifest = struct { |
| 854 | 834 | defer contents.deinit(); |
| 855 | 835 | |
| 856 | 836 | const writer = contents.writer(); |
| 857 | | var encoded_digest: [hex_digest_len]u8 = undefined; |
| 858 | | |
| 837 | try writer.writeAll(manifest_header ++ "\n"); |
| 859 | 838 | for (self.files.items) |file| { |
| 860 | | _ = fmt.bufPrint( |
| 861 | | &encoded_digest, |
| 862 | | "{s}", |
| 863 | | .{fmt.fmtSliceHexLower(&file.bin_digest)}, |
| 864 | | ) catch unreachable; |
| 865 | | try writer.print("{d} {d} {d} {s} {d} {s}\n", .{ |
| 839 | try writer.print("{d} {d} {d} {} {d} {s}\n", .{ |
| 866 | 840 | file.stat.size, |
| 867 | 841 | file.stat.inode, |
| 868 | 842 | file.stat.mtime, |
| 869 | | &encoded_digest, |
| 843 | fmt.fmtSliceHexLower(&file.bin_digest), |
| 870 | 844 | file.prefixed_path.?.prefix, |
| 871 | 845 | file.prefixed_path.?.sub_path, |
| 872 | 846 | }); |
| ... | ... | @@ -895,8 +869,8 @@ pub const Manifest = struct { |
| 895 | 869 | self.have_exclusive_lock = false; |
| 896 | 870 | } |
| 897 | 871 | |
| 898 | | fn upgradeToExclusiveLock(self: *Manifest) !void { |
| 899 | | if (self.have_exclusive_lock) return; |
| 872 | fn upgradeToExclusiveLock(self: *Manifest) !bool { |
| 873 | if (self.have_exclusive_lock) return false; |
| 900 | 874 | assert(self.manifest_file != null); |
| 901 | 875 | |
| 902 | 876 | // WASI does not currently support flock, so we bypass it here. |
| ... | ... | @@ -910,6 +884,7 @@ pub const Manifest = struct { |
| 910 | 884 | try manifest_file.lock(.Exclusive); |
| 911 | 885 | } |
| 912 | 886 | self.have_exclusive_lock = true; |
| 887 | return true; |
| 913 | 888 | } |
| 914 | 889 | |
| 915 | 890 | /// Obtain only the data needed to maintain a lock on the manifest file. |