fix: precent-encode hash and search

This commit is contained in:
Pierre Tachoire
2026-03-23 17:22:50 +01:00
parent 58c18114a5
commit 8ada67637f
2 changed files with 42 additions and 9 deletions

View File

@@ -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);
}

View File

@@ -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;