From a95b4ea7b95004b8ea4c9516b97fbbc16f73e1e6 Mon Sep 17 00:00:00 2001 From: Nikolay Govorov Date: Wed, 11 Mar 2026 05:44:59 +0000 Subject: [PATCH 1/9] Use global connections poll --- src/browser/HttpClient.zig | 124 ++++++++++++++++++++++++++----------- src/network/Runtime.zig | 34 ++++++++++ src/network/http.zig | 103 +++++------------------------- src/sys/libcurl.zig | 14 ++++- 4 files changed, 150 insertions(+), 125 deletions(-) diff --git a/src/browser/HttpClient.zig b/src/browser/HttpClient.zig index 98292efc..529d4e5a 100644 --- a/src/browser/HttpClient.zig +++ b/src/browser/HttpClient.zig @@ -66,9 +66,18 @@ active: usize, // 'networkAlmostIdle' Page.lifecycleEvent in CDP). intercepted: usize, -// Our easy handles, managed by a curl multi. +// Our curl multi handle. handles: Net.Handles, +// Connections currently in this client's curl_multi. +in_use: std.DoublyLinkedList = .{}, + +// Connections that failed to be removed from curl_multi during perform. +dirty: std.DoublyLinkedList = .{}, + +// Whether we're currently inside a curl_multi_perform call. +performing: bool = false, + // Use to generate the next request ID next_request_id: u32 = 0, @@ -88,8 +97,8 @@ pending_robots_queue: std.StringHashMapUnmanaged(std.ArrayList(Request)) = .empt // request. These wil come and go with each request. transfer_pool: std.heap.MemoryPool(Transfer), -// only needed for CDP which can change the proxy and then restore it. When -// restoring, this originally-configured value is what it goes to. +// The current proxy. CDP can change it, restoreOriginalProxy restores +// from config. http_proxy: ?[:0]const u8 = null, // track if the client use a proxy for connections. @@ -97,6 +106,9 @@ http_proxy: ?[:0]const u8 = null, // CDP. use_proxy: bool, +// Current TLS verification state, applied per-connection in makeRequest. +tls_verify: bool = true, + cdp_client: ?CDPClient = null, // libcurl can monitor arbitrary sockets, this lets us use libcurl to poll @@ -126,13 +138,8 @@ pub fn init(allocator: Allocator, network: *Network) !*Client { const client = try allocator.create(Client); errdefer allocator.destroy(client); - var handles = try Net.Handles.init(allocator, network.ca_blob, network.config); - errdefer handles.deinit(allocator); - - // Set transfer callbacks on each connection. - for (handles.connections) |*conn| { - try conn.setCallbacks(Transfer.headerCallback, Transfer.dataCallback); - } + var handles = try Net.Handles.init(network.config); + errdefer handles.deinit(); const http_proxy = network.config.httpProxy(); @@ -145,6 +152,7 @@ pub fn init(allocator: Allocator, network: *Network) !*Client { .network = network, .http_proxy = http_proxy, .use_proxy = http_proxy != null, + .tls_verify = network.config.tlsVerifyHost(), .transfer_pool = transfer_pool, }; @@ -153,7 +161,7 @@ pub fn init(allocator: Allocator, network: *Network) !*Client { pub fn deinit(self: *Client) void { self.abort(); - self.handles.deinit(self.allocator); + self.handles.deinit(); self.transfer_pool.deinit(); @@ -182,14 +190,14 @@ pub fn abortFrame(self: *Client, frame_id: u32) void { // but abort can avoid the frame_id check at comptime. fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void { { - var q = &self.handles.in_use; + var q = &self.in_use; var n = q.first; while (n) |node| { n = node.next; const conn: *Net.Connection = @fieldParentPtr("node", node); var transfer = Transfer.fromConnection(conn) catch |err| { // Let's cleanup what we can - self.handles.remove(conn); + self.removeConn(conn); log.err(.http, "get private info", .{ .err = err, .source = "abort" }); continue; }; @@ -226,8 +234,7 @@ fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void { } if (comptime IS_DEBUG and abort_all) { - std.debug.assert(self.handles.in_use.first == null); - std.debug.assert(self.handles.available.len() == self.handles.connections.len); + std.debug.assert(self.in_use.first == null); const running = self.handles.perform() catch |err| { lp.assert(false, "multi perform in abort", .{ .err = err }); @@ -237,15 +244,12 @@ fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void { } pub fn tick(self: *Client, timeout_ms: u32) !PerformStatus { - while (true) { - if (self.handles.hasAvailable() == false) { + while (self.queue.popFirst()) |queue_node| { + const conn = self.network.getConnection() orelse { + self.queue.prepend(queue_node); break; - } - const queue_node = self.queue.popFirst() orelse break; + }; const transfer: *Transfer = @fieldParentPtr("_node", queue_node); - - // we know this exists, because we checked hasAvailable() above - const conn = self.handles.get().?; try self.makeRequest(conn, transfer); } return self.perform(@intCast(timeout_ms)); @@ -529,8 +533,8 @@ fn waitForInterceptedResponse(self: *Client, transfer: *Transfer) !bool { fn process(self: *Client, transfer: *Transfer) !void { // libcurl doesn't allow recursive calls, if we're in a `perform()` operation // then we _have_ to queue this. - if (self.handles.performing == false) { - if (self.handles.get()) |conn| { + if (self.performing == false) { + if (self.network.getConnection()) |conn| { return self.makeRequest(conn, transfer); } } @@ -645,9 +649,12 @@ fn requestFailed(transfer: *Transfer, err: anyerror, comptime execute_callback: pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void { try self.ensureNoActiveConnection(); - for (self.handles.connections) |*conn| { - try conn.setProxy(proxy.ptr); + var it = self.in_use.first; + while (it) |node| : (it = node.next) { + const conn: *Net.Connection = @fieldParentPtr("node", node); + try conn.setProxy(proxy); } + self.http_proxy = proxy; self.use_proxy = true; } @@ -656,11 +663,13 @@ pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void { pub fn restoreOriginalProxy(self: *Client) !void { try self.ensureNoActiveConnection(); - const proxy = if (self.http_proxy) |p| p.ptr else null; - for (self.handles.connections) |*conn| { - try conn.setProxy(proxy); + self.http_proxy = self.network.config.httpProxy(); + var it = self.in_use.first; + while (it) |node| : (it = node.next) { + const conn: *Net.Connection = @fieldParentPtr("node", node); + try conn.setProxy(self.http_proxy); } - self.use_proxy = proxy != null; + self.use_proxy = self.http_proxy != null; } // Enable TLS verification on all connections. @@ -668,9 +677,12 @@ pub fn enableTlsVerify(self: *Client) !void { // Remove inflight connections check on enable TLS b/c chromiumoxide calls // the command during navigate and Curl seems to accept it... - for (self.handles.connections) |*conn| { + var it = self.in_use.first; + while (it) |node| : (it = node.next) { + const conn: *Net.Connection = @fieldParentPtr("node", node); try conn.setTlsVerify(true, self.use_proxy); } + self.tls_verify = true; } // Disable TLS verification on all connections. @@ -678,9 +690,12 @@ pub fn disableTlsVerify(self: *Client) !void { // Remove inflight connections check on disable TLS b/c chromiumoxide calls // the command during navigate and Curl seems to accept it... - for (self.handles.connections) |*conn| { + var it = self.in_use.first; + while (it) |node| : (it = node.next) { + const conn: *Net.Connection = @fieldParentPtr("node", node); try conn.setTlsVerify(false, self.use_proxy); } + self.tls_verify = false; } fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerror!void { @@ -691,9 +706,14 @@ fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerr errdefer { transfer._conn = null; transfer.deinit(); - self.handles.isAvailable(conn); + self.releaseConn(conn); } + // Set callbacks and per-client settings on the pooled connection. + try conn.setCallbacks(Transfer.headerCallback, Transfer.dataCallback); + try conn.setProxy(self.http_proxy); + try conn.setTlsVerify(self.tls_verify, self.use_proxy); + try conn.setURL(req.url); try conn.setMethod(req.method); if (req.body) |b| { @@ -728,10 +748,12 @@ fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerr // fails BEFORE `curl_multi_add_handle` succeeds, the we still need to do // cleanup. But if things fail after `curl_multi_add_handle`, we expect // perfom to pickup the failure and cleanup. + self.in_use.append(&conn.node); self.handles.add(conn) catch |err| { transfer._conn = null; transfer.deinit(); - self.handles.isAvailable(conn); + self.in_use.remove(&conn.node); + self.releaseConn(conn); return err; }; @@ -752,7 +774,19 @@ pub const PerformStatus = enum { }; fn perform(self: *Client, timeout_ms: c_int) !PerformStatus { + self.performing = true; const running = try self.handles.perform(); + self.performing = false; + + // Process dirty connections — return them to Runtime pool. + while (self.dirty.popFirst()) |node| { + const conn: *Net.Connection = @fieldParentPtr("node", node); + self.handles.remove(conn) catch |err| { + log.fatal(.http, "multi remove handle", .{ .err = err, .src = "perform" }); + @panic("multi_remove_handle"); + }; + self.releaseConn(conn); + } // We're potentially going to block for a while until we get data. Process // whatever messages we have waiting ahead of time. @@ -871,11 +905,27 @@ fn processMessages(self: *Client) !bool { fn endTransfer(self: *Client, transfer: *Transfer) void { const conn = transfer._conn.?; - self.handles.remove(conn); + self.removeConn(conn); transfer._conn = null; self.active -= 1; } +fn removeConn(self: *Client, conn: *Net.Connection) void { + self.in_use.remove(&conn.node); + if (self.handles.remove(conn)) { + self.releaseConn(conn); + } else |_| { + // Can happen if we're in a perform() call, so we'll queue this + // for cleanup later. + self.dirty.append(&conn.node); + } +} + +fn releaseConn(self: *Client, conn: *Net.Connection) void { + conn.reset() catch {}; + self.network.releaseConnection(conn); +} + fn ensureNoActiveConnection(self: *const Client) !void { if (self.active > 0) { return error.InflightConnection; @@ -1023,7 +1073,7 @@ pub const Transfer = struct { fn deinit(self: *Transfer) void { self.req.headers.deinit(); if (self._conn) |conn| { - self.client.handles.remove(conn); + self.client.removeConn(conn); } self.arena.deinit(); self.client.transfer_pool.destroy(self); @@ -1093,7 +1143,7 @@ pub const Transfer = struct { requestFailed(self, err, true); const client = self.client; - if (self._performing or client.handles.performing) { + if (self._performing or client.performing) { // We're currently in a curl_multi_perform. We cannot call endTransfer // as that calls curl_multi_remove_handle, and you can't do that // from a curl callback. Instead, we flag this transfer and all of diff --git a/src/network/Runtime.zig b/src/network/Runtime.zig index 0112dc18..7c534ee2 100644 --- a/src/network/Runtime.zig +++ b/src/network/Runtime.zig @@ -43,6 +43,10 @@ config: *const Config, ca_blob: ?net_http.Blob, robot_store: RobotStore, +connections: []net_http.Connection, +available: std.DoublyLinkedList = .{}, +conn_mutex: std.Thread.Mutex = .{}, + pollfds: []posix.pollfd, listener: ?Listener = null, @@ -79,11 +83,23 @@ pub fn init(allocator: Allocator, config: *const Config) !Runtime { ca_blob = try loadCerts(allocator); } + const count: usize = config.httpMaxConcurrent(); + const connections = try allocator.alloc(net_http.Connection, count); + errdefer allocator.free(connections); + + var available: std.DoublyLinkedList = .{}; + for (0..count) |i| { + connections[i] = try net_http.Connection.init(ca_blob, config); + available.append(&connections[i].node); + } + return .{ .allocator = allocator, .config = config, .ca_blob = ca_blob, .robot_store = RobotStore.init(allocator), + .connections = connections, + .available = available, .pollfds = pollfds, .wakeup_pipe = pipe, }; @@ -104,6 +120,11 @@ pub fn deinit(self: *Runtime) void { self.allocator.free(data[0..ca_blob.len]); } + for (self.connections) |*conn| { + conn.deinit(); + } + self.allocator.free(self.connections); + self.robot_store.deinit(); globalDeinit(); @@ -192,6 +213,19 @@ pub fn stop(self: *Runtime) void { _ = posix.write(self.wakeup_pipe[1], &.{1}) catch {}; } +pub fn getConnection(self: *Runtime) ?*net_http.Connection { + self.conn_mutex.lock(); + defer self.conn_mutex.unlock(); + const node = self.available.popFirst() orelse return null; + return @fieldParentPtr("node", node); +} + +pub fn releaseConnection(self: *Runtime, conn: *net_http.Connection) void { + self.conn_mutex.lock(); + defer self.conn_mutex.unlock(); + self.available.append(&conn.node); +} + pub fn newConnection(self: *Runtime) !net_http.Connection { return net_http.Connection.init(self.ca_blob, self.config); } diff --git a/src/network/http.zig b/src/network/http.zig index 28fd7736..b0f70375 100644 --- a/src/network/http.zig +++ b/src/network/http.zig @@ -237,7 +237,7 @@ pub const ResponseHead = struct { pub const Connection = struct { easy: *libcurl.Curl, - node: Handles.HandleList.Node = .{}, + node: std.DoublyLinkedList.Node = .{}, pub fn init( ca_blob_: ?libcurl.CurlBlob, @@ -385,8 +385,16 @@ pub const Connection = struct { try libcurl.curl_easy_setopt(self.easy, .write_function, data_cb); } - pub fn setProxy(self: *const Connection, proxy: ?[*:0]const u8) !void { - try libcurl.curl_easy_setopt(self.easy, .proxy, proxy); + pub fn reset(self: *const Connection) !void { + try libcurl.curl_easy_setopt(self.easy, .header_data, null); + try libcurl.curl_easy_setopt(self.easy, .header_function, null); + try libcurl.curl_easy_setopt(self.easy, .write_data, null); + try libcurl.curl_easy_setopt(self.easy, .write_function, null); + try libcurl.curl_easy_setopt(self.easy, .proxy, null); + } + + pub fn setProxy(self: *const Connection, proxy: ?[:0]const u8) !void { + try libcurl.curl_easy_setopt(self.easy, .proxy, if (proxy) |p| p.ptr else null); } pub fn setTlsVerify(self: *const Connection, verify: bool, use_proxy: bool) !void { @@ -467,111 +475,32 @@ pub const Connection = struct { }; pub const Handles = struct { - connections: []Connection, - dirty: HandleList, - in_use: HandleList, - available: HandleList, multi: *libcurl.CurlM, - performing: bool = false, - - pub const HandleList = std.DoublyLinkedList; - - pub fn init( - allocator: Allocator, - ca_blob: ?libcurl.CurlBlob, - config: *const Config, - ) !Handles { - const count: usize = config.httpMaxConcurrent(); - if (count == 0) return error.InvalidMaxConcurrent; + pub fn init(config: *const Config) !Handles { const multi = libcurl.curl_multi_init() orelse return error.FailedToInitializeMulti; errdefer libcurl.curl_multi_cleanup(multi) catch {}; try libcurl.curl_multi_setopt(multi, .max_host_connections, config.httpMaxHostOpen()); - const connections = try allocator.alloc(Connection, count); - errdefer allocator.free(connections); - - var available: HandleList = .{}; - for (0..count) |i| { - connections[i] = try Connection.init(ca_blob, config); - available.append(&connections[i].node); - } - - return .{ - .dirty = .{}, - .in_use = .{}, - .connections = connections, - .available = available, - .multi = multi, - }; + return .{ .multi = multi }; } - pub fn deinit(self: *Handles, allocator: Allocator) void { - for (self.connections) |*conn| { - conn.deinit(); - } - allocator.free(self.connections); + pub fn deinit(self: *Handles) void { libcurl.curl_multi_cleanup(self.multi) catch {}; } - pub fn hasAvailable(self: *const Handles) bool { - return self.available.first != null; - } - - pub fn get(self: *Handles) ?*Connection { - if (self.available.popFirst()) |node| { - self.in_use.append(node); - return @as(*Connection, @fieldParentPtr("node", node)); - } - return null; - } - pub fn add(self: *Handles, conn: *const Connection) !void { try libcurl.curl_multi_add_handle(self.multi, conn.easy); } - pub fn remove(self: *Handles, conn: *Connection) void { - if (libcurl.curl_multi_remove_handle(self.multi, conn.easy)) { - self.isAvailable(conn); - } else |err| { - // can happen if we're in a perform() call, so we'll queue this - // for cleanup later. - const node = &conn.node; - self.in_use.remove(node); - self.dirty.append(node); - log.warn(.http, "multi remove handle", .{ .err = err }); - } - } - - pub fn isAvailable(self: *Handles, conn: *Connection) void { - const node = &conn.node; - self.in_use.remove(node); - self.available.append(node); + pub fn remove(self: *Handles, conn: *const Connection) !void { + try libcurl.curl_multi_remove_handle(self.multi, conn.easy); } pub fn perform(self: *Handles) !c_int { - self.performing = true; - defer self.performing = false; - - const multi = self.multi; var running: c_int = undefined; try libcurl.curl_multi_perform(self.multi, &running); - - { - const list = &self.dirty; - while (list.first) |node| { - list.remove(node); - const conn: *Connection = @fieldParentPtr("node", node); - if (libcurl.curl_multi_remove_handle(multi, conn.easy)) { - self.available.append(node); - } else |err| { - log.fatal(.http, "multi remove handle", .{ .err = err, .src = "perform" }); - @panic("multi_remove_handle"); - } - } - } - return running; } diff --git a/src/sys/libcurl.zig b/src/sys/libcurl.zig index 759cba25..f9c9ac12 100644 --- a/src/sys/libcurl.zig +++ b/src/sys/libcurl.zig @@ -543,7 +543,10 @@ pub fn curl_easy_setopt(easy: *Curl, comptime option: CurlOption, value: anytype .header_data, .write_data, => blk: { - const ptr: *anyopaque = @ptrCast(value); + const ptr: ?*anyopaque = switch (@typeInfo(@TypeOf(value))) { + .null => null, + else => @ptrCast(value), + }; break :blk c.curl_easy_setopt(easy, opt, ptr); }, @@ -703,6 +706,15 @@ pub fn curl_multi_poll( try errorMCheck(c.curl_multi_poll(multi, raw_fds, @intCast(extra_fds.len), timeout_ms, numfds)); } +pub fn curl_multi_waitfds(multi: *CurlM, ufds: []CurlWaitFd, fd_count: *c_uint) ErrorMulti!void { + const raw_fds: [*c]c.curl_waitfd = if (ufds.len == 0) null else @ptrCast(ufds.ptr); + try errorMCheck(c.curl_multi_waitfds(multi, raw_fds, @intCast(ufds.len), fd_count)); +} + +pub fn curl_multi_timeout(multi: *CurlM, timeout_ms: *c_long) ErrorMulti!void { + try errorMCheck(c.curl_multi_timeout(multi, timeout_ms)); +} + pub fn curl_multi_info_read(multi: *CurlM, msgs_in_queue: *c_int) ?CurlMsg { const ptr = c.curl_multi_info_read(multi, msgs_in_queue); if (ptr == null) return null; From 619d27c773105b33b1a1977bcd6915a813dc14f1 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 12 Mar 2026 10:38:07 +0800 Subject: [PATCH 2/9] Add cleanup to Range In https://github.com/lightpanda-io/browser/pull/1774 we started to track Ranges in the page in order to correctly make them "live". But, without correct lifetime, they would continue to be "live" even if out of scope in JS. This commit adds finalizers to Range via reference counting similar to Events. It _is_ possible for a Range to outlive its page, so we can't just remove the range from the Page's _live_range list - the page might not be valid. This commit gives every page an unique id and the ability to try and get the page by id from the session. By capturing the page_id at creation-time, a Range can defensively remove itself from the page's list. If the page is already gone, then there's no need to do anything. --- src/browser/Factory.zig | 8 +-- src/browser/Page.zig | 3 ++ src/browser/Session.zig | 27 +++++++++- src/browser/webapi/AbstractRange.zig | 32 ++++++++++- src/browser/webapi/Range.zig | 24 +++++++-- src/browser/webapi/Selection.zig | 80 ++++++++++++++++++++-------- src/cdp/domains/accessibility.zig | 4 +- src/cdp/domains/dom.zig | 4 +- src/cdp/domains/network.zig | 2 +- 9 files changed, 147 insertions(+), 37 deletions(-) diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index edb6baee..0d8b87cd 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -265,13 +265,15 @@ pub fn blob(_: *const Factory, arena: Allocator, child: anytype) !*@TypeOf(child return chain.get(1); } -pub fn abstractRange(self: *Factory, child: anytype, page: *Page) !*@TypeOf(child) { - const allocator = self._slab.allocator(); - const chain = try PrototypeChain(&.{ AbstractRange, @TypeOf(child) }).allocate(allocator); +pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, page: *Page) !*@TypeOf(child) { + const chain = try PrototypeChain(&.{ AbstractRange, @TypeOf(child) }).allocate(arena); const doc = page.document.asNode(); const abstract_range = chain.get(0); abstract_range.* = AbstractRange{ + ._rc = 0, + ._arena = arena, + ._page_id = page.id, ._type = unionInit(AbstractRange.Type, chain.get(1)), ._end_offset = 0, ._start_offset = 0, diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 1e8fddeb..57c60071 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -80,6 +80,8 @@ pub const BUF_SIZE = 1024; const Page = @This(); +id: u32, + // This is the "id" of the frame. It can be re-used from page-to-page, e.g. // when navigating. _frame_id: u32, @@ -254,6 +256,7 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void })).asDocument(); self.* = .{ + .id = session.nextPageId(), .js = undefined, .parent = parent, .arena = session.page_arena, diff --git a/src/browser/Session.zig b/src/browser/Session.zig index 529f0847..fea56a87 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -84,6 +84,7 @@ queued_navigation: std.ArrayList(*Page), // about:blank navigations (which may add to queued_navigation). queued_queued_navigation: std.ArrayList(*Page), +page_id_gen: u32, frame_id_gen: u32, pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void { @@ -103,6 +104,7 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi .page_arena = page_arena, .factory = Factory.init(page_arena), .history = .{}, + .page_id_gen = 0, .frame_id_gen = 0, // The prototype (EventTarget) for Navigation is created when a Page is created. .navigation = .{ ._proto = undefined }, @@ -297,9 +299,24 @@ pub const WaitResult = enum { cdp_socket, }; -pub fn findPage(self: *Session, frame_id: u32) ?*Page { +pub fn findPageByFrameId(self: *Session, frame_id: u32) ?*Page { const page = self.currentPage() orelse return null; - return if (page._frame_id == frame_id) page else null; + return findPageBy(page, "_frame_id", frame_id); +} + +pub fn findPageById(self: *Session, id: u32) ?*Page { + const page = self.currentPage() orelse return null; + return findPageBy(page, "id", id); +} + +fn findPageBy(page: *Page, comptime field: []const u8, id: u32) ?*Page { + if (@field(page, field) == id) return page; + for (page.frames.items) |f| { + if (findPageBy(f, field, id)) |found| { + return found; + } + } + return null; } pub fn wait(self: *Session, wait_ms: u32) WaitResult { @@ -636,3 +653,9 @@ pub fn nextFrameId(self: *Session) u32 { self.frame_id_gen = id; return id; } + +pub fn nextPageId(self: *Session) u32 { + const id = self.page_id_gen +% 1; + self.page_id_gen = id; + return id; +} diff --git a/src/browser/webapi/AbstractRange.zig b/src/browser/webapi/AbstractRange.zig index e766ac29..d80fc115 100644 --- a/src/browser/webapi/AbstractRange.zig +++ b/src/browser/webapi/AbstractRange.zig @@ -19,15 +19,22 @@ const std = @import("std"); const js = @import("../js/js.zig"); +const Session = @import("../Session.zig"); + const Node = @import("Node.zig"); const Range = @import("Range.zig"); +const Allocator = std.mem.Allocator; +const IS_DEBUG = @import("builtin").mode == .Debug; + const AbstractRange = @This(); pub const _prototype_root = true; +_rc: u8, _type: Type, - +_page_id: u32, +_arena: Allocator, _end_offset: u32, _start_offset: u32, _end_container: *Node, @@ -36,6 +43,27 @@ _start_container: *Node, // Intrusive linked list node for tracking live ranges on the Page. _range_link: std.DoublyLinkedList.Node = .{}, +pub fn acquireRef(self: *AbstractRange) void { + self._rc += 1; +} + +pub fn deinit(self: *AbstractRange, shutdown: bool, session: *Session) void { + _ = shutdown; + const rc = self._rc; + if (comptime IS_DEBUG) { + std.debug.assert(rc != 0); + } + + if (rc == 1) { + if (session.findPageById(self._page_id)) |page| { + page._live_ranges.remove(&self._range_link); + } + session.releaseArena(self._arena); + return; + } + self._rc = rc - 1; +} + pub const Type = union(enum) { range: *Range, // TODO: static_range: *StaticRange, @@ -310,6 +338,8 @@ pub const JsApi = struct { pub const name = "AbstractRange"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const weak = true; + pub const finalizer = bridge.finalizer(AbstractRange.deinit); }; pub const startContainer = bridge.accessor(AbstractRange.getStartContainer, null, .{}); diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig index 66ce1c93..fd02ce25 100644 --- a/src/browser/webapi/Range.zig +++ b/src/browser/webapi/Range.zig @@ -21,22 +21,31 @@ const String = @import("../../string.zig").String; const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const Session = @import("../Session.zig"); const Node = @import("Node.zig"); const DocumentFragment = @import("DocumentFragment.zig"); const AbstractRange = @import("AbstractRange.zig"); const DOMRect = @import("DOMRect.zig"); +const Allocator = std.mem.Allocator; + const Range = @This(); _proto: *AbstractRange, -pub fn asAbstractRange(self: *Range) *AbstractRange { - return self._proto; +pub fn init(page: *Page) !*Range { + const arena = try page.getArena(.{ .debug = "Range" }); + errdefer page.releaseArena(arena); + return page._factory.abstractRange(arena, Range{ ._proto = undefined }, page); } -pub fn init(page: *Page) !*Range { - return page._factory.abstractRange(Range{ ._proto = undefined }, page); +pub fn deinit(self: *Range, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); +} + +pub fn asAbstractRange(self: *Range) *AbstractRange { + return self._proto; } pub fn setStart(self: *Range, node: *Node, offset: u32) !void { @@ -309,7 +318,10 @@ pub fn intersectsNode(self: *const Range, node: *Node) bool { } pub fn cloneRange(self: *const Range, page: *Page) !*Range { - const clone = try page._factory.abstractRange(Range{ ._proto = undefined }, page); + const arena = try page.getArena(.{ .debug = "Range.clone" }); + errdefer page.releaseArena(arena); + + const clone = try page._factory.abstractRange(arena, Range{ ._proto = undefined }, page); clone._proto._end_offset = self._proto._end_offset; clone._proto._start_offset = self._proto._start_offset; clone._proto._end_container = self._proto._end_container; @@ -687,6 +699,8 @@ pub const JsApi = struct { pub const name = "Range"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const weak = true; + pub const finalizer = bridge.finalizer(Range.deinit); }; // Constants for compareBoundaryPoints diff --git a/src/browser/webapi/Selection.zig b/src/browser/webapi/Selection.zig index a13390a3..7c261823 100644 --- a/src/browser/webapi/Selection.zig +++ b/src/browser/webapi/Selection.zig @@ -21,6 +21,8 @@ const log = @import("../../log.zig"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const Session = @import("../Session.zig"); + const Range = @import("Range.zig"); const AbstractRange = @import("AbstractRange.zig"); const Node = @import("Node.zig"); @@ -37,13 +39,22 @@ _direction: SelectionDirection = .none, pub const init: Selection = .{}; +pub fn deinit(self: *Selection, shutdown: bool, session: *Session) void { + if (self._range) |r| { + r.deinit(shutdown, session); + self._range = null; + } +} + fn dispatchSelectionChangeEvent(page: *Page) !void { const event = try Event.init("selectionchange", .{}, page); try page._event_manager.dispatch(page.document.asEventTarget(), event); } fn isInTree(self: *const Selection) bool { - if (self._range == null) return false; + if (self._range == null) { + return false; + } const anchor_node = self.getAnchorNode() orelse return false; const focus_node = self.getFocusNode() orelse return false; return anchor_node.isConnected() and focus_node.isConnected(); @@ -104,21 +115,33 @@ pub fn getIsCollapsed(self: *const Selection) bool { } pub fn getRangeCount(self: *const Selection) u32 { - if (self._range == null) return 0; - if (!self.isInTree()) return 0; + if (self._range == null) { + return 0; + } + if (!self.isInTree()) { + return 0; + } return 1; } pub fn getType(self: *const Selection) []const u8 { - if (self._range == null) return "None"; - if (!self.isInTree()) return "None"; - if (self.getIsCollapsed()) return "Caret"; + if (self._range == null) { + return "None"; + } + if (!self.isInTree()) { + return "None"; + } + if (self.getIsCollapsed()) { + return "Caret"; + } return "Range"; } pub fn addRange(self: *Selection, range: *Range, page: *Page) !void { - if (self._range != null) return; + if (self._range != null) { + return; + } // Only add the range if its root node is in the document associated with this selection const start_node = range.asAbstractRange().getStartContainer(); @@ -126,22 +149,25 @@ pub fn addRange(self: *Selection, range: *Range, page: *Page) !void { return; } - self._range = range; + self.setRange(range, page); try dispatchSelectionChangeEvent(page); } pub fn removeRange(self: *Selection, range: *Range, page: *Page) !void { - if (self._range == range) { - self._range = null; - try dispatchSelectionChangeEvent(page); - return; - } else { + const existing_range = self._range orelse return error.NotFound; + if (existing_range != range) { return error.NotFound; } + self.setRange(null, page); + try dispatchSelectionChangeEvent(page); } pub fn removeAllRanges(self: *Selection, page: *Page) !void { - self._range = null; + if (self._range == null) { + return; + } + + self.setRange(null, page); self._direction = .none; try dispatchSelectionChangeEvent(page); } @@ -157,7 +183,7 @@ pub fn collapseToEnd(self: *Selection, page: *Page) !void { try new_range.setStart(last_node, last_offset); try new_range.setEnd(last_node, last_offset); - self._range = new_range; + self.setRange(new_range, page); self._direction = .none; try dispatchSelectionChangeEvent(page); } @@ -173,7 +199,7 @@ pub fn collapseToStart(self: *Selection, page: *Page) !void { try new_range.setStart(first_node, first_offset); try new_range.setEnd(first_node, first_offset); - self._range = new_range; + self.setRange(new_range, page); self._direction = .none; try dispatchSelectionChangeEvent(page); } @@ -255,7 +281,7 @@ pub fn extend(self: *Selection, node: *Node, _offset: ?u32, page: *Page) !void { }, } - self._range = new_range; + self.setRange(new_range, page); try dispatchSelectionChangeEvent(page); } @@ -560,7 +586,8 @@ fn applyModify(self: *Selection, alter: ModifyAlter, new_node: *Node, new_offset const new_range = try Range.init(page); try new_range.setStart(new_node, new_offset); try new_range.setEnd(new_node, new_offset); - self._range = new_range; + + self.setRange(new_range, page); self._direction = .none; try dispatchSelectionChangeEvent(page); }, @@ -582,7 +609,7 @@ pub fn selectAllChildren(self: *Selection, parent: *Node, page: *Page) !void { const child_count = parent.getChildrenCount(); try range.setEnd(parent, @intCast(child_count)); - self._range = range; + self.setRange(range, page); self._direction = .forward; try dispatchSelectionChangeEvent(page); } @@ -630,7 +657,7 @@ pub fn setBaseAndExtent( }, } - self._range = range; + self.setRange(range, page); try dispatchSelectionChangeEvent(page); } @@ -656,7 +683,7 @@ pub fn collapse(self: *Selection, _node: ?*Node, _offset: ?u32, page: *Page) !vo try range.setStart(node, offset); try range.setEnd(node, offset); - self._range = range; + self.setRange(range, page); self._direction = .none; try dispatchSelectionChangeEvent(page); } @@ -666,6 +693,16 @@ pub fn toString(self: *const Selection, page: *Page) ![]const u8 { return try range.toString(page); } +fn setRange(self: *Selection, new_range: ?*Range, page: *Page) void { + if (self._range) |existing| { + existing.deinit(false, page._session); + } + if (new_range) |nr| { + nr.asAbstractRange().acquireRef(); + } + self._range = new_range; +} + pub const JsApi = struct { pub const bridge = js.Bridge(Selection); @@ -673,6 +710,7 @@ pub const JsApi = struct { pub const name = "Selection"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const finalizer = bridge.finalizer(Selection.deinit); }; pub const anchorNode = bridge.accessor(Selection.getAnchorNode, null, .{}); diff --git a/src/cdp/domains/accessibility.zig b/src/cdp/domains/accessibility.zig index f8e8df30..864c52b5 100644 --- a/src/cdp/domains/accessibility.zig +++ b/src/cdp/domains/accessibility.zig @@ -53,8 +53,8 @@ fn getFullAXTree(cmd: anytype) !void { const frame_id = params.frameId orelse { break :blk session.currentPage() orelse return error.PageNotLoaded; }; - const page_id = try id.toPageId(.frame_id, frame_id); - break :blk session.findPage(page_id) orelse { + const page_frame_id = try id.toPageId(.frame_id, frame_id); + break :blk session.findPageByFrameId(page_frame_id) orelse { return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{}); }; }; diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index df1d37c2..5150a8e5 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -502,9 +502,9 @@ fn getFrameOwner(cmd: anytype) !void { })) orelse return error.InvalidParams; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const page_id = try id.toPageId(.frame_id, params.frameId); + const page_frame_id = try id.toPageId(.frame_id, params.frameId); - const page = bc.session.findPage(page_id) orelse { + const page = bc.session.findPageByFrameId(page_frame_id) orelse { return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{}); }; diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index a2a36bbe..c04ee33b 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -238,7 +238,7 @@ pub fn httpRequestStart(bc: anytype, msg: *const Notification.RequestStart) !voi const transfer = msg.transfer; const req = &transfer.req; const frame_id = req.frame_id; - const page = bc.session.findPage(frame_id) orelse return; + const page = bc.session.findPageByFrameId(frame_id) orelse return; // Modify request with extra CDP headers for (bc.extra_headers.items) |extra| { From c4097e2b7ef3fd2e779d34864f55b0ccaf79b1c9 Mon Sep 17 00:00:00 2001 From: Nikolay Govorov Date: Thu, 12 Mar 2026 03:55:48 +0000 Subject: [PATCH 3/9] remove dead-code --- src/browser/HttpClient.zig | 40 ++++++++---------------------------- src/cdp/domains/security.zig | 7 +------ src/network/Runtime.zig | 6 ++++++ src/sys/libcurl.zig | 9 -------- 4 files changed, 16 insertions(+), 46 deletions(-) diff --git a/src/browser/HttpClient.zig b/src/browser/HttpClient.zig index 529d4e5a..41b5ea39 100644 --- a/src/browser/HttpClient.zig +++ b/src/browser/HttpClient.zig @@ -648,12 +648,6 @@ fn requestFailed(transfer: *Transfer, err: anyerror, comptime execute_callback: // can be changed at any point in the easy's lifecycle. pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void { try self.ensureNoActiveConnection(); - - var it = self.in_use.first; - while (it) |node| : (it = node.next) { - const conn: *Net.Connection = @fieldParentPtr("node", node); - try conn.setProxy(proxy); - } self.http_proxy = proxy; self.use_proxy = true; } @@ -664,38 +658,20 @@ pub fn restoreOriginalProxy(self: *Client) !void { try self.ensureNoActiveConnection(); self.http_proxy = self.network.config.httpProxy(); - var it = self.in_use.first; - while (it) |node| : (it = node.next) { - const conn: *Net.Connection = @fieldParentPtr("node", node); - try conn.setProxy(self.http_proxy); - } self.use_proxy = self.http_proxy != null; } // Enable TLS verification on all connections. -pub fn enableTlsVerify(self: *Client) !void { +pub fn setTlsVerify(self: *Client, verify: bool) !void { // Remove inflight connections check on enable TLS b/c chromiumoxide calls // the command during navigate and Curl seems to accept it... var it = self.in_use.first; while (it) |node| : (it = node.next) { const conn: *Net.Connection = @fieldParentPtr("node", node); - try conn.setTlsVerify(true, self.use_proxy); + try conn.setTlsVerify(verify, self.use_proxy); } - self.tls_verify = true; -} - -// Disable TLS verification on all connections. -pub fn disableTlsVerify(self: *Client) !void { - // Remove inflight connections check on disable TLS b/c chromiumoxide calls - // the command during navigate and Curl seems to accept it... - - var it = self.in_use.first; - while (it) |node| : (it = node.next) { - const conn: *Net.Connection = @fieldParentPtr("node", node); - try conn.setTlsVerify(false, self.use_proxy); - } - self.tls_verify = false; + self.tls_verify = verify; } fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerror!void { @@ -774,9 +750,12 @@ pub const PerformStatus = enum { }; fn perform(self: *Client, timeout_ms: c_int) !PerformStatus { - self.performing = true; - const running = try self.handles.perform(); - self.performing = false; + const running = blk: { + self.performing = true; + defer self.performing = false; + + break :blk try self.handles.perform(); + }; // Process dirty connections — return them to Runtime pool. while (self.dirty.popFirst()) |node| { @@ -922,7 +901,6 @@ fn removeConn(self: *Client, conn: *Net.Connection) void { } fn releaseConn(self: *Client, conn: *Net.Connection) void { - conn.reset() catch {}; self.network.releaseConnection(conn); } diff --git a/src/cdp/domains/security.zig b/src/cdp/domains/security.zig index 830cd591..0ebfedae 100644 --- a/src/cdp/domains/security.zig +++ b/src/cdp/domains/security.zig @@ -35,12 +35,7 @@ fn setIgnoreCertificateErrors(cmd: anytype) !void { ignore: bool, })) orelse return error.InvalidParams; - if (params.ignore) { - try cmd.cdp.browser.http_client.disableTlsVerify(); - } else { - try cmd.cdp.browser.http_client.enableTlsVerify(); - } - + try cmd.cdp.browser.http_client.setTlsVerify(!params.ignore); return cmd.sendResult(null, .{}); } diff --git a/src/network/Runtime.zig b/src/network/Runtime.zig index 7c534ee2..a88f8e95 100644 --- a/src/network/Runtime.zig +++ b/src/network/Runtime.zig @@ -216,13 +216,19 @@ pub fn stop(self: *Runtime) void { pub fn getConnection(self: *Runtime) ?*net_http.Connection { self.conn_mutex.lock(); defer self.conn_mutex.unlock(); + const node = self.available.popFirst() orelse return null; return @fieldParentPtr("node", node); } pub fn releaseConnection(self: *Runtime, conn: *net_http.Connection) void { + conn.reset() catch |err| { + lp.assert(false, "couldn't reset curl easy", .{ .err = err }); + }; + self.conn_mutex.lock(); defer self.conn_mutex.unlock(); + self.available.append(&conn.node); } diff --git a/src/sys/libcurl.zig b/src/sys/libcurl.zig index f9c9ac12..1a165111 100644 --- a/src/sys/libcurl.zig +++ b/src/sys/libcurl.zig @@ -706,15 +706,6 @@ pub fn curl_multi_poll( try errorMCheck(c.curl_multi_poll(multi, raw_fds, @intCast(extra_fds.len), timeout_ms, numfds)); } -pub fn curl_multi_waitfds(multi: *CurlM, ufds: []CurlWaitFd, fd_count: *c_uint) ErrorMulti!void { - const raw_fds: [*c]c.curl_waitfd = if (ufds.len == 0) null else @ptrCast(ufds.ptr); - try errorMCheck(c.curl_multi_waitfds(multi, raw_fds, @intCast(ufds.len), fd_count)); -} - -pub fn curl_multi_timeout(multi: *CurlM, timeout_ms: *c_long) ErrorMulti!void { - try errorMCheck(c.curl_multi_timeout(multi, timeout_ms)); -} - pub fn curl_multi_info_read(multi: *CurlM, msgs_in_queue: *c_int) ?CurlMsg { const ptr = c.curl_multi_info_read(multi, msgs_in_queue); if (ptr == null) return null; From 1b96087b081be7dcffb4ab7bb08bde8d789b539c Mon Sep 17 00:00:00 2001 From: Nikolay Govorov Date: Thu, 12 Mar 2026 08:50:33 +0000 Subject: [PATCH 4/9] Update zig-v8 --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index b7f9cf3b..9a28408b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.2.tar.gz", - .hash = "v8-0.0.0-xddH6wx-BABNgL7YIDgbnFgKZuXZ68yZNngNSrV6OjrY", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.3.tar.gz", + .hash = "v8-0.0.0-xddH6yx3BAAGD9jSoq_ttt_bk9MectTU44s_HZxxE5LD", }, // .v8 = .{ .path = "../zig-v8-fork" }, .brotli = .{ From 880205e8748333059af11ccd1cadef3314c15038 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 12 Mar 2026 17:53:00 +0800 Subject: [PATCH 5/9] Add dummy getImageData to canvas Probably doesn't solve many (if any) WPT tests, but it moves them further along. --- .../canvas/canvas_rendering_context_2d.html | 35 +++++++++++++++++++ .../tests/canvas/offscreen_canvas.html | 23 ++++++++++++ src/browser/webapi/ImageData.zig | 4 +-- .../canvas/CanvasRenderingContext2D.zig | 20 +++++++++-- .../OffscreenCanvasRenderingContext2D.zig | 20 +++++++++-- 5 files changed, 96 insertions(+), 6 deletions(-) diff --git a/src/browser/tests/canvas/canvas_rendering_context_2d.html b/src/browser/tests/canvas/canvas_rendering_context_2d.html index a6765d88..e065b81f 100644 --- a/src/browser/tests/canvas/canvas_rendering_context_2d.html +++ b/src/browser/tests/canvas/canvas_rendering_context_2d.html @@ -89,6 +89,41 @@ } + + + + + + diff --git a/src/browser/webapi/ImageData.zig b/src/browser/webapi/ImageData.zig index abb5abd5..e25e2dd6 100644 --- a/src/browser/webapi/ImageData.zig +++ b/src/browser/webapi/ImageData.zig @@ -52,7 +52,7 @@ pub const ConstructorSettings = struct { /// ``` /// /// We currently support only the first 2. -pub fn constructor( +pub fn init( width: u32, height: u32, maybe_settings: ?ConstructorSettings, @@ -106,7 +106,7 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; - pub const constructor = bridge.constructor(ImageData.constructor, .{ .dom_exception = true }); + pub const constructor = bridge.constructor(ImageData.init, .{ .dom_exception = true }); pub const colorSpace = bridge.property("srgb", .{ .template = false, .readonly = true }); pub const pixelFormat = bridge.property("rgba-unorm8", .{ .template = false, .readonly = true }); diff --git a/src/browser/webapi/canvas/CanvasRenderingContext2D.zig b/src/browser/webapi/canvas/CanvasRenderingContext2D.zig index 056100ee..a2127e5b 100644 --- a/src/browser/webapi/canvas/CanvasRenderingContext2D.zig +++ b/src/browser/webapi/canvas/CanvasRenderingContext2D.zig @@ -64,15 +64,30 @@ pub fn createImageData( switch (width_or_image_data) { .width => |width| { const height = maybe_height orelse return error.TypeError; - return ImageData.constructor(width, height, maybe_settings, page); + return ImageData.init(width, height, maybe_settings, page); }, .image_data => |image_data| { - return ImageData.constructor(image_data._width, image_data._height, null, page); + return ImageData.init(image_data._width, image_data._height, null, page); }, } } pub fn putImageData(_: *const CanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {} + +pub fn getImageData( + _: *const CanvasRenderingContext2D, + _: i32, // sx + _: i32, // sy + sw: i32, + sh: i32, + page: *Page, +) !*ImageData { + if (sw <= 0 or sh <= 0) { + return error.IndexSizeError; + } + return ImageData.init(@intCast(sw), @intCast(sh), null, page); +} + pub fn save(_: *CanvasRenderingContext2D) void {} pub fn restore(_: *CanvasRenderingContext2D) void {} pub fn scale(_: *CanvasRenderingContext2D, _: f64, _: f64) void {} @@ -125,6 +140,7 @@ pub const JsApi = struct { pub const createImageData = bridge.function(CanvasRenderingContext2D.createImageData, .{ .dom_exception = true }); pub const putImageData = bridge.function(CanvasRenderingContext2D.putImageData, .{ .noop = true }); + pub const getImageData = bridge.function(CanvasRenderingContext2D.getImageData, .{ .dom_exception = true }); pub const save = bridge.function(CanvasRenderingContext2D.save, .{ .noop = true }); pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{ .noop = true }); pub const scale = bridge.function(CanvasRenderingContext2D.scale, .{ .noop = true }); diff --git a/src/browser/webapi/canvas/OffscreenCanvasRenderingContext2D.zig b/src/browser/webapi/canvas/OffscreenCanvasRenderingContext2D.zig index 1a11bd68..faea97f0 100644 --- a/src/browser/webapi/canvas/OffscreenCanvasRenderingContext2D.zig +++ b/src/browser/webapi/canvas/OffscreenCanvasRenderingContext2D.zig @@ -63,15 +63,30 @@ pub fn createImageData( switch (width_or_image_data) { .width => |width| { const height = maybe_height orelse return error.TypeError; - return ImageData.constructor(width, height, maybe_settings, page); + return ImageData.init(width, height, maybe_settings, page); }, .image_data => |image_data| { - return ImageData.constructor(image_data._width, image_data._height, null, page); + return ImageData.init(image_data._width, image_data._height, null, page); }, } } pub fn putImageData(_: *const OffscreenCanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {} + +pub fn getImageData( + _: *const OffscreenCanvasRenderingContext2D, + _: i32, // sx + _: i32, // sy + sw: i32, + sh: i32, + page: *Page, +) !*ImageData { + if (sw <= 0 or sh <= 0) { + return error.IndexSizeError; + } + return ImageData.init(@intCast(sw), @intCast(sh), null, page); +} + pub fn save(_: *OffscreenCanvasRenderingContext2D) void {} pub fn restore(_: *OffscreenCanvasRenderingContext2D) void {} pub fn scale(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) void {} @@ -124,6 +139,7 @@ pub const JsApi = struct { pub const createImageData = bridge.function(OffscreenCanvasRenderingContext2D.createImageData, .{ .dom_exception = true }); pub const putImageData = bridge.function(OffscreenCanvasRenderingContext2D.putImageData, .{ .noop = true }); + pub const getImageData = bridge.function(OffscreenCanvasRenderingContext2D.getImageData, .{ .dom_exception = true }); pub const save = bridge.function(OffscreenCanvasRenderingContext2D.save, .{ .noop = true }); pub const restore = bridge.function(OffscreenCanvasRenderingContext2D.restore, .{ .noop = true }); pub const scale = bridge.function(OffscreenCanvasRenderingContext2D.scale, .{ .noop = true }); From 84e1cd08b67bbe9df06626e46e5804609eead7c6 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 12 Mar 2026 11:54:06 +0100 Subject: [PATCH 6/9] update zig-v8 in dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e30fbdbe..f106905a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM debian:stable-slim ARG MINISIG=0.12 ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG V8=14.0.365.4 -ARG ZIG_V8=v0.3.2 +ARG ZIG_V8=v0.3.3 ARG TARGETPLATFORM RUN apt-get update -yq && \ From e4e21f52b5d3b49e5c377a74f668a62a18aa10b9 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 12 Mar 2026 18:58:10 +0800 Subject: [PATCH 7/9] Allow navigation from a blob URL. These are used a lot in WPT test. --- src/browser/Page.zig | 49 +++++++++++++++++++++++++------- src/browser/URL.zig | 5 ++++ src/browser/tests/page/blob.html | 41 ++++++++++++++++++++++++++ src/browser/webapi/URL.zig | 8 ++++-- 4 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 src/browser/tests/page/blob.html diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 1e8fddeb..028d1fc1 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -404,6 +404,18 @@ pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool { return std.mem.startsWith(u8, url, current_origin); } +/// Look up a blob URL in this page's registry, walking up the parent chain. +pub fn lookupBlobUrl(self: *Page, url: []const u8) ?*Blob { + var current: ?*Page = self; + while (current) |page| { + if (page._blob_urls.get(url)) |blob| { + return blob; + } + current = page.parent; + } + return null; +} + pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void { lp.assert(self._load_state == .waiting, "page.renavigate", .{}); const session = self._session; @@ -419,12 +431,17 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi .type = self._type, }); - // if the url is about:blank, we load an empty HTML document in the - // page and dispatch the events. - if (std.mem.eql(u8, "about:blank", request_url)) { - self.url = "about:blank"; + // Handle synthetic navigations: about:blank and blob: URLs + const is_about_blank = std.mem.eql(u8, "about:blank", request_url); + const is_blob = !is_about_blank and std.mem.startsWith(u8, request_url, "blob:"); - if (self.parent) |parent| { + if (is_about_blank or is_blob) { + self.url = if (is_about_blank) "about:blank" else try self.arena.dupeZ(u8, request_url); + + if (is_blob) { + // strip out blob: + self.origin = try URL.getOrigin(self.arena, request_url[5.. :0]); + } else if (self.parent) |parent| { self.origin = parent.origin; } else { self.origin = null; @@ -435,10 +452,22 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi // It's important to force a reset during the following navigation. self._parse_state = .complete; - self.document.injectBlank(self) catch |err| { - log.err(.browser, "inject blank", .{ .err = err }); - return error.InjectBlankFailed; - }; + // Content injection + if (is_blob) { + const blob = self.lookupBlobUrl(request_url) orelse { + log.warn(.js, "invalid blob", .{ .url = request_url }); + return error.BlobNotFound; + }; + const parse_arena = try self.getArena(.{ .debug = "Page.parseBlob" }); + defer self.releaseArena(parse_arena); + var parser = Parser.init(parse_arena, self.document.asNode(), self); + parser.parse(blob._slice); + } else { + self.document.injectBlank(self) catch |err| { + log.err(.browser, "inject blank", .{ .err = err }); + return error.InjectBlankFailed; + }; + } self.documentIsComplete(); session.notification.dispatch(.page_navigate, &.{ @@ -452,7 +481,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi // Record telemetry for navigation session.browser.app.telemetry.record(.{ .navigate = .{ - .tls = false, // about:blank is not TLS + .tls = false, // about:blank and blob: are not TLS .proxy = session.browser.app.config.httpProxy() != null, }, }); diff --git a/src/browser/URL.zig b/src/browser/URL.zig index 19b87333..f8d7dd90 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -277,6 +277,11 @@ pub fn isCompleteHTTPUrl(url: []const u8) bool { return false; } + // blob: and data: URLs are complete but don't follow scheme:// pattern + if (std.mem.startsWith(u8, url, "blob:") or std.mem.startsWith(u8, url, "data:")) { + return true; + } + // Check if there's a scheme (protocol) ending with :// const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false; diff --git a/src/browser/tests/page/blob.html b/src/browser/tests/page/blob.html new file mode 100644 index 00000000..434c1ce6 --- /dev/null +++ b/src/browser/tests/page/blob.html @@ -0,0 +1,41 @@ + + + + + + + diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index fda7d2a5..e9263319 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -249,6 +249,8 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 { .{ page.origin orelse "null", uuid_buf }, ); try page._blob_urls.put(page.arena, blob_url, blob); + // prevent GC from cleaning up the blob while it's in the registry + page.js.strongRef(blob); return blob_url; } @@ -258,8 +260,10 @@ pub fn revokeObjectURL(url: []const u8, page: *Page) void { return; } - // Remove from registry (no-op if not found) - _ = page._blob_urls.remove(url); + // Remove from registry and release strong ref (no-op if not found) + if (page._blob_urls.fetchRemove(url)) |entry| { + page.js.weakRef(entry.value); + } } pub const JsApi = struct { From 51e90f5971d206078bfbe27f697fc3aa2d8f70c3 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 12 Mar 2026 18:36:28 +0100 Subject: [PATCH 8/9] parse cookies on redirection during header callback THe change to handle bot `\n` and `\r\n` for end HTTP headers skip the cookie parsing in case of redirection. --- src/browser/HttpClient.zig | 56 +++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/src/browser/HttpClient.zig b/src/browser/HttpClient.zig index 41b5ea39..f716904c 100644 --- a/src/browser/HttpClient.zig +++ b/src/browser/HttpClient.zig @@ -1286,6 +1286,16 @@ pub const Transfer = struct { if (buf_len < 3) { // could be \r\n or \n. + // We get the last header line. + if (transfer._redirecting) { + // parse and set cookies for the redirection. + redirectionCookies(transfer, &conn) catch |err| { + if (comptime IS_DEBUG) { + log.debug(.http, "redirection cookies", .{ .err = err }); + } + return 0; + }; + } return buf_len; } @@ -1352,38 +1362,22 @@ pub const Transfer = struct { transfer.bytes_received += buf_len; } - if (buf_len > 2) { - if (transfer._auth_challenge != null) { - // try to parse auth challenge. - if (std.ascii.startsWithIgnoreCase(header, "WWW-Authenticate") or - std.ascii.startsWithIgnoreCase(header, "Proxy-Authenticate")) - { - const ac = AuthChallenge.parse( - transfer._auth_challenge.?.status, - header, - ) catch |err| { - // We can't parse the auth challenge - log.err(.http, "parse auth challenge", .{ .err = err, .header = header }); - // Should we cancel the request? I don't think so. - return buf_len; - }; - transfer._auth_challenge = ac; - } + if (transfer._auth_challenge != null) { + // try to parse auth challenge. + if (std.ascii.startsWithIgnoreCase(header, "WWW-Authenticate") or + std.ascii.startsWithIgnoreCase(header, "Proxy-Authenticate")) + { + const ac = AuthChallenge.parse( + transfer._auth_challenge.?.status, + header, + ) catch |err| { + // We can't parse the auth challenge + log.err(.http, "parse auth challenge", .{ .err = err, .header = header }); + // Should we cancel the request? I don't think so. + return buf_len; + }; + transfer._auth_challenge = ac; } - return buf_len; - } - - // Starting here, we get the last header line. - - if (transfer._redirecting) { - // parse and set cookies for the redirection. - redirectionCookies(transfer, &conn) catch |err| { - if (comptime IS_DEBUG) { - log.debug(.http, "redirection cookies", .{ .err = err }); - } - return 0; - }; - return buf_len; } return buf_len; From f09e66e1ccc2c8a87d580489c6e5d7d7457e2e8e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 13 Mar 2026 07:15:23 +0800 Subject: [PATCH 9/9] update action.yml to latest zig-v8 --- .github/actions/install/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index d9d7b803..4c0a28f9 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -13,7 +13,7 @@ inputs: zig-v8: description: 'zig v8 version to install' required: false - default: 'v0.3.2' + default: 'v0.3.3' v8: description: 'v8 version to install' required: false