25 Commits

Author SHA1 Message Date
Halil Durak
fa00a5da52 fix link element test
Changes are made regarding to `host`, `port` and `hostname`. Definitions are provided by MDN.
2025-10-20 12:43:36 +03:00
Halil Durak
344420f708 bring back hostname getter/setter functions
This was a regression while testing things.
2025-10-20 12:41:47 +03:00
Halil Durak
b87a59fa49 href should have / if path not provided 2025-10-20 12:41:08 +03:00
Halil Durak
535a21c9f2 change the way ada is linked to the build system
Link the ada library to ada module rather than building alongside main module.
2025-10-18 16:19:59 +03:00
Halil Durak
51a328d357 don't link libcpp twice
This was causing an issue on ld linker but not on MachO.
2025-10-18 13:20:24 +03:00
Halil Durak
146b56c8c0 refactor HTMLAnchorElement
Prefer new URL implementation with separate store for object data.
2025-10-17 19:39:10 +03:00
Halil Durak
8e7d8225ba prefer getHref instead of raw
Now that we allocate for URLs, we know that lifetime of `href` is same as URL itself; so we don't need to keep a separate `raw` string.

Only difference is `href` is normalized whereas `raw` is not. Most things `raw` being used for require normalized URLs though, so such a change is fine.
2025-10-17 16:02:32 +03:00
Halil Durak
a46218cbae change in page url's init/deinit logic
this must be done in runtime now sadly, good thing is it doesn't add much and `getHref` can be spread everywhere without pointer life concerns
2025-10-17 15:54:29 +03:00
Halil Durak
e9755bd38b remove early free
yet another thing we should figure out; IMO cookie can have ownership to its url, would make it a lot simpler to use & deinitialize
2025-10-17 15:50:42 +03:00
Halil Durak
6820a00cd0 revert element test 2025-10-17 15:49:17 +03:00
Halil Durak
4c4feef9fc add nullable ada url functions
not fully sure how we should implement those; I believe we should move forward with nullable functions and put null-check logic outside of the wrappers
2025-10-17 15:48:28 +03:00
Halil Durak
c930d942fe invalidUrl test is no longer necessary 2025-10-17 15:46:33 +03:00
Halil Durak
c371538d27 rebase onto main 2025-10-14 16:43:18 +03:00
Halil Durak
7629bf274a various changes in ada-url module 2025-10-14 15:47:43 +03:00
Halil Durak
9e7e9b67ff add getHostname 2025-10-14 15:47:43 +03:00
Halil Durak
9b3be14650 prefer hostname instead of host in forRequest 2025-10-14 15:47:43 +03:00
Halil Durak
6af8add7ff fix cookie path parsing 2025-10-14 15:47:43 +03:00
Halil Durak
cf9ecbd9fd prefer URL instead of std.Uri everywhere 2025-10-14 15:47:42 +03:00
Halil Durak
cecdd47bbc bind more ada functions
also includes a fix for releasing memory if parsing failed
2025-10-14 15:42:14 +03:00
Halil Durak
900c8d2473 change after rebase 2025-10-14 15:42:14 +03:00
Halil Durak
c1160543ad basic url parsing working
* also reintroduces old `URLSearchParams` implementation since ada prefers its own iterator where we'd like to use our own.
2025-10-14 15:42:14 +03:00
Halil Durak
1c7971b096 bind more ada-url functions 2025-10-14 15:39:41 +03:00
nikneym
45cd494298 initial URL refactor 2025-10-14 15:39:41 +03:00
nikneym
8f99e36cde add ada-url wrappers
* also integrate it as module in build.zig rather than direct linking
2025-10-14 14:23:48 +03:00
nikneym
25a1d588a9 integrate ada-url dependency to build system 2025-10-14 14:23:48 +03:00
25 changed files with 1132 additions and 620 deletions

View File

@@ -384,6 +384,7 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo
try buildMbedtls(b, mod); try buildMbedtls(b, mod);
try buildNghttp2(b, mod); try buildNghttp2(b, mod);
try buildCurl(b, mod); try buildCurl(b, mod);
try buildAda(b, mod);
switch (target.result.os.tag) { switch (target.result.os.tag) {
.macos => { .macos => {
@@ -849,3 +850,34 @@ fn buildCurl(b: *Build, m: *Build.Module) !void {
}, },
}); });
} }
pub fn buildAda(b: *Build, m: *Build.Module) !void {
const ada_dep = b.dependency("ada-singleheader", .{});
const ada_mod = b.createModule(.{
.root_source_file = b.path("vendor/ada/root.zig"),
});
const ada_lib = b.addLibrary(.{
.name = "ada",
.root_module = b.createModule(.{
.link_libcpp = true,
.target = m.resolved_target,
.optimize = m.optimize,
}),
.linkage = .static,
});
ada_lib.addCSourceFile(.{
.file = ada_dep.path("ada.cpp"),
.flags = &.{ "-std=c++20", "-O3" },
.language = .cpp,
});
ada_lib.installHeader(ada_dep.path("ada_c.h"), "ada_c.h");
// Link the library to ada module.
ada_mod.linkLibrary(ada_lib);
// Expose ada module to main module.
m.addImport("ada", ada_mod);
}

View File

@@ -9,5 +9,9 @@
.hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5", .hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5",
}, },
//.v8 = .{ .path = "../zig-v8-fork" } //.v8 = .{ .path = "../zig-v8-fork" }
.@"ada-singleheader" = .{
.url = "https://github.com/ada-url/ada/releases/download/v3.3.0/singleheader.zip",
.hash = "N-V-__8AAPmhFAAw64ALjlzd5YMtzpSrmZ6KymsT84BKfB4s",
},
}, },
} }

View File

@@ -192,7 +192,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
if (try DataURI.parse(page.arena, src)) |data_uri| { if (try DataURI.parse(page.arena, src)) |data_uri| {
source = .{ .@"inline" = data_uri }; source = .{ .@"inline" = data_uri };
} else { } else {
remote_url = try URL.stitch(page.arena, src, page.url.raw, .{ .null_terminated = true }); remote_url = try URL.stitch(page.arena, src, page.url.getHref(), .{ .null_terminated = true });
source = .{ .remote = .{} }; source = .{ .remote = .{} };
} }
} else { } else {
@@ -204,7 +204,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c
.kind = kind, .kind = kind,
.element = element, .element = element,
.source = source, .source = source,
.url = remote_url orelse page.url.raw, .url = remote_url orelse page.url.getHref(),
.is_defer = if (remote_url == null) false else try parser.elementGetAttribute(element, "defer") != null, .is_defer = if (remote_url == null) false else try parser.elementGetAttribute(element, "defer") != null,
.is_async = if (remote_url == null) false else try parser.elementGetAttribute(element, "async") != null, .is_async = if (remote_url == null) false else try parser.elementGetAttribute(element, "async") != null,
}; };
@@ -503,7 +503,7 @@ fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
const resolved_url = try URL.stitch( const resolved_url = try URL.stitch(
self.page.arena, self.page.arena,
entry.value_ptr.*, entry.value_ptr.*,
self.page.url.raw, self.page.url.getHref(),
.{ .alloc = .if_needed, .null_terminated = true }, .{ .alloc = .if_needed, .null_terminated = true },
); );

View File

