13 Commits

Author SHA1 Message Date
Pierre Tachoire
c04ccf5e87 URL: bound '@' search to authority section
The '@' delimiter for userinfo was searched across the full URL string,
including path, query, and fragment. A URL like `http://attacker.com/@bank.com/`
would incorrectly parse `bank.com` as the host, enabling a Same-Origin Policy bypass.

Restrict the '@' search to the authority section (before any `/?#`) in
getOrigin, getUserInfo, and getHost.
2026-03-26 16:44:58 +01:00
Pierre Tachoire
a0dd14aaad Merge pull request #1999 from lightpanda-io/wait_until_default
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Fix --wait-until default value.
2026-03-26 15:03:59 +01:00
Pierre Tachoire
7ea8f3f766 Merge pull request #2000 from lightpanda-io/add-pre-version
add a -Dpre_version build flag for custom pre version
2026-03-26 12:06:38 +01:00
Pierre Tachoire
28cc60adb0 add a -Dpre_version build flag for custom pre version 2026-03-26 11:52:16 +01:00
Karl Seguin
c14a9ad986 Merge pull request #1992 from navidemad/cdp-page-reload
CDP: implement Page.reload
2026-03-26 18:14:49 +08:00
Karl Seguin
679f2104f4 Fix --wait-until default value.
This was `load`, but it should have been (and was documented as `done`). This
is my fault. Sorry.

Should help with: https://github.com/lightpanda-io/browser/issues/1947#issuecomment-4120597764
2026-03-26 18:06:14 +08:00
Navid EMAD
c6b0c75106 Address review: use arena.dupeZ for URL copy, add try to testing.context()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:09:48 +01:00
Navid EMAD
93485c1ef3 CDP: implement Page.reload
Add `Page.reload` to the CDP Page domain dispatch. Reuses the existing
`page.navigate()` path with `NavigationKind.reload`, matching what
`Location.reload` already does for the JS `location.reload()` API.

Accepts the standard CDP params (`ignoreCache`, `scriptToEvaluateOnLoad`)
per the Chrome DevTools Protocol spec.

The current page URL is copied to the stack before `replacePage()` to
avoid a use-after-free when the old page's arena is freed.

This unblocks CDP clients (Puppeteer, capybara-lightpanda, etc.) that
call `Page.reload` and currently get `UnknownMethod`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:09:48 +01:00
Karl Seguin
0324d5c232 Merge pull request #1997 from lightpanda-io/update-zig-v8
build: bump zig-v8 to v0.3.7
2026-03-26 16:01:40 +08:00
Adrià Arrufat
a75c0cf08d build: bump zig-v8 to v0.3.7 2026-03-26 12:34:10 +09:00
Karl Seguin
2812b8f07c Merge pull request #1991 from lightpanda-io/v8_signature
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Set v8::Signature on FunctionTemplates
2026-03-26 09:27:22 +08:00
Karl Seguin
e2afbec29d update v8 dep 2026-03-26 09:17:32 +08:00
Karl Seguin
a45f9cb810 Set v8::Signature on FunctionTemplates
This causes v8 to verify the receiver of a function, and prevents calling an
accessor or function with the wrong receiver, e.g.:

```
const g = Object.getOwnPropertyDescriptor(Window.prototype, 'document').get;
g.call(null);
```

A few other cleanups in this commit:
1 - Define any accessor with a getter as ReadOnly
2 - Ability to define an accessor with the DontDelete attribute
    (window.document and window.location)
3 - Replace v8__ObjectTemplate__SetAccessorProperty__DEFAULTX overloads with
    new v8__ObjectTemplate__SetAccessorProperty__Config
4 - Remove unnecessary @constCast for FunctionTemplate which can be const
    everywhere.
2026-03-26 09:15:33 +08:00
14 changed files with 243 additions and 42 deletions

View File

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

View File

@@ -7,7 +7,7 @@ env:
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
VERSION: ${{ github.ref_type == 'tag' && github.ref_name || '1.0.0-nightly' }}
VERSION_FLAG: ${{ github.ref_type == 'tag' && format('-Dversion_string={0}', github.ref_name) || format('-Dpre_version={0}', 'nightly') }}
on:
push:
@@ -45,7 +45,7 @@ jobs:
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
- name: zig build
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dversion_string=${{ env.VERSION }}
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 ${{ env.VERSION_FLAG }}
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -85,7 +85,7 @@ jobs:
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
- name: zig build
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dversion_string=${{ env.VERSION }}
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic ${{ env.VERSION_FLAG }}
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -127,7 +127,7 @@ jobs:
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
- name: zig build
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dversion_string=${{ env.VERSION }}
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast ${{ env.VERSION_FLAG }}
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -167,7 +167,7 @@ jobs:
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
- name: zig build
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dversion_string=${{ env.VERSION }}
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast ${{ env.VERSION_FLAG }}
- name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}

