From 4ccf150ed0d65b3fcbd817a44d0db9c817af489b Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 26 Mar 2026 15:36:53 -0700 Subject: [PATCH] use json for fs cache metadata file --- src/network/cache/FsCache.zig | 314 ++++++++++------------------------ 1 file changed, 93 insertions(+), 221 deletions(-) diff --git a/src/network/cache/FsCache.zig b/src/network/cache/FsCache.zig index df8a558f..6f708201 100644 --- a/src/network/cache/FsCache.zig +++ b/src/network/cache/FsCache.zig @@ -22,10 +22,53 @@ const Http = @import("../http.zig"); const CachedMetadata = Cache.CachedMetadata; const CachedResponse = Cache.CachedResponse; +const CACHE_VERSION: usize = 1; + pub const FsCache = @This(); dir: std.fs.Dir, +const CacheMetadataFile = struct { + version: usize, + metadata: CachedMetadata, +}; + +const HASHED_KEY_LEN = 64; +const HASHED_PATH_LEN = HASHED_KEY_LEN + 5; +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 metaPath(hashed_key: *const [HASHED_KEY_LEN]u8) [HASHED_PATH_LEN]u8 { + var path: [HASHED_PATH_LEN]u8 = undefined; + _ = std.fmt.bufPrint(&path, "{s}.meta", .{hashed_key}) catch unreachable; + return path; +} + +fn bodyPath(hashed_key: *const [HASHED_KEY_LEN]u8) [HASHED_PATH_LEN]u8 { + var path: [HASHED_PATH_LEN]u8 = undefined; + _ = std.fmt.bufPrint(&path, "{s}.body", .{hashed_key}) catch unreachable; + return path; +} + +fn metaTmpPath(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}.meta.tmp", .{hashed_key}) catch unreachable; + return path; +} + +fn bodyTmpPath(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}.body.tmp", .{hashed_key}) catch unreachable; + return path; +} + pub fn init(path: []const u8) !FsCache { const cwd = std.fs.cwd(); @@ -46,257 +89,86 @@ pub fn cache(self: *FsCache) Cache { return Cache.init(self); } -const HASHED_KEY_LEN = 64; -const HASHED_PATH_LEN = HASHED_KEY_LEN + 5; -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 serializeMeta(writer: *std.Io.Writer, meta: *const CachedMetadata) !void { - try writer.print("{s}\n{s}\n", .{ meta.url, meta.content_type }); - try writer.print("{d}\n{d}\n{d}\n", .{ - meta.status, - meta.stored_at, - meta.age_at_store, - }); - try writer.print("{s}\n", .{meta.etag orelse "null"}); - try writer.print("{s}\n", .{meta.last_modified orelse "null"}); - - // cache-control - try writer.print("{d}\n", .{meta.cache_control.max_age orelse 0}); - try writer.print("{}\n{}\n{}\n{}\n", .{ - meta.cache_control.max_age != null, - meta.cache_control.must_revalidate, - meta.cache_control.no_cache, - meta.cache_control.immutable, - }); - - // vary - if (meta.vary) |v| { - try writer.print("{s}\n", .{v.toString()}); - } else { - try writer.print("null\n", .{}); - } - try writer.flush(); - - try writer.print("{d}\n", .{meta.headers.len}); - for (meta.headers) |hdr| { - try writer.print("{s}\n{s}\n", .{ hdr.name, hdr.value }); - try writer.flush(); - } - try writer.flush(); -} - -fn deserializeMetaOptionalString(bytes: []const u8) ?[]const u8 { - if (std.mem.eql(u8, bytes, "null")) return null else return bytes; -} - -fn deserializeMetaBoolean(bytes: []const u8) !bool { - if (std.mem.eql(u8, bytes, "true")) return true; - if (std.mem.eql(u8, bytes, "false")) return false; - return error.Malformed; -} - -fn deserializeMeta(allocator: std.mem.Allocator, file: std.fs.File) !CachedMetadata { - var file_buf: [1024]u8 = undefined; - var file_reader = file.reader(&file_buf); - const reader = &file_reader.interface; - - const url = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - break :blk try allocator.dupeZ(u8, line); - }; - errdefer allocator.free(url); - - const content_type = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - break :blk try allocator.dupe(u8, line); - }; - errdefer allocator.free(content_type); - - const status = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - break :blk std.fmt.parseInt(u16, line, 10) catch return error.Malformed; - }; - const stored_at = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - break :blk std.fmt.parseInt(i64, line, 10) catch return error.Malformed; - }; - const age_at_store = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - break :blk std.fmt.parseInt(u64, line, 10) catch return error.Malformed; - }; - - const etag = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - break :blk if (std.mem.eql(u8, line, "null")) null else try allocator.dupe(u8, line); - }; - errdefer if (etag) |e| allocator.free(e); - - const last_modified = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - break :blk if (std.mem.eql(u8, line, "null")) null else try allocator.dupe(u8, line); - }; - errdefer if (last_modified) |lm| allocator.free(lm); - - // cache-control - const cc = cache_control: { - const max_age_val = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - break :blk std.fmt.parseInt(u64, line, 10) catch return error.Malformed; - }; - const max_age_present = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - break :blk try deserializeMetaBoolean(line); - }; - const must_revalidate = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - break :blk try deserializeMetaBoolean(line); - }; - const no_cache = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - break :blk try deserializeMetaBoolean(line); - }; - const immutable = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - break :blk try deserializeMetaBoolean(line); - }; - break :cache_control Cache.CacheControl{ - .max_age = if (max_age_present) max_age_val else null, - .must_revalidate = must_revalidate, - .no_cache = no_cache, - .immutable = immutable, - }; - }; - - // vary - const vary = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - if (std.mem.eql(u8, line, "null")) break :blk null; - const duped = try allocator.dupe(u8, line); - break :blk Cache.Vary.parse(duped); - }; - errdefer if (vary) |v| if (v == .value) allocator.free(v.value); - - const headers = blk: { - const line = try reader.takeDelimiter('\n') orelse return error.Malformed; - const count = std.fmt.parseInt(usize, line, 10) catch return error.Malformed; - - const hdrs = try allocator.alloc(Http.Header, count); - errdefer allocator.free(hdrs); - - for (hdrs) |*hdr| { - const name = try reader.takeDelimiter('\n') orelse return error.Malformed; - const value = try reader.takeDelimiter('\n') orelse return error.Malformed; - hdr.* = .{ - .name = try allocator.dupe(u8, name), - .value = try allocator.dupe(u8, value), - }; - } - - break :blk hdrs; - }; - errdefer { - for (headers) |hdr| { - allocator.free(hdr.name); - allocator.free(hdr.value); - } - allocator.free(headers); - } - - return .{ - .url = url, - .content_type = content_type, - .status = status, - .stored_at = stored_at, - .age_at_store = age_at_store, - .cache_control = cc, - .etag = etag, - .last_modified = last_modified, - .vary = vary, - .headers = headers, - }; -} - -pub fn get(self: *FsCache, allocator: std.mem.Allocator, key: []const u8) ?Cache.CachedResponse { +pub fn get(self: *FsCache, arena: std.mem.Allocator, key: []const u8) ?Cache.CachedResponse { const hashed_key = hashKey(key); + const meta_p = metaPath(&hashed_key); + const body_p = bodyPath(&hashed_key); - var meta_path: [HASHED_PATH_LEN]u8 = undefined; - _ = std.fmt.bufPrint(&meta_path, "{s}.meta", .{hashed_key}) catch @panic("FsCache.get meta path overflowed"); - - var body_path: [HASHED_PATH_LEN]u8 = undefined; - _ = std.fmt.bufPrint(&body_path, "{s}.body", .{hashed_key}) catch @panic("FsCache.get body path overflowed"); - - const meta_file = self.dir.openFile(&meta_path, .{ .mode = .read_only }) catch return null; + const meta_file = self.dir.openFile(&meta_p, .{ .mode = .read_only }) catch return null; defer meta_file.close(); - const meta = deserializeMeta(allocator, meta_file) catch { - self.dir.deleteFile(&meta_path) catch {}; - self.dir.deleteFile(&body_path) catch {}; + const contents = meta_file.readToEndAlloc(arena, 1 * 1024 * 1024) catch return null; + defer arena.free(contents); + + const cache_file: CacheMetadataFile = std.json.parseFromSliceLeaky( + CacheMetadataFile, + arena, + contents, + .{ .allocate = .alloc_always }, + ) catch { + self.dir.deleteFile(&meta_p) catch {}; + self.dir.deleteFile(&body_p) catch {}; return null; }; - const body_file = self.dir.openFile(&body_path, .{ .mode = .read_only }) catch return null; + const metadata = cache_file.metadata; + + if (cache_file.version != CACHE_VERSION) { + self.dir.deleteFile(&meta_p) catch {}; + self.dir.deleteFile(&body_p) catch {}; + return null; + } + + const body_file = self.dir.openFile( + &body_p, + .{ .mode = .read_only }, + ) catch return null; return .{ - .metadata = meta, + .metadata = metadata, .data = .{ .file = body_file }, }; } pub fn put(self: *FsCache, key: []const u8, meta: CachedMetadata, body: []const u8) !void { const hashed_key = hashKey(key); - - // Write meta to a temp file, then atomically rename into place - var meta_path: [HASHED_PATH_LEN]u8 = undefined; - _ = std.fmt.bufPrint(&meta_path, "{s}.meta", .{hashed_key}) catch - @panic("FsCache.put meta path overflowed"); - - var meta_tmp_path: [HASHED_TMP_PATH_LEN]u8 = undefined; - _ = std.fmt.bufPrint(&meta_tmp_path, "{s}.meta.tmp", .{hashed_key}) catch - @panic("FsCache.put meta tmp path overflowed"); + const meta_p = metaPath(&hashed_key); + const meta_tmp_p = metaTmpPath(&hashed_key); + const body_p = bodyPath(&hashed_key); + const body_tmp_p = bodyTmpPath(&hashed_key); { - const meta_file = try self.dir.createFile(&meta_tmp_path, .{}); + const meta_file = try self.dir.createFile(&meta_tmp_p, .{}); errdefer { meta_file.close(); - self.dir.deleteFile(&meta_tmp_path) catch {}; + self.dir.deleteFile(&meta_tmp_p) catch {}; } - var buf: [512]u8 = undefined; - var meta_file_writer = meta_file.writer(&buf); - try serializeMeta(&meta_file_writer.interface, &meta); + var meta_file_writer_buf: [512]u8 = undefined; + var meta_file_writer = meta_file.writer(&meta_file_writer_buf); + const meta_file_writer_iface = &meta_file_writer.interface; + try std.json.Stringify.value( + CacheMetadataFile{ .version = CACHE_VERSION, .metadata = meta }, + .{ .whitespace = .minified }, + meta_file_writer_iface, + ); + try meta_file_writer_iface.flush(); meta_file.close(); } - errdefer self.dir.deleteFile(&meta_tmp_path) catch {}; - try self.dir.rename(&meta_tmp_path, &meta_path); - - // Write body to a temp file, then atomically rename into place - var body_path: [HASHED_PATH_LEN]u8 = undefined; - _ = std.fmt.bufPrint(&body_path, "{s}.body", .{hashed_key}) catch - @panic("FsCache.put body path overflowed"); - - var body_tmp_path: [HASHED_TMP_PATH_LEN]u8 = undefined; - _ = std.fmt.bufPrint(&body_tmp_path, "{s}.body.tmp", .{hashed_key}) catch - @panic("FsCache.put body tmp path overflowed"); + errdefer self.dir.deleteFile(&meta_tmp_p) catch {}; + try self.dir.rename(&meta_tmp_p, &meta_p); { - const body_file = try self.dir.createFile(&body_tmp_path, .{}); + const body_file = try self.dir.createFile(&body_tmp_p, .{}); errdefer { body_file.close(); - self.dir.deleteFile(&body_tmp_path) catch {}; + self.dir.deleteFile(&body_tmp_p) catch {}; } try body_file.writeAll(body); body_file.close(); } - errdefer self.dir.deleteFile(&body_tmp_path) catch {}; + errdefer self.dir.deleteFile(&body_tmp_p) catch {}; - errdefer self.dir.deleteFile(&meta_path) catch {}; - try self.dir.rename(&body_tmp_path, &body_path); + errdefer self.dir.deleteFile(&meta_p) catch {}; + try self.dir.rename(&body_tmp_p, &body_p); }