| ... | ... | @@ -337,6 +337,7 @@ pub const Manifest = struct { |
| 337 | 337 | manifest_create: fs.File.OpenError, |
| 338 | 338 | manifest_read: fs.File.ReadError, |
| 339 | 339 | manifest_lock: fs.File.LockError, |
| 340 | manifest_seek: fs.File.SeekError, |
| 340 | 341 | file_open: FileOp, |
| 341 | 342 | file_stat: FileOp, |
| 342 | 343 | file_read: FileOp, |
| ... | ... | @@ -488,7 +489,6 @@ pub const Manifest = struct { |
| 488 | 489 | /// option, one may call `toOwnedLock` to obtain a smaller object which can represent |
| 489 | 490 | /// the lock. `deinit` is safe to call whether or not `toOwnedLock` has been called. |
| 490 | 491 | pub fn hit(self: *Manifest) HitError!bool { |
| 491 | | const gpa = self.cache.gpa; |
| 492 | 492 | assert(self.manifest_file == null); |
| 493 | 493 | |
| 494 | 494 | self.diagnostic = .none; |
| ... | ... | @@ -501,12 +501,12 @@ pub const Manifest = struct { |
| 501 | 501 | |
| 502 | 502 | self.hex_digest = binToHex(bin_digest); |
| 503 | 503 | |
| 504 | | self.hash.hasher = hasher_init; |
| 505 | | self.hash.hasher.update(&bin_digest); |
| 506 | | |
| 507 | 504 | @memcpy(manifest_file_path[0..self.hex_digest.len], &self.hex_digest); |
| 508 | 505 | manifest_file_path[hex_digest_len..][0..ext.len].* = ext.*; |
| 509 | 506 | |
| 507 | // We'll try to open the cache with an exclusive lock, but if that would block |
| 508 | // and `want_shared_lock` is set, a shared lock might be sufficient, so we'll |
| 509 | // open with a shared lock instead. |
| 510 | 510 | while (true) { |
| 511 | 511 | if (self.cache.manifest_dir.createFile(&manifest_file_path, .{ |
| 512 | 512 | .read = true, |
| ... | ... | @@ -575,26 +575,71 @@ pub const Manifest = struct { |
| 575 | 575 | self.want_refresh_timestamp = true; |
| 576 | 576 | |
| 577 | 577 | const input_file_count = self.files.entries.len; |
| 578 | | while (true) : (self.unhit(bin_digest, input_file_count)) { |
| 579 | | const file_contents = self.manifest_file.?.reader().readAllAlloc(gpa, manifest_file_size_max) catch |err| switch (err) { |
| 580 | | error.OutOfMemory => return error.OutOfMemory, |
| 581 | | error.StreamTooLong => return error.OutOfMemory, |
| 582 | | else => |e| { |
| 583 | | self.diagnostic = .{ .manifest_read = e }; |
| 578 | |
| 579 | // We're going to construct a second hash. Its input will begin with the digest we've |
| 580 | // already computed (`bin_digest`), and then it'll have the digests of each input file, |
| 581 | // including "post" files (see `addFilePost`). If this is a hit, we learn the set of "post" |
| 582 | // files from the manifest on disk. If this is a miss, we'll learn those from future calls |
| 583 | // to `addFilePost` etc. As such, the state of `self.hash.hasher` after this function |
| 584 | // depends on whether this is a hit or a miss. |
| 585 | // |
| 586 | // If we return `true` indicating a cache hit, then `self.hash.hasher` must already include |
| 587 | // the digests of the "post" files, so the caller can call `final`. Otherwise, on a cache |
| 588 | // miss, `self.hash.hasher` will include the digests of all non-"post" files -- that is, |
| 589 | // the ones we've already been told about. The rest will be discovered through calls to |
| 590 | // `addFilePost` etc, which will update the hasher. After all files are added, the user can |
| 591 | // use `final`, and will at some point `writeManifest` the file list to disk. |
| 592 | |
| 593 | self.hash.hasher = hasher_init; |
| 594 | self.hash.hasher.update(&bin_digest); |
| 595 | |
| 596 | hit: { |
| 597 | const file_digests_populated: usize = digests: { |
| 598 | switch (try self.hitWithCurrentLock()) { |
| 599 | .hit => break :hit, |
| 600 | .miss => |m| if (!try self.upgradeToExclusiveLock()) { |
| 601 | break :digests m.file_digests_populated; |
| 602 | }, |
| 603 | } |
| 604 | // We've just had a miss with the shared lock, and upgraded to an exclusive lock. Someone |
| 605 | // else might have modified the digest, so we need to check again before deciding to miss. |
| 606 | // Before trying again, we must reset `self.hash.hasher` and `self.files`. |
| 607 | // This is basically just the first half of `unhit`. |
| 608 | self.hash.hasher = hasher_init; |
| 609 | self.hash.hasher.update(&bin_digest); |
| 610 | while (self.files.count() != input_file_count) { |
| 611 | var file = self.files.pop().?; |
| 612 | file.key.deinit(self.cache.gpa); |
| 613 | } |
| 614 | // Also, seek the file back to the start. |
| 615 | self.manifest_file.?.seekTo(0) catch |err| { |
| 616 | self.diagnostic = .{ .manifest_seek = err }; |
| 584 | 617 | return error.CacheCheckFailed; |
| 585 | | }, |
| 618 | }; |
| 619 | |
| 620 | switch (try self.hitWithCurrentLock()) { |
| 621 | .hit => break :hit, |
| 622 | .miss => |m| break :digests m.file_digests_populated, |
| 623 | } |
| 586 | 624 | }; |
| 587 | | defer gpa.free(file_contents); |
| 588 | | |
| 589 | | var any_file_changed = false; |
| 590 | | var line_iter = mem.tokenizeScalar(u8, file_contents, '\n'); |
| 591 | | var idx: usize = 0; |
| 592 | | if (if (line_iter.next()) |line| !std.mem.eql(u8, line, manifest_header) else true) { |
| 593 | | if (try self.upgradeToExclusiveLock()) continue; |
| 594 | | self.manifest_dirty = true; |
| 595 | | while (idx < input_file_count) : (idx += 1) { |
| 596 | | const ch_file = &self.files.keys()[idx]; |
| 597 | | self.populateFileHash(ch_file) catch |err| { |
| 625 | |
| 626 | // This is a guaranteed cache miss. We're almost ready to return `false`, but there's a |
| 627 | // little bookkeeping to do first. The first `file_digests_populated` entries in `files` |
| 628 | // have their `bin_digest` populated; there may be some left in `input_file_count` which |
| 629 | // we'll need to populate ourselves. Other than that, this is basically `unhit`. |
| 630 | self.manifest_dirty = true; |
| 631 | self.hash.hasher = hasher_init; |
| 632 | self.hash.hasher.update(&bin_digest); |
| 633 | while (self.files.count() != input_file_count) { |
| 634 | var file = self.files.pop().?; |
| 635 | file.key.deinit(self.cache.gpa); |
| 636 | } |
| 637 | for (self.files.keys(), 0..) |*file, idx| { |
| 638 | if (idx < file_digests_populated) { |
| 639 | // `bin_digest` is already populated by `hitWithCurrentLock`, so we can use it directly. |
| 640 | self.hash.hasher.update(&file.bin_digest); |
| 641 | } else { |
| 642 | self.populateFileHash(file) catch |err| { |
| 598 | 643 | self.diagnostic = .{ .file_hash = .{ |
| 599 | 644 | .file_index = idx, |
| 600 | 645 | .err = err, |
| ... | ... | @@ -602,172 +647,195 @@ pub const Manifest = struct { |
| 602 | 647 | return error.CacheCheckFailed; |
| 603 | 648 | }; |
| 604 | 649 | } |
| 605 | | return false; |
| 606 | 650 | } |
| 607 | | while (line_iter.next()) |line| { |
| 608 | | defer idx += 1; |
| 609 | | |
| 610 | | var iter = mem.tokenizeScalar(u8, line, ' '); |
| 611 | | const size = iter.next() orelse return error.InvalidFormat; |
| 612 | | const inode = iter.next() orelse return error.InvalidFormat; |
| 613 | | const mtime_nsec_str = iter.next() orelse return error.InvalidFormat; |
| 614 | | const digest_str = iter.next() orelse return error.InvalidFormat; |
| 615 | | const prefix_str = iter.next() orelse return error.InvalidFormat; |
| 616 | | const file_path = iter.rest(); |
| 617 | | |
| 618 | | const stat_size = fmt.parseInt(u64, size, 10) catch return error.InvalidFormat; |
| 619 | | const stat_inode = fmt.parseInt(fs.File.INode, inode, 10) catch return error.InvalidFormat; |
| 620 | | const stat_mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat; |
| 621 | | const file_bin_digest = b: { |
| 622 | | if (digest_str.len != hex_digest_len) return error.InvalidFormat; |
| 623 | | var bd: BinDigest = undefined; |
| 624 | | _ = fmt.hexToBytes(&bd, digest_str) catch return error.InvalidFormat; |
| 625 | | break :b bd; |
| 626 | | }; |
| 651 | return false; |
| 652 | } |
| 653 | |
| 654 | if (self.want_shared_lock) { |
| 655 | self.downgradeToSharedLock() catch |err| { |
| 656 | self.diagnostic = .{ .manifest_lock = err }; |
| 657 | return error.CacheCheckFailed; |
| 658 | }; |
| 659 | } |
| 627 | 660 | |
| 628 | | const prefix = fmt.parseInt(u8, prefix_str, 10) catch return error.InvalidFormat; |
| 629 | | if (prefix >= self.cache.prefixes_len) return error.InvalidFormat; |
| 661 | return true; |
| 662 | } |
| 630 | 663 | |
| 631 | | if (file_path.len == 0) return error.InvalidFormat; |
| 664 | /// Assumes that `self.hash.hasher` has been updated only with the original digest, that |
| 665 | /// `self.files` contains only the original input files, and that `self.manifest_file.?` is |
| 666 | /// seeked to the start of the file. |
| 667 | fn hitWithCurrentLock(self: *Manifest) HitError!union(enum) { |
| 668 | hit, |
| 669 | miss: struct { |
| 670 | file_digests_populated: usize, |
| 671 | }, |
| 672 | } { |
| 673 | const gpa = self.cache.gpa; |
| 674 | const input_file_count = self.files.entries.len; |
| 632 | 675 | |
| 633 | | const cache_hash_file = f: { |
| 634 | | const prefixed_path: PrefixedPath = .{ |
| 635 | | .prefix = prefix, |
| 636 | | .sub_path = file_path, // expires with file_contents |
| 637 | | }; |
| 638 | | if (idx < input_file_count) { |
| 639 | | const file = &self.files.keys()[idx]; |
| 640 | | if (!file.prefixed_path.eql(prefixed_path)) |
| 641 | | return error.InvalidFormat; |
| 676 | const file_contents = self.manifest_file.?.reader().readAllAlloc(gpa, manifest_file_size_max) catch |err| switch (err) { |
| 677 | error.OutOfMemory => return error.OutOfMemory, |
| 678 | error.StreamTooLong => return error.OutOfMemory, |
| 679 | else => |e| { |
| 680 | self.diagnostic = .{ .manifest_read = e }; |
| 681 | return error.CacheCheckFailed; |
| 682 | }, |
| 683 | }; |
| 684 | defer gpa.free(file_contents); |
| 685 | |
| 686 | var any_file_changed = false; |
| 687 | var line_iter = mem.tokenizeScalar(u8, file_contents, '\n'); |
| 688 | var idx: usize = 0; |
| 689 | const header_valid = valid: { |
| 690 | const line = line_iter.next() orelse break :valid false; |
| 691 | break :valid std.mem.eql(u8, line, manifest_header); |
| 692 | }; |
| 693 | if (!header_valid) { |
| 694 | return .{ .miss = .{ .file_digests_populated = 0 } }; |
| 695 | } |
| 696 | while (line_iter.next()) |line| { |
| 697 | defer idx += 1; |
| 698 | |
| 699 | var iter = mem.tokenizeScalar(u8, line, ' '); |
| 700 | const size = iter.next() orelse return error.InvalidFormat; |
| 701 | const inode = iter.next() orelse return error.InvalidFormat; |
| 702 | const mtime_nsec_str = iter.next() orelse return error.InvalidFormat; |
| 703 | const digest_str = iter.next() orelse return error.InvalidFormat; |
| 704 | const prefix_str = iter.next() orelse return error.InvalidFormat; |
| 705 | const file_path = iter.rest(); |
| 706 | |
| 707 | const stat_size = fmt.parseInt(u64, size, 10) catch return error.InvalidFormat; |
| 708 | const stat_inode = fmt.parseInt(fs.File.INode, inode, 10) catch return error.InvalidFormat; |
| 709 | const stat_mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat; |
| 710 | const file_bin_digest = b: { |
| 711 | if (digest_str.len != hex_digest_len) return error.InvalidFormat; |
| 712 | var bd: BinDigest = undefined; |
| 713 | _ = fmt.hexToBytes(&bd, digest_str) catch return error.InvalidFormat; |
| 714 | break :b bd; |
| 715 | }; |
| 716 | |
| 717 | const prefix = fmt.parseInt(u8, prefix_str, 10) catch return error.InvalidFormat; |
| 718 | if (prefix >= self.cache.prefixes_len) return error.InvalidFormat; |
| 719 | |
| 720 | if (file_path.len == 0) return error.InvalidFormat; |
| 642 | 721 | |
| 643 | | file.stat = .{ |
| 722 | const cache_hash_file = f: { |
| 723 | const prefixed_path: PrefixedPath = .{ |
| 724 | .prefix = prefix, |
| 725 | .sub_path = file_path, // expires with file_contents |
| 726 | }; |
| 727 | if (idx < input_file_count) { |
| 728 | const file = &self.files.keys()[idx]; |
| 729 | if (!file.prefixed_path.eql(prefixed_path)) |
| 730 | return error.InvalidFormat; |
| 731 | |
| 732 | file.stat = .{ |
| 733 | .size = stat_size, |
| 734 | .inode = stat_inode, |
| 735 | .mtime = stat_mtime, |
| 736 | }; |
| 737 | file.bin_digest = file_bin_digest; |
| 738 | break :f file; |
| 739 | } |
| 740 | const gop = try self.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{}); |
| 741 | errdefer _ = self.files.pop(); |
| 742 | if (!gop.found_existing) { |
| 743 | gop.key_ptr.* = .{ |
| 744 | .prefixed_path = .{ |
| 745 | .prefix = prefix, |
| 746 | .sub_path = try gpa.dupe(u8, file_path), |
| 747 | }, |
| 748 | .contents = null, |
| 749 | .max_file_size = null, |
| 750 | .handle = null, |
| 751 | .stat = .{ |
| 644 | 752 | .size = stat_size, |
| 645 | 753 | .inode = stat_inode, |
| 646 | 754 | .mtime = stat_mtime, |
| 647 | | }; |
| 648 | | file.bin_digest = file_bin_digest; |
| 649 | | break :f file; |
| 650 | | } |
| 651 | | const gop = try self.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{}); |
| 652 | | errdefer _ = self.files.pop(); |
| 653 | | if (!gop.found_existing) { |
| 654 | | gop.key_ptr.* = .{ |
| 655 | | .prefixed_path = .{ |
| 656 | | .prefix = prefix, |
| 657 | | .sub_path = try gpa.dupe(u8, file_path), |
| 658 | | }, |
| 659 | | .contents = null, |
| 660 | | .max_file_size = null, |
| 661 | | .handle = null, |
| 662 | | .stat = .{ |
| 663 | | .size = stat_size, |
| 664 | | .inode = stat_inode, |
| 665 | | .mtime = stat_mtime, |
| 666 | | }, |
| 667 | | .bin_digest = file_bin_digest, |
| 668 | | }; |
| 669 | | } |
| 670 | | break :f gop.key_ptr; |
| 671 | | }; |
| 672 | | |
| 673 | | const pp = cache_hash_file.prefixed_path; |
| 674 | | const dir = self.cache.prefixes()[pp.prefix].handle; |
| 675 | | const this_file = dir.openFile(pp.sub_path, .{ .mode = .read_only }) catch |err| switch (err) { |
| 676 | | error.FileNotFound => { |
| 677 | | if (try self.upgradeToExclusiveLock()) continue; |
| 678 | | return false; |
| 679 | | }, |
| 680 | | else => |e| { |
| 681 | | self.diagnostic = .{ .file_open = .{ |
| 682 | | .file_index = idx, |
| 683 | | .err = e, |
| 684 | | } }; |
| 685 | | return error.CacheCheckFailed; |
| 686 | | }, |
| 687 | | }; |
| 688 | | defer this_file.close(); |
| 755 | }, |
| 756 | .bin_digest = file_bin_digest, |
| 757 | }; |
| 758 | } |
| 759 | break :f gop.key_ptr; |
| 760 | }; |
| 689 | 761 | |
| 690 | | const actual_stat = this_file.stat() catch |err| { |
| 691 | | self.diagnostic = .{ .file_stat = .{ |
| 762 | const pp = cache_hash_file.prefixed_path; |
| 763 | const dir = self.cache.prefixes()[pp.prefix].handle; |
| 764 | const this_file = dir.openFile(pp.sub_path, .{ .mode = .read_only }) catch |err| switch (err) { |
| 765 | error.FileNotFound => { |
| 766 | // Every digest before this one has been populated successfully. |
| 767 | return .{ .miss = .{ .file_digests_populated = idx } }; |
| 768 | }, |
| 769 | else => |e| { |
| 770 | self.diagnostic = .{ .file_open = .{ |
| 692 | 771 | .file_index = idx, |
| 693 | | .err = err, |
| 772 | .err = e, |
| 694 | 773 | } }; |
| 695 | 774 | return error.CacheCheckFailed; |
| 696 | | }; |
| 697 | | const size_match = actual_stat.size == cache_hash_file.stat.size; |
| 698 | | const mtime_match = actual_stat.mtime == cache_hash_file.stat.mtime; |
| 699 | | const inode_match = actual_stat.inode == cache_hash_file.stat.inode; |
| 700 | | |
| 701 | | if (!size_match or !mtime_match or !inode_match) { |
| 702 | | self.manifest_dirty = true; |
| 703 | | |
| 704 | | cache_hash_file.stat = .{ |
| 705 | | .size = actual_stat.size, |
| 706 | | .mtime = actual_stat.mtime, |
| 707 | | .inode = actual_stat.inode, |
| 708 | | }; |
| 709 | | |
| 710 | | if (self.isProblematicTimestamp(cache_hash_file.stat.mtime)) { |
| 711 | | // The actual file has an unreliable timestamp, force it to be hashed |
| 712 | | cache_hash_file.stat.mtime = 0; |
| 713 | | cache_hash_file.stat.inode = 0; |
| 714 | | } |
| 715 | | |
| 716 | | var actual_digest: BinDigest = undefined; |
| 717 | | hashFile(this_file, &actual_digest) catch |err| { |
| 718 | | self.diagnostic = .{ .file_read = .{ |
| 719 | | .file_index = idx, |
| 720 | | .err = err, |
| 721 | | } }; |
| 722 | | return error.CacheCheckFailed; |
| 723 | | }; |
| 775 | }, |
| 776 | }; |
| 777 | defer this_file.close(); |
| 724 | 778 | |
| 725 | | if (!mem.eql(u8, &cache_hash_file.bin_digest, &actual_digest)) { |
| 726 | | cache_hash_file.bin_digest = actual_digest; |
| 727 | | // keep going until we have the input file digests |
| 728 | | any_file_changed = true; |
| 729 | | } |
| 730 | | } |
| 779 | const actual_stat = this_file.stat() catch |err| { |
| 780 | self.diagnostic = .{ .file_stat = .{ |
| 781 | .file_index = idx, |
| 782 | .err = err, |
| 783 | } }; |
| 784 | return error.CacheCheckFailed; |
| 785 | }; |
| 786 | const size_match = actual_stat.size == cache_hash_file.stat.size; |
| 787 | const mtime_match = actual_stat.mtime == cache_hash_file.stat.mtime; |
| 788 | const inode_match = actual_stat.inode == cache_hash_file.stat.inode; |
| 789 | |
| 790 | if (!size_match or !mtime_match or !inode_match) { |
| 791 | cache_hash_file.stat = .{ |
| 792 | .size = actual_stat.size, |
| 793 | .mtime = actual_stat.mtime, |
| 794 | .inode = actual_stat.inode, |
| 795 | }; |
| 731 | 796 | |
| 732 | | if (!any_file_changed) { |
| 733 | | self.hash.hasher.update(&cache_hash_file.bin_digest); |
| 797 | if (self.isProblematicTimestamp(cache_hash_file.stat.mtime)) { |
| 798 | // The actual file has an unreliable timestamp, force it to be hashed |
| 799 | cache_hash_file.stat.mtime = 0; |
| 800 | cache_hash_file.stat.inode = 0; |
| 734 | 801 | } |
| 735 | | } |
| 736 | 802 | |
| 737 | | if (any_file_changed) { |
| 738 | | if (try self.upgradeToExclusiveLock()) continue; |
| 739 | | // cache miss |
| 740 | | // keep the manifest file open |
| 741 | | self.unhit(bin_digest, input_file_count); |
| 742 | | return false; |
| 743 | | } |
| 803 | var actual_digest: BinDigest = undefined; |
| 804 | hashFile(this_file, &actual_digest) catch |err| { |
| 805 | self.diagnostic = .{ .file_read = .{ |
| 806 | .file_index = idx, |
| 807 | .err = err, |
| 808 | } }; |
| 809 | return error.CacheCheckFailed; |
| 810 | }; |
| 744 | 811 | |
| 745 | | if (idx < input_file_count) { |
| 746 | | if (try self.upgradeToExclusiveLock()) continue; |
| 747 | | self.manifest_dirty = true; |
| 748 | | while (idx < input_file_count) : (idx += 1) { |
| 749 | | self.populateFileHash(&self.files.keys()[idx]) catch |err| { |
| 750 | | self.diagnostic = .{ .file_hash = .{ |
| 751 | | .file_index = idx, |
| 752 | | .err = err, |
| 753 | | } }; |
| 754 | | return error.CacheCheckFailed; |
| 755 | | }; |
| 812 | if (!mem.eql(u8, &cache_hash_file.bin_digest, &actual_digest)) { |
| 813 | cache_hash_file.bin_digest = actual_digest; |
| 814 | // keep going until we have the input file digests |
| 815 | any_file_changed = true; |
| 756 | 816 | } |
| 757 | | return false; |
| 758 | 817 | } |
| 759 | 818 | |
| 760 | | if (self.want_shared_lock) { |
| 761 | | self.downgradeToSharedLock() catch |err| { |
| 762 | | self.diagnostic = .{ .manifest_lock = err }; |
| 763 | | return error.CacheCheckFailed; |
| 764 | | }; |
| 819 | if (!any_file_changed) { |
| 820 | self.hash.hasher.update(&cache_hash_file.bin_digest); |
| 765 | 821 | } |
| 822 | } |
| 766 | 823 | |
| 767 | | return true; |
| 824 | // If the manifest was somehow missing one of our input files, or if any file hash has changed, |
| 825 | // then this is a cache miss. However, we have successfully populated some or all of the file |
| 826 | // digests. |
| 827 | if (any_file_changed or idx < input_file_count) { |
| 828 | return .{ .miss = .{ .file_digests_populated = idx } }; |
| 768 | 829 | } |
| 830 | |
| 831 | return .hit; |
| 769 | 832 | } |
| 770 | 833 | |
| 834 | /// Reset `self.hash.hasher` to the state it should be in after `hit` returns `false`. |
| 835 | /// The hasher contains the original input digest, and all original input file digests (i.e. |
| 836 | /// not including post files). |
| 837 | /// Assumes that `bin_digest` is populated for all files up to `input_file_count`. As such, |
| 838 | /// this is not necessarily safe to call within `hit`. |
| 771 | 839 | pub fn unhit(self: *Manifest, bin_digest: BinDigest, input_file_count: usize) void { |
| 772 | 840 | // Reset the hash. |
| 773 | 841 | self.hash.hasher = hasher_init; |