From 8ada67637f2824ac3390fdbf234fd77ee668d635 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 23 Mar 2026 17:22:50 +0100 Subject: [PATCH] fix: precent-encode hash and search --- src/browser/URL.zig | 23 ++++++++++++++--------- src/browser/tests/url.html | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/browser/URL.zig b/src/browser/URL.zig index 6dce6f1d..38429c54 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -204,7 +204,7 @@ pub fn ensureEncoded(allocator: Allocator, url: [:0]const u8) ![:0]const u8 { return buf.items[0 .. buf.items.len - 1 :0]; } -const EncodeSet = enum { path, query, userinfo }; +const EncodeSet = enum { path, query, userinfo, fragment }; fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime encode_set: EncodeSet) ![]const u8 { // Check if encoding is needed @@ -256,8 +256,10 @@ fn shouldPercentEncode(c: u8, comptime encode_set: EncodeSet) bool { ';', '=' => encode_set == .userinfo, // Separators: userinfo must encode these '/', ':', '@' => encode_set == .userinfo, - // '?' is allowed in queries but not in paths or userinfo + // '?' is allowed in queries only '?' => encode_set != .query, + // '#' is allowed in fragments only + '#' => encode_set != .fragment, // Everything else needs encoding (including space) else => true, }; @@ -595,7 +597,6 @@ pub fn setPathname(current: [:0]const u8, value: []const u8, allocator: Allocato const search = getSearch(current); const hash = getHash(current); - // Percent-encode the pathname per the URL spec (spaces → %20, etc.) const encoded = try percentEncodeSegment(allocator, value, .path); // Add / prefix if not present and value is not empty @@ -613,11 +614,13 @@ pub fn setSearch(current: [:0]const u8, value: []const u8, allocator: Allocator) const pathname = getPathname(current); const hash = getHash(current); + const encoded = try percentEncodeSegment(allocator, value, .query); + // Add ? prefix if not present and value is not empty - const search = if (value.len > 0 and value[0] != '?') - try std.fmt.allocPrint(allocator, "?{s}", .{value}) + const search = if (encoded.len > 0 and value[0] != '?') + try std.fmt.allocPrint(allocator, "?{s}", .{encoded}) else - value; + encoded; return buildUrl(allocator, protocol, host, pathname, search, hash); } @@ -628,11 +631,13 @@ pub fn setHash(current: [:0]const u8, value: []const u8, allocator: Allocator) ! const pathname = getPathname(current); const search = getSearch(current); + const encoded = try percentEncodeSegment(allocator, value, .fragment); + // Add # prefix if not present and value is not empty - const hash = if (value.len > 0 and value[0] != '#') - try std.fmt.allocPrint(allocator, "#{s}", .{value}) + const hash = if (encoded.len > 0 and encoded[0] != '#') + try std.fmt.allocPrint(allocator, "#{s}", .{encoded}) else - value; + encoded; return buildUrl(allocator, protocol, host, pathname, search, hash); } diff --git a/src/browser/tests/url.html b/src/browser/tests/url.html index 5e126db5..3b1d2add 100644 --- a/src/browser/tests/url.html +++ b/src/browser/tests/url.html @@ -685,6 +685,20 @@ testing.expectEqual('', url.hash); } +{ + const url = new URL('https://example.com/path'); + url.hash = '#a b'; + testing.expectEqual('https://example.com/path#a%20b', url.href); + testing.expectEqual('#a%20b', url.hash); +} + +{ + const url = new URL('https://example.com/path'); + url.hash = 'a b'; + testing.expectEqual('https://example.com/path#a%20b', url.href); + testing.expectEqual('#a%20b', url.hash); +} + { const url = new URL('https://example.com/path?a=b'); url.search = ''; @@ -702,6 +716,20 @@ testing.expectEqual(null, url.searchParams.get('a')); } +{ + const url = new URL('https://example.com/path?a=b'); + const sp = url.searchParams; + testing.expectEqual('b', sp.get('a')); + url.search = 'c=d b'; + + testing.expectEqual('d b', url.searchParams.get('c')); + testing.expectEqual(null, url.searchParams.get('a')); + + url.search = 'c d=d b'; + testing.expectEqual('d b', url.searchParams.get('c d')); + testing.expectEqual(null, url.searchParams.get('c')); +} + { const url = new URL('https://example.com/path?a=b'); const sp = url.searchParams;