@@ -119,8 +119,8 @@ pub const Node = struct {
// -------- // --------
// Read-only attributes // Read-only attributes
pub fn get_baseURI(_: *parser.Node, page: *Page) ![]const u8 { pub fn get_baseURI(_: *parser.Node, page: *Page) []const u8 {
return page.url.raw; return page.url.getHref();
} }
pub fn get_firstChild(self: *parser.Node) !?Union { pub fn get_firstChild(self: *parser.Node) !?Union {

View File

@@ -130,7 +130,7 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re
const url: [:0]const u8 = blk: switch (input) { const url: [:0]const u8 = blk: switch (input) {
.string => |str| { .string => |str| {
break :blk try URL.stitch(arena, str, page.url.raw, .{ .null_terminated = true }); break :blk try URL.stitch(arena, str, page.url.getHref(), .{ .null_terminated = true });
}, },
.request => |req| { .request => |req| {
break :blk try arena.dupeZ(u8, req.url); break :blk try arena.dupeZ(u8, req.url);

View File

@@ -113,26 +113,26 @@ fn _dispatchPopStateEvent(state: ?[]const u8, page: *Page) !void {
); );
} }
pub fn _pushState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void { pub fn _pushState(self: *History, state: js.Object, _: ?[]const u8, maybe_url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena; const arena = page.session.arena;
const json = try state.toJson(arena); const json = try state.toJson(arena);
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw); const url = if (maybe_url) |u| try arena.dupe(u8, u) else page.url.getHref();
const entry = HistoryEntry{ .state = json, .url = url }; const entry = HistoryEntry{ .state = json, .url = url };
try self.stack.append(arena, entry); try self.stack.append(arena, entry);
self.current = self.stack.items.len - 1; self.current = self.stack.items.len - 1;
} }
pub fn _replaceState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void { pub fn _replaceState(self: *History, state: js.Object, _: ?[]const u8, maybe_url: ?[]const u8, page: *Page) !void {
const arena = page.session.arena; const arena = page.session.arena;
if (self.current) |curr| { if (self.current) |curr| {
const entry = &self.stack.items[curr]; const entry = &self.stack.items[curr];
const json = try state.toJson(arena); const json = try state.toJson(arena);
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw); const url = if (maybe_url) |u| try arena.dupe(u8, u) else page.url.getHref();
entry.* = HistoryEntry{ .state = json, .url = url }; entry.* = HistoryEntry{ .state = json, .url = url };
} else { } else {
try self._pushState(state, "", _url, page); try self._pushState(state, "", maybe_url, page);
} }
} }

View File

@@ -42,12 +42,12 @@ pub const HTMLDocument = struct {
// JS funcs // JS funcs
// -------- // --------
pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 { pub fn get_domain(self: *parser.DocumentHTML) ![]const u8 {
// libdom's document_html get_domain always returns null, this is // libdom's document_html get_domain always returns null, this is
// the way MDN recommends getting the domain anyways, since document.domain // the way MDN recommends getting the domain anyways, since document.domain
// is deprecated. // is deprecated.
const location = try parser.documentHTMLGetLocation(Location, self) orelse return ""; const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
return location.get_host(page); return location.get_host();
} }
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 { pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
@@ -85,7 +85,7 @@ pub const HTMLDocument = struct {
pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 { pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 {
var buf: std.ArrayListUnmanaged(u8) = .{}; var buf: std.ArrayListUnmanaged(u8) = .{};
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ try page.cookie_jar.forRequest(page.url, buf.writer(page.arena), .{
.is_http = false, .is_http = false,
.is_navigation = true, .is_navigation = true,
}); });
@@ -95,7 +95,7 @@ pub const HTMLDocument = struct {
pub fn set_cookie(_: *parser.DocumentHTML, cookie_str: []const u8, page: *Page) ![]const u8 { pub fn set_cookie(_: *parser.DocumentHTML, cookie_str: []const u8, page: *Page) ![]const u8 {
// we use the cookie jar's allocator to parse the cookie because it // we use the cookie jar's allocator to parse the cookie because it
// outlives the page's arena. // outlives the page's arena.
const c = try Cookie.parse(page.cookie_jar.allocator, &page.url.uri, cookie_str); const c = try Cookie.parse(page.cookie_jar.allocator, page.url, cookie_str);
errdefer c.deinit(); errdefer c.deinit();
if (c.http_only) { if (c.http_only) {
c.deinit(); c.deinit();

View File

@@ -218,12 +218,32 @@ pub const HTMLAnchorElement = struct {
} }
pub fn get_href(self: *parser.Anchor) ![]const u8 { pub fn get_href(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetHref(self); return parser.anchorGetHref(self);
} }
pub fn set_href(self: *parser.Anchor, href: []const u8, page: *const Page) !void { pub fn set_href(self: *parser.Anchor, href: []const u8, page: *Page) !void {
const full = try urlStitch(page.call_arena, href, page.url.raw, .{}); const full = try urlStitch(page.call_arena, href, page.url.getHref(), .{});
return try parser.anchorSetHref(self, full);
// Get the stored internal URL if we had one.
if (page.getObjectData(self)) |internal_url| {
const u = NativeURL.fromInternal(internal_url);
// Reparse with the new href.
_ = try u.reparse(full);
errdefer u.deinit();
// TODO: Remove the entry from the map on an error situation.
return parser.anchorSetHref(self, u.getHref());
}
// We don't have internal URL stored in object_data yet.
// Create one for this anchor element.
const u = try NativeURL.parse(full, null);
errdefer u.deinit();
// Save to map.
try page.putObjectData(self, u.internal.?);
return parser.anchorSetHref(self, u.getHref());
} }
pub fn get_hreflang(self: *parser.Anchor) ![]const u8 { pub fn get_hreflang(self: *parser.Anchor) ![]const u8 {
@@ -258,193 +278,187 @@ pub const HTMLAnchorElement = struct {
return try parser.nodeSetTextContent(parser.anchorToNode(self), v); return try parser.nodeSetTextContent(parser.anchorToNode(self), v);
} }
fn url(self: *parser.Anchor, page: *Page) !URL { fn getHref(self: *parser.Anchor) !?[]const u8 {
// Although the URL.constructor union accepts an .{.element = X}, we return parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href");
// can't use this here because the behavior is different.
// URL.constructor(document.createElement('a')
// should fail (a.href isn't a valid URL)
// But
// document.createElement('a').host
// should not fail, it should return an empty string
if (try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href")) |href| {
return URL.constructor(.{ .string = href }, null, page); // TODO inject base url
} }
return .empty;
/// Returns the URL associated with given anchor element.
/// Creates a new URL object if not created before.
fn getURL(self: *parser.Anchor, page: *Page) !NativeURL {
if (page.getObjectData(self)) |internal_url| {
return NativeURL.fromInternal(internal_url);
}
// Try to get href string.
const maybe_anchor_href = try getHref(self);
if (maybe_anchor_href) |anchor_href| {
// Allocate a URL for this anchor element.
const u = try NativeURL.parse(anchor_href, null);
// Save in map.
try page.putObjectData(self, u.internal.?);
return u;
}
// No anchor href string found; let's just return an error.
return error.HrefAttributeNotGiven;
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = getURL(self, page) catch return "";
return try u.get_origin(page); // Though we store the URL in object data map, we still have to allocate
// for origin string sadly.
return u.getOrigin(page.arena);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = getURL(self, page) catch return "";
return u.get_protocol(); return u.getProtocol();
} }
pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void {
const arena = page.arena; const u = try getURL(self, page);
var u = try url(self, page); try u.setProtocol(v);
return parser.anchorSetHref(self, u.getHref());
u.uri.scheme = v;
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string const NativeURL = @import("../../url.zig").URL;
// TODO: Return a disposable string.
pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = getURL(self, page) catch return "";
return try u.get_host(page); return u.host();
} }
pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_host(self: *parser.Anchor, host_str: []const u8, page: *Page) !void {
// search : separator const u = blk: {
var p: ?u16 = null; if (page.getObjectData(self)) |internal_url| {
var h: []const u8 = undefined; break :blk NativeURL.fromInternal(internal_url);
for (v, 0..) |c, i| {
if (c == ':') {
h = v[0..i];
p = try std.fmt.parseInt(u16, v[i + 1 ..], 10);
break;
}
} }
const arena = page.arena; const maybe_anchor_href = try getHref(self);
var u = try url(self, page); if (maybe_anchor_href) |anchor_href| {
const new_u = try NativeURL.parse(anchor_href, null);
if (p) |pp| { try page.putObjectData(self, new_u.internal.?);
u.uri.host = .{ .raw = h }; break :blk new_u;
u.uri.port = pp;
} else {
u.uri.host = .{ .raw = v };
u.uri.port = null;
} }
const href = try u.toString(arena); // Last resort; try to create URL object out of host_str.
try parser.anchorSetHref(self, href); const new_u = try NativeURL.parse(host_str, null);
// We can just return here since host is updated.
return page.putObjectData(self, new_u.internal.?);
};
try u.setHost(host_str);
return parser.anchorSetHref(self, u.getHref());
} }
pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_hostname(self: *parser.Anchor, page: *Page) []const u8 {
var u = try url(self, page); const u = getURL(self, page) catch return "";
return u.get_hostname(); return u.getHostname();
} }
pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void {
const arena = page.arena; const u = blk: {
var u = try url(self, page); if (page.getObjectData(self)) |internal_url| {
u.uri.host = .{ .raw = v }; break :blk NativeURL.fromInternal(internal_url);
const href = try u.toString(arena); }
try parser.anchorSetHref(self, href);
const maybe_anchor_href = try getHref(self);
if (maybe_anchor_href) |anchor_href| {
const new_u = try NativeURL.parse(anchor_href, null);
try page.putObjectData(self, new_u.internal.?);
break :blk new_u;
}
// Last resort; try to create URL object out of hostname.
const new_u = try NativeURL.parse(hostname, null);
// We can just return here since hostname is updated.
return page.putObjectData(self, new_u.internal.?);
};
try u.setHostname(hostname);
return parser.anchorSetHref(self, u.getHref());
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = getURL(self, page) catch return "";
return try u.get_port(page); return u.getPort();
} }
pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void {
const arena = page.arena; // TODO: Check for valid port (u16 integer).
var u = try url(self, page); if (maybe_port) |port| {
const u = try getURL(self, page);
try u.setPort(port);
if (v != null and v.?.len > 0) { return parser.anchorSetHref(self, u.getHref());
u.uri.port = try std.fmt.parseInt(u16, v.?, 10);
} else {
u.uri.port = null;
} }
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = try getURL(self, page);
return u.get_username(); return u.getUsername() orelse "";
} }
pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void {
const arena = page.arena; if (maybe_username) |username| {
var u = try url(self, page); const u = try getURL(self, page);
try u.setUsername(username);
if (v) |vv| { try parser.anchorSetHref(self, u.getHref());
u.uri.user = .{ .raw = vv };
} else {
u.uri.user = null;
} }
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string
pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = try getURL(self, page);
return try page.arena.dupe(u8, u.get_password()); return u.getPassword() orelse "";
} }
pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void {
const arena = page.arena; if (maybe_password) |password| {
var u = try url(self, page); const u = try getURL(self, page);
try u.setPassword(password);
if (v) |vv| { try parser.anchorSetHref(self, u.getHref());
u.uri.password = .{ .raw = vv };
} else {
u.uri.password = null;
} }
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = try getURL(self, page);
return u.get_pathname(); return u.getPath();
} }
pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void { pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void {
const arena = page.arena; const u = try getURL(self, page);
var u = try url(self, page); try u.setPath(pathname);
u.uri.path = .{ .raw = v }; return parser.anchorSetHref(self, u.getHref());
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
} }
pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = try getURL(self, page);
return try u.get_search(page); return u.getSearch() orelse "";
} }
pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_search(self: *parser.Anchor, search: []const u8, page: *Page) !void {
var u = try url(self, page); const u = try getURL(self, page);
try u.set_search(v, page); u.setSearch(search);
return parser.anchorSetHref(self, u.getHref());
const href = try u.toString(page.call_arena);
try parser.anchorSetHref(self, href);
} }
// TODO return a disposable string // TODO return a disposable string
pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 { pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 {
var u = try url(self, page); const u = try getURL(self, page);
return try u.get_hash(page); return u.getHash() orelse "";
} }
pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { pub fn set_hash(self: *parser.Anchor, hash: []const u8, page: *Page) !void {
const arena = page.arena; const u = try getURL(self, page);
var u = try url(self, page); u.setHash(hash);
return parser.anchorSetHref(self, u.getHref());
if (v) |vv| {
u.uri.fragment = .{ .raw = vv };
} else {
u.uri.fragment = null;
}
const href = try u.toString(arena);
try parser.anchorSetHref(self, href);
} }
}; };
@@ -717,7 +731,7 @@ pub const HTMLInputElement = struct {
return try parser.inputGetSrc(self); return try parser.inputGetSrc(self);
} }
pub fn set_src(self: *parser.Input, src: []const u8, page: *Page) !void { pub fn set_src(self: *parser.Input, src: []const u8, page: *Page) !void {
const new_src = try urlStitch(page.call_arena, src, page.url.raw, .{ .alloc = .if_needed }); const new_src = try urlStitch(page.call_arena, src, page.url.getHref(), .{ .alloc = .if_needed });
try parser.inputSetSrc(self, new_src); try parser.inputSetSrc(self, new_src);
} }
pub fn get_type(self: *parser.Input) ![]const u8 { pub fn get_type(self: *parser.Input) ![]const u8 {
@@ -770,7 +784,7 @@ pub const HTMLLinkElement = struct {
} }
pub fn set_href(self: *parser.Link, href: []const u8, page: *const Page) !void { pub fn set_href(self: *parser.Link, href: []const u8, page: *const Page) !void {
const full = try urlStitch(page.call_arena, href, page.url.raw, .{}); const full = try urlStitch(page.call_arena, href, page.url.getHref(), .{});
return parser.linkSetHref(self, full); return parser.linkSetHref(self, full);
} }
}; };

View File

@@ -29,10 +29,6 @@ pub const Location = struct {
/// Chrome -> chrome://new-tab-page/ /// Chrome -> chrome://new-tab-page/
/// Firefox -> about:newtab /// Firefox -> about:newtab
/// Safari -> favorites:// /// Safari -> favorites://
pub const default = Location{
.url = .initWithoutSearchParams(Uri.parse("about:blank") catch unreachable),
};
pub fn get_href(self: *Location, page: *Page) ![]const u8 { pub fn get_href(self: *Location, page: *Page) ![]const u8 {
return self.url.get_href(page); return self.url.get_href(page);
} }
@@ -45,16 +41,16 @@ pub const Location = struct {
return self.url.get_protocol(); return self.url.get_protocol();
} }
pub fn get_host(self: *Location, page: *Page) ![]const u8 { pub fn get_host(self: *Location) []const u8 {
return self.url.get_host(page); return self.url.get_host();
} }
pub fn get_hostname(self: *Location) []const u8 { pub fn get_hostname(self: *Location) []const u8 {
return self.url.get_hostname(); return self.url.get_hostname();
} }
pub fn get_port(self: *Location, page: *Page) ![]const u8 { pub fn get_port(self: *Location) []const u8 {
return self.url.get_port(page); return self.url.get_port();
} }
pub fn get_pathname(self: *Location) []const u8 { pub fn get_pathname(self: *Location) []const u8 {
@@ -65,8 +61,8 @@ pub const Location = struct {
return self.url.get_search(page); return self.url.get_search(page);
} }
pub fn get_hash(self: *Location, page: *Page) ![]const u8 { pub fn get_hash(self: *Location) []const u8 {
return self.url.get_hash(page); return self.url.get_hash();
} }
pub fn get_origin(self: *Location, page: *Page) ![]const u8 { pub fn get_origin(self: *Location, page: *Page) ![]const u8 {
@@ -82,11 +78,11 @@ pub const Location = struct {
} }
pub fn _reload(_: *const Location, page: *Page) !void { pub fn _reload(_: *const Location, page: *Page) !void {
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script }); return page.navigateFromWebAPI(page.url.getHref(), .{ .reason = .script });
} }
pub fn _toString(self: *Location, page: *Page) ![]const u8 { pub fn _toString(self: *Location, page: *Page) ![]const u8 {
return try self.get_href(page); return self.get_href(page);
} }
}; };

View File