View File

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

View File

@@ -728,8 +728,17 @@ fn resolveVersion(b: *std.Build) std.SemanticVersion {
};
}
const pre_version = b.option([]const u8, "pre_version", "Override the pre version of this build");
const pre = blk: {
if (pre_version) |pre| {
break :blk pre;
}
break :blk lightpanda_version.pre;
};
// If it's a stable release (no pre or build metadata in build.zig.zon), use it as is
if (lightpanda_version.pre == null and lightpanda_version.build == null) return lightpanda_version;
if (pre == null and lightpanda_version.build == null) return lightpanda_version;
// For dev/nightly versions, calculate the commit count and hash
const git_hash_raw = runGit(b, &.{ "rev-parse", "--short", "HEAD" }) catch return lightpanda_version;
@@ -742,7 +751,7 @@ fn resolveVersion(b: *std.Build) std.SemanticVersion {
.major = lightpanda_version.major,
.minor = lightpanda_version.minor,
.patch = lightpanda_version.patch,
.pre = b.fmt("{s}.{s}", .{ lightpanda_version.pre.?, commit_count }),
.pre = b.fmt("{s}.{s}", .{ pre.?, commit_count }),
.build = commit_hash,
};
}

View File

@@ -5,8 +5,8 @@
.minimum_zig_version = "0.15.2",
.dependencies = .{
.v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.5.tar.gz",
.hash = "v8-0.0.0-xddH66Z5BABx8CmC6u6qNOjrT4_42uliDSnA7Yg0pcBe",
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.7.tar.gz",
.hash = "v8-0.0.0-xddH67uBBAD95hWsPQz3Ni1PlZjdywtPXrGUAp8rSKco",
},
// .v8 = .{ .path = "../zig-v8-fork" },
.brotli = .{

View File

@@ -247,7 +247,7 @@ pub const Fetch = struct {
with_frames: bool = false,
strip: dump.Opts.Strip = .{},
wait_ms: u32 = 5000,
wait_until: WaitUntil = .load,
wait_until: WaitUntil = .done,
};
pub const Common = struct {
@@ -665,7 +665,7 @@ fn parseFetchArgs(
var common: Common = .{};
var strip: dump.Opts.Strip = .{};
var wait_ms: u32 = 5000;
var wait_until: WaitUntil = .load;
var wait_until: WaitUntil = .done;
while (args.next()) |opt| {
if (std.mem.eql(u8, "--wait-ms", opt) or std.mem.eql(u8, "--wait_ms", opt)) {

View File

@@ -404,10 +404,6 @@ pub fn getOrigin(allocator: Allocator, raw: [:0]const u8) !?[]const u8 {
}
var authority_start = scheme_end + 3;
const has_user_info = if (std.mem.indexOf(u8, raw[authority_start..], "@")) |pos| blk: {
authority_start += pos + 1;
break :blk true;
} else false;
// Find end of authority (start of path/query/fragment or end of string)
const authority_end_relative = std.mem.indexOfAny(u8, raw[authority_start..], "/?#");
@@ -416,6 +412,12 @@ pub fn getOrigin(allocator: Allocator, raw: [:0]const u8) !?[]const u8 {
else
raw.len;
// We mustn't search the `@` after the first path separator.
const has_user_info = if (std.mem.indexOf(u8, raw[authority_start..authority_end], "@")) |pos| blk: {
authority_start += pos + 1;
break :blk true;
} else false;
// Check for port in the host:port section
const host_part = raw[authority_start..authority_end];
if (std.mem.lastIndexOfScalar(u8, host_part, ':')) |colon_pos_in_host| {
@@ -461,8 +463,15 @@ fn getUserInfo(raw: [:0]const u8) ?[]const u8 {
const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return null;
const authority_start = scheme_end + 3;
const pos = std.mem.indexOfScalar(u8, raw[authority_start..], '@') orelse return null;
const path_start = std.mem.indexOfScalarPos(u8, raw, authority_start, '/') orelse raw.len;
// We mustn't search the `@` after the first path separator.
const path_start = blk: {
if (std.mem.indexOfAny(u8, raw[authority_start..], "/?#")) |idx| {
break :blk authority_start + idx;
}
break :blk raw.len;
};
const pos = std.mem.indexOfScalar(u8, raw[authority_start..path_start], '@') orelse return null;
const full_pos = authority_start + pos;
if (full_pos < path_start) {
@@ -476,13 +485,20 @@ pub fn getHost(raw: [:0]const u8) []const u8 {
const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return "";
var authority_start = scheme_end + 3;
if (std.mem.indexOf(u8, raw[authority_start..], "@")) |pos| {
// We mustn't search the `@` after the first path separator.
const path_start = blk: {
if (std.mem.indexOfAny(u8, raw[authority_start..], "/?#")) |idx| {
break :blk authority_start + idx;
}
break :blk raw.len;
};
if (std.mem.indexOf(u8, raw[authority_start..path_start], "@")) |pos| {
authority_start += pos + 1;
}
const authority = raw[authority_start..];
const path_start = std.mem.indexOfAny(u8, authority, "/?#") orelse return authority;
return authority[0..path_start];
return raw[authority_start..path_start];
}
// Returns true if these two URLs point to the same document.
@@ -1449,3 +1465,36 @@ test "URL: setPathname percent-encodes" {
const result3 = try setPathname("https://example.com/path?a=b#hash", "/new path", allocator);
try testing.expectEqualSlices(u8, "https://example.com/new%20path?a=b#hash", result3);
}
test "URL: getOrigin" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
try testing.expectEqualSlices(u8, "http://example.com:8080", try getOrigin(allocator, "http://example.com:8080/path") orelse unreachable);
try testing.expectEqualSlices(u8, "https://example.com:8080", try getOrigin(allocator, "https://example.com:8080/path") orelse unreachable);
try testing.expectEqualSlices(u8, "https://example.com", try getOrigin(allocator, "https://example.com/path") orelse unreachable);
try testing.expectEqualSlices(u8, "https://example.com", try getOrigin(allocator, "https://example.com:443/") orelse unreachable);
try testing.expectEqualSlices(u8, "https://example.com", try getOrigin(allocator, "https://user:pass@example.com/page") orelse unreachable);
try testing.expectEqualSlices(u8, "https://example.com:8080", try getOrigin(allocator, "https://user:pass@example.com:8080/page") orelse unreachable);
try testing.expectEqual(null, try getOrigin(allocator, "not-a-url"));
}
test "URL: SOP bypass" {
// SOP Bypass
try testing.expectEqualSlices(u8, "attacker.com", getHost("http://attacker.com/@bank.com/"));
try testing.expectEqualSlices(u8, "attacker.com", getHost("https://attacker.com/@bank.com/"));
try testing.expectEqualSlices(u8, "attacker.com", getHost("http://attacker.com?@bank.com/"));
try testing.expectEqualSlices(u8, "attacker.com", getHost("http://attacker.com#@bank.com/"));
try testing.expectEqualSlices(u8, "attacker.com", getHostname("http://attacker.com/@bank.com/"));
try testing.expectEqualSlices(u8, "attacker.com", getHostname("https://attacker.com/@bank.com/"));
try testing.expectEqualSlices(u8, "attacker.com", getHostname("http://attacker.com?@bank.com/"));
try testing.expectEqualSlices(u8, "attacker.com", getHostname("http://attacker.com#@bank.com/"));
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
try testing.expectEqualSlices(u8, "http://attacker.com", try getOrigin(allocator, "http://attacker.com/@bank.com/") orelse unreachable);
try testing.expectEqualSlices(u8, "https://attacker.com", try getOrigin(allocator, "https://attacker.com/@bank.com/") orelse unreachable);
try testing.expectEqualSlices(u8, "http://attacker.com", try getOrigin(allocator, "http://attacker.com?bank.com/") orelse unreachable);
try testing.expectEqualSlices(u8, "http://attacker.com", try getOrigin(allocator, "http://attacker.com#bank.com/") orelse unreachable);
}

View File

@@ -505,6 +505,7 @@ pub const Function = struct {
pub const Opts = struct {
noop: bool = false,
static: bool = false,
deletable: bool = true,
dom_exception: bool = false,
as_typed_array: bool = false,
null_as_undefined: bool = false,

View File

@@ -137,7 +137,7 @@ pub fn create() !Snapshot {
defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
// Create templates (constructors only) FIRST
var templates: [JsApis.len]*v8.FunctionTemplate = undefined;
var templates: [JsApis.len]*const v8.FunctionTemplate = undefined;
inline for (JsApis, 0..) |JsApi, i| {
@setEvalBranchQuota(10_000);
templates[i] = generateConstructor(JsApi, isolate);
@@ -419,7 +419,7 @@ fn collectExternalReferences() [countExternalReferences()]isize {
// via `new ClassName()` - but they could, for example, be created in
// Zig and returned from a function call, which is why we need the
// FunctionTemplate.
fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionTemplate {
fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate {
const callback = blk: {
if (@hasDecl(JsApi, "constructor")) {
break :blk JsApi.constructor.func;
@@ -429,7 +429,7 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionT
break :blk illegalConstructorCallback;
};
const template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?);
const template = v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?;
{
const internal_field_count = comptime countInternalFields(JsApi);
if (internal_field_count > 0) {
@@ -482,10 +482,15 @@ pub fn countInternalFields(comptime JsApi: type) u8 {
}
// Attaches JsApi members to the prototype template (normal case)
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void {
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.FunctionTemplate) void {
const instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template);
// Create a signature that validates the receiver is an instance of this template.
// This prevents crashes when JavaScript extracts a getter/method and calls it
// with the wrong `this` (e.g., documentGetter.call(null)).
const signature = v8.v8__Signature__New(isolate, template);
const declarations = @typeInfo(JsApi).@"struct".decls;
var has_named_index_getter = false;
@@ -497,23 +502,47 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
switch (definition) {
bridge.Accessor => {
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
const getter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.getter }).?);
const getter_signature = if (value.static) null else signature;
const getter_callback = v8.v8__FunctionTemplate__New__Config(isolate, &.{
.callback = value.getter,
.signature = getter_signature,
}).?;
const setter_callback = if (value.setter) |setter|
v8.v8__FunctionTemplate__New__Config(isolate, &.{
.callback = setter,
.signature = getter_signature,
}).?
else
null;
var attribute: v8.PropertyAttribute = 0;
if (value.setter == null) {
if (value.static) {
v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
} else {
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(prototype, js_name, getter_callback);
}
attribute |= v8.ReadOnly;
}
if (value.deletable == false) {
attribute |= v8.DontDelete;
}
if (value.static) {
// Static accessors: use Template's SetAccessorProperty
v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute);
} else {
if (comptime IS_DEBUG) {
std.debug.assert(value.static == false);
}
const setter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.setter.? }).?);
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(prototype, js_name, getter_callback, setter_callback);
v8.v8__ObjectTemplate__SetAccessorProperty__Config(prototype, &.{
.key = js_name,
.getter = getter_callback,
.setter = setter_callback,
.attribute = attribute,
});
}
},
bridge.Function => {
const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func, .length = value.arity }).?);
// For non-static functions, use the signature to validate the receiver
const func_signature = if (value.static) null else signature;
const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{
.callback = value.func,
.length = value.arity,
.signature = func_signature,
}).?;
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
if (value.static) {
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
@@ -551,7 +580,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
has_named_index_getter = true;
},
bridge.Iterator => {
const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func }).?);
const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func }).?;
const js_name = if (value.async)
v8.v8__Symbol__GetAsyncIterator(isolate)
else

View File

@@ -198,6 +198,7 @@ pub const Function = struct {
pub const Accessor = struct {
static: bool = false,
deletable: bool = true,
cache: ?Caller.Function.Opts.Caching = null,
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
@@ -206,6 +207,7 @@ pub const Accessor = struct {
var accessor = Accessor{
.cache = opts.cache,
.static = opts.static,
.deletable = opts.deletable,
};
if (@typeInfo(@TypeOf(getter)) != .null) {

View File

@@ -871,3 +871,33 @@
testing.expectEqual('', url.search);
}
</script>
<script id="SOP Bypass">
{
const url = new URL('http://example.com/@bank.com');
testing.expectEqual('http:', url.protocol);
testing.expectEqual('example.com', url.hostname);
testing.expectEqual('', url.port);
testing.expectEqual('http://example.com', url.origin);
testing.expectEqual('', url.username);
testing.expectEqual('', url.password);
}
{
const url = new URL('http://example.com?@bank.com');
testing.expectEqual('http:', url.protocol);
testing.expectEqual('example.com', url.hostname);
testing.expectEqual('', url.port);
testing.expectEqual('http://example.com', url.origin);
testing.expectEqual('', url.username);
testing.expectEqual('', url.password);
}
{
const url = new URL('http://example.com#@bank.com');
testing.expectEqual('http:', url.protocol);
testing.expectEqual('example.com', url.hostname);
testing.expectEqual('', url.port);
testing.expectEqual('http://example.com', url.origin);
testing.expectEqual('', url.username);
testing.expectEqual('', url.password);
}
</script>

View File

@@ -262,6 +262,31 @@
}
</script>
<script id=cached_getter_wrong_this>
// Test that extracting a cached property getter and calling it with wrong `this`
// doesn't crash (V8 internal field out of bounds). V8's Signature validation
// should throw "Illegal invocation" for wrong receiver types.
const documentGetter = Object.getOwnPropertyDescriptor(Window.prototype, 'document').get;
// Verify we get an error with wrong this values
let errorCount = 0;
const testValues = [{}, null, undefined, 42, 'string', [], () => {}];
for (const val of testValues) {
try {
documentGetter.call(val);
} catch (e) {
if (e.message.includes('Illegal invocation')) {
errorCount++;
}
}
}
// At least some should throw (null/undefined/primitives get coerced in sloppy mode)
testing.expectEqual(true, errorCount > 0);
// Calling with correct this should still work
testing.expectEqual(document, documentGetter.call(window));
</script>
<script id=unhandled_rejection>
{
let unhandledCalled = 0;

View File

@@ -829,7 +829,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = .{ .internal = 1 } });
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = .{ .internal = 1 }, .deletable = false });
pub const console = bridge.accessor(Window.getConsole, null, .{ .cache = .{ .internal = 2 } });
pub const top = bridge.accessor(Window.getTop, null, .{});
@@ -842,7 +842,7 @@ pub const JsApi = struct {
pub const performance = bridge.accessor(Window.getPerformance, null, .{});
pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{});
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{});
pub const location = bridge.accessor(Window.getLocation, Window.setLocation, .{});
pub const location = bridge.accessor(Window.getLocation, Window.setLocation, .{ .deletable = false });
pub const history = bridge.accessor(Window.getHistory, null, .{});
pub const navigation = bridge.accessor(Window.getNavigation, null, .{});
pub const crypto = bridge.accessor(Window.getCrypto, null, .{});

View File

@@ -39,6 +39,7 @@ pub fn processMessage(cmd: anytype) !void {
addScriptToEvaluateOnNewDocument,
createIsolatedWorld,
navigate,
reload,
stopLoading,
close,
captureScreenshot,
@@ -52,6 +53,7 @@ pub fn processMessage(cmd: anytype) !void {
.addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd),
.createIsolatedWorld => return createIsolatedWorld(cmd),
.navigate => return navigate(cmd),
.reload => return doReload(cmd),
.stopLoading => return cmd.sendResult(null, .{}),
.close => return close(cmd),
.captureScreenshot => return captureScreenshot(cmd),
@@ -252,6 +254,36 @@ fn navigate(cmd: anytype) !void {
});
}
fn doReload(cmd: anytype) !void {
const params = try cmd.params(struct {
ignoreCache: ?bool = null,
scriptToEvaluateOnLoad: ?[]const u8 = null,
});
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
if (bc.session_id == null) {
return error.SessionIdNotLoaded;
}
const session = bc.session;
var page = session.currentPage() orelse return error.PageNotLoaded;
// Dupe URL before replacePage() frees the old page's arena.
const reload_url = try cmd.arena.dupeZ(u8, page.url);
if (page._load_state != .waiting) {
page = try session.replacePage();
}
try page.navigate(reload_url, .{
.reason = .address_bar,
.cdp_id = cmd.input.id,
.kind = .reload,
.force = if (params) |p| p.ignoreCache orelse false else false,
});
}
pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
@@ -784,3 +816,27 @@ test "cdp.page: getLayoutMetrics" {
},
}, .{ .id = 12 });
}
test "cdp.page: reload" {
var ctx = try testing.context();
defer ctx.deinit();
{
// reload without browser context — should error
try ctx.processMessage(.{ .id = 30, .method = "Page.reload" });
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 30 });
}
_ = try ctx.loadBrowserContext(.{ .id = "BID-9", .url = "hi.html", .target_id = "FID-000000000X".* });
{
// reload with no params — should not error (navigation is async,
// so no result is sent synchronously; we just verify no error)
try ctx.processMessage(.{ .id = 31, .method = "Page.reload" });
}
{
// reload with ignoreCache param
try ctx.processMessage(.{ .id = 32, .method = "Page.reload", .params = .{ .ignoreCache = true } });
}
}