// 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 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, };