Merge pull request #1750 from lightpanda-io/url_set_username_password

Add setters to URL.username and URL.password
This commit is contained in:
Pierre Tachoire
2026-03-10 10:15:10 +01:00
committed by GitHub
3 changed files with 201 additions and 20 deletions

View File

@@ -167,17 +167,17 @@ pub fn ensureEncoded(allocator: Allocator, url: [:0]const u8) ![:0]const u8 {
const query_end = if (query_start) |_| (fragment_start orelse url.len) else path_end; const query_end = if (query_start) |_| (fragment_start orelse url.len) else path_end;
const path_to_encode = url[path_start..path_end]; const path_to_encode = url[path_start..path_end];
const encoded_path = try percentEncodeSegment(allocator, path_to_encode, true); const encoded_path = try percentEncodeSegment(allocator, path_to_encode, .path);
const encoded_query = if (query_start) |qs| blk: { const encoded_query = if (query_start) |qs| blk: {
const query_to_encode = url[qs + 1 .. query_end]; const query_to_encode = url[qs + 1 .. query_end];
const encoded = try percentEncodeSegment(allocator, query_to_encode, false); const encoded = try percentEncodeSegment(allocator, query_to_encode, .query);
break :blk encoded; break :blk encoded;
} else null; } else null;
const encoded_fragment = if (fragment_start) |fs| blk: { const encoded_fragment = if (fragment_start) |fs| blk: {
const fragment_to_encode = url[fs + 1 ..]; const fragment_to_encode = url[fs + 1 ..];
const encoded = try percentEncodeSegment(allocator, fragment_to_encode, false); const encoded = try percentEncodeSegment(allocator, fragment_to_encode, .query);
break :blk encoded; break :blk encoded;
} else null; } else null;
@@ -204,11 +204,13 @@ pub fn ensureEncoded(allocator: Allocator, url: [:0]const u8) ![:0]const u8 {
return buf.items[0 .. buf.items.len - 1 :0]; return buf.items[0 .. buf.items.len - 1 :0];
} }
fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime is_path: bool) ![]const u8 { const EncodeSet = enum { path, query, userinfo };
fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime encode_set: EncodeSet) ![]const u8 {
// Check if encoding is needed // Check if encoding is needed
var needs_encoding = false; var needs_encoding = false;
for (segment) |c| { for (segment) |c| {
if (shouldPercentEncode(c, is_path)) { if (shouldPercentEncode(c, encode_set)) {
needs_encoding = true; needs_encoding = true;
break; break;
} }
@@ -235,7 +237,7 @@ fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime is_p
} }
} }
if (shouldPercentEncode(c, is_path)) { if (shouldPercentEncode(c, encode_set)) {
try buf.writer(allocator).print("%{X:0>2}", .{c}); try buf.writer(allocator).print("%{X:0>2}", .{c});
} else { } else {
try buf.append(allocator, c); try buf.append(allocator, c);
@@ -245,16 +247,17 @@ fn percentEncodeSegment(allocator: Allocator, segment: []const u8, comptime is_p
return buf.items; return buf.items;
} }
fn shouldPercentEncode(c: u8, comptime is_path: bool) bool { fn shouldPercentEncode(c: u8, comptime encode_set: EncodeSet) bool {
return switch (c) { return switch (c) {
// Unreserved characters (RFC 3986) // Unreserved characters (RFC 3986)
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => false, 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => false,
// sub-delims allowed in both path and query // sub-delims allowed in path/query but some must be encoded in userinfo
'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => false, '!', '$', '&', '\'', '(', ')', '*', '+', ',' => false,
// Separators allowed in both path and query ';', '=' => encode_set == .userinfo,
'/', ':', '@' => false, // Separators: userinfo must encode these
// Query-specific: '?' is allowed in queries but not in paths '/', ':', '@' => encode_set == .userinfo,
'?' => comptime is_path, // '?' is allowed in queries but not in paths or userinfo
'?' => encode_set != .query,
// Everything else needs encoding (including space) // Everything else needs encoding (including space)
else => true, else => true,
}; };
@@ -514,7 +517,7 @@ pub fn setHost(current: [:0]const u8, value: []const u8, allocator: Allocator) !
const search = getSearch(current); const search = getSearch(current);
const hash = getHash(current); const hash = getHash(current);
// Check if the host includes a port // Check if the new value includes a port
const colon_pos = std.mem.lastIndexOfScalar(u8, value, ':'); const colon_pos = std.mem.lastIndexOfScalar(u8, value, ':');
const clean_host = if (colon_pos) |pos| blk: { const clean_host = if (colon_pos) |pos| blk: {
const port_str = value[pos + 1 ..]; const port_str = value[pos + 1 ..];
@@ -526,7 +529,14 @@ pub fn setHost(current: [:0]const u8, value: []const u8, allocator: Allocator) !
break :blk value[0..pos]; break :blk value[0..pos];
} }
break :blk value; break :blk value;
} else value; } else blk: {
// No port in new value - preserve existing port
const current_port = getPort(current);
if (current_port.len > 0) {
break :blk try std.fmt.allocPrint(allocator, "{s}:{s}", .{ value, current_port });
}
break :blk value;
};
return buildUrl(allocator, protocol, clean_host, pathname, search, hash); return buildUrl(allocator, protocol, clean_host, pathname, search, hash);
} }
@@ -544,6 +554,9 @@ pub fn setHostname(current: [:0]const u8, value: []const u8, allocator: Allocato
pub fn setPort(current: [:0]const u8, value: ?[]const u8, allocator: Allocator) ![:0]const u8 { pub fn setPort(current: [:0]const u8, value: ?[]const u8, allocator: Allocator) ![:0]const u8 {
const hostname = getHostname(current); const hostname = getHostname(current);
const protocol = getProtocol(current); const protocol = getProtocol(current);
const pathname = getPathname(current);
const search = getSearch(current);
const hash = getHash(current);
// Handle null or default ports // Handle null or default ports
const new_host = if (value) |port_str| blk: { const new_host = if (value) |port_str| blk: {
@@ -560,7 +573,7 @@ pub fn setPort(current: [:0]const u8, value: ?[]const u8, allocator: Allocator)
break :blk try std.fmt.allocPrint(allocator, "{s}:{s}", .{ hostname, port_str }); break :blk try std.fmt.allocPrint(allocator, "{s}:{s}", .{ hostname, port_str });
} else hostname; } else hostname;
return setHost(current, new_host, allocator); return buildUrl(allocator, protocol, new_host, pathname, search, hash);
} }
pub fn setPathname(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 { pub fn setPathname(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
@@ -608,6 +621,64 @@ pub fn setHash(current: [:0]const u8, value: []const u8, allocator: Allocator) !
return buildUrl(allocator, protocol, host, pathname, search, hash); return buildUrl(allocator, protocol, host, pathname, search, hash);
} }
pub fn setUsername(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
const protocol = getProtocol(current);
const host = getHost(current);
const pathname = getPathname(current);
const search = getSearch(current);
const hash = getHash(current);
const password = getPassword(current);
const encoded_username = try percentEncodeSegment(allocator, value, .userinfo);
return buildUrlWithUserInfo(allocator, protocol, encoded_username, password, host, pathname, search, hash);
}
pub fn setPassword(current: [:0]const u8, value: []const u8, allocator: Allocator) ![:0]const u8 {
const protocol = getProtocol(current);
const host = getHost(current);
const pathname = getPathname(current);
const search = getSearch(current);
const hash = getHash(current);
const username = getUsername(current);
const encoded_password = try percentEncodeSegment(allocator, value, .userinfo);
return buildUrlWithUserInfo(allocator, protocol, username, encoded_password, host, pathname, search, hash);
}
fn buildUrlWithUserInfo(
allocator: Allocator,
protocol: []const u8,
username: []const u8,
password: []const u8,
host: []const u8,
pathname: []const u8,
search: []const u8,
hash: []const u8,
) ![:0]const u8 {
if (username.len == 0 and password.len == 0) {
return buildUrl(allocator, protocol, host, pathname, search, hash);
} else if (password.len == 0) {
return std.fmt.allocPrintSentinel(allocator, "{s}//{s}@{s}{s}{s}{s}", .{
protocol,
username,
host,
pathname,
search,
hash,
}, 0);
} else {
return std.fmt.allocPrintSentinel(allocator, "{s}//{s}:{s}@{s}{s}{s}{s}", .{
protocol,
username,
password,
host,
pathname,
search,
hash,
}, 0);
}
}
pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []const u8) ![:0]const u8 { pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []const u8) ![:0]const u8 {
if (query_string.len == 0) { if (query_string.len == 0) {
return arena.dupeZ(u8, url); return arena.dupeZ(u8, url);

View File

@@ -218,6 +218,106 @@
testing.expectEqual('', url.password); testing.expectEqual('', url.password);
} }
{
const url = new URL('https://example.com/path');
url.username = 'newuser';
testing.expectEqual('newuser', url.username);
testing.expectEqual('https://newuser@example.com/path', url.href);
}
{
const url = new URL('https://olduser@example.com/path');
url.username = 'newuser';
testing.expectEqual('newuser', url.username);
testing.expectEqual('https://newuser@example.com/path', url.href);
}
{
const url = new URL('https://olduser:pass@example.com/path');
url.username = 'newuser';
testing.expectEqual('newuser', url.username);
testing.expectEqual('pass', url.password);
testing.expectEqual('https://newuser:pass@example.com/path', url.href);
}
{
const url = new URL('https://user@example.com/path');
url.password = 'secret';
testing.expectEqual('user', url.username);
testing.expectEqual('secret', url.password);
testing.expectEqual('https://user:secret@example.com/path', url.href);
}
{
const url = new URL('https://user:oldpass@example.com/path');
url.password = 'newpass';
testing.expectEqual('user', url.username);
testing.expectEqual('newpass', url.password);
testing.expectEqual('https://user:newpass@example.com/path', url.href);
}
{
const url = new URL('https://user:pass@example.com/path');
url.username = '';
url.password = '';
testing.expectEqual('', url.username);
testing.expectEqual('', url.password);
testing.expectEqual('https://example.com/path', url.href);
}
{
const url = new URL('https://example.com/path');
url.username = 'user@domain';
testing.expectEqual('user%40domain', url.username);
testing.expectEqual('https://user%40domain@example.com/path', url.href);
}
{
const url = new URL('https://example.com/path');
url.username = 'user:name';
testing.expectEqual('user%3Aname', url.username);
}
{
const url = new URL('https://example.com/path');
url.password = 'pass@word';
testing.expectEqual('pass%40word', url.password);
}
{
const url = new URL('https://example.com/path');
url.password = 'pass:word';
testing.expectEqual('pass%3Aword', url.password);
}
{
const url = new URL('https://example.com/path');
url.username = 'user/name';
testing.expectEqual('user%2Fname', url.username);
}
{
const url = new URL('https://example.com/path');
url.password = 'pass?word';
testing.expectEqual('pass%3Fword', url.password);
}
{
const url = new URL('https://user%40domain:pass%3Aword@example.com/path');
testing.expectEqual('user%40domain', url.username);
testing.expectEqual('pass%3Aword', url.password);
}
{
const url = new URL('https://example.com:8080/path?a=b#hash');
url.username = 'user';
url.password = 'pass';
testing.expectEqual('https://user:pass@example.com:8080/path?a=b#hash', url.href);
testing.expectEqual('8080', url.port);
testing.expectEqual('?a=b', url.search);
testing.expectEqual('#hash', url.hash);
}
{ {
const url = new URL('http://user:pass@example.com:8080/path?query=1#hash'); const url = new URL('http://user:pass@example.com:8080/path?query=1#hash');
testing.expectEqual('http:', url.protocol); testing.expectEqual('http:', url.protocol);
@@ -437,9 +537,9 @@
{ {
const url = new URL('https://example.com:8080/path'); const url = new URL('https://example.com:8080/path');
url.host = 'newhost.com'; url.host = 'newhost.com';
testing.expectEqual('https://newhost.com/path', url.href); testing.expectEqual('https://newhost.com:8080/path', url.href);
testing.expectEqual('newhost.com', url.hostname); testing.expectEqual('newhost.com', url.hostname);
testing.expectEqual('', url.port); testing.expectEqual('8080', url.port);
} }
{ {

View File

@@ -66,10 +66,20 @@ pub fn getUsername(self: *const URL) []const u8 {
return U.getUsername(self._raw); return U.getUsername(self._raw);
} }
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);
}
pub fn getPassword(self: *const URL) []const u8 { pub fn getPassword(self: *const URL) []const u8 {
return U.getPassword(self._raw); return U.getPassword(self._raw);
} }
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);
}
pub fn getPathname(self: *const URL) []const u8 { pub fn getPathname(self: *const URL) []const u8 {
return U.getPathname(self._raw); return U.getPathname(self._raw);
} }
@@ -272,8 +282,8 @@ pub const JsApi = struct {
pub const search = bridge.accessor(URL.getSearch, URL.setSearch, .{}); pub const search = bridge.accessor(URL.getSearch, URL.setSearch, .{});
pub const hash = bridge.accessor(URL.getHash, URL.setHash, .{}); pub const hash = bridge.accessor(URL.getHash, URL.setHash, .{});
pub const pathname = bridge.accessor(URL.getPathname, URL.setPathname, .{}); pub const pathname = bridge.accessor(URL.getPathname, URL.setPathname, .{});
pub const username = bridge.accessor(URL.getUsername, null, .{}); pub const username = bridge.accessor(URL.getUsername, URL.setUsername, .{});
pub const password = bridge.accessor(URL.getPassword, null, .{}); pub const password = bridge.accessor(URL.getPassword, URL.setPassword, .{});
pub const hostname = bridge.accessor(URL.getHostname, URL.setHostname, .{}); pub const hostname = bridge.accessor(URL.getHostname, URL.setHostname, .{});
pub const host = bridge.accessor(URL.getHost, URL.setHost, .{}); pub const host = bridge.accessor(URL.getHost, URL.setHost, .{});
pub const port = bridge.accessor(URL.getPort, URL.setPort, .{}); pub const port = bridge.accessor(URL.getPort, URL.setPort, .{});