From 2dc41ab180fb261370d4dfd27ec2b43b252a9753 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Mon, 8 Jun 2026 16:38:34 +0300 Subject: [PATCH] `URL`: migrate search getter/setter, setHref and toString --- src/browser/webapi/URL.zig | 135 ++++++++++++++++++------------------- src/html5ever/url.rs | 6 ++ src/sys/url.zig | 3 + 3 files changed, 75 insertions(+), 69 deletions(-) diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index 93df470d..33988fc0 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -19,21 +19,16 @@ 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; -const Allocator = std.mem.Allocator; - const URL = @This(); -_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, // convenience @@ -180,29 +175,54 @@ pub fn setPort(self: *URL, maybe_value: ?[]const u8) void { } pub fn getSearch(self: *const URL, exec: *const Execution) ![]const u8 { - // If searchParams has been accessed, generate search from it - if (self._search_params) |sp| { - if (sp.getSize() == 0) { + if (self._search_params) |search_params| { + if (search_params.getSize() == 0) { return ""; } + var buf = std.Io.Writer.Allocating.init(exec.call_arena); try buf.writer.writeByte('?'); - try sp.toString(&buf.writer); + try search_params.toString(&buf.writer); return buf.written(); } - return U.getSearch(self._raw); + + var out: [*]const u8 = undefined; + var len: usize = 0; + const res = U_.url_get_query(self._url, &out, &len); + if (res != 0 or len == 0) { + return ""; + } + + // rust-url's query() omits the '?', which always precedes the query. + return (out - 1)[0 .. len + 1]; } 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); + // Empty value clears the query entirely. + if (value.len == 0) { + // Reset searchParams. + if (self._search_params) |search_params| { + search_params._params = .empty; + } - // 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); + U_.url_set_query_to_null(self._url); + return; } + + // Strip a single leading '?', then set the query. + const query = if (value[0] == '?') value[1..] else value; + + const res = U_.url_set_query(self._url, query.ptr, query.len); + if (res != 0) { + return error.SetSearch; + } + + // If searchParams exists, update it too. + const search_params = self._search_params orelse return; + var out: [*]const u8 = undefined; + var len: usize = 0; + const search_value = if (U_.url_get_query(self._url, &out, &len) == 0) out[0..len] else ""; + try search_params.updateFromString(search_value, exec); } pub fn getHash(self: *const URL) []const u8 { @@ -237,9 +257,10 @@ pub fn getSearchParams(self: *URL, exec: *const Execution) !*URLSearchParams { return sp; } - // Get current search string (without the '?') - const search = try self.getSearch(exec); - const search_value = if (search.len > 0) search[1..] else ""; + // Get current search string (omitting '?'). + var out: [*]const u8 = undefined; + var len: usize = 0; + const search_value = if (U_.url_get_query(self._url, &out, &len) == 0) out[0..len] else ""; const params = try URLSearchParams.init(.{ .query_string = search_value }, exec); self._search_params = params; @@ -254,66 +275,42 @@ pub fn getOrigin(self: *const URL, exec: *const Execution) ![]const u8 { } pub fn setHref(self: *URL, value: []const u8, exec: *const Execution) !void { - // This behaves the same as initializing a URL. + // Parse first: a failed href setter must leave the URL unchanged (and we + // must not free self._url before we know we have a replacement, or any + // later access would be a use-after-free). var err: i32 = 0; const url = U_.url_parse(value.ptr, value.len, &err) orelse return error.TypeError; - errdefer U_.url_free(url); - // 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(self.toString()); - const search_value = if (search.len > 0) search[1..] else ""; - try sp.updateFromString(search_value, exec); - } -} - -pub fn toString(self: *const URL) []const u8 { - var ptr: [*]const u8 = undefined; + // Update existing searchParams if exists. + const search_params = self._search_params orelse return; + var out: [*]const u8 = undefined; var len: usize = 0; - U_.url_to_string(self._url, &ptr, &len); - return ptr[0..len]; + const search_value = if (U_.url_get_query(url, &out, &len) == 0) out[0..len] else ""; + try search_params.updateFromString(search_value, exec); } -pub fn toString1(self: *const URL, exec: *const Execution) ![:0]const u8 { - const sp = self._search_params orelse { - return self._raw; - }; - - // Rebuild URL from searchParams - const raw = self._raw; - - // Find the base (everything before ? or #) - const base_end = std.mem.indexOfAnyPos(u8, raw, 0, "?#") orelse raw.len; - const base = raw[0..base_end]; - - // Get the hash if it exists - const hash = self.getHash(); - - // Build the new URL string - var buf = std.Io.Writer.Allocating.init(exec.call_arena); - try buf.writer.writeAll(base); - - // Add / if missing (e.g., "https://example.com" -> "https://example.com/") - // Only add if pathname is just "/" and not already in the base - const pathname = U.getPathname(raw); - if (std.mem.eql(u8, pathname, "/") and !std.mem.endsWith(u8, base, "/")) { - try buf.writer.writeByte('/'); +pub fn toString(self: *const URL, exec: *const Execution) ![]const u8 { + if (self._search_params) |search_params| { + if (search_params.getSize() == 0) { + U_.url_set_query_to_null(self._url); + } else { + var buf = std.Io.Writer.Allocating.init(exec.call_arena); + defer buf.deinit(); + try search_params.toString(&buf.writer); + const query = buf.written(); + if (U_.url_set_query(self._url, query.ptr, query.len) != 0) { + return error.ToString; + } + } } - // Only add ? if there are params - if (sp.getSize() > 0) { - try buf.writer.writeByte('?'); - try sp.toString(&buf.writer); - } - - try buf.writer.writeAll(hash); - try buf.writer.writeByte(0); - - return buf.written()[0 .. buf.written().len - 1 :0]; + var out: [*]const u8 = undefined; + var len: usize = 0; + U_.url_to_string(self._url, &out, &len); + return out[0..len]; } pub fn canParse(url: []const u8, maybe_base: ?[]const u8) bool { diff --git a/src/html5ever/url.rs b/src/html5ever/url.rs index 406ea829..f3d7aef8 100644 --- a/src/html5ever/url.rs +++ b/src/html5ever/url.rs @@ -461,6 +461,12 @@ pub unsafe extern "C" fn url_set_query(url: *mut Url, ptr: *const c_uchar, len: 0 } +#[no_mangle] +pub unsafe extern "C" fn url_set_query_to_null(url: *mut Url) { + let url = unsafe { &mut *url }; + url.set_query(None); +} + #[no_mangle] pub unsafe extern "C" fn url_get_query( url: *const Url, diff --git a/src/sys/url.zig b/src/sys/url.zig index 851fa2a4..b5f075d3 100644 --- a/src/sys/url.zig +++ b/src/sys/url.zig @@ -77,6 +77,9 @@ pub extern "c" fn url_get_scheme(url: *const Url, out_ptr: *[*]const u8, out_len pub extern "c" fn url_set_fragment(url: *Url, ptr: [*]const u8, len: usize) i32; pub extern "c" fn url_set_fragment_to_null(url: *Url) void; pub extern "c" fn url_get_fragment(url: *const Url, out_ptr: *[*]const u8, out_len: *usize) i32; +pub extern "c" fn url_set_query(url: *Url, ptr: [*]const u8, len: usize) i32; +pub extern "c" fn url_set_query_to_null(url: *Url) void; +pub extern "c" fn url_get_query(url: *const Url, out_ptr: *[*]const u8, out_len: *usize) i32; extern "c" fn url_get_port(url: *const Url) i32; pub inline fn urlGetPort(url: *const Url) ?u16 {