mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Unify Data URLs
Previously, the logic for data/blob URLs were spread out at every http_client
caller. fetch/XHR/ScriptManager all had their own "if this is a data url, ..."
logic.
This causes two issues.
1 - Duplication, particularly as we try to cover more edge cases that need to
be handled in all places.
2 - Correctness because data/blob URLs are still URLs and still need to be
"fetched" (from memory). They should still fire with the same timing as any
other URL. That means that for fetch/XHR, they should fire asynchronously
(i.e. on the next tick). And for ScriptManager they should fire depending
on the type of script (normal/defer/async).
This PR relies on the infrastructure added to:
https://github.com/lightpanda-io/browser/pull/2506 in order to fulfilled a
synthetic response on the next tick.
Frame.navigate is excluded from this refactor. For one, about:blank must be
special-cased and run synchronously (one of the few places where this is
strictly required) and even blob URLs are a bit different: the blob URL list
is the parent frame, not self, and there's more we need to do (set origin).
Potentially there _is_ some improvement here, but it's both less significant
and less simple.
This commit is contained in:
@@ -294,6 +294,7 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, parent: ?*Frame) !void {
|
||||
._event_manager = EventManager.init(arena, self),
|
||||
};
|
||||
self._to_load = &self._to_load_1;
|
||||
self._http_owner.blob_urls = &self._blob_urls;
|
||||
|
||||
var screen: *Screen = undefined;
|
||||
var visual_viewport: *VisualViewport = undefined;
|
||||
@@ -507,11 +508,6 @@ pub fn isSameOrigin(self: *const Frame, url: [:0]const u8) bool {
|
||||
return std.mem.eql(u8, URL.getHost(url), URL.getHost(current_origin));
|
||||
}
|
||||
|
||||
/// Look up a blob URL in this frame's registry.
|
||||
pub fn lookupBlobUrl(self: *Frame, url: []const u8) ?*Blob {
|
||||
return self._blob_urls.get(url);
|
||||
}
|
||||
|
||||
pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !void {
|
||||
lp.assert(self._load_state == .waiting, "frame.renavigate", .{});
|
||||
const session = self._session;
|
||||
|
||||
@@ -567,6 +567,21 @@ fn requestT(self: *Client, req: Request, owner: ?*Owner) !*Transfer {
|
||||
// via transfer.abort which fires error_callback and deinits. `.created`
|
||||
// means no commit happened — anything else is held by an owner that
|
||||
// will clean up.
|
||||
|
||||
// Synthetic schemes never touch the network or the layer chain — they skip
|
||||
// robots/cache/interception and deliver on the next tick
|
||||
if (Synthetic.isSynthetic(req.url)) {
|
||||
// The 2nd transfer is the callback context. We don't actually use it,
|
||||
// we're just sticking transfer in there to have something.
|
||||
self.runNextTick(transfer, transfer, .{ .run = Synthetic.run }) catch |err| {
|
||||
if (transfer.state == .created) {
|
||||
transfer.abort(err);
|
||||
}
|
||||
return err;
|
||||
};
|
||||
return transfer;
|
||||
}
|
||||
|
||||
self.entry_layer.request(transfer) catch |err| {
|
||||
if (transfer.state == .created) {
|
||||
transfer.abort(err);
|
||||
@@ -577,6 +592,80 @@ fn requestT(self: *Client, req: Request, owner: ?*Owner) !*Transfer {
|
||||
return transfer;
|
||||
}
|
||||
|
||||
// Non-network URL schemes whose response is synthesized in-process rather than
|
||||
// fetched, think blob data URLs.
|
||||
const Synthetic = struct {
|
||||
const data_url = @import("data_url.zig");
|
||||
|
||||
fn isSynthetic(url: []const u8) bool {
|
||||
return std.mem.startsWith(u8, url, "data:") or std.mem.startsWith(u8, url, "blob:");
|
||||
}
|
||||
|
||||
fn run(transfer: *Transfer, _: *anyopaque) void {
|
||||
defer transfer.deinit();
|
||||
|
||||
const fulfilled = build(transfer) catch |err| {
|
||||
transfer.req.error_callback(transfer.req.ctx, err);
|
||||
return;
|
||||
};
|
||||
deliver(&transfer.req, &fulfilled) catch |err| {
|
||||
transfer.req.error_callback(transfer.req.ctx, err);
|
||||
};
|
||||
}
|
||||
|
||||
fn build(transfer: *Transfer) !FulfilledResponse {
|
||||
const arena = transfer.arena;
|
||||
const url = transfer.req.url;
|
||||
|
||||
var body: []const u8 = "";
|
||||
var content_type: []const u8 = "";
|
||||
|
||||
if (std.mem.startsWith(u8, url, "data:")) {
|
||||
const parsed = try data_url.parse(arena, url);
|
||||
content_type = parsed.content_type;
|
||||
body = parsed.body;
|
||||
} else {
|
||||
// blob: — resolved against the owning frame's registry.
|
||||
const owner = transfer.owner orelse return error.BlobNotFound;
|
||||
const blob_urls = owner.blob_urls orelse return error.BlobNotFound;
|
||||
const blob = blob_urls.get(url) orelse return error.BlobNotFound;
|
||||
content_type = blob._mime;
|
||||
body = blob._slice;
|
||||
}
|
||||
|
||||
// A blob with no type yields no Content-Type header.
|
||||
const headers = if (content_type.len > 0) blk: {
|
||||
const h = try arena.alloc(http.Header, 1);
|
||||
h[0] = .{ .name = "content-type", .value = content_type };
|
||||
break :blk h;
|
||||
} else &[_]http.Header{};
|
||||
|
||||
return .{
|
||||
.url = url,
|
||||
.body = body,
|
||||
.status = 200,
|
||||
.headers = headers,
|
||||
};
|
||||
}
|
||||
|
||||
fn deliver(req: *Request, fulfilled: *const FulfilledResponse) !void {
|
||||
const response = Response.fromFulfilled(req.ctx, fulfilled);
|
||||
if (req.start_callback) |cb| {
|
||||
try cb(response);
|
||||
}
|
||||
const proceed = try req.header_callback(response);
|
||||
if (!proceed) {
|
||||
return error.Abort;
|
||||
}
|
||||
if (fulfilled.body) |b| {
|
||||
if (b.len > 0) {
|
||||
try req.data_callback(response, b);
|
||||
}
|
||||
}
|
||||
try req.done_callback(req.ctx);
|
||||
}
|
||||
};
|
||||
|
||||
const SyncContext = struct {
|
||||
allocator: Allocator,
|
||||
completion: union(enum) {
|
||||
@@ -1909,7 +1998,11 @@ pub const Owner = struct {
|
||||
transfers: std.DoublyLinkedList = .{},
|
||||
websockets: std.DoublyLinkedList = .{},
|
||||
|
||||
// The owning Frame's / WorkerGlobalScope's blob: registry,
|
||||
blob_urls: ?*const std.StringHashMapUnmanaged(*Blob) = null,
|
||||
|
||||
const WebSocket = @import("webapi/net/WebSocket.zig");
|
||||
const Blob = @import("webapi/Blob.zig");
|
||||
|
||||
pub fn addTransfer(self: *Owner, t: *Transfer) void {
|
||||
self.transfers.append(&t.owner_node);
|
||||
|
||||
@@ -141,12 +141,11 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
var remote_url: ?[:0]const u8 = null;
|
||||
const base_url = frame.base();
|
||||
if (element.getAttributeSafe(comptime .wrap("src"))) |src| {
|
||||
if (try parseDataURI(arena, src)) |data_uri| {
|
||||
source = .{ .@"inline" = data_uri };
|
||||
} else {
|
||||
remote_url = try URL.resolve(arena, base_url, src, .{ .encoding = frame.charset });
|
||||
source = .{ .remote = .{} };
|
||||
}
|
||||
// data: and blob: srcs flow through the normal request path; HttpClient
|
||||
// synthesizes the response. Execution mode (blocking vs async/defer) is
|
||||
// attribute-driven, the same as any other src.
|
||||
remote_url = try URL.resolve(arena, base_url, src, .{ .encoding = frame.charset });
|
||||
source = .{ .remote = .{} };
|
||||
} else {
|
||||
var buf = std.Io.Writer.Allocating.init(arena);
|
||||
try element.asNode().getChildTextContent(&buf.writer);
|
||||
@@ -333,65 +332,3 @@ pub fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
|
||||
pub fn staticScriptsDone(self: *ScriptManager) void {
|
||||
self.base.staticScriptsDone();
|
||||
}
|
||||
|
||||
// Parses data:[<media-type>][;base64],<data>
|
||||
fn parseDataURI(allocator: Allocator, src: []const u8) !?[]const u8 {
|
||||
if (!std.mem.startsWith(u8, src, "data:")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uri = src[5..];
|
||||
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;
|
||||
const data = uri[data_starts + 1 ..];
|
||||
|
||||
const unescaped = try URL.unescape(allocator, data);
|
||||
|
||||
const metadata = uri[0..data_starts];
|
||||
if (std.mem.endsWith(u8, metadata, ";base64") == false) {
|
||||
return unescaped;
|
||||
}
|
||||
|
||||
// Forgiving base64 decode per WHATWG spec:
|
||||
// https://infra.spec.whatwg.org/#forgiving-base64-decode
|
||||
// Step 1: Remove all ASCII whitespace
|
||||
var stripped = try std.ArrayList(u8).initCapacity(allocator, unescaped.len);
|
||||
for (unescaped) |c| {
|
||||
if (!std.ascii.isWhitespace(c)) {
|
||||
stripped.appendAssumeCapacity(c);
|
||||
}
|
||||
}
|
||||
const trimmed = std.mem.trimRight(u8, stripped.items, "=");
|
||||
|
||||
// Length % 4 == 1 is invalid
|
||||
if (trimmed.len % 4 == 1) {
|
||||
return error.InvalidCharacterError;
|
||||
}
|
||||
|
||||
const decoded_size = std.base64.standard_no_pad.Decoder.calcSizeForSlice(trimmed) catch return error.InvalidCharacterError;
|
||||
const buffer = try allocator.alloc(u8, decoded_size);
|
||||
std.base64.standard_no_pad.Decoder.decode(buffer, trimmed) catch return error.InvalidCharacterError;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "DataURI: parse valid" {
|
||||
try assertValidDataURI("data:text/javascript; charset=utf-8;base64,Zm9v", "foo");
|
||||
try assertValidDataURI("data:text/javascript; charset=utf-8;,foo", "foo");
|
||||
try assertValidDataURI("data:,foo", "foo");
|
||||
}
|
||||
|
||||
test "DataURI: parse invalid" {
|
||||
try assertInvalidDataURI("atad:,foo");
|
||||
try assertInvalidDataURI("data:foo");
|
||||
try assertInvalidDataURI("data:");
|
||||
}
|
||||
|
||||
fn assertValidDataURI(uri: []const u8, expected: []const u8) !void {
|
||||
defer testing.reset();
|
||||
const data_uri = try parseDataURI(testing.arena_allocator, uri) orelse return error.TestFailed;
|
||||
try testing.expectEqual(expected, data_uri);
|
||||
}
|
||||
|
||||
fn assertInvalidDataURI(uri: []const u8) !void {
|
||||
try testing.expectEqual(null, parseDataURI(undefined, uri));
|
||||
}
|
||||
|
||||
202
src/browser/data_url.zig
Normal file
202
src/browser/data_url.zig
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// "data: URL processor" — https://fetch.spec.whatwg.org/#data-url-processor.
|
||||
// The single home for data: parsing: the HttpClient synthetic-scheme path and
|
||||
// ScriptManager (<script src="data:...">) both go through here.
|
||||
|
||||
const std = @import("std");
|
||||
const URL = @import("URL.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const Parsed = struct {
|
||||
body: []const u8,
|
||||
content_type: []const u8,
|
||||
};
|
||||
|
||||
pub fn parse(arena: Allocator, url: []const u8) !Parsed {
|
||||
if (std.mem.startsWith(u8, url, "data:") == false) {
|
||||
return error.InvalidDataUrl;
|
||||
}
|
||||
|
||||
const after = url["data:".len..];
|
||||
|
||||
const comma = std.mem.indexOfScalarPos(u8, after, 0, ',') orelse return error.InvalidDataUrl;
|
||||
var meta = std.mem.trim(u8, after[0..comma], &std.ascii.whitespace);
|
||||
const encoded_body = after[comma + 1 ..];
|
||||
|
||||
// A trailing ";" + optional spaces + "base64" selects base64 decoding.
|
||||
const is_base64 = blk: {
|
||||
if (meta.len < "base64".len) break :blk false;
|
||||
const tail = meta[meta.len - "base64".len ..];
|
||||
if (!std.ascii.eqlIgnoreCase(tail, "base64")) break :blk false;
|
||||
const head = std.mem.trimRight(u8, meta[0 .. meta.len - "base64".len], " ");
|
||||
if (head.len == 0 or head[head.len - 1] != ';') break :blk false;
|
||||
meta = head[0 .. head.len - 1];
|
||||
break :blk true;
|
||||
};
|
||||
|
||||
var content_type: []const u8 = meta;
|
||||
if (content_type.len == 0) {
|
||||
content_type = "text/plain;charset=US-ASCII";
|
||||
} else if (content_type[0] == ';') {
|
||||
// e.g. "data:;charset=utf-8,x" -> "text/plain;charset=utf-8"
|
||||
content_type = try std.fmt.allocPrint(arena, "text/plain{s}", .{content_type});
|
||||
}
|
||||
|
||||
const body_text = try URL.unescape(arena, encoded_body);
|
||||
const body = if (is_base64) try base64Decode(arena, body_text) else body_text;
|
||||
|
||||
return .{ .content_type = content_type, .body = body };
|
||||
}
|
||||
|
||||
fn base64Decode(arena: Allocator, input: []const u8) ![]u8 {
|
||||
// Forgiving-base64 decode — https://infra.spec.whatwg.org/#forgiving-base64-decode.
|
||||
// std's decoders reject non-canonical trailing bits (e.g. "ab"), which
|
||||
// forgiving-base64 tolerates, so decode by hand after validating padding.
|
||||
const buf = try arena.alloc(u8, input.len);
|
||||
var n: usize = 0;
|
||||
for (input) |c| switch (c) {
|
||||
' ', '\t', '\n', '\r', std.ascii.control_code.ff => {},
|
||||
else => {
|
||||
buf[n] = c;
|
||||
n += 1;
|
||||
},
|
||||
};
|
||||
var src = buf[0..n];
|
||||
|
||||
// Only a multiple-of-4 length may carry (and shed) up to two "=" of padding.
|
||||
if (src.len % 4 == 0) {
|
||||
if (std.mem.endsWith(u8, src, "==")) {
|
||||
src = src[0 .. src.len - 2];
|
||||
} else if (std.mem.endsWith(u8, src, "=")) {
|
||||
src = src[0 .. src.len - 1];
|
||||
}
|
||||
}
|
||||
if (src.len % 4 == 1) return error.InvalidBase64;
|
||||
// Any "=" still present is misplaced padding.
|
||||
if (std.mem.indexOfScalar(u8, src, '=') != null) return error.InvalidBase64;
|
||||
|
||||
const out_len = src.len / 4 * 3 + switch (src.len % 4) {
|
||||
0 => @as(usize, 0),
|
||||
2 => 1,
|
||||
3 => 2,
|
||||
else => unreachable,
|
||||
};
|
||||
const out = try arena.alloc(u8, out_len);
|
||||
|
||||
var oi: usize = 0;
|
||||
var i: usize = 0;
|
||||
while (i + 4 <= src.len) : (i += 4) {
|
||||
const a = try b64Val(src[i]);
|
||||
const b = try b64Val(src[i + 1]);
|
||||
const c = try b64Val(src[i + 2]);
|
||||
const d = try b64Val(src[i + 3]);
|
||||
out[oi] = (a << 2) | (b >> 4);
|
||||
out[oi + 1] = (b << 4) | (c >> 2);
|
||||
out[oi + 2] = (c << 6) | d;
|
||||
oi += 3;
|
||||
}
|
||||
switch (src.len - i) {
|
||||
0 => {},
|
||||
2 => {
|
||||
const a = try b64Val(src[i]);
|
||||
const b = try b64Val(src[i + 1]);
|
||||
out[oi] = (a << 2) | (b >> 4);
|
||||
},
|
||||
3 => {
|
||||
const a = try b64Val(src[i]);
|
||||
const b = try b64Val(src[i + 1]);
|
||||
const c = try b64Val(src[i + 2]);
|
||||
out[oi] = (a << 2) | (b >> 4);
|
||||
out[oi + 1] = (b << 4) | (c >> 2);
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
fn b64Val(c: u8) !u8 {
|
||||
return switch (c) {
|
||||
'A'...'Z' => c - 'A',
|
||||
'a'...'z' => c - 'a' + 26,
|
||||
'0'...'9' => c - '0' + 52,
|
||||
'+' => 62,
|
||||
'/' => 63,
|
||||
else => error.InvalidBase64,
|
||||
};
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
test "data_url: plain text, default content-type" {
|
||||
defer testing.reset();
|
||||
const r = try parse(testing.arena_allocator, "data:,Hello%2C%20World");
|
||||
try testing.expectString("text/plain;charset=US-ASCII", r.content_type);
|
||||
try testing.expectString("Hello, World", r.body);
|
||||
}
|
||||
|
||||
test "data_url: explicit mediatype" {
|
||||
defer testing.reset();
|
||||
const r = try parse(testing.arena_allocator, "data:text/html,<b>hi</b>");
|
||||
try testing.expectString("text/html", r.content_type);
|
||||
try testing.expectString("<b>hi</b>", r.body);
|
||||
}
|
||||
|
||||
test "data_url: base64" {
|
||||
defer testing.reset();
|
||||
const r = try parse(testing.arena_allocator, "data:text/plain;base64,SGVsbG8=");
|
||||
try testing.expectString("text/plain", r.content_type);
|
||||
try testing.expectString("Hello", r.body);
|
||||
}
|
||||
|
||||
test "data_url: base64 without padding decodes (forgiving)" {
|
||||
defer testing.reset();
|
||||
const r = try parse(testing.arena_allocator, "data:application/octet-stream;base64,SGVsbG8");
|
||||
try testing.expectString("Hello", r.body);
|
||||
|
||||
// 2- and 3-char unpadded tails decode (non-canonical trailing bits are ok).
|
||||
try testing.expectString("i", (try parse(testing.arena_allocator, "data:;base64,ab")).body);
|
||||
try testing.expectString("a", (try parse(testing.arena_allocator, "data:;base64,YR")).body);
|
||||
|
||||
// ASCII whitespace inside the payload is ignored.
|
||||
try testing.expectString("Hello", (try parse(testing.arena_allocator, "data:;base64,SGVs bG8=")).body);
|
||||
}
|
||||
|
||||
test "data_url: forgiving-base64 rejects misplaced/over-padding" {
|
||||
defer testing.reset();
|
||||
const arena = testing.arena_allocator;
|
||||
try std.testing.expectError(error.InvalidBase64, parse(arena, "data:;base64,abcd=")); // len % 4 == 1
|
||||
try std.testing.expectError(error.InvalidBase64, parse(arena, "data:;base64,="));
|
||||
try std.testing.expectError(error.InvalidBase64, parse(arena, "data:;base64,ab=c")); // interior "="
|
||||
try std.testing.expectError(error.InvalidBase64, parse(arena, "data:;base64,==")); // no data
|
||||
}
|
||||
|
||||
test "data_url: bare charset gets text/plain prefix" {
|
||||
defer testing.reset();
|
||||
const r = try parse(testing.arena_allocator, "data:;charset=utf-8,x");
|
||||
try testing.expectString("text/plain;charset=utf-8", r.content_type);
|
||||
}
|
||||
|
||||
test "data_url: empty body" {
|
||||
defer testing.reset();
|
||||
const r = try parse(testing.arena_allocator, "data:text/plain,");
|
||||
try testing.expectString("", r.body);
|
||||
}
|
||||
|
||||
test "data_url: missing comma is an error" {
|
||||
defer testing.reset();
|
||||
try std.testing.expectError(error.InvalidDataUrl, parse(testing.arena_allocator, "data:text/plain"));
|
||||
}
|
||||
@@ -34,7 +34,6 @@ const Factory = @import("../Factory.zig");
|
||||
const HttpClient = @import("../HttpClient.zig");
|
||||
const EventManagerBase = @import("../EventManagerBase.zig");
|
||||
|
||||
const Blob = @import("../webapi/Blob.zig");
|
||||
const Event = @import("../webapi/Event.zig");
|
||||
const EventTarget = @import("../webapi/EventTarget.zig");
|
||||
const Performance = @import("../webapi/Performance.zig");
|
||||
@@ -91,12 +90,6 @@ pub fn isSameOrigin(self: *const Execution, url: [:0]const u8) bool {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn lookupBlobUrl(self: *const Execution, url: []const u8) ?*Blob {
|
||||
return switch (self.context.global) {
|
||||
inline else => |g| g.lookupBlobUrl(url),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn makeRequest(self: *const Execution, req: HttpClient.Request) !void {
|
||||
return switch (self.context.global) {
|
||||
inline else => |g| g.makeRequest(req),
|
||||
|
||||
29
src/browser/tests/element/html/script/data_url.html
Normal file
29
src/browser/tests/element/html/script/data_url.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../../testing.js"></script>
|
||||
|
||||
<!--
|
||||
data: <script src> resolves through the HTTP client's synthetic-scheme path
|
||||
(no special-casing in ScriptManager). Execution mode is attribute-driven,
|
||||
exactly like any other src: bare = blocking, defer/async = deferred.
|
||||
-->
|
||||
|
||||
<!-- Blocking, parser-inserted: run in document order, before the check below. -->
|
||||
<script src="data:text/javascript,window.__plain = 3;"></script>
|
||||
<script src="data:text/javascript;base64,d2luZG93Ll9fYjY0ID0gNDI7"></script>
|
||||
<script src="data:text/javascript,window.__enc%20%3D%207%3B"></script>
|
||||
|
||||
<script id=blocking_data_urls>
|
||||
{
|
||||
testing.expectEqual(3, window.__plain);
|
||||
testing.expectEqual(42, window.__b64);
|
||||
testing.expectEqual(7, window.__enc);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- A deferred data: src must still run; proves mode follows the attribute. -->
|
||||
<script src="data:text/javascript,window.__deferred = 'ran';" defer></script>
|
||||
<script id=deferred_check>
|
||||
testing.onload(() => {
|
||||
testing.expectEqual('ran', window.__deferred);
|
||||
});
|
||||
</script>
|
||||
23
src/browser/tests/frames/data_url_iframe.html
Normal file
23
src/browser/tests/frames/data_url_iframe.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=data_url_navigation type=module>
|
||||
// data: navigation flows through the HTTP client's synthetic-scheme path
|
||||
// (resource_type=.document -> serveSynthetic), like any URL. The data:
|
||||
// document is cross-origin (opaque origin), so we can't read its
|
||||
// contentDocument — instead it reports its own parsed content back via
|
||||
// postMessage, which proves serveSynthetic delivered the bytes and they
|
||||
// parsed + ran.
|
||||
{
|
||||
const state = await testing.async();
|
||||
window.addEventListener('message', (e) => state.resolve(e.data));
|
||||
|
||||
const f = document.createElement('iframe');
|
||||
f.src = "data:text/html,<p>hello</p><script>parent.postMessage(document.querySelector('p').textContent, '*')<\/script>";
|
||||
document.documentElement.appendChild(f);
|
||||
|
||||
await state.done((data) => {
|
||||
testing.expectEqual('hello', data);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -360,3 +360,25 @@
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="worker_from_blob_url" type=module>
|
||||
// A blob: worker resolves its initial script through the HTTP client's
|
||||
// synthetic-scheme path against the frame's blob registry, like any URL.
|
||||
{
|
||||
const state = await testing.async();
|
||||
const blob = new Blob(["onmessage = (e) => postMessage({ echo: e.data, from: 'blob-worker' });"], { type: 'text/javascript' });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const worker = new Worker(blobUrl);
|
||||
|
||||
worker.onmessage = function(event) {
|
||||
state.resolve(event.data);
|
||||
};
|
||||
worker.postMessage({ greeting: 'from-blob' });
|
||||
|
||||
await state.done((response) => {
|
||||
testing.expectEqual('from-blob', response.echo.greeting);
|
||||
testing.expectEqual('blob-worker', response.from);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -25,7 +25,6 @@ const URL = @import("../URL.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const HttpClient = @import("../HttpClient.zig");
|
||||
|
||||
const Blob = @import("Blob.zig");
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
const MessageEvent = @import("event/MessageEvent.zig");
|
||||
const ErrorEvent = @import("event/ErrorEvent.zig");
|
||||
@@ -89,16 +88,6 @@ pub fn init(url: []const u8, frame: *Frame) !*Worker {
|
||||
return self;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, url, "blob:")) {
|
||||
errdefer frame.removeWorker(self);
|
||||
const blob: *Blob = frame.lookupBlobUrl(url) orelse {
|
||||
log.warn(.js, "invalid blob", .{ .target = "worker" });
|
||||
return error.BlobNotFound;
|
||||
};
|
||||
try self.loadInitialScript(blob._slice);
|
||||
return self;
|
||||
}
|
||||
|
||||
const headers = try session.browser.http_client.newHeaders();
|
||||
frame.makeRequest(.{
|
||||
.ctx = self,
|
||||
|
||||
@@ -144,6 +144,8 @@ pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope {
|
||||
});
|
||||
errdefer factory.destroy(self);
|
||||
|
||||
self._http_owner.blob_urls = &self._blob_urls;
|
||||
|
||||
self._script_manager = ScriptManagerBase.init(
|
||||
arena,
|
||||
&session.browser.http_client,
|
||||
@@ -231,10 +233,6 @@ pub fn isSameOrigin(self: *const WorkerGlobalScope, url: [:0]const u8) bool {
|
||||
return std.mem.eql(u8, URL.getHost(url), URL.getHost(current_origin));
|
||||
}
|
||||
|
||||
pub fn lookupBlobUrl(self: *WorkerGlobalScope, url: []const u8) ?*Blob {
|
||||
return self._blob_urls.get(url);
|
||||
}
|
||||
|
||||
pub fn makeRequest(self: *WorkerGlobalScope, req: HttpClient.Request) !void {
|
||||
return self._session.browser.http_client.request(req, &self._http_owner);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const URL = @import("../../URL.zig");
|
||||
|
||||
const Blob = @import("../Blob.zig");
|
||||
const Request = @import("Request.zig");
|
||||
const Response = @import("Response.zig");
|
||||
const AbortSignal = @import("../AbortSignal.zig");
|
||||
@@ -58,10 +57,6 @@ pub fn init(input: Input, options: ?InitOpts, exec: *const Execution) !js.Promis
|
||||
}
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, request._url, "blob:")) {
|
||||
return handleBlobUrl(request._url, resolver, exec);
|
||||
}
|
||||
|
||||
const response = try Response.init(null, .{ .status = 0 }, exec);
|
||||
errdefer response.deinit(exec.context.page);
|
||||
|
||||
@@ -121,26 +116,6 @@ pub fn init(input: Input, options: ?InitOpts, exec: *const Execution) !js.Promis
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
fn handleBlobUrl(url: []const u8, resolver: js.PromiseResolver, exec: *const Execution) !js.Promise {
|
||||
const blob: *Blob = exec.lookupBlobUrl(url) orelse {
|
||||
resolver.rejectError("fetch blob error", .{ .type_error = "BlobNotFound" });
|
||||
return resolver.promise();
|
||||
};
|
||||
|
||||
const response = try Response.init(null, .{ .status = 200 }, exec);
|
||||
response._body = .{ .bytes = try response._arena.dupe(u8, blob._slice) };
|
||||
response._url = try response._arena.dupeZ(u8, url);
|
||||
response._type = .basic;
|
||||
|
||||
if (blob._mime.len > 0) {
|
||||
try response._headers.append("Content-Type", blob._mime, exec);
|
||||
}
|
||||
|
||||
const js_val = try exec.context.local.?.zigValueToJs(response, .{});
|
||||
resolver.resolve("fetch blob done", js_val);
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
fn httpStartCallback(response: HttpClient.Response) !void {
|
||||
const self: *Fetch = @ptrCast(@alignCast(response.ctx));
|
||||
if (comptime IS_DEBUG) {
|
||||
|
||||
@@ -249,10 +249,6 @@ pub fn send(self: *XMLHttpRequest, body_: ?BodyInit, exec_: *const Execution) !v
|
||||
|
||||
const exec = self._exec;
|
||||
|
||||
if (std.mem.startsWith(u8, self._url, "blob:")) {
|
||||
return self.handleBlobUrl(exec);
|
||||
}
|
||||
|
||||
const session = exec.context.page.session;
|
||||
const http_client = &session.browser.http_client;
|
||||
var headers = try http_client.newHeaders();
|
||||
@@ -293,38 +289,6 @@ pub fn send(self: *XMLHttpRequest, body_: ?BodyInit, exec_: *const Execution) !v
|
||||
};
|
||||
}
|
||||
|
||||
fn handleBlobUrl(self: *XMLHttpRequest, exec: *const Execution) !void {
|
||||
const blob = exec.lookupBlobUrl(self._url) orelse {
|
||||
self.handleError(error.BlobNotFound);
|
||||
return;
|
||||
};
|
||||
|
||||
self._response_status = 200;
|
||||
self._response_url = self._url;
|
||||
|
||||
try self._response_data.appendSlice(self._arena, blob._slice);
|
||||
self._response_len = blob._slice.len;
|
||||
|
||||
try self.stateChanged(.headers_received, exec);
|
||||
try self._proto.dispatch(.load_start, .{ .loaded = 0, .total = self._response_len orelse 0 }, exec);
|
||||
try self.stateChanged(.loading, exec);
|
||||
try self._proto.dispatch(.progress, .{
|
||||
.total = self._response_len orelse 0,
|
||||
.loaded = self._response_data.items.len,
|
||||
}, exec);
|
||||
try self.stateChanged(.done, exec);
|
||||
|
||||
const loaded = self._response_data.items.len;
|
||||
try self._proto.dispatch(.load, .{
|
||||
.total = loaded,
|
||||
.loaded = loaded,
|
||||
}, exec);
|
||||
try self._proto.dispatch(.load_end, .{
|
||||
.total = loaded,
|
||||
.loaded = loaded,
|
||||
}, exec);
|
||||
}
|
||||
|
||||
pub fn getReadyState(self: *const XMLHttpRequest) u32 {
|
||||
return @intFromEnum(self._ready_state);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user