@@ -36,6 +36,8 @@ const Screen = @import("screen.zig").Screen;
const domcss = @import("../dom/css.zig"); const domcss = @import("../dom/css.zig");
const Css = @import("../css/css.zig").Css; const Css = @import("../css/css.zig").Css;
const EventHandler = @import("../events/event.zig").EventHandler; const EventHandler = @import("../events/event.zig").EventHandler;
const URL = @import("../../url.zig").URL;
const WebApiURL = @import("../url/url.zig").URL;
const Request = @import("../fetch/Request.zig"); const Request = @import("../fetch/Request.zig");
const fetchFn = @import("../fetch/fetch.zig").fetch; const fetchFn = @import("../fetch/fetch.zig").fetch;
@@ -52,7 +54,7 @@ pub const Window = struct {
document: *parser.DocumentHTML, document: *parser.DocumentHTML,
target: []const u8 = "", target: []const u8 = "",
location: Location = .default, location: Location,
storage_shelf: ?*storage.Shelf = null, storage_shelf: ?*storage.Shelf = null,
// counter for having unique timer ids // counter for having unique timer ids
@@ -75,17 +77,30 @@ pub const Window = struct {
const doc = parser.documentHTMLToDocument(html_doc); const doc = parser.documentHTMLToDocument(html_doc);
try parser.documentSetDocumentURI(doc, "about:blank"); try parser.documentSetDocumentURI(doc, "about:blank");
const native_url = URL.parse("about:blank", null) catch unreachable;
// Here we manually initialize; this is a special case and
// one should prefer constructor functions instead.
const url = WebApiURL{
.internal = native_url.internal,
.search_params = .{},
};
return .{ return .{
.document = html_doc, .document = html_doc,
.location = .{ .url = url },
.target = target orelse "", .target = target orelse "",
.navigator = navigator orelse .{}, .navigator = navigator orelse .{},
.performance = Performance.init(), .performance = Performance.init(),
}; };
} }
pub fn replaceLocation(self: *Window, loc: Location) !void { pub fn replaceLocation(self: *Window, location: Location) !void {
self.location = loc; // Remove current.
try parser.documentHTMLSetLocation(Location, self.document, &self.location); self.location.url.destructor();
// Put the new one.
self.location = location;
return parser.documentHTMLSetLocation(Location, self.document, &self.location);
} }
pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void { pub fn replaceDocument(self: *Window, doc: *parser.DocumentHTML) !void {

View File

@@ -84,6 +84,11 @@ pub const Page = struct {
polyfill_loader: polyfill.Loader = .{}, polyfill_loader: polyfill.Loader = .{},
/// KV map for various object data; use pointers as unsigned integer keys
/// and store any `*anyopaque` as values. If a key or value will be
/// deinitialized (freed), it should be removed from the map too.
object_data: ObjectDataMap = .{},
scheduler: Scheduler, scheduler: Scheduler,
http_client: *Http.Client, http_client: *Http.Client,
script_manager: ScriptManager, script_manager: ScriptManager,
@@ -122,12 +127,30 @@ pub const Page = struct {
complete, complete,
}; };
const ObjectDataMap = std.HashMapUnmanaged(
usize,
*anyopaque,
struct {
pub fn hash(_: @This(), key: usize) usize {
return key;
}
pub fn eql(_: @This(), a: usize, b: usize) bool {
return a == b;
}
},
std.hash_map.default_max_load_percentage,
);
pub fn init(self: *Page, arena: Allocator, call_arena: Allocator, session: *Session) !void { pub fn init(self: *Page, arena: Allocator, call_arena: Allocator, session: *Session) !void {
const browser = session.browser; const browser = session.browser;
const script_manager = ScriptManager.init(browser, self); const script_manager = ScriptManager.init(browser, self);
const url = try URL.parse("about:blank", null);
errdefer url.deinit();
self.* = .{ self.* = .{
.url = URL.empty, .url = url,
.mode = .{ .pre = {} }, .mode = .{ .pre = {} },
.window = try Window.create(null, null), .window = try Window.create(null, null),
.arena = arena, .arena = arena,
@@ -150,11 +173,14 @@ pub const Page = struct {
try self.registerBackgroundTasks(); try self.registerBackgroundTasks();
} }
// FIXME: Deinit self.url.
pub fn deinit(self: *Page) void { pub fn deinit(self: *Page) void {
self.script_manager.shutdown = true; self.script_manager.shutdown = true;
self.http_client.abort(); self.http_client.abort();
self.script_manager.deinit(); self.script_manager.deinit();
self.url.deinit();
self.object_data.deinit(self.arena);
} }
fn reset(self: *Page) !void { fn reset(self: *Page) !void {
@@ -165,6 +191,12 @@ pub const Page = struct {
self.http_client.abort(); self.http_client.abort();
self.script_manager.reset(); self.script_manager.reset();
_ = try self.url.reparse("about:blank");
errdefer self.url.deinit();
self.object_data.deinit(self.arena);
self.object_data = .{};
self.load_state = .parsing; self.load_state = .parsing;
self.mode = .{ .pre = {} }; self.mode = .{ .pre = {} };
_ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); _ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
@@ -195,6 +227,21 @@ pub const Page = struct {
}.runMessageLoop, 5, .{ .name = "page.messageLoop" }); }.runMessageLoop, 5, .{ .name = "page.messageLoop" });
} }
/// Returns the object data by given key.
/// `key` must be a pointer type.
/// Type of value is unknown to map; so the caller must do the type casting.
pub fn getObjectData(self: *Page, key: anytype) ?*anyopaque {
std.debug.assert(@typeInfo(@TypeOf(key)) == .pointer);
return self.object_data.get(@intFromPtr(key));
}
/// Puts the object data by given key.
/// `key` must be a pointer type.
pub fn putObjectData(self: *Page, key: anytype, value: *anyopaque) Allocator.Error!void {
std.debug.assert(@typeInfo(@TypeOf(key)) == .pointer);
return self.object_data.put(self.arena, @intFromPtr(key), value);
}
pub const DumpOpts = struct { pub const DumpOpts = struct {
// set to include element shadowroots in the dump // set to include element shadowroots in the dump
page: ?*const Page = null, page: ?*const Page = null,
@@ -239,7 +286,7 @@ pub const Page = struct {
const doc = parser.documentHTMLToDocument(self.window.document); const doc = parser.documentHTMLToDocument(self.window.document);
// if the base si requested, add the base's node in the document's headers. // if the base is requested, add the base's node in the document's headers.
if (opts.with_base) { if (opts.with_base) {
try self.addDOMTreeBase(); try self.addDOMTreeBase();
} }
@@ -262,7 +309,7 @@ pub const Page = struct {
const head = parser.nodeListItem(list, 0) orelse return; const head = parser.nodeListItem(list, 0) orelse return;
const base = try parser.documentCreateElement(doc, "base"); const base = try parser.documentCreateElement(doc, "base");
try parser.elementSetAttribute(base, "href", self.url.raw); try parser.elementSetAttribute(base, "href", self.url.getHref());
const Node = @import("dom/node.zig").Node; const Node = @import("dom/node.zig").Node;
try Node.prepend(head, &[_]Node.NodeOrText{.{ .node = parser.elementToNode(base) }}); try Node.prepend(head, &[_]Node.NodeOrText{.{ .node = parser.elementToNode(base) }});
@@ -516,19 +563,18 @@ pub const Page = struct {
} }
pub fn origin(self: *const Page, arena: Allocator) ![]const u8 { pub fn origin(self: *const Page, arena: Allocator) ![]const u8 {
var aw = std.Io.Writer.Allocating.init(arena); return self.url.getOrigin(arena);
try self.url.origin(&aw.writer);
return aw.written();
} }
const RequestCookieOpts = struct { const RequestCookieOpts = struct {
is_http: bool = true, is_http: bool = true,
is_navigation: bool = false, is_navigation: bool = false,
}; };
pub fn requestCookie(self: *const Page, opts: RequestCookieOpts) Http.Client.RequestCookie { pub fn requestCookie(self: *const Page, opts: RequestCookieOpts) Http.Client.RequestCookie {
return .{ return .{
.jar = self.cookie_jar, .cookie_jar = self.cookie_jar,
.origin = &self.url.uri, .origin_url = self.url,
.is_http = opts.is_http, .is_http = opts.is_http,
.is_navigation = opts.is_navigation, .is_navigation = opts.is_navigation,
}; };
@@ -636,7 +682,7 @@ pub const Page = struct {
}; };
self.session.browser.notification.dispatch(.page_navigated, &.{ self.session.browser.notification.dispatch(.page_navigated, &.{
.url = self.url.raw, .url = self.url.getHref(),
.timestamp = timestamp(), .timestamp = timestamp(),
}); });
} }
@@ -816,7 +862,7 @@ pub const Page = struct {
} }
// Push the navigation after a successful load. // Push the navigation after a successful load.
try self.session.history.pushNavigation(self.url.raw, self); try self.session.history.pushNavigation(self.url.getHref(), self);
} }
fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void { fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
@@ -852,7 +898,7 @@ pub const Page = struct {
// extracted because this is called from tests to set things up. // extracted because this is called from tests to set things up.
pub fn setDocument(self: *Page, html_doc: *parser.DocumentHTML) !void { pub fn setDocument(self: *Page, html_doc: *parser.DocumentHTML) !void {
const doc = parser.documentHTMLToDocument(html_doc); const doc = parser.documentHTMLToDocument(html_doc);
try parser.documentSetDocumentURI(doc, self.url.raw); try parser.documentSetDocumentURI(doc, self.url.getHref());
// TODO set the referrer to the document. // TODO set the referrer to the document.
try self.window.replaceDocument(html_doc); try self.window.replaceDocument(html_doc);
@@ -1042,7 +1088,7 @@ pub const Page = struct {
session.queued_navigation = .{ session.queued_navigation = .{
.opts = opts, .opts = opts,
.url = try URL.stitch(session.transfer_arena, url, self.url.raw, .{ .alloc = .always }), .url = try URL.stitch(session.transfer_arena, url, self.url.getHref(), .{ .alloc = .always }),
}; };
self.http_client.abort(); self.http_client.abort();
@@ -1082,7 +1128,7 @@ pub const Page = struct {
try form_data.write(encoding, buf.writer(transfer_arena)); try form_data.write(encoding, buf.writer(transfer_arena));
const method = try parser.elementGetAttribute(@ptrCast(@alignCast(form)), "method") orelse ""; const method = try parser.elementGetAttribute(@ptrCast(@alignCast(form)), "method") orelse "";
var action = try parser.elementGetAttribute(@ptrCast(@alignCast(form)), "action") orelse self.url.raw; var action = try parser.elementGetAttribute(@ptrCast(@alignCast(form)), "action") orelse self.url.getHref();
var opts = NavigateOpts{ var opts = NavigateOpts{
.reason = .form, .reason = .form,

View File

@@ -1,19 +1,11 @@
const std = @import("std"); const std = @import("std");
const Uri = std.Uri;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const DateTime = @import("../../datetime.zig").DateTime; const DateTime = @import("../../datetime.zig").DateTime;
const public_suffix_list = @import("../../data/public_suffix_list.zig").lookup; const public_suffix_list = @import("../../data/public_suffix_list.zig").lookup;
const URL = @import("../../url.zig").URL;
pub const LookupOpts = struct {
request_time: ?i64 = null,
origin_uri: ?*const Uri = null,
is_http: bool,
is_navigation: bool = true,
prefix: ?[]const u8 = null,
};
pub const Jar = struct { pub const Jar = struct {
allocator: Allocator, allocator: Allocator,
@@ -80,13 +72,22 @@ pub const Jar = struct {
} }
} }
pub fn forRequest(self: *Jar, target_uri: *const Uri, writer: anytype, opts: LookupOpts) !void { pub const LookupOpts = struct {
const target = PreparedUri{ request_time: ?i64 = null,
.host = (target_uri.host orelse return error.InvalidURI).percent_encoded, origin_url: ?URL = null,
.path = target_uri.path.percent_encoded, is_http: bool,
.secure = std.mem.eql(u8, target_uri.scheme, "https"), is_navigation: bool = true,
prefix: ?[]const u8 = null,
}; };
const same_site = try areSameSite(opts.origin_uri, target.host);
// FIXME: Invalid behavior.
pub fn forRequest(self: *Jar, target_url: URL, writer: anytype, opts: LookupOpts) !void {
const target = PreparedUri{
.host = target_url.getHostname(),
.path = target_url.getPath(),
.secure = target_url.isSecure(),
};
const same_site = try areSameSite(opts.origin_url, target.host);
removeExpired(self, opts.request_time); removeExpired(self, opts.request_time);
@@ -109,8 +110,8 @@ pub const Jar = struct {
} }
} }
pub fn populateFromResponse(self: *Jar, uri: *const Uri, set_cookie: []const u8) !void { pub fn populateFromResponse(self: *Jar, url: URL, set_cookie: []const u8) !void {
const c = Cookie.parse(self.allocator, uri, set_cookie) catch |err| { const c = Cookie.parse(self.allocator, url, set_cookie) catch |err| {
log.warn(.web_api, "cookie parse failed", .{ .raw = set_cookie, .err = err }); log.warn(.web_api, "cookie parse failed", .{ .raw = set_cookie, .err = err });
return; return;
}; };
@@ -148,9 +149,9 @@ fn areCookiesEqual(a: *const Cookie, b: *const Cookie) bool {
return true; return true;
} }
fn areSameSite(origin_uri_: ?*const std.Uri, target_host: []const u8) !bool { fn areSameSite(maybe_origin_url: ?URL, target_host: []const u8) !bool {
const origin_uri = origin_uri_ orelse return true; const origin_url = maybe_origin_url orelse return true;
const origin_host = (origin_uri.host orelse return error.InvalidURI).percent_encoded; const origin_host = origin_url.host();
// common case // common case
if (std.mem.eql(u8, target_host, origin_host)) { if (std.mem.eql(u8, target_host, origin_host)) {
@@ -161,6 +162,7 @@ fn areSameSite(origin_uri_: ?*const std.Uri, target_host: []const u8) !bool {
} }
fn findSecondLevelDomain(host: []const u8) []const u8 { fn findSecondLevelDomain(host: []const u8) []const u8 {
// TODO: maybe reverseIterator?
var i = std.mem.lastIndexOfScalar(u8, host, '.') orelse return host; var i = std.mem.lastIndexOfScalar(u8, host, '.') orelse return host;
while (true) { while (true) {
i = std.mem.lastIndexOfScalar(u8, host[0..i], '.') orelse return host; i = std.mem.lastIndexOfScalar(u8, host[0..i], '.') orelse return host;
@@ -206,7 +208,7 @@ pub const Cookie = struct {
// Invalid attribute values? Ignore. // Invalid attribute values? Ignore.
// Duplicate attributes - use the last valid // Duplicate attributes - use the last valid
// Value-less attributes with a value? Ignore the value // Value-less attributes with a value? Ignore the value
pub fn parse(allocator: Allocator, uri: *const std.Uri, str: []const u8) !Cookie { pub fn parse(allocator: Allocator, url: URL, str: []const u8) !Cookie {
try validateCookieString(str); try validateCookieString(str);
const cookie_name, const cookie_value, const rest = parseNameValue(str) catch { const cookie_name, const cookie_value, const rest = parseNameValue(str) catch {
@@ -269,8 +271,8 @@ pub const Cookie = struct {
const aa = arena.allocator(); const aa = arena.allocator();
const owned_name = try aa.dupe(u8, cookie_name); const owned_name = try aa.dupe(u8, cookie_name);
const owned_value = try aa.dupe(u8, cookie_value); const owned_value = try aa.dupe(u8, cookie_value);
const owned_path = try parsePath(aa, uri, path); const owned_path = try parsePath(aa, url, path);
const owned_domain = try parseDomain(aa, uri, domain); const owned_domain = try parseDomain(aa, url, domain);
var normalized_expires: ?f64 = null; var normalized_expires: ?f64 = null;
if (max_age) |ma| { if (max_age) |ma| {
@@ -362,37 +364,39 @@ pub const Cookie = struct {
} }
} }
pub fn parsePath(arena: Allocator, uri: ?*const std.Uri, explicit_path: ?[]const u8) ![]const u8 { pub fn parsePath(arena: Allocator, maybe_url: ?URL, maybe_explicit_path: ?[]const u8) ![]const u8 {
// path attribute value either begins with a '/' or we // path attribute value either begins with a '/' or we
// ignore it and use the "default-path" algorithm // ignore it and use the "default-path" algorithm
if (explicit_path) |path| { if (maybe_explicit_path) |path| {
if (path.len > 0 and path[0] == '/') { if (path.len > 0 and path[0] == '/') {
return try arena.dupe(u8, path); return arena.dupe(u8, path);
} }
} }
// default-path const url_path = blk: {
const url_path = (uri orelse return "/").path; if (maybe_url) |url| {
break :blk url.getPath();
const either = url_path.percent_encoded;
if (either.len == 0 or (either.len == 1 and either[0] == '/')) {
return "/";
} }
var owned_path: []const u8 = try percentEncode(arena, url_path, isPathChar);
const last = std.mem.lastIndexOfScalar(u8, owned_path[1..], '/') orelse {
return "/"; return "/";
}; };
return try arena.dupe(u8, owned_path[0 .. last + 1]);
if (url_path.len == 0 or (url_path.len == 1 and url_path[0] == '/')) {
return "/";
} }
pub fn parseDomain(arena: Allocator, uri: ?*const std.Uri, explicit_domain: ?[]const u8) ![]const u8 { const last = std.mem.lastIndexOfScalar(u8, url_path[1..], '/') orelse {
return "/";
};
return arena.dupe(u8, url_path[0 .. last + 1]);
}
pub fn parseDomain(arena: Allocator, maybe_url: ?URL, explicit_domain: ?[]const u8) ![]const u8 {
var encoded_host: ?[]const u8 = null; var encoded_host: ?[]const u8 = null;
if (uri) |uri_| { if (maybe_url) |url| {
const uri_host = uri_.host orelse return error.InvalidURI; const url_host = url.getHostname();
const host = try percentEncode(arena, uri_host, isHostChar); encoded_host = url_host;
_ = toLower(host);
encoded_host = host;
} }
if (explicit_domain) |domain| { if (explicit_domain) |domain| {
@@ -421,19 +425,6 @@ pub const Cookie = struct {
return encoded_host orelse return error.InvalidDomain; // default-domain return encoded_host orelse return error.InvalidDomain; // default-domain
} }
pub fn percentEncode(arena: Allocator, component: std.Uri.Component, comptime isValidChar: fn (u8) bool) ![]u8 {
switch (component) {
.raw => |str| {
var aw = try std.Io.Writer.Allocating.initCapacity(arena, str.len);
try std.Uri.Component.percentEncode(&aw.writer, str, isValidChar);
return aw.written(); // @memory retains memory used before growing
},
.percent_encoded => |str| {
return try arena.dupe(u8, str);
},
}
}
pub fn isHostChar(c: u8) bool { pub fn isHostChar(c: u8) bool {
return switch (c) { return switch (c) {
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true, 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
@@ -601,37 +592,40 @@ test "Jar: add" {
defer jar.deinit(); defer jar.deinit();
try expectCookies(&.{}, jar); try expectCookies(&.{}, jar);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9000;Max-Age=0"), now); const test_url = try URL.parse("http://lightpanda.io/", null);
defer test_url.deinit();
try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9000;Max-Age=0"), now);
try expectCookies(&.{}, jar); try expectCookies(&.{}, jar);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9000"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9000"), now);
try expectCookies(&.{.{ "over", "9000" }}, jar); try expectCookies(&.{.{ "over", "9000" }}, jar);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9000!!"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9000!!"), now);
try expectCookies(&.{.{ "over", "9000!!" }}, jar); try expectCookies(&.{.{ "over", "9000!!" }}, jar);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "spice=flow"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "spice=flow"), now);
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flow" } }, jar); try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flow" } }, jar);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "spice=flows;Path=/"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "spice=flows;Path=/"), now);
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" } }, jar); try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" } }, jar);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9001;Path=/other"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9001;Path=/other"), now);
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9001" } }, jar); try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9001" } }, jar);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=9002;Path=/;Domain=lightpanda.io"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "over=9002;Path=/;Domain=lightpanda.io"), now);
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9001" }, .{ "over", "9002" } }, jar); try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9001" }, .{ "over", "9002" } }, jar);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "over=x;Path=/other;Max-Age=-200"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "over=x;Path=/other;Max-Age=-200"), now);
try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9002" } }, jar); try expectCookies(&.{ .{ "over", "9000!!" }, .{ "spice", "flows" }, .{ "over", "9002" } }, jar);
} }
test "Jar: forRequest" { test "Jar: forRequest" {
const expectCookies = struct { const expectCookies = struct {
fn expect(expected: []const u8, jar: *Jar, target_uri: Uri, opts: LookupOpts) !void { fn expect(expected: []const u8, jar: *Jar, target_url: URL, opts: Jar.LookupOpts) !void {
var arr: std.ArrayListUnmanaged(u8) = .empty; var arr: std.ArrayListUnmanaged(u8) = .empty;
defer arr.deinit(testing.allocator); defer arr.deinit(testing.allocator);
try jar.forRequest(&target_uri, arr.writer(testing.allocator), opts); try jar.forRequest(target_url, arr.writer(testing.allocator), opts);
try testing.expectEqual(expected, arr.items); try testing.expectEqual(expected, arr.items);
} }
}.expect; }.expect;
@@ -641,131 +635,142 @@ test "Jar: forRequest" {
var jar = Jar.init(testing.allocator); var jar = Jar.init(testing.allocator);
defer jar.deinit(); defer jar.deinit();
const test_uri_2 = Uri.parse("http://test.lightpanda.io/") catch unreachable; const test_url = try URL.parse("http://lightpanda.io/", null);
defer test_url.deinit();
const test_url_2 = try URL.parse("http://test.lightpanda.io/", null);
defer test_url_2.deinit();
{ {
// test with no cookies // test with no cookies
try expectCookies("", &jar, test_uri, .{ .is_http = true }); try expectCookies("", &jar, test_url, .{ .is_http = true });
} }
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "global1=1"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "global1=1"), now);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "global2=2;Max-Age=30;domain=lightpanda.io"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "global2=2;Max-Age=30;domain=lightpanda.io"), now);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "path1=3;Path=/about"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "path1=3;Path=/about"), now);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "path2=4;Path=/docs/"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "path2=4;Path=/docs/"), now);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "secure=5;Secure"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "secure=5;Secure"), now);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "sitenone=6;SameSite=None;Path=/x/;Secure"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "sitenone=6;SameSite=None;Path=/x/;Secure"), now);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "sitelax=7;SameSite=Lax;Path=/x/"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "sitelax=7;SameSite=Lax;Path=/x/"), now);
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "sitestrict=8;SameSite=Strict;Path=/x/"), now); try jar.add(try Cookie.parse(testing.allocator, test_url, "sitestrict=8;SameSite=Strict;Path=/x/"), now);
try jar.add(try Cookie.parse(testing.allocator, &test_uri_2, "domain1=9;domain=test.lightpanda.io"), now); try jar.add(try Cookie.parse(testing.allocator, test_url_2, "domain1=9;domain=test.lightpanda.io"), now);
// nothing fancy here // nothing fancy here
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .is_http = true }); try expectCookies("global1=1; global2=2", &jar, test_url, .{ .is_http = true });
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .is_navigation = false, .is_http = true }); try expectCookies("global1=1; global2=2", &jar, test_url, .{ .origin_url = test_url, .is_navigation = false, .is_http = true });
// We reuse this URL to reparse.
const reuse_url = try URL.parse("http://anothersitelightpanda.io/", null);
defer reuse_url.deinit();
// We have a cookie where Domain=lightpanda.io // We have a cookie where Domain=lightpanda.io
// This should _not_ match xyxlightpanda.io // This should _not_ match xyxlightpanda.io
try expectCookies("", &jar, try std.Uri.parse("http://anothersitelightpanda.io/"), .{ try expectCookies("", &jar, reuse_url, .{
.origin_uri = &test_uri, .origin_url = test_url,
.is_http = true, .is_http = true,
}); });
// matching path without trailing / // matching path without trailing /
try expectCookies("global1=1; global2=2; path1=3", &jar, try std.Uri.parse("http://lightpanda.io/about"), .{ try expectCookies("global1=1; global2=2; path1=3", &jar, try reuse_url.reparse("http://lightpanda.io/about"), .{
.origin_uri = &test_uri, .origin_url = test_url,
.is_http = true, .is_http = true,
}); });
// incomplete prefix path // incomplete prefix path
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/abou"), .{ try expectCookies("global1=1; global2=2", &jar, try reuse_url.reparse("http://lightpanda.io/abou"), .{
.origin_uri = &test_uri, .origin_url = test_url,
.is_http = true, .is_http = true,
}); });
// path doesn't match // path doesn't match
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/aboutus"), .{ try expectCookies("global1=1; global2=2", &jar, try reuse_url.reparse("http://lightpanda.io/aboutus"), .{
.origin_uri = &test_uri, .origin_url = test_url,
.is_http = true, .is_http = true,
}); });
// path doesn't match cookie directory // path doesn't match cookie directory
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/docs"), .{ try expectCookies("global1=1; global2=2", &jar, try reuse_url.reparse("http://lightpanda.io/docs"), .{
.origin_uri = &test_uri, .origin_url = test_url,
.is_http = true, .is_http = true,
}); });
// exact directory match // exact directory match
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/"), .{ try expectCookies("global1=1; global2=2; path2=4", &jar, try reuse_url.reparse("http://lightpanda.io/docs/"), .{
.origin_uri = &test_uri, .origin_url = test_url,
.is_http = true, .is_http = true,
}); });
// sub directory match // sub directory match
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/more"), .{ try expectCookies("global1=1; global2=2; path2=4", &jar, try reuse_url.reparse("http://lightpanda.io/docs/more"), .{
.origin_uri = &test_uri, .origin_url = test_url,
.is_http = true, .is_http = true,
}); });
// secure // secure
try expectCookies("global1=1; global2=2; secure=5", &jar, try std.Uri.parse("https://lightpanda.io/"), .{ try expectCookies("global1=1; global2=2; secure=5", &jar, try reuse_url.reparse("https://lightpanda.io/"), .{
.origin_uri = &test_uri, .origin_url = test_url,
.is_http = true, .is_http = true,
}); });
// navigational cross domain, secure // navigational cross domain, secure
try expectCookies("global1=1; global2=2; secure=5; sitenone=6; sitelax=7", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{ const example_com_url = try URL.parse("https://example.com/", null);
.origin_uri = &(try std.Uri.parse("https://example.com/")), defer example_com_url.deinit();
try expectCookies("global1=1; global2=2; secure=5; sitenone=6; sitelax=7", &jar, try reuse_url.reparse("https://lightpanda.io/x/"), .{
.origin_url = example_com_url,
.is_http = true, .is_http = true,
}); });
// navigational cross domain, insecure // navigational cross domain, insecure
try expectCookies("global1=1; global2=2; sitelax=7", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{ try expectCookies("global1=1; global2=2; sitelax=7", &jar, try reuse_url.reparse("http://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")), .origin_url = example_com_url,
.is_http = true, .is_http = true,
}); });
// non-navigational cross domain, insecure // non-navigational cross domain, insecure
try expectCookies("", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{ try expectCookies("", &jar, try reuse_url.reparse("http://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")), .origin_url = example_com_url,
.is_http = true, .is_http = true,
.is_navigation = false, .is_navigation = false,
}); });
// non-navigational cross domain, secure // non-navigational cross domain, secure
try expectCookies("sitenone=6", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{ try expectCookies("sitenone=6", &jar, try reuse_url.reparse("https://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")), .origin_url = example_com_url,
.is_http = true, .is_http = true,
.is_navigation = false, .is_navigation = false,
}); });
// non-navigational same origin // non-navigational same origin
try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{ try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try reuse_url.reparse("http://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://lightpanda.io/")), .origin_url = test_url,
.is_http = true, .is_http = true,
.is_navigation = false, .is_navigation = false,
}); });
// exact domain match + suffix // exact domain match + suffix
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://test.lightpanda.io/"), .{ try expectCookies("global2=2; domain1=9", &jar, try reuse_url.reparse("http://test.lightpanda.io/"), .{
.origin_uri = &test_uri, .origin_url = test_url,
.is_http = true, .is_http = true,
}); });
// domain suffix match + suffix // domain suffix match + suffix
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://1.test.lightpanda.io/"), .{ try expectCookies("global2=2; domain1=9", &jar, try reuse_url.reparse("http://1.test.lightpanda.io/"), .{
.origin_uri = &test_uri, .origin_url = test_url,
.is_http = true, .is_http = true,
}); });
// non-matching domain // non-matching domain
try expectCookies("global2=2", &jar, try std.Uri.parse("http://other.lightpanda.io/"), .{ try expectCookies("global2=2", &jar, try reuse_url.reparse("http://other.lightpanda.io/"), .{
.origin_uri = &test_uri, .origin_url = test_url,
.is_http = true, .is_http = true,
}); });
const l = jar.cookies.items.len; const l = jar.cookies.items.len;
try expectCookies("global1=1", &jar, test_uri, .{ try expectCookies("global1=1", &jar, test_url, .{
.request_time = now + 100, .request_time = now + 100,
.origin_uri = &test_uri, .origin_url = test_url,
.is_http = true, .is_http = true,
}); });
try testing.expectEqual(l - 1, jar.cookies.items.len); try testing.expectEqual(l - 1, jar.cookies.items.len);
@@ -961,9 +966,11 @@ const ExpectedCookie = struct {
same_site: Cookie.SameSite = .lax, same_site: Cookie.SameSite = .lax,
}; };
fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u8) !void { fn expectCookie(expected: ExpectedCookie, url_str: []const u8, set_cookie: []const u8) !void {
const uri = try Uri.parse(url); const url = try URL.parse(url_str, null);
var cookie = try Cookie.parse(testing.allocator, &uri, set_cookie); defer url.deinit();
var cookie = try Cookie.parse(testing.allocator, url, set_cookie);
defer cookie.deinit(); defer cookie.deinit();
try testing.expectEqual(expected.name, cookie.name); try testing.expectEqual(expected.name, cookie.name);
@@ -977,9 +984,11 @@ fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u
try testing.expectDelta(expected.expires, cookie.expires, 2.0); try testing.expectDelta(expected.expires, cookie.expires, 2.0);
} }
fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8) !void { fn expectAttribute(expected: anytype, maybe_url_str: ?[]const u8, set_cookie: []const u8) !void {
const uri = if (url) |u| try Uri.parse(u) else test_uri; const url = try URL.parse(if (maybe_url_str) |url_str| url_str else "https://lightpanda.io/", null);
var cookie = try Cookie.parse(testing.allocator, &uri, set_cookie); defer url.deinit();
var cookie = try Cookie.parse(testing.allocator, url, set_cookie);
defer cookie.deinit(); defer cookie.deinit();
inline for (@typeInfo(@TypeOf(expected)).@"struct".fields) |f| { inline for (@typeInfo(@TypeOf(expected)).@"struct".fields) |f| {
@@ -994,9 +1003,7 @@ fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8)
} }
} }
fn expectError(expected: anyerror, url: ?[]const u8, set_cookie: []const u8) !void { fn expectError(expected: anyerror, maybe_url_str: ?[]const u8, set_cookie: []const u8) !void {
const uri = if (url) |u| try Uri.parse(u) else test_uri; const url = try URL.parse(if (maybe_url_str) |url_str| url_str else "https://lightpanda.io/", null);
try testing.expectError(expected, Cookie.parse(testing.allocator, &uri, set_cookie)); try testing.expectError(expected, Cookie.parse(testing.allocator, url, set_cookie));
} }
const test_uri = Uri.parse("http://lightpanda.io/") catch unreachable;

