// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . const std = @import("std"); const lp = @import("lightpanda"); const Cache = @import("Cache.zig"); const log = lp.log; const CacheRequest = Cache.CacheRequest; const CachedMetadata = Cache.CachedMetadata; const CachedResponse = Cache.CachedResponse; const CACHE_VERSION: usize = 1; const LOCK_STRIPES = 16; comptime { std.debug.assert(std.math.isPowerOfTwo(LOCK_STRIPES)); } pub const FsCache = @This(); dir: std.fs.Dir, locks: [LOCK_STRIPES]std.Thread.Mutex = .{std.Thread.Mutex{}} ** LOCK_STRIPES, const CacheMetadataJson = struct { version: usize, metadata: CachedMetadata, }; fn getLockPtr(self: *FsCache, key: *const [HASHED_KEY_LEN]u8) *std.Thread.Mutex { const lock_idx = std.hash.Wyhash.hash(0, key[0..]) & (LOCK_STRIPES - 1); return &self.locks[lock_idx]; } const BODY_LEN_HEADER_LEN = 8; const HASHED_KEY_LEN = 64; const HASHED_PATH_LEN = HASHED_KEY_LEN + 6; const HASHED_TMP_PATH_LEN = HASHED_PATH_LEN + 4; fn hashKey(key: []const u8) [HASHED_KEY_LEN]u8 { var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; std.crypto.hash.sha2.Sha256.hash(key, &digest, .{}); var hex: [HASHED_KEY_LEN]u8 = undefined; _ = std.fmt.bufPrint(&hex, "{s}", .{std.fmt.bytesToHex(&digest, .lower)}) catch unreachable; return hex; } fn cachePath(hashed_key: *const [HASHED_KEY_LEN]u8) [HASHED_PATH_LEN]u8 { var path: [HASHED_PATH_LEN]u8 = undefined; _ = std.fmt.bufPrint(&path, "{s}.cache", .{hashed_key}) catch unreachable; return path; } fn cacheTmpPath(hashed_key: *const [HASHED_KEY_LEN]u8) [HASHED_TMP_PATH_LEN]u8 { var path: [HASHED_TMP_PATH_LEN]u8 = undefined; _ = std.fmt.bufPrint(&path, "{s}.cache.tmp", .{hashed_key}) catch unreachable; return path; } fn writeCacheFile( self: *FsCache, hashed_key: *const [HASHED_KEY_LEN]u8, body_reader: *std.io.Reader, body_len: u64, meta: CachedMetadata, ) !void { const cache_p = cachePath(hashed_key); const cache_tmp_p = cacheTmpPath(hashed_key); const file = self.dir.createFile(&cache_tmp_p, .{ .truncate = true }) catch |e| { log.err(.cache, "create file", .{ .url = meta.url, .file = &cache_tmp_p, .err = e }); return e; }; errdefer self.dir.deleteFile(&cache_tmp_p) catch {}; defer file.close(); var writer_buf: [1024]u8 = undefined; var file_writer = file.writer(&writer_buf); const w = &file_writer.interface; var len_buf: [BODY_LEN_HEADER_LEN]u8 = undefined; std.mem.writeInt(u64, &len_buf, body_len, .little); try w.writeAll(&len_buf); var copy_buf: [4096]u8 = undefined; var remaining = body_len; while (remaining > 0) { const to_read = @min(copy_buf.len, remaining); const n = try body_reader.readSliceShort(copy_buf[0..to_read]); if (n == 0) break; try w.writeAll(copy_buf[0..n]); remaining -= n; } try std.json.Stringify.value( CacheMetadataJson{ .version = CACHE_VERSION, .metadata = meta }, .{ .whitespace = .minified }, w, ); try w.flush(); self.dir.rename(&cache_tmp_p, &cache_p) catch |e| { log.err(.cache, "rename", .{ .url = meta.url, .from = &cache_tmp_p, .to = &cache_p, .err = e }); return e; }; } pub fn init(path: []const u8) !FsCache { const cwd = std.fs.cwd(); cwd.makeDir(path) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; const dir = try cwd.openDir(path, .{ .iterate = true }); return .{ .dir = dir }; } pub fn deinit(self: *FsCache) void { self.dir.close(); } pub fn get(self: *FsCache, arena: std.mem.Allocator, req: CacheRequest) !?CachedResponse { const hashed_key = hashKey(req.url); const cache_p = cachePath(&hashed_key); const lock = self.getLockPtr(&hashed_key); lock.lock(); defer lock.unlock(); const file = self.dir.openFile(&cache_p, .{ .mode = .read_only }) catch |e| { switch (e) { std.fs.File.OpenError.FileNotFound => { log.debug(.cache, "miss", .{ .url = req.url, .hash = &hashed_key, .reason = "missing" }); }, else => return e, } return null; }; var cleanup = false; defer if (cleanup) { file.close(); self.dir.deleteFile(&cache_p) catch |e| { log.err(.cache, "clean fail", .{ .url = req.url, .file = &cache_p, .err = e }); }; }; var file_buf: [1024]u8 = undefined; var len_buf: [BODY_LEN_HEADER_LEN]u8 = undefined; var file_reader = file.reader(&file_buf); const file_reader_iface = &file_reader.interface; file_reader_iface.readSliceAll(&len_buf) catch |e| { log.warn(.cache, "read header", .{ .url = req.url, .err = e }); cleanup = true; return null; }; const body_len = std.mem.readInt(u64, &len_buf, .little); // Now we read metadata. file_reader.seekTo(body_len + BODY_LEN_HEADER_LEN) catch |e| { log.warn(.cache, "seek metadata", .{ .url = req.url, .err = e }); cleanup = true; return null; }; var json_reader = std.json.Reader.init(arena, file_reader_iface); const cache_file: CacheMetadataJson = std.json.parseFromTokenSourceLeaky( CacheMetadataJson, arena, &json_reader, .{ .allocate = .alloc_always }, ) catch |e| { // Warn because malformed metadata can be a deeper symptom. log.warn(.cache, "miss", .{ .url = req.url, .err = e, .reason = "malformed metadata" }); cleanup = true; return null; }; if (cache_file.version != CACHE_VERSION) { log.debug(.cache, "miss", .{ .url = req.url, .reason = "version mismatch", .expected = CACHE_VERSION, .got = cache_file.version, }); cleanup = true; return null; } const metadata = cache_file.metadata; // If we have Vary headers, ensure they are present & matching. for (metadata.vary_headers) |vary_hdr| { const name = vary_hdr.name; const value = vary_hdr.value; const incoming = for (req.request_headers) |h| { if (std.ascii.eqlIgnoreCase(h.name, name)) break h.value; } else ""; if (!std.ascii.eqlIgnoreCase(value, incoming)) { log.debug(.cache, "miss", .{ .url = req.url, .reason = "vary mismatch", .header = name, .expected = value, .got = incoming, }); return null; } } // On the case of a hash collision. if (!std.ascii.eqlIgnoreCase(metadata.url, req.url)) { log.warn(.cache, "collision", .{ .url = req.url, .expected = metadata.url, .got = req.url }); cleanup = true; return null; } // Check entry expiration. const expired = metadata.isStale(req.timestamp); log.debug(.cache, "hit", .{ .url = req.url, .hash = &hashed_key, .expired = expired }); return .{ .metadata = metadata, .data = .{ .file = .{ .file = file, .offset = BODY_LEN_HEADER_LEN, .len = body_len, }, }, .expired = expired, }; } pub fn put(self: *FsCache, meta: CachedMetadata, body: []const u8) !void { const hashed_key = hashKey(meta.url); const lock = self.getLockPtr(&hashed_key); lock.lock(); defer lock.unlock(); var body_reader = std.io.Reader.fixed(body); try self.writeCacheFile(&hashed_key, &body_reader, body.len, meta); log.debug(.cache, "put", .{ .url = meta.url, .hash = &hashed_key, .body_len = body.len }); } pub fn clear(self: *FsCache) !void { for (&self.locks) |*lock| lock.lock(); defer for (&self.locks) |*lock| lock.unlock(); var iter = self.dir.iterate(); while (try iter.next()) |entry| { if (entry.kind != .file) continue; if (!std.mem.endsWith(u8, entry.name, ".cache") and !std.mem.endsWith(u8, entry.name, ".cache.tmp")) continue; self.dir.deleteFile(entry.name) catch |e| { log.err(.cache, "clear delete fail", .{ .file = entry.name, .err = e }); }; } } pub fn evict(self: *FsCache, url: []const u8) !void { const hashed_key = hashKey(url); const cache_p = cachePath(&hashed_key); const lock = self.getLockPtr(&hashed_key); lock.lock(); defer lock.unlock(); self.dir.deleteFile(&cache_p) catch |e| switch (e) { error.FileNotFound => {}, else => return e, }; } pub fn renew(self: *FsCache, arena: std.mem.Allocator, url: []const u8, timestamp: i64) !void { const hashed_key = hashKey(url); const cache_p = cachePath(&hashed_key); const lock = self.getLockPtr(&hashed_key); lock.lock(); defer lock.unlock(); const file = self.dir.openFile(&cache_p, .{ .mode = .read_only }) catch |e| { log.warn(.cache, "renew open failed", .{ .url = url, .err = e }); return e; }; defer file.close(); var file_buf: [1024]u8 = undefined; var file_reader = file.reader(&file_buf); const r = &file_reader.interface; var len_buf: [BODY_LEN_HEADER_LEN]u8 = undefined; r.readSliceAll(&len_buf) catch |e| { log.warn(.cache, "renew read len", .{ .url = url, .err = e }); return e; }; const body_len = std.mem.readInt(u64, &len_buf, .little); try file_reader.seekTo(BODY_LEN_HEADER_LEN + body_len); var json_reader = std.json.Reader.init(arena, r); var parsed = std.json.parseFromTokenSourceLeaky( CacheMetadataJson, arena, &json_reader, .{ .allocate = .alloc_always }, ) catch |e| { log.warn(.cache, "renew parse", .{ .url = url, .err = e }); return e; }; parsed.metadata.stored_at = timestamp; parsed.metadata.age_at_store = 0; try file_reader.seekTo(BODY_LEN_HEADER_LEN); self.writeCacheFile(&hashed_key, r, body_len, parsed.metadata) catch |e| { log.warn(.cache, "renew write", .{ .url = url, .err = e }); return e; }; log.debug(.cache, "renewed", .{ .url = url }); } const testing = std.testing; fn setupCache() !struct { tmp: testing.TmpDir, cache: Cache } { var tmp = testing.tmpDir(.{}); errdefer tmp.cleanup(); const path = try tmp.dir.realpathAlloc(testing.allocator, "."); defer testing.allocator.free(path); return .{ .tmp = tmp, .cache = Cache{ .kind = .{ .fs = try FsCache.init(path) } }, }; } test "FsCache: basic put and get" { var setup = try setupCache(); defer { setup.cache.deinit(); setup.tmp.cleanup(); } const cache = &setup.cache; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const now = std.time.timestamp(); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", .status = 200, .stored_at = now, .age_at_store = 0, .cache_control = .{ .max_age = 600 }, .headers = &.{}, .vary_headers = &.{}, }; const body = "hello world"; try cache.put(meta, body); const result = try cache.get( arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{}, }, ) orelse return error.CacheMiss; const f = result.data.file; const file = f.file; defer file.close(); var buf: [64]u8 = undefined; var file_reader = file.reader(&buf); try file_reader.seekTo(f.offset); const read_buf = try file_reader.interface.readAlloc(testing.allocator, f.len); defer testing.allocator.free(read_buf); try testing.expectEqualStrings(body, read_buf); } test "FsCache: get expiration" { var setup = try setupCache(); defer { setup.cache.deinit(); setup.tmp.cleanup(); } const cache = &setup.cache; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const now = 5000; const max_age = 1000; const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", .status = 200, .stored_at = now, .age_at_store = 900, .cache_control = .{ .max_age = max_age }, .headers = &.{}, .vary_headers = &.{}, }; const body = "hello world"; try cache.put(meta, body); const result = try cache.get( arena.allocator(), .{ .url = "https://example.com", .timestamp = now + 50, .request_headers = &.{}, }, ) orelse return error.CacheMiss; result.data.file.file.close(); // Expired: age = 200 + 900 = 1100 >= 1000 const stale = try cache.get( arena.allocator(), .{ .url = "https://example.com", .timestamp = now + 200, .request_headers = &.{}, }, ) orelse return error.CacheMiss; defer stale.data.file.file.close(); try testing.expectEqual(true, stale.expired); } test "FsCache: put override" { var setup = try setupCache(); defer { setup.cache.deinit(); setup.tmp.cleanup(); } const cache = &setup.cache; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); { const now = 5000; const max_age = 1000; const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", .status = 200, .stored_at = now, .age_at_store = 900, .cache_control = .{ .max_age = max_age }, .headers = &.{}, .vary_headers = &.{}, }; const body = "hello world"; try cache.put(meta, body); const result = try cache.get( arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{}, }, ) orelse return error.CacheMiss; const f = result.data.file; const file = f.file; defer file.close(); var buf: [64]u8 = undefined; var file_reader = file.reader(&buf); try file_reader.seekTo(f.offset); const read_buf = try file_reader.interface.readAlloc(testing.allocator, f.len); defer testing.allocator.free(read_buf); try testing.expectEqualStrings(body, read_buf); } { const now = 10000; const max_age = 2000; const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", .status = 200, .stored_at = now, .age_at_store = 0, .cache_control = .{ .max_age = max_age }, .headers = &.{}, .vary_headers = &.{}, }; const body = "goodbye world"; try cache.put(meta, body); const result = try cache.get( arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{}, }, ) orelse return error.CacheMiss; const f = result.data.file; const file = f.file; defer file.close(); var buf: [64]u8 = undefined; var file_reader = file.reader(&buf); try file_reader.seekTo(f.offset); const read_buf = try file_reader.interface.readAlloc(testing.allocator, f.len); defer testing.allocator.free(read_buf); try testing.expectEqualStrings(body, read_buf); } } test "FsCache: garbage file" { const LogFilter = @import("../../testing.zig").LogFilter; const filter: LogFilter = .init(&.{.cache}); defer filter.deinit(); var setup = try setupCache(); defer { setup.cache.deinit(); setup.tmp.cleanup(); } const cache = &setup.cache; const hashed_key = hashKey("https://example.com"); const cache_p = cachePath(&hashed_key); const file = try cache.kind.fs.dir.createFile(&cache_p, .{}); try file.writeAll("this is not a valid cache file !@#$%"); file.close(); var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); try testing.expectEqual( null, try cache.get(arena.allocator(), .{ .url = "https://example.com", .timestamp = 5000, .request_headers = &.{}, }), ); } test "FsCache: vary hit and miss" { var setup = try setupCache(); defer { setup.cache.deinit(); setup.tmp.cleanup(); } const cache = &setup.cache; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const now = std.time.timestamp(); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", .status = 200, .stored_at = now, .age_at_store = 0, .cache_control = .{ .max_age = 600 }, .headers = &.{}, .vary_headers = &.{ .{ .name = "Accept-Encoding", .value = "gzip" }, }, }; try cache.put(meta, "hello world"); const result = try cache.get(arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{ .{ .name = "Accept-Encoding", .value = "gzip" }, }, }) orelse return error.CacheMiss; result.data.file.file.close(); try testing.expectEqual(null, try cache.get(arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{ .{ .name = "Accept-Encoding", .value = "br" }, }, })); try testing.expectEqual(null, try cache.get(arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{}, })); const result2 = try cache.get(arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{ .{ .name = "Accept-Encoding", .value = "gzip" }, }, }) orelse return error.CacheMiss; result2.data.file.file.close(); } test "FsCache: vary multiple headers" { var setup = try setupCache(); defer { setup.cache.deinit(); setup.tmp.cleanup(); } const cache = &setup.cache; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const now = std.time.timestamp(); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", .status = 200, .stored_at = now, .age_at_store = 0, .cache_control = .{ .max_age = 600 }, .headers = &.{}, .vary_headers = &.{ .{ .name = "Accept-Encoding", .value = "gzip" }, .{ .name = "Accept-Language", .value = "en" }, }, }; try cache.put(meta, "hello world"); const result = try cache.get(arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{ .{ .name = "Accept-Encoding", .value = "gzip" }, .{ .name = "Accept-Language", .value = "en" }, }, }) orelse return error.CacheMiss; result.data.file.file.close(); try testing.expectEqual(null, try cache.get(arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{ .{ .name = "Accept-Encoding", .value = "gzip" }, .{ .name = "Accept-Language", .value = "fr" }, }, })); } test "FsCache: clear removes all entries" { var setup = try setupCache(); defer { setup.cache.deinit(); setup.tmp.cleanup(); } const cache = &setup.cache; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const now = std.time.timestamp(); const base_meta_a = CachedMetadata{ .url = "https://example.com/a", .status = 200, .stored_at = now, .age_at_store = 0, .cache_control = .{ .max_age = 600 }, .headers = &.{}, .vary_headers = &.{}, .content_type = "text/html", }; const base_meta_b = CachedMetadata{ .url = "https://example.com/b", .status = 200, .stored_at = now, .age_at_store = 0, .cache_control = .{ .max_age = 600 }, .headers = &.{}, .vary_headers = &.{}, .content_type = "text/html", }; try cache.put(base_meta_a, "body a"); try cache.put(base_meta_b, "body b"); // Sanity check: both are cached const r1 = try cache.get( arena.allocator(), .{ .url = "https://example.com/a", .timestamp = now, .request_headers = &.{}, }, ); try testing.expect(r1 != null); r1.?.data.file.file.close(); const r2 = try cache.get( arena.allocator(), .{ .url = "https://example.com/b", .timestamp = now, .request_headers = &.{}, }, ); try testing.expect(r2 != null); r2.?.data.file.file.close(); try cache.clear(); try testing.expectEqual(null, try cache.get( arena.allocator(), .{ .url = "https://example.com/a", .timestamp = now, .request_headers = &.{}, }, )); try testing.expectEqual(null, try cache.get( arena.allocator(), .{ .url = "https://example.com/b", .timestamp = now, .request_headers = &.{}, }, )); } test "FsCache: put after clear works" { var setup = try setupCache(); defer { setup.cache.deinit(); setup.tmp.cleanup(); } const cache = &setup.cache; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const now = std.time.timestamp(); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", .status = 200, .stored_at = now, .age_at_store = 0, .cache_control = .{ .max_age = 600 }, .headers = &.{}, .vary_headers = &.{}, }; try cache.put(meta, "before clear"); try cache.clear(); // Should be a miss after clear try testing.expectEqual( null, try cache.get( arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{}, }, ), ); // Put again after clear — should work normally try cache.put(meta, "after clear"); const result = try cache.get( arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{}, }, ) orelse return error.CacheMiss; const f = result.data.file; defer f.file.close(); var buf: [64]u8 = undefined; var file_reader = f.file.reader(&buf); try file_reader.seekTo(f.offset); const read_buf = try file_reader.interface.readAlloc(testing.allocator, f.len); defer testing.allocator.free(read_buf); try testing.expectEqualStrings("after clear", read_buf); } test "FsCache: evict removes entry" { var setup = try setupCache(); defer { setup.cache.deinit(); setup.tmp.cleanup(); } const cache = &setup.cache; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const now = std.time.timestamp(); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", .status = 200, .stored_at = now, .age_at_store = 0, .cache_control = .{ .max_age = 600 }, .headers = &.{}, .vary_headers = &.{}, }; try cache.put(meta, "hello world"); const result = try cache.get( arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{}, }, ) orelse return error.CacheMiss; result.data.file.file.close(); try cache.evict("https://example.com"); try testing.expectEqual(null, try cache.get( arena.allocator(), .{ .url = "https://example.com", .timestamp = now, .request_headers = &.{}, }, )); } test "FsCache: renew refreshes expiry" { var setup = try setupCache(); defer { setup.cache.deinit(); setup.tmp.cleanup(); } const cache = &setup.cache; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const now: i64 = 5000; const max_age: u64 = 1000; const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", .status = 200, .stored_at = now, .age_at_store = 0, .cache_control = .{ .max_age = max_age }, .headers = &.{}, .vary_headers = &.{}, }; try cache.put(meta, "hello world"); // renew while still fresh at now+500 try cache.renew(arena.allocator(), "https://example.com", now + 500); // Without revalidation would expire at now+1000, but clock reset to now+500 // so still fresh at now+1200 const r1 = try cache.get( arena.allocator(), .{ .url = "https://example.com", .timestamp = now + 1200, .request_headers = &.{}, }, ) orelse return error.CacheMiss; r1.data.file.file.close(); // Expires at now+500+1000 = now+1500 const stale1 = try cache.get( arena.allocator(), .{ .url = "https://example.com", .timestamp = now + 1500, .request_headers = &.{}, }, ) orelse return error.CacheMiss; stale1.data.file.file.close(); try testing.expectEqual(true, stale1.expired); } test "FsCache: renew preserves body" { var setup = try setupCache(); defer { setup.cache.deinit(); setup.tmp.cleanup(); } const cache = &setup.cache; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const now = std.time.timestamp(); const meta = CachedMetadata{ .url = "https://example.com", .content_type = "text/html", .status = 200, .stored_at = now, .age_at_store = 0, .cache_control = .{ .max_age = 600 }, .headers = &.{}, .vary_headers = &.{}, }; const body = "original body"; try cache.put(meta, body); try cache.renew(arena.allocator(), "https://example.com", now + 100); const result = try cache.get(arena.allocator(), .{ .url = "https://example.com", .timestamp = now + 100, .request_headers = &.{}, }) orelse return error.CacheMiss; const f = result.data.file; defer f.file.close(); var buf: [64]u8 = undefined; var file_reader = f.file.reader(&buf); try file_reader.seekTo(f.offset); const read_buf = try file_reader.interface.readAlloc(testing.allocator, f.len); defer testing.allocator.free(read_buf); try testing.expectEqualStrings(body, read_buf); }