mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-31 01:28:55 +00:00
use json for fs cache metadata file
This commit is contained in:
314
src/network/cache/FsCache.zig
vendored
314
src/network/cache/FsCache.zig
vendored
@@ -22,10 +22,53 @@ const Http = @import("../http.zig");
|
|||||||
const CachedMetadata = Cache.CachedMetadata;
|
const CachedMetadata = Cache.CachedMetadata;
|
||||||
const CachedResponse = Cache.CachedResponse;
|
const CachedResponse = Cache.CachedResponse;
|
||||||
|
|
||||||
|
const CACHE_VERSION: usize = 1;
|
||||||
|
|
||||||
pub const FsCache = @This();
|
pub const FsCache = @This();
|
||||||
|
|
||||||
dir: std.fs.Dir,
|
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 {
|
pub fn init(path: []const u8) !FsCache {
|
||||||
const cwd = std.fs.cwd();
|
const cwd = std.fs.cwd();
|
||||||
|
|
||||||
@@ -46,257 +89,86 @@ pub fn cache(self: *FsCache) Cache {
|
|||||||
return Cache.init(self);
|
return Cache.init(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
const HASHED_KEY_LEN = 64;
|
pub fn get(self: *FsCache, arena: std.mem.Allocator, key: []const u8) ?Cache.CachedResponse {
|
||||||
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 {
|
|
||||||
const hashed_key = hashKey(key);
|
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;
|
const meta_file = self.dir.openFile(&meta_p, .{ .mode = .read_only }) catch return null;
|
||||||
_ = 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;
|
|
||||||
defer meta_file.close();
|
defer meta_file.close();
|
||||||
|
|
||||||
const meta = deserializeMeta(allocator, meta_file) catch {
|
const contents = meta_file.readToEndAlloc(arena, 1 * 1024 * 1024) catch return null;
|
||||||
self.dir.deleteFile(&meta_path) catch {};
|
defer arena.free(contents);
|
||||||
self.dir.deleteFile(&body_path) catch {};
|
|
||||||
|
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;
|
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 .{
|
return .{
|
||||||
.metadata = meta,
|
.metadata = metadata,
|
||||||
.data = .{ .file = body_file },
|
.data = .{ .file = body_file },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn put(self: *FsCache, key: []const u8, meta: CachedMetadata, body: []const u8) !void {
|
pub fn put(self: *FsCache, key: []const u8, meta: CachedMetadata, body: []const u8) !void {
|
||||||
const hashed_key = hashKey(key);
|
const hashed_key = hashKey(key);
|
||||||
|
const meta_p = metaPath(&hashed_key);
|
||||||
// Write meta to a temp file, then atomically rename into place
|
const meta_tmp_p = metaTmpPath(&hashed_key);
|
||||||
var meta_path: [HASHED_PATH_LEN]u8 = undefined;
|
const body_p = bodyPath(&hashed_key);
|
||||||
_ = std.fmt.bufPrint(&meta_path, "{s}.meta", .{hashed_key}) catch
|
const body_tmp_p = bodyTmpPath(&hashed_key);
|
||||||
@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_file = try self.dir.createFile(&meta_tmp_path, .{});
|
const meta_file = try self.dir.createFile(&meta_tmp_p, .{});
|
||||||
errdefer {
|
errdefer {
|
||||||
meta_file.close();
|
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_buf: [512]u8 = undefined;
|
||||||
var meta_file_writer = meta_file.writer(&buf);
|
var meta_file_writer = meta_file.writer(&meta_file_writer_buf);
|
||||||
try serializeMeta(&meta_file_writer.interface, &meta);
|
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();
|
meta_file.close();
|
||||||
}
|
}
|
||||||
errdefer self.dir.deleteFile(&meta_tmp_path) catch {};
|
errdefer self.dir.deleteFile(&meta_tmp_p) catch {};
|
||||||
try self.dir.rename(&meta_tmp_path, &meta_path);
|
try self.dir.rename(&meta_tmp_p, &meta_p);
|
||||||
|
|
||||||
// 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");
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const body_file = try self.dir.createFile(&body_tmp_path, .{});
|
const body_file = try self.dir.createFile(&body_tmp_p, .{});
|
||||||
errdefer {
|
errdefer {
|
||||||
body_file.close();
|
body_file.close();
|
||||||
self.dir.deleteFile(&body_tmp_path) catch {};
|
self.dir.deleteFile(&body_tmp_p) catch {};
|
||||||
}
|
}
|
||||||
try body_file.writeAll(body);
|
try body_file.writeAll(body);
|
||||||
body_file.close();
|
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 {};
|
errdefer self.dir.deleteFile(&meta_p) catch {};
|
||||||
try self.dir.rename(&body_tmp_path, &body_path);
|
try self.dir.rename(&body_tmp_p, &body_p);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user