mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Merge branch 'main' into agent
This commit is contained in:
@@ -532,6 +532,7 @@ pub const DumpFormat = enum {
|
||||
pub const WaitUntil = enum {
|
||||
load,
|
||||
domcontentloaded,
|
||||
networkalmostidle,
|
||||
networkidle,
|
||||
done,
|
||||
};
|
||||
|
||||
@@ -111,10 +111,15 @@ network: *Network,
|
||||
|
||||
arena_pool: *ArenaPool,
|
||||
|
||||
// The current proxy. CDP can change it, changeProxy(null) restores
|
||||
// from config.
|
||||
// The current proxy. Callers can change it, changeProxy(null) restores
|
||||
// from config. May point either at `http_proxy_owned` (a caller-supplied
|
||||
// dupe) or at the config string (which we must not free).
|
||||
http_proxy: ?[:0]const u8 = null,
|
||||
|
||||
// When a caller (e.g. CDP) supplies a proxy, we have to dupe it to take ownership
|
||||
// which we'll be responsible for freeing.
|
||||
http_proxy_owned: ?[:0]const u8 = null,
|
||||
|
||||
// track if the client use a proxy for connections.
|
||||
// We can't use http_proxy because we want also to track proxy configured via
|
||||
// CDP.
|
||||
@@ -260,6 +265,9 @@ pub fn deinit(self: *Client) void {
|
||||
self.handles.deinit();
|
||||
|
||||
self.clearUserAgentOverride();
|
||||
if (self.http_proxy_owned) |owned| {
|
||||
self.allocator.free(owned);
|
||||
}
|
||||
|
||||
self.robots_layer.deinit(self.allocator);
|
||||
self.deferring_layer.deinit();
|
||||
@@ -337,7 +345,22 @@ pub fn setTlsVerify(self: *Client, verify: bool) !void {
|
||||
// can be changed at any point in the easy's lifecycle.
|
||||
pub fn changeProxy(self: *Client, proxy: ?[:0]const u8) !void {
|
||||
try self.ensureNoActiveConnection();
|
||||
self.http_proxy = proxy orelse self.network.config.httpProxy();
|
||||
|
||||
// Free any previously-duped proxy before we overwrite http_proxy.
|
||||
if (self.http_proxy_owned) |owned| {
|
||||
self.allocator.free(owned);
|
||||
self.http_proxy_owned = null;
|
||||
}
|
||||
|
||||
// Reset to the config default; if dupeZ below fails, http_proxy is
|
||||
// left pointing at this rather than at the freed dup.
|
||||
self.http_proxy = self.network.config.httpProxy();
|
||||
|
||||
if (proxy) |p| {
|
||||
const owned = try self.allocator.dupeZ(u8, p);
|
||||
self.http_proxy_owned = owned;
|
||||
self.http_proxy = owned;
|
||||
}
|
||||
self.use_proxy = self.http_proxy != null;
|
||||
}
|
||||
|
||||
@@ -1564,10 +1587,28 @@ pub const Transfer = struct {
|
||||
// post-perform would need to be improved),
|
||||
pub fn unpark(self: *Transfer) void {
|
||||
lp.assert(self.state == .parked, "Transfer.unpark", .{ .state = self.state });
|
||||
self.leaveIntercept();
|
||||
self.state = .created;
|
||||
}
|
||||
|
||||
// Decrement the interception counter iff this transfer is currently
|
||||
// parked for CDP interception.
|
||||
fn leaveIntercept(self: *Transfer) void {
|
||||
if (self.state != .parked) {
|
||||
return;
|
||||
}
|
||||
switch (self.state.parked) {
|
||||
.robots => {},
|
||||
.intercept_request, .intercept_auth => {
|
||||
const intercept_layer = &self.client.interception_layer;
|
||||
lp.assert(intercept_layer.intercepted > 0, "Transfer.leaveIntercept", .{ .value = intercept_layer.intercepted });
|
||||
intercept_layer.intercepted -= 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Transfer) void {
|
||||
self.leaveIntercept();
|
||||
if (self._conn) |c| {
|
||||
self.client.removeConn(c);
|
||||
self._conn = null;
|
||||
@@ -1790,6 +1831,7 @@ pub const Transfer = struct {
|
||||
return writer.print("{s} {s}", .{ @tagName(req.method), req.url });
|
||||
}
|
||||
|
||||
// `url` must have transfer-arena lifetime: it's stored as-is, not duped.
|
||||
pub fn updateURL(self: *Transfer, url: [:0]const u8) !void {
|
||||
self.req.url = url;
|
||||
}
|
||||
@@ -1829,7 +1871,9 @@ pub const Transfer = struct {
|
||||
}
|
||||
|
||||
const base_url = try conn.getEffectiveUrl();
|
||||
const resolved = try URL.resolve(arena, std.mem.span(base_url), location.value, .{});
|
||||
// base_url and location.value are owned by curl. The returned value
|
||||
// will be stored in transfer.req.url, hence the always_dupe.
|
||||
const resolved = try URL.resolve(arena, std.mem.span(base_url), location.value, .{ .always_dupe = true });
|
||||
|
||||
// RFC 7231 §7.1.2: if the Location value has no fragment, the redirect
|
||||
// inherits the fragment from the URI used to generate the request.
|
||||
@@ -1904,9 +1948,9 @@ pub const Transfer = struct {
|
||||
log.debug(.http, "abort auth transfer", .{ .intercepted = self.client.interception_layer.intercepted });
|
||||
}
|
||||
|
||||
self.client.interception_layer.intercepted -= 1;
|
||||
// The transfer is still .parked(.intercept_auth)
|
||||
// abort -> deinit -> leaveIntercept decrements the counter.
|
||||
self.abort(error.AbortAuthChallenge);
|
||||
return;
|
||||
}
|
||||
|
||||
// headerDoneCallback is called once the headers have been read.
|
||||
@@ -2067,13 +2111,16 @@ pub const Transfer = struct {
|
||||
|
||||
pub fn continueTransfer(self: *Client, transfer: *Transfer) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
lp.assert(self.interception_layer.intercepted > 0, "HttpClient.continueTransfer", .{ .value = self.interception_layer.intercepted });
|
||||
log.debug(.http, "continue transfer", .{ .intercepted = self.interception_layer.intercepted });
|
||||
}
|
||||
|
||||
self.interception_layer.intercepted -= 1;
|
||||
transfer.unpark();
|
||||
return self.process(transfer);
|
||||
self.process(transfer) catch |err| {
|
||||
if (transfer.state == .created) {
|
||||
transfer.abort(err);
|
||||
}
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
const Noop = struct {
|
||||
|
||||
@@ -244,7 +244,10 @@ pub fn scheduleNavigation(self: *Page, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
pub fn findFrameByFrameId(self: *Page, frame_id: u32) ?*Frame {
|
||||
return findFrameBy(&self.frame, "_frame_id", frame_id);
|
||||
if (findFrameBy(&self.frame, "_frame_id", frame_id)) |found| {
|
||||
return found;
|
||||
}
|
||||
return self.findPopupBy("_frame_id", frame_id);
|
||||
}
|
||||
|
||||
// Returns the popup Frame registered under `name`, or null.
|
||||
@@ -258,11 +261,16 @@ pub fn findPopupByName(self: *Page, name: []const u8) ?*Frame {
|
||||
}
|
||||
|
||||
pub fn findFrameByLoaderId(self: *Page, loader_id: u32) ?*Frame {
|
||||
return findFrameBy(&self.frame, "_loader_id", loader_id);
|
||||
if (findFrameBy(&self.frame, "_loader_id", loader_id)) |found| {
|
||||
return found;
|
||||
}
|
||||
return self.findPopupBy("_loader_id", loader_id);
|
||||
}
|
||||
|
||||
fn findFrameBy(frame: *Frame, comptime field: []const u8, id: u32) ?*Frame {
|
||||
if (@field(frame, field) == id) return frame;
|
||||
if (@field(frame, field) == id) {
|
||||
return frame;
|
||||
}
|
||||
for (frame.child_frames.items) |f| {
|
||||
if (findFrameBy(f, field, id)) |found| {
|
||||
return found;
|
||||
@@ -270,3 +278,12 @@ fn findFrameBy(frame: *Frame, comptime field: []const u8, id: u32) ?*Frame {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn findPopupBy(self: *Page, comptime field: []const u8, id: u32) ?*Frame {
|
||||
for (self.popups.items) |frame| {
|
||||
if (findFrameBy(frame, field, id)) |found| {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ fn _wait(self: *Runner, comptime is_cdp: bool, opts: WaitOpts) !WaitResult {
|
||||
const tick_result = self._tick(is_cdp, tick_opts) catch |err| {
|
||||
switch (err) {
|
||||
error.JsError => {}, // already logged (with hopefully more context)
|
||||
error.ClientDisconnected => {}, // CDP layer already logged this
|
||||
else => log.err(.browser, "session wait", .{
|
||||
.err = err,
|
||||
.url = self.frame.url,
|
||||
@@ -215,6 +216,9 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !TickResult {
|
||||
.networkidle => if (frame._notified_network_idle == .done) {
|
||||
return .done;
|
||||
},
|
||||
.networkalmostidle => if (frame._notified_network_almost_idle == .done) {
|
||||
return .done;
|
||||
},
|
||||
}
|
||||
|
||||
if (http_active == 0 and http_next_tick == 0 and http_client.ws_active == 0 and http_client.queue.first == null and http_client.ready_queue.first == null and (comptime is_cdp) == false) {
|
||||
|
||||
@@ -631,7 +631,7 @@ test "Env: Worker context " {
|
||||
const frame = try session.createPage();
|
||||
defer session.removePage();
|
||||
|
||||
const worker = try @import("../webapi/Worker.zig").init("http://localhost:9582/src/browser/tests/testing.js", frame);
|
||||
const worker = try @import("../webapi/Worker.zig").init("http://localhost:9582/src/browser/tests/testing.js", null, frame);
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
worker._worker_scope.js.localScope(&ls);
|
||||
|
||||
56
src/browser/tests/element/html/embed.html
Normal file
56
src/browser/tests/element/html/embed.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<embed id="em1" src="/plugin.swf" type="application/x-shockwave-flash" width="640" height="480">
|
||||
<embed id="em2">
|
||||
|
||||
<script id="embed-from-html">
|
||||
{
|
||||
const em1 = document.getElementById('em1');
|
||||
testing.expectEqual('HTMLEmbedElement', em1.constructor.name);
|
||||
testing.expectEqual(testing.ORIGIN + '/plugin.swf', em1.src);
|
||||
testing.expectEqual('application/x-shockwave-flash', em1.type);
|
||||
testing.expectEqual('640', em1.width);
|
||||
testing.expectEqual('480', em1.height);
|
||||
|
||||
const em2 = document.getElementById('em2');
|
||||
testing.expectEqual('', em2.src);
|
||||
testing.expectEqual('', em2.type);
|
||||
testing.expectEqual('', em2.width);
|
||||
testing.expectEqual('', em2.height);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="embed-reflection">
|
||||
{
|
||||
const em = document.createElement('embed');
|
||||
|
||||
em.type = 'video/mp4';
|
||||
testing.expectEqual('video/mp4', em.getAttribute('type'));
|
||||
em.setAttribute('type', 'image/svg+xml');
|
||||
testing.expectEqual('image/svg+xml', em.type);
|
||||
|
||||
em.width = '320';
|
||||
testing.expectEqual('320', em.getAttribute('width'));
|
||||
em.setAttribute('width', '160');
|
||||
testing.expectEqual('160', em.width);
|
||||
|
||||
em.height = '240';
|
||||
testing.expectEqual('240', em.getAttribute('height'));
|
||||
em.setAttribute('height', '120');
|
||||
testing.expectEqual('120', em.height);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="embed-src-url">
|
||||
{
|
||||
const em = document.createElement('embed');
|
||||
testing.expectEqual('', em.src);
|
||||
|
||||
em.src = 'https://lightpanda.io/plugin.swf';
|
||||
testing.expectEqual('https://lightpanda.io/plugin.swf', em.src);
|
||||
|
||||
em.src = '/relative.swf';
|
||||
testing.expectEqual(testing.ORIGIN + '/relative.swf', em.src);
|
||||
}
|
||||
</script>
|
||||
25
src/browser/tests/worker/module-worker.js
Normal file
25
src/browser/tests/worker/module-worker.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// A module worker (`new Worker(url, { type: "module" })`). Unlike a classic
|
||||
// worker, the entry script may use top-level static `import`/`export`, and
|
||||
// `importScripts()` is not supported (it throws a TypeError).
|
||||
import { baseValue } from './modules/base.js';
|
||||
import { importedValue, localValue } from './modules/importer.js';
|
||||
|
||||
export const exported = 'top-level-export-ok';
|
||||
|
||||
let importScriptsError = null;
|
||||
try {
|
||||
importScripts('./import-script1.js');
|
||||
} catch (e) {
|
||||
importScriptsError = e.constructor.name;
|
||||
}
|
||||
|
||||
onmessage = function (event) {
|
||||
postMessage({
|
||||
echo: event.data,
|
||||
baseValue: baseValue,
|
||||
importedValue: importedValue,
|
||||
localValue: localValue,
|
||||
importScriptsError: importScriptsError,
|
||||
from: 'module-worker',
|
||||
});
|
||||
};
|
||||
@@ -380,6 +380,29 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="worker_module_type" type=module>
|
||||
// A module-type worker: its entry script uses top-level static import (only
|
||||
// valid in module workers), and importScripts() throws a TypeError.
|
||||
{
|
||||
const state = await testing.async();
|
||||
const worker = new Worker('./module-worker.js', { type: 'module' });
|
||||
|
||||
worker.onmessage = function(event) {
|
||||
state.resolve(event.data);
|
||||
};
|
||||
worker.postMessage({ greeting: 'to-module' });
|
||||
|
||||
await state.done((response) => {
|
||||
testing.expectEqual('to-module', response.echo.greeting);
|
||||
testing.expectEqual('module-worker', response.from);
|
||||
testing.expectEqual('from-base', response.baseValue);
|
||||
testing.expectEqual('from-base', response.importedValue);
|
||||
testing.expectEqual('local', response.localValue);
|
||||
testing.expectEqual('TypeError', response.importScriptsError);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="worker_from_blob_url" type=module>
|
||||
// A blob: worker resolves its initial script through the HTTP client's
|
||||
// synthetic-scheme path against the frame's blob registry, like any URL.
|
||||
|
||||
@@ -36,6 +36,12 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Worker = @This();
|
||||
|
||||
pub const WorkerType = enum {
|
||||
classic,
|
||||
module,
|
||||
pub const js_enum_from_string = true;
|
||||
};
|
||||
|
||||
// used by HttpClient when generating notification
|
||||
// Ultimately used by CDP to generate request/loader ids.
|
||||
_frame_id: u32,
|
||||
@@ -47,6 +53,7 @@ _arena: Allocator,
|
||||
_worker_scope: *WorkerGlobalScope,
|
||||
|
||||
_url: [:0]const u8,
|
||||
_type: WorkerType = .classic,
|
||||
_script_loaded: bool = false,
|
||||
_script_buffer: std.ArrayList(u8) = .empty,
|
||||
_http_response: ?HttpClient.Response = null,
|
||||
@@ -56,7 +63,11 @@ _on_error: ?js.Function.Global = null,
|
||||
_on_message: ?js.Function.Global = null,
|
||||
_on_messageerror: ?js.Function.Global = null,
|
||||
|
||||
pub fn init(url: []const u8, frame: *Frame) !*Worker {
|
||||
const WorkerOptions = struct {
|
||||
type: WorkerType = .classic,
|
||||
};
|
||||
|
||||
pub fn init(url: []const u8, options: ?WorkerOptions, frame: *Frame) !*Worker {
|
||||
const session = frame._session;
|
||||
|
||||
const arena = try session.getArena(.large, "Worker");
|
||||
@@ -68,6 +79,7 @@ pub fn init(url: []const u8, frame: *Frame) !*Worker {
|
||||
._proto = undefined,
|
||||
._frame = frame,
|
||||
._url = resolved_url,
|
||||
._type = if (options) |o| o.type else .classic,
|
||||
._worker_scope = undefined,
|
||||
._frame_id = session.nextFrameId(),
|
||||
._loader_id = session.nextLoaderId(),
|
||||
@@ -195,12 +207,24 @@ fn loadInitialScript(self: *Worker, script: []const u8) !void {
|
||||
try_catch.init(&ls.local);
|
||||
defer try_catch.deinit();
|
||||
|
||||
_ = ls.local.eval(script, self._url) catch |err| {
|
||||
const caught = try_catch.caughtOrError(self._arena, err);
|
||||
log.err(.browser, "worker script error", .{ .url = self._url, .caught = caught });
|
||||
self.fireErrorEvent(caught.exception orelse @errorName(err), null);
|
||||
return;
|
||||
};
|
||||
// Classic workers evaluate the entry script as a classic script; module
|
||||
// workers (`new Worker(url, { type: "module" })`) instantiate it as a
|
||||
// module so top-level `import`/`export` work. Static imports load
|
||||
// synchronously through ScriptManagerBase (client.tick sync_wait).
|
||||
switch (self._type) {
|
||||
.classic => _ = ls.local.eval(script, self._url) catch |err| {
|
||||
const caught = try_catch.caughtOrError(self._arena, err);
|
||||
log.err(.browser, "worker script error", .{ .url = self._url, .caught = caught });
|
||||
self.fireErrorEvent(caught.exception orelse @errorName(err), null);
|
||||
return;
|
||||
},
|
||||
.module => self._worker_scope.js.module(false, &ls.local, script, self._url, true) catch |err| {
|
||||
const caught = try_catch.caughtOrError(self._arena, err);
|
||||
log.err(.browser, "worker module error", .{ .url = self._url, .caught = caught });
|
||||
self.fireErrorEvent(caught.exception orelse @errorName(err), null);
|
||||
return;
|
||||
},
|
||||
}
|
||||
|
||||
ls.local.runMacrotasks();
|
||||
}
|
||||
|
||||
@@ -430,6 +430,12 @@ pub fn close(self: *WorkerGlobalScope) void {
|
||||
}
|
||||
|
||||
pub fn importScripts(self: *WorkerGlobalScope, urls: []const [:0]const u8) !void {
|
||||
if (self._worker._type == .module) {
|
||||
// not allowed to be called when the worker type is module (scripts should
|
||||
// use actual imports).
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const session = self._session;
|
||||
const arena = try session.getArena(.large, "importScript");
|
||||
defer session.releaseArena(arena);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
@@ -27,10 +28,50 @@ _proto: *HtmlElement,
|
||||
pub fn asElement(self: *Embed) *Element {
|
||||
return self._proto._proto;
|
||||
}
|
||||
pub fn asConstElement(self: *const Embed) *const Element {
|
||||
return self._proto._proto;
|
||||
}
|
||||
pub fn asNode(self: *Embed) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
pub fn getSrc(self: *const Embed, frame: *Frame) ![]const u8 {
|
||||
const element = self.asConstElement();
|
||||
const src = element.getAttributeSafe(comptime .wrap("src")) orelse return "";
|
||||
if (src.len == 0) {
|
||||
return "";
|
||||
}
|
||||
return element.asConstNode().resolveURL(src, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setSrc(self: *Embed, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("src"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getType(self: *const Embed) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("type")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setType(self: *Embed, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("type"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getWidth(self: *const Embed) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("width")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setWidth(self: *Embed, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("width"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub fn getHeight(self: *const Embed) []const u8 {
|
||||
return self.asConstElement().getAttributeSafe(comptime .wrap("height")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setHeight(self: *Embed, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("height"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Embed);
|
||||
|
||||
@@ -39,4 +80,14 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const height = bridge.accessor(Embed.getHeight, Embed.setHeight, .{ .ce_reactions = true });
|
||||
pub const src = bridge.accessor(Embed.getSrc, Embed.setSrc, .{ .ce_reactions = true });
|
||||
pub const @"type" = bridge.accessor(Embed.getType, Embed.setType, .{ .ce_reactions = true });
|
||||
pub const width = bridge.accessor(Embed.getWidth, Embed.setWidth, .{ .ce_reactions = true });
|
||||
};
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
test "WebApi: HTML.Embed" {
|
||||
try testing.htmlRunner("element/html/embed.html", .{});
|
||||
}
|
||||
|
||||
@@ -812,11 +812,20 @@ fn parseIdentifier(self: *Parser, arena: Allocator, err: ParseError) ParseError!
|
||||
}
|
||||
|
||||
// Slow path: has escapes or nulls
|
||||
return self.consumeEscapedIdentTail(arena, i, err);
|
||||
}
|
||||
|
||||
// Decode the tail of an <ident-token> that contains escape sequences or null.
|
||||
// The caller is expecteed to have already done a "fast path" scan, and
|
||||
// `prefix_len` is the part of `self.input` that needs to this "slow path" for
|
||||
// decoding
|
||||
fn consumeEscapedIdentTail(self: *Parser, arena: Allocator, prefix_len: usize, err: ParseError) ![]const u8 {
|
||||
const input = self.input;
|
||||
|
||||
var result = try std.ArrayList(u8).initCapacity(arena, input.len);
|
||||
result.appendSliceAssumeCapacity(input[0..prefix_len]);
|
||||
|
||||
try result.appendSlice(arena, input[0..i]);
|
||||
|
||||
var j = i;
|
||||
var j = prefix_len;
|
||||
while (j < input.len) {
|
||||
const b = input[j];
|
||||
|
||||
@@ -834,14 +843,10 @@ fn parseIdentifier(self: *Parser, arena: Allocator, err: ParseError) ParseError!
|
||||
continue;
|
||||
}
|
||||
|
||||
const is_ident_char = switch (b) {
|
||||
'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => true,
|
||||
0x80...0xFF => true,
|
||||
else => false,
|
||||
};
|
||||
|
||||
if (!is_ident_char) {
|
||||
break;
|
||||
switch (b) {
|
||||
'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {},
|
||||
0x80...0xFF => {},
|
||||
else => break,
|
||||
}
|
||||
try result.append(arena, b);
|
||||
j += 1;
|
||||
@@ -906,7 +911,7 @@ fn attribute(self: *Parser, arena: Allocator) !Selector.Attribute {
|
||||
self.input = self.input[1..];
|
||||
_ = self.skipSpaces();
|
||||
|
||||
const attr_name = try self.attributeName();
|
||||
const attr_name = try self.attributeName(arena);
|
||||
|
||||
// Normalize the name to lowercase for fast matching (consistent with Attribute.normalizeNameForLookup)
|
||||
const name = try Attribute.normalizeNameForLookupAlloc(arena, .wrap(attr_name));
|
||||
@@ -954,29 +959,39 @@ fn attribute(self: *Parser, arena: Allocator) !Selector.Attribute {
|
||||
return .{ .name = name, .matcher = matcher, .case_insensitive = case_insensitive };
|
||||
}
|
||||
|
||||
fn attributeName(self: *Parser) ![]const u8 {
|
||||
fn attributeName(self: *Parser, arena: Allocator) ![]const u8 {
|
||||
const input = self.input;
|
||||
if (input.len == 0) {
|
||||
return error.InvalidAttributeSelector;
|
||||
}
|
||||
|
||||
const first = input[0];
|
||||
if (!std.ascii.isAlphabetic(first) and first != '_' and first < 0x80) {
|
||||
if (first != '\\' and first != 0 and !std.ascii.isAlphabetic(first) and first != '_' and first < 0x80) {
|
||||
return error.InvalidAttributeSelector;
|
||||
}
|
||||
|
||||
var i: usize = 1;
|
||||
for (input[1..]) |b| {
|
||||
switch (b) {
|
||||
// Fast scan until we hit a character that needs special handling
|
||||
var i: usize = if (first == '\\' or first == 0) 0 else 1;
|
||||
while (i < input.len) : (i += 1) {
|
||||
switch (input[i]) {
|
||||
'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {},
|
||||
0x80...0xFF => {},
|
||||
else => break,
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
self.input = input[i..];
|
||||
return input[0..i];
|
||||
if (i == input.len or (input[i] != '\\' and input[i] != 0)) {
|
||||
// Fast path fully completed (no escapes/nulls in the name)
|
||||
if (i == 0) {
|
||||
@branchHint(.cold);
|
||||
return error.InvalidAttributeSelector;
|
||||
}
|
||||
self.input = input[i..];
|
||||
return input[0..i];
|
||||
}
|
||||
|
||||
// Slow path: decode escape sequences (and null -> U+FFFD).
|
||||
return self.consumeEscapedIdentTail(arena, i, error.InvalidAttributeSelector);
|
||||
}
|
||||
|
||||
fn attributeMatcher(self: *Parser) !std.meta.FieldEnum(Selector.AttributeMatcher) {
|
||||
@@ -1669,3 +1684,58 @@ test "Selector: Parser.attributeValue" {
|
||||
try testing.expectError(error.InvalidAttributeSelector, parser.attributeValue(arena));
|
||||
}
|
||||
}
|
||||
|
||||
test "Selector: Parser.attributeName" {
|
||||
defer testing.reset();
|
||||
const arena = testing.arena_allocator;
|
||||
|
||||
// Plain name (fast path).
|
||||
{
|
||||
var parser = Parser{ .input = "ng-app]" };
|
||||
try testing.expectEqual("ng-app", try parser.attributeName(arena));
|
||||
try testing.expectEqual("]", parser.input);
|
||||
}
|
||||
|
||||
// Escaped colon in the name: [ng\:jq] -> attribute literally named "ng:jq".
|
||||
// AngularJS probes for these during bootstrap; rejecting them aborts the
|
||||
// whole framework. (see workingnomads.com regression)
|
||||
{
|
||||
var parser = Parser{ .input = "ng\\:jq]" };
|
||||
try testing.expectEqual("ng:jq", try parser.attributeName(arena));
|
||||
try testing.expectEqual("]", parser.input);
|
||||
}
|
||||
|
||||
// Escape as the very first character.
|
||||
{
|
||||
var parser = Parser{ .input = "\\:foo]" };
|
||||
try testing.expectEqual(":foo", try parser.attributeName(arena));
|
||||
try testing.expectEqual("]", parser.input);
|
||||
}
|
||||
|
||||
// Hex escape: [\41 bc] -> "Abc" (space terminates the hex escape).
|
||||
{
|
||||
var parser = Parser{ .input = "\\41 bc]" };
|
||||
try testing.expectEqual("Abc", try parser.attributeName(arena));
|
||||
try testing.expectEqual("]", parser.input);
|
||||
}
|
||||
|
||||
// Name followed by a matcher stops at the matcher (fast path, no escape).
|
||||
{
|
||||
var parser = Parser{ .input = "data-ng-csp=foo]" };
|
||||
try testing.expectEqual("data-ng-csp", try parser.attributeName(arena));
|
||||
try testing.expectEqual("=foo]", parser.input);
|
||||
}
|
||||
|
||||
// Escaped name followed by a matcher.
|
||||
{
|
||||
var parser = Parser{ .input = "ng\\:csp~=foo]" };
|
||||
try testing.expectEqual("ng:csp", try parser.attributeName(arena));
|
||||
try testing.expectEqual("~=foo]", parser.input);
|
||||
}
|
||||
|
||||
// Invalid first character.
|
||||
{
|
||||
var parser = Parser{ .input = "=foo]" };
|
||||
try testing.expectError(error.InvalidAttributeSelector, parser.attributeName(arena));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,18 +618,9 @@ pub const BrowserContext = struct {
|
||||
|
||||
// abort all intercepted requests before closing the session/page
|
||||
// since some of these might callback into the page/scriptmanager.
|
||||
// intercept_state stores ids — look each one up; if it's already
|
||||
// gone (out-of-band destroy), there's nothing to abort, but the
|
||||
// intercepted counter still needs decrementing because we
|
||||
// incremented it on pause.
|
||||
// intercept_state stores ids.
|
||||
const http_client = &browser.http_client;
|
||||
for (self.intercept_state.pendingIntercepts()) |transfer_id| {
|
||||
lp.assert(
|
||||
http_client.interception_layer.intercepted > 0,
|
||||
"BrowserContext.deinit.intercepted",
|
||||
.{ .value = http_client.interception_layer.intercepted },
|
||||
);
|
||||
http_client.interception_layer.intercepted -= 1;
|
||||
if (http_client.findTransfer(transfer_id)) |transfer| {
|
||||
transfer.abort(error.ClientDisconnect);
|
||||
}
|
||||
|
||||
@@ -198,6 +198,7 @@ pub fn requestIntercept(bc: *CDP.BrowserContext, intercept: *const Notification.
|
||||
|
||||
const transfer = intercept.transfer;
|
||||
try bc.intercept_state.put(transfer.id);
|
||||
errdefer _ = bc.intercept_state.remove(transfer.id);
|
||||
|
||||
try bc.cdp.sendEvent("Fetch.requestPaused", .{
|
||||
.requestId = &id.toInterceptId(transfer.id),
|
||||
@@ -332,23 +333,21 @@ fn continueWithAuth(cmd: *CDP.Command) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// TODO: double-decrement of interception_layer.intercepted if
|
||||
// continueTransfer fails: continueTransfer decrements unconditionally,
|
||||
// and the errdefer below decrements again via abortAuthChallenge.
|
||||
// Worse: if continueTransfer's failure path destroys the transfer
|
||||
// (start_callback fail in makeRequest), this errdefer hits a freed
|
||||
// transfer. Pre-existing; needs makeRequest failure-semantics cleanup.
|
||||
errdefer transfer.abortAuthChallenge();
|
||||
|
||||
transfer.updateCredentials(try std.fmt.allocPrintSentinel(
|
||||
transfer.arena,
|
||||
"{s}:{s}",
|
||||
.{
|
||||
params.authChallengeResponse.username,
|
||||
params.authChallengeResponse.password,
|
||||
},
|
||||
0,
|
||||
));
|
||||
{
|
||||
// The transfer is still parked here; if building the credentials
|
||||
// fails, release it. Scoped so the errdefer does NOT cover
|
||||
// continueTransfer (which owns its failures).
|
||||
errdefer transfer.abortAuthChallenge();
|
||||
transfer.updateCredentials(try std.fmt.allocPrintSentinel(
|
||||
transfer.arena,
|
||||
"{s}:{s}",
|
||||
.{
|
||||
params.authChallengeResponse.username,
|
||||
params.authChallengeResponse.password,
|
||||
},
|
||||
0,
|
||||
));
|
||||
}
|
||||
|
||||
try client.continueTransfer(transfer);
|
||||
return cmd.sendResult(null, .{});
|
||||
@@ -440,6 +439,7 @@ pub fn requestAuthRequired(bc: *CDP.BrowserContext, intercept: *const Notificati
|
||||
|
||||
const transfer = intercept.transfer;
|
||||
try bc.intercept_state.put(transfer.id);
|
||||
errdefer _ = bc.intercept_state.remove(transfer.id);
|
||||
const request = &transfer.req;
|
||||
|
||||
const challenge = transfer._auth_challenge orelse return error.NullAuthChallenge;
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
\\ Wait until the specified event. Checked before other --wait-* options.
|
||||
\\ Defaults to 'done'. If --wait-selector, --wait-script or
|
||||
\\ --wait-script-file specified, defaults to none.
|
||||
\\ Allowed values: "load", "domcontentloaded", "networkidle", "done".
|
||||
\\ Allowed values: "load", "domcontentloaded", "networkalmostidle",
|
||||
\\ "networkidle", "done".
|
||||
\\ --wait-selector <QUERY>
|
||||
\\ Wait for an element matching the CSS selector to appear. Checked after
|
||||
\\ --wait-until condition is met.
|
||||
|
||||
@@ -194,7 +194,6 @@ pub fn continueRequest(self: *InterceptionLayer, transfer: *Transfer) anyerror!v
|
||||
lp.assert(self.intercepted > 0, "InterceptionLayer.continueRequest", .{ .value = self.intercepted });
|
||||
log.debug(.http, "continue transfer", .{ .intercepted = self.intercepted });
|
||||
}
|
||||
self.intercepted -= 1;
|
||||
|
||||
// Resume the layer chain. Ownership is re-handed to whichever subsequent
|
||||
// layer commits the transfer (queue, multi, or another park). If the
|
||||
@@ -214,7 +213,6 @@ pub fn abortRequest(self: *InterceptionLayer, transfer: *Transfer) void {
|
||||
lp.assert(self.intercepted > 0, "InterceptionLayer.abortRequest", .{ .value = self.intercepted });
|
||||
log.debug(.http, "abort transfer", .{ .intercepted = self.intercepted });
|
||||
}
|
||||
self.intercepted -= 1;
|
||||
transfer.abort(error.Abort);
|
||||
}
|
||||
|
||||
@@ -229,7 +227,6 @@ pub fn fulfillRequest(
|
||||
lp.assert(self.intercepted > 0, "InterceptionLayer.fulfillRequest", .{ .value = self.intercepted });
|
||||
log.debug(.http, "fulfill transfer", .{ .intercepted = self.intercepted });
|
||||
}
|
||||
self.intercepted -= 1;
|
||||
|
||||
// `done` flips true once we've called the user's done_callback. If
|
||||
// done_callback itself throws, the user already saw their end-of-flow
|
||||
|
||||
Reference in New Issue
Block a user