diff options
-rw-r--r-- | src/bun.js/bindings/URLSearchParams.cpp | 24 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.zig | 40 | ||||
-rw-r--r-- | src/bun.js/webcore/blob.zig | 28 | ||||
-rw-r--r-- | src/bun.js/webcore/body.zig | 6 | ||||
-rw-r--r-- | src/bun.js/webcore/request.zig | 15 | ||||
-rw-r--r-- | src/bun.js/webcore/response.zig | 18 | ||||
-rw-r--r-- | src/http/mime_type.zig | 2 | ||||
-rw-r--r-- | test/bun.js/FormData.test.ts | 16 |
8 files changed, 148 insertions, 1 deletions
diff --git a/src/bun.js/bindings/URLSearchParams.cpp b/src/bun.js/bindings/URLSearchParams.cpp index 9c168b4f6..2b0ffef67 100644 --- a/src/bun.js/bindings/URLSearchParams.cpp +++ b/src/bun.js/bindings/URLSearchParams.cpp @@ -26,9 +26,33 @@ #include "DOMURL.h" #include "wtf/URLParser.h" +#include "helpers.h" +#include "JSURLSearchParams.h" namespace WebCore { +extern "C" JSC::EncodedJSValue URLSearchParams__create(JSDOMGlobalObject* globalObject, const ZigString* input) +{ + String str = Zig::toString(*input); + auto result = URLSearchParams::create(str, nullptr); + return JSC::JSValue::encode(WebCore::toJSNewlyCreated(globalObject, globalObject, WTFMove(result))); +} + +extern "C" WebCore::URLSearchParams* URLSearchParams__fromJS(JSC::EncodedJSValue value) +{ + return WebCoreCast<WebCore::JSURLSearchParams, WebCore::URLSearchParams>(value); +} + +// callback accepting a void* and a const ZigString*, returning void +typedef void (*URLSearchParams__toStringCallback)(void* ctx, const ZigString* str); + +extern "C" void URLSearchParams__toString(WebCore::URLSearchParams* urlSearchParams, void* ctx, URLSearchParams__toStringCallback callback) +{ + String str = urlSearchParams->toString(); + auto zig = Zig::toZigString(str); + callback(ctx, &zig); +} + URLSearchParams::URLSearchParams(const String& init, DOMURL* associatedURL) : m_associatedURL(associatedURL) , m_pairs(init.startsWith('?') ? WTF::URLParser::parseURLEncodedForm(StringView(init).substring(1)) : WTF::URLParser::parseURLEncodedForm(init)) diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index d0ccacd06..4c1469564 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -4365,6 +4365,46 @@ pub fn untrackFunction( return private.Bun__untrackFFIFunction(globalObject, value); } +pub const URLSearchParams = opaque { + extern fn URLSearchParams__create(globalObject: *JSGlobalObject, *const ZigString) JSValue; + pub fn create(globalObject: *JSGlobalObject, init: ZigString) JSValue { + JSC.markBinding(@src()); + return URLSearchParams__create(globalObject, &init); + } + + extern fn URLSearchParams__fromJS(JSValue) ?*URLSearchParams; + pub fn fromJS(value: JSValue) ?*URLSearchParams { + JSC.markBinding(@src()); + return URLSearchParams__fromJS(value); + } + + extern fn URLSearchParams__toString( + self: *URLSearchParams, + ctx: *anyopaque, + callback: *const fn (ctx: *anyopaque, str: *const ZigString) void, + ) void; + + pub fn toString( + self: *URLSearchParams, + comptime Ctx: type, + ctx: *Ctx, + comptime callback: *const fn (ctx: *Ctx, str: ZigString) void, + ) void { + JSC.markBinding(@src()); + const Wrap = struct { + const cb_ = callback; + pub fn cb(c: *anyopaque, str: *const ZigString) void { + cb_( + bun.cast(*Ctx, c), + str.*, + ); + } + }; + + URLSearchParams__toString(self, ctx, Wrap.cb); + } +}; + pub const WTF = struct { extern fn WTF__copyLCharsFromUCharSource(dest: [*]u8, source: *const anyopaque, len: usize) void; extern fn WTF__toBase64URLStringValue(bytes: [*]const u8, length: usize, globalObject: *JSGlobalObject) JSValue; diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index a1b848397..de0ec1cb2 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -196,6 +196,34 @@ pub const Blob = struct { return null; } + const URLSearchParamsConverter = struct { + allocator: std.mem.Allocator, + buf: []u8 = "", + globalThis: *JSC.JSGlobalObject, + pub fn convert(this: *URLSearchParamsConverter, str: ZigString) void { + var out = str.toSlice(this.allocator).cloneIfNeeded(this.allocator) catch unreachable; + this.buf = bun.constStrToU8(out.slice()); + } + }; + + pub fn fromURLSearchParams( + globalThis: *JSC.JSGlobalObject, + allocator: std.mem.Allocator, + search_params: *JSC.URLSearchParams, + ) Blob { + var converter = URLSearchParamsConverter{ + .allocator = allocator, + .globalThis = globalThis, + }; + search_params.toString(URLSearchParamsConverter, &converter, URLSearchParamsConverter.convert); + var store = Blob.Store.init(converter.buf, allocator) catch unreachable; + store.mime_type = MimeType.all.@"application/x-www-form-urlencoded"; + + var blob = Blob.initWithStore(store, globalThis); + blob.content_type = store.mime_type.value; + return blob; + } + pub fn fromDOMFormData( globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator, diff --git a/src/bun.js/webcore/body.zig b/src/bun.js/webcore/body.zig index b96686265..23f00d16e 100644 --- a/src/bun.js/webcore/body.zig +++ b/src/bun.js/webcore/body.zig @@ -558,6 +558,12 @@ pub const Body = struct { }; } + if (value.as(JSC.URLSearchParams)) |search_params| { + return Body.Value{ + .Blob = Blob.fromURLSearchParams(globalThis, globalThis.allocator(), search_params), + }; + } + if (js_type == .DOMWrapper) { if (value.as(Blob)) |blob| { return Body.Value{ diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig index 1248767c3..fb0e40d2e 100644 --- a/src/bun.js/webcore/request.zig +++ b/src/bun.js/webcore/request.zig @@ -422,6 +422,14 @@ pub const Request = struct { }, } + if (request.body == .Blob and + request.headers != null and + request.body.Blob.content_type.len > 0 and + !request.headers.?.fastHas(.ContentType)) + { + request.headers.?.put("content-type", request.body.Blob.content_type); + } + return request; } @@ -462,6 +470,13 @@ pub const Request = struct { this.headers = FetchHeaders.createFromUWS(globalThis, req); } else { this.headers = FetchHeaders.createEmpty(); + + if (this.body == .Blob) { + const content_type = this.body.Blob.content_type; + if (content_type.len > 0) { + this.headers.?.put("content-type", content_type); + } + } } } diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 53e1f3fb6..8af1c3958 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -203,7 +203,15 @@ pub const Response = struct { fn getOrCreateHeaders(this: *Response) *FetchHeaders { if (this.body.init.headers == null) { this.body.init.headers = FetchHeaders.createEmpty(); + + if (this.body.value == .Blob) { + const content_type = this.body.value.Blob.content_type; + if (content_type.len > 0) { + this.body.init.headers.?.put("content-type", content_type); + } + } } + return this.body.init.headers.?; } @@ -501,11 +509,21 @@ pub const Response = struct { }) orelse return null; var response = getAllocator(globalThis).create(Response) catch unreachable; + response.* = Response{ .body = body, .allocator = getAllocator(globalThis), .url = "", }; + + if (response.body.value == .Blob and + response.body.init.headers != null and + response.body.value.Blob.content_type.len > 0 and + !response.body.init.headers.?.fastHas(.ContentType)) + { + response.body.init.headers.?.put("content-type", response.body.value.Blob.content_type); + } + return response; } }; diff --git a/src/http/mime_type.zig b/src/http/mime_type.zig index 418bcaf2d..3b3d8b961 100644 --- a/src/http/mime_type.zig +++ b/src/http/mime_type.zig @@ -1844,7 +1844,7 @@ pub const all = struct { pub const @"application/x-virtualbox-vmdk": MimeType = MimeType{ .category = .application, .value = "application/x-virtualbox-vmdk" }; pub const @"application/x-wais-source": MimeType = MimeType{ .category = .application, .value = "application/x-wais-source" }; pub const @"application/x-web-app-manifest+json": MimeType = MimeType{ .category = .application, .value = "application/x-web-app-manifest+json" }; - pub const @"application/x-www-form-urlencoded": MimeType = MimeType{ .category = .application, .value = "application/x-www-form-urlencoded" }; + pub const @"application/x-www-form-urlencoded": MimeType = MimeType{ .category = .application, .value = "application/x-www-form-urlencoded;charset=UTF-8" }; pub const @"application/x-x509-ca-cert": MimeType = MimeType{ .category = .application, .value = "application/x-x509-ca-cert" }; pub const @"application/x-x509-ca-ra-cert": MimeType = MimeType{ .category = .application, .value = "application/x-x509-ca-ra-cert" }; pub const @"application/x-x509-next-ca-cert": MimeType = MimeType{ .category = .application, .value = "application/x-x509-next-ca-cert" }; diff --git a/test/bun.js/FormData.test.ts b/test/bun.js/FormData.test.ts index b9d0f0856..dbe1a8ef0 100644 --- a/test/bun.js/FormData.test.ts +++ b/test/bun.js/FormData.test.ts @@ -307,6 +307,8 @@ describe("FormData", () => { const formData = new FormData(); formData.append("foo", Bun.file(path)); const response = C === Response ? new Response(formData) : new Request({ body: formData }); + expect(response.headers.get("content-type")?.startsWith("multipart/form-data;")).toBe(true); + const formData2 = await response.formData(); expect(formData2 instanceof FormData).toBe(true); expect(formData2.get("foo") instanceof Blob).toBe(true); @@ -335,6 +337,20 @@ describe("FormData", () => { expect(formData.get("baz")).toBe("qux"); }); + test("should parse URLSearchParams", async () => { + const searchParams = new URLSearchParams("foo=bar&baz=qux"); + const response = new Response(searchParams); + expect(response.headers.get("Content-Type")).toBe("application/x-www-form-urlencoded;charset=UTF-8"); + + expect(searchParams instanceof URLSearchParams).toBe(true); + expect(searchParams.get("foo")).toBe("bar"); + + const formData = await response.formData(); + expect(formData instanceof FormData).toBe(true); + expect(formData.get("foo")).toBe("bar"); + expect(formData.get("baz")).toBe("qux"); + }); + test("should parse URL encoded with charset", async () => { const response = new Response("foo=bar&baz=qux", { headers: { |