mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 01:36:15 -04:00
Merge branch 'main' into agent-async-goto-parallel-navigation
This commit is contained in:
4
.github/actions/install/action.yml
vendored
4
.github/actions/install/action.yml
vendored
@@ -13,11 +13,11 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.4.8'
|
||||
default: 'v0.4.9'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
default: '14.0.365.4'
|
||||
default: '14.9.207.35'
|
||||
cache-dir:
|
||||
description: 'cache dir to use'
|
||||
required: false
|
||||
|
||||
2
.github/actions/v8-snapshot/action.yml
vendored
2
.github/actions/v8-snapshot/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig-v8 release tag the prebuilt lib came from'
|
||||
required: false
|
||||
default: 'v0.4.8'
|
||||
default: 'v0.4.9'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -161,6 +161,12 @@ jobs:
|
||||
with:
|
||||
name: lightpanda-x86_64-linux
|
||||
|
||||
- uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: "Generate and publish versions.json"
|
||||
run: |
|
||||
chmod +x lightpanda-x86_64-linux
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
|
||||
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.4.8
|
||||
ARG V8=14.9.207.35
|
||||
ARG ZIG_V8=v0.4.9
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
|
||||
110
build.zig
110
build.zig
@@ -511,6 +511,7 @@ fn buildCurl(
|
||||
|
||||
.USE_OPENSSL = true,
|
||||
.OPENSSL_IS_BORINGSSL = true,
|
||||
.CURL_BORINGSSL_VERSION = null,
|
||||
.CURL_CA_PATH = null,
|
||||
.CURL_CA_BUNDLE = null,
|
||||
.CURL_CA_FALLBACK = false,
|
||||
@@ -607,6 +608,7 @@ fn buildCurl(
|
||||
|
||||
// general environment
|
||||
.CURL_KRB5_VERSION = null,
|
||||
.CURL_PATCHSTAMP = null,
|
||||
.HAVE_ALARM = !is_windows,
|
||||
.HAVE_ARC4RANDOM = is_android,
|
||||
.HAVE_ATOMIC = true,
|
||||
@@ -626,6 +628,7 @@ fn buildCurl(
|
||||
.HAVE_MEMRCHR = !is_darwin and !is_windows,
|
||||
.HAVE_POSIX_STRERROR_R = !is_gnu and !is_windows,
|
||||
.HAVE_PTHREAD_H = !is_windows,
|
||||
.HAVE_THREADS_POSIX = !is_windows,
|
||||
.HAVE_SETLOCALE = true,
|
||||
.HAVE_SETRLIMIT = !is_windows,
|
||||
.HAVE_SIGACTION = !is_windows,
|
||||
@@ -647,6 +650,7 @@ fn buildCurl(
|
||||
.HAVE_WRITABLE_ARGV = !is_windows,
|
||||
.HAVE__SETMODE = is_windows,
|
||||
.USE_THREADS_POSIX = !is_windows,
|
||||
.USE_RESOLV_THREADED = !is_windows,
|
||||
|
||||
// filesystem, network
|
||||
.HAVE_ACCEPT4 = is_linux or is_freebsd or is_netbsd or is_openbsd,
|
||||
@@ -734,66 +738,52 @@ fn buildCurl(
|
||||
},
|
||||
.files = &.{
|
||||
// You can include all files from lib, libcurl uses #ifdef-guards to exclude code for disabled functions
|
||||
"altsvc.c", "amigaos.c", "asyn-ares.c",
|
||||
"asyn-base.c", "asyn-thrdd.c", "bufq.c",
|
||||
"bufref.c", "cf-h1-proxy.c", "cf-h2-proxy.c",
|
||||
"cf-haproxy.c", "cf-https-connect.c", "cf-ip-happy.c",
|
||||
"cf-socket.c", "cfilters.c", "conncache.c",
|
||||
"connect.c", "content_encoding.c", "cookie.c",
|
||||
"cshutdn.c", "curl_addrinfo.c", "curl_endian.c",
|
||||
"curl_fnmatch.c", "curl_fopen.c", "curl_get_line.c",
|
||||
"curl_gethostname.c", "curl_gssapi.c", "curl_memrchr.c",
|
||||
"curl_ntlm_core.c", "curl_range.c", "curl_rtmp.c",
|
||||
"curl_sasl.c", "curl_sha512_256.c", "curl_share.c",
|
||||
"curl_sspi.c", "curl_threads.c", "curl_trc.c",
|
||||
"curlx/base64.c", "curlx/dynbuf.c", "curlx/fopen.c",
|
||||
"curlx/inet_ntop.c", "curlx/inet_pton.c", "curlx/multibyte.c",
|
||||
"curlx/nonblock.c", "curlx/strcopy.c", "curlx/strerr.c",
|
||||
"curlx/strparse.c", "curlx/timediff.c", "curlx/timeval.c",
|
||||
"curlx/version_win32.c", "curlx/wait.c", "curlx/warnless.c",
|
||||
"curlx/winapi.c", "cw-out.c", "cw-pause.c",
|
||||
"dict.c", "dllmain.c", "doh.c",
|
||||
"dynhds.c", "easy.c", "easygetopt.c",
|
||||
"easyoptions.c", "escape.c", "fake_addrinfo.c",
|
||||
"file.c", "fileinfo.c", "formdata.c",
|
||||
"ftp.c", "ftplistparser.c", "getenv.c",
|
||||
"getinfo.c", "gopher.c", "hash.c",
|
||||
"headers.c", "hmac.c", "hostip.c",
|
||||
"hostip4.c", "hostip6.c", "hsts.c",
|
||||
"http.c", "http1.c", "http2.c",
|
||||
"http_aws_sigv4.c", "http_chunks.c", "http_digest.c",
|
||||
"http_negotiate.c", "http_ntlm.c", "http_proxy.c",
|
||||
"httpsrr.c", "idn.c", "if2ip.c",
|
||||
"imap.c", "ldap.c", "llist.c",
|
||||
"macos.c", "md4.c", "md5.c",
|
||||
"memdebug.c", "mime.c", "mprintf.c",
|
||||
"mqtt.c", "multi.c", "multi_ev.c",
|
||||
"multi_ntfy.c", "netrc.c", "noproxy.c",
|
||||
"openldap.c", "parsedate.c", "pingpong.c",
|
||||
"pop3.c", "progress.c", "psl.c",
|
||||
"rand.c", "ratelimit.c", "request.c",
|
||||
"rtsp.c", "select.c", "sendf.c",
|
||||
"setopt.c", "sha256.c", "slist.c",
|
||||
"smb.c", "smtp.c", "socketpair.c",
|
||||
"socks.c", "socks_gssapi.c", "socks_sspi.c",
|
||||
"splay.c", "strcase.c", "strdup.c",
|
||||
"strequal.c", "strerror.c", "system_win32.c",
|
||||
"telnet.c", "tftp.c", "transfer.c",
|
||||
"uint-bset.c", "uint-hash.c", "uint-spbset.c",
|
||||
"uint-table.c", "url.c", "urlapi.c",
|
||||
"vauth/cleartext.c", "vauth/cram.c", "vauth/digest.c",
|
||||
"vauth/digest_sspi.c", "vauth/gsasl.c", "vauth/krb5_gssapi.c",
|
||||
"vauth/krb5_sspi.c", "vauth/ntlm.c", "vauth/ntlm_sspi.c",
|
||||
"vauth/oauth2.c", "vauth/spnego_gssapi.c", "vauth/spnego_sspi.c",
|
||||
"vauth/vauth.c", "version.c", "vquic/curl_ngtcp2.c",
|
||||
"vquic/curl_osslq.c", "vquic/curl_quiche.c", "vquic/vquic-tls.c",
|
||||
"vquic/vquic.c", "vssh/libssh.c", "vssh/libssh2.c",
|
||||
"vssh/vssh.c", "vtls/apple.c", "vtls/cipher_suite.c",
|
||||
"vtls/gtls.c", "vtls/hostcheck.c", "vtls/keylog.c",
|
||||
"vtls/mbedtls.c", "vtls/openssl.c", "vtls/rustls.c",
|
||||
"vtls/schannel.c", "vtls/schannel_verify.c", "vtls/vtls.c",
|
||||
"vtls/vtls_scache.c", "vtls/vtls_spack.c", "vtls/wolfssl.c",
|
||||
"vtls/x509asn1.c", "ws.c",
|
||||
"cf-dns.c", "dnscache.c", "protocol.c", "curlx/strdup.c",
|
||||
"thrdpool.c", "thrdqueue.c", "altsvc.c", "amigaos.c",
|
||||
"asyn-ares.c", "asyn-base.c", "asyn-thrdd.c", "bufq.c",
|
||||
"bufref.c", "cf-h1-proxy.c", "cf-h2-proxy.c", "cf-haproxy.c",
|
||||
"cf-https-connect.c", "cf-ip-happy.c", "cf-socket.c", "cfilters.c",
|
||||
"conncache.c", "connect.c", "content_encoding.c", "cookie.c",
|
||||
"cshutdn.c", "curl_addrinfo.c", "curl_endian.c", "curl_fnmatch.c",
|
||||
"curl_fopen.c", "curl_get_line.c", "curl_gethostname.c", "curl_gssapi.c",
|
||||
"curl_memrchr.c", "curl_ntlm_core.c", "curl_range.c", "curl_sasl.c",
|
||||
"curl_sha512_256.c", "curl_share.c", "curl_sspi.c", "curl_threads.c",
|
||||
"curl_trc.c", "curlx/base64.c", "curlx/dynbuf.c", "curlx/fopen.c",
|
||||
"curlx/inet_ntop.c", "curlx/inet_pton.c", "curlx/multibyte.c", "curlx/nonblock.c",
|
||||
"curlx/strcopy.c", "curlx/strerr.c", "curlx/strparse.c", "curlx/timediff.c",
|
||||
"curlx/timeval.c", "curlx/version_win32.c", "curlx/wait.c", "curlx/warnless.c",
|
||||
"curlx/winapi.c", "cw-out.c", "cw-pause.c", "dict.c",
|
||||
"dllmain.c", "doh.c", "dynhds.c", "easy.c",
|
||||
"easygetopt.c", "easyoptions.c", "escape.c", "fake_addrinfo.c",
|
||||
"file.c", "fileinfo.c", "formdata.c", "ftp.c",
|
||||
"ftplistparser.c", "getenv.c", "getinfo.c", "gopher.c",
|
||||
"hash.c", "headers.c", "hmac.c", "hostip.c",
|
||||
"hostip4.c", "hostip6.c", "hsts.c", "http.c",
|
||||
"http1.c", "http2.c", "http_aws_sigv4.c", "http_chunks.c",
|
||||
"http_digest.c", "http_negotiate.c", "http_ntlm.c", "http_proxy.c",
|
||||
"httpsrr.c", "idn.c", "if2ip.c", "imap.c",
|
||||
"ldap.c", "llist.c", "macos.c", "md4.c",
|
||||
"md5.c", "memdebug.c", "mime.c", "mprintf.c",
|
||||
"mqtt.c", "multi.c", "multi_ev.c", "multi_ntfy.c",
|
||||
"netrc.c", "noproxy.c", "openldap.c", "parsedate.c",
|
||||
"pingpong.c", "pop3.c", "progress.c", "psl.c",
|
||||
"rand.c", "ratelimit.c", "request.c", "rtsp.c",
|
||||
"select.c", "sendf.c", "setopt.c", "sha256.c",
|
||||
"slist.c", "smb.c", "smtp.c", "socketpair.c",
|
||||
"socks.c", "socks_gssapi.c", "socks_sspi.c", "splay.c",
|
||||
"strcase.c", "strequal.c", "strerror.c", "system_win32.c",
|
||||
"telnet.c", "tftp.c", "transfer.c", "uint-bset.c",
|
||||
"uint-hash.c", "uint-spbset.c", "uint-table.c", "url.c",
|
||||
"urlapi.c", "vauth/cleartext.c", "vauth/cram.c", "vauth/digest.c",
|
||||
"vauth/digest_sspi.c", "vauth/gsasl.c", "vauth/krb5_gssapi.c", "vauth/krb5_sspi.c",
|
||||
"vauth/ntlm.c", "vauth/ntlm_sspi.c", "vauth/oauth2.c", "vauth/spnego_gssapi.c",
|
||||
"vauth/spnego_sspi.c", "vauth/vauth.c", "version.c", "vquic/curl_ngtcp2.c",
|
||||
"vquic/curl_quiche.c", "vquic/vquic-tls.c", "vquic/vquic.c", "vssh/libssh.c",
|
||||
"vssh/libssh2.c", "vssh/vssh.c", "vtls/apple.c", "vtls/cipher_suite.c",
|
||||
"vtls/gtls.c", "vtls/hostcheck.c", "vtls/keylog.c", "vtls/mbedtls.c",
|
||||
"vtls/openssl.c", "vtls/rustls.c", "vtls/schannel.c", "vtls/schannel_verify.c",
|
||||
"vtls/vtls.c", "vtls/vtls_scache.c", "vtls/vtls_spack.c", "vtls/wolfssl.c",
|
||||
"vtls/x509asn1.c", "ws.c",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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.4.8.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH65qdBAA1hmB-gqEmhEzO0-aXKyEsmo1s34mssemb",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.4.9.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH63jnAgB8guO5jPzJD4pnX-CI5od-g4v-fIIjmQXr",
|
||||
},
|
||||
// .v8 = .{ .path = "../zig-v8-fork" },
|
||||
.brotli = .{
|
||||
@@ -19,16 +19,16 @@
|
||||
.hash = "N-V-__8AAJ2cNgAgfBtAw33Bxfu1IWISDeKKSr3DAqoAysIJ",
|
||||
},
|
||||
.nghttp2 = .{
|
||||
.url = "https://github.com/nghttp2/nghttp2/releases/download/v1.68.0/nghttp2-1.68.0.tar.gz",
|
||||
.hash = "N-V-__8AAL15vQCI63ZL6Zaz5hJg6JTEgYXGbLnMFSnf7FT3",
|
||||
.url = "https://github.com/nghttp2/nghttp2/releases/download/v1.69.0/nghttp2-1.69.0.tar.gz",
|
||||
.hash = "N-V-__8AAMHpvwChC6orFCDB8MeiumT6OlfmKfmrWlak7Int",
|
||||
},
|
||||
.@"boringssl-zig" = .{
|
||||
.url = "git+https://github.com/Syndica/boringssl-zig.git#c53df00d06b02b755ad88bbf4d1202ed9687b096",
|
||||
.hash = "boringssl-0.1.0-VtJeWehMAAA4RNnwRnzEvKcS9rjsR1QVRw1uJrwXxmVK",
|
||||
},
|
||||
.curl = .{
|
||||
.url = "https://github.com/curl/curl/releases/download/curl-8_18_0/curl-8.18.0.tar.gz",
|
||||
.hash = "N-V-__8AALp9QAGn6CCHZ6fK_FfMyGtG824LSHYHHasM3w-y",
|
||||
.url = "https://github.com/curl/curl/releases/download/curl-8_20_0/curl-8.20.0.tar.gz",
|
||||
.hash = "N-V-__8AAHM9RQHjzJo8Wn4dbGPqNOC21zZJJA9PcCj7FPF5",
|
||||
},
|
||||
.sqlite3 = .{
|
||||
.url = "https://github.com/allyourcodebase/sqlite3/archive/8f840560eae88ab66668c6827c64ffbd0d74ef37.tar.gz",
|
||||
|
||||
@@ -32,7 +32,6 @@ const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
const Document = @import("webapi/Document.zig");
|
||||
const EventTarget = @import("webapi/EventTarget.zig");
|
||||
const WorkerGlobalScope = @import("webapi/WorkerGlobalScope.zig");
|
||||
const XMLHttpRequestEventTarget = @import("webapi/net/XMLHttpRequestEventTarget.zig");
|
||||
const Blob = @import("webapi/Blob.zig");
|
||||
const AbstractRange = @import("webapi/AbstractRange.zig");
|
||||
|
||||
@@ -40,7 +40,6 @@ const FileList = @import("webapi/FileList.zig");
|
||||
const Node = @import("webapi/Node.zig");
|
||||
const Event = @import("webapi/Event.zig");
|
||||
const EventTarget = @import("webapi/EventTarget.zig");
|
||||
const Text = @import("webapi/cdata/Text.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
const HtmlElement = @import("webapi/element/Html.zig");
|
||||
const Window = @import("webapi/Window.zig");
|
||||
@@ -51,8 +50,6 @@ const Performance = @import("webapi/Performance.zig");
|
||||
const Screen = @import("webapi/Screen.zig");
|
||||
const VisualViewport = @import("webapi/VisualViewport.zig");
|
||||
const AbstractRange = @import("webapi/AbstractRange.zig");
|
||||
const MutationObserver = @import("webapi/MutationObserver.zig");
|
||||
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
|
||||
const Worker = @import("webapi/Worker.zig");
|
||||
const CSSStyleSheet = @import("webapi/css/CSSStyleSheet.zig");
|
||||
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
|
||||
@@ -817,7 +814,7 @@ fn scheduleNavigationWithArena(originator: *Frame, arena: Allocator, request_url
|
||||
arena,
|
||||
frame_base,
|
||||
request_url,
|
||||
.{ .always_dupe = true, .encoding = originator.charset },
|
||||
.{ .encoding = originator.charset },
|
||||
);
|
||||
break :blk .{ u, false };
|
||||
};
|
||||
@@ -1769,7 +1766,7 @@ pub fn openPopup(self: *Frame, opts: OpenPopupOpts) !*Frame {
|
||||
frame = frame.parent orelse break :base_blk "";
|
||||
}
|
||||
};
|
||||
break :blk try URL.resolve(self.call_arena, frame_base, opts.url, .{ .always_dupe = true, .encoding = self.charset });
|
||||
break :blk try URL.resolve(self.call_arena, frame_base, opts.url, .{ .encoding = self.charset });
|
||||
};
|
||||
|
||||
const popup = try page.frame_arena.create(Frame);
|
||||
|
||||
@@ -1048,7 +1048,7 @@ fn isTeardownMethod(method: []const u8) bool {
|
||||
std.mem.eql(u8, method, "Page.close");
|
||||
}
|
||||
|
||||
fn isRedirectStatus(status: u16) bool {
|
||||
pub fn isRedirectStatus(status: u16) bool {
|
||||
return switch (status) {
|
||||
301, 302, 303, 307, 308 => true,
|
||||
else => false,
|
||||
@@ -1937,12 +1937,6 @@ pub const Transfer = struct {
|
||||
fn handleRedirect(transfer: *Transfer, location: []const u8) !void {
|
||||
const req = &transfer.req;
|
||||
const conn = transfer._conn.?;
|
||||
const arena = transfer.arena;
|
||||
|
||||
transfer._redirect_count += 1;
|
||||
if (transfer._redirect_count > transfer.client.network.config.httpMaxRedirects()) {
|
||||
return error.TooManyRedirects;
|
||||
}
|
||||
|
||||
// retrieve cookies from the redirect's response.
|
||||
if (req.cookie_jar) |jar| {
|
||||
@@ -1956,18 +1950,32 @@ pub const Transfer = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// base_url and location are owned by curl; applyRedirectTarget resolves a
|
||||
// fresh arena-owned copy that gets stored in transfer.req.url.
|
||||
const base_url = try conn.getEffectiveUrl();
|
||||
const status = try conn.getResponseCode();
|
||||
try transfer.applyRedirectTarget(std.mem.span(base_url), location, status);
|
||||
}
|
||||
|
||||
// Called above (in handleRedirect) and by a CDP fulfill request which redirects
|
||||
pub fn applyRedirectTarget(transfer: *Transfer, base: [:0]const u8, location: []const u8, status: u16) !void {
|
||||
const req = &transfer.req;
|
||||
const arena = transfer.arena;
|
||||
|
||||
transfer._redirect_count += 1;
|
||||
if (transfer._redirect_count > transfer.client.network.config.httpMaxRedirects()) {
|
||||
return error.TooManyRedirects;
|
||||
}
|
||||
|
||||
// resolve the redirect target.
|
||||
const url: [:0]const u8 = blk: {
|
||||
if (location.len == 0) {
|
||||
// Might seem silly, but URL.resovle will return location as-is
|
||||
// if empty, and location is memory owned by libcurl.
|
||||
// if empty, and location may be memory owned by libcurl.
|
||||
break :blk "";
|
||||
}
|
||||
|
||||
const base_url = try conn.getEffectiveUrl();
|
||||
// base_url and location 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, .{ .always_dupe = true });
|
||||
const resolved = try URL.resolve(arena, base, location, .{});
|
||||
|
||||
// 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.
|
||||
@@ -1985,7 +1993,6 @@ pub const Transfer = struct {
|
||||
try transfer.updateURL(url);
|
||||
// 301, 302, 303 → change to GET, drop body.
|
||||
// 307, 308 → keep method and body.
|
||||
const status = try conn.getResponseCode();
|
||||
if (status == 301 or status == 302 or status == 303) {
|
||||
req.method = .GET;
|
||||
req.body = null;
|
||||
@@ -2478,6 +2485,196 @@ test "HttpClient: fulfillRequest survives a done_callback that tears down the ow
|
||||
try testing.expectEqual(null, owner.transfers.first);
|
||||
}
|
||||
|
||||
test "HttpClient: fulfillRequest follows a 3xx redirect" {
|
||||
// Regression for #2828: a CDP Fetch.fulfillRequest with a 3xx status + a
|
||||
// Location header must be followed like a real network redirect (re-issued
|
||||
// down the chain to the resolved target), not delivered as a final response.
|
||||
var pool = ArenaPool.init(testing.allocator, .{});
|
||||
defer pool.deinit();
|
||||
|
||||
// Only network.config is read (httpMaxRedirects, which ignores its config),
|
||||
// so a pointer to an otherwise-undefined Network is safe here.
|
||||
var net: Network = undefined;
|
||||
|
||||
var client: Client = undefined;
|
||||
client.allocator = testing.allocator;
|
||||
client.arena_pool = &pool;
|
||||
client.network = &net;
|
||||
client.transfers = .empty;
|
||||
client.queue = .{};
|
||||
client.next_tick_queue = .{};
|
||||
client.next_tick_count = 0;
|
||||
client.performing = false;
|
||||
client.interception_layer = .{};
|
||||
defer client.transfers.deinit(testing.allocator);
|
||||
|
||||
// Capturing stub for interception_layer.next: records the re-issued request
|
||||
// and returns without committing (transfer stays .created; we clean up).
|
||||
const Captor = struct {
|
||||
captured: bool = false,
|
||||
url: []const u8 = "",
|
||||
method: Method = undefined,
|
||||
|
||||
fn request(ptr: *anyopaque, transfer: *Transfer) anyerror!void {
|
||||
const self: *@This() = @ptrCast(@alignCast(ptr));
|
||||
self.captured = true;
|
||||
self.url = transfer.req.url;
|
||||
self.method = transfer.req.method;
|
||||
}
|
||||
};
|
||||
var captor = Captor{};
|
||||
client.interception_layer.next = .{
|
||||
.ptr = &captor,
|
||||
.vtable = &.{ .request = Captor.request },
|
||||
};
|
||||
|
||||
// 302 with a relative Location: rewrite to GET, drop body, resolve target.
|
||||
{
|
||||
const arena = try pool.acquire(.small, "test");
|
||||
const transfer = try arena.create(Transfer);
|
||||
transfer.* = .{
|
||||
.arena = arena,
|
||||
.owner = null,
|
||||
.req = .{
|
||||
.frame_id = 0,
|
||||
.loader_id = 0,
|
||||
.method = .POST,
|
||||
.url = "http://example.com/start",
|
||||
.body = "payload",
|
||||
.headers = .{ .headers = null },
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.resource_type = .document,
|
||||
.notification = undefined,
|
||||
.ctx = undefined,
|
||||
},
|
||||
.client = &client,
|
||||
.id = 1,
|
||||
.start_time = 0,
|
||||
};
|
||||
try client.transfers.putNoClobber(testing.allocator, transfer.id, transfer);
|
||||
|
||||
transfer.park(.intercept_request);
|
||||
client.interception_layer.intercepted += 1;
|
||||
|
||||
try client.interception_layer.fulfillRequest(transfer, 302, &.{
|
||||
.{ .name = "Location", .value = "/end" },
|
||||
}, null);
|
||||
|
||||
try testing.expect(captor.captured);
|
||||
try testing.expectEqual("http://example.com/end", captor.url);
|
||||
try testing.expectEqual(.GET, captor.method);
|
||||
try testing.expectEqual(null, transfer.req.body);
|
||||
// Unparked exactly once; transfer is still alive (re-issued, not deinited).
|
||||
try testing.expectEqual(0, client.interception_layer.intercepted);
|
||||
try testing.expectEqual(1, client.transfers.count());
|
||||
transfer.deinit();
|
||||
}
|
||||
|
||||
// 307 with an absolute Location: keep method and body.
|
||||
captor = .{};
|
||||
{
|
||||
const arena = try pool.acquire(.small, "test");
|
||||
const transfer = try arena.create(Transfer);
|
||||
transfer.* = .{
|
||||
.arena = arena,
|
||||
.owner = null,
|
||||
.req = .{
|
||||
.frame_id = 0,
|
||||
.loader_id = 0,
|
||||
.method = .POST,
|
||||
.url = "http://example.com/start",
|
||||
.body = "payload",
|
||||
.headers = .{ .headers = null },
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.resource_type = .document,
|
||||
.notification = undefined,
|
||||
.ctx = undefined,
|
||||
},
|
||||
.client = &client,
|
||||
.id = 2,
|
||||
.start_time = 0,
|
||||
};
|
||||
try client.transfers.putNoClobber(testing.allocator, transfer.id, transfer);
|
||||
|
||||
transfer.park(.intercept_request);
|
||||
client.interception_layer.intercepted += 1;
|
||||
|
||||
try client.interception_layer.fulfillRequest(transfer, 307, &.{
|
||||
.{ .name = "location", .value = "http://example.com/other" },
|
||||
}, null);
|
||||
|
||||
try testing.expect(captor.captured);
|
||||
try testing.expectEqual("http://example.com/other", captor.url);
|
||||
try testing.expectEqual(.POST, captor.method);
|
||||
try testing.expectEqual("payload", transfer.req.body.?);
|
||||
try testing.expectEqual(0, client.interception_layer.intercepted);
|
||||
transfer.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
test "HttpClient: fulfillRequest delivers a 3xx without a Location as the response" {
|
||||
// A redirect status with no Location header is not a redirect: the body is
|
||||
// delivered as the final response (matching the real-network path).
|
||||
var pool = ArenaPool.init(testing.allocator, .{});
|
||||
defer pool.deinit();
|
||||
|
||||
var client: Client = undefined;
|
||||
client.allocator = testing.allocator;
|
||||
client.arena_pool = &pool;
|
||||
client.transfers = .empty;
|
||||
client.queue = .{};
|
||||
client.next_tick_queue = .{};
|
||||
client.next_tick_count = 0;
|
||||
client.performing = false;
|
||||
client.interception_layer = .{};
|
||||
defer client.transfers.deinit(testing.allocator);
|
||||
|
||||
const Ctx = struct {
|
||||
done_called: bool = false,
|
||||
fn doneCallback(ctx: *anyopaque) !void {
|
||||
const self: *@This() = @ptrCast(@alignCast(ctx));
|
||||
self.done_called = true;
|
||||
}
|
||||
};
|
||||
var ctx = Ctx{};
|
||||
|
||||
const arena = try pool.acquire(.small, "test");
|
||||
const transfer = try arena.create(Transfer);
|
||||
transfer.* = .{
|
||||
.arena = arena,
|
||||
.owner = null,
|
||||
.req = .{
|
||||
.frame_id = 0,
|
||||
.loader_id = 0,
|
||||
.method = .GET,
|
||||
.url = "http://example.com/",
|
||||
.headers = .{ .headers = null },
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.resource_type = .document,
|
||||
.notification = undefined,
|
||||
.ctx = &ctx,
|
||||
.done_callback = Ctx.doneCallback,
|
||||
},
|
||||
.client = &client,
|
||||
.id = 1,
|
||||
.start_time = 0,
|
||||
};
|
||||
try client.transfers.putNoClobber(testing.allocator, transfer.id, transfer);
|
||||
|
||||
transfer.park(.intercept_request);
|
||||
client.interception_layer.intercepted += 1;
|
||||
|
||||
try client.interception_layer.fulfillRequest(transfer, 302, &.{}, "body");
|
||||
|
||||
// Delivered (done_callback ran) and freed exactly once.
|
||||
try testing.expect(ctx.done_called);
|
||||
try testing.expectEqual(0, client.interception_layer.intercepted);
|
||||
try testing.expectEqual(0, client.transfers.count());
|
||||
}
|
||||
|
||||
test "HttpClient: abortParked survives an error_callback that tears down the owner" {
|
||||
// Same re-entrancy hazard as fulfillRequest, but on the abort path
|
||||
// (failRequest / continueWithAuth-cancel / session teardown). abortParked
|
||||
|
||||
@@ -234,7 +234,7 @@ fn parseScopeKey(arena: Allocator, base: [:0]const u8, key: []const u8) ![]const
|
||||
if (key.len == 0) {
|
||||
return base;
|
||||
}
|
||||
return URL.resolve(arena, base, key, .{ .always_dupe = true, .encoding = "UTF-8" });
|
||||
return URL.resolve(arena, base, key, .{ .encoding = "UTF-8" });
|
||||
}
|
||||
|
||||
/// Returns the parsed URL if `specifier` looks like a URL. Else returns null;
|
||||
@@ -248,7 +248,7 @@ fn parseIfLikeURL(arena: Allocator, base: [:0]const u8, specifier: []const u8) ?
|
||||
std.mem.startsWith(u8, specifier, "../") or
|
||||
hasScheme(specifier))
|
||||
{
|
||||
return URL.resolve(arena, base, specifier, .{ .always_dupe = true, .encoding = "UTF-8" }) catch return null;
|
||||
return URL.resolve(arena, base, specifier, .{ .encoding = "UTF-8" }) catch return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -331,7 +331,7 @@ fn resolveImportsMatch(
|
||||
|
||||
const base_addr = entry.resolved orelse return error.SpecifierResolutionFailed;
|
||||
const after = normalized[entry.specifier.len..];
|
||||
const url = URL.resolve(arena, base_addr, after, .{ .always_dupe = true, .encoding = "UTF-8" }) catch {
|
||||
const url = URL.resolve(arena, base_addr, after, .{ .encoding = "UTF-8" }) catch {
|
||||
return error.SpecifierResolutionFailed;
|
||||
};
|
||||
|
||||
|
||||
@@ -18,10 +18,8 @@
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const js = @import("js/js.zig");
|
||||
const Frame = @import("Frame.zig");
|
||||
const Browser = @import("Browser.zig");
|
||||
const Session = @import("Session.zig");
|
||||
const HttpClient = @import("HttpClient.zig");
|
||||
@@ -30,7 +28,6 @@ const Node = @import("webapi/Node.zig");
|
||||
const Selector = @import("webapi/selector/Selector.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
const Runner = @This();
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ const builtin = @import("builtin");
|
||||
|
||||
const App = @import("../App.zig");
|
||||
|
||||
const js = @import("js/js.zig");
|
||||
const storage = @import("webapi/storage/storage.zig");
|
||||
const Navigation = @import("webapi/navigation/Navigation.zig");
|
||||
const History = @import("webapi/History.zig");
|
||||
|
||||
@@ -17,316 +17,52 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const idna = @import("../sys/idna.zig");
|
||||
|
||||
const U = @import("../sys/url.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const ResolveOpts = struct {
|
||||
pub const ResolveOptions = struct {
|
||||
/// null = don't encode, "UTF-8" = standard percent encoding,
|
||||
/// other charset = encode query string using that charset with NCR fallback
|
||||
/// other charset = encode query string using that charset with NCR fallback.
|
||||
encoding: ?[]const u8 = null,
|
||||
always_dupe: bool = false,
|
||||
};
|
||||
|
||||
// path is anytype, so that it can be used with both []const u8 and [:0]const u8
|
||||
pub fn resolve(allocator: Allocator, base: [:0]const u8, source_path: anytype, opts: ResolveOpts) ![:0]const u8 {
|
||||
const PT = @TypeOf(source_path);
|
||||
pub fn resolve(
|
||||
allocator: Allocator,
|
||||
base: [:0]const u8,
|
||||
source_path: anytype,
|
||||
options: ResolveOptions,
|
||||
) ![:0]const u8 {
|
||||
const path = source_path;
|
||||
|
||||
const needs_dupe = comptime !isNullTerminated(PT);
|
||||
var path: [:0]const u8 = if (needs_dupe or opts.always_dupe) try allocator.dupeZ(u8, source_path) else source_path;
|
||||
var err: i32 = 0;
|
||||
const href = if (options.encoding) |encoding|
|
||||
U.url_resolve_with_encoding(base.ptr, base.len, path.ptr, path.len, encoding.ptr, encoding.len, &err)
|
||||
else
|
||||
U.url_resolve_without_encoding(base.ptr, base.len, path.ptr, path.len, &err);
|
||||
|
||||
if (std.mem.indexOfAny(u8, path, "\t\r\n")) |first| {
|
||||
path = blk: {
|
||||
var buf: std.ArrayList(u8) = try .initCapacity(allocator, path.len);
|
||||
buf.appendSliceAssumeCapacity(path[0..first]);
|
||||
for (path[first + 1 ..]) |c| {
|
||||
if (c != '\t' and c != '\r' and c != '\n') {
|
||||
buf.appendAssumeCapacity(c);
|
||||
}
|
||||
}
|
||||
buf.appendAssumeCapacity(0);
|
||||
break :blk buf.items[0 .. buf.items.len - 1 :0];
|
||||
};
|
||||
if (err != 0) {
|
||||
return error.TypeError;
|
||||
}
|
||||
defer href.deinit();
|
||||
|
||||
if (base.len == 0) {
|
||||
return processResolved(allocator, path, opts);
|
||||
}
|
||||
|
||||
// Minimum is "x:" and skip relative path (very common case)
|
||||
if (path.len >= 2 and path[0] != '/') {
|
||||
if (std.mem.indexOfScalar(u8, path[0..], ':')) |scheme_path_end| {
|
||||
scheme_check: {
|
||||
const scheme_path = path[0..scheme_path_end];
|
||||
//from "ws" to "https"
|
||||
if (scheme_path_end >= 2 and scheme_path_end <= 5) {
|
||||
const has_double_slashes: bool = scheme_path_end + 3 <= path.len and path[scheme_path_end + 1] == '/' and path[scheme_path_end + 2] == '/';
|
||||
const special_schemes = [_][]const u8{ "https", "http", "ws", "wss", "file", "ftp" };
|
||||
|
||||
for (special_schemes) |special_scheme| {
|
||||
if (std.ascii.eqlIgnoreCase(scheme_path, special_scheme)) {
|
||||
const base_scheme_end = std.mem.indexOf(u8, base, "://") orelse 0;
|
||||
|
||||
if (base_scheme_end > 0 and std.mem.eql(u8, base[0..base_scheme_end], scheme_path) and !has_double_slashes) {
|
||||
//Skip ":" and exit as relative state
|
||||
path = path[scheme_path_end + 1 ..];
|
||||
break :scheme_check;
|
||||
} else {
|
||||
var rest_start: usize = scheme_path_end + 1;
|
||||
//Skip any slashas after "scheme:"
|
||||
while (rest_start < path.len and (path[rest_start] == '/' or path[rest_start] == '\\')) {
|
||||
rest_start += 1;
|
||||
}
|
||||
// A special scheme (exclude "file") must contain at least any chars after "://"
|
||||
if (rest_start == path.len and !std.ascii.eqlIgnoreCase(scheme_path, "file")) {
|
||||
return error.TypeError;
|
||||
}
|
||||
//File scheme allow empty host
|
||||
const separator: []const u8 = if (!has_double_slashes and std.ascii.eqlIgnoreCase(scheme_path, "file")) ":///" else "://";
|
||||
|
||||
path = try std.mem.joinZ(allocator, "", &.{ scheme_path, separator, path[rest_start..] });
|
||||
return processResolved(allocator, path, opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scheme_path.len > 0) {
|
||||
for (scheme_path[1..]) |c| {
|
||||
if (!std.ascii.isAlphanumeric(c) and c != '+' and c != '-' and c != '.') {
|
||||
//Exit as relative state
|
||||
break :scheme_check;
|
||||
}
|
||||
}
|
||||
}
|
||||
//path is complete http url
|
||||
return processResolved(allocator, path, opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (path.len == 0) {
|
||||
if (opts.always_dupe) {
|
||||
const dupe = try allocator.dupeZ(u8, base);
|
||||
return processResolved(allocator, dupe, opts);
|
||||
}
|
||||
return processResolved(allocator, base, opts);
|
||||
}
|
||||
|
||||
if (path[0] == '?') {
|
||||
const base_path_end = std.mem.indexOfAny(u8, base, "?#") orelse base.len;
|
||||
const result = try std.mem.joinZ(allocator, "", &.{ base[0..base_path_end], path });
|
||||
return processResolved(allocator, result, opts);
|
||||
}
|
||||
if (path[0] == '#') {
|
||||
const base_fragment_start = std.mem.indexOfScalar(u8, base, '#') orelse base.len;
|
||||
const result = try std.mem.joinZ(allocator, "", &.{ base[0..base_fragment_start], path });
|
||||
return processResolved(allocator, result, opts);
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, path, "//")) {
|
||||
// network-path reference
|
||||
const index = std.mem.indexOfScalar(u8, base, ':') orelse {
|
||||
return processResolved(allocator, path, opts);
|
||||
};
|
||||
const protocol = base[0 .. index + 1];
|
||||
const result = try std.mem.joinZ(allocator, "", &.{ protocol, path });
|
||||
return processResolved(allocator, result, opts);
|
||||
}
|
||||
|
||||
const scheme_end = std.mem.indexOf(u8, base, "://");
|
||||
const authority_start = if (scheme_end) |end| end + 3 else 0;
|
||||
const path_start = std.mem.indexOfAnyPos(u8, base, authority_start, "/?#") orelse base.len;
|
||||
const path_end = std.mem.indexOfAnyPos(u8, base, path_start, "?#") orelse base.len;
|
||||
|
||||
var out: []u8 = undefined;
|
||||
if (path[0] == '/') {
|
||||
// Absolute path — keep base authority, replace path. Two trailing
|
||||
// spaces give us safe lookahead for the dot-segment loop below.
|
||||
out = try std.mem.join(allocator, "", &.{ base[0..path_start], path, " " });
|
||||
} else {
|
||||
var normalized_base: []const u8 = base[0..path_start];
|
||||
if (path_start < path_end) {
|
||||
if (std.mem.lastIndexOfScalar(u8, base[path_start + 1 .. path_end], '/')) |pos| {
|
||||
normalized_base = base[0 .. path_start + 1 + pos];
|
||||
}
|
||||
}
|
||||
|
||||
// trailing space so that we always have space to append the null terminator
|
||||
// and so that we can compare the next two characters without needing to length check
|
||||
out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
|
||||
}
|
||||
|
||||
const end = out.len - 2;
|
||||
|
||||
const path_marker = path_start + 1;
|
||||
|
||||
// Strip out ./ and ../. This is done in-place, because doing so can
|
||||
// only ever make `out` smaller. After this, `out` cannot be freed by
|
||||
// an allocator, which is ok, because we expect allocator to be an arena.
|
||||
var in_i: usize = 0;
|
||||
var out_i: usize = 0;
|
||||
while (in_i < end) {
|
||||
if (out[in_i] == '.' and (out_i == 0 or out[out_i - 1] == '/')) {
|
||||
if (out[in_i + 1] == '/') { // always safe, because we added a whitespace
|
||||
// /./
|
||||
in_i += 2;
|
||||
continue;
|
||||
}
|
||||
if (out[in_i + 1] == '.' and (out[in_i + 2] == '/' or in_i + 2 == end)) {
|
||||
// /../ or trailing /.. — both step up one segment. The
|
||||
// trailing slash stays implicit (out_i ends up right after
|
||||
// the previous '/'), matching `new URL("..", base)`.
|
||||
if (out_i > path_marker) {
|
||||
// go back before the /
|
||||
out_i -= 2;
|
||||
while (out_i > 1 and out[out_i - 1] != '/') {
|
||||
out_i -= 1;
|
||||
}
|
||||
} else {
|
||||
// if out_i == path_marker, than we've reached the start of
|
||||
// the path. We can't ../ any more. E.g.:
|
||||
// http://www.example.com/../hello.
|
||||
// You might think that's an error, but, at least with
|
||||
// new URL('../hello', 'http://www.example.com/')
|
||||
// it just ignores the extra ../
|
||||
}
|
||||
in_i += 3;
|
||||
continue;
|
||||
}
|
||||
if (in_i == end - 1) {
|
||||
// ignore trailing dot
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const c = out[in_i];
|
||||
out[out_i] = c;
|
||||
in_i += 1;
|
||||
out_i += 1;
|
||||
}
|
||||
|
||||
// we always have an extra space
|
||||
out[out_i] = 0;
|
||||
return processResolved(allocator, out[0..out_i :0], opts);
|
||||
return allocator.dupeZ(u8, href.slice());
|
||||
}
|
||||
|
||||
fn processResolved(allocator: Allocator, url: [:0]const u8, opts: ResolveOpts) ![:0]const u8 {
|
||||
const encoding = opts.encoding orelse return ensureHostAscii(allocator, url);
|
||||
return ensureEncoded(allocator, url, encoding);
|
||||
/// Resolves a user-provided "address bar" URL the way curl does. Bare host like
|
||||
/// `lightpanda.io` has no scheme, so it can't be parsed as an absolute URL.
|
||||
pub fn resolveNavigation(allocator: Allocator, url: []const u8, options: ResolveOptions) ![:0]const u8 {
|
||||
return resolve(allocator, "", url, options) catch |err| switch (err) {
|
||||
error.TypeError => {
|
||||
const with_scheme = try std.fmt.allocPrintSentinel(allocator, "http://{s}", .{url}, 0);
|
||||
return resolve(allocator, "", with_scheme, options);
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
|
||||
/// IDNA pass: converts a non-ASCII host (`räksmörgås.se`) to its punycode form
|
||||
/// (`xn--rksmrgs-5wao1o.se`), validates any ASCII punycode (`xn--…`) labels,
|
||||
/// and leaves everything else alone. Returns `error.Idna` for an invalid
|
||||
/// domain (e.g. malformed punycode), which surfaces as a URL parse failure.
|
||||
fn ensureHostAscii(allocator: Allocator, url: [:0]const u8) ![:0]const u8 {
|
||||
const hostname = getHostname(url);
|
||||
if (hostname.len == 0 or (!idna.needsAscii(hostname) and !hasAceLabel(hostname))) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const ascii = try idna.toAscii(allocator, hostname);
|
||||
|
||||
// hostname is a slice of url, so its start offset is just pointer arithmetic.
|
||||
const start = @intFromPtr(hostname.ptr) - @intFromPtr(url.ptr);
|
||||
const end = start + hostname.len;
|
||||
var buf = try std.ArrayList(u8).initCapacity(allocator, url.len - hostname.len + ascii.len + 1);
|
||||
buf.appendSliceAssumeCapacity(url[0..start]);
|
||||
buf.appendSliceAssumeCapacity(ascii);
|
||||
buf.appendSliceAssumeCapacity(url[end..]);
|
||||
buf.appendAssumeCapacity(0);
|
||||
return buf.items[0 .. buf.items.len - 1 :0];
|
||||
}
|
||||
|
||||
/// True if any dot-separated label of `host` begins with the IDNA ACE prefix
|
||||
/// "xn--" (case-insensitive). Such labels are punycode: even though they're
|
||||
/// pure ASCII, UTS#46 must decode and validate them, so they can't take the
|
||||
/// `needsAscii` fast path.
|
||||
fn hasAceLabel(host: []const u8) bool {
|
||||
var pos: usize = 0;
|
||||
while (std.mem.indexOfScalarPos(u8, host, pos, '-')) |i| {
|
||||
pos = i + 1;
|
||||
if (i < 2 or i + 1 >= host.len or host[i + 1] != '-') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!std.ascii.eqlIgnoreCase(host[i - 2 .. i], "xn")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label_start = i - 2;
|
||||
if (label_start == 0 or host[label_start - 1] == '.') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn ensureEncoded(allocator: Allocator, url_in: [:0]const u8, encoding: []const u8) ![:0]const u8 {
|
||||
// Resolve any IDN host first; everything below operates on the ASCII form.
|
||||
const url = try ensureHostAscii(allocator, url_in);
|
||||
|
||||
const scheme_end = std.mem.indexOf(u8, url, "://");
|
||||
const authority_start = if (scheme_end) |end| end + 3 else 0;
|
||||
const path_start = std.mem.indexOfScalarPos(u8, url, authority_start, '/') orelse return url;
|
||||
|
||||
const query_start = std.mem.indexOfScalarPos(u8, url, path_start, '?');
|
||||
const fragment_start = std.mem.indexOfScalarPos(u8, url, query_start orelse path_start, '#');
|
||||
|
||||
const path_end = query_start orelse fragment_start orelse url.len;
|
||||
const query_end = if (query_start) |_| (fragment_start orelse url.len) else path_end;
|
||||
|
||||
const path_to_encode = url[path_start..path_end];
|
||||
// Path is always UTF-8 percent encoded per URL spec
|
||||
const encoded_path = try percentEncodeSegment(allocator, path_to_encode, .path);
|
||||
|
||||
// Query string uses document encoding
|
||||
const encoded_query = if (query_start) |qs| blk: {
|
||||
const query_to_encode = url[qs + 1 .. query_end];
|
||||
break :blk try encodeQueryString(allocator, query_to_encode, encoding);
|
||||
} else null;
|
||||
|
||||
const encoded_fragment = if (fragment_start) |fs| blk: {
|
||||
const fragment_to_encode = url[fs + 1 ..];
|
||||
break :blk try percentEncodeSegment(allocator, fragment_to_encode, .query);
|
||||
} else null;
|
||||
|
||||
if (encoded_path.ptr == path_to_encode.ptr and
|
||||
(encoded_query == null or encoded_query.?.ptr == url[query_start.? + 1 .. query_end].ptr) and
|
||||
(encoded_fragment == null or encoded_fragment.?.ptr == url[fragment_start.? + 1 ..].ptr))
|
||||
{
|
||||
// nothing has changed
|
||||
return url;
|
||||
}
|
||||
|
||||
var buf = try std.ArrayList(u8).initCapacity(allocator, url.len + 20);
|
||||
try buf.appendSlice(allocator, url[0..path_start]);
|
||||
try buf.appendSlice(allocator, encoded_path);
|
||||
if (encoded_query) |eq| {
|
||||
try buf.append(allocator, '?');
|
||||
try buf.appendSlice(allocator, eq);
|
||||
}
|
||||
if (encoded_fragment) |ef| {
|
||||
try buf.append(allocator, '#');
|
||||
try buf.appendSlice(allocator, ef);
|
||||
}
|
||||
try buf.append(allocator, 0);
|
||||
return buf.items[0 .. buf.items.len - 1 :0];
|
||||
}
|
||||
|
||||
/// Selects which RFC 3986 / WHATWG URL Standard percent-encode set to apply.
|
||||
///
|
||||
/// The `path`, `query`, `query_legacy`, `userinfo`, and `fragment` variants
|
||||
/// match the corresponding URL spec sets — they assume the input is already
|
||||
/// structured (e.g. `key=val&key=val` for `query`) and only encode characters
|
||||
/// disallowed in that location.
|
||||
///
|
||||
/// `component` is stricter: it encodes everything outside the RFC 3986
|
||||
/// unreserved set, including sub-delims (`& = + ! * ' ( ) , $ ;`). Use this
|
||||
/// when embedding an arbitrary string as a single URI component such as a
|
||||
/// query-parameter value, where reserved characters in the input would
|
||||
/// otherwise change the URL's structure.
|
||||
pub const EncodeSet = enum { path, query, query_legacy, userinfo, fragment, component };
|
||||
const EncodeSet = enum { path, query, query_legacy, userinfo, fragment, component };
|
||||
|
||||
pub fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime encode_set: EncodeSet) ![]const u8 {
|
||||
// Check if encoding is needed
|
||||
@@ -374,52 +110,6 @@ pub fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
const h5e = @import("parser/html5ever.zig");
|
||||
|
||||
/// Encode a query string using the specified encoding.
|
||||
/// For UTF-8, this is standard percent encoding.
|
||||
/// For legacy encodings, unmappable characters are replaced with NCRs (&#codepoint;).
|
||||
fn encodeQueryString(allocator: Allocator, query: []const u8, encoding: []const u8) ![]const u8 {
|
||||
// For UTF-8, use standard percent encoding
|
||||
if (std.mem.eql(u8, encoding, "UTF-8")) {
|
||||
return percentEncodeSegment(allocator, query, .query);
|
||||
}
|
||||
|
||||
// For legacy encodings, first encode to the target charset with NCR fallback
|
||||
const enc_info = h5e.encoding_for_label(encoding.ptr, encoding.len);
|
||||
if (!enc_info.isValid()) {
|
||||
// Unknown encoding, fall back to UTF-8
|
||||
return percentEncodeSegment(allocator, query, .query);
|
||||
}
|
||||
|
||||
// Calculate max buffer size for encoded output
|
||||
const max_encoded_len = h5e.encoding_max_encode_buffer_length(enc_info.handle.?, query.len);
|
||||
if (max_encoded_len == 0) {
|
||||
return percentEncodeSegment(allocator, query, .query);
|
||||
}
|
||||
|
||||
const encode_buf = try allocator.alloc(u8, max_encoded_len);
|
||||
defer allocator.free(encode_buf);
|
||||
|
||||
// Encode UTF-8 to legacy encoding with NCR fallback
|
||||
const result = h5e.encoding_encode_with_ncr(
|
||||
enc_info.handle.?,
|
||||
query.ptr,
|
||||
query.len,
|
||||
encode_buf.ptr,
|
||||
encode_buf.len,
|
||||
);
|
||||
|
||||
if (!result.isSuccess()) {
|
||||
// Encoding failed, fall back to UTF-8
|
||||
return percentEncodeSegment(allocator, query, .query);
|
||||
}
|
||||
|
||||
// Now percent-encode the result using query_legacy to preserve NCRs
|
||||
const encoded_bytes = encode_buf[0..result.bytes_written];
|
||||
return percentEncodeSegment(allocator, encoded_bytes, .query_legacy);
|
||||
}
|
||||
|
||||
fn shouldPercentEncode(c: u8, comptime encode_set: EncodeSet) bool {
|
||||
return switch (c) {
|
||||
// Unreserved characters (RFC 3986)
|
||||
@@ -441,10 +131,6 @@ fn shouldPercentEncode(c: u8, comptime encode_set: EncodeSet) bool {
|
||||
};
|
||||
}
|
||||
|
||||
fn isNullTerminated(comptime value: type) bool {
|
||||
return @typeInfo(value).pointer.sentinel_ptr != null;
|
||||
}
|
||||
|
||||
pub fn isCompleteHTTPUrl(url: []const u8) bool {
|
||||
if (url.len < 3) { // Minimum is "x://"
|
||||
return false;
|
||||
@@ -1024,6 +710,7 @@ test "URL: resolve" {
|
||||
base: [:0]const u8,
|
||||
path: [:0]const u8,
|
||||
expected: [:0]const u8,
|
||||
expected_error: bool = false,
|
||||
};
|
||||
|
||||
const cases = [_]Case{
|
||||
@@ -1177,30 +864,37 @@ test "URL: resolve" {
|
||||
.path = "./.././.././hello",
|
||||
.expected = "https://example/hello",
|
||||
},
|
||||
// A base must itself be a valid absolute URL; schemeless bases are
|
||||
// rejected, matching `new URL(path, base)`.
|
||||
.{
|
||||
.base = "some/page",
|
||||
.path = "hello",
|
||||
.expected = "some/hello",
|
||||
.expected = "",
|
||||
.expected_error = true,
|
||||
},
|
||||
.{
|
||||
.base = "some/page/",
|
||||
.path = "hello",
|
||||
.expected = "some/page/hello",
|
||||
.expected = "",
|
||||
.expected_error = true,
|
||||
},
|
||||
.{
|
||||
.base = "some/page/other",
|
||||
.path = ".././hello",
|
||||
.expected = "some/hello",
|
||||
.expected = "",
|
||||
.expected_error = true,
|
||||
},
|
||||
.{
|
||||
.base = "https://www.example.com/hello/world",
|
||||
.path = "//example/about",
|
||||
.expected = "https://example/about",
|
||||
},
|
||||
// "http:" alone is not a valid base (special scheme without a host).
|
||||
.{
|
||||
.base = "http:",
|
||||
.path = "//example.com/over/9000",
|
||||
.expected = "http://example.com/over/9000",
|
||||
.expected = "",
|
||||
.expected_error = true,
|
||||
},
|
||||
.{
|
||||
.base = "https://example.com/",
|
||||
@@ -1245,8 +939,13 @@ test "URL: resolve" {
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
const result = try resolve(testing.arena_allocator, case.base, case.path, .{});
|
||||
try testing.expectString(case.expected, result);
|
||||
if (case.expected_error) {
|
||||
const result = resolve(testing.arena_allocator, case.base, case.path, .{});
|
||||
try testing.expectError(error.TypeError, result);
|
||||
} else {
|
||||
const result = try resolve(testing.arena_allocator, case.base, case.path, .{});
|
||||
try testing.expectString(case.expected, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1286,188 +985,13 @@ test "URL: resolve strips tab and newline from input" {
|
||||
test "URL: resolve validates ASCII punycode (xn--) labels" {
|
||||
defer testing.reset();
|
||||
|
||||
// Valid punycode is left untouched (the needsAscii fast path would skip it,
|
||||
// so this exercises the xn-- gate going through toAscii and back).
|
||||
const ok = try resolve(testing.arena_allocator, "", "https://xn--rksmrgs-5wao1o.se/x", .{});
|
||||
// Valid punycode is left untouched.
|
||||
const ok = try resolve(testing.arena_allocator, "https://example.com/", "https://xn--rksmrgs-5wao1o.se/x", .{});
|
||||
try testing.expectString("https://xn--rksmrgs-5wao1o.se/x", ok);
|
||||
|
||||
// Malformed punycode must be rejected rather than passed through verbatim.
|
||||
// (URL.init remaps this error.Idna to TypeError for `new URL`.)
|
||||
try testing.expectError(error.Idna, resolve(testing.arena_allocator, "", "https://xn--0.pt/x", .{}));
|
||||
try testing.expectError(error.Idna, resolve(testing.arena_allocator, "", "https://xn--a.pt/x", .{}));
|
||||
}
|
||||
|
||||
test "URL: hasAceLabel" {
|
||||
// ACE prefix at a label start (case-insensitive).
|
||||
try testing.expectEqual(true, hasAceLabel("xn--a"));
|
||||
try testing.expectEqual(true, hasAceLabel("xn--rksmrgs-5wao1o.se"));
|
||||
try testing.expectEqual(true, hasAceLabel("a.xn--b.com"));
|
||||
try testing.expectEqual(true, hasAceLabel("XN--ab.com"));
|
||||
try testing.expectEqual(true, hasAceLabel("foo.example.xn--p1ai"));
|
||||
|
||||
// Has '-', but no ACE label.
|
||||
try testing.expectEqual(false, hasAceLabel("example.com"));
|
||||
try testing.expectEqual(false, hasAceLabel("my-site.com"));
|
||||
try testing.expectEqual(false, hasAceLabel("axn--b.com")); // xn-- not at a label start
|
||||
try testing.expectEqual(false, hasAceLabel("x-n--a.com")); // not "xn" before the '-'
|
||||
try testing.expectEqual(false, hasAceLabel("-.com"));
|
||||
try testing.expectEqual(false, hasAceLabel(""));
|
||||
}
|
||||
|
||||
test "URL: ensureEncoded" {
|
||||
defer testing.reset();
|
||||
|
||||
const Case = struct {
|
||||
url: [:0]const u8,
|
||||
expected: [:0]const u8,
|
||||
};
|
||||
|
||||
const cases = [_]Case{
|
||||
.{
|
||||
.url = "https://example.com/over 9000!",
|
||||
.expected = "https://example.com/over%209000!",
|
||||
},
|
||||
.{
|
||||
.url = "http://example.com/hello world.html",
|
||||
.expected = "http://example.com/hello%20world.html",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/file[1].html",
|
||||
.expected = "https://example.com/file%5B1%5D.html",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/file{name}.html",
|
||||
.expected = "https://example.com/file%7Bname%7D.html",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/page?query=hello world",
|
||||
.expected = "https://example.com/page?query=hello%20world",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/page?a=1&b=value with spaces",
|
||||
.expected = "https://example.com/page?a=1&b=value%20with%20spaces",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/page#section one",
|
||||
.expected = "https://example.com/page#section%20one",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/my path?query=my value#my anchor",
|
||||
.expected = "https://example.com/my%20path?query=my%20value#my%20anchor",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/already%20encoded",
|
||||
.expected = "https://example.com/already%20encoded",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/file%5B1%5D.html",
|
||||
.expected = "https://example.com/file%5B1%5D.html",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/caf%C3%A9",
|
||||
.expected = "https://example.com/caf%C3%A9",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/page?query=already%20encoded",
|
||||
.expected = "https://example.com/page?query=already%20encoded",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/page?a=1&b=value%20here",
|
||||
.expected = "https://example.com/page?a=1&b=value%20here",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/page#section%20one",
|
||||
.expected = "https://example.com/page#section%20one",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/part%20encoded and not",
|
||||
.expected = "https://example.com/part%20encoded%20and%20not",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/page?a=encoded%20value&b=not encoded",
|
||||
.expected = "https://example.com/page?a=encoded%20value&b=not%20encoded",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/my%20path?query=not encoded#encoded%20anchor",
|
||||
.expected = "https://example.com/my%20path?query=not%20encoded#encoded%20anchor",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/fully%20encoded?query=also%20encoded#and%20this",
|
||||
.expected = "https://example.com/fully%20encoded?query=also%20encoded#and%20this",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/path-with_under~tilde",
|
||||
.expected = "https://example.com/path-with_under~tilde",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/sub-delims!$&'()*+,;=",
|
||||
.expected = "https://example.com/sub-delims!$&'()*+,;=",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com",
|
||||
.expected = "https://example.com",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com?query=value",
|
||||
.expected = "https://example.com?query=value",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/clean/path",
|
||||
.expected = "https://example.com/clean/path",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/path?clean=query#clean-fragment",
|
||||
.expected = "https://example.com/path?clean=query#clean-fragment",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/100% complete",
|
||||
.expected = "https://example.com/100%25%20complete",
|
||||
},
|
||||
.{
|
||||
.url = "https://example.com/path?value=100% done",
|
||||
.expected = "https://example.com/path?value=100%25%20done",
|
||||
},
|
||||
.{
|
||||
.url = "about:blank",
|
||||
.expected = "about:blank",
|
||||
},
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
const result = try ensureEncoded(testing.arena_allocator, case.url, "UTF-8");
|
||||
try testing.expectString(case.expected, result);
|
||||
}
|
||||
}
|
||||
|
||||
test "URL: percentEncodeSegment component passes unreserved chars through" {
|
||||
defer testing.reset();
|
||||
const r = try percentEncodeSegment(testing.arena_allocator, "abcXYZ012-._~", .component);
|
||||
try testing.expectString("abcXYZ012-._~", r);
|
||||
}
|
||||
|
||||
test "URL: percentEncodeSegment component encodes spaces, sub-delims and reserved chars" {
|
||||
defer testing.reset();
|
||||
const r = try percentEncodeSegment(testing.arena_allocator, "hello world&q=1", .component);
|
||||
try testing.expectString("hello%20world%26q%3D1", r);
|
||||
|
||||
const r2 = try percentEncodeSegment(testing.arena_allocator, "a+b!c*d", .component);
|
||||
try testing.expectString("a%2Bb%21c%2Ad", r2);
|
||||
}
|
||||
|
||||
test "URL: percentEncodeSegment component encodes UTF-8 bytes" {
|
||||
defer testing.reset();
|
||||
const r = try percentEncodeSegment(testing.arena_allocator, "café", .component);
|
||||
try testing.expectString("caf%C3%A9", r);
|
||||
}
|
||||
|
||||
test "URL: percentEncodeSegment component re-encodes literal '%' in raw input" {
|
||||
defer testing.reset();
|
||||
const r = try percentEncodeSegment(testing.arena_allocator, "100%", .component);
|
||||
try testing.expectString("100%25", r);
|
||||
|
||||
// Even when followed by hex digits, treat as opaque user data.
|
||||
const r2 = try percentEncodeSegment(testing.arena_allocator, "100%2A", .component);
|
||||
try testing.expectString("100%252A", r2);
|
||||
try testing.expectError(error.TypeError, resolve(testing.arena_allocator, "https://example.com/", "https://xn--0.pt/x", .{}));
|
||||
try testing.expectError(error.TypeError, resolve(testing.arena_allocator, "https://example.com/", "https://xn--a.pt/x", .{}));
|
||||
}
|
||||
|
||||
test "URL: resolve with encoding" {
|
||||
@@ -1497,11 +1021,11 @@ test "URL: resolve with encoding" {
|
||||
.path = "path with multiple spaces",
|
||||
.expected = "https://example.com/path%20with%20%20multiple%20%20%20spaces",
|
||||
},
|
||||
// Special characters that need encoding
|
||||
// Brackets are not in the WHATWG path percent-encode set
|
||||
.{
|
||||
.base = "https://example.com/",
|
||||
.path = "file[1].html",
|
||||
.expected = "https://example.com/file%5B1%5D.html",
|
||||
.expected = "https://example.com/file[1].html",
|
||||
},
|
||||
.{
|
||||
.base = "https://example.com/",
|
||||
@@ -1518,20 +1042,24 @@ test "URL: resolve with encoding" {
|
||||
.path = "file\"quote\".html",
|
||||
.expected = "https://example.com/file%22quote%22.html",
|
||||
},
|
||||
// Pipe is not in the WHATWG path percent-encode set
|
||||
.{
|
||||
.base = "https://example.com/",
|
||||
.path = "file|pipe.html",
|
||||
.expected = "https://example.com/file%7Cpipe.html",
|
||||
.expected = "https://example.com/file|pipe.html",
|
||||
},
|
||||
// Backslash is a path separator in special URLs
|
||||
.{
|
||||
.base = "https://example.com/",
|
||||
.path = "file\\backslash.html",
|
||||
.expected = "https://example.com/file%5Cbackslash.html",
|
||||
.expected = "https://example.com/file/backslash.html",
|
||||
},
|
||||
// Note: the current URL spec percent-encodes '^' in paths, but
|
||||
// rust-url does not (yet); harmless divergence locked in here.
|
||||
.{
|
||||
.base = "https://example.com/",
|
||||
.path = "file^caret.html",
|
||||
.expected = "https://example.com/file%5Ecaret.html",
|
||||
.expected = "https://example.com/file^caret.html",
|
||||
},
|
||||
.{
|
||||
.base = "https://example.com/",
|
||||
@@ -1986,31 +1514,31 @@ test "URL: resolve path scheme" {
|
||||
.{
|
||||
.base = "https://www.example.com/example",
|
||||
.path = "https://about",
|
||||
.expected = "https://about",
|
||||
.expected = "https://about/",
|
||||
},
|
||||
//different schemes and path as absolute (without slash)
|
||||
.{
|
||||
.base = "https://www.example.com/example",
|
||||
.path = "http:about",
|
||||
.expected = "http://about",
|
||||
.expected = "http://about/",
|
||||
},
|
||||
//different schemes and path as absolute (with one slash)
|
||||
.{
|
||||
.base = "https://www.example.com/example",
|
||||
.path = "http:/about",
|
||||
.expected = "http://about",
|
||||
.expected = "http://about/",
|
||||
},
|
||||
//different schemes and path as absolute (with two slashes)
|
||||
.{
|
||||
.base = "https://www.example.com/example",
|
||||
.path = "http://about",
|
||||
.expected = "http://about",
|
||||
.expected = "http://about/",
|
||||
},
|
||||
//same schemes and path as absolute (with more slashes)
|
||||
.{
|
||||
.base = "https://site/",
|
||||
.path = "https://path",
|
||||
.expected = "https://path",
|
||||
.expected = "https://path/",
|
||||
},
|
||||
//path scheme is not special and path as absolute (without additional slashes)
|
||||
.{
|
||||
@@ -2022,19 +1550,19 @@ test "URL: resolve path scheme" {
|
||||
.{
|
||||
.base = "https://www.example.com/example",
|
||||
.path = "ws://about",
|
||||
.expected = "ws://about",
|
||||
.expected = "ws://about/",
|
||||
},
|
||||
//different schemes and path as absolute (path scheme=wss)
|
||||
.{
|
||||
.base = "https://www.example.com/example",
|
||||
.path = "wss://about",
|
||||
.expected = "wss://about",
|
||||
.expected = "wss://about/",
|
||||
},
|
||||
//different schemes and path as absolute (path scheme=ftp)
|
||||
.{
|
||||
.base = "https://www.example.com/example",
|
||||
.path = "ftp://about",
|
||||
.expected = "ftp://about",
|
||||
.expected = "ftp://about/",
|
||||
},
|
||||
//different schemes and path as absolute (path scheme=file)
|
||||
.{
|
||||
@@ -2139,3 +1667,28 @@ test "URL: resolve path scheme" {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "URL: resolveNavigation defaults a schemeless host to http (curl-like)" {
|
||||
defer testing.reset();
|
||||
|
||||
const Case = struct {
|
||||
url: [:0]const u8,
|
||||
expected: [:0]const u8,
|
||||
};
|
||||
|
||||
const cases = [_]Case{
|
||||
// Schemeless input (the regression): assume http://, like curl.
|
||||
.{ .url = "lightpanda.io", .expected = "http://lightpanda.io/" },
|
||||
.{ .url = "example.com/path?q=1", .expected = "http://example.com/path?q=1" },
|
||||
// An explicit scheme is preserved, not double-prefixed.
|
||||
.{ .url = "https://example.com/x", .expected = "https://example.com/x" },
|
||||
.{ .url = "http://example.com/", .expected = "http://example.com/" },
|
||||
// Non-http absolute URLs still parse as-is.
|
||||
.{ .url = "about:blank", .expected = "about:blank" },
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
const result = try resolveNavigation(testing.arena_allocator, case.url, .{});
|
||||
try testing.expectString(case.expected, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo, compti
|
||||
info.getReturnValue().set(this.handle);
|
||||
}
|
||||
|
||||
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u32 {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
@@ -183,12 +183,11 @@ pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return _getIndex(T, local, func, idx, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||
// not intercepted
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
};
|
||||
}
|
||||
|
||||
fn _getIndex(comptime T: type, local: *const Local, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
fn _getIndex(comptime T: type, local: *const Local, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u32 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@@ -200,7 +199,7 @@ fn _getIndex(comptime T: type, local: *const Local, func: anytype, idx: u32, inf
|
||||
return handleIndexedReturn(T, F, true, local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u32 {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
@@ -210,12 +209,11 @@ pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *cons
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return _getNamedIndex(T, local, func, name, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||
// not intercepted
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
};
|
||||
}
|
||||
|
||||
fn _getNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
fn _getNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u32 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@@ -227,7 +225,7 @@ fn _getNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *c
|
||||
return handleIndexedReturn(T, F, true, local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u32 {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
@@ -237,12 +235,11 @@ pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *cons
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return _setNamedIndex(T, local, func, name, .{ .local = &self.local, .handle = js_value }, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||
// not intercepted
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
};
|
||||
}
|
||||
|
||||
fn _setNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
fn _setNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u32 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@@ -255,7 +252,7 @@ fn _setNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *c
|
||||
return handleIndexedReturn(T, F, false, local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u32 {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
@@ -265,11 +262,11 @@ pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *c
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return _deleteNamedIndex(T, local, func, name, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
};
|
||||
}
|
||||
|
||||
fn _deleteNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
fn _deleteNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u32 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@@ -281,7 +278,7 @@ fn _deleteNamedIndex(comptime T: type, local: *const Local, func: anytype, name:
|
||||
return handleIndexedReturn(T, F, false, local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn getEnumerator(self: *Caller, comptime T: type, func: anytype, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
pub fn getEnumerator(self: *Caller, comptime T: type, func: anytype, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u32 {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
@@ -291,12 +288,11 @@ pub fn getEnumerator(self: *Caller, comptime T: type, func: anytype, handle: *co
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return _getEnumerator(T, local, func, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||
// not intercepted
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
};
|
||||
}
|
||||
|
||||
fn _getEnumerator(comptime T: type, local: *const Local, func: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
fn _getEnumerator(comptime T: type, local: *const Local, func: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u32 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@@ -307,7 +303,7 @@ fn _getEnumerator(comptime T: type, local: *const Local, func: anytype, info: Pr
|
||||
return handleIndexedReturn(T, F, true, local, ret, info, opts);
|
||||
}
|
||||
|
||||
fn handleIndexedReturn(comptime T: type, comptime F: type, comptime with_value: bool, local: *const Local, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
fn handleIndexedReturn(comptime T: type, comptime F: type, comptime with_value: bool, local: *const Local, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u32 {
|
||||
// need to unwrap this error immediately for when opts.null_as_undefined == true
|
||||
// and we need to compare it to null;
|
||||
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
|
||||
@@ -318,13 +314,11 @@ fn handleIndexedReturn(comptime T: type, comptime F: type, comptime with_value:
|
||||
// if error.NotHandled is part of the error set.
|
||||
if (isInErrorSet(error.NotHandled, eu.error_set)) {
|
||||
if (err == error.NotHandled) {
|
||||
// not intercepted
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
}
|
||||
}
|
||||
handleError(T, F, local, err, info, opts);
|
||||
// not intercepted
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
};
|
||||
},
|
||||
else => ret,
|
||||
@@ -333,8 +327,7 @@ fn handleIndexedReturn(comptime T: type, comptime F: type, comptime with_value:
|
||||
if (comptime with_value) {
|
||||
info.getReturnValue().set(try local.zigValueToJs(non_error_ret, opts));
|
||||
}
|
||||
// intercepted
|
||||
return 1;
|
||||
return js.Intercepted.yes;
|
||||
}
|
||||
|
||||
fn isInErrorSet(err: anyerror, comptime T: type) bool {
|
||||
@@ -372,7 +365,6 @@ fn handleError(comptime T: type, comptime F: type, local: *const Local, err: any
|
||||
error.TryCatchRethrow => return,
|
||||
error.InvalidArgument => isolate.createTypeError("invalid argument"),
|
||||
error.TypeError => isolate.createTypeError(""),
|
||||
error.Idna => isolate.createTypeError("invalid domain"),
|
||||
error.RangeError => isolate.createRangeError(""),
|
||||
error.OutOfMemory => isolate.createError("out of memory"),
|
||||
error.IllegalConstructor => isolate.createError("Illegal Constructor"),
|
||||
|
||||
@@ -581,8 +581,7 @@ fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) v
|
||||
return;
|
||||
}
|
||||
|
||||
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
||||
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
||||
const v8_isolate = v8.v8__Isolate__GetCurrent().?;
|
||||
const isolate = js.Isolate{ .handle = v8_isolate };
|
||||
const ctx, const v8_context = Context.fromIsolate(isolate) orelse return;
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ pub fn getException(self: Module) js.Value {
|
||||
|
||||
pub fn getModuleRequests(self: Module) Requests {
|
||||
return .{
|
||||
.context_handle = self.local.handle,
|
||||
.handle = v8.v8__Module__GetModuleRequests(self.handle).?,
|
||||
};
|
||||
}
|
||||
@@ -117,14 +116,13 @@ pub const Global = struct {
|
||||
|
||||
const Requests = struct {
|
||||
handle: *const v8.FixedArray,
|
||||
context_handle: *const v8.Context,
|
||||
|
||||
pub fn len(self: Requests) usize {
|
||||
return @intCast(v8.v8__FixedArray__Length(self.handle));
|
||||
}
|
||||
|
||||
pub fn get(self: Requests, idx: usize) Request {
|
||||
return .{ .handle = v8.v8__FixedArray__Get(self.handle, self.context_handle, @intCast(idx)).? };
|
||||
return .{ .handle = v8.v8__FixedArray__Get(self.handle, @intCast(idx)).? };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -798,6 +798,7 @@ fn attachClass(comptime JsApi: type, comptime flatten: bool, isolate: *v8.Isolat
|
||||
.deleter = null,
|
||||
.definer = null,
|
||||
.descriptor = null,
|
||||
.index_of = null,
|
||||
.data = null,
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
@@ -259,8 +259,8 @@ pub const Accessor = struct {
|
||||
};
|
||||
|
||||
pub const Indexed = struct {
|
||||
getter: *const fn (idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
||||
enumerator: ?*const fn (handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
||||
getter: *const fn (idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u32,
|
||||
enumerator: ?*const fn (handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u32,
|
||||
|
||||
const Opts = struct {
|
||||
as_typed_array: bool = false,
|
||||
@@ -271,11 +271,11 @@ pub const Indexed = struct {
|
||||
var indexed = Indexed{
|
||||
.enumerator = null,
|
||||
.getter = struct {
|
||||
fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u32 {
|
||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
if (!caller.init(v8_isolate)) {
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
}
|
||||
defer caller.deinit();
|
||||
|
||||
@@ -289,11 +289,11 @@ pub const Indexed = struct {
|
||||
|
||||
if (@typeInfo(@TypeOf(enumerator)) != .null) {
|
||||
indexed.enumerator = struct {
|
||||
fn wrap(handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
fn wrap(handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u32 {
|
||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
if (!caller.init(v8_isolate)) {
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
}
|
||||
defer caller.deinit();
|
||||
return caller.getEnumerator(T, enumerator, handle.?, .{});
|
||||
@@ -306,9 +306,9 @@ pub const Indexed = struct {
|
||||
};
|
||||
|
||||
pub const NamedIndexed = struct {
|
||||
getter: *const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8,
|
||||
setter: ?*const fn (c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,
|
||||
deleter: ?*const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 = null,
|
||||
getter: *const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u32,
|
||||
setter: ?*const fn (c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u32 = null,
|
||||
deleter: ?*const fn (c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u32 = null,
|
||||
|
||||
const Opts = struct {
|
||||
as_typed_array: bool = false,
|
||||
@@ -321,11 +321,11 @@ pub const NamedIndexed = struct {
|
||||
|
||||
fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed {
|
||||
const getter_fn = struct {
|
||||
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u32 {
|
||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
if (!caller.init(v8_isolate)) {
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
}
|
||||
defer caller.deinit();
|
||||
|
||||
@@ -337,11 +337,11 @@ pub const NamedIndexed = struct {
|
||||
}.wrap;
|
||||
|
||||
const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct {
|
||||
fn wrap(c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
fn wrap(c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u32 {
|
||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
if (!caller.init(v8_isolate)) {
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
}
|
||||
defer caller.deinit();
|
||||
|
||||
@@ -365,11 +365,11 @@ pub const NamedIndexed = struct {
|
||||
}.wrap;
|
||||
|
||||
const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct {
|
||||
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u32 {
|
||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
if (!caller.init(v8_isolate)) {
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
}
|
||||
defer caller.deinit();
|
||||
|
||||
@@ -480,14 +480,14 @@ pub const Property = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u32 {
|
||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||
|
||||
// During snapshot creation, there's no Context in embedder data yet.
|
||||
// I hate this check, but there doesn't seem to be a way to add this method
|
||||
// to the global, without triggering it during snapshot creation.
|
||||
const v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate) orelse return 0;
|
||||
const ctx: *Context = @ptrCast(@alignCast(v8.v8__Context__GetAlignedPointerFromEmbedderData(v8_context, 1) orelse return 0));
|
||||
const v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate) orelse return js.Intercepted.no;
|
||||
const ctx: *Context = @ptrCast(@alignCast(v8.v8__Context__GetAlignedPointerFromEmbedderData(v8_context, 1) orelse return js.Intercepted.no));
|
||||
|
||||
var caller: Caller = undefined;
|
||||
caller.initWithContext(ctx, v8_context);
|
||||
@@ -500,7 +500,7 @@ pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8
|
||||
defer hs.deinit();
|
||||
|
||||
const property: []const u8 = js.String.toSlice(.{ .local = local, .handle = @ptrCast(c_name.?) }) catch {
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
};
|
||||
|
||||
// Only Page contexts have document.getElementById lookup
|
||||
@@ -508,10 +508,10 @@ pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8
|
||||
.frame => |frame| {
|
||||
const document = frame.document;
|
||||
if (document.getElementById(property, frame)) |el| {
|
||||
const js_val = local.zigValueToJs(el, .{}) catch return 0;
|
||||
const js_val = local.zigValueToJs(el, .{}) catch return js.Intercepted.no;
|
||||
var pc = Caller.PropertyCallbackInfo{ .handle = handle.? };
|
||||
pc.getReturnValue().set(js_val);
|
||||
return 1;
|
||||
return js.Intercepted.yes;
|
||||
}
|
||||
},
|
||||
.worker => {}, // no global lookup in a worker
|
||||
@@ -521,7 +521,7 @@ pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8
|
||||
if (std.mem.startsWith(u8, property, "__")) {
|
||||
// some frameworks will extend built-in types using a __ prefix
|
||||
// these should always be safe to ignore.
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
}
|
||||
|
||||
const ignored = std.StaticStringMap(void).initComptime(.{
|
||||
@@ -553,28 +553,27 @@ pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8
|
||||
});
|
||||
if (!ignored.has(property)) {
|
||||
var buf: [2048]u8 = undefined;
|
||||
const key = std.fmt.bufPrint(&buf, "Window:{s}", .{property}) catch return 0;
|
||||
logUnknownProperty(local, key) catch return 0;
|
||||
const key = std.fmt.bufPrint(&buf, "Window:{s}", .{property}) catch return js.Intercepted.no;
|
||||
logUnknownProperty(local, key) catch return js.Intercepted.no;
|
||||
}
|
||||
}
|
||||
|
||||
// not intercepted
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
}
|
||||
|
||||
// Only used for debugging
|
||||
pub fn unknownObjectPropertyCallback(comptime JsApi: type) *const fn (?*const v8.Name, ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
pub fn unknownObjectPropertyCallback(comptime JsApi: type) *const fn (?*const v8.Name, ?*const v8.PropertyCallbackInfo) callconv(.c) u32 {
|
||||
if (comptime !IS_DEBUG) {
|
||||
@compileError("unknownObjectPropertyCallback should only be used in debug builds");
|
||||
}
|
||||
|
||||
return struct {
|
||||
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
|
||||
fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u32 {
|
||||
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
|
||||
|
||||
var caller: Caller = undefined;
|
||||
if (!caller.init(v8_isolate)) {
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
}
|
||||
defer caller.deinit();
|
||||
|
||||
@@ -585,45 +584,44 @@ pub fn unknownObjectPropertyCallback(comptime JsApi: type) *const fn (?*const v8
|
||||
defer hs.deinit();
|
||||
|
||||
const property: []const u8 = js.String.toSlice(.{ .local = local, .handle = @ptrCast(c_name.?) }) catch {
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
};
|
||||
|
||||
if (std.mem.startsWith(u8, property, "__")) {
|
||||
// some frameworks will extend built-in types using a __ prefix
|
||||
// these should always be safe to ignore.
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, property, "jQuery")) {
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
}
|
||||
|
||||
if (JsApi == @import("../webapi/cdata/Text.zig").JsApi or JsApi == @import("../webapi/cdata/Comment.zig").JsApi) {
|
||||
if (std.mem.eql(u8, property, "tagName")) {
|
||||
// knockout does this, a lot.
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
}
|
||||
}
|
||||
|
||||
if (JsApi == @import("../webapi/element/Html.zig").JsApi or JsApi == @import("../webapi/Element.zig").JsApi or JsApi == @import("../webapi/element/html/Custom.zig").JsApi) {
|
||||
// react ?
|
||||
if (std.mem.eql(u8, property, "props")) return 0;
|
||||
if (std.mem.eql(u8, property, "hydrated")) return 0;
|
||||
if (std.mem.eql(u8, property, "isHydrated")) return 0;
|
||||
if (std.mem.eql(u8, property, "props")) return js.Intercepted.no;
|
||||
if (std.mem.eql(u8, property, "hydrated")) return js.Intercepted.no;
|
||||
if (std.mem.eql(u8, property, "isHydrated")) return js.Intercepted.no;
|
||||
}
|
||||
|
||||
if (JsApi == @import("../webapi/Console.zig").JsApi) {
|
||||
if (std.mem.eql(u8, property, "firebug")) return 0;
|
||||
if (std.mem.eql(u8, property, "firebug")) return js.Intercepted.no;
|
||||
}
|
||||
|
||||
const ignored = std.StaticStringMap(void).initComptime(.{});
|
||||
if (!ignored.has(property)) {
|
||||
var buf: [2048]u8 = undefined;
|
||||
const key = std.fmt.bufPrint(&buf, "{s}:{s}", .{ if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi), property }) catch return 0;
|
||||
logUnknownProperty(local, key) catch return 0;
|
||||
const key = std.fmt.bufPrint(&buf, "{s}:{s}", .{ if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi), property }) catch return js.Intercepted.no;
|
||||
logUnknownProperty(local, key) catch return js.Intercepted.no;
|
||||
}
|
||||
// not intercepted
|
||||
return 0;
|
||||
return js.Intercepted.no;
|
||||
}
|
||||
}.wrap;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ const lp = @import("lightpanda");
|
||||
|
||||
pub const v8 = @import("v8").c;
|
||||
|
||||
pub const Intercepted = struct {
|
||||
pub const yes: u32 = 0;
|
||||
pub const no: u32 = 1;
|
||||
};
|
||||
|
||||
const string = @import("../../string.zig");
|
||||
|
||||
pub const Env = @import("Env.zig");
|
||||
|
||||
@@ -709,7 +709,7 @@ test "browser.markdown: block link" {
|
||||
\\### Title
|
||||
\\
|
||||
\\Description
|
||||
\\[https://example.com](https://example.com)
|
||||
\\[https://example.com/](https://example.com/)
|
||||
\\
|
||||
);
|
||||
}
|
||||
@@ -725,7 +725,7 @@ test "browser.markdown: block link with aria-label" {
|
||||
\\### Title
|
||||
\\
|
||||
\\Description
|
||||
\\[Docs](https://example.com)
|
||||
\\[Docs](https://example.com/)
|
||||
\\
|
||||
);
|
||||
}
|
||||
@@ -741,7 +741,7 @@ test "browser.markdown: block link with title" {
|
||||
\\### Title
|
||||
\\
|
||||
\\Description
|
||||
\\[Docs](https://example.com)
|
||||
\\[Docs](https://example.com/)
|
||||
\\
|
||||
);
|
||||
}
|
||||
@@ -751,7 +751,7 @@ test "browser.markdown: inline link" {
|
||||
\\<p>Visit <a href="https://example.com">Example</a>.</p>
|
||||
,
|
||||
\\
|
||||
\\Visit [Example](https://example.com).
|
||||
\\Visit [Example](https://example.com/).
|
||||
\\
|
||||
);
|
||||
}
|
||||
|
||||
@@ -253,3 +253,37 @@
|
||||
testing.expectEqual(testing.BASE_URL + 'element/html/over%209000!', a.href);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=reflecting_getter_about_blank_base>
|
||||
// Regression: with an `about:blank` document base, the WHATWG URL parser
|
||||
// can't join a relative reference to a cannot-be-a-base URL. A reflecting
|
||||
// URL getter must still NOT throw — per spec it returns the raw content
|
||||
// attribute unchanged. (A thrown TypeError here used to break the WPT
|
||||
// harness's get_stack(), which reads script.src, and hung the run.)
|
||||
{
|
||||
let base = document.createElement('base');
|
||||
document.head.appendChild(base);
|
||||
base.href = 'about:blank';
|
||||
|
||||
let a = document.createElement('a');
|
||||
a.setAttribute('href', 'foo/bar');
|
||||
testing.expectEqual('foo/bar', a.href);
|
||||
|
||||
let s = document.createElement('script');
|
||||
s.src = 'resources/x.js';
|
||||
testing.expectEqual('resources/x.js', s.src);
|
||||
|
||||
let img = document.createElement('img');
|
||||
img.setAttribute('src', 'pic.png');
|
||||
testing.expectEqual('pic.png', img.src);
|
||||
|
||||
// An absolute reference still resolves normally against an about:blank base.
|
||||
let abs = document.createElement('a');
|
||||
abs.setAttribute('href', 'https://example.com/x');
|
||||
testing.expectEqual('https://example.com/x', abs.href);
|
||||
|
||||
// Restore the document base so later test blocks are unaffected.
|
||||
base.href = '';
|
||||
document.head.removeChild(base);
|
||||
}
|
||||
</script>
|
||||
|
||||
49
src/browser/tests/frames/cross_origin_message_source.html
Normal file
49
src/browser/tests/frames/cross_origin_message_source.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<!--
|
||||
MessageEvent.source is the WindowProxy of the frame the message came from, so it
|
||||
is reference-equal to that frame's iframe.contentWindow — for a cross-origin
|
||||
sender as well as a same-origin one. The two children below differ only by origin:
|
||||
localhost is cross-origin to the 127.0.0.1 test page, 127.0.0.1 is same-origin;
|
||||
both must satisfy event.source === iframe.contentWindow.
|
||||
-->
|
||||
|
||||
<script id="message_source_identity" type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
const ALT_BASE = testing.BASE_URL.replace('127.0.0.1', 'localhost');
|
||||
const ALT_ORIGIN = testing.ORIGIN.replace('127.0.0.1', 'localhost');
|
||||
|
||||
function loadAndAwait(src, expectedOrigin) {
|
||||
return new Promise((resolve) => {
|
||||
const iframe = document.createElement('iframe');
|
||||
const onmsg = (e) => {
|
||||
if (e.origin !== expectedOrigin) return;
|
||||
window.removeEventListener('message', onmsg);
|
||||
resolve({ iframe, event: e });
|
||||
};
|
||||
window.addEventListener('message', onmsg);
|
||||
iframe.src = src;
|
||||
document.documentElement.appendChild(iframe);
|
||||
});
|
||||
}
|
||||
|
||||
// localhost is a different origin than the 127.0.0.1 test page: cross-origin sender.
|
||||
const cross = await loadAndAwait(ALT_BASE + 'frames/support/post_to_parent.html', ALT_ORIGIN);
|
||||
// 127.0.0.1 is the test page's own origin: same-origin control.
|
||||
const same = await loadAndAwait(testing.BASE_URL + 'frames/support/post_to_parent.html', testing.ORIGIN);
|
||||
state.resolve();
|
||||
|
||||
await state.done(() => {
|
||||
testing.expectEqual('hello', cross.event.data);
|
||||
testing.expectEqual(ALT_ORIGIN, cross.event.origin);
|
||||
testing.expectFalse(cross.event.source === null);
|
||||
testing.expectTrue(cross.event.source === cross.iframe.contentWindow);
|
||||
|
||||
testing.expectEqual('hello', same.event.data);
|
||||
testing.expectEqual(testing.ORIGIN, same.event.origin);
|
||||
testing.expectTrue(same.event.source === same.iframe.contentWindow);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
4
src/browser/tests/frames/support/post_to_parent.html
Normal file
4
src/browser/tests/frames/support/post_to_parent.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<script>
|
||||
parent.postMessage('hello', '*');
|
||||
</script>
|
||||
@@ -125,7 +125,7 @@
|
||||
headers: { "Sender": "me", "Target": "you" }
|
||||
}
|
||||
);
|
||||
testing.expectEqual("https://google.com", request2.url);
|
||||
testing.expectEqual("https://google.com/", request2.url);
|
||||
testing.expectEqual("POST", request2.method);
|
||||
testing.expectEqual("omit", request2.credentials);
|
||||
testing.expectEqual("reload", request2.cache);
|
||||
|
||||
@@ -481,3 +481,30 @@
|
||||
testing.expectEqual(false, registered);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=xhr_send_flag type=module>
|
||||
{
|
||||
// Per spec, calling send() while a request is already in flight throws
|
||||
// InvalidStateError (the "send() flag"). Without it, two concurrent
|
||||
// transfers race on the XHR's single response slot and one is left dangling
|
||||
// at teardown (the "release overflow" UAF).
|
||||
const state = await testing.async();
|
||||
const req = new XMLHttpRequest();
|
||||
req.onload = () => state.resolve();
|
||||
req.open('GET', 'http://127.0.0.1:9582/xhr');
|
||||
req.send();
|
||||
testing.expectError('InvalidStateError', () => req.send());
|
||||
|
||||
await state.done(() => {
|
||||
// the first request was unaffected by the rejected second send()
|
||||
testing.expectEqual(200, req.status);
|
||||
});
|
||||
|
||||
// a fresh open() clears the flag, so the object is reusable
|
||||
const state2 = await testing.async();
|
||||
req.onload = () => state2.resolve();
|
||||
req.open('GET', 'http://127.0.0.1:9582/xhr');
|
||||
req.send();
|
||||
await state2.done(() => testing.expectEqual(200, req.status));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -333,31 +333,88 @@
|
||||
<script id=parse_static>
|
||||
testing.expectEqual('function', typeof URL.parse);
|
||||
|
||||
// Parseable input returns a URL instance (mirrors the constructor).
|
||||
// Parseable absolute URLs (no base) -> a URL instance. parse mirrors the
|
||||
// constructor: it returns an object exactly when `new URL(...)` would not
|
||||
// throw, and null otherwise.
|
||||
{
|
||||
const url = URL.parse('https://example.com/path?a=b#h');
|
||||
const url = URL.parse('https://example.com:8080/path?a=b#hash');
|
||||
testing.expectEqual(true, url instanceof URL);
|
||||
testing.expectEqual('https://example.com/path?a=b#h', url.href);
|
||||
testing.expectEqual('https://example.com:8080/path?a=b#hash', url.href);
|
||||
}
|
||||
|
||||
// Relative input resolves against a valid base.
|
||||
// The returned object is a fully-functional URL: its components are
|
||||
// populated and its accessors behave like a constructed instance.
|
||||
{
|
||||
const url = URL.parse('/x', 'http://example.com/a/b');
|
||||
testing.expectEqual('http://example.com/x', url.href);
|
||||
const url = URL.parse('https://user:pass@example.com:8080/p/q?x=1#frag');
|
||||
testing.expectEqual('https:', url.protocol);
|
||||
testing.expectEqual('user', url.username);
|
||||
testing.expectEqual('pass', url.password);
|
||||
testing.expectEqual('example.com', url.hostname);
|
||||
testing.expectEqual('8080', url.port);
|
||||
testing.expectEqual('example.com:8080', url.host);
|
||||
testing.expectEqual('/p/q', url.pathname);
|
||||
testing.expectEqual('?x=1', url.search);
|
||||
testing.expectEqual('#frag', url.hash);
|
||||
testing.expectEqual('https://example.com:8080', url.origin);
|
||||
testing.expectEqual('1', url.searchParams.get('x'));
|
||||
}
|
||||
|
||||
// Unlike the constructor, a parse failure returns null instead of throwing.
|
||||
// The returned object is mutable like any other URL instance.
|
||||
{
|
||||
const url = URL.parse('https://example.com/path');
|
||||
url.pathname = '/changed';
|
||||
testing.expectEqual('https://example.com/changed', url.href);
|
||||
}
|
||||
|
||||
// Non-special but parseable schemes -> a URL instance.
|
||||
{
|
||||
testing.expectEqual(true, URL.parse('about:blank') instanceof URL);
|
||||
testing.expectEqual(true, URL.parse('mailto:foo@bar.com') instanceof URL);
|
||||
testing.expectEqual(true, URL.parse('data:text/plain,hello') instanceof URL);
|
||||
testing.expectEqual(true, URL.parse('file:///etc/hosts') instanceof URL);
|
||||
}
|
||||
|
||||
// Relative or malformed input (no base) -> null instead of throwing.
|
||||
{
|
||||
testing.expectEqual(null, URL.parse('foo.js'));
|
||||
testing.expectEqual(null, URL.parse(''));
|
||||
testing.expectEqual(null, URL.parse('http://example.com', 'invalid base'));
|
||||
testing.expectEqual(null, URL.parse('foo.js'));
|
||||
testing.expectEqual(null, URL.parse('/path/to/file'));
|
||||
testing.expectEqual(null, URL.parse('?query=value'));
|
||||
testing.expectEqual(null, URL.parse('#fragment'));
|
||||
testing.expectEqual(null, URL.parse('//only-host'));
|
||||
testing.expectEqual(null, URL.parse('http://'));
|
||||
}
|
||||
|
||||
// Each call returns a distinct object.
|
||||
// Relative input resolved against a valid base -> a URL instance with the
|
||||
// input resolved against the base.
|
||||
{
|
||||
const a = URL.parse('https://example.com/');
|
||||
const b = URL.parse('https://example.com/');
|
||||
testing.expectEqual(false, a === b);
|
||||
testing.expectEqual('http://example.com/path/subpath', URL.parse('subpath', 'http://example.com/path/').href);
|
||||
testing.expectEqual('http://example.com/newpath', URL.parse('/newpath', 'http://example.com/path').href);
|
||||
testing.expectEqual('http://example.com/path?a=b', URL.parse('?a=b', 'http://example.com/path').href);
|
||||
testing.expectEqual('http://example.com/path#hash', URL.parse('#hash', 'http://example.com/path').href);
|
||||
testing.expectEqual('http://example.com/a/foo', URL.parse('../foo', 'http://example.com/a/b/c').href);
|
||||
testing.expectEqual('http://other.com/p', URL.parse('//other.com/p', 'http://example.com/').href);
|
||||
}
|
||||
|
||||
// Absolute input with a valid base -> the input wins, the base is ignored.
|
||||
{
|
||||
testing.expectEqual('http://example.com/absolute', URL.parse('http://example.com/absolute', 'http://base.com').href);
|
||||
}
|
||||
|
||||
// The base is parsed unconditionally: an unparseable base -> null, even when
|
||||
// the input itself is absolute (mirrors the constructor throwing).
|
||||
{
|
||||
testing.expectEqual(null, URL.parse('http://example.com/absolute', 'invalid base'));
|
||||
testing.expectEqual(null, URL.parse('foo.js', 'not a url'));
|
||||
testing.expectEqual(null, URL.parse('subpath', 'relative/path'));
|
||||
testing.expectEqual(null, URL.parse('http://example.com', ''));
|
||||
}
|
||||
|
||||
// An undefined base is treated as "no base given".
|
||||
{
|
||||
testing.expectEqual(true, URL.parse('https://example.com', undefined) instanceof URL);
|
||||
testing.expectEqual(null, URL.parse('foo.js', undefined));
|
||||
testing.expectEqual(null, URL.parse('subpath', undefined));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -572,12 +629,13 @@
|
||||
testing.expectEqual('https://[::1]:9000/path', url.href);
|
||||
}
|
||||
|
||||
// A trailing colon clears the port.
|
||||
// A trailing colon supplies an empty port, which the parser ignores: the
|
||||
// existing port is left untouched (matches Chrome and Firefox).
|
||||
{
|
||||
const url = new URL('https://example.com:8080/path');
|
||||
url.host = 'newhost.com:';
|
||||
testing.expectEqual('newhost.com', url.host);
|
||||
testing.expectEqual('https://newhost.com/path', url.href);
|
||||
testing.expectEqual('newhost.com:8080', url.host);
|
||||
testing.expectEqual('https://newhost.com:8080/path', url.href);
|
||||
}
|
||||
|
||||
// An invalid host is silently ignored, leaving it unchanged (the setter must
|
||||
@@ -1263,3 +1321,43 @@
|
||||
testing.expectEqual('?a=%7E&b=%7E&c=foo', url.search);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=wpt_searchparams_appends_equals>
|
||||
// url-constructor: a query key with no value serializes back with a
|
||||
// trailing '=' via URLSearchParams.
|
||||
{
|
||||
const url = new URL('http://foo.bar/baz?qux');
|
||||
testing.expectEqual('qux=', url.searchParams.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=wpt_searchparams_encodes_question_mark>
|
||||
// url-constructor: '?' inside a key must be percent-encoded as %3F when
|
||||
// re-serialized through searchParams.
|
||||
{
|
||||
const url = new URL('??a=b&c=d', 'http://example.org/foo/bar');
|
||||
testing.expectEqual('%3Fa=b&c=d', url.searchParams.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=wpt_setter_host_with_slash>
|
||||
// url-setters: setting host to a value containing a '/' takes only the part
|
||||
// before the delimiter as the host (the rest is ignored).
|
||||
{
|
||||
const url = new URL('http://example.net/path');
|
||||
url.host = 'example.com/stuff';
|
||||
testing.expectEqual('example.com', url.host);
|
||||
testing.expectEqual('http://example.com/path', url.href);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=wpt_setter_hostname_strips_tab>
|
||||
// url-setters: a tab in a hostname is stripped (here clearing the host),
|
||||
// the setter must not throw.
|
||||
{
|
||||
const url = new URL('sc://x/');
|
||||
url.hostname = '\t';
|
||||
testing.expectEqual('', url.hostname);
|
||||
testing.expectEqual('sc:///', url.href);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -191,6 +191,31 @@ pub fn setDomain(self: *Document, value: []const u8) !void {
|
||||
try doc_frame.js.setOrigin(key);
|
||||
}
|
||||
|
||||
pub fn getCookie(_: *Document, frame: *Frame) ![]const u8 {
|
||||
var buf: std.ArrayList(u8) = .empty;
|
||||
try frame._session.cookie_jar.forRequest(frame.url, buf.writer(frame.call_arena), .{
|
||||
.is_http = false,
|
||||
.is_navigation = true,
|
||||
});
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn setCookie(_: *Document, cookie_str: []const u8, frame: *Frame) ![]const u8 {
|
||||
// we use the cookie jar's allocator to parse the cookie because it
|
||||
// outlives the frame's arena.
|
||||
const Cookie = @import("storage/Cookie.zig");
|
||||
const c = Cookie.parse(frame._session.cookie_jar.allocator, frame.url, cookie_str) catch {
|
||||
// Invalid cookies should be silently ignored, not throw errors
|
||||
return "";
|
||||
};
|
||||
if (c.http_only) {
|
||||
c.deinit();
|
||||
return ""; // HttpOnly cookies cannot be set from JS
|
||||
}
|
||||
try frame._session.cookie_jar.add(c, std.time.timestamp(), false);
|
||||
return cookie_str;
|
||||
}
|
||||
|
||||
// Returns true if the requested domain is valid for the given host
|
||||
fn isRelaxableTo(host: []const u8, requested: []const u8) bool {
|
||||
if (requested.len == 0) {
|
||||
@@ -1254,6 +1279,7 @@ pub const JsApi = struct {
|
||||
pub const fonts = bridge.accessor(Document.getFonts, null, .{});
|
||||
pub const contentType = bridge.accessor(Document.getContentType, null, .{});
|
||||
pub const domain = bridge.accessor(Document.getDomain, Document.setDomain, .{ .dom_exception = true });
|
||||
pub const cookie = bridge.accessor(Document.getCookie, Document.setCookie, .{});
|
||||
pub const createElement = bridge.function(Document.createElement, .{ .dom_exception = true });
|
||||
pub const createElementNS = bridge.function(Document.createElementNS, .{ .dom_exception = true });
|
||||
pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{});
|
||||
|
||||
@@ -224,31 +224,6 @@ pub fn getAll(self: *HTMLDocument, frame: *Frame) !*collections.HTMLAllCollectio
|
||||
return frame._factory.create(collections.HTMLAllCollection.init(self.asNode(), frame));
|
||||
}
|
||||
|
||||
pub fn getCookie(_: *HTMLDocument, frame: *Frame) ![]const u8 {
|
||||
var buf: std.ArrayList(u8) = .empty;
|
||||
try frame._session.cookie_jar.forRequest(frame.url, buf.writer(frame.call_arena), .{
|
||||
.is_http = false,
|
||||
.is_navigation = true,
|
||||
});
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
pub fn setCookie(_: *HTMLDocument, cookie_str: []const u8, frame: *Frame) ![]const u8 {
|
||||
// we use the cookie jar's allocator to parse the cookie because it
|
||||
// outlives the frame's arena.
|
||||
const Cookie = @import("storage/Cookie.zig");
|
||||
const c = Cookie.parse(frame._session.cookie_jar.allocator, frame.url, cookie_str) catch {
|
||||
// Invalid cookies should be silently ignored, not throw errors
|
||||
return "";
|
||||
};
|
||||
if (c.http_only) {
|
||||
c.deinit();
|
||||
return ""; // HttpOnly cookies cannot be set from JS
|
||||
}
|
||||
try frame._session.cookie_jar.add(c, std.time.timestamp(), false);
|
||||
return cookie_str;
|
||||
}
|
||||
|
||||
pub fn getDocType(self: *HTMLDocument, frame: *Frame) !*DocumentType {
|
||||
if (self._document_type) |dt| {
|
||||
return dt;
|
||||
@@ -302,6 +277,5 @@ pub const JsApi = struct {
|
||||
pub const plugins = bridge.accessor(HTMLDocument.getEmbeds, null, .{});
|
||||
pub const currentScript = bridge.accessor(HTMLDocument.getCurrentScript, null, .{});
|
||||
pub const all = bridge.accessor(HTMLDocument.getAll, null, .{});
|
||||
pub const cookie = bridge.accessor(HTMLDocument.getCookie, HTMLDocument.setCookie, .{});
|
||||
pub const doctype = bridge.accessor(HTMLDocument.getDocType, null, .{});
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ pub fn setScrollRestoration(self: *History, str: []const u8) void {
|
||||
pub fn pushState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, frame: *Frame) !void {
|
||||
const arena = frame._session.arena;
|
||||
const url = if (_url) |u|
|
||||
try @import("../URL.zig").resolve(arena, frame.url, u, .{ .always_dupe = true })
|
||||
try @import("../URL.zig").resolve(arena, frame.url, u, .{})
|
||||
else
|
||||
try arena.dupeZ(u8, frame.url);
|
||||
|
||||
@@ -67,7 +67,7 @@ pub fn pushState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8
|
||||
pub fn replaceState(_: *History, state: js.Value, _: ?[]const u8, _url: ?[]const u8, frame: *Frame) !void {
|
||||
const arena = frame._session.arena;
|
||||
const url = if (_url) |u|
|
||||
try @import("../URL.zig").resolve(arena, frame.url, u, .{ .always_dupe = true })
|
||||
try @import("../URL.zig").resolve(arena, frame.url, u, .{})
|
||||
else
|
||||
try arena.dupeZ(u8, frame.url);
|
||||
|
||||
|
||||
@@ -195,14 +195,12 @@ pub fn urlEncode(self: *const KeyValueList, comptime mode: URLEncodeMode, alloca
|
||||
|
||||
fn urlEncodeEntry(entry: Entry, comptime mode: URLEncodeMode, allocator_: ?Allocator, charset: []const u8, writer: *std.Io.Writer) !void {
|
||||
try urlEncodeValue(entry.name.str(), mode, allocator_, charset, writer);
|
||||
try writer.writeByte('=');
|
||||
|
||||
// for a form, for an empty value, we'll do "spice="
|
||||
// but for a query, we do "spice"
|
||||
if ((comptime mode == .query) and entry.value.len == 0) {
|
||||
if (entry.value.len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try writer.writeByte('=');
|
||||
try urlEncodeValue(entry.value.str(), mode, allocator_, charset, writer);
|
||||
}
|
||||
|
||||
@@ -260,7 +258,7 @@ fn urlEncodeValue(value: []const u8, comptime mode: URLEncodeMode, allocator_: ?
|
||||
|
||||
/// Percent-encode a UTF-8 value - bytes >= 0x80 are percent-encoded directly.
|
||||
fn urlEncodeValueUtf8(value: []const u8, comptime mode: URLEncodeMode, writer: *std.Io.Writer) !void {
|
||||
if (!urlEncodeShouldEscape(value, mode)) {
|
||||
if (!urlEncodeShouldEscape(value)) {
|
||||
return writer.writeAll(value);
|
||||
}
|
||||
|
||||
@@ -270,7 +268,7 @@ fn urlEncodeValueUtf8(value: []const u8, comptime mode: URLEncodeMode, writer: *
|
||||
if (comptime mode == .form) {
|
||||
if (try writeFormLineEnd(value, &i, b, writer)) continue;
|
||||
}
|
||||
if (urlEncodeUnreserved(b, mode)) {
|
||||
if (urlEncodeUnreserved(b)) {
|
||||
try writer.writeByte(b);
|
||||
} else if (b == ' ') {
|
||||
try writer.writeByte('+');
|
||||
@@ -288,7 +286,7 @@ fn urlEncodeValueLegacy(value: []const u8, comptime mode: URLEncodeMode, writer:
|
||||
if (comptime mode == .form) {
|
||||
if (try writeFormLineEnd(value, &i, b, writer)) continue;
|
||||
}
|
||||
if (urlEncodeUnreserved(b, mode)) {
|
||||
if (urlEncodeUnreserved(b)) {
|
||||
try writer.writeByte(b);
|
||||
} else if (b == ' ') {
|
||||
try writer.writeByte('+');
|
||||
@@ -320,19 +318,18 @@ fn writeFormLineEnd(value: []const u8, i: *usize, b: u8, writer: *std.Io.Writer)
|
||||
return false;
|
||||
}
|
||||
|
||||
fn urlEncodeShouldEscape(value: []const u8, comptime mode: URLEncodeMode) bool {
|
||||
fn urlEncodeShouldEscape(value: []const u8) bool {
|
||||
for (value) |b| {
|
||||
if (!urlEncodeUnreserved(b, mode)) {
|
||||
if (!urlEncodeUnreserved(b)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn urlEncodeUnreserved(b: u8, comptime mode: URLEncodeMode) bool {
|
||||
fn urlEncodeUnreserved(b: u8) bool {
|
||||
return switch (b) {
|
||||
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '*' => true,
|
||||
'~' => comptime mode == .form,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
@@ -391,6 +388,19 @@ test "KeyValueList: urlEncode UTF-8" {
|
||||
try testing.expectString("cafe=caf%C3%A9", buf.written());
|
||||
}
|
||||
|
||||
test "KeyValueList: urlEncode .form percent-encodes ~" {
|
||||
// '~' is in the application/x-www-form-urlencoded percent-encode set, so it
|
||||
// must become %7E in both .form and .query modes (not left literal).
|
||||
const allocator = testing.arena_allocator;
|
||||
var list = KeyValueList.init();
|
||||
try list.append(allocator, "q", "a~b");
|
||||
|
||||
var buf = std.Io.Writer.Allocating.init(allocator);
|
||||
try list.urlEncode(.form, null, "UTF-8", &buf.writer);
|
||||
|
||||
try testing.expectString("q=a%7Eb", buf.written());
|
||||
}
|
||||
|
||||
test "KeyValueList: urlEncode UTF-8 CJK" {
|
||||
// Test 3-byte UTF-8 characters (Chinese/Japanese)
|
||||
const allocator = testing.arena_allocator;
|
||||
|
||||
@@ -35,7 +35,6 @@ pub const DocumentFragment = @import("DocumentFragment.zig");
|
||||
pub const DocumentType = @import("DocumentType.zig");
|
||||
pub const ShadowRoot = @import("ShadowRoot.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const String = lp.String;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const LinkedList = std.DoublyLinkedList;
|
||||
@@ -586,6 +585,15 @@ pub fn resolveURL(self: *const Node, url: anytype, frame: *Frame, opts: ResolveU
|
||||
return URL.resolve(allocator, owner_frame.base(), url, .{ .encoding = owner_frame.charset });
|
||||
}
|
||||
|
||||
// Same as `resolveURL` but can't return `TypeError`, this is needed for multiple
|
||||
// getters throughout codebase. Returns provided `url` on `TypeError`.
|
||||
pub fn resolveURLReflect(self: *const Node, url: []const u8, frame: *Frame, opts: ResolveURLOpts) ![]const u8 {
|
||||
return self.resolveURL(url, frame, opts) catch |err| switch (err) {
|
||||
error.TypeError => url,
|
||||
else => err,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isSameDocumentAs(self: *const Node, other: *const Node, frame: *const Frame) bool {
|
||||
// Get the root document for each node
|
||||
const self_doc = if (self._type == .document) self._type.document else self.ownerDocument(frame);
|
||||
|
||||
@@ -53,6 +53,11 @@ pub fn init(url: []const u8, maybe_base: ?[]const u8, exec: *const Execution) !*
|
||||
return exec._factory.create(URL{ ._url = u });
|
||||
}
|
||||
|
||||
/// Like the constructor, but returns null instead of throwing when parsing fails.
|
||||
pub fn parse(url: []const u8, maybe_base: ?[]const u8, exec: *const Execution) ?*URL {
|
||||
return URL.init(url, maybe_base, exec) catch null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *URL, _: *Page) void {
|
||||
// Not tracked by arena.
|
||||
U.url_free(self._url);
|
||||
@@ -209,7 +214,7 @@ pub fn setSearch(self: *URL, value: []const u8, exec: *const Execution) !void {
|
||||
const search_params = self._search_params orelse return;
|
||||
var out: [*]const u8 = undefined;
|
||||
var len: usize = 0;
|
||||
const search_value = if (U.url_get_query(self._url, &out, &len) == 0) out[0..len] else "";
|
||||
const search_value = if (U.url_get_query(self._url, &out, &len) == 0) (out - 1)[0 .. len + 1] else "";
|
||||
try search_params.updateFromString(search_value, exec);
|
||||
}
|
||||
|
||||
@@ -242,10 +247,11 @@ pub fn getSearchParams(self: *URL, exec: *const Execution) !*URLSearchParams {
|
||||
return sp;
|
||||
}
|
||||
|
||||
// Get current search string (omitting '?').
|
||||
// Get current search string; rust-url always skips '?' so we have to
|
||||
// go a byte back to include it.
|
||||
var out: [*]const u8 = undefined;
|
||||
var len: usize = 0;
|
||||
const search_value = if (U.url_get_query(self._url, &out, &len) == 0) out[0..len] else "";
|
||||
const search_value = if (U.url_get_query(self._url, &out, &len) == 0) (out - 1)[0 .. len + 1] else "";
|
||||
|
||||
const params = try URLSearchParams.init(.{ .query_string = search_value }, exec);
|
||||
self._search_params = params;
|
||||
@@ -273,7 +279,7 @@ pub fn setHref(self: *URL, value: []const u8, exec: *const Execution) !void {
|
||||
const search_params = self._search_params orelse return;
|
||||
var out: [*]const u8 = undefined;
|
||||
var len: usize = 0;
|
||||
const search_value = if (U.url_get_query(url, &out, &len) == 0) out[0..len] else "";
|
||||
const search_value = if (U.url_get_query(url, &out, &len) == 0) (out - 1)[0 .. len + 1] else "";
|
||||
try search_params.updateFromString(search_value, exec);
|
||||
}
|
||||
|
||||
@@ -298,14 +304,6 @@ pub fn toString(self: *const URL, exec: *const Execution) ![]const u8 {
|
||||
return out[0..len];
|
||||
}
|
||||
|
||||
/// Like the constructor, but returns null instead of throwing when parsing fails.
|
||||
pub fn parse(url: []const u8, maybe_base: ?[]const u8, exec: *const Execution) !?*URL {
|
||||
return URL.init(url, maybe_base, exec) catch |err| switch (err) {
|
||||
error.TypeError => null,
|
||||
else => err,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn canParse(url: []const u8, maybe_base: ?[]const u8) bool {
|
||||
if (maybe_base) |base| {
|
||||
return U.url_can_parse_with_base(base.ptr, base.len, url.ptr, url.len);
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
//! agreement. Key bytes live as a libcrypto EVP_PKEY in `CryptoKey._vary.pkey`.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const crypto = @import("../../../sys/libcrypto.zig");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
|
||||
@@ -43,7 +43,11 @@ pub fn getHref(self: *Anchor, frame: *Frame) ![]const u8 {
|
||||
if (href.len == 0) {
|
||||
return "";
|
||||
}
|
||||
return self.asNode().resolveURL(href, frame, .{});
|
||||
return self.asNode().resolveURL(href, frame, .{}) catch |err| switch (err) {
|
||||
// Per spec the getter must not throw; it returns the content attribute.
|
||||
error.TypeError => href,
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setHref(self: *Anchor, value: []const u8, frame: *Frame) !void {
|
||||
@@ -155,7 +159,7 @@ pub fn setPathname(self: *Anchor, value: []const u8, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
pub fn getProtocol(self: *Anchor, frame: *Frame) ![]const u8 {
|
||||
const href = try getResolvedHref(self, frame) orelse return "";
|
||||
const href = try getResolvedHref(self, frame) orelse return ":";
|
||||
return URL.getProtocol(href);
|
||||
}
|
||||
|
||||
@@ -224,7 +228,12 @@ fn getResolvedHref(self: *Anchor, frame: *Frame) !?[:0]const u8 {
|
||||
if (href.len == 0) {
|
||||
return null;
|
||||
}
|
||||
return try self.asNode().resolveURL(href, frame, .{});
|
||||
return self.asNode().resolveURL(href, frame, .{}) catch |err| switch (err) {
|
||||
// Unparseable against the base: treat as no resolved URL so the
|
||||
// component getters return "" instead of throwing.
|
||||
error.TypeError => null,
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -38,7 +38,11 @@ pub fn getHref(self: *Area, frame: *Frame) ![]const u8 {
|
||||
if (href.len == 0) {
|
||||
return "";
|
||||
}
|
||||
return self.asNode().resolveURL(href, frame, .{});
|
||||
return self.asNode().resolveURL(href, frame, .{}) catch |err| switch (err) {
|
||||
// Per spec the getter must not throw; it returns the content attribute.
|
||||
error.TypeError => href,
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setHref(self: *Area, value: []const u8, frame: *Frame) !void {
|
||||
@@ -164,7 +168,7 @@ pub fn setPathname(self: *Area, value: []const u8, frame: *Frame) !void {
|
||||
}
|
||||
|
||||
pub fn getProtocol(self: *Area, frame: *Frame) ![]const u8 {
|
||||
const href = try getResolvedHref(self, frame) orelse return "";
|
||||
const href = try getResolvedHref(self, frame) orelse return ":";
|
||||
return URL.getProtocol(href);
|
||||
}
|
||||
|
||||
@@ -179,7 +183,12 @@ fn getResolvedHref(self: *Area, frame: *Frame) !?[:0]const u8 {
|
||||
if (href.len == 0) {
|
||||
return null;
|
||||
}
|
||||
return try self.asNode().resolveURL(href, frame, .{});
|
||||
return self.asNode().resolveURL(href, frame, .{}) catch |err| switch (err) {
|
||||
// Unparseable against the base: treat as no resolved URL so the
|
||||
// component getters return "" instead of throwing.
|
||||
error.TypeError => null,
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -142,7 +142,7 @@ pub fn getFormAction(self: *Button, frame: *Frame) ![]const u8 {
|
||||
if (action.len == 0) {
|
||||
return owner_url;
|
||||
}
|
||||
return element.asNode().resolveURL(action, frame, .{});
|
||||
return element.asNode().resolveURLReflect(action, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setFormAction(self: *Button, value: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -41,7 +41,7 @@ pub fn getSrc(self: *const Embed, frame: *Frame) ![]const u8 {
|
||||
if (src.len == 0) {
|
||||
return "";
|
||||
}
|
||||
return element.asConstNode().resolveURL(src, frame, .{});
|
||||
return element.asConstNode().resolveURLReflect(src, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setSrc(self: *Embed, value: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -120,7 +120,7 @@ pub fn getAction(self: *Form, frame: *Frame) ![]const u8 {
|
||||
if (action.len == 0) {
|
||||
return owner_url;
|
||||
}
|
||||
return element.asNode().resolveURL(action, frame, .{});
|
||||
return element.asNode().resolveURLReflect(action, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setAction(self: *Form, value: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -47,9 +47,9 @@ pub fn getContentDocument(self: *const IFrame) ?*Document {
|
||||
return window._document;
|
||||
}
|
||||
|
||||
pub fn getSrc(self: *IFrame, frame: *Frame) ![:0]const u8 {
|
||||
pub fn getSrc(self: *IFrame, frame: *Frame) ![]const u8 {
|
||||
if (self._src.len == 0) return "";
|
||||
return self.asNode().resolveURL(self._src, frame, .{});
|
||||
return self.asNode().resolveURLReflect(self._src, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setSrc(self: *IFrame, src: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -39,7 +39,7 @@ pub fn getSrc(self: *const Image, frame: *Frame) ![]const u8 {
|
||||
if (src.len == 0) {
|
||||
return "";
|
||||
}
|
||||
return element.asConstNode().resolveURL(src, frame, .{});
|
||||
return element.asConstNode().resolveURLReflect(src, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setSrc(self: *Image, value: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -659,7 +659,7 @@ pub fn setSize(self: *Input, size: i32, frame: *Frame) !void {
|
||||
|
||||
pub fn getSrc(self: *const Input, frame: *Frame) ![]const u8 {
|
||||
const src = self.asConstElement().getAttributeSafe(comptime .wrap("src")) orelse return "";
|
||||
return self.asConstElement().asConstNode().resolveURL(src, frame, .{});
|
||||
return self.asConstElement().asConstNode().resolveURLReflect(src, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setSrc(self: *Input, src: []const u8, frame: *Frame) !void {
|
||||
@@ -928,7 +928,7 @@ pub fn getFormAction(self: *Input, frame: *Frame) ![]const u8 {
|
||||
if (action.len == 0) {
|
||||
return owner_url;
|
||||
}
|
||||
return element.asNode().resolveURL(action, frame, .{});
|
||||
return element.asNode().resolveURLReflect(action, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setFormAction(self: *Input, value: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -49,7 +49,7 @@ pub fn getHref(self: *Link, frame: *Frame) ![]const u8 {
|
||||
if (href.len == 0) {
|
||||
return "";
|
||||
}
|
||||
return element.asNode().resolveURL(href, frame, .{});
|
||||
return element.asNode().resolveURLReflect(href, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setHref(self: *Link, value: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -236,7 +236,7 @@ pub fn getSrc(self: *const Media, frame: *Frame) ![]const u8 {
|
||||
if (src.len == 0) {
|
||||
return "";
|
||||
}
|
||||
return element.asConstNode().resolveURL(src, frame, .{});
|
||||
return element.asConstNode().resolveURLReflect(src, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setSrc(self: *Media, value: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn asNode(self: *Quote) *Node {
|
||||
pub fn getCite(self: *Quote, frame: *Frame) ![]const u8 {
|
||||
const attr = self.asElement().getAttributeSafe(comptime .wrap("cite")) orelse return "";
|
||||
if (attr.len == 0) return "";
|
||||
return self.asNode().resolveURL(attr, frame, .{});
|
||||
return self.asNode().resolveURLReflect(attr, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setCite(self: *Quote, value: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -46,7 +46,7 @@ pub fn asNode(self: *Script) *Node {
|
||||
|
||||
pub fn getSrc(self: *Script, frame: *Frame) ![]const u8 {
|
||||
if (self._src.len == 0) return "";
|
||||
return self.asNode().resolveURL(self._src, frame, .{});
|
||||
return self.asNode().resolveURLReflect(self._src, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setSrc(self: *Script, src: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -25,7 +25,7 @@ pub fn getSrc(self: *const Source, frame: *Frame) ![]const u8 {
|
||||
if (src.len == 0) {
|
||||
return "";
|
||||
}
|
||||
return element.asConstNode().resolveURL(src, frame, .{});
|
||||
return element.asConstNode().resolveURLReflect(src, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setSrc(self: *Source, value: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -57,7 +57,7 @@ pub fn getPoster(self: *const Video, frame: *Frame) ![]const u8 {
|
||||
if (poster.len == 0) {
|
||||
return "";
|
||||
}
|
||||
return element.asConstNode().resolveURL(poster, frame, .{});
|
||||
return element.asConstNode().resolveURLReflect(poster, frame, .{});
|
||||
}
|
||||
|
||||
pub fn setPoster(self: *Video, value: []const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -21,6 +21,7 @@ const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
|
||||
const Event = @import("../Event.zig");
|
||||
const MessagePort = @import("../MessagePort.zig");
|
||||
@@ -116,8 +117,9 @@ pub fn getOrigin(self: *const MessageEvent) []const u8 {
|
||||
return self._origin;
|
||||
}
|
||||
|
||||
pub fn getSource(self: *const MessageEvent) ?*Window {
|
||||
return self._source;
|
||||
pub fn getSource(self: *const MessageEvent, frame: *Frame) ?Window.Access {
|
||||
const source = self._source orelse return null;
|
||||
return Window.Access.init(frame.window, source);
|
||||
}
|
||||
|
||||
pub fn getPorts(self: *const MessageEvent) []const *MessagePort {
|
||||
|
||||
@@ -81,7 +81,7 @@ const Cache = enum {
|
||||
pub fn init(input: Input, opts_: ?InitOpts, exec: *const Execution) !*Request {
|
||||
const arena = exec.arena;
|
||||
const url = switch (input) {
|
||||
.url => |u| try URL.resolve(arena, exec.base(), u, .{ .always_dupe = true, .encoding = exec.charset.* }),
|
||||
.url => |u| try URL.resolve(arena, exec.base(), u, .{ .encoding = exec.charset.* }),
|
||||
.request => |r| try arena.dupeZ(u8, r._url),
|
||||
};
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ pub fn init(url: []const u8, protocols: [][]const u8, exec: *const Execution) !*
|
||||
const arena = try exec.getArena(.medium, "WebSocket");
|
||||
errdefer exec.releaseArena(arena);
|
||||
|
||||
const resolved_url = try URL.resolve(arena, exec.base(), url, .{ .always_dupe = true, .encoding = exec.charset.* });
|
||||
const resolved_url = try URL.resolve(arena, exec.base(), url, .{ .encoding = exec.charset.* });
|
||||
|
||||
const http_client = &exec.session.browser.http_client;
|
||||
const conn = http_client.network.newConnection() orelse {
|
||||
|
||||
@@ -52,6 +52,7 @@ _http_response: ?HttpClient.Response = null,
|
||||
// number of inflight requests, we can have multiple, e.g. xhr calling its own
|
||||
// send from the onload callback
|
||||
_active_requests: u8 = 0,
|
||||
_send_flag: bool = false,
|
||||
|
||||
_url: [:0]const u8 = "",
|
||||
_method: http.Method = .GET,
|
||||
@@ -185,6 +186,7 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void
|
||||
transfer.abort(error.Abort);
|
||||
self._http_response = null;
|
||||
}
|
||||
self._send_flag = false;
|
||||
|
||||
// Reset internal state. _override_mime intentionally survives open()
|
||||
// per https://xhr.spec.whatwg.org/#the-overridemimetype()-method.
|
||||
@@ -200,7 +202,7 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void
|
||||
|
||||
const exec = self._exec;
|
||||
self._method = try parseMethod(method_);
|
||||
self._url = try URL.resolve(self._arena, exec.base(), url, .{ .always_dupe = true, .encoding = exec.charset.* });
|
||||
self._url = try URL.resolve(self._arena, exec.base(), url, .{ .encoding = exec.charset.* });
|
||||
try self.stateChanged(.opened, exec);
|
||||
}
|
||||
|
||||
@@ -224,7 +226,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?BodyInit, exec_: *const Execution) !v
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.http, "XMLHttpRequest.send", .{ .url = self._url });
|
||||
}
|
||||
if (self._ready_state != .opened) {
|
||||
if (self._ready_state != .opened or self._send_flag) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
|
||||
@@ -259,6 +261,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?BodyInit, exec_: *const Execution) !v
|
||||
|
||||
self.acquireRef();
|
||||
self._active_requests += 1;
|
||||
self._send_flag = true;
|
||||
|
||||
exec.makeRequest(.{
|
||||
.ctx = self,
|
||||
@@ -282,6 +285,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?BodyInit, exec_: *const Execution) !v
|
||||
.body_outlives_request = true,
|
||||
}) catch |err| {
|
||||
self.releaseSelfRef();
|
||||
self._send_flag = false;
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ fn resolveQueryUrl(exec: *const Execution, _override: ?[]const u8) ![:0]const u8
|
||||
const current = exec.url.*;
|
||||
const override = _override orelse return current;
|
||||
|
||||
const resolved = try URL.resolve(exec.call_arena, exec.base(), override, .{ .always_dupe = true });
|
||||
const resolved = try URL.resolve(exec.call_arena, exec.base(), override, .{});
|
||||
if (!exec.isSameOrigin(resolved)) return error.SecurityError;
|
||||
|
||||
switch (exec.js.global) {
|
||||
|
||||
@@ -24,13 +24,11 @@ const CDP = @import("CDP.zig");
|
||||
|
||||
const App = @import("../App.zig");
|
||||
const Inbox = @import("../Inbox.zig");
|
||||
const Config = @import("../Config.zig");
|
||||
const WS = @import("../network/WS.zig");
|
||||
const ArenaPool = @import("../ArenaPool.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const posix = std.posix;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
pub const Connection = @This();
|
||||
|
||||
@@ -308,7 +308,7 @@ fn navigate(cmd: *CDP.Command) !void {
|
||||
const session = bc.session;
|
||||
const frame = bc.mainFrame() orelse return error.FrameNotLoaded;
|
||||
|
||||
const encoded_url = try URL.ensureEncoded(frame.call_arena, params.url, "UTF-8");
|
||||
const encoded_url = try URL.resolveNavigation(frame.call_arena, params.url, .{});
|
||||
|
||||
// Fast path: a freshly-created target whose root frame hasn't navigated
|
||||
// yet has nothing to preserve across the HTTP round-trip. Skip the
|
||||
@@ -585,10 +585,10 @@ pub fn frameChildFrameCreated(bc: *CDP.BrowserContext, event: *const Notificatio
|
||||
const cdp = bc.cdp;
|
||||
const frame_id = &id.toFrameId(event.frame_id);
|
||||
|
||||
try cdp.sendEvent("Page.frameAttached", .{ .params = .{
|
||||
try cdp.sendEvent("Page.frameAttached", .{
|
||||
.frameId = frame_id,
|
||||
.parentFrameId = &id.toFrameId(event.parent_id),
|
||||
} }, .{ .session_id = session_id });
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
if (bc.page_life_cycle_events) {
|
||||
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
|
||||
@@ -1055,6 +1055,25 @@ test "cdp.frame: getFrameTree" {
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.frame: frameAttached" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .session_id = "SID-X" });
|
||||
|
||||
bc.notification.dispatch(.frame_child_frame_created, &.{
|
||||
.frame_id = 2,
|
||||
.parent_id = 1,
|
||||
.loader_id = 7,
|
||||
.timestamp = 0,
|
||||
});
|
||||
|
||||
try ctx.expectSentEvent("Page.frameAttached", .{
|
||||
.frameId = "FID-0000000002",
|
||||
.parentFrameId = "FID-0000000001",
|
||||
}, .{ .session_id = "SID-X" });
|
||||
}
|
||||
|
||||
test "cdp.frame: captureScreenshot" {
|
||||
const LogFilter = @import("../../testing.zig").LogFilter;
|
||||
const filter: LogFilter = .init(&.{.not_implemented});
|
||||
@@ -1377,6 +1396,9 @@ test "cdp.frame: navigate does not follow Location on a non-redirect 3xx" {
|
||||
}
|
||||
|
||||
test "cdp.frame: navigate answers with errorText when the navigation fails" {
|
||||
const filter: testing.LogFilter = .init(&.{.frame});
|
||||
defer filter.deinit();
|
||||
|
||||
// A root navigation that fails before commit (here: connection refused —
|
||||
// nothing listens on port 1) must still answer the Page.navigate command.
|
||||
// Chrome resolves it with an errorText field ("present if and only if
|
||||
|
||||
@@ -222,7 +222,7 @@ fn createTarget(cmd: *CDP.Command) !void {
|
||||
}
|
||||
|
||||
if (!std.mem.eql(u8, "about:blank", params.url)) {
|
||||
const encoded_url = try URL.ensureEncoded(frame.call_arena, params.url, "UTF-8");
|
||||
const encoded_url = try URL.resolveNavigation(frame.call_arena, params.url, .{});
|
||||
try frame.navigate(
|
||||
encoded_url,
|
||||
.{ .reason = .address_bar, .kind = .{ .push = null } },
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
// host string becomes its punycode form, or an error.
|
||||
|
||||
use ::url::Url;
|
||||
use encoding_rs::{EncoderResult, Encoding};
|
||||
use std::borrow::Cow;
|
||||
use std::os::raw::c_uchar;
|
||||
use std::slice;
|
||||
|
||||
@@ -280,6 +282,11 @@ pub struct OwnedString {
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
const EMPTY_OWNED_STRING: OwnedString = OwnedString {
|
||||
ptr: std::ptr::null_mut(),
|
||||
len: 0,
|
||||
};
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn free_owned_string(owned: OwnedString) {
|
||||
if owned.ptr.is_null() || owned.len == 0 {
|
||||
@@ -328,17 +335,40 @@ pub unsafe extern "C" fn url_get_port(url: *const Url) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn clean_hostname_input(url: &Url, raw: &str) -> String {
|
||||
let special = matches!(
|
||||
url.scheme(),
|
||||
"http" | "https" | "ws" | "wss" | "ftp" | "file"
|
||||
);
|
||||
let mut out = String::with_capacity(raw.len());
|
||||
let mut in_brackets = false;
|
||||
for c in raw.chars() {
|
||||
match c {
|
||||
'\t' | '\n' | '\r' => continue,
|
||||
'[' => in_brackets = true,
|
||||
']' => in_brackets = false,
|
||||
'/' | '?' | '#' => break,
|
||||
'\\' if special => break,
|
||||
':' if !in_brackets => break,
|
||||
_ => {}
|
||||
}
|
||||
out.push(c);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// WHATWG `hostname` setter: sets the host without touching the port.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn url_set_hostname(url: *mut Url, ptr: *const c_uchar, len: usize) -> i32 {
|
||||
let url = unsafe { &mut *url };
|
||||
|
||||
let slice = match str_from(ptr, len) {
|
||||
let raw = match str_from(ptr, len) {
|
||||
Some(s) => s,
|
||||
None => return -1,
|
||||
};
|
||||
let host = clean_hostname_input(url, raw);
|
||||
|
||||
match url.set_host(Some(slice)) {
|
||||
match url.set_host(Some(host.as_str())) {
|
||||
Ok(()) => 0,
|
||||
Err(_) => -1,
|
||||
}
|
||||
@@ -399,6 +429,18 @@ pub unsafe extern "C" fn url_set_host(url: *mut Url, ptr: *const c_uchar, len: u
|
||||
None => return -1,
|
||||
};
|
||||
|
||||
// The WHATWG host setter parses in "host state", which stops at the first
|
||||
// '/', '?', '#' (and '\' for special schemes); the remainder is ignored
|
||||
// rather than making the whole value invalid.
|
||||
let special = matches!(
|
||||
url.scheme(),
|
||||
"http" | "https" | "ws" | "wss" | "ftp" | "file"
|
||||
);
|
||||
let end = input
|
||||
.find(|c| c == '/' || c == '?' || c == '#' || (special && c == '\\'))
|
||||
.unwrap_or(input.len());
|
||||
let input = &input[..end];
|
||||
|
||||
// Find the port separator ':', but only outside an IPv6 [...] literal.
|
||||
let colon = if input.starts_with('[') {
|
||||
input
|
||||
@@ -419,21 +461,28 @@ pub unsafe extern "C" fn url_set_host(url: *mut Url, ptr: *const c_uchar, len: u
|
||||
|
||||
let (host, port_str) = (&input[..i], &input[i + 1..]);
|
||||
|
||||
// A trailing colon with no digits ("host:") supplies an empty port, which
|
||||
// the WHATWG host setter ignores: set the host only, leaving any existing
|
||||
// port untouched (matches Chrome and Firefox).
|
||||
if port_str.is_empty() {
|
||||
return if url.set_host(Some(host)).is_ok() {
|
||||
0
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
}
|
||||
|
||||
// Validate the port up-front so we never apply the host and then fail.
|
||||
let new_port: Option<u16> = if port_str.is_empty() {
|
||||
None // "host:" clears the port
|
||||
} else {
|
||||
match port_str.parse::<u16>() {
|
||||
Ok(p) => Some(p),
|
||||
Err(_) => return -1,
|
||||
}
|
||||
let new_port: u16 = match port_str.parse::<u16>() {
|
||||
Ok(p) => p,
|
||||
Err(_) => return -1,
|
||||
};
|
||||
|
||||
if url.set_host(Some(host)).is_err() {
|
||||
return -1;
|
||||
}
|
||||
// set_port only errors on cannot-be-a-base, already ruled out by set_host.
|
||||
let _ = url.set_port(new_port);
|
||||
let _ = url.set_port(Some(new_port));
|
||||
0
|
||||
}
|
||||
|
||||
@@ -568,3 +617,168 @@ pub unsafe extern "C" fn url_get_href(
|
||||
*out_ptr = href.as_ptr();
|
||||
*out_len = href.len();
|
||||
}
|
||||
|
||||
fn encode_query_ncr(encoding: &'static Encoding, s: &str) -> Cow<'static, [u8]> {
|
||||
// fast path: fully mappable
|
||||
let (out, _, had_errors) = encoding.encode(s);
|
||||
if !had_errors {
|
||||
return Cow::Owned(out.into_owned());
|
||||
}
|
||||
|
||||
let mut encoder = encoding.new_encoder();
|
||||
let mut result = Vec::with_capacity(s.len() * 2);
|
||||
let mut input = s;
|
||||
loop {
|
||||
let needed = encoder
|
||||
.max_buffer_length_from_utf8_without_replacement(input.len())
|
||||
.unwrap();
|
||||
let start = result.len();
|
||||
result.resize(start + needed, 0);
|
||||
let (r, read, written) =
|
||||
encoder.encode_from_utf8_without_replacement(input, &mut result[start..], true);
|
||||
result.truncate(start + written);
|
||||
input = &input[read..];
|
||||
match r {
|
||||
EncoderResult::InputEmpty => break,
|
||||
EncoderResult::Unmappable(c) => {
|
||||
result.extend_from_slice(format!("%26%23{}%3B", c as u32).as_bytes());
|
||||
}
|
||||
// Output was sized with max_buffer_length, so it cannot run out.
|
||||
EncoderResult::OutputFull => unreachable!(),
|
||||
}
|
||||
}
|
||||
Cow::Owned(result)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn url_resolve_with_encoding(
|
||||
base_ptr: *const c_uchar,
|
||||
base_len: usize,
|
||||
input_ptr: *const c_uchar,
|
||||
input_len: usize,
|
||||
enc_ptr: *const c_uchar,
|
||||
enc_len: usize,
|
||||
err: *mut i32,
|
||||
) -> OwnedString {
|
||||
let base_slice = match str_from(base_ptr, base_len) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
*err = -1;
|
||||
return EMPTY_OWNED_STRING;
|
||||
}
|
||||
};
|
||||
// An empty base means the input must be an absolute URL.
|
||||
let base = if base_slice.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match Url::parse(base_slice) {
|
||||
Ok(u) => Some(u),
|
||||
Err(_) => {
|
||||
*err = -1;
|
||||
return EMPTY_OWNED_STRING;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let slice = match str_from(input_ptr, input_len) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
*err = -1;
|
||||
return EMPTY_OWNED_STRING;
|
||||
}
|
||||
};
|
||||
|
||||
let encoding_slice = match str_from(enc_ptr, enc_len) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
*err = -1;
|
||||
return EMPTY_OWNED_STRING;
|
||||
}
|
||||
};
|
||||
// Per the URL spec, queries use the document encoding's *output encoding*.
|
||||
let encoding = Encoding::for_label(encoding_slice.as_bytes())
|
||||
.map(|encoding| encoding.output_encoding())
|
||||
.filter(|&encoding| encoding != encoding_rs::UTF_8);
|
||||
|
||||
let result = match encoding {
|
||||
Some(encoding) => Url::options()
|
||||
.base_url(base.as_ref())
|
||||
.encoding_override(Some(&move |s| encode_query_ncr(encoding, s)))
|
||||
.parse(slice),
|
||||
// Fallback to default.
|
||||
None => match &base {
|
||||
Some(base) => base.join(slice),
|
||||
None => Url::parse(slice),
|
||||
},
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(url) => {
|
||||
*err = 0;
|
||||
let s = String::from(url); // Moves the serialization, no copy.
|
||||
let len = s.len();
|
||||
let ptr = Box::into_raw(s.into_bytes().into_boxed_slice()) as *mut c_uchar;
|
||||
OwnedString { ptr, len }
|
||||
}
|
||||
Err(_) => {
|
||||
*err = -1;
|
||||
EMPTY_OWNED_STRING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Similar to url_parse_with_base; returns a href instead.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn url_resolve_without_encoding(
|
||||
base_ptr: *const c_uchar,
|
||||
base_len: usize,
|
||||
input_ptr: *const c_uchar,
|
||||
input_len: usize,
|
||||
err: *mut i32,
|
||||
) -> OwnedString {
|
||||
let base_slice = match str_from(base_ptr, base_len) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
*err = -1;
|
||||
return EMPTY_OWNED_STRING;
|
||||
}
|
||||
};
|
||||
// An empty base means the input must be an absolute URL.
|
||||
let base = if base_slice.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match Url::parse(base_slice) {
|
||||
Ok(u) => Some(u),
|
||||
Err(_) => {
|
||||
*err = -1;
|
||||
return EMPTY_OWNED_STRING;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let input = match str_from(input_ptr, input_len) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
*err = -1;
|
||||
return EMPTY_OWNED_STRING;
|
||||
}
|
||||
};
|
||||
|
||||
let result = match &base {
|
||||
Some(base) => base.join(input),
|
||||
None => Url::parse(input),
|
||||
};
|
||||
match result {
|
||||
Ok(url) => {
|
||||
*err = 0;
|
||||
let s = String::from(url); // Moves the serialization, no copy.
|
||||
let len = s.len();
|
||||
let ptr = Box::into_raw(s.into_bytes().into_boxed_slice()) as *mut c_uchar;
|
||||
OwnedString { ptr, len }
|
||||
}
|
||||
Err(_) => {
|
||||
*err = -1;
|
||||
EMPTY_OWNED_STRING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +55,6 @@ pub const cookies = @import("cookies.zig");
|
||||
pub const build_config = @import("build_config");
|
||||
pub const crash_handler = @import("crash_handler.zig");
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
pub const FetchOpts = struct {
|
||||
wait_ms: u32 = 5000,
|
||||
wait_until: ?Config.WaitUntil = null,
|
||||
@@ -106,7 +104,7 @@ pub fn fetch(app: *App, browser: *Browser, urls: []const [:0]const u8, opts: Fet
|
||||
const page = try session.createPage();
|
||||
const frame = page.frame().?;
|
||||
// not guaranteed to be valid after navigate
|
||||
const encoded_url = try URL.ensureEncoded(frame.call_arena, url, "UTF-8");
|
||||
const encoded_url = try URL.resolveNavigation(frame.call_arena, url, .{});
|
||||
_ = try frame.navigate(encoded_url, .{
|
||||
.reason = .address_bar,
|
||||
.kind = .{ .push = null },
|
||||
|
||||
@@ -20,7 +20,6 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const Layer = @import("../../browser/HttpClient.zig").Layer;
|
||||
const Request = @import("../../browser/HttpClient.zig").Request;
|
||||
const Transfer = @import("../../browser/HttpClient.zig").Transfer;
|
||||
const Response = @import("../../browser/HttpClient.zig").Response;
|
||||
|
||||
|
||||
@@ -20,10 +20,8 @@ const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
const log = lp.log;
|
||||
|
||||
const Client = @import("../../browser/HttpClient.zig").Client;
|
||||
const Network = @import("../Network.zig");
|
||||
const Transfer = @import("../../browser/HttpClient.zig").Transfer;
|
||||
const Request = @import("../../browser/HttpClient.zig").Request;
|
||||
const Response = @import("../../browser/HttpClient.zig").Response;
|
||||
const Layer = @import("../../browser/HttpClient.zig").Layer;
|
||||
const StableResponse = @import("../../browser/HttpClient.zig").StableResponse;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const lp = @import("lightpanda");
|
||||
const log = lp.log;
|
||||
@@ -23,6 +24,7 @@ const log = lp.log;
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
const http = @import("../http.zig");
|
||||
const HttpClient = @import("../../browser/HttpClient.zig");
|
||||
const Request = @import("../../browser/HttpClient.zig").Request;
|
||||
const Transfer = @import("../../browser/HttpClient.zig").Transfer;
|
||||
const Response = @import("../../browser/HttpClient.zig").Response;
|
||||
@@ -232,6 +234,26 @@ pub fn fulfillRequest(
|
||||
// Leave the parked state (accounting `intercepted` exactly once) and move to
|
||||
// .completing BEFORE running the user callbacks.
|
||||
transfer.unpark();
|
||||
|
||||
if (HttpClient.isRedirectStatus(status)) {
|
||||
if (findLocation(headers)) |location| {
|
||||
fulfilledRedirect(transfer, status, headers, location) catch |err| {
|
||||
if (transfer.state == .created) {
|
||||
transfer.abort(err);
|
||||
}
|
||||
return err;
|
||||
};
|
||||
self.next.request(transfer) catch |err| {
|
||||
if (transfer.state == .created) {
|
||||
transfer.abort(err);
|
||||
}
|
||||
return err;
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Not a redirect: move to .completing BEFORE running the user callbacks.
|
||||
transfer.state = .completing;
|
||||
defer transfer.deinit();
|
||||
|
||||
@@ -281,3 +303,25 @@ fn fulfillInner(
|
||||
done.* = true;
|
||||
try req.done_callback(req.ctx);
|
||||
}
|
||||
|
||||
fn fulfilledRedirect(transfer: *Transfer, status: u16, headers: []const http.Header, location: []const u8) !void {
|
||||
// retrieve cookies from the fulfilled response's headers.
|
||||
if (transfer.req.cookie_jar) |jar| {
|
||||
for (headers) |hdr| {
|
||||
if (std.ascii.eqlIgnoreCase(hdr.name, "set-cookie")) {
|
||||
try jar.populateFromResponse(transfer.req.url, hdr.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try transfer.applyRedirectTarget(transfer.req.url, location, status);
|
||||
}
|
||||
|
||||
fn findLocation(headers: []const http.Header) ?[]const u8 {
|
||||
for (headers) |hdr| {
|
||||
if (std.ascii.eqlIgnoreCase(hdr.name, "location")) {
|
||||
return hdr.value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
//! Bindings for Servo's rust-url (https://github.com/servo/rust-url).
|
||||
//! Check @src/html5ever/url.rs for Rust-side of the bindings.
|
||||
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
|
||||
pub const Url = anyopaque;
|
||||
|
||||
pub const OwnedString = extern struct {
|
||||
@@ -83,6 +80,12 @@ pub extern "c" fn url_get_fragment(url: *const Url, out_ptr: *[*]const u8, out_l
|
||||
pub extern "c" fn url_set_query(url: *Url, ptr: [*]const u8, len: usize) i32;
|
||||
pub extern "c" fn url_set_query_to_null(url: *Url) void;
|
||||
pub extern "c" fn url_get_query(url: *const Url, out_ptr: *[*]const u8, out_len: *usize) i32;
|
||||
/// `err` is `0` if there's no error.
|
||||
/// Returned `OwnedString` doesn't have a sentinel; callers must be aware of that.
|
||||
pub extern "c" fn url_resolve_with_encoding(base_ptr: [*]const u8, base_len: usize, input_ptr: [*]const u8, input_len: usize, enc_ptr: [*]const u8, enc_len: usize, err: *i32) OwnedString;
|
||||
/// `err` is `0` if there's no error.
|
||||
/// Returned `OwnedString` doesn't have a sentinel; callers must be aware of that.
|
||||
pub extern "c" fn url_resolve_without_encoding(base_ptr: [*]const u8, base_len: usize, input_ptr: [*]const u8, input_len: usize, err: *i32) OwnedString;
|
||||
|
||||
extern "c" fn url_get_port(url: *const Url) i32;
|
||||
pub inline fn urlGetPort(url: *const Url) ?u16 {
|
||||
|
||||
Reference in New Issue
Block a user