Files
browser/src/network/cache/Cache.zig
2026-03-20 02:18:40 -07:00

187 lines
6.0 KiB
Zig

// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
const std = @import("std");
const Http = @import("../http.zig");
const FsCache = @import("FsCache.zig");
/// A browser-wide cache for resources across the network.
/// This mostly conforms to RFC9111 with regards to caching behavior.
pub const Cache = @This();
kind: union(enum) {
fs: FsCache,
},
pub fn get(self: *Cache, allocator: std.mem.Allocator, key: []const u8) ?CachedResponse {
return switch (self.kind) {
inline else => |*c| c.get(allocator, key),
};
}
pub fn put(self: *Cache, key: []const u8, metadata: CachedMetadata, body: []const u8) !void {
return switch (self.kind) {
inline else => |*c| c.put(key, metadata, body),
};
}
pub const CacheControl = struct {
max_age: ?u64 = null,
must_revalidate: bool = false,
no_cache: bool = false,
no_store: bool = false,
immutable: bool = false,
pub fn parse(value: []const u8) CacheControl {
var cc: CacheControl = .{};
var iter = std.mem.splitScalar(u8, value, ',');
while (iter.next()) |part| {
const directive = std.mem.trim(u8, part, &std.ascii.whitespace);
if (std.ascii.eqlIgnoreCase(directive, "no-store")) {
cc.no_store = true;
} else if (std.ascii.eqlIgnoreCase(directive, "no-cache")) {
cc.no_cache = true;
} else if (std.ascii.eqlIgnoreCase(directive, "must-revalidate")) {
cc.must_revalidate = true;
} else if (std.ascii.eqlIgnoreCase(directive, "immutable")) {
cc.immutable = true;
} else if (std.ascii.startsWithIgnoreCase(directive, "max-age=")) {
cc.max_age = std.fmt.parseInt(u64, directive[8..], 10) catch null;
} else if (std.ascii.startsWithIgnoreCase(directive, "s-maxage=")) {
// s-maxage takes precedence over max-age
cc.max_age = std.fmt.parseInt(u64, directive[9..], 10) catch cc.max_age;
}
}
return cc;
}
};
pub const Vary = union(enum) {
wildcard: void,
value: []const u8,
pub fn parse(value: []const u8) Vary {
if (std.mem.eql(u8, value, "*")) return .wildcard;
return .{ .value = value };
}
pub fn deinit(self: Vary, allocator: std.mem.Allocator) void {
switch (self) {
.wildcard => {},
.value => |v| allocator.free(v),
}
}
pub fn toString(self: Vary) []const u8 {
return switch (self) {
.wildcard => "*",
.value => |v| v,
};
}
};
pub const CachedMetadata = struct {
url: [:0]const u8,
content_type: []const u8,
status: u16,
stored_at: i64,
age_at_store: u64,
// for If-None-Match
etag: ?[]const u8,
// for If-Modified-Since
last_modified: ?[]const u8,
cache_control: CacheControl,
vary: ?Vary,
headers: []const Http.Header,
pub fn fromHeaders(
url: [:0]const u8,
status: u16,
timestamp: i64,
headers: []const Http.Header,
) !?CachedMetadata {
var cc: CacheControl = .{};
var vary: ?Vary = null;
var etag: ?[]const u8 = null;
var last_modified: ?[]const u8 = null;
var age_at_store: u64 = 0;
var content_type: []const u8 = "application/octet-stream";
for (headers) |hdr| {
if (std.ascii.eqlIgnoreCase(hdr.name, "cache-control")) {
cc = CacheControl.parse(hdr.value);
} else if (std.ascii.eqlIgnoreCase(hdr.name, "etag")) {
etag = hdr.value;
} else if (std.ascii.eqlIgnoreCase(hdr.name, "last-modified")) {
last_modified = hdr.value;
} else if (std.ascii.eqlIgnoreCase(hdr.name, "vary")) {
vary = Vary.parse(hdr.value);
} else if (std.ascii.eqlIgnoreCase(hdr.name, "age")) {
age_at_store = std.fmt.parseInt(u64, hdr.value, 10) catch 0;
} else if (std.ascii.eqlIgnoreCase(hdr.name, "content-type")) {
content_type = hdr.value;
}
}
// return null for uncacheable responses
if (cc.no_store) return null;
if (vary) |v| if (v == .wildcard) return null;
if (cc.max_age == null) return null;
return .{
.url = url,
.content_type = content_type,
.status = status,
.stored_at = timestamp,
.age_at_store = age_at_store,
.etag = etag,
.last_modified = last_modified,
.cache_control = cc,
.vary = vary,
.headers = headers,
};
}
pub fn deinit(self: CachedMetadata, allocator: std.mem.Allocator) void {
allocator.free(self.url);
allocator.free(self.content_type);
for (self.headers) |header| {
allocator.free(header.name);
allocator.free(header.value);
}
allocator.free(self.headers);
if (self.vary) |v| v.deinit(allocator);
if (self.etag) |e| allocator.free(e);
if (self.last_modified) |lm| allocator.free(lm);
}
};
pub const CachedData = union(enum) {
buffer: []const u8,
file: std.fs.File,
};
pub const CachedResponse = struct {
metadata: CachedMetadata,
data: CachedData,
};