| ... | ... | @@ -0,0 +1,329 @@ |
| 1 | const Blake3 = @import("crypto.zig").Blake3; |
| 2 | const fs = @import("fs.zig"); |
| 3 | const File = fs.File; |
| 4 | const base64 = @import("base64.zig"); |
| 5 | const ArrayList = @import("array_list.zig").ArrayList; |
| 6 | const debug = @import("debug.zig"); |
| 7 | const testing = @import("testing.zig"); |
| 8 | const mem = @import("mem.zig"); |
| 9 | const fmt = @import("fmt.zig"); |
| 10 | const Allocator = mem.Allocator; |
| 11 | const Buffer = @import("buffer.zig").Buffer; |
| 12 | const os = @import("os.zig"); |
| 13 | |
| 14 | const base64_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; |
| 15 | const base64_pad_char = '='; |
| 16 | const encoder = base64.Base64Encoder.init(base64_alphabet, base64_pad_char); |
| 17 | const decoder = base64.Base64Decoder.init(base64_alphabet, base64_pad_char); |
| 18 | const BIN_DIGEST_LEN = 32; |
| 19 | |
| 20 | pub const CacheHashFile = struct { |
| 21 | path: ?[]const u8, |
| 22 | stat: fs.File.Stat, |
| 23 | file_handle: os.fd_t, |
| 24 | bin_digest: [BIN_DIGEST_LEN]u8, |
| 25 | contents: ?[]const u8, |
| 26 | |
| 27 | pub fn deinit(self: *@This(), alloc: *Allocator) void { |
| 28 | if (self.path) |owned_slice| { |
| 29 | alloc.free(owned_slice); |
| 30 | self.path = null; |
| 31 | } |
| 32 | if (self.contents) |owned_slice| { |
| 33 | alloc.free(owned_slice); |
| 34 | self.contents = null; |
| 35 | } |
| 36 | } |
| 37 | }; |
| 38 | |
| 39 | pub const CacheHash = struct { |
| 40 | alloc: *Allocator, |
| 41 | blake3: Blake3, |
| 42 | manifest_dir: []const u8, |
| 43 | manifest_file_path: ?[]const u8, |
| 44 | manifest_file: ?File, |
| 45 | manifest_dirty: bool, |
| 46 | force_check_manifest: bool, |
| 47 | files: ArrayList(CacheHashFile), |
| 48 | b64_digest: ArrayList(u8), |
| 49 | |
| 50 | pub fn init(alloc: *Allocator, manifest_dir_path: []const u8) !@This() { |
| 51 | return CacheHash{ |
| 52 | .alloc = alloc, |
| 53 | .blake3 = Blake3.init(), |
| 54 | .manifest_dir = manifest_dir_path, |
| 55 | .manifest_file_path = null, |
| 56 | .manifest_file = null, |
| 57 | .manifest_dirty = false, |
| 58 | .force_check_manifest = false, |
| 59 | .files = ArrayList(CacheHashFile).init(alloc), |
| 60 | .b64_digest = ArrayList(u8).init(alloc), |
| 61 | }; |
| 62 | } |
| 63 | |
| 64 | pub fn cache_buf(self: *@This(), val: []const u8) !void { |
| 65 | debug.assert(self.manifest_file_path == null); |
| 66 | |
| 67 | var temp_buffer = try self.alloc.alloc(u8, val.len + 1); |
| 68 | defer self.alloc.free(temp_buffer); |
| 69 | |
| 70 | mem.copy(u8, temp_buffer, val); |
| 71 | temp_buffer[val.len] = 0; |
| 72 | |
| 73 | self.blake3.update(temp_buffer); |
| 74 | } |
| 75 | |
| 76 | pub fn cache_file(self: *@This(), file_path: []const u8) !void { |
| 77 | debug.assert(self.manifest_file_path == null); |
| 78 | |
| 79 | var cache_hash_file = try self.files.addOne(); |
| 80 | cache_hash_file.path = try fs.path.resolve(self.alloc, &[_][]const u8{file_path}); |
| 81 | |
| 82 | try self.cache_buf(cache_hash_file.path.?); |
| 83 | } |
| 84 | |
| 85 | pub fn hit(self: *@This(), out_digest: *ArrayList(u8)) !bool { |
| 86 | debug.assert(self.manifest_file_path == null); |
| 87 | |
| 88 | var bin_digest: [BIN_DIGEST_LEN]u8 = undefined; |
| 89 | self.blake3.final(&bin_digest); |
| 90 | |
| 91 | const OUT_DIGEST_LEN = base64.Base64Encoder.calcSize(BIN_DIGEST_LEN); |
| 92 | try self.b64_digest.resize(OUT_DIGEST_LEN); |
| 93 | encoder.encode(self.b64_digest.toSlice(), &bin_digest); |
| 94 | |
| 95 | if (self.files.toSlice().len == 0 and !self.force_check_manifest) { |
| 96 | try out_digest.resize(OUT_DIGEST_LEN); |
| 97 | mem.copy(u8, out_digest.toSlice(), self.b64_digest.toSlice()); |
| 98 | return true; |
| 99 | } |
| 100 | |
| 101 | self.blake3 = Blake3.init(); |
| 102 | self.blake3.update(&bin_digest); |
| 103 | |
| 104 | { |
| 105 | const manifest_file_path_slice = try fs.path.join(self.alloc, &[_][]const u8{ self.manifest_dir, self.b64_digest.toSlice() }); |
| 106 | var path_buf = ArrayList(u8).fromOwnedSlice(self.alloc, manifest_file_path_slice); |
| 107 | defer path_buf.deinit(); |
| 108 | try path_buf.appendSlice(".txt"); |
| 109 | |
| 110 | self.manifest_file_path = path_buf.toOwnedSlice(); |
| 111 | } |
| 112 | |
| 113 | const cwd = fs.cwd(); |
| 114 | |
| 115 | try cwd.makePath(self.manifest_dir); |
| 116 | |
| 117 | // TODO: Open file with a file lock |
| 118 | self.manifest_file = try cwd.createFile(self.manifest_file_path.?, .{ .read = true, .truncate = false }); |
| 119 | |
| 120 | // TODO: Figure out a good max value? |
| 121 | const file_contents = try self.manifest_file.?.inStream().stream.readAllAlloc(self.alloc, 16 * 1024); |
| 122 | defer self.alloc.free(file_contents); |
| 123 | |
| 124 | const input_file_count = self.files.len; |
| 125 | var any_file_changed = false; |
| 126 | var line_iter = mem.tokenize(file_contents, "\n"); |
| 127 | var idx: usize = 0; |
| 128 | while (line_iter.next()) |line| { |
| 129 | defer idx += 1; |
| 130 | |
| 131 | var cache_hash_file: *CacheHashFile = undefined; |
| 132 | if (idx < input_file_count) { |
| 133 | cache_hash_file = self.files.ptrAt(idx); |
| 134 | } else { |
| 135 | cache_hash_file = try self.files.addOne(); |
| 136 | cache_hash_file.path = null; |
| 137 | } |
| 138 | |
| 139 | var iter = mem.tokenize(line, " "); |
| 140 | const file_handle_str = iter.next() orelse return error.InvalidFormat; |
| 141 | const mtime_nsec_str = iter.next() orelse return error.InvalidFormat; |
| 142 | const digest_str = iter.next() orelse return error.InvalidFormat; |
| 143 | const file_path = iter.rest(); |
| 144 | |
| 145 | cache_hash_file.file_handle = fmt.parseInt(os.fd_t, file_handle_str, 10) catch return error.InvalidFormat; |
| 146 | cache_hash_file.stat.mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat; |
| 147 | decoder.decode(&cache_hash_file.bin_digest, digest_str) catch return error.InvalidFormat; |
| 148 | |
| 149 | if (file_path.len == 0) { |
| 150 | return error.InvalidFormat; |
| 151 | } |
| 152 | if (cache_hash_file.path != null and !mem.eql(u8, file_path, cache_hash_file.path.?)) { |
| 153 | return error.InvalidFormat; |
| 154 | } |
| 155 | cache_hash_file.path = try mem.dupe(self.alloc, u8, file_path); |
| 156 | |
| 157 | const this_file = cwd.openFile(cache_hash_file.path.?, .{ .read = true }) catch { |
| 158 | self.manifest_file.?.close(); |
| 159 | self.manifest_file = null; |
| 160 | return error.CacheUnavailable; |
| 161 | }; |
| 162 | defer this_file.close(); |
| 163 | cache_hash_file.stat = try this_file.stat(); |
| 164 | // TODO: check mtime |
| 165 | if (false) {} else { |
| 166 | self.manifest_dirty = true; |
| 167 | |
| 168 | // TODO: check for problematic timestamp |
| 169 | |
| 170 | var actual_digest: [32]u8 = undefined; |
| 171 | try hash_file(self.alloc, &actual_digest, &this_file); |
| 172 | |
| 173 | if (!mem.eql(u8, &cache_hash_file.bin_digest, &actual_digest)) { |
| 174 | mem.copy(u8, &cache_hash_file.bin_digest, &actual_digest); |
| 175 | // keep going until we have the input file digests |
| 176 | any_file_changed = true; |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | if (!any_file_changed) { |
| 181 | self.blake3.update(&cache_hash_file.bin_digest); |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | if (any_file_changed) { |
| 186 | // cache miss |
| 187 | // keep the manifest file open (TODO: with rw lock) |
| 188 | // reset the hash |
| 189 | self.blake3 = Blake3.init(); |
| 190 | self.blake3.update(&bin_digest); |
| 191 | try self.files.resize(input_file_count); |
| 192 | for (self.files.toSlice()) |file| { |
| 193 | self.blake3.update(&file.bin_digest); |
| 194 | } |
| 195 | return false; |
| 196 | } |
| 197 | |
| 198 | if (idx < input_file_count or idx == 0) { |
| 199 | self.manifest_dirty = true; |
| 200 | while (idx < input_file_count) : (idx += 1) { |
| 201 | var cache_hash_file = self.files.ptrAt(idx); |
| 202 | self.populate_file_hash(cache_hash_file) catch |err| { |
| 203 | self.manifest_file.?.close(); |
| 204 | self.manifest_file = null; |
| 205 | return error.CacheUnavailable; |
| 206 | }; |
| 207 | } |
| 208 | return false; |
| 209 | } |
| 210 | |
| 211 | try self.final(out_digest); |
| 212 | return true; |
| 213 | } |
| 214 | |
| 215 | pub fn populate_file_hash(self: *@This(), cache_hash_file: *CacheHashFile) !void { |
| 216 | debug.assert(cache_hash_file.path != null); |
| 217 | |
| 218 | const this_file = try fs.cwd().openFile(cache_hash_file.path.?, .{}); |
| 219 | defer this_file.close(); |
| 220 | |
| 221 | cache_hash_file.stat = try this_file.stat(); |
| 222 | |
| 223 | // TODO: check for problematic timestamp |
| 224 | |
| 225 | try hash_file(self.alloc, &cache_hash_file.bin_digest, &this_file); |
| 226 | self.blake3.update(&cache_hash_file.bin_digest); |
| 227 | } |
| 228 | |
| 229 | pub fn final(self: *@This(), out_digest: *ArrayList(u8)) !void { |
| 230 | debug.assert(self.manifest_file_path != null); |
| 231 | |
| 232 | var bin_digest: [BIN_DIGEST_LEN]u8 = undefined; |
| 233 | self.blake3.final(&bin_digest); |
| 234 | |
| 235 | const OUT_DIGEST_LEN = base64.Base64Encoder.calcSize(BIN_DIGEST_LEN); |
| 236 | try out_digest.resize(OUT_DIGEST_LEN); |
| 237 | encoder.encode(out_digest.toSlice(), &bin_digest); |
| 238 | } |
| 239 | |
| 240 | pub fn write_manifest(self: *@This()) !void { |
| 241 | debug.assert(self.manifest_file_path != null); |
| 242 | |
| 243 | const OUT_DIGEST_LEN = base64.Base64Encoder.calcSize(BIN_DIGEST_LEN); |
| 244 | var encoded_digest = try Buffer.initSize(self.alloc, OUT_DIGEST_LEN); |
| 245 | defer encoded_digest.deinit(); |
| 246 | var contents = try Buffer.init(self.alloc, ""); |
| 247 | defer contents.deinit(); |
| 248 | |
| 249 | for (self.files.toSlice()) |file| { |
| 250 | encoder.encode(encoded_digest.toSlice(), &file.bin_digest); |
| 251 | try contents.print("{} {} {} {}\n", .{ file.file_handle, file.stat.mtime, encoded_digest.toSlice(), file.path }); |
| 252 | } |
| 253 | |
| 254 | try self.manifest_file.?.seekTo(0); |
| 255 | try self.manifest_file.?.writeAll(contents.toSlice()); |
| 256 | } |
| 257 | |
| 258 | pub fn release(self: *@This()) void { |
| 259 | debug.assert(self.manifest_file_path != null); |
| 260 | |
| 261 | if (self.manifest_dirty) { |
| 262 | self.write_manifest() catch |err| { |
| 263 | debug.warn("Unable to write cache file '{}': {}\n", .{ self.manifest_file_path, err }); |
| 264 | }; |
| 265 | } |
| 266 | |
| 267 | self.manifest_file.?.close(); |
| 268 | if (self.manifest_file_path) |owned_slice| { |
| 269 | self.alloc.free(owned_slice); |
| 270 | } |
| 271 | for (self.files.toSlice()) |*file| { |
| 272 | file.deinit(self.alloc); |
| 273 | } |
| 274 | self.files.deinit(); |
| 275 | self.b64_digest.deinit(); |
| 276 | } |
| 277 | }; |
| 278 | |
| 279 | fn hash_file(alloc: *Allocator, bin_digest: []u8, handle: *const File) !void { |
| 280 | var blake3 = Blake3.init(); |
| 281 | var in_stream = handle.inStream().stream; |
| 282 | |
| 283 | const contents = try handle.inStream().stream.readAllAlloc(alloc, 64 * 1024); |
| 284 | defer alloc.free(contents); |
| 285 | |
| 286 | blake3.update(contents); |
| 287 | |
| 288 | blake3.final(bin_digest); |
| 289 | } |
| 290 | |
| 291 | test "see if imported" { |
| 292 | const cwd = fs.cwd(); |
| 293 | |
| 294 | const temp_manifest_dir = "temp_manifest_dir"; |
| 295 | |
| 296 | try cwd.writeFile("test.txt", "Hello, world!\n"); |
| 297 | |
| 298 | var digest1 = try ArrayList(u8).initCapacity(testing.allocator, 32); |
| 299 | defer digest1.deinit(); |
| 300 | var digest2 = try ArrayList(u8).initCapacity(testing.allocator, 32); |
| 301 | defer digest2.deinit(); |
| 302 | |
| 303 | { |
| 304 | var ch = try CacheHash.init(testing.allocator, temp_manifest_dir); |
| 305 | defer ch.release(); |
| 306 | |
| 307 | try ch.cache_buf("1234"); |
| 308 | try ch.cache_file("test.txt"); |
| 309 | |
| 310 | // There should be nothing in the cache |
| 311 | debug.assert((try ch.hit(&digest1)) == false); |
| 312 | |
| 313 | try ch.final(&digest1); |
| 314 | } |
| 315 | { |
| 316 | var ch = try CacheHash.init(testing.allocator, temp_manifest_dir); |
| 317 | defer ch.release(); |
| 318 | |
| 319 | try ch.cache_buf("1234"); |
| 320 | try ch.cache_file("test.txt"); |
| 321 | |
| 322 | // Cache hit! We just "built" the same file |
| 323 | debug.assert((try ch.hit(&digest2)) == true); |
| 324 | } |
| 325 | |
| 326 | debug.assert(mem.eql(u8, digest1.toSlice(), digest2.toSlice())); |
| 327 | |
| 328 | try cwd.deleteTree(temp_manifest_dir); |
| 329 | } |