From 25a1d588a90067574f211f3638cafab27b771a27 Mon Sep 17 00:00:00 2001 From: nikneym Date: Thu, 2 Oct 2025 18:23:30 +0300 Subject: [PATCH 01/25] integrate ada-url dependency to build system --- build.zig | 25 +++++++++++++++++++++++++ build.zig.zon | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/build.zig b/build.zig index 3437dfad0..9d4594fb4 100644 --- a/build.zig +++ b/build.zig @@ -384,6 +384,7 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo try buildMbedtls(b, mod); try buildNghttp2(b, mod); try buildCurl(b, mod); + try buildAda(b, mod); switch (target.result.os.tag) { .macos => { @@ -849,3 +850,27 @@ 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 = b.addLibrary(.{ + .name = "ada", + .linkage = .static, + .root_module = b.createModule(.{ + .target = m.resolved_target, + .optimize = m.optimize, + .link_libcpp = true, + }), + }); + + // Expose "ada_c.h". + ada.installHeader(ada_dep.path("ada_c.h"), "ada_c.h"); + + ada.root_module.addCSourceFiles(.{ + .root = ada_dep.path(""), + .files = &.{"ada.cpp"}, + .flags = &.{"-std=c++20"}, + }); + + m.linkLibrary(ada); +} diff --git a/build.zig.zon b/build.zig.zon index 9d57095f9..6e4f544b9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,5 +9,9 @@ .hash = "v8-0.0.0-xddH63bVAwBSEobaUok9J0er1FqsvEujCDDVy6ItqKQ5", }, //.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", + }, }, } From 8f99e36cde514ee9eeba41f311b082f527d7eaf3 Mon Sep 17 00:00:00 2001 From: nikneym Date: Sun, 5 Oct 2025 11:02:05 +0300 Subject: [PATCH 02/25] add ada-url wrappers * also integrate it as module in build.zig rather than direct linking --- build.zig | 26 +++++------ vendor/ada/root.zig | 103 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 vendor/ada/root.zig diff --git a/build.zig b/build.zig index 9d4594fb4..61667069c 100644 --- a/build.zig +++ b/build.zig @@ -853,24 +853,24 @@ 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 = b.addLibrary(.{ - .name = "ada", - .linkage = .static, - .root_module = b.createModule(.{ - .target = m.resolved_target, - .optimize = m.optimize, - .link_libcpp = true, - }), + const dep_root = ada_dep.path(""); + + // Private module that binds ada functions. + const ada_mod = b.createModule(.{ + .root_source_file = b.path("vendor/ada/root.zig"), + .target = m.resolved_target, + .optimize = m.optimize, + .link_libcpp = true, }); - // Expose "ada_c.h". - ada.installHeader(ada_dep.path("ada_c.h"), "ada_c.h"); + // Expose headers; note that "ada.h" is a C++ header so no use here. + ada_mod.addIncludePath(dep_root); - ada.root_module.addCSourceFiles(.{ - .root = ada_dep.path(""), + ada_mod.addCSourceFiles(.{ + .root = dep_root, .files = &.{"ada.cpp"}, .flags = &.{"-std=c++20"}, }); - m.linkLibrary(ada); + m.addImport("ada", ada_mod); } diff --git a/vendor/ada/root.zig b/vendor/ada/root.zig new file mode 100644 index 000000000..4deaec699 --- /dev/null +++ b/vendor/ada/root.zig @@ -0,0 +1,103 @@ +//! 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 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)) { + free(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)) { + free(url); + return error.Invalid; + } + + return 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); +} + +/// 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]; +} + +/// Can return an empty string. +pub inline fn getHref(url: URL) []const u8 { + const href = c.ada_get_href(url); + return href.data[0..href.length]; +} + +/// 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]; +} + +/// 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 getPort(url: URL) []const u8 { + 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 getHost(url: URL) []const u8 { + const host = c.ada_get_host(url); + return host.data[0..host.length]; +} + +pub inline fn getHostname(url: URL) []const u8 { + const hostname = c.ada_get_hostname(url); + return hostname.data[0..hostname.length]; +} + +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) []const u8 { + const search = c.ada_get_search(url); + return search.data[0..search.length]; +} + +pub inline fn getProtocol(url: URL) []const u8 { + const protocol = c.ada_get_protocol(url); + return protocol.data[0..protocol.length]; +} From 45cd4942986e5390d9fb51ddc46d0e1ac0b98c04 Mon Sep 17 00:00:00 2001 From: nikneym Date: Sun, 5 Oct 2025 11:02:53 +0300 Subject: [PATCH 03/25] initial `URL` refactor --- src/browser/url/url.zig | 331 ++++++++-------------------------------- 1 file changed, 62 insertions(+), 269 deletions(-) diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig index 08c97bf61..123660c9b 100644 --- a/src/browser/url/url.zig +++ b/src/browser/url/url.zig @@ -18,6 +18,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const ada = @import("ada"); const js = @import("../js/js.zig"); const parser = @import("../netsurf.zig"); @@ -35,215 +36,113 @@ pub const Interfaces = .{ EntryIterable, }; -// https://url.spec.whatwg.org/#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. +/// https://developer.mozilla.org/en-US/docs/Web/API/URL/URL pub const URL = struct { - uri: std.Uri, - search_params: URLSearchParams, + internal: ada.URL, - pub const empty = URL{ - .uri = .{ .scheme = "" }, - .search_params = .{}, - }; - - const URLArg = union(enum) { + // You can use an existing URL object for either argument, and it will be + // stringified from the object's href property. + pub const ConstructorArg = union(enum) { url: *URL, - element: *parser.ElementHTML, + element: *parser.Element, string: []const u8, - fn toString(self: URLArg, arena: Allocator) !?[]const u8 { - switch (self) { - .string => |s| return s, - .url => |url| return try url.toString(arena), - .element => |e| return try parser.elementGetAttribute(@ptrCast(e), "href"), - } + fn toString(self: *const ConstructorArg) error{Invalid}![]const u8 { + return switch (self) { + .string => |s| s, + .url => |url| url._toString(), + .element => |e| parser.elementGetAttribute(@ptrCast(e), "href") orelse error.Invalid, + }; } }; - pub fn constructor(url: URLArg, base: ?URLArg, page: *Page) !URL { - const arena = page.arena; - const url_str = try url.toString(arena) orelse return error.InvalidArgument; - - var raw: ?[]const u8 = null; - if (base) |b| { - if (try b.toString(arena)) |bb| { - raw = try @import("../../url.zig").URL.stitch(arena, url_str, bb, .{}); + pub fn constructor(url: ConstructorArg, maybe_base: ?ConstructorArg, _: *Page) !URL { + const u = blk: { + const url_str = try url.toString(); + if (maybe_base) |base| { + break :blk ada.parseWithBase(url_str, try base.toString()); } - } - - if (raw == null) { - // 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 = "" }, - }; + break :blk ada.parse(url_str); }; - return init(arena, uri); + return .{ .url = u }; } - pub fn init(arena: Allocator, uri: std.Uri) !URL { - return .{ - .uri = uri, - .search_params = try URLSearchParams.init( - arena, - uriComponentNullStr(uri.query), - ), - }; + pub fn destructor(self: *const URL) void { + ada.free(self.internal); } pub fn initWithoutSearchParams(uri: std.Uri) URL { return .{ .uri = uri, .search_params = .{} }; } - - pub fn get_origin(self: *URL, page: *Page) ![]const u8 { - var aw = std.Io.Writer.Allocating.init(page.arena); - try self.uri.writeToStream(&aw.writer, .{ - .scheme = true, - .authentication = false, - .authority = true, - .path = false, - .query = false, - .fragment = false, - }); - return aw.written(); + pub fn _toString(self: *const URL) []const u8 { + return ada.getHref(self.internal); } - // get_href returns the URL by writing all its components. - pub fn get_href(self: *URL, page: *Page) ![]const u8 { - return self.toString(page.arena); - } + // Getters. - pub fn _toString(self: *URL, page: *Page) ![]const u8 { - return self.toString(page.arena); - } - - // format the url with all its components. - 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) { - try aw.writer.writeByte('?'); - try self.search_params.write(&aw.writer); - } - - { - const fragment = uriComponentNullStr(self.uri.fragment); - if (fragment.len > 0) { - try aw.writer.writeByte('#'); - try aw.writer.writeAll(fragment); - } - } + pub fn get_origin(self: *const URL, page: *Page) ![]const u8 { + const arena = page.arena; + // `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 }); - return aw.written(); + return arena.dupe(u8, origin); } - 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_href(self: *const URL) []const u8 { + return ada.getHref(self.internal); } - pub fn get_username(self: *URL) []const u8 { - return uriComponentNullStr(self.uri.user); + pub fn get_username(self: *const URL) []const u8 { + return ada.getUsername(self.internal); } - pub fn get_password(self: *URL) []const u8 { - return uriComponentNullStr(self.uri.password); + pub fn get_password(self: *const URL) []const u8 { + return ada.getPassword(self.internal); } - 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_port(self: *const URL) []const u8 { + return ada.getPort(self.internal); } - pub fn get_hostname(self: *URL) []const u8 { - return uriComponentNullStr(self.uri.host); + pub fn get_hash(self: *const URL) []const u8 { + return ada.getHash(self.internal); } - pub fn get_port(self: *URL, page: *Page) ![]const u8 { - const arena = page.arena; - if (self.uri.port == null) return try arena.dupe(u8, ""); - - var aw = std.Io.Writer.Allocating.init(arena); - try aw.writer.printInt(self.uri.port.?, 10, .lower, .{}); - return aw.written(); + pub fn get_host(self: *const URL) []const u8 { + return ada.getHost(self.internal); } - pub fn get_pathname(self: *URL) []const u8 { - if (uriComponentStr(self.uri.path).len == 0) return "/"; - return uriComponentStr(self.uri.path); + pub fn get_hostname(self: *const URL) []const u8 { + return ada.getHostname(self.internal); } - pub fn get_search(self: *URL, page: *Page) ![]const u8 { - const arena = page.arena; - - if (self.search_params.get_size() == 0) { - return ""; - } - - var buf: std.ArrayListUnmanaged(u8) = .{}; - try buf.append(arena, '?'); - try self.search_params.encode(buf.writer(arena)); - return buf.items; + pub fn get_pathname(self: *const URL) []const u8 { + return ada.getPathname(self.internal); } - pub fn set_search(self: *URL, qs_: ?[]const u8, page: *Page) !void { - self.search_params = .{}; - if (qs_) |qs| { - self.search_params = try URLSearchParams.init(page.arena, qs); - } + pub fn get_search(self: *const URL) []const u8 { + return ada.getSearch(self.internal); } - pub fn get_hash(self: *URL, page: *Page) ![]const u8 { - const arena = page.arena; - 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_protocol(self: *const URL) []const u8 { + return ada.getProtocol(self.internal); } +}; - pub fn get_searchParams(self: *URL) *URLSearchParams { - return &self.search_params; - } +pub const URLSearchParams = struct { + internal: ada.URLSearchParams, - pub fn _toJSON(self: *URL, page: *Page) ![]const u8 { - return self.get_href(page); - } + pub const ConstructorOptions = union(enum) { + string: []const u8, + form_data: *const FormData, + object: js.JsObject, + }; }; // uriComponentNullStr converts an optional std.Uri.Component to string value. @@ -261,112 +160,6 @@ fn uriComponentStr(c: std.Uri.Component) []const u8 { }; } -// https://url.spec.whatwg.org/#interface-urlsearchparams -pub const URLSearchParams = struct { - entries: kv.List = .{}, - - const URLSearchParamsOpts = union(enum) { - qs: []const u8, - form_data: *const FormData, - js_obj: 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 = .{}; - try entries.ensureTotalCapacity(arena, it.count); - - while (try it.next()) |js_name| { - const name = try js_name.toString(arena); - const js_val = try js_obj.get(name); - entries.appendOwnedAssumeCapacity( - name, - try js_val.toString(arena), - ); - } - - return .{ .entries = entries }; - }, - }; - } - - pub fn init(arena: Allocator, qs_: ?[]const u8) !URLSearchParams { - return .{ - .entries = if (qs_) |qs| try parseQuery(arena, qs) else .{}, - }; - } - - pub fn get_size(self: *const URLSearchParams) u32 { - return @intCast(self.entries.count()); - } - - pub fn _append(self: *URLSearchParams, name: []const u8, value: []const u8, page: *Page) !void { - return self.entries.append(page.arena, name, value); - } - - pub fn _set(self: *URLSearchParams, name: []const u8, value: []const u8, page: *Page) !void { - return self.entries.set(page.arena, name, value); - } - - pub fn _delete(self: *URLSearchParams, name: []const u8, value_: ?[]const u8) void { - if (value_) |value| { - return self.entries.deleteKeyValue(name, value); - } - return self.entries.delete(name); - } - - pub fn _get(self: *const URLSearchParams, name: []const u8) ?[]const u8 { - return self.entries.get(name); - } - - pub fn _getAll(self: *const URLSearchParams, name: []const u8, page: *Page) ![]const []const u8 { - return self.entries.getAll(page.call_arena, name); - } - - pub fn _has(self: *const URLSearchParams, name: []const u8) bool { - return self.entries.has(name); - } - - pub fn _keys(self: *const URLSearchParams) KeyIterable { - return .{ .inner = self.entries.keyIterator() }; - } - - pub fn _values(self: *const URLSearchParams) ValueIterable { - return .{ .inner = self.entries.valueIterator() }; - } - - pub fn _entries(self: *const URLSearchParams) EntryIterable { - return .{ .inner = self.entries.entryIterator() }; - } - - pub fn _symbol_iterator(self: *const URLSearchParams) EntryIterable { - return self._entries(); - } - - pub fn _toString(self: *const URLSearchParams, page: *Page) ![]const u8 { - var arr: std.ArrayListUnmanaged(u8) = .empty; - try self.write(arr.writer(page.call_arena)); - return arr.items; - } - - fn write(self: *const URLSearchParams, writer: anytype) !void { - return kv.urlEncode(self.entries, .query, writer); - } - - // TODO - pub fn _sort(_: *URLSearchParams) void {} - - fn encode(self: *const URLSearchParams, writer: anytype) !void { - return kv.urlEncode(self.entries, .query, writer); - } -}; - // Parse the given query. fn parseQuery(arena: Allocator, s: []const u8) !kv.List { var list = kv.List{}; From 1c7971b0969049074f1723a4574ddaf2918b8250 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 7 Oct 2025 17:11:51 +0300 Subject: [PATCH 04/25] bind more ada-url functions --- vendor/ada/root.zig | 73 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/vendor/ada/root.zig b/vendor/ada/root.zig index 4deaec699..64d48b83b 100644 --- a/vendor/ada/root.zig +++ b/vendor/ada/root.zig @@ -7,6 +7,8 @@ const c = @cImport({ /// 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. @@ -34,6 +36,10 @@ pub fn parseWithBase(input: []const u8, base: []const u8) ParseError!URL { 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); } @@ -42,6 +48,11 @@ pub inline fn freeOwnedString(owned: OwnedString) void { return c.ada_free_owned_string(owned); } +/// Returns true if given URL is valid (not NULL). +pub inline fn isValid(url: URL) bool { + return c.ada_is_valid(url); +} + /// Can return an empty string. /// Contrary to other getters, returned slice is heap allocated. pub inline fn getOrigin(url: URL) []const u8 { @@ -68,6 +79,10 @@ pub inline fn getPassword(url: URL) []const u8 { } 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]; } @@ -77,12 +92,21 @@ pub inline fn getHash(url: URL) []const u8 { return hash.data[0..hash.length]; } +/// Returns an empty string if not provided. pub inline fn getHost(url: URL) []const u8 { const host = c.ada_get_host(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]; } @@ -92,12 +116,55 @@ pub inline fn getPathname(url: URL) []const u8 { return pathname.data[0..pathname.length]; } -pub inline fn getSearch(url: URL) []const u8 { - const search = c.ada_get_search(url); - return search.data[0..search.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]; } + +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); +} From c1160543ad826ecf499241472db2d579220b8d7f Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 7 Oct 2025 17:18:08 +0300 Subject: [PATCH 05/25] basic url parsing working * also reintroduces old `URLSearchParams` implementation since ada prefers its own iterator where we'd like to use our own. --- src/browser/html/document.zig | 4 +- src/browser/html/elements.zig | 99 +++++-------- src/browser/html/location.zig | 2 +- src/browser/url/url.zig | 253 +++++++++++++++++++++++++++++----- 4 files changed, 263 insertions(+), 95 deletions(-) diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index 4020b4980..0516e76d1 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -42,12 +42,12 @@ pub const HTMLDocument = struct { // 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 // the way MDN recommends getting the domain anyways, since document.domain // is deprecated. 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 { diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index a3104fa33..291212fa1 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -275,7 +275,7 @@ pub const HTMLAnchorElement = struct { // TODO return a disposable string pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 { var u = try url(self, page); - return try u.get_origin(page); + return u.get_origin(page); } // TODO return a disposable string @@ -286,43 +286,42 @@ pub const HTMLAnchorElement = struct { pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void { const arena = page.arena; + _ = arena; var u = try url(self, page); - u.uri.scheme = v; - const href = try u.toString(arena); + u.set_protocol(v); + const href = try u.get_href(page); try parser.anchorSetHref(self, href); } - // TODO return a disposable string pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 { var u = try url(self, page); - return try u.get_host(page); + return u.get_host(); } pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void { // search : separator - var p: ?u16 = null; + var p: ?[]const u8 = null; var h: []const u8 = undefined; for (v, 0..) |c, i| { if (c == ':') { h = v[0..i]; - p = try std.fmt.parseInt(u16, v[i + 1 ..], 10); + //p = try std.fmt.parseInt(u16, v[i + 1 ..], 10); + p = v[i + 1 ..]; break; } } - const arena = page.arena; var u = try url(self, page); - if (p) |pp| { - u.uri.host = .{ .raw = h }; - u.uri.port = pp; + if (p) |port| { + u.set_host(h); + u.set_port(port); } else { - u.uri.host = .{ .raw = v }; - u.uri.port = null; + u.set_host(v); } - const href = try u.toString(arena); + const href = try u.get_href(page); try parser.anchorSetHref(self, href); } @@ -332,30 +331,26 @@ pub const HTMLAnchorElement = struct { } pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void { - const arena = page.arena; var u = try url(self, page); - u.uri.host = .{ .raw = v }; - const href = try u.toString(arena); + u.set_host(v); + const href = try u.get_href(page); try parser.anchorSetHref(self, href); } // TODO return a disposable string pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 { var u = try url(self, page); - return try u.get_port(page); + return u.get_port(); } pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { - const arena = page.arena; var u = try url(self, page); if (v != null and v.?.len > 0) { - u.uri.port = try std.fmt.parseInt(u16, v.?, 10); - } else { - u.uri.port = null; + u.set_host(v.?); } - const href = try u.toString(arena); + const href = try u.get_href(page); try parser.anchorSetHref(self, href); } @@ -366,37 +361,28 @@ pub const HTMLAnchorElement = struct { } pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { - const arena = page.arena; - var u = try url(self, page); + if (v) |username| { + var u = try url(self, page); + u.set_username(username); - if (v) |vv| { - u.uri.user = .{ .raw = vv }; - } else { - u.uri.user = null; + const href = try u.get_href(page); + try parser.anchorSetHref(self, href); } - 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 { var u = try url(self, page); - return try page.arena.dupe(u8, u.get_password()); + return u.get_password(); } pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { - const arena = page.arena; - var u = try url(self, page); + if (v) |password| { + var u = try url(self, page); + u.set_password(password); - if (v) |vv| { - u.uri.password = .{ .raw = vv }; - } else { - u.uri.password = null; + const href = try u.get_href(page); + try parser.anchorSetHref(self, href); } - const href = try u.toString(arena); - - try parser.anchorSetHref(self, href); } // TODO return a disposable string @@ -406,44 +392,35 @@ pub const HTMLAnchorElement = struct { } pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void { - const arena = page.arena; var u = try url(self, page); - u.uri.path = .{ .raw = v }; - const href = try u.toString(arena); - + u.set_pathname(v); + const href = try u.get_href(page); try parser.anchorSetHref(self, href); } pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 { var u = try url(self, page); - return try u.get_search(page); + return u.get_search(page); } - pub fn set_search(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { + pub fn set_search(self: *parser.Anchor, v: []const u8, page: *Page) !void { var u = try url(self, page); try u.set_search(v, page); - const href = try u.toString(page.call_arena); + const href = try u.get_href(page); try parser.anchorSetHref(self, href); } // TODO return a disposable string pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 { var u = try url(self, page); - return try u.get_hash(page); + return u.get_hash(); } - pub fn set_hash(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { - const arena = page.arena; + pub fn set_hash(self: *parser.Anchor, v: []const u8, page: *Page) !void { var u = try url(self, page); - - if (v) |vv| { - u.uri.fragment = .{ .raw = vv }; - } else { - u.uri.fragment = null; - } - const href = try u.toString(arena); - + u.set_hash(v); + const href = try u.get_href(page); try parser.anchorSetHref(self, href); } }; diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig index 3e3e593bc..743375d96 100644 --- a/src/browser/html/location.zig +++ b/src/browser/html/location.zig @@ -86,7 +86,7 @@ pub const Location = struct { } pub fn _toString(self: *Location, page: *Page) ![]const u8 { - return try self.get_href(page); + return self.get_href(page); } }; diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig index 123660c9b..465ede947 100644 --- a/src/browser/url/url.zig +++ b/src/browser/url/url.zig @@ -18,6 +18,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const Writer = std.Io.Writer; const ada = @import("ada"); const js = @import("../js/js.zig"); @@ -39,38 +40,69 @@ pub const Interfaces = .{ /// https://developer.mozilla.org/en-US/docs/Web/API/URL/URL pub const URL = struct { 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, + + pub const empty = URL{ + .internal = null, + .search_params = .{}, + }; // You can use an existing URL object for either argument, and it will be // stringified from the object's href property. - pub const ConstructorArg = union(enum) { - url: *URL, - element: *parser.Element, + const ConstructorArg = union(enum) { string: []const u8, + url: *const URL, + element: *parser.Element, - fn toString(self: *const ConstructorArg) error{Invalid}![]const u8 { + fn toString(self: ConstructorArg, page: *Page) ![]const u8 { return switch (self) { .string => |s| s, - .url => |url| url._toString(), - .element => |e| parser.elementGetAttribute(@ptrCast(e), "href") orelse error.Invalid, + .url => |url| url._toString(page), + .element => |e| { + const attrib = try parser.elementGetAttribute(@ptrCast(e), "href") orelse { + return error.InvalidArgument; + }; + + return attrib; + }, }; } }; - pub fn constructor(url: ConstructorArg, maybe_base: ?ConstructorArg, _: *Page) !URL { - const u = blk: { - const url_str = try url.toString(); + pub fn constructor(url: ConstructorArg, maybe_base: ?ConstructorArg, page: *Page) !URL { + const url_str = try url.toString(page); + + const internal = try blk: { if (maybe_base) |base| { - break :blk ada.parseWithBase(url_str, try base.toString()); + break :blk ada.parseWithBase(url_str, try base.toString(page)); } break :blk ada.parse(url_str); }; - return .{ .url = u }; + // Prepare search_params. + const params: URLSearchParams = blk: { + const search = ada.getSearch(internal); + if (search.data == null) { + break :blk .{}; + } + + break :blk try .initFromString(page.arena, search.data[0..search.length]); + }; + + // We're doing this since we track search params separately. + ada.clearSearch(internal); + + return .{ .internal = internal, .search_params = params }; } pub fn destructor(self: *const URL) void { - ada.free(self.internal); + // Not tracked by arena. + return ada.free(self.internal); } pub fn initWithoutSearchParams(uri: std.Uri) URL { @@ -82,6 +114,10 @@ pub const URL = struct { // Getters. + pub fn get_searchParams(self: *URL) *URLSearchParams { + return &self.search_params; + } + pub fn get_origin(self: *const URL, page: *Page) ![]const u8 { const arena = page.arena; // `ada.getOrigin` allocates memory in order to find the `origin`. @@ -94,8 +130,27 @@ pub const URL = struct { return arena.dupe(u8, origin); } - pub fn get_href(self: *const URL) []const u8 { - return ada.getHref(self.internal); + pub fn get_href(self: *const URL, page: *Page) ![]const u8 { + var w: Writer.Allocating = .init(page.arena); + + const href = ada.getHref(self.internal); + 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 { @@ -126,39 +181,175 @@ pub const URL = struct { return ada.getPathname(self.internal); } - pub fn get_search(self: *const URL) []const u8 { - return ada.getSearch(self.internal); + // 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; + + if (self.search_params.get_size() == 0) { + return ""; + } + + var buf: std.ArrayListUnmanaged(u8) = .{}; + try buf.append(arena, '?'); + try self.search_params.encode(buf.writer(arena)); + return buf.items; } 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 = .{}; + if (maybe_input) |input| { + self.search_params = try .initFromString(page.arena, input); + } + } + + pub fn set_hash(self: *const URL, input: []const u8) void { + _ = ada.setHash(self.internal, input); + } }; pub const URLSearchParams = struct { - internal: ada.URLSearchParams, + entries: kv.List = .{}, pub const ConstructorOptions = union(enum) { - string: []const u8, + query_string: []const u8, form_data: *const FormData, object: js.JsObject, }; -}; -// 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 ""; + pub fn constructor(maybe_options: ?ConstructorOptions, page: *Page) !URLSearchParams { + const options = maybe_options orelse return .{}; - return uriComponentStr(c.?); -} + 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(); -fn uriComponentStr(c: std.Uri.Component) []const u8 { - return switch (c) { - .raw => |v| v, - .percent_encoded => |v| v, - }; -} + var entries = kv.List{}; + try entries.ensureTotalCapacity(arena, it.count); + + while (try it.next()) |js_name| { + const name = try js_name.toString(arena); + const js_value = try object.get(name); + const value = try js_value.toString(arena); + + entries.appendOwnedAssumeCapacity(name, value); + } + + return .{ .entries = entries }; + }, + }; + } + + /// Initializes URLSearchParams from a query string. + pub fn initFromString(arena: Allocator, query_string: []const u8) !URLSearchParams { + return .{ .entries = try parseQuery(arena, query_string) }; + } + + pub fn get_size(self: *const URLSearchParams) u32 { + return @intCast(self.entries.count()); + } + + pub fn _append(self: *URLSearchParams, name: []const u8, value: []const u8, page: *Page) !void { + return self.entries.append(page.arena, name, value); + } + + pub fn _set(self: *URLSearchParams, name: []const u8, value: []const u8, page: *Page) !void { + return self.entries.set(page.arena, name, value); + } + + pub fn _delete(self: *URLSearchParams, name: []const u8, value_: ?[]const u8) void { + if (value_) |value| { + return self.entries.deleteKeyValue(name, value); + } + return self.entries.delete(name); + } + + pub fn _get(self: *const URLSearchParams, name: []const u8) ?[]const u8 { + return self.entries.get(name); + } + + pub fn _getAll(self: *const URLSearchParams, name: []const u8, page: *Page) ![]const []const u8 { + return self.entries.getAll(page.call_arena, name); + } + + pub fn _has(self: *const URLSearchParams, name: []const u8) bool { + return self.entries.has(name); + } + + pub fn _keys(self: *const URLSearchParams) KeyIterable { + return .{ .inner = self.entries.keyIterator() }; + } + + pub fn _values(self: *const URLSearchParams) ValueIterable { + return .{ .inner = self.entries.valueIterator() }; + } + + pub fn _entries(self: *const URLSearchParams) EntryIterable { + return .{ .inner = self.entries.entryIterator() }; + } + + pub fn _symbol_iterator(self: *const URLSearchParams) EntryIterable { + return self._entries(); + } + + pub fn _toString(self: *const URLSearchParams, page: *Page) ![]const u8 { + var arr: std.ArrayListUnmanaged(u8) = .empty; + try self.write(arr.writer(page.call_arena)); + return arr.items; + } + + fn write(self: *const URLSearchParams, writer: anytype) !void { + return kv.urlEncode(self.entries, .query, writer); + } + + // TODO + pub fn _sort(_: *URLSearchParams) void {} + + fn encode(self: *const URLSearchParams, writer: anytype) !void { + return kv.urlEncode(self.entries, .query, writer); + } +}; // Parse the given query. fn parseQuery(arena: Allocator, s: []const u8) !kv.List { From 900c8d247310cbf10a025056ce541b97f4ac8e2f Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Thu, 9 Oct 2025 10:12:00 +0300 Subject: [PATCH 06/25] change after rebase --- src/browser/url/url.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig index 465ede947..039bfc9e6 100644 --- a/src/browser/url/url.zig +++ b/src/browser/url/url.zig @@ -252,7 +252,7 @@ pub const URLSearchParams = struct { pub const ConstructorOptions = union(enum) { query_string: []const u8, form_data: *const FormData, - object: js.JsObject, + object: js.Object, }; pub fn constructor(maybe_options: ?ConstructorOptions, page: *Page) !URLSearchParams { From cecdd47bbc3f52af2d5179d1e41a5bf64ed7452f Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 13 Oct 2025 16:29:09 +0300 Subject: [PATCH 07/25] bind more ada functions also includes a fix for releasing memory if parsing failed --- vendor/ada/root.zig | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/vendor/ada/root.zig b/vendor/ada/root.zig index 64d48b83b..98bb60e29 100644 --- a/vendor/ada/root.zig +++ b/vendor/ada/root.zig @@ -19,7 +19,6 @@ 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)) { - free(url); return error.Invalid; } @@ -29,7 +28,6 @@ pub fn parse(input: []const u8) ParseError!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)) { - free(url); return error.Invalid; } @@ -53,6 +51,11 @@ 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 { @@ -92,9 +95,18 @@ pub inline fn getHash(url: URL) []const u8 { return hash.data[0..hash.length]; } -/// Returns an empty string if not provided. +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 = c.ada_get_host(url); + const host = getHostNullable(url); if (host.data == null) { return ""; } @@ -111,6 +123,10 @@ pub inline fn getHostname(url: URL) []const u8 { return hostname.data[0..hostname.length]; } +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]; @@ -168,3 +184,18 @@ pub inline fn setHash(url: URL, input: []const u8) void { 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); +} From cf9ecbd9fd1fa1f3620298226596b2c05fb68dc7 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 13 Oct 2025 16:35:35 +0300 Subject: [PATCH 08/25] prefer `URL` instead of `std.Uri` everywhere --- src/browser/html/document.zig | 4 +- src/browser/page.zig | 10 +- src/browser/storage/cookie.zig | 241 +++++++++++++++++---------------- src/browser/url/url.zig | 57 +++++--- src/cdp/domains/fetch.zig | 10 +- src/cdp/domains/network.zig | 70 +++++----- src/cdp/domains/storage.zig | 28 +++- src/http/Client.zig | 44 +++--- src/url.zig | 149 +++++++++++++------- 9 files changed, 356 insertions(+), 257 deletions(-) diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index 0516e76d1..30a66359c 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -85,7 +85,7 @@ pub const HTMLDocument = struct { pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const 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_navigation = true, }); @@ -95,7 +95,7 @@ pub const HTMLDocument = struct { 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 // 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(); if (c.http_only) { c.deinit(); diff --git a/src/browser/page.zig b/src/browser/page.zig index d668b67ce..4355724da 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -150,6 +150,7 @@ pub const Page = struct { try self.registerBackgroundTasks(); } + // FIXME: Deinit self.url. pub fn deinit(self: *Page) void { self.script_manager.shutdown = true; @@ -239,7 +240,7 @@ pub const Page = struct { 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) { try self.addDOMTreeBase(); } @@ -525,10 +526,11 @@ pub const Page = struct { is_http: bool = true, is_navigation: bool = false, }; + pub fn requestCookie(self: *const Page, opts: RequestCookieOpts) Http.Client.RequestCookie { return .{ - .jar = self.cookie_jar, - .origin = &self.url.uri, + .cookie_jar = self.cookie_jar, + .origin_url = self.url, .is_http = opts.is_http, .is_navigation = opts.is_navigation, }; @@ -859,7 +861,7 @@ pub const Page = struct { self.window.setStorageShelf( try self.session.storage_shed.getOrPut(try self.origin(self.arena)), ); - try self.window.replaceLocation(.{ .url = try self.url.toWebApi(self.arena) }); + //try self.window.replaceLocation(.{ .url = try self.url.toWebApi(self.arena) }); } pub const MouseEvent = struct { diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig index f4242ea7f..bf6b41efa 100644 --- a/src/browser/storage/cookie.zig +++ b/src/browser/storage/cookie.zig @@ -6,14 +6,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const log = @import("../../log.zig"); const DateTime = @import("../../datetime.zig").DateTime; const public_suffix_list = @import("../../data/public_suffix_list.zig").lookup; - -pub const LookupOpts = struct { - request_time: ?i64 = null, - origin_uri: ?*const Uri = null, - is_http: bool, - is_navigation: bool = true, - prefix: ?[]const u8 = null, -}; +const URL = @import("../../url.zig").URL; pub const Jar = struct { allocator: Allocator, @@ -80,13 +73,21 @@ pub const Jar = struct { } } - pub fn forRequest(self: *Jar, target_uri: *const Uri, writer: anytype, opts: LookupOpts) !void { + pub const LookupOpts = struct { + request_time: ?i64 = null, + origin_url: ?URL = null, + is_http: bool, + is_navigation: bool = true, + prefix: ?[]const u8 = null, + }; + + pub fn forRequest(self: *Jar, target_url: URL, writer: anytype, opts: LookupOpts) !void { const target = PreparedUri{ - .host = (target_uri.host orelse return error.InvalidURI).percent_encoded, - .path = target_uri.path.percent_encoded, - .secure = std.mem.eql(u8, target_uri.scheme, "https"), + .host = target_url.host(), + .path = target_url.getPath(), + .secure = target_url.isSecure(), }; - const same_site = try areSameSite(opts.origin_uri, target.host); + const same_site = try areSameSite(opts.origin_url, target.host); 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 { - const c = Cookie.parse(self.allocator, uri, set_cookie) catch |err| { + pub fn populateFromResponse(self: *Jar, url: URL, set_cookie: []const u8) !void { + const c = Cookie.parse(self.allocator, url, set_cookie) catch |err| { log.warn(.web_api, "cookie parse failed", .{ .raw = set_cookie, .err = err }); return; }; @@ -148,9 +149,9 @@ fn areCookiesEqual(a: *const Cookie, b: *const Cookie) bool { return true; } -fn areSameSite(origin_uri_: ?*const std.Uri, target_host: []const u8) !bool { - const origin_uri = origin_uri_ orelse return true; - const origin_host = (origin_uri.host orelse return error.InvalidURI).percent_encoded; +fn areSameSite(maybe_origin_url: ?URL, target_host: []const u8) !bool { + const origin_url = maybe_origin_url orelse return true; + const origin_host = origin_url.host(); // common case 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 { + // TODO: maybe reverseIterator? var i = std.mem.lastIndexOfScalar(u8, host, '.') orelse return host; while (true) { i = std.mem.lastIndexOfScalar(u8, host[0..i], '.') orelse return host; @@ -269,8 +271,8 @@ pub const Cookie = struct { const aa = arena.allocator(); const owned_name = try aa.dupe(u8, cookie_name); const owned_value = try aa.dupe(u8, cookie_value); - const owned_path = try parsePath(aa, uri, path); - const owned_domain = try parseDomain(aa, uri, domain); + const owned_path = try parsePath(aa, url, path); + const owned_domain = try parseDomain(aa, url, domain); var normalized_expires: ?f64 = null; if (max_age) |ma| { @@ -362,37 +364,35 @@ 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 // ignore it and use the "default-path" algorithm - if (explicit_path) |path| { + if (maybe_explicit_path) |path| { if (path.len > 0 and path[0] == '/') { - return try arena.dupe(u8, path); + return arena.dupe(u8, path); } } - // default-path - const url_path = (uri orelse return "/").path; + const url_path = blk: { + if (maybe_url) |url| { + break :blk url.getPath(); + } + + return "/"; + }; - const either = url_path.percent_encoded; - if (either.len == 0 or (either.len == 1 and either[0] == '/')) { + if (url_path.len == 0 or (url_path.len == 1 and url_path[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 try arena.dupe(u8, owned_path[0 .. last + 1]); + return arena.dupe(u8, url_path); } - pub fn parseDomain(arena: Allocator, uri: ?*const std.Uri, explicit_domain: ?[]const u8) ![]const u8 { + pub fn parseDomain(arena: Allocator, maybe_url: ?URL, explicit_domain: ?[]const u8) ![]const u8 { var encoded_host: ?[]const u8 = null; - if (uri) |uri_| { - const uri_host = uri_.host orelse return error.InvalidURI; - const host = try percentEncode(arena, uri_host, isHostChar); - _ = toLower(host); - encoded_host = host; + if (maybe_url) |url| { + const url_host = url.hostname(); + encoded_host = url_host; } if (explicit_domain) |domain| { @@ -421,19 +421,6 @@ pub const Cookie = struct { 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 { return switch (c) { 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true, @@ -601,37 +588,40 @@ test "Jar: add" { defer jar.deinit(); 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 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 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 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 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 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 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 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); } test "Jar: forRequest" { 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; 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); } }.expect; @@ -641,131 +631,142 @@ test "Jar: forRequest" { var jar = Jar.init(testing.allocator); 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 - 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_uri, "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_uri, "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_uri, "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_uri, "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, "global1=1"), 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_url, "path1=3;Path=/about"), 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_url, "secure=5;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_url, "sitelax=7;SameSite=Lax;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_url_2, "domain1=9;domain=test.lightpanda.io"), now); // nothing fancy here - try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .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, .{ .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 // This should _not_ match xyxlightpanda.io - try expectCookies("", &jar, try std.Uri.parse("http://anothersitelightpanda.io/"), .{ - .origin_uri = &test_uri, + try expectCookies("", &jar, reuse_url, .{ + .origin_url = test_url, .is_http = true, }); // matching path without trailing / - try expectCookies("global1=1; global2=2; path1=3", &jar, try std.Uri.parse("http://lightpanda.io/about"), .{ - .origin_uri = &test_uri, + try expectCookies("global1=1; global2=2; path1=3", &jar, try reuse_url.reparse("http://lightpanda.io/about"), .{ + .origin_url = test_url, .is_http = true, }); // incomplete prefix path - try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/abou"), .{ - .origin_uri = &test_uri, + try expectCookies("global1=1; global2=2", &jar, try reuse_url.reparse("http://lightpanda.io/abou"), .{ + .origin_url = test_url, .is_http = true, }); // path doesn't match - try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/aboutus"), .{ - .origin_uri = &test_uri, + try expectCookies("global1=1; global2=2", &jar, try reuse_url.reparse("http://lightpanda.io/aboutus"), .{ + .origin_url = test_url, .is_http = true, }); // path doesn't match cookie directory - try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/docs"), .{ - .origin_uri = &test_uri, + try expectCookies("global1=1; global2=2", &jar, try reuse_url.reparse("http://lightpanda.io/docs"), .{ + .origin_url = test_url, .is_http = true, }); // exact directory match - try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/"), .{ - .origin_uri = &test_uri, + try expectCookies("global1=1; global2=2; path2=4", &jar, try reuse_url.reparse("http://lightpanda.io/docs/"), .{ + .origin_url = test_url, .is_http = true, }); // sub directory match - try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/more"), .{ - .origin_uri = &test_uri, + try expectCookies("global1=1; global2=2; path2=4", &jar, try reuse_url.reparse("http://lightpanda.io/docs/more"), .{ + .origin_url = test_url, .is_http = true, }); // secure - try expectCookies("global1=1; global2=2; secure=5", &jar, try std.Uri.parse("https://lightpanda.io/"), .{ - .origin_uri = &test_uri, + try expectCookies("global1=1; global2=2; secure=5", &jar, try reuse_url.reparse("https://lightpanda.io/"), .{ + .origin_url = test_url, .is_http = true, }); // 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/"), .{ - .origin_uri = &(try std.Uri.parse("https://example.com/")), + const example_com_url = try URL.parse("https://example.com/", null); + 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, }); // navigational cross domain, insecure - try expectCookies("global1=1; global2=2; sitelax=7", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{ - .origin_uri = &(try std.Uri.parse("https://example.com/")), + try expectCookies("global1=1; global2=2; sitelax=7", &jar, try reuse_url.reparse("http://lightpanda.io/x/"), .{ + .origin_url = example_com_url, .is_http = true, }); // non-navigational cross domain, insecure - try expectCookies("", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{ - .origin_uri = &(try std.Uri.parse("https://example.com/")), + try expectCookies("", &jar, try reuse_url.reparse("http://lightpanda.io/x/"), .{ + .origin_url = example_com_url, .is_http = true, .is_navigation = false, }); // non-navigational cross domain, secure - try expectCookies("sitenone=6", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{ - .origin_uri = &(try std.Uri.parse("https://example.com/")), + try expectCookies("sitenone=6", &jar, try reuse_url.reparse("https://lightpanda.io/x/"), .{ + .origin_url = example_com_url, .is_http = true, .is_navigation = false, }); // non-navigational same origin - try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{ - .origin_uri = &(try std.Uri.parse("https://lightpanda.io/")), + try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try reuse_url.reparse("http://lightpanda.io/x/"), .{ + .origin_url = test_url, .is_http = true, .is_navigation = false, }); // exact domain match + suffix - try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://test.lightpanda.io/"), .{ - .origin_uri = &test_uri, + try expectCookies("global2=2; domain1=9", &jar, try reuse_url.reparse("http://test.lightpanda.io/"), .{ + .origin_url = test_url, .is_http = true, }); // domain suffix match + suffix - try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://1.test.lightpanda.io/"), .{ - .origin_uri = &test_uri, + try expectCookies("global2=2; domain1=9", &jar, try reuse_url.reparse("http://1.test.lightpanda.io/"), .{ + .origin_url = test_url, .is_http = true, }); // non-matching domain - try expectCookies("global2=2", &jar, try std.Uri.parse("http://other.lightpanda.io/"), .{ - .origin_uri = &test_uri, + try expectCookies("global2=2", &jar, try reuse_url.reparse("http://other.lightpanda.io/"), .{ + .origin_url = test_url, .is_http = true, }); const l = jar.cookies.items.len; - try expectCookies("global1=1", &jar, test_uri, .{ + try expectCookies("global1=1", &jar, test_url, .{ .request_time = now + 100, - .origin_uri = &test_uri, + .origin_url = test_url, .is_http = true, }); try testing.expectEqual(l - 1, jar.cookies.items.len); @@ -961,9 +962,11 @@ const ExpectedCookie = struct { same_site: Cookie.SameSite = .lax, }; -fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u8) !void { - const uri = try Uri.parse(url); - var cookie = try Cookie.parse(testing.allocator, &uri, set_cookie); +fn expectCookie(expected: ExpectedCookie, url_str: []const u8, set_cookie: []const u8) !void { + const url = try URL.parse(url_str, null); + defer url.deinit(); + + var cookie = try Cookie.parse(testing.allocator, url, set_cookie); defer cookie.deinit(); try testing.expectEqual(expected.name, cookie.name); @@ -977,9 +980,11 @@ fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u try testing.expectDelta(expected.expires, cookie.expires, 2.0); } -fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8) !void { - const uri = if (url) |u| try Uri.parse(u) else test_uri; - var cookie = try Cookie.parse(testing.allocator, &uri, set_cookie); +fn expectAttribute(expected: anytype, maybe_url_str: ?[]const u8, set_cookie: []const u8) !void { + const url = try URL.parse(if (maybe_url_str) |url_str| url_str else "https://lightpanda.io/", null); + defer url.deinit(); + + var cookie = try Cookie.parse(testing.allocator, url, set_cookie); defer cookie.deinit(); inline for (@typeInfo(@TypeOf(expected)).@"struct".fields) |f| { @@ -994,9 +999,7 @@ fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8) } } -fn expectError(expected: anyerror, url: ?[]const u8, set_cookie: []const u8) !void { - const uri = if (url) |u| try Uri.parse(u) else test_uri; - try testing.expectError(expected, Cookie.parse(testing.allocator, &uri, set_cookie)); +fn expectError(expected: anyerror, maybe_url_str: ?[]const u8, set_cookie: []const u8) !void { + 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, url, set_cookie)); } - -const test_uri = Uri.parse("http://lightpanda.io/") catch unreachable; diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig index 039bfc9e6..837a15604 100644 --- a/src/browser/url/url.zig +++ b/src/browser/url/url.zig @@ -84,20 +84,10 @@ pub const URL = struct { break :blk ada.parse(url_str); }; - // Prepare search_params. - const params: URLSearchParams = blk: { - const search = ada.getSearch(internal); - if (search.data == null) { - break :blk .{}; - } - - break :blk try .initFromString(page.arena, search.data[0..search.length]); + return .{ + .internal = internal, + .search_params = try prepareSearchParams(page.arena, internal), }; - - // We're doing this since we track search params separately. - ada.clearSearch(internal); - - return .{ .internal = internal, .search_params = params }; } pub fn destructor(self: *const URL) void { @@ -105,8 +95,37 @@ pub const URL = struct { return ada.free(self.internal); } - pub fn initWithoutSearchParams(uri: std.Uri) URL { - return .{ .uri = uri, .search_params = .{} }; + /// Initializes a `URL` from given `internal`. + /// Note that this copies the given `internal`; meaning 2 instances + /// of it has to be tracked separately. + pub fn constructFromInternal(arena: Allocator, internal: ada.URL) !URL { + const copy = ada.copy(internal); + + return .{ + .internal = copy, + .search_params = try prepareSearchParams(arena, copy), + }; + } + + /// Prepares a `URLSearchParams` from given `internal`. + /// Resets `search` of `internal`. + 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; + } + + // Alias to get_href. + pub fn _toString(self: *const URL, page: *Page) ![]const u8 { + return self.get_href(page); } pub fn _toString(self: *const URL) []const u8 { return ada.getHref(self.internal); @@ -178,7 +197,13 @@ pub const URL = struct { } pub fn get_pathname(self: *const URL) []const u8 { - return ada.getPathname(self.internal); + 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`. diff --git a/src/cdp/domains/fetch.zig b/src/cdp/domains/fetch.zig index f6fb302b9..5697605a1 100644 --- a/src/cdp/domains/fetch.zig +++ b/src/cdp/domains/fetch.zig @@ -208,7 +208,7 @@ pub fn requestIntercept(arena: Allocator, bc: anytype, intercept: *const Notific log.debug(.cdp, "request intercept", .{ .state = "paused", .id = transfer.id, - .url = transfer.uri, + .url = transfer.url, }); // Await either continueRequest, failRequest or fulfillRequest @@ -237,7 +237,7 @@ fn continueRequest(cmd: anytype) !void { log.debug(.cdp, "request intercept", .{ .state = "continue", .id = transfer.id, - .url = transfer.uri, + .url = transfer.url, .new_url = params.url, }); @@ -342,7 +342,7 @@ fn fulfillRequest(cmd: anytype) !void { log.debug(.cdp, "request intercept", .{ .state = "fulfilled", .id = transfer.id, - .url = transfer.uri, + .url = transfer.url, .status = params.responseCode, .body = params.body != null, }); @@ -376,7 +376,7 @@ fn failRequest(cmd: anytype) !void { log.info(.cdp, "request intercept", .{ .state = "fail", .id = request_id, - .url = transfer.uri, + .url = transfer.url, .reason = params.errorReason, }); return cmd.sendResult(null, .{}); @@ -420,7 +420,7 @@ pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Noti log.debug(.cdp, "request auth required", .{ .state = "paused", .id = transfer.id, - .url = transfer.uri, + .url = transfer.url, }); // Await continueWithAuth diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 0d7014d0e..1c2c84c8d 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator; const CdpStorage = @import("storage.zig"); const Transfer = @import("../../http/Client.zig").Transfer; const Notification = @import("../../notification.zig").Notification; +const URL = @import("../../url.zig").URL; pub fn processMessage(cmd: anytype) !void { 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 cookies = &bc.session.cookie_jar.cookies; - const uri = if (params.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null; - const uri_ptr = if (uri) |u| &u else null; + const maybe_url: ?URL = blk: { + if (params.url) |url| { + break :blk URL.parse(url, null) catch return error.InvalidParams; + } + + break :blk null; + }; var index = cookies.items.len; while (index > 0) { index -= 1; const cookie = &cookies.items[index]; - const domain = try Cookie.parseDomain(cmd.arena, uri_ptr, params.domain); - const path = try Cookie.parsePath(cmd.arena, uri_ptr, params.path); + const domain = try Cookie.parseDomain(cmd.arena, maybe_url, params.domain); + 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. // Similar to deduplicating with areCookiesEqual, except domain and path are optional. @@ -133,6 +139,12 @@ fn deleteCookies(cmd: anytype) !void { cookies.swapRemove(index).deinit(); } } + + // Deinit URL if we had. + if (maybe_url) |url| { + url.deinit(); + } + return cmd.sendResult(null, .{}); } @@ -177,13 +189,14 @@ fn getCookies(cmd: anytype) !void { 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); - for (param_urls) |url| { - const uri = std.Uri.parse(url) catch return error.InvalidParams; + for (param_urls) |url_str| { + const url = URL.parse(url_str, null) catch return error.InvalidParams; + defer url.deinit(); urls.appendAssumeCapacity(.{ - .host = try Cookie.parseDomain(cmd.arena, &uri, null), - .path = try Cookie.parsePath(cmd.arena, &uri, null), - .secure = std.mem.eql(u8, uri.scheme, "https"), + .host = try Cookie.parseDomain(cmd.arena, url, null), + .path = try Cookie.parsePath(cmd.arena, url, null), + .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}), .frameId = target_id, .loaderId = bc.loader_id, - .documentUrl = DocumentUrlWriter.init(&page.url.uri), + .documentUrl = DocumentUrlWriter.init(page.url), .request = TransferAsRequestWriter.init(transfer), .initiator = .{ .type = "other" }, }, .{ .session_id = session_id }); @@ -300,23 +313,17 @@ pub const TransferAsRequestWriter = struct { try jws.objectField("url"); try jws.beginWriteRaw(); try writer.writeByte('\"'); - try transfer.uri.writeToStream(writer, .{ - .scheme = true, - .authentication = true, - .authority = true, - .path = true, - .query = true, - }); + try transfer.url.writeToStream(writer); try writer.writeByte('\"'); jws.endWriteRaw(); } { - if (transfer.uri.fragment) |frag| { + if (transfer.url.getFragment()) |frag| { try jws.objectField("urlFragment"); try jws.beginWriteRaw(); try writer.writeAll("\"#"); - try writer.writeAll(frag.percent_encoded); + try writer.writeAll(frag); try writer.writeByte('\"'); jws.endWriteRaw(); } @@ -370,13 +377,7 @@ const TransferAsResponseWriter = struct { try jws.objectField("url"); try jws.beginWriteRaw(); try writer.writeByte('\"'); - try transfer.uri.writeToStream(writer, .{ - .scheme = true, - .authentication = true, - .authority = true, - .path = true, - .query = true, - }); + try transfer.url.writeToStream(writer); try writer.writeByte('\"'); jws.endWriteRaw(); } @@ -417,29 +418,22 @@ const TransferAsResponseWriter = struct { }; const DocumentUrlWriter = struct { - uri: *std.Uri, + url: URL, - fn init(uri: *std.Uri) DocumentUrlWriter { - return .{ - .uri = uri, - }; + fn init(url: URL) DocumentUrlWriter { + return .{ .url = url }; } pub fn jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void { self._jsonStringify(jws) catch return error.WriteFailed; } + fn _jsonStringify(self: *const DocumentUrlWriter, jws: anytype) !void { const writer = jws.writer; try jws.beginWriteRaw(); try writer.writeByte('\"'); - try self.uri.writeToStream(writer, .{ - .scheme = true, - .authentication = true, - .authority = true, - .path = true, - .query = true, - }); + try self.url.writeToStream(writer); try writer.writeByte('\"'); jws.endWriteRaw(); } diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig index 662d079f0..54ab5e352 100644 --- a/src/cdp/domains/storage.zig +++ b/src/cdp/domains/storage.zig @@ -21,6 +21,7 @@ const std = @import("std"); const log = @import("../../log.zig"); const Cookie = @import("../../browser/storage/storage.zig").Cookie; const CookieJar = @import("../../browser/storage/storage.zig").CookieJar; +const URL = @import("../../url.zig").URL; pub const PreparedUri = @import("../../browser/storage/cookie.zig").PreparedUri; pub fn processMessage(cmd: anytype) !void { @@ -136,12 +137,25 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void { const a = arena.allocator(); // 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 uri_ptr = if (uri) |*u| u else null; - const domain = try Cookie.parseDomain(a, uri_ptr, param.domain); + const maybe_url: ?URL = blk: { + if (param.url) |url| { + 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 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{ .arena = arena, @@ -158,6 +172,12 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void { .None => .none, }, }; + + // Free if we had. + if (maybe_url) |url| { + url.deinit(); + } + try cookie_jar.add(cookie, std.time.timestamp()); } diff --git a/src/http/Client.zig b/src/http/Client.zig index 7eb84b24b..8fee4f1c5 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -22,9 +22,9 @@ const builtin = @import("builtin"); const Http = @import("Http.zig"); const Notification = @import("../notification.zig").Notification; -const CookieJar = @import("../browser/storage/storage.zig").CookieJar; - -const urlStitch = @import("../url.zig").stitch; +const CookieJar = @import("../browser/storage/cookie.zig").Jar; +const URL = @import("../url.zig").URL; +const urlStitch = URL.stitch; const c = Http.c; const posix = std.posix; @@ -259,7 +259,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer { errdefer req.headers.deinit(); // 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 }); return err; }; @@ -272,7 +272,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer { transfer.* = .{ .arena = ArenaAllocator.init(self.allocator), .id = id, - .uri = uri, + .url = url, .req = req, .ctx = req.ctx, .client = self, @@ -595,20 +595,20 @@ pub const Handle = struct { pub const RequestCookie = struct { is_http: bool, is_navigation: bool, - origin: *const std.Uri, - jar: *@import("../browser/storage/cookie.zig").Jar, + origin_url: URL, + cookie_jar: *CookieJar, - pub fn headersForRequest(self: *const RequestCookie, temp: Allocator, url: [:0]const u8, headers: *Http.Headers) !void { - const uri = std.Uri.parse(url) catch |err| { - log.warn(.http, "invalid url", .{ .err = err, .url = url }); + pub fn headersForRequest(self: *const RequestCookie, temp: Allocator, url_str: [:0]const u8, headers: *Http.Headers) !void { + const url = URL.parse(url_str, null) catch |err| { + log.warn(.http, "invalid url", .{ .err = err, .url = url_str }); return error.InvalidUrl; }; 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_navigation = self.is_navigation, - .origin_uri = self.origin, + .origin_url = self.origin_url, }); if (arr.items.len > 0) { @@ -688,7 +688,7 @@ pub const Transfer = struct { arena: ArenaAllocator, id: usize = 0, 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 client: *Client, // 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 { // for cookies - self.uri = try std.Uri.parse(url); + self.url = try self.url.reparse(url); // for the request itself self.req.url = url; @@ -833,7 +833,7 @@ pub const Transfer = struct { while (true) { const ct = getResponseHeader(easy, "set-cookie", i); if (ct == null) break; - try req.cookie_jar.populateFromResponse(&transfer.uri, ct.?.value); + try req.cookie_jar.populateFromResponse(transfer.url, ct.?.value); i += 1; if (i >= ct.?.amount) break; } @@ -847,14 +847,16 @@ pub const Transfer = struct { var baseurl: [*c]u8 = undefined; try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_EFFECTIVE_URL, &baseurl)); - const url = try urlStitch(arena, hlocation.?.value, std.mem.span(baseurl), .{}); - const uri = try std.Uri.parse(url); - transfer.uri = uri; + const stitched = try urlStitch(arena, hlocation.?.value, std.mem.span(baseurl), .{}); + // Since we're being redirected, we know url is valid. + // An assertation won't hurt, though. + std.debug.assert(transfer.url.isValid()); + _ = try transfer.url.reparse(stitched); 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, - .origin_uri = &transfer.uri, + .origin_url = transfer.url, // used to enforce samesite cookie rules .is_navigation = req.resource_type == .document, }); @@ -883,7 +885,7 @@ pub const Transfer = struct { while (true) { const ct = getResponseHeader(easy, "set-cookie", i); 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 }); return err; }; diff --git a/src/url.zig b/src/url.zig index acfac2560..f70c4422b 100644 --- a/src/url.zig +++ b/src/url.zig @@ -1,82 +1,135 @@ const std = @import("std"); -const Uri = std.Uri; const Allocator = std.mem.Allocator; const WebApiURL = @import("browser/url/url.zig").URL; +const ada = @import("ada"); + pub const stitch = URL.stitch; pub const URL = struct { - uri: Uri, + internal: ada.URL, + /// This must outlive the URL structure. raw: []const u8, - pub const empty = URL{ .uri = .{ .scheme = "" }, .raw = "" }; - pub const about_blank = URL{ .uri = .{ .scheme = "" }, .raw = "about:blank" }; + pub const empty = URL{ .internal = null, .raw = "" }; + pub const invalid = URL{ .internal = null, .raw = "" }; + pub const blank = parse("about:blank", null) catch unreachable; + + pub const ParseError = ada.ParseError; + + /// We assume str will last as long as the URL + /// In some cases, this is safe to do, because we know the URL is short lived. + /// In most cases though, we assume the caller will just dupe the string URL + /// into an arena. + /// If `str` does not contain a scheme, `fallback_scheme` be used instead. + /// `fallback_scheme` is `https` if not provided. + pub fn parse(str: []const u8, fallback_scheme: ?[]const u8) ParseError!URL { + // Try parsing directly; if it fails, we might have to provide a base. + const internal = ada.parse(str) catch blk: { + break :blk try ada.parseWithBase(fallback_scheme orelse "https", str); + }; - // We assume str will last as long as the URL - // In some cases, this is safe to do, because we know the URL is short lived. - // In most cases though, we assume the caller will just dupe the string URL - // into an arena - pub fn parse(str: []const u8, default_scheme: ?[]const u8) !URL { - var uri = Uri.parse(str) catch try Uri.parseAfterScheme(default_scheme orelse "https", str); + return .{ .internal = internal, .raw = str }; + } - // special case, url scheme is about, like about:blank. - // Use an empty string as host. - if (std.mem.eql(u8, uri.scheme, "about")) { - uri.host = .{ .percent_encoded = "" }; - } + /// Uses the same URL to parse in-place. + /// Assumes `internal` is valid. + pub fn reparse(self: URL, str: []const u8) ParseError!URL { + std.debug.assert(self.internal != null); - if (uri.host == null) { - return error.MissingHost; + _ = ada.setHref(self.internal, str); + if (!ada.isValid(self.internal)) { + return error.Invalid; } + //self.raw = str; - std.debug.assert(uri.host.? == .percent_encoded); + return self; + } - return .{ - .uri = uri, - .raw = str, - }; + /// Deinitializes internal url. + pub fn deinit(self: URL) void { + std.debug.assert(self.internal != null); + ada.free(self.internal); } - pub fn fromURI(arena: Allocator, uri: *const Uri) !URL { - // This is embarrassing. - var buf: std.ArrayListUnmanaged(u8) = .{}; - try uri.writeToStream(.{ - .scheme = true, - .authentication = true, - .authority = true, - .path = true, - .query = true, - .fragment = true, - }, buf.writer(arena)); - - return parse(buf.items, null); + /// Returns true if `internal` is initialized. + pub fn isValid(self: URL) bool { + return ada.isValid(self.internal); } - // 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: *const URL) []const u8 { - return self.uri.host.?.percent_encoded; + /// 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); + return str.data[0..str.length]; } - pub fn port(self: *const URL) ?u16 { - return self.uri.port; + pub fn href(self: URL) []const u8 { + return ada.getHref(self.internal); } - pub fn scheme(self: *const URL) []const u8 { - return self.uri.scheme; + pub fn hostname(self: URL) []const u8 { + return ada.getHostname(self.internal); } - pub fn origin(self: *const URL, writer: *std.Io.Writer) !void { - return self.uri.writeToStream(writer, .{ .scheme = true, .authority = true }); + 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 getProtocol(self: URL) []const u8 { + return ada.getProtocol(self.internal); + } + + 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]; + } + + /// 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.href()); + } + + // 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 proto = ada.getOrigin(self.internal); + defer ada.freeOwnedString(.{ .data = proto.ptr, .length = proto.len }); + + return writer.writeAll(proto); } - pub fn format(self: *const URL, writer: *std.Io.Writer) !void { + pub fn format(self: URL, writer: *std.Io.Writer) !void { return writer.writeAll(self.raw); } - pub fn toWebApi(self: *const URL, allocator: Allocator) !WebApiURL { - return WebApiURL.init(allocator, self.uri); + /// Converts `URL` to `WebApiURL`. + pub fn toWebApi(self: URL, allocator: Allocator) !WebApiURL { + return WebApiURL.constructFromInternal(allocator, self.internal); } /// Properly stitches two URL fragments together. From 6af8add7ffa3ca6e4403ac3893a1fe18090cd2ad Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 14 Oct 2025 12:08:36 +0300 Subject: [PATCH 09/25] fix cookie path parsing --- src/browser/storage/cookie.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig index bf6b41efa..c43fb5f0e 100644 --- a/src/browser/storage/cookie.zig +++ b/src/browser/storage/cookie.zig @@ -385,7 +385,11 @@ pub const Cookie = struct { return "/"; } - return arena.dupe(u8, url_path); + 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 { From 9b3be14650cf986595b4d0f667e19b5d5282cc65 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 14 Oct 2025 12:12:11 +0300 Subject: [PATCH 10/25] prefer `hostname` instead of `host` in `forRequest` --- src/browser/storage/cookie.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig index c43fb5f0e..4a8216781 100644 --- a/src/browser/storage/cookie.zig +++ b/src/browser/storage/cookie.zig @@ -81,9 +81,10 @@ pub const Jar = struct { prefix: ?[]const u8 = null, }; + // FIXME: Invalid behavior. pub fn forRequest(self: *Jar, target_url: URL, writer: anytype, opts: LookupOpts) !void { const target = PreparedUri{ - .host = target_url.host(), + .host = target_url.getHostname(), .path = target_url.getPath(), .secure = target_url.isSecure(), }; From 9e7e9b67ffcee6f6056f7b1b84772c3a5e249700 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 14 Oct 2025 12:12:36 +0300 Subject: [PATCH 11/25] add `getHostname` --- src/url.zig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/url.zig b/src/url.zig index f70c4422b..0c0cc9b33 100644 --- a/src/url.zig +++ b/src/url.zig @@ -69,8 +69,9 @@ pub const URL = struct { return ada.getHref(self.internal); } - pub fn hostname(self: URL) []const u8 { - return ada.getHostname(self.internal); + pub fn getHostname(self: URL) []const u8 { + const hostname = ada.getHostnameNullable(self.internal); + return hostname.data[0..hostname.length]; } pub fn getFragment(self: URL) ?[]const u8 { From 7629bf274ac69985b7f618962c2b72accda96fe2 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 14 Oct 2025 12:13:13 +0300 Subject: [PATCH 12/25] various changes in ada-url module --- vendor/ada/root.zig | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/vendor/ada/root.zig b/vendor/ada/root.zig index 98bb60e29..56663133c 100644 --- a/vendor/ada/root.zig +++ b/vendor/ada/root.zig @@ -46,7 +46,7 @@ pub inline fn freeOwnedString(owned: OwnedString) void { return c.ada_free_owned_string(owned); } -/// Returns true if given URL is valid (not NULL). +/// Returns true if given URL is valid. pub inline fn isValid(url: URL) bool { return c.ada_is_valid(url); } @@ -63,10 +63,8 @@ pub inline fn getOrigin(url: URL) []const u8 { return origin.data[0..origin.length]; } -/// Can return an empty string. -pub inline fn getHref(url: URL) []const u8 { - const href = c.ada_get_href(url); - return href.data[0..href.length]; +pub inline fn getHrefNullable(url: URL) String { + return c.ada_get_href(url); } /// Can return an empty string. @@ -123,6 +121,10 @@ pub inline fn getHostname(url: URL) []const u8 { 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); } @@ -141,6 +143,8 @@ pub inline fn getProtocol(url: URL) []const u8 { 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); } From c371538d27a63573e4eebd1ccd49e05d2a2c8b19 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 14 Oct 2025 16:43:18 +0300 Subject: [PATCH 13/25] rebase onto main --- src/browser/html/location.zig | 16 ++++++---------- src/browser/html/window.zig | 23 +++++++++++++++++++---- src/browser/page.zig | 2 +- src/browser/storage/cookie.zig | 5 ++--- src/browser/url/url.zig | 11 +++++++---- src/tests/html/element.html | 9 +++++---- src/url.zig | 12 ++++++++---- 7 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig index 743375d96..af8651416 100644 --- a/src/browser/html/location.zig +++ b/src/browser/html/location.zig @@ -29,10 +29,6 @@ pub const Location = struct { /// Chrome -> chrome://new-tab-page/ /// Firefox -> about:newtab /// Safari -> favorites:// - pub const default = Location{ - .url = .initWithoutSearchParams(Uri.parse("about:blank") catch unreachable), - }; - pub fn get_href(self: *Location, page: *Page) ![]const u8 { return self.url.get_href(page); } @@ -45,16 +41,16 @@ pub const Location = struct { return self.url.get_protocol(); } - pub fn get_host(self: *Location, page: *Page) ![]const u8 { - return self.url.get_host(page); + pub fn get_host(self: *Location) []const u8 { + return self.url.get_host(); } pub fn get_hostname(self: *Location) []const u8 { return self.url.get_hostname(); } - pub fn get_port(self: *Location, page: *Page) ![]const u8 { - return self.url.get_port(page); + pub fn get_port(self: *Location) []const u8 { + return self.url.get_port(); } pub fn get_pathname(self: *Location) []const u8 { @@ -65,8 +61,8 @@ pub const Location = struct { return self.url.get_search(page); } - pub fn get_hash(self: *Location, page: *Page) ![]const u8 { - return self.url.get_hash(page); + pub fn get_hash(self: *Location) []const u8 { + return self.url.get_hash(); } pub fn get_origin(self: *Location, page: *Page) ![]const u8 { diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index 5e03c0229..88647d0af 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -36,6 +36,8 @@ const Screen = @import("screen.zig").Screen; const domcss = @import("../dom/css.zig"); const Css = @import("../css/css.zig").Css; 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 fetchFn = @import("../fetch/fetch.zig").fetch; @@ -52,7 +54,7 @@ pub const Window = struct { document: *parser.DocumentHTML, target: []const u8 = "", - location: Location = .default, + location: Location, storage_shelf: ?*storage.Shelf = null, // counter for having unique timer ids @@ -75,17 +77,30 @@ pub const Window = struct { const doc = parser.documentHTMLToDocument(html_doc); 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 .{ .document = html_doc, + .location = .{ .url = url }, .target = target orelse "", .navigator = navigator orelse .{}, .performance = Performance.init(), }; } - pub fn replaceLocation(self: *Window, loc: Location) !void { - self.location = loc; - try parser.documentHTMLSetLocation(Location, self.document, &self.location); + pub fn replaceLocation(self: *Window, location: Location) !void { + // Remove current. + 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 { diff --git a/src/browser/page.zig b/src/browser/page.zig index 4355724da..25c883fff 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -861,7 +861,7 @@ pub const Page = struct { self.window.setStorageShelf( try self.session.storage_shed.getOrPut(try self.origin(self.arena)), ); - //try self.window.replaceLocation(.{ .url = try self.url.toWebApi(self.arena) }); + try self.window.replaceLocation(.{ .url = try self.url.toWebApi(self.arena) }); } pub const MouseEvent = struct { diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig index 4a8216781..d104ec364 100644 --- a/src/browser/storage/cookie.zig +++ b/src/browser/storage/cookie.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const Uri = std.Uri; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; @@ -209,7 +208,7 @@ pub const Cookie = struct { // Invalid attribute values? Ignore. // Duplicate attributes - use the last valid // 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); const cookie_name, const cookie_value, const rest = parseNameValue(str) catch { @@ -396,7 +395,7 @@ pub const Cookie = struct { pub fn parseDomain(arena: Allocator, maybe_url: ?URL, explicit_domain: ?[]const u8) ![]const u8 { var encoded_host: ?[]const u8 = null; if (maybe_url) |url| { - const url_host = url.hostname(); + const url_host = url.getHostname(); encoded_host = url_host; } diff --git a/src/browser/url/url.zig b/src/browser/url/url.zig index 837a15604..d95a9871d 100644 --- a/src/browser/url/url.zig +++ b/src/browser/url/url.zig @@ -127,9 +127,6 @@ pub const URL = struct { pub fn _toString(self: *const URL, page: *Page) ![]const u8 { return self.get_href(page); } - pub fn _toString(self: *const URL) []const u8 { - return ada.getHref(self.internal); - } // Getters. @@ -152,7 +149,13 @@ pub const URL = struct { pub fn get_href(self: *const URL, page: *Page) ![]const u8 { var w: Writer.Allocating = .init(page.arena); - const href = ada.getHref(self.internal); + const maybe_href = ada.getHrefNullable(self.internal); + if (maybe_href.data == null) { + return ""; + } + + const href = maybe_href.data[0..maybe_href.length]; + const comps = ada.getComponents(self.internal); const has_hash = comps.hash_start != ada.URLOmitted; diff --git a/src/tests/html/element.html b/src/tests/html/element.html index 4de1f0581..29f230b3a 100644 --- a/src/tests/html/element.html +++ b/src/tests/html/element.html @@ -29,10 +29,11 @@ - - From 4c4feef9fc3666403622b09d4cc9d4c94d60521f Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Fri, 17 Oct 2025 15:48:28 +0300 Subject: [PATCH 15/25] 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 --- vendor/ada/root.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vendor/ada/root.zig b/vendor/ada/root.zig index 56663133c..1af8e8235 100644 --- a/vendor/ada/root.zig +++ b/vendor/ada/root.zig @@ -63,6 +63,10 @@ pub inline fn getOrigin(url: URL) []const u8 { 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); } @@ -79,6 +83,10 @@ pub inline fn getPassword(url: URL) []const u8 { 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 ""; From 6820a00cd04088174deb511bcb434237851b58fa Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Fri, 17 Oct 2025 15:49:17 +0300 Subject: [PATCH 16/25] revert element test --- src/tests/html/element.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/html/element.html b/src/tests/html/element.html index 29f230b3a..3e9e9ea41 100644 --- a/src/tests/html/element.html +++ b/src/tests/html/element.html @@ -29,10 +29,10 @@ From e9755bd38b03dfa79ad0f55089cf5ac6bdbb93fb Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Fri, 17 Oct 2025 15:50:42 +0300 Subject: [PATCH 17/25] 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 --- src/cdp/domains/storage.zig | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig index 54ab5e352..5dea914c2 100644 --- a/src/cdp/domains/storage.zig +++ b/src/cdp/domains/storage.zig @@ -173,11 +173,6 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void { }, }; - // Free if we had. - if (maybe_url) |url| { - url.deinit(); - } - try cookie_jar.add(cookie, std.time.timestamp()); } From a46218cbae8e72895fa9a3dcd8ee8c96945e1a0e Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Fri, 17 Oct 2025 15:54:29 +0300 Subject: [PATCH 18/25] 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 --- src/browser/page.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/browser/page.zig b/src/browser/page.zig index 25c883fff..3bd09854e 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -126,8 +126,11 @@ pub const Page = struct { const browser = session.browser; const script_manager = ScriptManager.init(browser, self); + const url = try URL.parse("about:blank", null); + errdefer url.deinit(); + self.* = .{ - .url = URL.empty, + .url = url, .mode = .{ .pre = {} }, .window = try Window.create(null, null), .arena = arena, @@ -156,6 +159,7 @@ pub const Page = struct { self.http_client.abort(); self.script_manager.deinit(); + self.url.deinit(); } fn reset(self: *Page) !void { From 8e7d8225ba00c37ae7ff940a2bfeb86470cee829 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Fri, 17 Oct 2025 16:02:32 +0300 Subject: [PATCH 19/25] 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. --- src/browser/ScriptManager.zig | 6 +-- src/browser/dom/node.zig | 4 +- src/browser/fetch/Request.zig | 2 +- src/browser/html/History.zig | 10 ++--- src/browser/html/elements.zig | 10 ++--- src/browser/html/location.zig | 2 +- src/browser/page.zig | 16 ++++--- src/browser/xhr/xhr.zig | 2 +- src/cdp/cdp.zig | 2 +- src/cdp/domains/network.zig | 4 +- src/url.zig | 79 ++++++++++++++++++++++++++--------- 11 files changed, 88 insertions(+), 49 deletions(-) diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 735a2eab8..336346b7d 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -192,7 +192,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c if (try DataURI.parse(page.arena, src)) |data_uri| { source = .{ .@"inline" = data_uri }; } 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 = .{} }; } } else { @@ -204,7 +204,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element, comptime c .kind = kind, .element = element, .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_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( self.page.arena, entry.value_ptr.*, - self.page.url.raw, + self.page.url.getHref(), .{ .alloc = .if_needed, .null_terminated = true }, ); diff --git a/src/browser/dom/node.zig b/src/browser/dom/node.zig index 71b88e0e9..92d97dfb8 100644 --- a/src/browser/dom/node.zig +++ b/src/browser/dom/node.zig @@ -119,8 +119,8 @@ pub const Node = struct { // -------- // Read-only attributes - pub fn get_baseURI(_: *parser.Node, page: *Page) ![]const u8 { - return page.url.raw; + pub fn get_baseURI(_: *parser.Node, page: *Page) []const u8 { + return page.url.getHref(); } pub fn get_firstChild(self: *parser.Node) !?Union { diff --git a/src/browser/fetch/Request.zig b/src/browser/fetch/Request.zig index f13a8cb8c..54ee689e2 100644 --- a/src/browser/fetch/Request.zig +++ b/src/browser/fetch/Request.zig @@ -130,7 +130,7 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re const url: [:0]const u8 = blk: switch (input) { .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| { break :blk try arena.dupeZ(u8, req.url); diff --git a/src/browser/html/History.zig b/src/browser/html/History.zig index f8be6bb33..79dad8de0 100644 --- a/src/browser/html/History.zig +++ b/src/browser/html/History.zig @@ -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 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 }; try self.stack.append(arena, entry); 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; if (self.current) |curr| { const entry = &self.stack.items[curr]; 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 }; } else { - try self._pushState(state, "", _url, page); + try self._pushState(state, "", maybe_url, page); } } diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 291212fa1..38d39fbdc 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -218,12 +218,12 @@ pub const HTMLAnchorElement = struct { } 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 { - const full = try urlStitch(page.call_arena, href, page.url.raw, .{}); - return try parser.anchorSetHref(self, full); + const full = try urlStitch(page.call_arena, href, page.url.getHref(), .{}); + return parser.anchorSetHref(self, full); } pub fn get_hreflang(self: *parser.Anchor) ![]const u8 { @@ -694,7 +694,7 @@ pub const HTMLInputElement = struct { return try parser.inputGetSrc(self); } 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); } pub fn get_type(self: *parser.Input) ![]const u8 { @@ -747,7 +747,7 @@ pub const HTMLLinkElement = struct { } 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); } }; diff --git a/src/browser/html/location.zig b/src/browser/html/location.zig index af8651416..354fda76c 100644 --- a/src/browser/html/location.zig +++ b/src/browser/html/location.zig @@ -78,7 +78,7 @@ pub const Location = struct { } 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 { diff --git a/src/browser/page.zig b/src/browser/page.zig index 3bd09854e..718ed756e 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -267,7 +267,7 @@ pub const Page = struct { const head = parser.nodeListItem(list, 0) orelse return; 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; try Node.prepend(head, &[_]Node.NodeOrText{.{ .node = parser.elementToNode(base) }}); @@ -521,9 +521,7 @@ pub const Page = struct { } pub fn origin(self: *const Page, arena: Allocator) ![]const u8 { - var aw = std.Io.Writer.Allocating.init(arena); - try self.url.origin(&aw.writer); - return aw.written(); + return self.url.getOrigin(arena); } const RequestCookieOpts = struct { @@ -642,7 +640,7 @@ pub const Page = struct { }; self.session.browser.notification.dispatch(.page_navigated, &.{ - .url = self.url.raw, + .url = self.url.getHref(), .timestamp = timestamp(), }); } @@ -822,7 +820,7 @@ pub const Page = struct { } // 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 { @@ -858,7 +856,7 @@ pub const Page = struct { // extracted because this is called from tests to set things up. pub fn setDocument(self: *Page, html_doc: *parser.DocumentHTML) !void { 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. try self.window.replaceDocument(html_doc); @@ -1048,7 +1046,7 @@ pub const Page = struct { session.queued_navigation = .{ .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(); @@ -1088,7 +1086,7 @@ pub const Page = struct { try form_data.write(encoding, buf.writer(transfer_arena)); 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{ .reason = .form, diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig index ee6a6c04e..4255677a0 100644 --- a/src/browser/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -240,7 +240,7 @@ pub const XMLHttpRequest = struct { self.reset(); 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.state = .opened; diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 7b6590e8c..f703bd900 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -464,7 +464,7 @@ pub fn BrowserContext(comptime CDP_T: type) type { pub fn getURL(self: *const Self) ?[]const u8 { 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; } diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 1c2c84c8d..98317ea1f 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -185,13 +185,13 @@ fn getCookies(cmd: anytype) !void { const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{}; // 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}; var urls = try std.ArrayListUnmanaged(CdpStorage.PreparedUri).initCapacity(cmd.arena, param_urls.len); for (param_urls) |url_str| { const url = URL.parse(url_str, null) catch return error.InvalidParams; - defer url.deinit(); + //defer url.deinit(); urls.appendAssumeCapacity(.{ .host = try Cookie.parseDomain(cmd.arena, url, null), diff --git a/src/url.zig b/src/url.zig index 22d6b9ae7..42d083abc 100644 --- a/src/url.zig +++ b/src/url.zig @@ -8,28 +8,27 @@ const ada = @import("ada"); pub const stitch = URL.stitch; pub const URL = struct { + /// Internal ada structure. internal: ada.URL, - /// This must outlive the URL structure. - raw: []const u8, - - pub const empty = URL{ .internal = null, .raw = "" }; - pub const invalid = URL{ .internal = null, .raw = "" }; pub const ParseError = ada.ParseError; - /// We assume str will last as long as the URL - /// In some cases, this is safe to do, because we know the URL is short lived. - /// In most cases though, we assume the caller will just dupe the string URL - /// into an arena. - /// If `str` does not contain a scheme, `fallback_scheme` be used instead. + /// Creates a new URL by parsing given `input`. + /// `input` will be duped; so it can be freed after a call to this function. + /// If `input` does not contain a scheme, `fallback_scheme` be used instead. /// `fallback_scheme` is `https` if not provided. - pub fn parse(str: []const u8, fallback_scheme: ?[]const u8) ParseError!URL { + pub fn parse(input: []const u8, fallback_scheme: ?[]const u8) ParseError!URL { // Try parsing directly; if it fails, we might have to provide a base. - const internal = ada.parse(str) catch blk: { - break :blk try ada.parseWithBase(fallback_scheme orelse "https", str); + const internal = ada.parse(input) catch blk: { + break :blk try ada.parseWithBase(fallback_scheme orelse "https", input); }; - return .{ .internal = internal, .raw = str }; + return .{ .internal = internal }; + } + + pub fn parseWithBase(input: []const u8, base: []const u8) ParseError!URL { + const internal = try ada.parseWithBase(input, base); + return .{ .internal = internal }; } /// Uses the same URL to parse in-place. @@ -41,7 +40,6 @@ pub const URL = struct { if (!ada.isValid(self.internal)) { return error.Invalid; } - //self.raw = str; return self; } @@ -57,10 +55,29 @@ pub const URL = struct { return ada.isValid(self.internal); } + pub fn setHost(self: URL, host_str: []const u8) error{InvalidHost}!void { + 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]; } @@ -78,6 +95,11 @@ pub const URL = struct { 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 getFragment(self: URL) ?[]const u8 { // Ada calls it "hash" instead of "fragment". const hash = ada.getHashNullable(self.internal); @@ -90,6 +112,11 @@ pub const URL = struct { 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] == ':'); @@ -118,18 +145,32 @@ pub const URL = struct { 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 proto = ada.getOrigin(self.internal); - defer ada.freeOwnedString(.{ .data = proto.ptr, .length = proto.len }); + const s = ada.getOriginNullable(self.internal); + if (s.data == null) { + return; + } + defer ada.freeOwnedString(.{ .data = s.data, .length = s.length }); - return writer.writeAll(proto); + return writer.writeAll(s.data[0..s.length]); } pub fn format(self: URL, writer: *std.Io.Writer) !void { - return writer.writeAll(self.raw); + return self.writeToStream(writer); } /// Converts `URL` to `WebApiURL`. From 146b56c8c08720a65a9ae22c498d1c1b0fd4b95b Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Fri, 17 Oct 2025 19:39:10 +0300 Subject: [PATCH 20/25] refactor `HTMLAnchorElement` Prefer new URL implementation with separate store for object data. --- src/browser/html/elements.zig | 260 +++++++++++++++++++--------------- src/browser/page.zig | 42 ++++++ src/url.zig | 52 +++++++ vendor/ada/root.zig | 12 ++ 4 files changed, 255 insertions(+), 111 deletions(-) diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 38d39fbdc..6dd18601b 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -221,9 +221,29 @@ pub const HTMLAnchorElement = struct { 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.getHref(), .{}); - return 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 { @@ -258,170 +278,188 @@ pub const HTMLAnchorElement = struct { return try parser.nodeSetTextContent(parser.anchorToNode(self), v); } - fn url(self: *parser.Anchor, page: *Page) !URL { - // Although the URL.constructor union accepts an .{.element = X}, we - // 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 + fn getHref(self: *parser.Anchor) !?[]const u8 { + return parser.elementGetAttribute(@ptrCast(@alignCast(self)), "href"); + } + + /// 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); } - return .empty; + + // 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 pub fn get_origin(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_origin(page); + const u = getURL(self, page) catch return ""; + // 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 pub fn get_protocol(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_protocol(); + const u = getURL(self, page) catch return ""; + return u.getProtocol(); } pub fn set_protocol(self: *parser.Anchor, v: []const u8, page: *Page) !void { - const arena = page.arena; - _ = arena; - var u = try url(self, page); - - u.set_protocol(v); - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + const u = try getURL(self, page); + try u.setProtocol(v); + return parser.anchorSetHref(self, u.getHref()); } - pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_host(); - } - - pub fn set_host(self: *parser.Anchor, v: []const u8, page: *Page) !void { - // search : separator - var p: ?[]const u8 = null; - var h: []const u8 = undefined; - for (v, 0..) |c, i| { - if (c == ':') { - h = v[0..i]; - //p = try std.fmt.parseInt(u16, v[i + 1 ..], 10); - p = v[i + 1 ..]; - break; - } - } - - var u = try url(self, page); - - if (p) |port| { - u.set_host(h); - u.set_port(port); - } else { - u.set_host(v); - } + const NativeURL = @import("../../url.zig").URL; - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + // TODO: Return a disposable string. + pub fn get_host(self: *parser.Anchor, page: *Page) ![]const u8 { + const u = getURL(self, page) catch return ""; + return u.host(); } - pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_hostname(); - } + pub fn set_host(self: *parser.Anchor, host_str: []const u8, page: *Page) !void { + const u = blk: { + if (page.getObjectData(self)) |internal_url| { + break :blk NativeURL.fromInternal(internal_url); + } - pub fn set_hostname(self: *parser.Anchor, v: []const u8, page: *Page) !void { - var u = try url(self, page); - u.set_host(v); - const href = try u.get_href(page); - 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 host_str. + 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 { + // const maybe_href_str = try getAnchorHref(self); + // const href_str = maybe_href_str orelse return ""; + // + // const u = try NativeURL.parse(href_str, null); + // defer u.deinit(); + // + // return page.arena.dupe(u8, u.getHostname()); + //} + + //pub fn set_hostname(self: *parser.Anchor, v: []const u8) !void { + // const maybe_href_str = try getAnchorHref(self); + // + // if (maybe_href_str) |href_str| { + // const u = try NativeURL.parse(href_str, null); + // defer u.deinit(); + // + // try u.setHostname(v); + // + // return parser.anchorSetHref(self, u.getHref()); + // } + // + // // No href string there; use the given value as href. + // const u = try NativeURL.parse(v, null); + // defer u.deinit(); + // + // return parser.anchorSetHref(self, u.getHref()); + //} // TODO return a disposable string pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_port(); + const u = getURL(self, page) catch return ""; + return u.getPort(); } - pub fn set_port(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { - var u = try url(self, page); + pub fn set_port(self: *parser.Anchor, maybe_port: ?[]const u8, page: *Page) !void { + // TODO: Check for valid port (u16 integer). + if (maybe_port) |port| { + const u = try getURL(self, page); + try u.setPort(port); - if (v != null and v.?.len > 0) { - u.set_host(v.?); + return parser.anchorSetHref(self, u.getHref()); } - - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); } // TODO return a disposable string pub fn get_username(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_username(); + const u = try getURL(self, page); + return u.getUsername() orelse ""; } - pub fn set_username(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { - if (v) |username| { - var u = try url(self, page); - u.set_username(username); - - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + pub fn set_username(self: *parser.Anchor, maybe_username: ?[]const u8, page: *Page) !void { + if (maybe_username) |username| { + const u = try getURL(self, page); + try u.setUsername(username); + try parser.anchorSetHref(self, u.getHref()); } } pub fn get_password(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_password(); + const u = try getURL(self, page); + return u.getPassword() orelse ""; } - pub fn set_password(self: *parser.Anchor, v: ?[]const u8, page: *Page) !void { - if (v) |password| { - var u = try url(self, page); - u.set_password(password); - - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + pub fn set_password(self: *parser.Anchor, maybe_password: ?[]const u8, page: *Page) !void { + if (maybe_password) |password| { + const u = try getURL(self, page); + try u.setPassword(password); + try parser.anchorSetHref(self, u.getHref()); } } // TODO return a disposable string pub fn get_pathname(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_pathname(); + const u = try getURL(self, page); + return u.getPath(); } - pub fn set_pathname(self: *parser.Anchor, v: []const u8, page: *Page) !void { - var u = try url(self, page); - u.set_pathname(v); - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + pub fn set_pathname(self: *parser.Anchor, pathname: []const u8, page: *Page) !void { + const u = try getURL(self, page); + try u.setPath(pathname); + return parser.anchorSetHref(self, u.getHref()); } pub fn get_search(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_search(page); + const u = try getURL(self, page); + return u.getSearch() orelse ""; } - pub fn set_search(self: *parser.Anchor, v: []const u8, page: *Page) !void { - var u = try url(self, page); - try u.set_search(v, page); - - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + pub fn set_search(self: *parser.Anchor, search: []const u8, page: *Page) !void { + const u = try getURL(self, page); + u.setSearch(search); + return parser.anchorSetHref(self, u.getHref()); } // TODO return a disposable string pub fn get_hash(self: *parser.Anchor, page: *Page) ![]const u8 { - var u = try url(self, page); - return u.get_hash(); + const u = try getURL(self, page); + return u.getHash() orelse ""; } - pub fn set_hash(self: *parser.Anchor, v: []const u8, page: *Page) !void { - var u = try url(self, page); - u.set_hash(v); - const href = try u.get_href(page); - try parser.anchorSetHref(self, href); + pub fn set_hash(self: *parser.Anchor, hash: []const u8, page: *Page) !void { + const u = try getURL(self, page); + u.setHash(hash); + return parser.anchorSetHref(self, u.getHref()); } }; diff --git a/src/browser/page.zig b/src/browser/page.zig index 718ed756e..a553ae11c 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -84,6 +84,11 @@ pub const Page = struct { 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, http_client: *Http.Client, script_manager: ScriptManager, @@ -122,6 +127,21 @@ pub const Page = struct { 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 { const browser = session.browser; const script_manager = ScriptManager.init(browser, self); @@ -160,6 +180,7 @@ pub const Page = struct { self.http_client.abort(); self.script_manager.deinit(); self.url.deinit(); + self.object_data.deinit(self.arena); } fn reset(self: *Page) !void { @@ -170,6 +191,12 @@ pub const Page = struct { self.http_client.abort(); 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.mode = .{ .pre = {} }; _ = self.session.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); @@ -200,6 +227,21 @@ pub const Page = struct { }.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 { // set to include element shadowroots in the dump page: ?*const Page = null, diff --git a/src/url.zig b/src/url.zig index 42d083abc..8266af1d7 100644 --- a/src/url.zig +++ b/src/url.zig @@ -44,6 +44,11 @@ pub const URL = struct { return self; } + /// Forms a `URL` from given `internal`. Memory is not copied. + pub fn fromInternal(internal: ada.URL) URL { + return .{ .internal = internal }; + } + /// Deinitializes internal url. pub fn deinit(self: URL) void { std.debug.assert(self.internal != null); @@ -100,6 +105,28 @@ pub const URL = struct { 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); @@ -108,6 +135,26 @@ pub const URL = struct { 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); } @@ -135,6 +182,11 @@ pub const URL = struct { 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); diff --git a/vendor/ada/root.zig b/vendor/ada/root.zig index 1af8e8235..1a54b40ed 100644 --- a/vendor/ada/root.zig +++ b/vendor/ada/root.zig @@ -71,12 +71,24 @@ 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); From 51a328d3570c89492901db49af4edb2a260b3439 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Sat, 18 Oct 2025 13:20:24 +0300 Subject: [PATCH 21/25] don't link libcpp twice This was causing an issue on ld linker but not on MachO. --- build.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/build.zig b/build.zig index 61667069c..eaa4de7e0 100644 --- a/build.zig +++ b/build.zig @@ -860,7 +860,6 @@ pub fn buildAda(b: *Build, m: *Build.Module) !void { .root_source_file = b.path("vendor/ada/root.zig"), .target = m.resolved_target, .optimize = m.optimize, - .link_libcpp = true, }); // Expose headers; note that "ada.h" is a C++ header so no use here. From 535a21c9f282d753e518a4f60850a61737697dca Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Sat, 18 Oct 2025 16:19:59 +0300 Subject: [PATCH 22/25] change the way ada is linked to the build system Link the ada library to ada module rather than building alongside main module. --- build.zig | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/build.zig b/build.zig index eaa4de7e0..3bc876076 100644 --- a/build.zig +++ b/build.zig @@ -853,23 +853,31 @@ 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 dep_root = ada_dep.path(""); - // Private module that binds ada functions. const ada_mod = b.createModule(.{ .root_source_file = b.path("vendor/ada/root.zig"), - .target = m.resolved_target, - .optimize = m.optimize, }); - // Expose headers; note that "ada.h" is a C++ header so no use here. - ada_mod.addIncludePath(dep_root); + const ada_lib = b.addLibrary(.{ + .name = "ada", + .root_module = b.createModule(.{ + .link_libcpp = true, + .target = m.resolved_target, + .optimize = m.optimize, + }), + .linkage = .static, + }); - ada_mod.addCSourceFiles(.{ - .root = dep_root, - .files = &.{"ada.cpp"}, - .flags = &.{"-std=c++20"}, + 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); } From b87a59fa491cc0a9c51745c653797914dbdd239c Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 20 Oct 2025 12:41:08 +0300 Subject: [PATCH 23/25] href should have `/` if path not provided --- src/tests/html/document.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/html/document.html b/src/tests/html/document.html index b903dd280..988b51e75 100644 --- a/src/tests/html/document.html +++ b/src/tests/html/document.html @@ -68,10 +68,10 @@ let a_again = document.elementFromPoint(1.5, 0.5); 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); - testing.expectEqual('https://lightpanda.io', a_agains[0].href); + testing.expectEqual('https://lightpanda.io/', a_agains[0].href); testing.expectEqual(true, !document.all); From 344420f7084a8208e72c81bfc90019706d945ad0 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 20 Oct 2025 12:41:47 +0300 Subject: [PATCH 24/25] bring back `hostname` getter/setter functions This was a regression while testing things. --- src/browser/html/elements.zig | 55 +++++++++++++++++------------------ 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/src/browser/html/elements.zig b/src/browser/html/elements.zig index 6dd18601b..d58a66944 100644 --- a/src/browser/html/elements.zig +++ b/src/browser/html/elements.zig @@ -355,34 +355,33 @@ pub const HTMLAnchorElement = struct { return parser.anchorSetHref(self, u.getHref()); } - //pub fn get_hostname(self: *parser.Anchor, page: *Page) ![]const u8 { - // const maybe_href_str = try getAnchorHref(self); - // const href_str = maybe_href_str orelse return ""; - // - // const u = try NativeURL.parse(href_str, null); - // defer u.deinit(); - // - // return page.arena.dupe(u8, u.getHostname()); - //} - - //pub fn set_hostname(self: *parser.Anchor, v: []const u8) !void { - // const maybe_href_str = try getAnchorHref(self); - // - // if (maybe_href_str) |href_str| { - // const u = try NativeURL.parse(href_str, null); - // defer u.deinit(); - // - // try u.setHostname(v); - // - // return parser.anchorSetHref(self, u.getHref()); - // } - // - // // No href string there; use the given value as href. - // const u = try NativeURL.parse(v, null); - // defer u.deinit(); - // - // return parser.anchorSetHref(self, u.getHref()); - //} + pub fn get_hostname(self: *parser.Anchor, page: *Page) []const u8 { + const u = getURL(self, page) catch return ""; + return u.getHostname(); + } + + pub fn set_hostname(self: *parser.Anchor, hostname: []const u8, page: *Page) !void { + const u = blk: { + if (page.getObjectData(self)) |internal_url| { + break :blk NativeURL.fromInternal(internal_url); + } + + 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 pub fn get_port(self: *parser.Anchor, page: *Page) ![]const u8 { From fa00a5da529a2cecb9dfbc42ea3b6887c7fd0ba5 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 20 Oct 2025 12:43:36 +0300 Subject: [PATCH 25/25] fix link element test Changes are made regarding to `host`, `port` and `hostname`. Definitions are provided by MDN. --- src/tests/html/link.html | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/tests/html/link.html b/src/tests/html/link.html index 0f1d869bf..0433a0cd9 100644 --- a/src/tests/html/link.html +++ b/src/tests/html/link.html @@ -16,8 +16,10 @@ testing.expectEqual('https://lightpanda.io', link.origin); link.host = 'lightpanda.io:443'; - testing.expectEqual('lightpanda.io:443', link.host); - testing.expectEqual('443', link.port); + // Port is omitted if its the default one for the scheme. + 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); link.host = 'lightpanda.io'; @@ -42,9 +44,10 @@ testing.expectEqual('', link.port); 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('https://foo.bar:443/?q=bar#frag', link.href); + testing.expectEqual('https://foo.bar/?q=bar#frag', link.href); link.port = null; testing.expectEqual('https://foo.bar/?q=bar#frag', link.href);