Merge pull request #1795 from lightpanda-io/navigate_blob_url

Allow navigation from a blob URL.
This commit is contained in:
Karl Seguin
2026-03-13 07:22:50 +08:00
committed by GitHub
4 changed files with 91 additions and 12 deletions

View File

@@ -407,6 +407,18 @@ pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
return std.mem.startsWith(u8, url, current_origin); 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 { pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {
lp.assert(self._load_state == .waiting, "page.renavigate", .{}); lp.assert(self._load_state == .waiting, "page.renavigate", .{});
const session = self._session; const session = self._session;
@@ -422,12 +434,17 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
.type = self._type, .type = self._type,
}); });
// if the url is about:blank, we load an empty HTML document in the // Handle synthetic navigations: about:blank and blob: URLs
// page and dispatch the events. const is_about_blank = std.mem.eql(u8, "about:blank", request_url);
if (std.mem.eql(u8, "about:blank", request_url)) { const is_blob = !is_about_blank and std.mem.startsWith(u8, request_url, "blob:");
self.url = "about:blank";
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; self.origin = parent.origin;
} else { } else {
self.origin = null; self.origin = null;
@@ -438,10 +455,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. // It's important to force a reset during the following navigation.
self._parse_state = .complete; self._parse_state = .complete;
self.document.injectBlank(self) catch |err| { // Content injection
log.err(.browser, "inject blank", .{ .err = err }); if (is_blob) {
return error.InjectBlankFailed; 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(); self.documentIsComplete();
session.notification.dispatch(.page_navigate, &.{ session.notification.dispatch(.page_navigate, &.{
@@ -455,7 +484,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
// Record telemetry for navigation // Record telemetry for navigation
session.browser.app.telemetry.record(.{ session.browser.app.telemetry.record(.{
.navigate = .{ .navigate = .{
.tls = false, // about:blank is not TLS .tls = false, // about:blank and blob: are not TLS
.proxy = session.browser.app.config.httpProxy() != null, .proxy = session.browser.app.config.httpProxy() != null,
}, },
}); });

View File

@@ -277,6 +277,11 @@ pub fn isCompleteHTTPUrl(url: []const u8) bool {
return false; 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 :// // Check if there's a scheme (protocol) ending with ://
const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false; const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false;

View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<body></body>
<script src="../testing.js"></script>
<script id="basic_blob_navigation">
{
const html = '<html><head></head><body><div id="test">Hello Blob</div></body></html>';
const blob = new Blob([html], { type: 'text/html' });
const blob_url = URL.createObjectURL(blob);
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
iframe.src = blob_url;
testing.eventually(() => {
testing.expectEqual('Hello Blob', iframe.contentDocument.getElementById('test').textContent);
});
}
</script>
<script id="multiple_blobs">
{
const blob1 = new Blob(['<html><body>First</body></html>'], { type: 'text/html' });
const blob2 = new Blob(['<html><body>Second</body></html>'], { type: 'text/html' });
const url1 = URL.createObjectURL(blob1);
const url2 = URL.createObjectURL(blob2);
const iframe1 = document.createElement('iframe');
document.body.appendChild(iframe1);
iframe1.src = url1;
const iframe2 = document.createElement('iframe');
document.body.appendChild(iframe2);
iframe2.src = url2;
testing.eventually(() => {
testing.expectEqual('First', iframe1.contentDocument.body.textContent);
testing.expectEqual('Second', iframe2.contentDocument.body.textContent);
});
}
</script>

View File

@@ -249,6 +249,8 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 {
.{ page.origin orelse "null", uuid_buf }, .{ page.origin orelse "null", uuid_buf },
); );
try page._blob_urls.put(page.arena, blob_url, blob); 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; return blob_url;
} }
@@ -258,8 +260,10 @@ pub fn revokeObjectURL(url: []const u8, page: *Page) void {
return; return;
} }
// Remove from registry (no-op if not found) // Remove from registry and release strong ref (no-op if not found)
_ = page._blob_urls.remove(url); if (page._blob_urls.fetchRemove(url)) |entry| {
page.js.weakRef(entry.value);
}
} }
pub const JsApi = struct { pub const JsApi = struct {