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

@@ -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,

View File

@@ -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);
}
}
@@ -644,10 +648,7 @@ 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();
for (self.handles.connections) |*conn| {
try conn.setProxy(proxy.ptr);
}
self.http_proxy = proxy;
self.use_proxy = true;
}
@@ -656,31 +657,21 @@ 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.use_proxy = proxy != null;
self.http_proxy = self.network.config.httpProxy();
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...
for (self.handles.connections) |*conn| {
try conn.setTlsVerify(true, 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);
var it = self.in_use.first;
while (it) |node| : (it = node.next) {
const conn: *Net.Connection = @fieldParentPtr("node", node);
try conn.setTlsVerify(verify, self.use_proxy);
}
self.tls_verify = verify;
}
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 {
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 +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
// 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 +750,22 @@ pub const PerformStatus = enum {
};
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
// whatever messages we have waiting ahead of time.
@@ -871,11 +884,26 @@ 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 {
self.network.releaseConnection(conn);
}
fn ensureNoActiveConnection(self: *const Client) !void {
if (self.active > 0) {
return error.InflightConnection;
@@ -1023,7 +1051,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 +1121,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
@@ -1258,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;
}
@@ -1324,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;

View File

@@ -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,
@@ -404,6 +407,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 +434,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 +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.
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 +484,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,
},
});

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -89,6 +89,41 @@
}
</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">
{

View File

@@ -62,3 +62,26 @@
testing.expectEqual(offscreen.height, 96);
}
</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 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, .{});

View File

@@ -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 });

View File

@@ -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

View File

@@ -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, .{});

View File

@@ -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 {

View File

@@ -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 });

View File

@@ -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 });

View File

@@ -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.", .{});
};
};

View File

@@ -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.", .{});
};

View File

@@ -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| {

View File

@@ -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, .{});
}

View File

@@ -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,
@@ -191,11 +195,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,
};
@@ -216,6 +232,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();
@@ -310,6 +331,25 @@ 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 {
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 {
return net_http.Connection.init(self.ca_blob, self.config);
}

View File

@@ -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;
}

View File

@@ -590,7 +590,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);
},