Merge branch 'main' into css-improvements

This commit is contained in:
Adrià Arrufat
2026-03-13 10:07:06 +09:00
25 changed files with 504 additions and 236 deletions

View File

@@ -13,7 +13,7 @@ inputs:
zig-v8: zig-v8:
description: 'zig v8 version to install' description: 'zig v8 version to install'
required: false required: false
default: 'v0.3.2' default: 'v0.3.3'
v8: v8:
description: 'v8 version to install' description: 'v8 version to install'
required: false required: false

View File

@@ -3,7 +3,7 @@ FROM debian:stable-slim
ARG MINISIG=0.12 ARG MINISIG=0.12
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.0.365.4 ARG V8=14.0.365.4
ARG ZIG_V8=v0.3.2 ARG ZIG_V8=v0.3.3
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN apt-get update -yq && \ RUN apt-get update -yq && \

View File

@@ -5,8 +5,8 @@
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.15.2",
.dependencies = .{ .dependencies = .{
.v8 = .{ .v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.2.tar.gz", .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.3.tar.gz",
.hash = "v8-0.0.0-xddH6wx-BABNgL7YIDgbnFgKZuXZ68yZNngNSrV6OjrY", .hash = "v8-0.0.0-xddH6yx3BAAGD9jSoq_ttt_bk9MectTU44s_HZxxE5LD",
}, },
// .v8 = .{ .path = "../zig-v8-fork" }, // .v8 = .{ .path = "../zig-v8-fork" },
.brotli = .{ .brotli = .{

View File

@@ -265,13 +265,15 @@ pub fn blob(_: *const Factory, arena: Allocator, child: anytype) !*@TypeOf(child
return chain.get(1); return chain.get(1);
} }
pub fn abstractRange(self: *Factory, child: anytype, page: *Page) !*@TypeOf(child) { pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, page: *Page) !*@TypeOf(child) {
const allocator = self._slab.allocator(); const chain = try PrototypeChain(&.{ AbstractRange, @TypeOf(child) }).allocate(arena);
const chain = try PrototypeChain(&.{ AbstractRange, @TypeOf(child) }).allocate(allocator);
const doc = page.document.asNode(); const doc = page.document.asNode();
const abstract_range = chain.get(0); const abstract_range = chain.get(0);
abstract_range.* = AbstractRange{ abstract_range.* = AbstractRange{
._rc = 0,
._arena = arena,
._page_id = page.id,
._type = unionInit(AbstractRange.Type, chain.get(1)), ._type = unionInit(AbstractRange.Type, chain.get(1)),
._end_offset = 0, ._end_offset = 0,
._start_offset = 0, ._start_offset = 0,

View File

@@ -66,9 +66,18 @@ active: usize,
// 'networkAlmostIdle' Page.lifecycleEvent in CDP). // 'networkAlmostIdle' Page.lifecycleEvent in CDP).
intercepted: usize, intercepted: usize,
// Our easy handles, managed by a curl multi. // Our curl multi handle.
handles: Net.Handles, 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 // Use to generate the next request ID
next_request_id: u32 = 0, 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. // request. These wil come and go with each request.
transfer_pool: std.heap.MemoryPool(Transfer), transfer_pool: std.heap.MemoryPool(Transfer),
// only needed for CDP which can change the proxy and then restore it. When // The current proxy. CDP can change it, restoreOriginalProxy restores
// restoring, this originally-configured value is what it goes to. // from config.
http_proxy: ?[:0]const u8 = null, http_proxy: ?[:0]const u8 = null,
// track if the client use a proxy for connections. // track if the client use a proxy for connections.
@@ -97,6 +106,9 @@ http_proxy: ?[:0]const u8 = null,
// CDP. // CDP.
use_proxy: bool, use_proxy: bool,
// Current TLS verification state, applied per-connection in makeRequest.
tls_verify: bool = true,
cdp_client: ?CDPClient = null, cdp_client: ?CDPClient = null,
// libcurl can monitor arbitrary sockets, this lets us use libcurl to poll // 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); const client = try allocator.create(Client);
errdefer allocator.destroy(client); errdefer allocator.destroy(client);
var handles = try Net.Handles.init(allocator, network.ca_blob, network.config); var handles = try Net.Handles.init(network.config);
errdefer handles.deinit(allocator); errdefer handles.deinit();
// Set transfer callbacks on each connection.
for (handles.connections) |*conn| {
try conn.setCallbacks(Transfer.headerCallback, Transfer.dataCallback);
}
const http_proxy = network.config.httpProxy(); const http_proxy = network.config.httpProxy();
@@ -145,6 +152,7 @@ pub fn init(allocator: Allocator, network: *Network) !*Client {
.network = network, .network = network,
.http_proxy = http_proxy, .http_proxy = http_proxy,
.use_proxy = http_proxy != null, .use_proxy = http_proxy != null,
.tls_verify = network.config.tlsVerifyHost(),
.transfer_pool = transfer_pool, .transfer_pool = transfer_pool,
}; };
@@ -153,7 +161,7 @@ pub fn init(allocator: Allocator, network: *Network) !*Client {
pub fn deinit(self: *Client) void { pub fn deinit(self: *Client) void {
self.abort(); self.abort();
self.handles.deinit(self.allocator); self.handles.deinit();
self.transfer_pool.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. // but abort can avoid the frame_id check at comptime.
fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void { 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; var n = q.first;
while (n) |node| { while (n) |node| {
n = node.next; n = node.next;
const conn: *Net.Connection = @fieldParentPtr("node", node); const conn: *Net.Connection = @fieldParentPtr("node", node);
var transfer = Transfer.fromConnection(conn) catch |err| { var transfer = Transfer.fromConnection(conn) catch |err| {
// Let's cleanup what we can // Let's cleanup what we can
self.handles.remove(conn); self.removeConn(conn);
log.err(.http, "get private info", .{ .err = err, .source = "abort" }); log.err(.http, "get private info", .{ .err = err, .source = "abort" });
continue; continue;
}; };
@@ -226,8 +234,7 @@ fn _abort(self: *Client, comptime abort_all: bool, frame_id: u32) void {
} }
if (comptime IS_DEBUG and abort_all) { if (comptime IS_DEBUG and abort_all) {
std.debug.assert(self.handles.in_use.first == null); std.debug.assert(self.in_use.first == null);
std.debug.assert(self.handles.available.len() == self.handles.connections.len);
const running = self.handles.perform() catch |err| { const running = self.handles.perform() catch |err| {
lp.assert(false, "multi perform in abort", .{ .err = 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 { pub fn tick(self: *Client, timeout_ms: u32) !PerformStatus {
while (true) { while (self.queue.popFirst()) |queue_node| {
if (self.handles.hasAvailable() == false) { const conn = self.network.getConnection() orelse {
self.queue.prepend(queue_node);
break; break;
} };
const queue_node = self.queue.popFirst() orelse break;
const transfer: *Transfer = @fieldParentPtr("_node", queue_node); 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); try self.makeRequest(conn, transfer);
} }
return self.perform(@intCast(timeout_ms)); return self.perform(@intCast(timeout_ms));
@@ -529,8 +533,8 @@ fn waitForInterceptedResponse(self: *Client, transfer: *Transfer) !bool {
fn process(self: *Client, transfer: *Transfer) !void { fn process(self: *Client, transfer: *Transfer) !void {
// libcurl doesn't allow recursive calls, if we're in a `perform()` operation // libcurl doesn't allow recursive calls, if we're in a `perform()` operation
// then we _have_ to queue this. // then we _have_ to queue this.
if (self.handles.performing == false) { if (self.performing == false) {
if (self.handles.get()) |conn| { if (self.network.getConnection()) |conn| {
return self.makeRequest(conn, transfer); return self.makeRequest(conn, transfer);
} }
} }
@@ -644,10 +648,7 @@ fn requestFailed(transfer: *Transfer, err: anyerror, comptime execute_callback:
// can be changed at any point in the easy's lifecycle. // can be changed at any point in the easy's lifecycle.
pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void { pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void {
try self.ensureNoActiveConnection(); try self.ensureNoActiveConnection();
self.http_proxy = proxy;
for (self.handles.connections) |*conn| {
try conn.setProxy(proxy.ptr);
}
self.use_proxy = true; self.use_proxy = true;
} }
@@ -656,31 +657,21 @@ pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void {
pub fn restoreOriginalProxy(self: *Client) !void { pub fn restoreOriginalProxy(self: *Client) !void {
try self.ensureNoActiveConnection(); try self.ensureNoActiveConnection();
const proxy = if (self.http_proxy) |p| p.ptr else null; self.http_proxy = self.network.config.httpProxy();
for (self.handles.connections) |*conn| { self.use_proxy = self.http_proxy != null;
try conn.setProxy(proxy);
}
self.use_proxy = proxy != null;
} }
// Enable TLS verification on all connections. // 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 // Remove inflight connections check on enable TLS b/c chromiumoxide calls
// the command during navigate and Curl seems to accept it... // the command during navigate and Curl seems to accept it...
for (self.handles.connections) |*conn| { var it = self.in_use.first;
try conn.setTlsVerify(true, self.use_proxy); while (it) |node| : (it = node.next) {
} const conn: *Net.Connection = @fieldParentPtr("node", node);
} try conn.setTlsVerify(verify, self.use_proxy);
// 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...
for (self.handles.connections) |*conn| {
try conn.setTlsVerify(false, self.use_proxy);
} }
self.tls_verify = verify;
} }
fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerror!void { fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerror!void {
@@ -691,9 +682,14 @@ fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerr
errdefer { errdefer {
transfer._conn = null; transfer._conn = null;
transfer.deinit(); 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.setURL(req.url);
try conn.setMethod(req.method); try conn.setMethod(req.method);
if (req.body) |b| { if (req.body) |b| {
@@ -728,10 +724,12 @@ fn makeRequest(self: *Client, conn: *Net.Connection, transfer: *Transfer) anyerr
// fails BEFORE `curl_multi_add_handle` succeeds, the we still need to do // 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 // cleanup. But if things fail after `curl_multi_add_handle`, we expect
// perfom to pickup the failure and cleanup. // perfom to pickup the failure and cleanup.
self.in_use.append(&conn.node);
self.handles.add(conn) catch |err| { self.handles.add(conn) catch |err| {
transfer._conn = null; transfer._conn = null;
transfer.deinit(); transfer.deinit();
self.handles.isAvailable(conn); self.in_use.remove(&conn.node);
self.releaseConn(conn);
return err; return err;
}; };
@@ -752,7 +750,22 @@ pub const PerformStatus = enum {
}; };
fn perform(self: *Client, timeout_ms: c_int) !PerformStatus { fn perform(self: *Client, timeout_ms: c_int) !PerformStatus {
const running = try self.handles.perform(); 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| {
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 // We're potentially going to block for a while until we get data. Process
// whatever messages we have waiting ahead of time. // whatever messages we have waiting ahead of time.
@@ -871,11 +884,26 @@ fn processMessages(self: *Client) !bool {
fn endTransfer(self: *Client, transfer: *Transfer) void { fn endTransfer(self: *Client, transfer: *Transfer) void {
const conn = transfer._conn.?; const conn = transfer._conn.?;
self.handles.remove(conn); self.removeConn(conn);
transfer._conn = null; transfer._conn = null;
self.active -= 1; 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 {
self.network.releaseConnection(conn);
}
fn ensureNoActiveConnection(self: *const Client) !void { fn ensureNoActiveConnection(self: *const Client) !void {
if (self.active > 0) { if (self.active > 0) {
return error.InflightConnection; return error.InflightConnection;
@@ -1023,7 +1051,7 @@ pub const Transfer = struct {
fn deinit(self: *Transfer) void { fn deinit(self: *Transfer) void {
self.req.headers.deinit(); self.req.headers.deinit();
if (self._conn) |conn| { if (self._conn) |conn| {
self.client.handles.remove(conn); self.client.removeConn(conn);
} }
self.arena.deinit(); self.arena.deinit();
self.client.transfer_pool.destroy(self); self.client.transfer_pool.destroy(self);
@@ -1093,7 +1121,7 @@ pub const Transfer = struct {
requestFailed(self, err, true); requestFailed(self, err, true);
const client = self.client; 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 // 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 // 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 // from a curl callback. Instead, we flag this transfer and all of
@@ -1258,6 +1286,16 @@ pub const Transfer = struct {
if (buf_len < 3) { if (buf_len < 3) {
// could be \r\n or \n. // 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; return buf_len;
} }
@@ -1324,38 +1362,22 @@ pub const Transfer = struct {
transfer.bytes_received += buf_len; transfer.bytes_received += buf_len;
} }
if (buf_len > 2) { if (transfer._auth_challenge != null) {
if (transfer._auth_challenge != null) { // try to parse auth challenge.
// try to parse auth challenge. if (std.ascii.startsWithIgnoreCase(header, "WWW-Authenticate") or
if (std.ascii.startsWithIgnoreCase(header, "WWW-Authenticate") or std.ascii.startsWithIgnoreCase(header, "Proxy-Authenticate"))
std.ascii.startsWithIgnoreCase(header, "Proxy-Authenticate")) {
{ const ac = AuthChallenge.parse(
const ac = AuthChallenge.parse( transfer._auth_challenge.?.status,
transfer._auth_challenge.?.status, header,
header, ) catch |err| {
) catch |err| { // We can't parse the auth challenge
// We can't parse the auth challenge log.err(.http, "parse auth challenge", .{ .err = err, .header = header });
log.err(.http, "parse auth challenge", .{ .err = err, .header = header }); // Should we cancel the request? I don't think so.
// Should we cancel the request? I don't think so. return buf_len;
return buf_len; };
}; transfer._auth_challenge = ac;
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; return buf_len;

View File

@@ -80,6 +80,8 @@ pub const BUF_SIZE = 1024;
const Page = @This(); const Page = @This();
id: u32,
// This is the "id" of the frame. It can be re-used from page-to-page, e.g. // This is the "id" of the frame. It can be re-used from page-to-page, e.g.
// when navigating. // when navigating.
_frame_id: u32, _frame_id: u32,
@@ -254,6 +256,7 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
})).asDocument(); })).asDocument();
self.* = .{ self.* = .{
.id = session.nextPageId(),
.js = undefined, .js = undefined,
.parent = parent, .parent = parent,
.arena = session.page_arena, .arena = session.page_arena,
@@ -404,6 +407,18 @@ pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
return std.mem.startsWith(u8, url, current_origin); 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 { pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {
lp.assert(self._load_state == .waiting, "page.renavigate", .{}); lp.assert(self._load_state == .waiting, "page.renavigate", .{});
const session = self._session; const session = self._session;
@@ -419,12 +434,17 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
.type = self._type, .type = self._type,
}); });
// if the url is about:blank, we load an empty HTML document in the // Handle synthetic navigations: about:blank and blob: URLs
// page and dispatch the events. const is_about_blank = std.mem.eql(u8, "about:blank", request_url);
if (std.mem.eql(u8, "about:blank", request_url)) { const is_blob = !is_about_blank and std.mem.startsWith(u8, request_url, "blob:");
self.url = "about:blank";
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; self.origin = parent.origin;
} else { } else {
self.origin = null; self.origin = null;
@@ -435,10 +455,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. // It's important to force a reset during the following navigation.
self._parse_state = .complete; self._parse_state = .complete;
self.document.injectBlank(self) catch |err| { // Content injection
log.err(.browser, "inject blank", .{ .err = err }); if (is_blob) {
return error.InjectBlankFailed; 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(); self.documentIsComplete();
session.notification.dispatch(.page_navigate, &.{ session.notification.dispatch(.page_navigate, &.{
@@ -452,7 +484,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
// Record telemetry for navigation // Record telemetry for navigation
session.browser.app.telemetry.record(.{ session.browser.app.telemetry.record(.{
.navigate = .{ .navigate = .{
.tls = false, // about:blank is not TLS .tls = false, // about:blank and blob: are not TLS
.proxy = session.browser.app.config.httpProxy() != null, .proxy = session.browser.app.config.httpProxy() != null,
}, },
}); });

View File

@@ -84,6 +84,7 @@ queued_navigation: std.ArrayList(*Page),
// about:blank navigations (which may add to queued_navigation). // about:blank navigations (which may add to queued_navigation).
queued_queued_navigation: std.ArrayList(*Page), queued_queued_navigation: std.ArrayList(*Page),
page_id_gen: u32,
frame_id_gen: u32, frame_id_gen: u32,
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void { 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, .page_arena = page_arena,
.factory = Factory.init(page_arena), .factory = Factory.init(page_arena),
.history = .{}, .history = .{},
.page_id_gen = 0,
.frame_id_gen = 0, .frame_id_gen = 0,
// The prototype (EventTarget) for Navigation is created when a Page is created. // The prototype (EventTarget) for Navigation is created when a Page is created.
.navigation = .{ ._proto = undefined }, .navigation = .{ ._proto = undefined },
@@ -297,9 +299,24 @@ pub const WaitResult = enum {
cdp_socket, 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; 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 { pub fn wait(self: *Session, wait_ms: u32) WaitResult {
@@ -636,3 +653,9 @@ pub fn nextFrameId(self: *Session) u32 {
self.frame_id_gen = id; self.frame_id_gen = id;
return id; return id;
} }
pub fn nextPageId(self: *Session) u32 {
const id = self.page_id_gen +% 1;
self.page_id_gen = id;
return id;
}

View File

@@ -277,6 +277,11 @@ pub fn isCompleteHTTPUrl(url: []const u8) bool {
return false; 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 :// // Check if there's a scheme (protocol) ending with ://
const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false; const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false;

View File

@@ -89,6 +89,41 @@
} }
</script> </script>
<script id="CanvasRenderingContext2D#getImageData">
{
const element = document.createElement("canvas");
element.width = 100;
element.height = 50;
const ctx = element.getContext("2d");
const imageData = ctx.getImageData(0, 0, 10, 20);
testing.expectEqual(true, imageData instanceof ImageData);
testing.expectEqual(imageData.width, 10);
testing.expectEqual(imageData.height, 20);
testing.expectEqual(imageData.data.length, 10 * 20 * 4);
testing.expectEqual(true, imageData.data instanceof Uint8ClampedArray);
// Undrawn canvas should return transparent black pixels.
testing.expectEqual(imageData.data[0], 0);
testing.expectEqual(imageData.data[1], 0);
testing.expectEqual(imageData.data[2], 0);
testing.expectEqual(imageData.data[3], 0);
}
</script>
<script id="CanvasRenderingContext2D#getImageData invalid">
{
const element = document.createElement("canvas");
const ctx = element.getContext("2d");
// Zero or negative width/height should throw IndexSizeError.
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 0, 10));
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 10, 0));
testing.expectError('Index or size', () => ctx.getImageData(0, 0, -5, 10));
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 10, -5));
}
</script>
<script id="getter"> <script id="getter">
{ {

View File

@@ -62,3 +62,26 @@
testing.expectEqual(offscreen.height, 96); testing.expectEqual(offscreen.height, 96);
} }
</script> </script>
<script id=OffscreenCanvasRenderingContext2D#getImageData>
{
const canvas = new OffscreenCanvas(100, 50);
const ctx = canvas.getContext("2d");
const imageData = ctx.getImageData(0, 0, 10, 20);
testing.expectEqual(true, imageData instanceof ImageData);
testing.expectEqual(imageData.width, 10);
testing.expectEqual(imageData.height, 20);
testing.expectEqual(imageData.data.length, 10 * 20 * 4);
// Undrawn canvas should return transparent black pixels.
testing.expectEqual(imageData.data[0], 0);
testing.expectEqual(imageData.data[1], 0);
testing.expectEqual(imageData.data[2], 0);
testing.expectEqual(imageData.data[3], 0);
// Zero or negative dimensions should throw.
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 0, 10));
testing.expectError('Index or size', () => ctx.getImageData(0, 0, 10, -5));
}
</script>

View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<body></body>
<script src="../testing.js"></script>
<script id="basic_blob_navigation">
{
const html = '<html><head></head><body><div id="test">Hello Blob</div></body></html>';
const blob = new Blob([html], { type: 'text/html' });
const blob_url = URL.createObjectURL(blob);
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
iframe.src = blob_url;
testing.eventually(() => {
testing.expectEqual('Hello Blob', iframe.contentDocument.getElementById('test').textContent);
});
}
</script>
<script id="multiple_blobs">
{
const blob1 = new Blob(['<html><body>First</body></html>'], { type: 'text/html' });
const blob2 = new Blob(['<html><body>Second</body></html>'], { type: 'text/html' });
const url1 = URL.createObjectURL(blob1);
const url2 = URL.createObjectURL(blob2);
const iframe1 = document.createElement('iframe');
document.body.appendChild(iframe1);
iframe1.src = url1;
const iframe2 = document.createElement('iframe');
document.body.appendChild(iframe2);
iframe2.src = url2;
testing.eventually(() => {
testing.expectEqual('First', iframe1.contentDocument.body.textContent);
testing.expectEqual('Second', iframe2.contentDocument.body.textContent);
});
}
</script>

View File

@@ -19,15 +19,22 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Session = @import("../Session.zig");
const Node = @import("Node.zig"); const Node = @import("Node.zig");
const Range = @import("Range.zig"); const Range = @import("Range.zig");
const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const AbstractRange = @This(); const AbstractRange = @This();
pub const _prototype_root = true; pub const _prototype_root = true;
_rc: u8,
_type: Type, _type: Type,
_page_id: u32,
_arena: Allocator,
_end_offset: u32, _end_offset: u32,
_start_offset: u32, _start_offset: u32,
_end_container: *Node, _end_container: *Node,
@@ -36,6 +43,27 @@ _start_container: *Node,
// Intrusive linked list node for tracking live ranges on the Page. // Intrusive linked list node for tracking live ranges on the Page.
_range_link: std.DoublyLinkedList.Node = .{}, _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) { pub const Type = union(enum) {
range: *Range, range: *Range,
// TODO: static_range: *StaticRange, // TODO: static_range: *StaticRange,
@@ -310,6 +338,8 @@ pub const JsApi = struct {
pub const name = "AbstractRange"; pub const name = "AbstractRange";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; 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, .{}); pub const startContainer = bridge.accessor(AbstractRange.getStartContainer, null, .{});

View File

@@ -52,7 +52,7 @@ pub const ConstructorSettings = struct {
/// ``` /// ```
/// ///
/// We currently support only the first 2. /// We currently support only the first 2.
pub fn constructor( pub fn init(
width: u32, width: u32,
height: u32, height: u32,
maybe_settings: ?ConstructorSettings, maybe_settings: ?ConstructorSettings,
@@ -106,7 +106,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined; 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 colorSpace = bridge.property("srgb", .{ .template = false, .readonly = true });
pub const pixelFormat = bridge.property("rgba-unorm8", .{ .template = false, .readonly = true }); pub const pixelFormat = bridge.property("rgba-unorm8", .{ .template = false, .readonly = true });

View File

@@ -21,22 +21,31 @@ const String = @import("../../string.zig").String;
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const Node = @import("Node.zig"); const Node = @import("Node.zig");
const DocumentFragment = @import("DocumentFragment.zig"); const DocumentFragment = @import("DocumentFragment.zig");
const AbstractRange = @import("AbstractRange.zig"); const AbstractRange = @import("AbstractRange.zig");
const DOMRect = @import("DOMRect.zig"); const DOMRect = @import("DOMRect.zig");
const Allocator = std.mem.Allocator;
const Range = @This(); const Range = @This();
_proto: *AbstractRange, _proto: *AbstractRange,
pub fn asAbstractRange(self: *Range) *AbstractRange { pub fn init(page: *Page) !*Range {
return self._proto; 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 { pub fn deinit(self: *Range, shutdown: bool, session: *Session) void {
return page._factory.abstractRange(Range{ ._proto = undefined }, page); self._proto.deinit(shutdown, session);
}
pub fn asAbstractRange(self: *Range) *AbstractRange {
return self._proto;
} }
pub fn setStart(self: *Range, node: *Node, offset: u32) !void { 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 { 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._end_offset = self._proto._end_offset;
clone._proto._start_offset = self._proto._start_offset; clone._proto._start_offset = self._proto._start_offset;
clone._proto._end_container = self._proto._end_container; clone._proto._end_container = self._proto._end_container;
@@ -687,6 +699,8 @@ pub const JsApi = struct {
pub const name = "Range"; pub const name = "Range";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Range.deinit);
}; };
// Constants for compareBoundaryPoints // Constants for compareBoundaryPoints

View File

@@ -21,6 +21,8 @@ const log = @import("../../log.zig");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const Range = @import("Range.zig"); const Range = @import("Range.zig");
const AbstractRange = @import("AbstractRange.zig"); const AbstractRange = @import("AbstractRange.zig");
const Node = @import("Node.zig"); const Node = @import("Node.zig");
@@ -37,13 +39,22 @@ _direction: SelectionDirection = .none,
pub const init: Selection = .{}; 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 { fn dispatchSelectionChangeEvent(page: *Page) !void {
const event = try Event.init("selectionchange", .{}, page); const event = try Event.init("selectionchange", .{}, page);
try page._event_manager.dispatch(page.document.asEventTarget(), event); try page._event_manager.dispatch(page.document.asEventTarget(), event);
} }
fn isInTree(self: *const Selection) bool { 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 anchor_node = self.getAnchorNode() orelse return false;
const focus_node = self.getFocusNode() orelse return false; const focus_node = self.getFocusNode() orelse return false;
return anchor_node.isConnected() and focus_node.isConnected(); 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 { pub fn getRangeCount(self: *const Selection) u32 {
if (self._range == null) return 0; if (self._range == null) {
if (!self.isInTree()) return 0; return 0;
}
if (!self.isInTree()) {
return 0;
}
return 1; return 1;
} }
pub fn getType(self: *const Selection) []const u8 { pub fn getType(self: *const Selection) []const u8 {
if (self._range == null) return "None"; if (self._range == null) {
if (!self.isInTree()) return "None"; return "None";
if (self.getIsCollapsed()) return "Caret"; }
if (!self.isInTree()) {
return "None";
}
if (self.getIsCollapsed()) {
return "Caret";
}
return "Range"; return "Range";
} }
pub fn addRange(self: *Selection, range: *Range, page: *Page) !void { 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 // Only add the range if its root node is in the document associated with this selection
const start_node = range.asAbstractRange().getStartContainer(); const start_node = range.asAbstractRange().getStartContainer();
@@ -126,22 +149,25 @@ pub fn addRange(self: *Selection, range: *Range, page: *Page) !void {
return; return;
} }
self._range = range; self.setRange(range, page);
try dispatchSelectionChangeEvent(page); try dispatchSelectionChangeEvent(page);
} }
pub fn removeRange(self: *Selection, range: *Range, page: *Page) !void { pub fn removeRange(self: *Selection, range: *Range, page: *Page) !void {
if (self._range == range) { const existing_range = self._range orelse return error.NotFound;
self._range = null; if (existing_range != range) {
try dispatchSelectionChangeEvent(page);
return;
} else {
return error.NotFound; return error.NotFound;
} }
self.setRange(null, page);
try dispatchSelectionChangeEvent(page);
} }
pub fn removeAllRanges(self: *Selection, page: *Page) !void { pub fn removeAllRanges(self: *Selection, page: *Page) !void {
self._range = null; if (self._range == null) {
return;
}
self.setRange(null, page);
self._direction = .none; self._direction = .none;
try dispatchSelectionChangeEvent(page); 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.setStart(last_node, last_offset);
try new_range.setEnd(last_node, last_offset); try new_range.setEnd(last_node, last_offset);
self._range = new_range; self.setRange(new_range, page);
self._direction = .none; self._direction = .none;
try dispatchSelectionChangeEvent(page); 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.setStart(first_node, first_offset);
try new_range.setEnd(first_node, first_offset); try new_range.setEnd(first_node, first_offset);
self._range = new_range; self.setRange(new_range, page);
self._direction = .none; self._direction = .none;
try dispatchSelectionChangeEvent(page); 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); 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); const new_range = try Range.init(page);
try new_range.setStart(new_node, new_offset); try new_range.setStart(new_node, new_offset);
try new_range.setEnd(new_node, new_offset); try new_range.setEnd(new_node, new_offset);
self._range = new_range;
self.setRange(new_range, page);
self._direction = .none; self._direction = .none;
try dispatchSelectionChangeEvent(page); try dispatchSelectionChangeEvent(page);
}, },
@@ -582,7 +609,7 @@ pub fn selectAllChildren(self: *Selection, parent: *Node, page: *Page) !void {
const child_count = parent.getChildrenCount(); const child_count = parent.getChildrenCount();
try range.setEnd(parent, @intCast(child_count)); try range.setEnd(parent, @intCast(child_count));
self._range = range; self.setRange(range, page);
self._direction = .forward; self._direction = .forward;
try dispatchSelectionChangeEvent(page); try dispatchSelectionChangeEvent(page);
} }
@@ -630,7 +657,7 @@ pub fn setBaseAndExtent(
}, },
} }
self._range = range; self.setRange(range, page);
try dispatchSelectionChangeEvent(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.setStart(node, offset);
try range.setEnd(node, offset); try range.setEnd(node, offset);
self._range = range; self.setRange(range, page);
self._direction = .none; self._direction = .none;
try dispatchSelectionChangeEvent(page); try dispatchSelectionChangeEvent(page);
} }
@@ -666,6 +693,16 @@ pub fn toString(self: *const Selection, page: *Page) ![]const u8 {
return try range.toString(page); 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 JsApi = struct {
pub const bridge = js.Bridge(Selection); pub const bridge = js.Bridge(Selection);
@@ -673,6 +710,7 @@ pub const JsApi = struct {
pub const name = "Selection"; pub const name = "Selection";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const finalizer = bridge.finalizer(Selection.deinit);
}; };
pub const anchorNode = bridge.accessor(Selection.getAnchorNode, null, .{}); pub const anchorNode = bridge.accessor(Selection.getAnchorNode, null, .{});

View File

@@ -249,6 +249,8 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 {
.{ page.origin orelse "null", uuid_buf }, .{ page.origin orelse "null", uuid_buf },
); );
try page._blob_urls.put(page.arena, blob_url, blob); 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; return blob_url;
} }
@@ -258,8 +260,10 @@ pub fn revokeObjectURL(url: []const u8, page: *Page) void {
return; return;
} }
// Remove from registry (no-op if not found) // Remove from registry and release strong ref (no-op if not found)
_ = page._blob_urls.remove(url); if (page._blob_urls.fetchRemove(url)) |entry| {
page.js.weakRef(entry.value);
}
} }
pub const JsApi = struct { pub const JsApi = struct {

View File

@@ -64,15 +64,30 @@ pub fn createImageData(
switch (width_or_image_data) { switch (width_or_image_data) {
.width => |width| { .width => |width| {
const height = maybe_height orelse return error.TypeError; 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| { .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 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 save(_: *CanvasRenderingContext2D) void {}
pub fn restore(_: *CanvasRenderingContext2D) void {} pub fn restore(_: *CanvasRenderingContext2D) void {}
pub fn scale(_: *CanvasRenderingContext2D, _: f64, _: f64) 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 createImageData = bridge.function(CanvasRenderingContext2D.createImageData, .{ .dom_exception = true });
pub const putImageData = bridge.function(CanvasRenderingContext2D.putImageData, .{ .noop = 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 save = bridge.function(CanvasRenderingContext2D.save, .{ .noop = true });
pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{ .noop = true }); pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{ .noop = true });
pub const scale = bridge.function(CanvasRenderingContext2D.scale, .{ .noop = true }); pub const scale = bridge.function(CanvasRenderingContext2D.scale, .{ .noop = true });

View File

@@ -63,15 +63,30 @@ pub fn createImageData(
switch (width_or_image_data) { switch (width_or_image_data) {
.width => |width| { .width => |width| {
const height = maybe_height orelse return error.TypeError; 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| { .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 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 save(_: *OffscreenCanvasRenderingContext2D) void {}
pub fn restore(_: *OffscreenCanvasRenderingContext2D) void {} pub fn restore(_: *OffscreenCanvasRenderingContext2D) void {}
pub fn scale(_: *OffscreenCanvasRenderingContext2D, _: f64, _: f64) 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 createImageData = bridge.function(OffscreenCanvasRenderingContext2D.createImageData, .{ .dom_exception = true });
pub const putImageData = bridge.function(OffscreenCanvasRenderingContext2D.putImageData, .{ .noop = 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 save = bridge.function(OffscreenCanvasRenderingContext2D.save, .{ .noop = true });
pub const restore = bridge.function(OffscreenCanvasRenderingContext2D.restore, .{ .noop = true }); pub const restore = bridge.function(OffscreenCanvasRenderingContext2D.restore, .{ .noop = true });
pub const scale = bridge.function(OffscreenCanvasRenderingContext2D.scale, .{ .noop = true }); pub const scale = bridge.function(OffscreenCanvasRenderingContext2D.scale, .{ .noop = true });

View File

@@ -53,8 +53,8 @@ fn getFullAXTree(cmd: anytype) !void {
const frame_id = params.frameId orelse { const frame_id = params.frameId orelse {
break :blk session.currentPage() orelse return error.PageNotLoaded; break :blk session.currentPage() orelse return error.PageNotLoaded;
}; };
const page_id = try id.toPageId(.frame_id, frame_id); const page_frame_id = try id.toPageId(.frame_id, frame_id);
break :blk session.findPage(page_id) orelse { break :blk session.findPageByFrameId(page_frame_id) orelse {
return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{}); return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{});
}; };
}; };

View File

@@ -502,9 +502,9 @@ fn getFrameOwner(cmd: anytype) !void {
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; 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.", .{}); return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{});
}; };

View File

@@ -238,7 +238,7 @@ pub fn httpRequestStart(bc: anytype, msg: *const Notification.RequestStart) !voi
const transfer = msg.transfer; const transfer = msg.transfer;
const req = &transfer.req; const req = &transfer.req;
const frame_id = req.frame_id; 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 // Modify request with extra CDP headers
for (bc.extra_headers.items) |extra| { for (bc.extra_headers.items) |extra| {

View File

@@ -35,12 +35,7 @@ fn setIgnoreCertificateErrors(cmd: anytype) !void {
ignore: bool, ignore: bool,
})) orelse return error.InvalidParams; })) orelse return error.InvalidParams;
if (params.ignore) { try cmd.cdp.browser.http_client.setTlsVerify(!params.ignore);
try cmd.cdp.browser.http_client.disableTlsVerify();
} else {
try cmd.cdp.browser.http_client.enableTlsVerify();
}
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }

View File

@@ -43,6 +43,10 @@ config: *const Config,
ca_blob: ?net_http.Blob, ca_blob: ?net_http.Blob,
robot_store: RobotStore, robot_store: RobotStore,
connections: []net_http.Connection,
available: std.DoublyLinkedList = .{},
conn_mutex: std.Thread.Mutex = .{},
pollfds: []posix.pollfd, pollfds: []posix.pollfd,
listener: ?Listener = null, listener: ?Listener = null,
@@ -191,11 +195,23 @@ pub fn init(allocator: Allocator, config: *const Config) !Runtime {
ca_blob = try loadCerts(allocator); 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 .{ return .{
.allocator = allocator, .allocator = allocator,
.config = config, .config = config,
.ca_blob = ca_blob, .ca_blob = ca_blob,
.robot_store = RobotStore.init(allocator), .robot_store = RobotStore.init(allocator),
.connections = connections,
.available = available,
.pollfds = pollfds, .pollfds = pollfds,
.wakeup_pipe = pipe, .wakeup_pipe = pipe,
}; };
@@ -216,6 +232,11 @@ pub fn deinit(self: *Runtime) void {
self.allocator.free(data[0..ca_blob.len]); self.allocator.free(data[0..ca_blob.len]);
} }
for (self.connections) |*conn| {
conn.deinit();
}
self.allocator.free(self.connections);
self.robot_store.deinit(); self.robot_store.deinit();
globalDeinit(); globalDeinit();
@@ -310,6 +331,25 @@ pub fn stop(self: *Runtime) void {
_ = posix.write(self.wakeup_pipe[1], &.{1}) catch {}; _ = 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 {
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);
}
pub fn newConnection(self: *Runtime) !net_http.Connection { pub fn newConnection(self: *Runtime) !net_http.Connection {
return net_http.Connection.init(self.ca_blob, self.config); return net_http.Connection.init(self.ca_blob, self.config);
} }

View File

@@ -237,7 +237,7 @@ pub const ResponseHead = struct {
pub const Connection = struct { pub const Connection = struct {
easy: *libcurl.Curl, easy: *libcurl.Curl,
node: Handles.HandleList.Node = .{}, node: std.DoublyLinkedList.Node = .{},
pub fn init( pub fn init(
ca_blob_: ?libcurl.CurlBlob, ca_blob_: ?libcurl.CurlBlob,
@@ -385,8 +385,16 @@ pub const Connection = struct {
try libcurl.curl_easy_setopt(self.easy, .write_function, data_cb); try libcurl.curl_easy_setopt(self.easy, .write_function, data_cb);
} }
pub fn setProxy(self: *const Connection, proxy: ?[*:0]const u8) !void { pub fn reset(self: *const Connection) !void {
try libcurl.curl_easy_setopt(self.easy, .proxy, proxy); 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 { pub fn setTlsVerify(self: *const Connection, verify: bool, use_proxy: bool) !void {
@@ -467,111 +475,32 @@ pub const Connection = struct {
}; };
pub const Handles = struct { pub const Handles = struct {
connections: []Connection,
dirty: HandleList,
in_use: HandleList,
available: HandleList,
multi: *libcurl.CurlM, 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; const multi = libcurl.curl_multi_init() orelse return error.FailedToInitializeMulti;
errdefer libcurl.curl_multi_cleanup(multi) catch {}; errdefer libcurl.curl_multi_cleanup(multi) catch {};
try libcurl.curl_multi_setopt(multi, .max_host_connections, config.httpMaxHostOpen()); try libcurl.curl_multi_setopt(multi, .max_host_connections, config.httpMaxHostOpen());
const connections = try allocator.alloc(Connection, count); return .{ .multi = multi };
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,
};
} }
pub fn deinit(self: *Handles, allocator: Allocator) void { pub fn deinit(self: *Handles) void {
for (self.connections) |*conn| {
conn.deinit();
}
allocator.free(self.connections);
libcurl.curl_multi_cleanup(self.multi) catch {}; 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 { pub fn add(self: *Handles, conn: *const Connection) !void {
try libcurl.curl_multi_add_handle(self.multi, conn.easy); try libcurl.curl_multi_add_handle(self.multi, conn.easy);
} }
pub fn remove(self: *Handles, conn: *Connection) void { pub fn remove(self: *Handles, conn: *const Connection) !void {
if (libcurl.curl_multi_remove_handle(self.multi, conn.easy)) { try 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 perform(self: *Handles) !c_int { pub fn perform(self: *Handles) !c_int {
self.performing = true;
defer self.performing = false;
const multi = self.multi;
var running: c_int = undefined; var running: c_int = undefined;
try libcurl.curl_multi_perform(self.multi, &running); 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; return running;
} }

View File

@@ -590,7 +590,10 @@ pub fn curl_easy_setopt(easy: *Curl, comptime option: CurlOption, value: anytype
.header_data, .header_data,
.write_data, .write_data,
=> blk: { => 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); break :blk c.curl_easy_setopt(easy, opt, ptr);
}, },