From e4e21f52b5d3b49e5c377a74f668a62a18aa10b9 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 12 Mar 2026 18:58:10 +0800 Subject: [PATCH] Allow navigation from a blob URL. These are used a lot in WPT test. --- src/browser/Page.zig | 49 +++++++++++++++++++++++++------- src/browser/URL.zig | 5 ++++ src/browser/tests/page/blob.html | 41 ++++++++++++++++++++++++++ src/browser/webapi/URL.zig | 8 ++++-- 4 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 src/browser/tests/page/blob.html diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 1e8fddeb..028d1fc1 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -404,6 +404,18 @@ pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool { return std.mem.startsWith(u8, url, current_origin); } +/// Look up a blob URL in this page's registry, walking up the parent chain. +pub fn lookupBlobUrl(self: *Page, url: []const u8) ?*Blob { + var current: ?*Page = self; + while (current) |page| { + if (page._blob_urls.get(url)) |blob| { + return blob; + } + current = page.parent; + } + return null; +} + pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void { lp.assert(self._load_state == .waiting, "page.renavigate", .{}); const session = self._session; @@ -419,12 +431,17 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi .type = self._type, }); - // if the url is about:blank, we load an empty HTML document in the - // page and dispatch the events. - if (std.mem.eql(u8, "about:blank", request_url)) { - self.url = "about:blank"; + // Handle synthetic navigations: about:blank and blob: URLs + const is_about_blank = std.mem.eql(u8, "about:blank", request_url); + const is_blob = !is_about_blank and std.mem.startsWith(u8, request_url, "blob:"); - if (self.parent) |parent| { + if (is_about_blank or is_blob) { + self.url = if (is_about_blank) "about:blank" else try self.arena.dupeZ(u8, request_url); + + if (is_blob) { + // strip out blob: + self.origin = try URL.getOrigin(self.arena, request_url[5.. :0]); + } else if (self.parent) |parent| { self.origin = parent.origin; } else { self.origin = null; @@ -435,10 +452,22 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi // It's important to force a reset during the following navigation. self._parse_state = .complete; - self.document.injectBlank(self) catch |err| { - log.err(.browser, "inject blank", .{ .err = err }); - return error.InjectBlankFailed; - }; + // Content injection + if (is_blob) { + const blob = self.lookupBlobUrl(request_url) orelse { + log.warn(.js, "invalid blob", .{ .url = request_url }); + return error.BlobNotFound; + }; + const parse_arena = try self.getArena(.{ .debug = "Page.parseBlob" }); + defer self.releaseArena(parse_arena); + var parser = Parser.init(parse_arena, self.document.asNode(), self); + parser.parse(blob._slice); + } else { + self.document.injectBlank(self) catch |err| { + log.err(.browser, "inject blank", .{ .err = err }); + return error.InjectBlankFailed; + }; + } self.documentIsComplete(); session.notification.dispatch(.page_navigate, &.{ @@ -452,7 +481,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi // Record telemetry for navigation session.browser.app.telemetry.record(.{ .navigate = .{ - .tls = false, // about:blank is not TLS + .tls = false, // about:blank and blob: are not TLS .proxy = session.browser.app.config.httpProxy() != null, }, }); diff --git a/src/browser/URL.zig b/src/browser/URL.zig index 19b87333..f8d7dd90 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -277,6 +277,11 @@ pub fn isCompleteHTTPUrl(url: []const u8) bool { return false; } + // blob: and data: URLs are complete but don't follow scheme:// pattern + if (std.mem.startsWith(u8, url, "blob:") or std.mem.startsWith(u8, url, "data:")) { + return true; + } + // Check if there's a scheme (protocol) ending with :// const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false; diff --git a/src/browser/tests/page/blob.html b/src/browser/tests/page/blob.html new file mode 100644 index 00000000..434c1ce6 --- /dev/null +++ b/src/browser/tests/page/blob.html @@ -0,0 +1,41 @@ + + + + + + + diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index fda7d2a5..e9263319 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -249,6 +249,8 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 { .{ page.origin orelse "null", uuid_buf }, ); try page._blob_urls.put(page.arena, blob_url, blob); + // prevent GC from cleaning up the blob while it's in the registry + page.js.strongRef(blob); return blob_url; } @@ -258,8 +260,10 @@ pub fn revokeObjectURL(url: []const u8, page: *Page) void { return; } - // Remove from registry (no-op if not found) - _ = page._blob_urls.remove(url); + // Remove from registry and release strong ref (no-op if not found) + if (page._blob_urls.fetchRemove(url)) |entry| { + page.js.weakRef(entry.value); + } } pub const JsApi = struct {