From 6dfeabc99998b390d92066f3587ac4e3a5537d68 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Fri, 5 Jun 2026 17:44:28 +0300 Subject: [PATCH] spread new implementation (phase 1) Will have to come up with a solution for search getter/setter and URLSearchParams still; We've also encountered with a bug in URLSearchParams, a fix for it will also be included in the second phase. --- src/browser/webapi/Location.zig | 2 +- src/browser/webapi/URL.zig | 266 ++++++++++++++++++++------------ 2 files changed, 172 insertions(+), 96 deletions(-) diff --git a/src/browser/webapi/Location.zig b/src/browser/webapi/Location.zig index f57db2c04..718534b10 100644 --- a/src/browser/webapi/Location.zig +++ b/src/browser/webapi/Location.zig @@ -116,7 +116,7 @@ pub fn reload(_: *const Location, frame: *Frame) !void { } pub fn toString(self: *const Location, exec: *const js.Execution) ![:0]const u8 { - return self._url.toString(exec); + return self._url.toString1(exec); } pub const JsApi = struct { diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index 6ca775ffc..93df470d0 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -20,6 +20,7 @@ const std = @import("std"); const js = @import("../js/js.zig"); const U = @import("../URL.zig"); +const U_ = @import("../../sys/url.zig"); const URLSearchParams = @import("net/URLSearchParams.zig"); const Blob = @import("Blob.zig"); const Execution = js.Execution; @@ -28,7 +29,10 @@ const Allocator = std.mem.Allocator; const URL = @This(); -_raw: [:0]const u8, +_raw: [:0]const u8 = undefined, +_url: *U_.Url = undefined, +/// Largest port possible is 65535; which require 5 bytes. +_port: [5]u8 = undefined, _arena: ?Allocator = null, _search_params: ?*URLSearchParams = null, @@ -36,83 +40,143 @@ _search_params: ?*URLSearchParams = null, pub const resolve = @import("../URL.zig").resolve; pub const eqlDocument = @import("../URL.zig").eqlDocument; -pub fn init(url: [:0]const u8, base_: ?[:0]const u8, exec: *const Execution) !*URL { - const arena = exec.arena; - const context_url = exec.url.*; +pub fn init(url: []const u8, maybe_base: ?[]const u8, exec: *const Execution) !*URL { + // NOTE: about:blank address is valid in rust-url. - if (std.mem.eql(u8, url, "about:blank")) { - return exec._factory.create(URL{ - ._raw = "about:blank", - ._arena = arena, - }); + var err: i32 = 0; + if (maybe_base) |base| { + const base_url = U_.url_parse(base.ptr, base.len, &err) orelse return error.TypeError; + errdefer U_.url_free(base_url); + + const joined_url = U_.url_join(base_url, url.ptr, url.len, &err) orelse return error.TypeError; + errdefer U_.url_free(joined_url); + // `base_url` has no use now. + U_.url_free(base_url); + + return exec._factory.create(URL{ ._url = joined_url }); } - const url_is_absolute = @import("../URL.zig").isCompleteHTTPUrl(url); - const base = if (base_) |b| blk: { - // If URL is absolute, base is ignored (but we still use context url internally) - if (url_is_absolute) { - break :blk context_url; - } - // For relative URLs, base must be a valid absolute URL - if (!@import("../URL.zig").isCompleteHTTPUrl(b)) { - return error.TypeError; - } - break :blk b; - } else if (!url_is_absolute) { + const u = U_.url_parse(url.ptr, url.len, &err) orelse { return error.TypeError; - } else context_url; + }; + errdefer U_.url_free(u); - const raw = try resolve(arena, base, url, .{ .always_dupe = true }); - - return exec._factory.create(URL{ - ._raw = raw, - ._arena = arena, - }); + return exec._factory.create(URL{ ._url = u }); } pub fn getUsername(self: *const URL) []const u8 { - return U.getUsername(self._raw); + var out: [*]const u8 = undefined; + var len: usize = 0; + U_.url_get_username(self._url, &out, &len); + return out[0..len]; } pub fn setUsername(self: *URL, value: []const u8) !void { - const allocator = self._arena orelse return error.NoAllocator; - self._raw = try U.setUsername(self._raw, value, allocator); + const res = U_.url_set_username(self._url, value.ptr, value.len); + if (res != 0) { + return error.SetUsername; + } } pub fn getPassword(self: *const URL) []const u8 { - return U.getPassword(self._raw); + var out: [*]const u8 = undefined; + var len: usize = 0; + const res = U_.url_get_password(self._url, &out, &len); + if (res != 0) { + return ""; + } + return out[0..len]; } pub fn setPassword(self: *URL, value: []const u8) !void { - const allocator = self._arena orelse return error.NoAllocator; - self._raw = try U.setPassword(self._raw, value, allocator); + const res = U_.url_set_password(self._url, value.ptr, value.len); + if (res != 0) { + return error.SetPassword; + } } pub fn getPathname(self: *const URL) []const u8 { - return U.getPathname(self._raw); + var out: [*]const u8 = undefined; + var len: usize = 0; + U_.url_get_path(self._url, &out, &len); + return out[0..len]; +} + +pub fn setPathname(self: *URL, value: []const u8) !void { + const res = U_.url_set_path(self._url, value.ptr, value.len); + if (res != 0) { + return error.SetPathname; + } } pub fn getProtocol(self: *const URL) []const u8 { - return U.getProtocol(self._raw); + var out: [*]const u8 = undefined; + var len: usize = 0; + U_.url_get_scheme(self._url, &out, &len); + // rust-url's scheme() omits the ':'. The serialization always has it right + // after the scheme ("https://..."), so we extend the borrowed slice by one. + return out[0 .. len + 1]; +} + +pub fn setProtocol(self: *URL, value: []const u8) !void { + const res = U_.url_set_scheme(self._url, value.ptr, value.len); + if (res != 0) { + return error.SetProtocol; + } } pub fn getHostname(self: *const URL) []const u8 { - return U.getHostname(self._raw); + var out: [*]const u8 = undefined; + var len: usize = 0; + if (U_.url_get_hostname(self._url, &out, &len) != 0) { + return ""; + } + return out[0..len]; +} + +pub fn setHostname(self: *URL, value: []const u8) !void { + const res = U_.url_set_hostname(self._url, value.ptr, value.len); + if (res != 0) { + return error.SetHostname; + } } pub fn getHost(self: *const URL) []const u8 { - return U.getHost(self._raw); + var out: [*]const u8 = undefined; + var len: usize = 0; + if (U_.url_get_host(self._url, &out, &len) != 0) { + return ""; + } + return out[0..len]; } -pub fn getPort(self: *const URL) []const u8 { - return U.getPort(self._raw); +pub fn setHost(self: *URL, value: []const u8) !void { + const res = U_.url_set_host(self._url, value.ptr, value.len); + if (res != 0) { + return error.SetHost; + } } -pub fn getOrigin(self: *const URL, exec: *const Execution) ![]const u8 { - return (try U.getOrigin(exec.call_arena, self._raw)) orelse { - // yes, a null string, that's what the spec wants - return "null"; +pub fn getPort(self: *URL) []const u8 { + const port = U_.urlGetPort(self._url) orelse return ""; + return std.fmt.bufPrint(&self._port, "{d}", .{port}) catch unreachable; +} + +/// Spec requires us to silently ignore errors of this setter. +pub fn setPort(self: *URL, maybe_value: ?[]const u8) void { + // A null or empty value clears the port. + const value = maybe_value orelse { + _ = U_.url_set_port_to_null(self._url); + return; }; + if (value.len == 0) { + _ = U_.url_set_port_to_null(self._url); + return; + } + + // Ignore invalid port numbers, leaving the port unchanged. + const port = std.fmt.parseInt(u16, value, 10) catch return; + _ = U_.url_set_port(self._url, port); } pub fn getSearch(self: *const URL, exec: *const Execution) ![]const u8 { @@ -129,8 +193,43 @@ pub fn getSearch(self: *const URL, exec: *const Execution) ![]const u8 { return U.getSearch(self._raw); } +pub fn setSearch(self: *URL, value: []const u8, exec: *const Execution) !void { + const allocator = self._arena orelse return error.NoAllocator; + self._raw = try U.setSearch(self._raw, value, allocator); + + // Update existing searchParams if it exists + if (self._search_params) |sp| { + const search = U.getSearch(self._raw); + const search_value = if (search.len > 0) search[1..] else ""; + try sp.updateFromString(search_value, exec); + } +} + pub fn getHash(self: *const URL) []const u8 { - return U.getHash(self._raw); + var out: [*]const u8 = undefined; + var len: usize = 0; + const res = U_.url_get_fragment(self._url, &out, &len); + // WHATWG `hash` is "" for both a null and an empty fragment. + if (res != 0 or len == 0) { + return ""; + } + // rust-url's fragment() omits the '#', which always precedes the fragment + // in the serialization, so step the borrowed slice back one byte for it. + return (out - 1)[0 .. len + 1]; +} + +pub fn setHash(self: *URL, value: []const u8) !void { + // An empty value clears the fragment entirely (removes the '#'). + if (value.len == 0) { + U_.url_set_fragment_to_null(self._url); + return; + } + // Strip a single leading '#', then set the fragment. + const fragment = if (value[0] == '#') value[1..] else value; + const res = U_.url_set_fragment(self._url, fragment.ptr, fragment.len); + if (res != 0) { + return error.SetHash; + } } pub fn getSearchParams(self: *URL, exec: *const Execution) !*URLSearchParams { @@ -147,62 +246,39 @@ pub fn getSearchParams(self: *URL, exec: *const Execution) !*URLSearchParams { return params; } +pub fn getOrigin(self: *const URL, exec: *const Execution) ![]const u8 { + const origin = U_.url_get_origin(self._url); + defer origin.deinit(); + + return exec.call_arena.dupe(u8, origin.slice()); +} + pub fn setHref(self: *URL, value: []const u8, exec: *const Execution) !void { - const base = if (U.isCompleteHTTPUrl(value)) exec.url.* else self._raw; - const raw = try U.resolve(self._arena orelse exec.arena, base, value, .{ .always_dupe = true }); - self._raw = raw; + // This behaves the same as initializing a URL. + var err: i32 = 0; + const url = U_.url_parse(value.ptr, value.len, &err) orelse return error.TypeError; + errdefer U_.url_free(url); - // Update existing searchParams if it exists + // Free the current URL. + U_.url_free(self._url); + self._url = url; + + // Update existing searchParams if it exists. if (self._search_params) |sp| { - const search = U.getSearch(raw); + const search = U.getSearch(self.toString()); const search_value = if (search.len > 0) search[1..] else ""; try sp.updateFromString(search_value, exec); } } -pub fn setProtocol(self: *URL, value: []const u8) !void { - const allocator = self._arena orelse return error.NoAllocator; - self._raw = try U.setProtocol(self._raw, value, allocator); +pub fn toString(self: *const URL) []const u8 { + var ptr: [*]const u8 = undefined; + var len: usize = 0; + U_.url_to_string(self._url, &ptr, &len); + return ptr[0..len]; } -pub fn setHost(self: *URL, value: []const u8) !void { - const allocator = self._arena orelse return error.NoAllocator; - self._raw = try U.setHost(self._raw, value, allocator); -} - -pub fn setHostname(self: *URL, value: []const u8) !void { - const allocator = self._arena orelse return error.NoAllocator; - self._raw = try U.setHostname(self._raw, value, allocator); -} - -pub fn setPort(self: *URL, value: ?[]const u8) !void { - const allocator = self._arena orelse return error.NoAllocator; - self._raw = try U.setPort(self._raw, value, allocator); -} - -pub fn setPathname(self: *URL, value: []const u8) !void { - const allocator = self._arena orelse return error.NoAllocator; - self._raw = try U.setPathname(self._raw, value, allocator); -} - -pub fn setSearch(self: *URL, value: []const u8, exec: *const Execution) !void { - const allocator = self._arena orelse return error.NoAllocator; - self._raw = try U.setSearch(self._raw, value, allocator); - - // Update existing searchParams if it exists - if (self._search_params) |sp| { - const search = U.getSearch(self._raw); - const search_value = if (search.len > 0) search[1..] else ""; - try sp.updateFromString(search_value, exec); - } -} - -pub fn setHash(self: *URL, value: []const u8) !void { - const allocator = self._arena orelse return error.NoAllocator; - self._raw = try U.setHash(self._raw, value, allocator); -} - -pub fn toString(self: *const URL, exec: *const Execution) ![:0]const u8 { +pub fn toString1(self: *const URL, exec: *const Execution) ![:0]const u8 { const sp = self._search_params orelse { return self._raw; }; @@ -240,11 +316,11 @@ pub fn toString(self: *const URL, exec: *const Execution) ![:0]const u8 { return buf.written()[0 .. buf.written().len - 1 :0]; } -pub fn canParse(url: []const u8, base_: ?[]const u8) bool { - if (base_) |b| { - return U.isCompleteHTTPUrl(b); +pub fn canParse(url: []const u8, maybe_base: ?[]const u8) bool { + if (maybe_base) |base| { + return U_.url_can_parse_with_base(base.ptr, base.len, url.ptr, url.len); } - return U.isCompleteHTTPUrl(url); + return U_.url_can_parse(url.ptr, url.len); } pub fn createObjectURL(blob: *Blob, exec: *const Execution) ![]const u8 {