View File

@@ -18,6 +18,8 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Writer = std.Io.Writer;
const ada = @import("ada");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
@@ -35,182 +37,180 @@ pub const Interfaces = .{
EntryIterable, EntryIterable,
}; };
// https://url.spec.whatwg.org/#url /// https://developer.mozilla.org/en-US/docs/Web/API/URL/URL
//
// TODO we could avoid many of these getter string allocatoration in two differents
// way:
//
// 1. We can eventually get the slice of scheme *with* the following char in
// the underlying string. But I don't know if it's possible and how to do that.
// I mean, if the rawuri contains `https://foo.bar`, uri.scheme is a slice
// containing only `https`. I want `https:` so, in theory, I don't need to
// allocatorate data, I should be able to retrieve the scheme + the following `:`
// from rawuri.
//
// 2. The other way would be to copy the `std.Uri` code to have a dedicated
// parser including the characters we want for the web API.
pub const URL = struct { pub const URL = struct {
uri: std.Uri, internal: ada.URL,
/// We prefer in-house search params solution here;
/// ada's search params impl use more memory.
/// It also offers it's own iterator implementation
/// where we'd like to use ours.
search_params: URLSearchParams, search_params: URLSearchParams,
pub const empty = URL{ pub const empty = URL{
.uri = .{ .scheme = "" }, .internal = null,
.search_params = .{}, .search_params = .{},
}; };
const URLArg = union(enum) { // You can use an existing URL object for either argument, and it will be
url: *URL, // stringified from the object's href property.
element: *parser.ElementHTML, const ConstructorArg = union(enum) {
string: []const u8, string: []const u8,
url: *const URL,
element: *parser.Element,
fn toString(self: URLArg, arena: Allocator) !?[]const u8 { fn toString(self: ConstructorArg, page: *Page) ![]const u8 {
switch (self) { return switch (self) {
.string => |s| return s, .string => |s| s,
.url => |url| return try url.toString(arena), .url => |url| url._toString(page),
.element => |e| return try parser.elementGetAttribute(@ptrCast(e), "href"), .element => |e| {
} const attrib = try parser.elementGetAttribute(@ptrCast(e), "href") orelse {
return error.InvalidArgument;
};
return attrib;
},
};
} }
}; };
pub fn constructor(url: URLArg, base: ?URLArg, page: *Page) !URL { pub fn constructor(url: ConstructorArg, maybe_base: ?ConstructorArg, page: *Page) !URL {
const arena = page.arena; const url_str = try url.toString(page);
const url_str = try url.toString(arena) orelse return error.InvalidArgument;
var raw: ?[]const u8 = null; const internal = try blk: {
if (base) |b| { if (maybe_base) |base| {
if (try b.toString(arena)) |bb| { break :blk ada.parseWithBase(url_str, try base.toString(page));
raw = try @import("../../url.zig").URL.stitch(arena, url_str, bb, .{});
}
} }
if (raw == null) { break :blk ada.parse(url_str);
// if it was a URL, then it's already be owned by the arena
raw = if (url == .url) url_str else try arena.dupe(u8, url_str);
}
const uri = std.Uri.parse(raw.?) catch blk: {
if (!std.mem.endsWith(u8, raw.?, "://")) {
return error.TypeError;
}
// schema only is valid!
break :blk std.Uri{
.scheme = raw.?[0 .. raw.?.len - 3],
.host = .{ .percent_encoded = "" },
};
}; };
return init(arena, uri);
}
pub fn init(arena: Allocator, uri: std.Uri) !URL {
return .{ return .{
.uri = uri, .internal = internal,
.search_params = try URLSearchParams.init( .search_params = try prepareSearchParams(page.arena, internal),
arena,
uriComponentNullStr(uri.query),
),
}; };
} }
pub fn initWithoutSearchParams(uri: std.Uri) URL { pub fn destructor(self: *const URL) void {
return .{ .uri = uri, .search_params = .{} }; // Not tracked by arena.
return ada.free(self.internal);
} }
pub fn get_origin(self: *URL, page: *Page) ![]const u8 { /// Initializes a `URL` from given `internal`.
var aw = std.Io.Writer.Allocating.init(page.arena); /// Note that this copies the given `internal`; meaning 2 instances
try self.uri.writeToStream(&aw.writer, .{ /// of it has to be tracked separately.
.scheme = true, pub fn constructFromInternal(arena: Allocator, internal: ada.URL) !URL {
.authentication = false, const copy = ada.copy(internal);
.authority = true,
.path = false, return .{
.query = false, .internal = copy,
.fragment = false, .search_params = try prepareSearchParams(arena, copy),
}); };
return aw.written();
} }
// get_href returns the URL by writing all its components. /// Prepares a `URLSearchParams` from given `internal`.
pub fn get_href(self: *URL, page: *Page) ![]const u8 { /// Resets `search` of `internal`.
return self.toString(page.arena); fn prepareSearchParams(arena: Allocator, internal: ada.URL) !URLSearchParams {
const search = ada.getSearch(internal);
// Empty.
if (search.data == null) return .{};
const slice = search.data[0..search.length];
const search_params = URLSearchParams.initFromString(arena, slice);
// After a call to this function, search params are tracked by
// `search_params`. So we reset the internal's search.
ada.clearSearch(internal);
return search_params;
} }
pub fn _toString(self: *URL, page: *Page) ![]const u8 { // Alias to get_href.
return self.toString(page.arena); pub fn _toString(self: *const URL, page: *Page) ![]const u8 {
return self.get_href(page);
} }
// format the url with all its components. // Getters.
pub fn toString(self: *const URL, arena: Allocator) ![]const u8 {
var aw = std.Io.Writer.Allocating.init(arena);
try self.uri.writeToStream(&aw.writer, .{
.scheme = true,
.authentication = true,
.authority = true,
.path = uriComponentNullStr(self.uri.path).len > 0,
});
if (self.search_params.get_size() > 0) { pub fn get_searchParams(self: *URL) *URLSearchParams {
try aw.writer.writeByte('?'); return &self.search_params;
try self.search_params.write(&aw.writer);
} }
{ pub fn get_origin(self: *const URL, page: *Page) ![]const u8 {
const fragment = uriComponentNullStr(self.uri.fragment);
if (fragment.len > 0) {
try aw.writer.writeByte('#');
try aw.writer.writeAll(fragment);
}
}
return aw.written();
}
pub fn get_protocol(self: *const URL) []const u8 {
// std.Uri keeps a pointer to "https", "http" (scheme part) so we know
// its followed by ':'.
const scheme = self.uri.scheme;
return scheme.ptr[0 .. scheme.len + 1];
}
pub fn get_username(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.user);
}
pub fn get_password(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.password);
}
pub fn get_host(self: *URL, page: *Page) ![]const u8 {
var aw = std.Io.Writer.Allocating.init(page.arena);
try self.uri.writeToStream(&aw.writer, .{
.scheme = false,
.authentication = false,
.authority = true,
.path = false,
.query = false,
.fragment = false,
});
return aw.written();
}
pub fn get_hostname(self: *URL) []const u8 {
return uriComponentNullStr(self.uri.host);
}
pub fn get_port(self: *URL, page: *Page) ![]const u8 {
const arena = page.arena; const arena = page.arena;
if (self.uri.port == null) return try arena.dupe(u8, ""); // `ada.getOrigin` allocates memory in order to find the `origin`.
// We'd like to use our arena allocator for such case;
// so here we allocate the `origin` in page arena and free the original.
const origin = ada.getOrigin(self.internal);
// `OwnedString` itself is not heap allocated so this is safe.
defer ada.freeOwnedString(.{ .data = origin.ptr, .length = origin.len });
var aw = std.Io.Writer.Allocating.init(arena); return arena.dupe(u8, origin);
try aw.writer.printInt(self.uri.port.?, 10, .lower, .{});
return aw.written();
} }
pub fn get_pathname(self: *URL) []const u8 { pub fn get_href(self: *const URL, page: *Page) ![]const u8 {
if (uriComponentStr(self.uri.path).len == 0) return "/"; var w: Writer.Allocating = .init(page.arena);
return uriComponentStr(self.uri.path);
const maybe_href = ada.getHrefNullable(self.internal);
if (maybe_href.data == null) {
return "";
} }
pub fn get_search(self: *URL, page: *Page) ![]const u8 { const href = maybe_href.data[0..maybe_href.length];
const comps = ada.getComponents(self.internal);
const has_hash = comps.hash_start != ada.URLOmitted;
const href_part = if (has_hash) href[0..comps.hash_start] else href;
try w.writer.writeAll(href_part);
// Write search params if provided.
if (self.search_params.get_size() > 0) {
try w.writer.writeByte('?');
try self.search_params.write(&w.writer);
}
// Write hash if provided before.
const hash = self.get_hash();
try w.writer.writeAll(hash);
return w.written();
}
pub fn get_username(self: *const URL) []const u8 {
return ada.getUsername(self.internal);
}
pub fn get_password(self: *const URL) []const u8 {
return ada.getPassword(self.internal);
}
pub fn get_port(self: *const URL) []const u8 {
return ada.getPort(self.internal);
}
pub fn get_hash(self: *const URL) []const u8 {
return ada.getHash(self.internal);
}
pub fn get_host(self: *const URL) []const u8 {
return ada.getHost(self.internal);
}
pub fn get_hostname(self: *const URL) []const u8 {
return ada.getHostname(self.internal);
}
pub fn get_pathname(self: *const URL) []const u8 {
const path = ada.getPathnameNullable(self.internal);
// Return a slash if path is null.
if (path.data == null) {
return "/";
}
return path.data[0..path.length];
}
// get_search depends on the current state of `search_params`.
pub fn get_search(self: *const URL, page: *Page) ![]const u8 {
const arena = page.arena; const arena = page.arena;
if (self.search_params.get_size() == 0) { if (self.search_params.get_size() == 0) {
@@ -223,72 +223,85 @@ pub const URL = struct {
return buf.items; return buf.items;
} }
pub fn set_search(self: *URL, qs_: ?[]const u8, page: *Page) !void { pub fn get_protocol(self: *const URL) []const u8 {
return ada.getProtocol(self.internal);
}
// Setters.
// FIXME: reinit search_params?
pub fn set_href(self: *const URL, input: []const u8) void {
_ = ada.setHref(self.internal, input);
}
pub fn set_host(self: *const URL, input: []const u8) void {
_ = ada.setHost(self.internal, input);
}
pub fn set_hostname(self: *const URL, input: []const u8) void {
_ = ada.setHostname(self.internal, input);
}
pub fn set_protocol(self: *const URL, input: []const u8) void {
_ = ada.setProtocol(self.internal, input);
}
pub fn set_username(self: *const URL, input: []const u8) void {
_ = ada.setUsername(self.internal, input);
}
pub fn set_password(self: *const URL, input: []const u8) void {
_ = ada.setPassword(self.internal, input);
}
pub fn set_port(self: *const URL, input: []const u8) void {
_ = ada.setPort(self.internal, input);
}
pub fn set_pathname(self: *const URL, input: []const u8) void {
_ = ada.setPathname(self.internal, input);
}
pub fn set_search(self: *URL, maybe_input: ?[]const u8, page: *Page) !void {
self.search_params = .{}; self.search_params = .{};
if (qs_) |qs| { if (maybe_input) |input| {
self.search_params = try URLSearchParams.init(page.arena, qs); self.search_params = try .initFromString(page.arena, input);
} }
} }
pub fn get_hash(self: *URL, page: *Page) ![]const u8 { pub fn set_hash(self: *const URL, input: []const u8) void {
const arena = page.arena; _ = ada.setHash(self.internal, input);
if (self.uri.fragment == null) return try arena.dupe(u8, "");
return try std.mem.concat(arena, u8, &[_][]const u8{ "#", uriComponentNullStr(self.uri.fragment) });
}
pub fn get_searchParams(self: *URL) *URLSearchParams {
return &self.search_params;
}
pub fn _toJSON(self: *URL, page: *Page) ![]const u8 {
return self.get_href(page);
} }
}; };
// uriComponentNullStr converts an optional std.Uri.Component to string value.
// The string value can be undecoded.
fn uriComponentNullStr(c: ?std.Uri.Component) []const u8 {
if (c == null) return "";
return uriComponentStr(c.?);
}
fn uriComponentStr(c: std.Uri.Component) []const u8 {
return switch (c) {
.raw => |v| v,
.percent_encoded => |v| v,
};
}
// https://url.spec.whatwg.org/#interface-urlsearchparams
pub const URLSearchParams = struct { pub const URLSearchParams = struct {
entries: kv.List = .{}, entries: kv.List = .{},
const URLSearchParamsOpts = union(enum) { pub const ConstructorOptions = union(enum) {
qs: []const u8, query_string: []const u8,
form_data: *const FormData, form_data: *const FormData,
js_obj: js.Object, object: js.Object,
}; };
pub fn constructor(opts_: ?URLSearchParamsOpts, page: *Page) !URLSearchParams {
const opts = opts_ orelse return .{ .entries = .{} };
return switch (opts) {
.qs => |qs| init(page.arena, qs),
.form_data => |fd| .{ .entries = try fd.entries.clone(page.arena) },
.js_obj => |js_obj| {
const arena = page.arena;
var it = js_obj.nameIterator();
var entries: kv.List = .{}; pub fn constructor(maybe_options: ?ConstructorOptions, page: *Page) !URLSearchParams {
const options = maybe_options orelse return .{};
const arena = page.arena;
return switch (options) {
.query_string => |string| .{ .entries = try parseQuery(arena, string) },
.form_data => |form_data| .{ .entries = try form_data.entries.clone(arena) },
.object => |object| {
var it = object.nameIterator();
var entries = kv.List{};
try entries.ensureTotalCapacity(arena, it.count); try entries.ensureTotalCapacity(arena, it.count);
while (try it.next()) |js_name| { while (try it.next()) |js_name| {
const name = try js_name.toString(arena); const name = try js_name.toString(arena);
const js_val = try js_obj.get(name); const js_value = try object.get(name);
entries.appendOwnedAssumeCapacity( const value = try js_value.toString(arena);
name,
try js_val.toString(arena), entries.appendOwnedAssumeCapacity(name, value);
);
} }
return .{ .entries = entries }; return .{ .entries = entries };
@@ -296,10 +309,9 @@ pub const URLSearchParams = struct {
}; };
} }
pub fn init(arena: Allocator, qs_: ?[]const u8) !URLSearchParams { /// Initializes URLSearchParams from a query string.
return .{ pub fn initFromString(arena: Allocator, query_string: []const u8) !URLSearchParams {
.entries = if (qs_) |qs| try parseQuery(arena, qs) else .{}, return .{ .entries = try parseQuery(arena, query_string) };
};
} }
pub fn get_size(self: *const URLSearchParams) u32 { pub fn get_size(self: *const URLSearchParams) u32 {

View File

@@ -240,7 +240,7 @@ pub const XMLHttpRequest = struct {
self.reset(); self.reset();
self.method = try validMethod(method); self.method = try validMethod(method);
self.url = try URL.stitch(page.arena, url, page.url.raw, .{ .null_terminated = true }); self.url = try URL.stitch(page.arena, url, page.url.getHref(), .{ .null_terminated = true });
self.sync = if (asyn) |b| !b else false; self.sync = if (asyn) |b| !b else false;
self.state = .opened; self.state = .opened;

View File

@@ -464,7 +464,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn getURL(self: *const Self) ?[]const u8 { pub fn getURL(self: *const Self) ?[]const u8 {
const page = self.session.currentPage() orelse return null; const page = self.session.currentPage() orelse return null;
const raw_url = page.url.raw; const raw_url = page.url.getHref();
return if (raw_url.len == 0) null else raw_url; return if (raw_url.len == 0) null else raw_url;
} }

View File

@@ -208,7 +208,7 @@ pub fn requestIntercept(arena: Allocator, bc: anytype, intercept: *const Notific
log.debug(.cdp, "request intercept", .{ log.debug(.cdp, "request intercept", .{
.state = "paused", .state = "paused",
.id = transfer.id, .id = transfer.id,
.url = transfer.uri, .url = transfer.url,
}); });
// Await either continueRequest, failRequest or fulfillRequest // Await either continueRequest, failRequest or fulfillRequest
@@ -237,7 +237,7 @@ fn continueRequest(cmd: anytype) !void {
log.debug(.cdp, "request intercept", .{ log.debug(.cdp, "request intercept", .{
.state = "continue", .state = "continue",
.id = transfer.id, .id = transfer.id,
.url = transfer.uri, .url = transfer.url,
.new_url = params.url, .new_url = params.url,
}); });
@@ -342,7 +342,7 @@ fn fulfillRequest(cmd: anytype) !void {
log.debug(.cdp, "request intercept", .{ log.debug(.cdp, "request intercept", .{
.state = "fulfilled", .state = "fulfilled",
.id = transfer.id, .id = transfer.id,
.url = transfer.uri, .url = transfer.url,
.status = params.responseCode, .status = params.responseCode,
.body = params.body != null, .body = params.body != null,
}); });
@@ -376,7 +376,7 @@ fn failRequest(cmd: anytype) !void {
log.info(.cdp, "request intercept", .{ log.info(.cdp, "request intercept", .{
.state = "fail", .state = "fail",
.id = request_id, .id = request_id,
.url = transfer.uri, .url = transfer.url,
.reason = params.errorReason, .reason = params.errorReason,
}); });
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
@@ -420,7 +420,7 @@ pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Noti
log.debug(.cdp, "request auth required", .{ log.debug(.cdp, "request auth required", .{
.state = "paused", .state = "paused",
.id = transfer.id, .id = transfer.id,
.url = transfer.uri, .url = transfer.url,
}); });
// Await continueWithAuth // Await continueWithAuth

View File

@@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator;
const CdpStorage = @import("storage.zig"); const CdpStorage = @import("storage.zig");
const Transfer = @import("../../http/Client.zig").Transfer; const Transfer = @import("../../http/Client.zig").Transfer;
const Notification = @import("../../notification.zig").Notification; const Notification = @import("../../notification.zig").Notification;
const URL = @import("../../url.zig").URL;
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
@@ -117,15 +118,20 @@ fn deleteCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const cookies = &bc.session.cookie_jar.cookies; const cookies = &bc.session.cookie_jar.cookies;
const uri = if (params.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null; const maybe_url: ?URL = blk: {
const uri_ptr = if (uri) |u| &u else null; if (params.url) |url| {
break :blk URL.parse(url, null) catch return error.InvalidParams;
}
break :blk null;
};
var index = cookies.items.len; var index = cookies.items.len;
while (index > 0) { while (index > 0) {
index -= 1; index -= 1;
const cookie = &cookies.items[index]; const cookie = &cookies.items[index];
const domain = try Cookie.parseDomain(cmd.arena, uri_ptr, params.domain); const domain = try Cookie.parseDomain(cmd.arena, maybe_url, params.domain);
const path = try Cookie.parsePath(cmd.arena, uri_ptr, params.path); const path = try Cookie.parsePath(cmd.arena, maybe_url, params.path);
// We do not want to use Cookie.appliesTo here. As a Cookie with a shorter path would match. // We do not want to use Cookie.appliesTo here. As a Cookie with a shorter path would match.
// Similar to deduplicating with areCookiesEqual, except domain and path are optional. // Similar to deduplicating with areCookiesEqual, except domain and path are optional.
@@ -133,6 +139,12 @@ fn deleteCookies(cmd: anytype) !void {
cookies.swapRemove(index).deinit(); cookies.swapRemove(index).deinit();
} }
} }
// Deinit URL if we had.
if (maybe_url) |url| {
url.deinit();
}
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
@@ -173,17 +185,18 @@ fn getCookies(cmd: anytype) !void {
const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{}; const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{};
// If not specified, use the URLs of the page and all of its subframes. TODO subframes // If not specified, use the URLs of the page and all of its subframes. TODO subframes
const page_url = if (bc.session.page) |*page| page.url.raw else null; // @speed: avoid repasing the URL const page_url = if (bc.session.page) |page| page.url.getHref() else null; // @speed: avoid repasing the URL
const param_urls = params.urls orelse &[_][]const u8{page_url orelse return error.InvalidParams}; const param_urls = params.urls orelse &[_][]const u8{page_url orelse return error.InvalidParams};
var urls = try std.ArrayListUnmanaged(CdpStorage.PreparedUri).initCapacity(cmd.arena, param_urls.len); var urls = try std.ArrayListUnmanaged(CdpStorage.PreparedUri).initCapacity(cmd.arena, param_urls.len);
for (param_urls) |url| { for (param_urls) |url_str| {
const uri = std.Uri.parse(url) catch return error.InvalidParams; const url = URL.parse(url_str, null) catch return error.InvalidParams;
//defer url.deinit();
urls.appendAssumeCapacity(.{ urls.appendAssumeCapacity(.{
.host = try Cookie.parseDomain(cmd.arena, &uri, null), .host = try Cookie.parseDomain(cmd.arena, url, null),
.path = try Cookie.parsePath(cmd.arena, &uri, null), .path = try Cookie.parsePath(cmd.arena, url, null),
.secure = std.mem.eql(u8, uri.scheme, "https"), .secure = url.isSecure(),
}); });
} }
@@ -247,7 +260,7 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification.
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}), .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
.frameId = target_id, .frameId = target_id,
.loaderId = bc.loader_id, .loaderId = bc.loader_id,
.documentUrl = DocumentUrlWriter.init(&page.url.uri), .documentUrl = DocumentUrlWriter.init(page.url),
.request = TransferAsRequestWriter.init(transfer), .request = TransferAsRequestWriter.init(transfer),
.initiator = .{ .type = "other" }, .initiator = .{ .type = "other" },
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
@@ -300,23 +313,17 @@ pub const TransferAsRequestWriter = struct {
try jws.objectField("url"); try jws.objectField("url");
try jws.beginWriteRaw(); try jws.beginWriteRaw();
try writer.writeByte('\"'); try writer.writeByte('\"');
try transfer.uri.writeToStream(writer, .{ try transfer.url.writeToStream(writer);
.scheme = true,
.authentication = true,
.authority = true,
.path = true,
.query = true,
});
try writer.writeByte('\"'); try writer.writeByte('\"');
jws.endWriteRaw(); jws.endWriteRaw();
} }
{ {
if (transfer.uri.fragment) |frag| { if (transfer.url.getFragment()) |frag| {
try jws.objectField("urlFragment"); try jws.objectField("urlFragment");
try jws.beginWriteRaw(); try jws.beginWriteRaw();
try writer.writeAll("\"#"); try writer.writeAll("\"#");
try writer.writeAll(frag.percent_encoded); try writer.writeAll(frag);
try writer.writeByte('\"'); try writer.writeByte('\"');
jws.endWriteRaw(); jws.endWriteRaw();
} }
@@ -370,13 +377,7 @@ const TransferAsResponseWriter = struct {
try jws.objectField("url"); try jws.objectField("url");
try jws.beginWriteRaw(); try jws.beginWriteRaw();
try writer.writeByte('\"'); try writer.writeByte('\"');
try transfer.uri.writeToStream(writer, .{ try transfer.url.writeToStream(writer);
.scheme = true,
.authentication = true,
.authority = true,
.path = true,
.query = true,
});
try writer.writeByte('\"'); try writer.writeByte('\"');
jws.endWriteRaw(); jws.endWriteRaw();
} }
@@ -417,29 +418,22 @@ const TransferAsResponseWriter = struct {
}; };
const DocumentUrlWriter = struct { const DocumentUrlWriter = struct {
uri: *std.Uri, url: URL,
fn init(uri: *std.Uri) DocumentUrlWriter { fn init(url: URL) DocumentUrlWriter {
return .{ return .{ .url = url };
.uri = uri,
};
} }
pub fn jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void { pub fn jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void {
self._jsonStringify(jws) catch return error.WriteFailed; self._jsonStringify(jws) catch return error.WriteFailed;
} }
fn _jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void { fn _jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void {
const writer = jws.writer; const writer = jws.writer;
try jws.beginWriteRaw(); try jws.beginWriteRaw();
try writer.writeByte('\"'); try writer.writeByte('\"');
try self.uri.writeToStream(writer, .{ try self.url.writeToStream(writer);
.scheme = true,
.authentication = true,
.authority = true,
.path = true,
.query = true,
});
try writer.writeByte('\"'); try writer.writeByte('\"');
jws.endWriteRaw(); jws.endWriteRaw();
} }

View File

@@ -21,6 +21,7 @@ const std = @import("std");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const Cookie = @import("../../browser/storage/storage.zig").Cookie; const Cookie = @import("../../browser/storage/storage.zig").Cookie;
const CookieJar = @import("../../browser/storage/storage.zig").CookieJar; const CookieJar = @import("../../browser/storage/storage.zig").CookieJar;
const URL = @import("../../url.zig").URL;
pub const PreparedUri = @import("../../browser/storage/cookie.zig").PreparedUri; pub const PreparedUri = @import("../../browser/storage/cookie.zig").PreparedUri;
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
@@ -136,12 +137,25 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
const a = arena.allocator(); const a = arena.allocator();
// NOTE: The param.url can affect the default domain, (NOT path), secure, source port, and source scheme. // NOTE: The param.url can affect the default domain, (NOT path), secure, source port, and source scheme.
const uri = if (param.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null; const maybe_url: ?URL = blk: {
const uri_ptr = if (uri) |*u| u else null; if (param.url) |url| {
const domain = try Cookie.parseDomain(a, uri_ptr, param.domain); break :blk URL.parse(url, null) catch return error.InvalidParams;
}
break :blk null;
};
const domain = try Cookie.parseDomain(a, maybe_url, param.domain);
const path = if (param.path == null) "/" else try Cookie.parsePath(a, null, param.path); const path = if (param.path == null) "/" else try Cookie.parsePath(a, null, param.path);
const secure = if (param.secure) |s| s else if (uri) |uri_| std.mem.eql(u8, uri_.scheme, "https") else false; const secure: bool = blk: {
// Check if params indicate security.
if (param.secure) |s| break :blk s;
// Check if protocol is secure.
if (maybe_url) |url| break :blk url.isSecure();
// If all fails, insecure.
break :blk false;
};
const cookie = Cookie{ const cookie = Cookie{
.arena = arena, .arena = arena,
@@ -158,6 +172,7 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
.None => .none, .None => .none,
}, },
}; };
try cookie_jar.add(cookie, std.time.timestamp()); try cookie_jar.add(cookie, std.time.timestamp());
} }

View File

@@ -22,9 +22,9 @@ const builtin = @import("builtin");
const Http = @import("Http.zig"); const Http = @import("Http.zig");
const Notification = @import("../notification.zig").Notification; const Notification = @import("../notification.zig").Notification;
const CookieJar = @import("../browser/storage/storage.zig").CookieJar; const CookieJar = @import("../browser/storage/cookie.zig").Jar;
const URL = @import("../url.zig").URL;
const urlStitch = @import("../url.zig").stitch; const urlStitch = URL.stitch;
const c = Http.c; const c = Http.c;
const posix = std.posix; const posix = std.posix;
@@ -259,7 +259,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer {
errdefer req.headers.deinit(); errdefer req.headers.deinit();
// we need this for cookies // we need this for cookies
const uri = std.Uri.parse(req.url) catch |err| { const url = URL.parse(req.url, null) catch |err| {
log.warn(.http, "invalid url", .{ .err = err, .url = req.url }); log.warn(.http, "invalid url", .{ .err = err, .url = req.url });
return err; return err;
}; };
@@ -272,7 +272,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer {
transfer.* = .{ transfer.* = .{
.arena = ArenaAllocator.init(self.allocator), .arena = ArenaAllocator.init(self.allocator),
.id = id, .id = id,
.uri = uri, .url = url,
.req = req, .req = req,
.ctx = req.ctx, .ctx = req.ctx,
.client = self, .client = self,
@@ -595,20 +595,20 @@ pub const Handle = struct {
pub const RequestCookie = struct { pub const RequestCookie = struct {
is_http: bool, is_http: bool,
is_navigation: bool, is_navigation: bool,
origin: *const std.Uri, origin_url: URL,
jar: *@import("../browser/storage/cookie.zig").Jar, cookie_jar: *CookieJar,
pub fn headersForRequest(self: *const RequestCookie, temp: Allocator, url: [:0]const u8, headers: *Http.Headers) !void { pub fn headersForRequest(self: *const RequestCookie, temp: Allocator, url_str: [:0]const u8, headers: *Http.Headers) !void {
const uri = std.Uri.parse(url) catch |err| { const url = URL.parse(url_str, null) catch |err| {
log.warn(.http, "invalid url", .{ .err = err, .url = url }); log.warn(.http, "invalid url", .{ .err = err, .url = url_str });
return error.InvalidUrl; return error.InvalidUrl;
}; };
var arr: std.ArrayListUnmanaged(u8) = .{}; var arr: std.ArrayListUnmanaged(u8) = .{};
try self.jar.forRequest(&uri, arr.writer(temp), .{ try self.cookie_jar.forRequest(url, arr.writer(temp), .{
.is_http = self.is_http, .is_http = self.is_http,
.is_navigation = self.is_navigation, .is_navigation = self.is_navigation,
.origin_uri = self.origin, .origin_url = self.origin_url,
}); });
if (arr.items.len > 0) { if (arr.items.len > 0) {
@@ -688,7 +688,7 @@ pub const Transfer = struct {
arena: ArenaAllocator, arena: ArenaAllocator,
id: usize = 0, id: usize = 0,
req: Request, req: Request,
uri: std.Uri, // used for setting/getting the cookie url: URL, // used for setting/getting the cookie
ctx: *anyopaque, // copied from req.ctx to make it easier for callback handlers ctx: *anyopaque, // copied from req.ctx to make it easier for callback handlers
client: *Client, client: *Client,
// total bytes received in the response, including the response status line, // total bytes received in the response, including the response status line,
@@ -774,7 +774,7 @@ pub const Transfer = struct {
pub fn updateURL(self: *Transfer, url: [:0]const u8) !void { pub fn updateURL(self: *Transfer, url: [:0]const u8) !void {
// for cookies // for cookies
self.uri = try std.Uri.parse(url); self.url = try self.url.reparse(url);
// for the request itself // for the request itself
self.req.url = url; self.req.url = url;
@@ -833,7 +833,7 @@ pub const Transfer = struct {
while (true) { while (true) {
const ct = getResponseHeader(easy, "set-cookie", i); const ct = getResponseHeader(easy, "set-cookie", i);
if (ct == null) break; if (ct == null) break;
try req.cookie_jar.populateFromResponse(&transfer.uri, ct.?.value); try req.cookie_jar.populateFromResponse(transfer.url, ct.?.value);
i += 1; i += 1;
if (i >= ct.?.amount) break; if (i >= ct.?.amount) break;
} }
@@ -847,14 +847,16 @@ pub const Transfer = struct {
var baseurl: [*c]u8 = undefined; var baseurl: [*c]u8 = undefined;
try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_EFFECTIVE_URL, &baseurl)); try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_EFFECTIVE_URL, &baseurl));
const url = try urlStitch(arena, hlocation.?.value, std.mem.span(baseurl), .{}); const stitched = try urlStitch(arena, hlocation.?.value, std.mem.span(baseurl), .{});
const uri = try std.Uri.parse(url); // Since we're being redirected, we know url is valid.
transfer.uri = uri; // An assertation won't hurt, though.
std.debug.assert(transfer.url.isValid());
_ = try transfer.url.reparse(stitched);
var cookies: std.ArrayListUnmanaged(u8) = .{}; var cookies: std.ArrayListUnmanaged(u8) = .{};
try req.cookie_jar.forRequest(&uri, cookies.writer(arena), .{ try req.cookie_jar.forRequest(transfer.url, cookies.writer(arena), .{
.is_http = true, .is_http = true,
.origin_uri = &transfer.uri, .origin_url = transfer.url,
// used to enforce samesite cookie rules // used to enforce samesite cookie rules
.is_navigation = req.resource_type == .document, .is_navigation = req.resource_type == .document,
}); });
@@ -883,7 +885,7 @@ pub const Transfer = struct {
while (true) { while (true) {
const ct = getResponseHeader(easy, "set-cookie", i); const ct = getResponseHeader(easy, "set-cookie", i);
if (ct == null) break; if (ct == null) break;
transfer.req.cookie_jar.populateFromResponse(&transfer.uri, ct.?.value) catch |err| { transfer.req.cookie_jar.populateFromResponse(transfer.url, ct.?.value) catch |err| {
log.err(.http, "set cookie", .{ .err = err, .req = transfer }); log.err(.http, "set cookie", .{ .err = err, .req = transfer });
return err; return err;
}; };

View File

@@ -68,10 +68,10 @@
let a_again = document.elementFromPoint(1.5, 0.5); let a_again = document.elementFromPoint(1.5, 0.5);
testing.expectEqual('[object HTMLAnchorElement]', a_again.toString()); testing.expectEqual('[object HTMLAnchorElement]', a_again.toString());
testing.expectEqual('https://lightpanda.io', a_again.href); testing.expectEqual('https://lightpanda.io/', a_again.href);
let a_agains = document.elementsFromPoint(1.5, 0.5); let a_agains = document.elementsFromPoint(1.5, 0.5);
testing.expectEqual('https://lightpanda.io', a_agains[0].href); testing.expectEqual('https://lightpanda.io/', a_agains[0].href);
testing.expectEqual(true, !document.all); testing.expectEqual(true, !document.all);

View File

@@ -33,6 +33,7 @@
testing.expectEqual('', a.host); testing.expectEqual('', a.host);
a.href = 'about'; a.href = 'about';
testing.expectEqual('http://localhost:9582/src/tests/html/about', a.href); testing.expectEqual('http://localhost:9582/src/tests/html/about', a.href);
testing.expectEqual(true, true);
</script> </script>
<script id=focus> <script id=focus>

View File

@@ -16,8 +16,10 @@
testing.expectEqual('https://lightpanda.io', link.origin); testing.expectEqual('https://lightpanda.io', link.origin);
link.host = 'lightpanda.io:443'; link.host = 'lightpanda.io:443';
testing.expectEqual('lightpanda.io:443', link.host); // Port is omitted if its the default one for the scheme.
testing.expectEqual('443', link.port); testing.expectEqual('lightpanda.io', link.host);
// Port is omitted if its the default one for the scheme.
testing.expectEqual('', link.port);
testing.expectEqual('lightpanda.io', link.hostname); testing.expectEqual('lightpanda.io', link.hostname);
link.host = 'lightpanda.io'; link.host = 'lightpanda.io';
@@ -42,9 +44,10 @@
testing.expectEqual('', link.port); testing.expectEqual('', link.port);
link.port = '443'; link.port = '443';
testing.expectEqual('foo.bar:443', link.host); // Port is omitted if its the default one for the scheme.
testing.expectEqual('foo.bar', link.host);
testing.expectEqual('foo.bar', link.hostname); testing.expectEqual('foo.bar', link.hostname);
testing.expectEqual('https://foo.bar:443/?q=bar#frag', link.href); testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);
link.port = null; link.port = null;
testing.expectEqual('https://foo.bar/?q=bar#frag', link.href); testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);

View File

@@ -76,8 +76,3 @@
testing.expectEqual("", sk.hostname); testing.expectEqual("", sk.hostname);
testing.expectEqual("sveltekit-internal://", sk.href); testing.expectEqual("sveltekit-internal://", sk.href);
</script> </script>
<script id=invalidUrl>
let u = new URL("://foo.bar/path?query#fragment");
testing.expectEqual(":", u.protocol);
</script>

View File

@@ -1,82 +1,233 @@
const std = @import("std"); const std = @import("std");
const Uri = std.Uri;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const WebApiURL = @import("browser/url/url.zig").URL; const WebApiURL = @import("browser/url/url.zig").URL;
const ada = @import("ada");
pub const stitch = URL.stitch; pub const stitch = URL.stitch;
pub const URL = struct { pub const URL = struct {
uri: Uri, /// Internal ada structure.
raw: []const u8, internal: ada.URL,
pub const empty = URL{ .uri = .{ .scheme = "" }, .raw = "" }; pub const ParseError = ada.ParseError;
pub const about_blank = URL{ .uri = .{ .scheme = "" }, .raw = "about:blank" };
// We assume str will last as long as the URL /// Creates a new URL by parsing given `input`.
// In some cases, this is safe to do, because we know the URL is short lived. /// `input` will be duped; so it can be freed after a call to this function.
// In most cases though, we assume the caller will just dupe the string URL /// If `input` does not contain a scheme, `fallback_scheme` be used instead.
// into an arena /// `fallback_scheme` is `https` if not provided.
pub fn parse(str: []const u8, default_scheme: ?[]const u8) !URL { pub fn parse(input: []const u8, fallback_scheme: ?[]const u8) ParseError!URL {
var uri = Uri.parse(str) catch try Uri.parseAfterScheme(default_scheme orelse "https", str); // Try parsing directly; if it fails, we might have to provide a base.
const internal = ada.parse(input) catch blk: {
// special case, url scheme is about, like about:blank. break :blk try ada.parseWithBase(fallback_scheme orelse "https", input);
// Use an empty string as host.
if (std.mem.eql(u8, uri.scheme, "about")) {
uri.host = .{ .percent_encoded = "" };
}
if (uri.host == null) {
return error.MissingHost;
}
std.debug.assert(uri.host.? == .percent_encoded);
return .{
.uri = uri,
.raw = str,
}; };
return .{ .internal = internal };
} }
pub fn fromURI(arena: Allocator, uri: *const Uri) !URL { pub fn parseWithBase(input: []const u8, base: []const u8) ParseError!URL {
// This is embarrassing. const internal = try ada.parseWithBase(input, base);
var buf: std.ArrayListUnmanaged(u8) = .{}; return .{ .internal = internal };
try uri.writeToStream(.{
.scheme = true,
.authentication = true,
.authority = true,
.path = true,
.query = true,
.fragment = true,
}, buf.writer(arena));
return parse(buf.items, null);
} }
// Above, in `parse`, we error if a host doesn't exist /// Uses the same URL to parse in-place.
// In other words, we can't have a URL with a null host. /// Assumes `internal` is valid.
pub fn host(self: *const URL) []const u8 { pub fn reparse(self: URL, str: []const u8) ParseError!URL {
return self.uri.host.?.percent_encoded; std.debug.assert(self.internal != null);
_ = ada.setHref(self.internal, str);
if (!ada.isValid(self.internal)) {
return error.Invalid;
} }
pub fn port(self: *const URL) ?u16 { return self;
return self.uri.port;
} }
pub fn scheme(self: *const URL) []const u8 { /// Forms a `URL` from given `internal`. Memory is not copied.
return self.uri.scheme; pub fn fromInternal(internal: ada.URL) URL {
return .{ .internal = internal };
} }
pub fn origin(self: *const URL, writer: *std.Io.Writer) !void { /// Deinitializes internal url.
return self.uri.writeToStream(writer, .{ .scheme = true, .authority = true }); pub fn deinit(self: URL) void {
std.debug.assert(self.internal != null);
ada.free(self.internal);
} }
pub fn format(self: *const URL, writer: *std.Io.Writer) !void { /// Returns true if `internal` is initialized.
return writer.writeAll(self.raw); pub fn isValid(self: URL) bool {
return ada.isValid(self.internal);
} }
pub fn toWebApi(self: *const URL, allocator: Allocator) !WebApiURL { pub fn setHost(self: URL, host_str: []const u8) error{InvalidHost}!void {
return WebApiURL.init(allocator, self.uri); const is_set = ada.setHost(self.internal, host_str);
if (!is_set) return error.InvalidHost;
}
pub fn setPort(self: URL, port_str: []const u8) error{InvalidPort}!void {
const is_set = ada.setPort(self.internal, port_str);
if (!is_set) return error.InvalidPort;
}
pub fn getPort(self: URL) []const u8 {
const port = ada.getPortNullable(self.internal);
return port.data[0..port.length];
}
/// Above, in `parse`, we error if a host doesn't exist
/// In other words, we can't have a URL with a null host.
pub fn host(self: URL) []const u8 {
const str = ada.getHostNullable(self.internal);
if (str.data == null) {
return "";
}
return str.data[0..str.length];
}
pub fn getHref(self: URL) []const u8 {
const href = ada.getHrefNullable(self.internal);
if (href.data == null) {
return "";
}
return href.data[0..href.length];
}
pub fn getHostname(self: URL) []const u8 {
const hostname = ada.getHostnameNullable(self.internal);
return hostname.data[0..hostname.length];
}
pub fn setHostname(self: URL, hostname_str: []const u8) error{InvalidHostname}!void {
const is_set = ada.setHostname(self.internal, hostname_str);
if (!is_set) return error.InvalidHostname;
}
pub fn getUsername(self: URL) ?[]const u8 {
const username = ada.getUsernameNullable(self.internal);
if (username.data == null) return null;
return username.data[0..username.length];
}
pub fn setUsername(self: URL, username: []const u8) error{InvalidUsername}!void {
const is_set = ada.setUsername(self.internal, username);
if (!is_set) return error.InvalidUsername;
}
pub fn getPassword(self: URL) ?[]const u8 {
const password = ada.getPasswordNullable(self.internal);
if (password.data == null) return null;
return password.data[0..password.length];
}
pub fn setPassword(self: URL, password: []const u8) error{InvalidPassword}!void {
const is_set = ada.setPassword(self.internal, password);
if (!is_set) return error.InvalidPassword;
}
pub fn getFragment(self: URL) ?[]const u8 {
// Ada calls it "hash" instead of "fragment".
const hash = ada.getHashNullable(self.internal);
if (hash.data == null) return null;
return hash.data[0..hash.length];
}
pub fn getSearch(self: URL) ?[]const u8 {
const search = ada.getSearchNullable(self.internal);
if (search.data == null) return null;
return search.data[0..search.length];
}
pub fn setSearch(self: URL, search: []const u8) void {
return ada.setSearch(self.internal, search);
}
pub fn getHash(self: URL) ?[]const u8 {
const hash = ada.getHashNullable(self.internal);
if (hash.data == null) return null;
return hash.data[0..hash.length];
}
pub fn setHash(self: URL, hash: []const u8) void {
return ada.setHash(self.internal, hash);
}
pub fn getProtocol(self: URL) []const u8 {
return ada.getProtocol(self.internal);
}
pub fn setProtocol(self: URL, protocol_str: []const u8) error{InvalidProtocol}!void {
const is_set = ada.setProtocol(self.internal, protocol_str);
if (!is_set) return error.InvalidProtocol;
}
pub fn getScheme(self: URL) []const u8 {
const proto = self.getProtocol();
std.debug.assert(proto[proto.len - 1] == ':');
return proto.ptr[0 .. proto.len - 1];
}
/// Returns the path.
pub fn getPath(self: URL) []const u8 {
const pathname = ada.getPathnameNullable(self.internal);
// Return a slash if path is null.
if (pathname.data == null) {
return "/";
}
return pathname.data[0..pathname.length];
}
pub fn setPath(self: URL, path: []const u8) error{InvalidPath}!void {
const is_set = ada.setPathname(self.internal, path);
if (!is_set) return error.InvalidPath;
}
/// Returns true if the URL's protocol is secure.
pub fn isSecure(self: URL) bool {
const scheme = ada.getSchemeType(self.internal);
return scheme == ada.Scheme.https or scheme == ada.Scheme.wss;
}
pub fn writeToStream(self: URL, writer: anytype) !void {
return writer.writeAll(self.getHref());
}
/// Returns the origin string; caller owns the memory.
pub fn getOrigin(self: URL, allocator: Allocator) ![]const u8 {
const s = ada.getOriginNullable(self.internal);
if (s.data == null) {
return "";
}
defer ada.freeOwnedString(.{ .data = s.data, .length = s.length });
return allocator.dupe(u8, s.data[0..s.length]);
}
// TODO: Skip unnecessary allocation by writing url parts directly to stream.
pub fn origin(self: URL, writer: *std.Io.Writer) !void {
// Ada manages its own memory for origin.
// Here we write it to stream and free it afterwards.
const s = ada.getOriginNullable(self.internal);
if (s.data == null) {
return;
}
defer ada.freeOwnedString(.{ .data = s.data, .length = s.length });
return writer.writeAll(s.data[0..s.length]);
}
pub fn format(self: URL, writer: *std.Io.Writer) !void {
return self.writeToStream(writer);
}
/// Converts `URL` to `WebApiURL`.
pub fn toWebApi(self: URL, allocator: Allocator) !WebApiURL {
return WebApiURL.constructFromInternal(allocator, self.internal);
} }
/// Properly stitches two URL fragments together. /// Properly stitches two URL fragments together.

225
vendor/ada/root.zig vendored Normal file
View File

@@ -0,0 +1,225 @@
//! Wrappers for ada URL parser.
//! https://github.com/ada-url/ada
const c = @cImport({
@cInclude("ada_c.h");
});
/// Pointer type.
pub const URL = c.ada_url;
pub const URLComponents = c.ada_url_components;
pub const URLOmitted = c.ada_url_omitted;
pub const String = c.ada_string;
pub const OwnedString = c.ada_owned_string;
/// Pointer type.
pub const URLSearchParams = c.ada_url_search_params;
pub const ParseError = error{Invalid};
pub fn parse(input: []const u8) ParseError!URL {
const url = c.ada_parse(input.ptr, input.len);
if (!c.ada_is_valid(url)) {
return error.Invalid;
}
return url;
}
pub fn parseWithBase(input: []const u8, base: []const u8) ParseError!URL {
const url = c.ada_parse_with_base(input.ptr, input.len, base.ptr, base.len);
if (!c.ada_is_valid(url)) {
return error.Invalid;
}
return url;
}
pub inline fn getComponents(url: URL) *const URLComponents {
return c.ada_get_components(url);
}
pub inline fn free(url: URL) void {
return c.ada_free(url);
}
pub inline fn freeOwnedString(owned: OwnedString) void {
return c.ada_free_owned_string(owned);
}
/// Returns true if given URL is valid.
pub inline fn isValid(url: URL) bool {
return c.ada_is_valid(url);
}
/// Creates a new `URL` from given `URL`.
pub inline fn copy(url: URL) URL {
return c.ada_copy(url);
}
/// Can return an empty string.
/// Contrary to other getters, returned slice is heap allocated.
pub inline fn getOrigin(url: URL) []const u8 {
const origin = c.ada_get_origin(url);
return origin.data[0..origin.length];
}
pub inline fn getOriginNullable(url: URL) OwnedString {
return c.ada_get_origin(url);
}
pub inline fn getHrefNullable(url: URL) String {
return c.ada_get_href(url);
}
pub inline fn getUsernameNullable(url: URL) String {
return c.ada_get_username(url);
}
/// Can return an empty string.
pub inline fn getUsername(url: URL) []const u8 {
const username = c.ada_get_username(url);
return username.data[0..username.length];
}
pub inline fn getPasswordNullable(url: URL) String {
return c.ada_get_password(url);
}
pub inline fn getSearchNullable(url: URL) String {
return c.ada_get_search(url);
}
/// Can return an empty string.
pub inline fn getPassword(url: URL) []const u8 {
const password = c.ada_get_password(url);
return password.data[0..password.length];
}
pub inline fn getPortNullable(url: URL) String {
return c.ada_get_port(url);
}
pub inline fn getPort(url: URL) []const u8 {
if (!c.ada_has_port(url)) {
return "";
}
const port = c.ada_get_port(url);
return port.data[0..port.length];
}
pub inline fn getHash(url: URL) []const u8 {
const hash = c.ada_get_hash(url);
return hash.data[0..hash.length];
}
pub inline fn getHashNullable(url: URL) String {
return c.ada_get_hash(url);
}
/// `data` is null if host not provided.
pub inline fn getHostNullable(url: URL) String {
return c.ada_get_host(url);
}
/// Returns an empty string if host not provided.
pub inline fn getHost(url: URL) []const u8 {
const host = getHostNullable(url);
if (host.data == null) {
return "";
}
return host.data[0..host.length];
}
pub inline fn getHostname(url: URL) []const u8 {
if (!c.ada_has_hostname(url)) {
return "";
}
const hostname = c.ada_get_hostname(url);
return hostname.data[0..hostname.length];
}
pub inline fn getHostnameNullable(url: URL) String {
return c.ada_get_hostname(url);
}
pub inline fn getPathnameNullable(url: URL) String {
return c.ada_get_pathname(url);
}
pub inline fn getPathname(url: URL) []const u8 {
const pathname = c.ada_get_pathname(url);
return pathname.data[0..pathname.length];
}
pub inline fn getSearch(url: URL) String {
return c.ada_get_search(url);
}
pub inline fn getProtocol(url: URL) []const u8 {
const protocol = c.ada_get_protocol(url);
return protocol.data[0..protocol.length];
}
/// Sets the href for given URL.
/// Call `isInvalid` afterwards to check correctness.
pub inline fn setHref(url: URL, input: []const u8) bool {
return c.ada_set_href(url, input.ptr, input.len);
}
pub inline fn setHost(url: URL, input: []const u8) bool {
return c.ada_set_host(url, input.ptr, input.len);
}
pub inline fn setHostname(url: URL, input: []const u8) bool {
return c.ada_set_hostname(url, input.ptr, input.len);
}
pub inline fn setProtocol(url: URL, input: []const u8) bool {
return c.ada_set_protocol(url, input.ptr, input.len);
}
pub inline fn setUsername(url: URL, input: []const u8) bool {
return c.ada_set_username(url, input.ptr, input.len);
}
pub inline fn setPassword(url: URL, input: []const u8) bool {
return c.ada_set_password(url, input.ptr, input.len);
}
pub inline fn setPort(url: URL, input: []const u8) bool {
return c.ada_set_port(url, input.ptr, input.len);
}
pub inline fn setPathname(url: URL, input: []const u8) bool {
return c.ada_set_pathname(url, input.ptr, input.len);
}
pub inline fn setSearch(url: URL, input: []const u8) void {
return c.ada_set_search(url, input.ptr, input.len);
}
pub inline fn setHash(url: URL, input: []const u8) void {
return c.ada_set_hash(url, input.ptr, input.len);
}
pub inline fn clearSearch(url: URL) void {
return c.ada_clear_search(url);
}
pub const Scheme = struct {
pub const http: u8 = 0;
pub const not_special: u8 = 1;
pub const https: u8 = 2;
pub const ws: u8 = 3;
pub const ftp: u8 = 4;
pub const wss: u8 = 5;
pub const file: u8 = 6;
};
/// Returns one of the constants defined in `Scheme`.
pub inline fn getSchemeType(url: URL) u8 {
return c.ada_get_scheme_type(url);
}