From 3ca1f230b9626458d21072e141192adc3ff44180 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 16 Apr 2026 10:35:34 +0800 Subject: [PATCH] Serialize sameSite Tweak ergonomics (public functions log internally and are infallible). Use readFileAlloc directly. Fix possible memory leak with cookie arena - I don't think you can make a copy of the arena, and then dupe with the original. --- src/browser/webapi/storage/Cookie.zig | 2 +- src/cdp/CDP.zig | 4 +- src/cookies.zig | 93 ++++++++++++++++----------- src/lightpanda.zig | 8 +-- src/mcp/Server.zig | 8 +-- 5 files changed, 62 insertions(+), 53 deletions(-) diff --git a/src/browser/webapi/storage/Cookie.zig b/src/browser/webapi/storage/Cookie.zig index 58728653..f6fb3ec5 100644 --- a/src/browser/webapi/storage/Cookie.zig +++ b/src/browser/webapi/storage/Cookie.zig @@ -41,7 +41,7 @@ secure: bool = false, http_only: bool = false, same_site: SameSite = .none, -const SameSite = enum { +pub const SameSite = enum { strict, lax, none, diff --git a/src/cdp/CDP.zig b/src/cdp/CDP.zig index 799d3ea9..0a55b6a9 100644 --- a/src/cdp/CDP.zig +++ b/src/cdp/CDP.zig @@ -395,9 +395,7 @@ pub const BrowserContext = struct { const session = try cdp.browser.newSession(notification); if (cdp.client.app.config.cookieFile()) |cookie_path| { - lp.cookies.loadFromFile(session, cookie_path) catch |err| { - log.err(.app, "cookie load error", .{ .err = err }); - }; + lp.cookies.loadFromFile(session, cookie_path); } const browser = &cdp.browser; diff --git a/src/cookies.zig b/src/cookies.zig index 8d5df888..aa069393 100644 --- a/src/cookies.zig +++ b/src/cookies.zig @@ -14,74 +14,87 @@ // along with this program. If not, see . const std = @import("std"); -const Allocator = std.mem.Allocator; +const log = @import("log.zig"); const Session = @import("browser/Session.zig"); const Cookie = @import("browser/webapi/storage/Cookie.zig"); -const log = @import("log.zig"); + +const Allocator = std.mem.Allocator; /// Load cookies from a JSON file into the cookie jar. /// The file format is an array of objects with: name, value, domain, path, /// expires (optional, float), secure (optional, bool), httpOnly (optional, bool). /// This matches the CDP Network.Cookie format used by Puppeteer and Playwright. -pub fn loadFromFile(session: *Session, path: []const u8) !void { - const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) { - error.FileNotFound => return, // No file yet, nothing to load - else => { - log.err(.app, "failed to open cookies file", .{ .path = path, .err = err }); - return err; - }, +pub fn loadFromFile(session: *Session, path: []const u8) void { + _loadFromFile(session, path) catch |err| { + log.err(.app, "Cookie.loadFromFile", .{ .err = err, .path = path }); }; - defer file.close(); +} - const call_arena = try session.getArena(.medium, "cookies.jar.allocatorloadFromFile"); - defer session.releaseArena(call_arena); +fn _loadFromFile(session: *Session, path: []const u8) !void { + const arena = try session.getArena(.medium, "Cookies.loadFromFile"); + defer session.releaseArena(arena); - const content = file.readToEndAlloc(call_arena, 1024 * 1024) catch |err| { - log.err(.app, "failed to read cookies file", .{ .path = path, .err = err }); - return err; + const content = std.fs.cwd().readFileAlloc(arena, path, 1024 * 1024) catch |err| { + switch (err) { + error.FileNotFound => log.debug(.app, "Cookie.readFile", .{ .path = path, .note = "file not found" }), + else => log.err(.app, "Cookie.readFile", .{ .path = path, .err = err }), + } + return; }; - const parsed = std.json.parseFromSlice([]const JsonCookie, call_arena, content, .{ + const json_cookies = std.json.parseFromSliceLeaky([]const JsonCookie, arena, content, .{ .ignore_unknown_fields = true, }) catch |err| { - log.err(.app, "failed to parse cookies JSON", .{ .path = path, .err = err }); - return err; + log.err(.app, "Cookie.parseFile", .{ .path = path, .err = err }); + return; }; - defer parsed.deinit(); const jar = &session.cookie_jar; + const now = std.time.timestamp(); + var loaded: usize = 0; - for (parsed.value) |jc| { - var arena = std.heap.ArenaAllocator.init(jar.allocator); - errdefer arena.deinit(); - const a = arena.allocator(); + for (json_cookies) |jc| { + var cookie_arena = std.heap.ArenaAllocator.init(jar.allocator); + errdefer cookie_arena.deinit(); + + const a = cookie_arena.allocator(); + const name = try a.dupe(u8, jc.name); + const value = try a.dupe(u8, jc.value); + const domain = try a.dupe(u8, jc.domain); + const cookie_path = if (jc.path) |p| try a.dupe(u8, p) else "/"; const cookie = Cookie{ - .arena = arena, - .name = try a.dupe(u8, jc.name), - .value = try a.dupe(u8, jc.value), - .domain = try a.dupe(u8, jc.domain), - .path = try a.dupe(u8, jc.path orelse "/"), + .arena = cookie_arena, + .name = name, + .value = value, + .domain = domain, + .path = cookie_path, .expires = jc.expires, .secure = jc.secure orelse false, .http_only = jc.httpOnly orelse false, - .same_site = .none, + .same_site = jc.sameSite, }; - jar.add(cookie, std.time.timestamp()) catch |err| { + jar.add(cookie, now) catch |err| { cookie.deinit(); - log.warn(.app, "skipping cookie", .{ .name = jc.name, .err = err }); + log.warn(.app, "invalid cookie", .{ .name = jc.name, .err = err }); continue; }; loaded += 1; } - log.info(.app, "loaded cookies from file", .{ .path = path, .count = loaded }); + log.info(.app, "Cookie.loadFromFile", .{ .path = path, .count = loaded }); } /// Save all cookies from the jar to a JSON file. -pub fn saveToFile(jar: *Cookie.Jar, path: []const u8) !void { +pub fn saveToFile(jar: *Cookie.Jar, path: []const u8) void { + _saveToFile(jar, path) catch |err| { + log.err(.app, "Cookie.saveToFile", .{ .path = path, .err = err }); + }; +} + +fn _saveToFile(jar: *Cookie.Jar, path: []const u8) !void { jar.removeExpired(null); var file = try std.fs.cwd().createFile(path, .{}); @@ -91,9 +104,12 @@ pub fn saveToFile(jar: *Cookie.Jar, path: []const u8) !void { var writer = file.writer(&buf); const w = &writer.interface; - try w.writeAll("["); + try w.writeByte('['); for (jar.cookies.items, 0..) |c, i| { - if (i > 0) try w.writeAll(","); + if (i > 0) { + try w.writeByte(','); + } + try w.writeAll("\n "); try std.json.Stringify.value(JsonCookie{ .name = c.name, @@ -103,15 +119,17 @@ pub fn saveToFile(jar: *Cookie.Jar, path: []const u8) !void { .expires = c.expires, .secure = c.secure, .httpOnly = c.http_only, + .sameSite = c.same_site, }, .{}, w); } + if (jar.cookies.items.len > 0) { - try w.writeAll("\n"); + try w.writeByte('\n'); } try w.writeAll("]\n"); try writer.end(); - log.info(.app, "saved cookies to file", .{ .path = path, .count = jar.cookies.items.len }); + log.info(.app, "Cookie.saveToFile", .{ .path = path, .count = jar.cookies.items.len }); } const JsonCookie = struct { @@ -122,4 +140,5 @@ const JsonCookie = struct { expires: ?f64 = null, secure: ?bool = null, httpOnly: ?bool = null, + sameSite: Cookie.SameSite = .none, }; diff --git a/src/lightpanda.zig b/src/lightpanda.zig index 84775a7c..42f7c937 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -69,16 +69,12 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void { var session = try browser.newSession(notification); if (app.config.cookieFile()) |cookie_path| { - cookies.loadFromFile(session, cookie_path) catch |err| { - log.err(.app, "cookie load error", .{ .err = err }); - }; + cookies.loadFromFile(session, cookie_path); } defer { if (app.config.cookieJarFile()) |cookie_jar_path| { - cookies.saveToFile(&session.cookie_jar, cookie_jar_path) catch |err| { - log.err(.app, "cookie save error", .{ .err = err }); - }; + cookies.saveToFile(&session.cookie_jar, cookie_jar_path); } } diff --git a/src/mcp/Server.zig b/src/mcp/Server.zig index 5dbb8a0f..59668398 100644 --- a/src/mcp/Server.zig +++ b/src/mcp/Server.zig @@ -52,9 +52,7 @@ pub fn init(allocator: std.mem.Allocator, app: *App, writer: *std.io.Writer) !*S self.session = try self.browser.newSession(self.notification); if (app.config.cookieFile()) |cookie_path| { - lp.cookies.loadFromFile(self.session, cookie_path) catch |err| { - lp.log.err(.mcp, "cookie load error", .{ .err = err }); - }; + lp.cookies.loadFromFile(self.session, cookie_path); } return self; @@ -62,9 +60,7 @@ pub fn init(allocator: std.mem.Allocator, app: *App, writer: *std.io.Writer) !*S pub fn deinit(self: *Self) void { if (self.app.config.cookieJarFile()) |cookie_jar_path| { - lp.cookies.saveToFile(&self.session.cookie_jar, cookie_jar_path) catch |err| { - lp.log.err(.mcp, "cookie save error", .{ .err = err }); - }; + lp.cookies.saveToFile(&self.session.cookie_jar, cookie_jar_path); } self.node_registry.deinit();