Merge pull request #1661 from lightpanda-io/escape_navigate_url
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled

Callers to page.navigate ensure URL is properly encoded.
This commit is contained in:
Karl Seguin
2026-02-26 15:19:11 +08:00
committed by GitHub
4 changed files with 147 additions and 16 deletions

View File

@@ -30,10 +30,10 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
if (base.len == 0 or isCompleteHTTPUrl(path)) { if (base.len == 0 or isCompleteHTTPUrl(path)) {
if (comptime opts.always_dupe or !isNullTerminated(PT)) { if (comptime opts.always_dupe or !isNullTerminated(PT)) {
const duped = try allocator.dupeZ(u8, path); const duped = try allocator.dupeZ(u8, path);
return encodeURL(allocator, duped, opts); return processResolved(allocator, duped, opts);
} }
if (comptime opts.encode) { if (comptime opts.encode) {
return encodeURL(allocator, path, opts); return processResolved(allocator, path, opts);
} }
return path; return path;
} }
@@ -41,10 +41,10 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
if (path.len == 0) { if (path.len == 0) {
if (comptime opts.always_dupe) { if (comptime opts.always_dupe) {
const duped = try allocator.dupeZ(u8, base); const duped = try allocator.dupeZ(u8, base);
return encodeURL(allocator, duped, opts); return processResolved(allocator, duped, opts);
} }
if (comptime opts.encode) { if (comptime opts.encode) {
return encodeURL(allocator, base, opts); return processResolved(allocator, base, opts);
} }
return base; return base;
} }
@@ -52,12 +52,12 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
if (path[0] == '?') { if (path[0] == '?') {
const base_path_end = std.mem.indexOfAny(u8, base, "?#") orelse base.len; const base_path_end = std.mem.indexOfAny(u8, base, "?#") orelse base.len;
const result = try std.mem.joinZ(allocator, "", &.{ base[0..base_path_end], path }); const result = try std.mem.joinZ(allocator, "", &.{ base[0..base_path_end], path });
return encodeURL(allocator, result, opts); return processResolved(allocator, result, opts);
} }
if (path[0] == '#') { if (path[0] == '#') {
const base_fragment_start = std.mem.indexOfScalar(u8, base, '#') orelse base.len; const base_fragment_start = std.mem.indexOfScalar(u8, base, '#') orelse base.len;
const result = try std.mem.joinZ(allocator, "", &.{ base[0..base_fragment_start], path }); const result = try std.mem.joinZ(allocator, "", &.{ base[0..base_fragment_start], path });
return encodeURL(allocator, result, opts); return processResolved(allocator, result, opts);
} }
if (std.mem.startsWith(u8, path, "//")) { if (std.mem.startsWith(u8, path, "//")) {
@@ -65,16 +65,16 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
const index = std.mem.indexOfScalar(u8, base, ':') orelse { const index = std.mem.indexOfScalar(u8, base, ':') orelse {
if (comptime isNullTerminated(PT)) { if (comptime isNullTerminated(PT)) {
if (comptime opts.encode) { if (comptime opts.encode) {
return encodeURL(allocator, path, opts); return processResolved(allocator, path, opts);
} }
return path; return path;
} }
const duped = try allocator.dupeZ(u8, path); const duped = try allocator.dupeZ(u8, path);
return encodeURL(allocator, duped, opts); return processResolved(allocator, duped, opts);
}; };
const protocol = base[0 .. index + 1]; const protocol = base[0 .. index + 1];
const result = try std.mem.joinZ(allocator, "", &.{ protocol, path }); const result = try std.mem.joinZ(allocator, "", &.{ protocol, path });
return encodeURL(allocator, result, opts); return processResolved(allocator, result, opts);
} }
const scheme_end = std.mem.indexOf(u8, base, "://"); const scheme_end = std.mem.indexOf(u8, base, "://");
@@ -83,7 +83,7 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
if (path[0] == '/') { if (path[0] == '/') {
const result = try std.mem.joinZ(allocator, "", &.{ base[0..path_start], path }); const result = try std.mem.joinZ(allocator, "", &.{ base[0..path_start], path });
return encodeURL(allocator, result, opts); return processResolved(allocator, result, opts);
} }
var normalized_base: []const u8 = base[0..path_start]; var normalized_base: []const u8 = base[0..path_start];
@@ -145,14 +145,17 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
// we always have an extra space // we always have an extra space
out[out_i] = 0; out[out_i] = 0;
return encodeURL(allocator, out[0..out_i :0], opts); return processResolved(allocator, out[0..out_i :0], opts);
} }
fn encodeURL(allocator: Allocator, url: [:0]const u8, comptime opts: ResolveOpts) ![:0]const u8 { fn processResolved(allocator: Allocator, url: [:0]const u8, comptime opts: ResolveOpts) ![:0]const u8 {
if (!comptime opts.encode) { if (!comptime opts.encode) {
return url; return url;
} }
return ensureEncoded(allocator, url);
}
pub fn ensureEncoded(allocator: Allocator, url: [:0]const u8) ![:0]const u8 {
const scheme_end = std.mem.indexOf(u8, url, "://"); const scheme_end = std.mem.indexOf(u8, url, "://");
const authority_start = if (scheme_end) |end| end + 3 else 0; const authority_start = if (scheme_end) |end| end + 3 else 0;
const path_start = std.mem.indexOfScalarPos(u8, url, authority_start, '/') orelse return url; const path_start = std.mem.indexOfScalarPos(u8, url, authority_start, '/') orelse return url;
@@ -180,7 +183,8 @@ fn encodeURL(allocator: Allocator, url: [:0]const u8, comptime opts: ResolveOpts
if (encoded_path.ptr == path_to_encode.ptr and if (encoded_path.ptr == path_to_encode.ptr and
(encoded_query == null or encoded_query.?.ptr == url[query_start.? + 1 .. query_end].ptr) and (encoded_query == null or encoded_query.?.ptr == url[query_start.? + 1 .. query_end].ptr) and
(encoded_fragment == null or encoded_fragment.?.ptr == url[fragment_start.? + 1 ..].ptr)) { (encoded_fragment == null or encoded_fragment.?.ptr == url[fragment_start.? + 1 ..].ptr))
{
// nothing has changed // nothing has changed
return url; return url;
} }
@@ -817,6 +821,127 @@ test "URL: resolve" {
} }
} }
test "URL: ensureEncoded" {
defer testing.reset();
const Case = struct {
url: [:0]const u8,
expected: [:0]const u8,
};
const cases = [_]Case{
.{
.url = "https://example.com/over 9000!",
.expected = "https://example.com/over%209000!",
},
.{
.url = "http://example.com/hello world.html",
.expected = "http://example.com/hello%20world.html",
},
.{
.url = "https://example.com/file[1].html",
.expected = "https://example.com/file%5B1%5D.html",
},
.{
.url = "https://example.com/file{name}.html",
.expected = "https://example.com/file%7Bname%7D.html",
},
.{
.url = "https://example.com/page?query=hello world",
.expected = "https://example.com/page?query=hello%20world",
},
.{
.url = "https://example.com/page?a=1&b=value with spaces",
.expected = "https://example.com/page?a=1&b=value%20with%20spaces",
},
.{
.url = "https://example.com/page#section one",
.expected = "https://example.com/page#section%20one",
},
.{
.url = "https://example.com/my path?query=my value#my anchor",
.expected = "https://example.com/my%20path?query=my%20value#my%20anchor",
},
.{
.url = "https://example.com/already%20encoded",
.expected = "https://example.com/already%20encoded",
},
.{
.url = "https://example.com/file%5B1%5D.html",
.expected = "https://example.com/file%5B1%5D.html",
},
.{
.url = "https://example.com/caf%C3%A9",
.expected = "https://example.com/caf%C3%A9",
},
.{
.url = "https://example.com/page?query=already%20encoded",
.expected = "https://example.com/page?query=already%20encoded",
},
.{
.url = "https://example.com/page?a=1&b=value%20here",
.expected = "https://example.com/page?a=1&b=value%20here",
},
.{
.url = "https://example.com/page#section%20one",
.expected = "https://example.com/page#section%20one",
},
.{
.url = "https://example.com/part%20encoded and not",
.expected = "https://example.com/part%20encoded%20and%20not",
},
.{
.url = "https://example.com/page?a=encoded%20value&b=not encoded",
.expected = "https://example.com/page?a=encoded%20value&b=not%20encoded",
},
.{
.url = "https://example.com/my%20path?query=not encoded#encoded%20anchor",
.expected = "https://example.com/my%20path?query=not%20encoded#encoded%20anchor",
},
.{
.url = "https://example.com/fully%20encoded?query=also%20encoded#and%20this",
.expected = "https://example.com/fully%20encoded?query=also%20encoded#and%20this",
},
.{
.url = "https://example.com/path-with_under~tilde",
.expected = "https://example.com/path-with_under~tilde",
},
.{
.url = "https://example.com/sub-delims!$&'()*+,;=",
.expected = "https://example.com/sub-delims!$&'()*+,;=",
},
.{
.url = "https://example.com",
.expected = "https://example.com",
},
.{
.url = "https://example.com?query=value",
.expected = "https://example.com?query=value",
},
.{
.url = "https://example.com/clean/path",
.expected = "https://example.com/clean/path",
},
.{
.url = "https://example.com/path?clean=query#clean-fragment",
.expected = "https://example.com/path?clean=query#clean-fragment",
},
.{
.url = "https://example.com/100% complete",
.expected = "https://example.com/100%25%20complete",
},
.{
.url = "https://example.com/path?value=100% done",
.expected = "https://example.com/path?value=100%25%20done",
},
};
for (cases) |case| {
const result = try ensureEncoded(testing.arena_allocator, case.url);
try testing.expectString(case.expected, result);
}
}
test "URL: resolve with encoding" { test "URL: resolve with encoding" {
defer testing.reset(); defer testing.reset();

View File

@@ -22,6 +22,7 @@ const lp = @import("lightpanda");
const id = @import("../id.zig"); const id = @import("../id.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("../../browser/js/js.zig"); const js = @import("../../browser/js/js.zig");
const URL = @import("../../browser/URL.zig");
const Page = @import("../../browser/Page.zig"); const Page = @import("../../browser/Page.zig");
const timestampF = @import("../../datetime.zig").timestamp; const timestampF = @import("../../datetime.zig").timestamp;
const Notification = @import("../../Notification.zig"); const Notification = @import("../../Notification.zig");
@@ -224,7 +225,8 @@ fn navigate(cmd: anytype) !void {
page = try session.replacePage(); page = try session.replacePage();
} }
try page.navigate(params.url, .{ const encoded_url = try URL.ensureEncoded(page.call_arena, params.url);
try page.navigate(encoded_url, .{
.reason = .address_bar, .reason = .address_bar,
.cdp_id = cmd.input.id, .cdp_id = cmd.input.id,
.kind = .{ .push = null }, .kind = .{ .push = null },

View File

@@ -21,6 +21,7 @@ const lp = @import("lightpanda");
const id = @import("../id.zig"); const id = @import("../id.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const URL = @import("../../browser/URL.zig");
const js = @import("../../browser/js/js.zig"); const js = @import("../../browser/js/js.zig");
// TODO: hard coded IDs // TODO: hard coded IDs
@@ -218,8 +219,9 @@ fn createTarget(cmd: anytype) !void {
} }
if (!std.mem.eql(u8, "about:blank", params.url)) { if (!std.mem.eql(u8, "about:blank", params.url)) {
const encoded_url = try URL.ensureEncoded(page.call_arena, params.url);
try page.navigate( try page.navigate(
params.url, encoded_url,
.{ .reason = .address_bar, .kind = .{ .push = null } }, .{ .reason = .address_bar, .kind = .{ .push = null } },
); );
} }

View File

@@ -20,6 +20,7 @@ const std = @import("std");
pub const App = @import("App.zig"); pub const App = @import("App.zig");
pub const Server = @import("Server.zig"); pub const Server = @import("Server.zig");
pub const Config = @import("Config.zig"); pub const Config = @import("Config.zig");
pub const URL = @import("browser/URL.zig");
pub const Page = @import("browser/Page.zig"); pub const Page = @import("browser/Page.zig");
pub const Browser = @import("browser/Browser.zig"); pub const Browser = @import("browser/Browser.zig");
pub const Session = @import("browser/Session.zig"); pub const Session = @import("browser/Session.zig");
@@ -92,7 +93,8 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
// } // }
// } // }
_ = try page.navigate(url, .{ const encoded_url = try URL.ensureEncoded(page.call_arena, url);
_ = try page.navigate(encoded_url, .{
.reason = .address_bar, .reason = .address_bar,
.kind = .{ .push = null }, .kind = .{ .push = null },
}); });