From 33d75354a2ae800bdfc459151164c5a4835ba276 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 10 Mar 2026 09:05:06 +0800 Subject: [PATCH] Add new Response and Request methods -Response.blob -Response.clone -Request.blob -Request.text -Request.json -Request.arrayBuffer -Request.bytes -Request.clone --- src/browser/tests/blob.html | 58 +++++++++++ src/browser/tests/net/request.html | 76 ++++++++++++++ src/browser/tests/net/response.html | 150 ++++++++++++++++++++-------- src/browser/webapi/Blob.zig | 36 ++++++- src/browser/webapi/net/Request.zig | 56 +++++++++++ src/browser/webapi/net/Response.zig | 45 +++++++++ 6 files changed, 374 insertions(+), 47 deletions(-) diff --git a/src/browser/tests/blob.html b/src/browser/tests/blob.html index 12cd13f5..0cbf8ea5 100644 --- a/src/browser/tests/blob.html +++ b/src/browser/tests/blob.html @@ -98,6 +98,64 @@ } + + + + + + diff --git a/src/browser/tests/net/response.html b/src/browser/tests/net/response.html index b7c149ba..3b74e72c 100644 --- a/src/browser/tests/net/response.html +++ b/src/browser/tests/net/response.html @@ -2,51 +2,113 @@ - - diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig index ac31560a..aa955ce5 100644 --- a/src/browser/webapi/Blob.zig +++ b/src/browser/webapi/Blob.zig @@ -21,6 +21,7 @@ const Writer = std.Io.Writer; const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const Mime = @import("../Mime.zig"); /// https://w3c.github.io/FileAPI/#blob-section /// https://developer.mozilla.org/en-US/docs/Web/API/Blob @@ -50,21 +51,50 @@ const InitOptions = struct { endings: []const u8 = "transparent", }; -/// Creates a new Blob. +/// Creates a new Blob (JS constructor). pub fn init( maybe_blob_parts: ?[]const []const u8, maybe_options: ?InitOptions, page: *Page, +) !*Blob { + return initWithMimeValidation(maybe_blob_parts, maybe_options, false, page); +} + +/// Creates a new Blob with optional MIME validation. +/// When validate_mime is true, uses full MIME parsing (for Response/Request). +/// When false, uses simple ASCII validation per FileAPI spec (for Blob constructor). +pub fn initWithMimeValidation( + maybe_blob_parts: ?[]const []const u8, + maybe_options: ?InitOptions, + validate_mime: bool, + page: *Page, ) !*Blob { const options: InitOptions = maybe_options orelse .{}; - // Setup MIME; This can be any string according to my observations. + const mime: []const u8 = blk: { const t = options.type; if (t.len == 0) { break :blk ""; } - break :blk try page.arena.dupe(u8, t); + const buf = try page.arena.dupe(u8, t); + + if (validate_mime) { + // Full MIME parsing per MIME sniff spec (for Content-Type headers) + _ = Mime.parse(buf) catch break :blk ""; + } else { + // Simple validation per FileAPI spec (for Blob constructor): + // - If any char is outside U+0020-U+007E, return empty string + // - Otherwise lowercase + for (t) |c| { + if (c < 0x20 or c > 0x7E) { + break :blk ""; + } + } + _ = std.ascii.lowerString(buf, buf); + } + + break :blk buf; }; const data = blk: { diff --git a/src/browser/webapi/net/Request.zig b/src/browser/webapi/net/Request.zig index 4316ddbb..aa7e0dd7 100644 --- a/src/browser/webapi/net/Request.zig +++ b/src/browser/webapi/net/Request.zig @@ -24,6 +24,7 @@ const Http = @import("../../../http/Http.zig"); const URL = @import("../URL.zig"); const Page = @import("../../Page.zig"); const Headers = @import("Headers.zig"); +const Blob = @import("../Blob.zig"); const Allocator = std.mem.Allocator; const Request = @This(); @@ -153,6 +154,55 @@ pub fn getHeaders(self: *Request, page: *Page) !*Headers { return headers; } +pub fn blob(self: *Request, page: *Page) !js.Promise { + const body = self._body orelse ""; + const headers = try self.getHeaders(page); + const content_type = try headers.get("content-type", page) orelse ""; + + const b = try Blob.initWithMimeValidation( + &.{body}, + .{ .type = content_type }, + true, + page, + ); + + return page.js.local.?.resolvePromise(b); +} + +pub fn text(self: *const Request, page: *Page) !js.Promise { + const body = self._body orelse ""; + return page.js.local.?.resolvePromise(body); +} + +pub fn json(self: *const Request, page: *Page) !js.Promise { + const body = self._body orelse ""; + const local = page.js.local.?; + const value = local.parseJSON(body) catch |err| { + return local.rejectPromise(.{@errorName(err)}); + }; + return local.resolvePromise(try value.persist()); +} + +pub fn arrayBuffer(self: *const Request, page: *Page) !js.Promise { + return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._body orelse "" }); +} + +pub fn bytes(self: *const Request, page: *Page) !js.Promise { + return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._body orelse "" }); +} + +pub fn clone(self: *const Request, page: *Page) !*Request { + return page._factory.create(Request{ + ._url = self._url, + ._arena = self._arena, + ._method = self._method, + ._headers = self._headers, + ._cache = self._cache, + ._credentials = self._credentials, + ._body = self._body, + }); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Request); @@ -168,6 +218,12 @@ pub const JsApi = struct { pub const headers = bridge.accessor(Request.getHeaders, null, .{}); pub const cache = bridge.accessor(Request.getCache, null, .{}); pub const credentials = bridge.accessor(Request.getCredentials, null, .{}); + pub const blob = bridge.function(Request.blob, .{}); + pub const text = bridge.function(Request.text, .{}); + pub const json = bridge.function(Request.json, .{}); + pub const arrayBuffer = bridge.function(Request.arrayBuffer, .{}); + pub const bytes = bridge.function(Request.bytes, .{}); + pub const clone = bridge.function(Request.clone, .{}); }; const testing = @import("../../../testing.zig"); diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index ba1a754d..d2c270ce 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -23,6 +23,7 @@ const Http = @import("../../../http/Http.zig"); const Page = @import("../../Page.zig"); const Headers = @import("Headers.zig"); const ReadableStream = @import("../streams/ReadableStream.zig"); +const Blob = @import("../Blob.zig"); const Allocator = std.mem.Allocator; @@ -147,6 +148,47 @@ pub fn arrayBuffer(self: *const Response, page: *Page) !js.Promise { return page.js.local.?.resolvePromise(js.ArrayBuffer{ .values = self._body orelse "" }); } +pub fn blob(self: *const Response, page: *Page) !js.Promise { + const body = self._body orelse ""; + const content_type = try self._headers.get("content-type", page) orelse ""; + + const b = try Blob.initWithMimeValidation( + &.{body}, + .{ .type = content_type }, + true, + page, + ); + + return page.js.local.?.resolvePromise(b); +} + +pub fn bytes(self: *const Response, page: *Page) !js.Promise { + return page.js.local.?.resolvePromise(js.TypedArray(u8){ .values = self._body orelse "" }); +} + +pub fn clone(self: *const Response, page: *Page) !*Response { + const arena = try page.getArena(.{ .debug = "Response.clone" }); + errdefer page.releaseArena(arena); + + const body = if (self._body) |b| try arena.dupe(u8, b) else null; + const status_text = try arena.dupe(u8, self._status_text); + const url = try arena.dupeZ(u8, self._url); + + const cloned = try arena.create(Response); + cloned.* = .{ + ._arena = arena, + ._status = self._status, + ._status_text = status_text, + ._url = url, + ._body = body, + ._type = self._type, + ._is_redirected = self._is_redirected, + ._headers = try Headers.init(.{ .obj = self._headers }, page), + ._transfer = null, + }; + return cloned; +} + pub const JsApi = struct { pub const bridge = js.Bridge(Response); @@ -170,6 +212,9 @@ pub const JsApi = struct { pub const url = bridge.accessor(Response.getURL, null, .{}); pub const redirected = bridge.accessor(Response.isRedirected, null, .{}); pub const arrayBuffer = bridge.function(Response.arrayBuffer, .{}); + pub const blob = bridge.function(Response.blob, .{}); + pub const bytes = bridge.function(Response.bytes, .{}); + pub const clone = bridge.function(Response.clone, .{}); }; const testing = @import("../../../testing.zig");