From 98885704566f0eae8db20b8fdfa0393f1abebb52 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 20 Mar 2025 21:29:00 -0700 Subject: [PATCH] Introduce Bun.Cookie & Bun.CookieMap & request.cookies (in BunRequest) (#18073) Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: pfg --- packages/bun-types/bun.d.ts | 83 ++ src/bun.js/bindings/BunCommonStrings.h | 5 +- src/bun.js/bindings/BunInjectedScriptHost.cpp | 18 + src/bun.js/bindings/BunObject.cpp | 19 + src/bun.js/bindings/Cookie.cpp | 310 ++++++ src/bun.js/bindings/Cookie.h | 90 ++ src/bun.js/bindings/CookieMap.cpp | 303 ++++++ src/bun.js/bindings/CookieMap.h | 89 ++ src/bun.js/bindings/JSBunRequest.cpp | 63 ++ src/bun.js/bindings/JSBunRequest.h | 4 + src/bun.js/bindings/bindings.cpp | 26 +- .../bindings/webcore/DOMClientIsoSubspaces.h | 5 +- src/bun.js/bindings/webcore/DOMConstructors.h | 4 +- src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 4 + src/bun.js/bindings/webcore/JSCookie.cpp | 893 ++++++++++++++++++ src/bun.js/bindings/webcore/JSCookie.h | 75 ++ src/bun.js/bindings/webcore/JSCookieMap.cpp | 893 ++++++++++++++++++ src/bun.js/bindings/webcore/JSCookieMap.h | 75 ++ .../webcore/JSPerformanceResourceTiming.cpp | 2 +- .../webcore/JSPerformanceServerTiming.cpp | 2 +- src/js/builtins/BunBuiltinNames.h | 7 + test/js/bun/cookie/cookie-map.test.ts | 248 +++++ test/js/bun/http/bun-serve-cookies.test.ts | 653 +++++++++++++ test/js/bun/util/cookie.test.js | 228 +++++ 24 files changed, 4071 insertions(+), 28 deletions(-) create mode 100644 src/bun.js/bindings/Cookie.cpp create mode 100644 src/bun.js/bindings/Cookie.h create mode 100644 src/bun.js/bindings/CookieMap.cpp create mode 100644 src/bun.js/bindings/CookieMap.h create mode 100644 src/bun.js/bindings/webcore/JSCookie.cpp create mode 100644 src/bun.js/bindings/webcore/JSCookie.h create mode 100644 src/bun.js/bindings/webcore/JSCookieMap.cpp create mode 100644 src/bun.js/bindings/webcore/JSCookieMap.h create mode 100644 test/js/bun/cookie/cookie-map.test.ts create mode 100644 test/js/bun/http/bun-serve-cookies.test.ts create mode 100644 test/js/bun/util/cookie.test.js diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index af184e9ae3..7bdc9dfe54 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3760,6 +3760,7 @@ declare module "bun" { interface BunRequest extends Request { params: RouterTypes.ExtractRouteParams; + readonly cookies: CookieMap; } interface GenericServeOptions { @@ -7528,4 +7529,86 @@ declare module "bun" { | [pkg: string, info: BunLockFilePackageInfo, bunTag: string] /** root */ | [pkg: string, info: Pick]; + + interface CookieInit { + name?: string; + value?: string; + domain?: string; + path?: string; + expires?: number | Date; + secure?: boolean; + sameSite?: CookieSameSite; + httpOnly?: boolean; + partitioned?: boolean; + maxAge?: number; + } + + interface CookieStoreDeleteOptions { + name: string; + domain?: string | null; + path?: string; + } + + interface CookieStoreGetOptions { + name?: string; + url?: string; + } + + type CookieSameSite = "strict" | "lax" | "none"; + + class Cookie { + constructor(name: string, value: string, options?: CookieInit); + constructor(cookieString: string); + constructor(cookieObject?: CookieInit); + + name: string; + value: string; + domain?: string; + path: string; + expires?: number; + secure: boolean; + sameSite: CookieSameSite; + partitioned: boolean; + maxAge?: number; + httpOnly: boolean; + + isExpired(): boolean; + + toString(): string; + toJSON(): CookieInit; + + static parse(cookieString: string): Cookie; + static from(name: string, value: string, options?: CookieInit): Cookie; + static serialize(...cookies: Cookie[]): string; + } + + class CookieMap implements Iterable<[string, Cookie]> { + constructor(init?: string[][] | Record | string); + + get(name: string): Cookie | null; + get(options?: CookieStoreGetOptions): Cookie | null; + + getAll(name: string): Cookie[]; + getAll(options?: CookieStoreGetOptions): Cookie[]; + + has(name: string, value?: string): boolean; + + set(name: string, value: string): void; + set(options: CookieInit): void; + + delete(name: string): void; + delete(options: CookieStoreDeleteOptions): void; + + toString(): string; + + toJSON(): Record>; + readonly size: number; + + entries(): IterableIterator<[string, Cookie]>; + keys(): IterableIterator; + values(): IterableIterator; + forEach(callback: (value: Cookie, key: string, map: CookieMap) => void, thisArg?: any): void; + + [Symbol.iterator](): IterableIterator<[string, Cookie]>; + } } diff --git a/src/bun.js/bindings/BunCommonStrings.h b/src/bun.js/bindings/BunCommonStrings.h index 734caa02d3..4d44948130 100644 --- a/src/bun.js/bindings/BunCommonStrings.h +++ b/src/bun.js/bindings/BunCommonStrings.h @@ -34,7 +34,10 @@ macro(OperationWasAborted, "The operation was aborted.") \ macro(OperationTimedOut, "The operation timed out.") \ macro(ConnectionWasClosed, "The connection was closed.") \ - macro(OperationFailed, "The operation failed.") + macro(OperationFailed, "The operation failed.") \ + macro(strict, "strict") \ + macro(lax, "lax") \ + macro(none, "none") // clang-format on diff --git a/src/bun.js/bindings/BunInjectedScriptHost.cpp b/src/bun.js/bindings/BunInjectedScriptHost.cpp index b044e9bce1..51e8ef1ef5 100644 --- a/src/bun.js/bindings/BunInjectedScriptHost.cpp +++ b/src/bun.js/bindings/BunInjectedScriptHost.cpp @@ -14,6 +14,9 @@ #include "JSURLSearchParams.h" #include "JSDOMFormData.h" #include +#include "JSCookie.h" +#include "JSCookieMap.h" + namespace Bun { using namespace JSC; @@ -150,6 +153,7 @@ JSValue BunInjectedScriptHost::getInternalProperties(VM& vm, JSGlobalObject* exe RETURN_IF_EXCEPTION(scope, {}); return array; } + } else if (type == JSAsJSONType) { if (auto* params = jsDynamicCast(value)) { auto* array = constructEmptyArray(exec, nullptr); @@ -157,6 +161,20 @@ JSValue BunInjectedScriptHost::getInternalProperties(VM& vm, JSGlobalObject* exe RETURN_IF_EXCEPTION(scope, {}); return array; } + + if (auto* cookie = jsDynamicCast(value)) { + auto* array = constructEmptyArray(exec, nullptr); + constructDataProperties(vm, exec, array, WebCore::getInternalProperties(vm, exec, cookie)); + RETURN_IF_EXCEPTION(scope, {}); + return array; + } + + if (auto* cookieMap = jsDynamicCast(value)) { + auto* array = constructEmptyArray(exec, nullptr); + constructDataProperties(vm, exec, array, WebCore::getInternalProperties(vm, exec, cookieMap)); + RETURN_IF_EXCEPTION(scope, {}); + return array; + } } } diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index e619e7b6da..964d9cad4b 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -39,6 +39,8 @@ #include "GeneratedBunObject.h" #include "JavaScriptCore/BunV8HeapSnapshotBuilder.h" #include "BunObjectModule.h" +#include "JSCookie.h" +#include "JSCookieMap.h" #ifdef WIN32 #include @@ -82,6 +84,9 @@ static JSValue BunObject_getter_wrap_ArrayBufferSink(VM& vm, JSObject* bunObject return jsCast(bunObject->globalObject())->ArrayBufferSink(); } +static JSValue constructCookieObject(VM& vm, JSObject* bunObject); +static JSValue constructCookieMapObject(VM& vm, JSObject* bunObject); + static JSValue constructEnvObject(VM& vm, JSObject* object) { return jsCast(object->globalObject())->processEnvObject(); @@ -694,6 +699,8 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj @begin bunObjectTable $ constructBunShell DontDelete|PropertyCallback ArrayBufferSink BunObject_getter_wrap_ArrayBufferSink DontDelete|PropertyCallback + Cookie constructCookieObject DontDelete|ReadOnly|PropertyCallback + CookieMap constructCookieMapObject DontDelete|ReadOnly|PropertyCallback CryptoHasher BunObject_getter_wrap_CryptoHasher DontDelete|PropertyCallback FFI BunObject_getter_wrap_FFI DontDelete|PropertyCallback FileSystemRouter BunObject_getter_wrap_FileSystemRouter DontDelete|PropertyCallback @@ -845,6 +852,18 @@ public: const JSC::ClassInfo JSBunObject::s_info = { "Bun"_s, &Base::s_info, &bunObjectTable, nullptr, CREATE_METHOD_TABLE(JSBunObject) }; +static JSValue constructCookieObject(VM& vm, JSObject* bunObject) +{ + auto* zigGlobalObject = jsCast(bunObject->globalObject()); + return WebCore::JSCookie::getConstructor(vm, zigGlobalObject); +} + +static JSValue constructCookieMapObject(VM& vm, JSObject* bunObject) +{ + auto* zigGlobalObject = jsCast(bunObject->globalObject()); + return WebCore::JSCookieMap::getConstructor(vm, zigGlobalObject); +} + JSC::JSObject* createBunObject(VM& vm, JSObject* globalObject) { return JSBunObject::create(vm, jsCast(globalObject)); diff --git a/src/bun.js/bindings/Cookie.cpp b/src/bun.js/bindings/Cookie.cpp new file mode 100644 index 0000000000..24d5a15bdc --- /dev/null +++ b/src/bun.js/bindings/Cookie.cpp @@ -0,0 +1,310 @@ +#include "Cookie.h" +#include "JSCookie.h" +#include "helpers.h" +#include +#include +#include +namespace WebCore { + +extern "C" JSC::EncodedJSValue Cookie__create(JSDOMGlobalObject* globalObject, const ZigString* name, const ZigString* value, const ZigString* domain, const ZigString* path, double expires, bool secure, int32_t sameSite, bool httpOnly, double maxAge, bool partitioned) +{ + String nameStr = Zig::toString(*name); + String valueStr = Zig::toString(*value); + String domainStr = Zig::toString(*domain); + String pathStr = Zig::toString(*path); + + CookieSameSite sameSiteEnum; + switch (sameSite) { + case 0: + sameSiteEnum = CookieSameSite::Strict; + break; + case 1: + sameSiteEnum = CookieSameSite::Lax; + break; + case 2: + sameSiteEnum = CookieSameSite::None; + break; + default: + sameSiteEnum = CookieSameSite::Strict; + } + + auto result = Cookie::create(nameStr, valueStr, domainStr, pathStr, expires, secure, sameSiteEnum, httpOnly, maxAge, partitioned); + return JSC::JSValue::encode(WebCore::toJSNewlyCreated(globalObject, globalObject, WTFMove(result))); +} + +extern "C" WebCore::Cookie* Cookie__fromJS(JSC::EncodedJSValue value) +{ + return WebCoreCast(value); +} + +Cookie::~Cookie() = default; + +Cookie::Cookie(const String& name, const String& value, + const String& domain, const String& path, + double expires, bool secure, CookieSameSite sameSite, + bool httpOnly, double maxAge, bool partitioned) + : m_name(name) + , m_value(value) + , m_domain(domain) + , m_path(path.isEmpty() ? "/"_s : path) + , m_expires(expires) + , m_secure(secure) + , m_sameSite(sameSite) + , m_httpOnly(httpOnly) + , m_maxAge(maxAge) + , m_partitioned(partitioned) +{ +} + +Ref Cookie::create(const String& name, const String& value, + const String& domain, const String& path, + double expires, bool secure, CookieSameSite sameSite, + bool httpOnly, double maxAge, bool partitioned) +{ + return adoptRef(*new Cookie(name, value, domain, path, expires, secure, sameSite, httpOnly, maxAge, partitioned)); +} + +Ref Cookie::from(const String& name, const String& value, + const String& domain, const String& path, + double expires, bool secure, CookieSameSite sameSite, + bool httpOnly, double maxAge, bool partitioned) +{ + return create(name, value, domain, path, expires, secure, sameSite, httpOnly, maxAge, partitioned); +} + +String Cookie::serialize(JSC::VM& vm, const Vector>& cookies) +{ + if (cookies.isEmpty()) + return emptyString(); + + StringBuilder builder; + bool first = true; + + for (const auto& cookie : cookies) { + if (!first) + builder.append("; "_s); + + cookie->appendTo(vm, builder); + first = false; + } + + return builder.toString(); +} + +ExceptionOr> Cookie::parse(const String& cookieString) +{ + // Split the cookieString by semicolons + Vector parts = cookieString.split(';'); + + if (parts.isEmpty()) + return Exception { TypeError, "Invalid cookie string: empty string"_s }; + + // First part is the name-value pair + String nameValueStr = parts[0].trim(isASCIIWhitespace); + size_t equalsPos = nameValueStr.find('='); + + if (equalsPos == notFound) + return Exception { TypeError, "Invalid cookie string: missing '=' in name-value pair"_s }; + + String name = nameValueStr.substring(0, equalsPos).trim(isASCIIWhitespace); + String value = nameValueStr.substring(equalsPos + 1).trim(isASCIIWhitespace); + + if (name.isEmpty()) + return Exception { TypeError, "Invalid cookie string: name cannot be empty"_s }; + + // Default values + String domain; + String path = "/"_s; + double expires = 0; + double maxAge = 0; + bool secure = false; + bool httpOnly = false; + bool partitioned = false; + CookieSameSite sameSite = CookieSameSite::Lax; + + // Parse attributes + for (size_t i = 1; i < parts.size(); i++) { + String part = parts[i].trim(isASCIIWhitespace); + size_t attrEqualsPos = part.find('='); + + String attrName; + String attrValue; + + if (attrEqualsPos == notFound) { + // Flag attribute like "Secure" + attrName = part.convertToASCIILowercase(); + attrValue = emptyString(); + } else { + attrName = part.substring(0, attrEqualsPos).trim(isASCIIWhitespace).convertToASCIILowercase(); + attrValue = part.substring(attrEqualsPos + 1).trim(isASCIIWhitespace); + } + + if (attrName == "domain"_s) + domain = attrValue; + else if (attrName == "path"_s) + path = attrValue; + else if (attrName == "expires"_s) { + if (!attrValue.containsOnlyLatin1()) + return Exception { TypeError, "Invalid cookie string: expires is not a valid date"_s }; + + if (UNLIKELY(!attrValue.is8Bit())) { + auto asLatin1 = attrValue.latin1(); + if (auto parsed = WTF::parseDate({ reinterpret_cast(asLatin1.data()), asLatin1.length() })) { + expires = parsed; + } else { + return Exception { TypeError, "Invalid cookie string: expires is not a valid date"_s }; + } + } else { + if (auto parsed = WTF::parseDate(attrValue.span())) { + expires = parsed; + } else { + return Exception { TypeError, "Invalid cookie string: expires is not a valid date"_s }; + } + } + } else if (attrName == "max-age"_s) { + if (auto parsed = WTF::parseIntegerAllowingTrailingJunk(attrValue); parsed.has_value()) { + maxAge = static_cast(parsed.value()); + } else { + return Exception { TypeError, "Invalid cookie string: max-age is not a number"_s }; + } + } else if (attrName == "secure"_s) + secure + = true; + else if (attrName == "httponly"_s) + httpOnly + = true; + else if (attrName == "partitioned"_s) + partitioned + = true; + else if (attrName == "samesite"_s) { + if (WTF::equalIgnoringASCIICase(attrValue, "strict"_s)) + sameSite = CookieSameSite::Strict; + else if (WTF::equalIgnoringASCIICase(attrValue, "lax"_s)) + sameSite = CookieSameSite::Lax; + else if (WTF::equalIgnoringASCIICase(attrValue, "none"_s)) + sameSite = CookieSameSite::None; + } + } + + return adoptRef(*new Cookie(name, value, domain, path, expires, secure, sameSite, httpOnly, maxAge, partitioned)); +} + +bool Cookie::isExpired() const +{ + if (m_expires == 0) + return false; // Session cookie + + auto currentTime = WTF::WallTime::now().secondsSinceEpoch().seconds() * 1000.0; + return currentTime > m_expires; +} + +String Cookie::toString(JSC::VM& vm) const +{ + StringBuilder builder; + appendTo(vm, builder); + return builder.toString(); +} + +void Cookie::appendTo(JSC::VM& vm, StringBuilder& builder) const +{ + // Name=Value is the basic format + builder.append(m_name); + builder.append('='); + builder.append(m_value); + + // Add domain if present + if (!m_domain.isEmpty()) { + builder.append("; Domain="_s); + builder.append(m_domain); + } + + if (!m_path.isEmpty() && m_path != "/"_s) { + builder.append("; Path="_s); + builder.append(m_path); + } + + // Add expires if present (not 0) + if (m_expires != 0) { + builder.append("; Expires="_s); + // In a real implementation, this would convert the timestamp to a proper date string + // For now, just use a numeric timestamp + WTF::GregorianDateTime dateTime; + vm.dateCache.msToGregorianDateTime(m_expires * 1000, WTF::TimeType::UTCTime, dateTime); + builder.append(WTF::makeRFC2822DateString(dateTime.weekDay(), dateTime.monthDay(), dateTime.month(), dateTime.year(), dateTime.hour(), dateTime.minute(), dateTime.second(), dateTime.utcOffsetInMinute())); + } + + // Add Max-Age if present + if (m_maxAge != 0) { + builder.append("; Max-Age="_s); + builder.append(String::number(m_maxAge)); + } + + // Add secure flag if true + if (m_secure) + builder.append("; Secure"_s); + + // Add HttpOnly flag if true + if (m_httpOnly) + builder.append("; HttpOnly"_s); + + // Add Partitioned flag if true + if (m_partitioned) + builder.append("; Partitioned"_s); + + // Add SameSite directive + + switch (m_sameSite) { + case CookieSameSite::Strict: + builder.append("; SameSite=strict"_s); + break; + case CookieSameSite::Lax: + // lax is the default. + break; + case CookieSameSite::None: + builder.append("; SameSite=none"_s); + break; + } +} + +JSC::JSValue Cookie::toJSON(JSC::VM& vm, JSC::JSGlobalObject* globalObject) const +{ + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* object = JSC::constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure()); + RETURN_IF_EXCEPTION(scope, JSC::jsNull()); + + auto& builtinNames = Bun::builtinNames(vm); + + object->putDirect(vm, vm.propertyNames->name, JSC::jsString(vm, m_name)); + object->putDirect(vm, vm.propertyNames->value, JSC::jsString(vm, m_value)); + + if (!m_domain.isEmpty()) + object->putDirect(vm, builtinNames.domainPublicName(), JSC::jsString(vm, m_domain)); + + object->putDirect(vm, builtinNames.pathPublicName(), JSC::jsString(vm, m_path)); + + if (m_expires != 0) + object->putDirect(vm, builtinNames.expiresPublicName(), JSC::jsNumber(m_expires)); + + if (m_maxAge != 0) + object->putDirect(vm, builtinNames.maxAgePublicName(), JSC::jsNumber(m_maxAge)); + + object->putDirect(vm, builtinNames.securePublicName(), JSC::jsBoolean(m_secure)); + object->putDirect(vm, builtinNames.sameSitePublicName(), toJS(globalObject, m_sameSite)); + object->putDirect(vm, builtinNames.httpOnlyPublicName(), JSC::jsBoolean(m_httpOnly)); + object->putDirect(vm, builtinNames.partitionedPublicName(), JSC::jsBoolean(m_partitioned)); + + return object; +} + +size_t Cookie::memoryCost() const +{ + size_t cost = sizeof(Cookie); + cost += m_name.sizeInBytes(); + cost += m_value.sizeInBytes(); + cost += m_domain.sizeInBytes(); + cost += m_path.sizeInBytes(); + return cost; +} + +} // namespace WebCore diff --git a/src/bun.js/bindings/Cookie.h b/src/bun.js/bindings/Cookie.h new file mode 100644 index 0000000000..cf9d1b946b --- /dev/null +++ b/src/bun.js/bindings/Cookie.h @@ -0,0 +1,90 @@ +#pragma once +#include "root.h" + +#include "ExceptionOr.h" +#include +#include + +namespace WebCore { + +enum class CookieSameSite : uint8_t { + Strict, + Lax, + None +}; + +JSC::JSValue toJS(JSC::JSGlobalObject*, CookieSameSite); + +class Cookie : public RefCounted { +public: + ~Cookie(); + + static Ref create(const String& name, const String& value, + const String& domain, const String& path, + double expires, bool secure, CookieSameSite sameSite, + bool httpOnly, double maxAge, bool partitioned); + + static ExceptionOr> parse(const String& cookieString); + static Ref from(const String& name, const String& value, + const String& domain, const String& path, + double expires, bool secure, CookieSameSite sameSite, + bool httpOnly, double maxAge, bool partitioned); + + static String serialize(JSC::VM& vm, const Vector>& cookies); + + const String& name() const { return m_name; } + void setName(const String& name) { m_name = name; } + + const String& value() const { return m_value; } + void setValue(const String& value) { m_value = value; } + + const String& domain() const { return m_domain; } + void setDomain(const String& domain) { m_domain = domain; } + + const String& path() const { return m_path; } + void setPath(const String& path) { m_path = path; } + + double expires() const { return m_expires; } + void setExpires(double expires) { m_expires = expires; } + + bool secure() const { return m_secure; } + void setSecure(bool secure) { m_secure = secure; } + + CookieSameSite sameSite() const { return m_sameSite; } + void setSameSite(CookieSameSite sameSite) { m_sameSite = sameSite; } + + bool httpOnly() const { return m_httpOnly; } + void setHttpOnly(bool httpOnly) { m_httpOnly = httpOnly; } + + double maxAge() const { return m_maxAge; } + void setMaxAge(double maxAge) { m_maxAge = maxAge; } + + bool partitioned() const { return m_partitioned; } + void setPartitioned(bool partitioned) { m_partitioned = partitioned; } + + bool isExpired() const; + + void appendTo(JSC::VM& vm, StringBuilder& builder) const; + String toString(JSC::VM& vm) const; + JSC::JSValue toJSON(JSC::VM& vm, JSC::JSGlobalObject*) const; + size_t memoryCost() const; + +private: + Cookie(const String& name, const String& value, + const String& domain, const String& path, + double expires, bool secure, CookieSameSite sameSite, + bool httpOnly, double maxAge, bool partitioned); + + String m_name; + String m_value; + String m_domain; + String m_path; + double m_expires; + bool m_secure; + CookieSameSite m_sameSite; + bool m_httpOnly; + double m_maxAge; + bool m_partitioned; +}; + +} // namespace WebCore diff --git a/src/bun.js/bindings/CookieMap.cpp b/src/bun.js/bindings/CookieMap.cpp new file mode 100644 index 0000000000..0168572045 --- /dev/null +++ b/src/bun.js/bindings/CookieMap.cpp @@ -0,0 +1,303 @@ +#include "CookieMap.h" +#include "JSCookieMap.h" +#include "helpers.h" +#include +#include + +namespace WebCore { + +extern "C" JSC::EncodedJSValue CookieMap__create(JSDOMGlobalObject* globalObject, const ZigString* initStr) +{ + String str = Zig::toString(*initStr); + auto result = CookieMap::create(std::variant>, HashMap, String>(str)); + return JSC::JSValue::encode(WebCore::toJSNewlyCreated(globalObject, globalObject, result.releaseReturnValue())); +} + +extern "C" WebCore::CookieMap* CookieMap__fromJS(JSC::EncodedJSValue value) +{ + return WebCoreCast(value); +} + +CookieMap::~CookieMap() = default; + +CookieMap::CookieMap() +{ +} + +CookieMap::CookieMap(const String& cookieString) +{ + if (cookieString.isEmpty()) + return; + + Vector pairs = cookieString.split(';'); + for (auto& pair : pairs) { + pair = pair.trim(isASCIIWhitespace); + if (pair.isEmpty()) + continue; + + size_t equalsPos = pair.find('='); + if (equalsPos == notFound) + continue; + + String name = pair.substring(0, equalsPos).trim(isASCIIWhitespace); + String value = pair.substring(equalsPos + 1).trim(isASCIIWhitespace); + + auto cookie = Cookie::create(name, value, String(), "/"_s, 0, false, CookieSameSite::Lax, false, 0, false); + m_cookies.append(WTFMove(cookie)); + } +} + +CookieMap::CookieMap(const HashMap& pairs) +{ + for (auto& entry : pairs) { + auto cookie = Cookie::create(entry.key, entry.value, String(), "/"_s, 0, false, CookieSameSite::Lax, + false, 0, false); + m_cookies.append(WTFMove(cookie)); + } +} + +CookieMap::CookieMap(const Vector>& pairs) +{ + for (const auto& pair : pairs) { + if (pair.size() == 2) { + auto cookie = Cookie::create(pair[0], pair[1], String(), "/"_s, 0, false, CookieSameSite::Lax, false, 0, false); + m_cookies.append(WTFMove(cookie)); + } + } +} + +ExceptionOr> CookieMap::create(std::variant>, HashMap, String>&& variant) +{ + auto visitor = WTF::makeVisitor( + [&](const Vector>& pairs) -> ExceptionOr> { + return adoptRef(*new CookieMap(pairs)); + }, + [&](const HashMap& pairs) -> ExceptionOr> { + return adoptRef(*new CookieMap(pairs)); + }, + [&](const String& cookieString) -> ExceptionOr> { + return adoptRef(*new CookieMap(cookieString)); + }); + + return std::visit(visitor, variant); +} + +RefPtr CookieMap::get(const String& name) const +{ + // Return the first cookie with the matching name + for (auto& cookie : m_cookies) { + if (cookie->name() == name) + return RefPtr(cookie.ptr()); + } + return nullptr; +} + +RefPtr CookieMap::get(const CookieStoreGetOptions& options) const +{ + // If name is provided, use that for lookup + if (!options.name.isEmpty()) + return get(options.name); + + // If url is provided, use that for lookup + if (!options.url.isEmpty()) { + // TODO: Implement URL-based cookie lookup + // This would involve parsing the URL, extracting the domain, and + // finding the first cookie that matches that domain + } + + return nullptr; +} + +Vector> CookieMap::getAll(const String& name) const +{ + // Return all cookies with the matching name + Vector> result; + for (auto& cookie : m_cookies) { + if (cookie->name() == name) + result.append(cookie); + } + return result; +} + +Vector> CookieMap::getAll(const CookieStoreGetOptions& options) const +{ + // If name is provided, use that for lookup + if (!options.name.isEmpty()) + return getAll(options.name); + + // If url is provided, use that for lookup + if (!options.url.isEmpty()) { + // TODO: Implement URL-based cookie lookup + // This would involve parsing the URL, extracting the domain, and + // finding all cookies that match that domain + } + + return Vector>(); +} + +bool CookieMap::has(const String& name, const String& value) const +{ + for (auto& cookie : m_cookies) { + if (cookie->name() == name && (value.isEmpty() || cookie->value() == value)) + return true; + } + return false; +} + +void CookieMap::set(const String& name, const String& value, bool httpOnly, bool partitioned, double maxAge) +{ + // Remove any existing cookies with the same name + remove(name); + + // Add the new cookie with proper settings + auto cookie = Cookie::create(name, value, String(), "/"_s, 0, false, CookieSameSite::Strict, + httpOnly, maxAge, partitioned); + m_cookies.append(WTFMove(cookie)); +} + +// Maintain backward compatibility with code that uses the old signature +void CookieMap::set(const String& name, const String& value) +{ + // Remove any existing cookies with the same name + remove(name); + + // Add the new cookie + auto cookie = Cookie::create(name, value, String(), "/"_s, 0, false, CookieSameSite::Strict, false, 0, false); + m_cookies.append(WTFMove(cookie)); +} + +void CookieMap::set(Ref cookie) +{ + // Remove any existing cookies with the same name + remove(cookie->name()); + + // Add the new cookie + m_cookies.append(WTFMove(cookie)); +} + +void CookieMap::remove(const String& name) +{ + m_cookies.removeAllMatching([&name](const auto& cookie) { + return cookie->name() == name; + }); +} + +void CookieMap::remove(const CookieStoreDeleteOptions& options) +{ + String name = options.name; + String domain = options.domain; + String path = options.path; + + m_cookies.removeAllMatching([&](const auto& cookie) { + if (cookie->name() != name) + return false; + + // If domain is specified, it must match + if (!domain.isNull() && cookie->domain() != domain) + return false; + + // If path is specified, it must match + if (!path.isNull() && cookie->path() != path) + return false; + + return true; + }); +} + +Vector> CookieMap::getCookiesMatchingDomain(const String& domain) const +{ + Vector> result; + for (auto& cookie : m_cookies) { + const auto& cookieDomain = cookie->domain(); + if (cookieDomain.isEmpty() || cookieDomain == domain) { + result.append(cookie); + } + } + return result; +} + +Vector> CookieMap::getCookiesMatchingPath(const String& path) const +{ + Vector> result; + for (auto& cookie : m_cookies) { + // Simple path matching logic - a cookie matches if its path is a prefix of the requested path + if (path.startsWith(cookie->path())) { + result.append(cookie); + } + } + return result; +} + +String CookieMap::toString(JSC::VM& vm) const +{ + if (m_cookies.isEmpty()) + return emptyString(); + + StringBuilder builder; + bool first = true; + + for (auto& cookie : m_cookies) { + if (!first) + builder.append("; "_s); + + cookie->appendTo(vm, builder); + + first = false; + } + + return builder.toString(); +} + +JSC::JSValue CookieMap::toJSON(JSC::JSGlobalObject* globalObject) const +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // Create an array of cookie entries + auto* array = JSC::constructEmptyArray(globalObject, nullptr, m_cookies.size()); + RETURN_IF_EXCEPTION(scope, JSC::jsNull()); + + unsigned index = 0; + for (const auto& cookie : m_cookies) { + // For each cookie, create a [name, cookie JSON] entry + auto* entryArray = JSC::constructEmptyArray(globalObject, nullptr, 2); + RETURN_IF_EXCEPTION(scope, JSC::jsNull()); + + entryArray->putDirectIndex(globalObject, 0, JSC::jsString(vm, cookie->name())); + RETURN_IF_EXCEPTION(scope, JSC::jsNull()); + + entryArray->putDirectIndex(globalObject, 1, cookie->toJSON(vm, globalObject)); + RETURN_IF_EXCEPTION(scope, JSC::jsNull()); + + array->putDirectIndex(globalObject, index++, entryArray); + RETURN_IF_EXCEPTION(scope, JSC::jsNull()); + } + + return array; +} + +size_t CookieMap::memoryCost() const +{ + size_t cost = sizeof(CookieMap); + for (auto& cookie : m_cookies) { + cost += cookie->memoryCost(); + } + return cost; +} + +std::optional CookieMap::Iterator::next() +{ + auto& cookies = m_target->m_cookies; + if (m_index >= cookies.size()) + return std::nullopt; + + auto& cookie = cookies[m_index++]; + return KeyValuePair(cookie->name(), cookie.ptr()); +} + +CookieMap::Iterator::Iterator(CookieMap& cookieMap) + : m_target(cookieMap) +{ +} + +} // namespace WebCore diff --git a/src/bun.js/bindings/CookieMap.h b/src/bun.js/bindings/CookieMap.h new file mode 100644 index 0000000000..c31cfe301c --- /dev/null +++ b/src/bun.js/bindings/CookieMap.h @@ -0,0 +1,89 @@ +#pragma once +#include "root.h" + +#include "Cookie.h" +#include "ExceptionOr.h" +#include +#include +#include +#include +#include + +namespace WebCore { + +struct CookieStoreGetOptions { + String name; + String url; +}; + +struct CookieStoreDeleteOptions { + String name; + String domain; + String path; +}; + +class CookieMap : public RefCounted { +public: + ~CookieMap(); + + static ExceptionOr> create(std::variant>, HashMap, String>&& init); + + RefPtr get(const String& name) const; + RefPtr get(const CookieStoreGetOptions& options) const; + + Vector> getAll(const String& name) const; + Vector> getAll(const CookieStoreGetOptions& options) const; + + bool has(const String& name, const String& value = String()) const; + + void set(const String& name, const String& value, bool httpOnly, bool partitioned, double maxAge); + void set(const String& name, const String& value); + void set(Ref); + + void remove(const String& name); + void remove(const CookieStoreDeleteOptions& options); + + String toString(JSC::VM& vm) const; + JSC::JSValue toJSON(JSC::JSGlobalObject*) const; + size_t size() const { return m_cookies.size(); } + size_t memoryCost() const; + + // Define a simple struct to hold the key-value pair + struct KeyValuePair { + KeyValuePair(const String& k, RefPtr c) + : key(k) + , value(c) + { + } + + String key; + RefPtr value; + }; + + class Iterator { + public: + explicit Iterator(CookieMap&); + + std::optional next(); + + private: + Ref m_target; + size_t m_index { 0 }; + }; + + Iterator createIterator() { return Iterator { *this }; } + Iterator createIterator(const void*) { return Iterator { *this }; } + +private: + CookieMap(); + CookieMap(const String& cookieString); + CookieMap(const HashMap& pairs); + CookieMap(const Vector>& pairs); + + Vector> getCookiesMatchingDomain(const String& domain) const; + Vector> getCookiesMatchingPath(const String& path) const; + + Vector> m_cookies; +}; + +} // namespace WebCore diff --git a/src/bun.js/bindings/JSBunRequest.cpp b/src/bun.js/bindings/JSBunRequest.cpp index 82f87ca1dc..8c9c0f5944 100644 --- a/src/bun.js/bindings/JSBunRequest.cpp +++ b/src/bun.js/bindings/JSBunRequest.cpp @@ -7,13 +7,20 @@ #include "ZigGlobalObject.h" #include "AsyncContextFrame.h" #include +#include "JSFetchHeaders.h" +#include "JSCookieMap.h" +#include "Cookie.h" +#include "CookieMap.h" +#include "JSDOMExceptionHandling.h" namespace Bun { static JSC_DECLARE_CUSTOM_GETTER(jsJSBunRequestGetParams); +static JSC_DECLARE_CUSTOM_GETTER(jsJSBunRequestGetCookies); static const HashTableValue JSBunRequestPrototypeValues[] = { { "params"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsJSBunRequestGetParams, nullptr } }, + { "cookies"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsJSBunRequestGetCookies, nullptr } }, }; JSBunRequest* JSBunRequest::create(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr, JSObject* params) @@ -51,6 +58,19 @@ void JSBunRequest::setParams(JSObject* params) m_params.set(Base::vm(), this, params); } +JSObject* JSBunRequest::cookies() const +{ + if (m_cookies) { + return m_cookies.get(); + } + return nullptr; +} + +void JSBunRequest::setCookies(JSObject* cookies) +{ + m_cookies.set(Base::vm(), this, cookies); +} + JSBunRequest::JSBunRequest(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr) : Base(vm, structure, sinkPtr) { @@ -61,6 +81,7 @@ void JSBunRequest::finishCreation(JSC::VM& vm, JSObject* params) { Base::finishCreation(vm); m_params.setMayBeNull(vm, this, params); + m_cookies.clear(); Bun__JSRequest__calculateEstimatedByteSize(this->wrapped()); auto size = Request__estimatedSize(this->wrapped()); @@ -73,6 +94,7 @@ void JSBunRequest::visitChildrenImpl(JSCell* cell, Visitor& visitor) JSBunRequest* thisCallSite = jsCast(cell); Base::visitChildren(thisCallSite, visitor); visitor.append(thisCallSite->m_params); + visitor.append(thisCallSite->m_cookies); } DEFINE_VISIT_CHILDREN(JSBunRequest); @@ -137,6 +159,47 @@ JSC_DEFINE_CUSTOM_GETTER(jsJSBunRequestGetParams, (JSC::JSGlobalObject * globalO return JSValue::encode(params); } +JSC_DEFINE_CUSTOM_GETTER(jsJSBunRequestGetCookies, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + JSBunRequest* request = jsDynamicCast(JSValue::decode(thisValue)); + if (!request) + return JSValue::encode(jsUndefined()); + + auto* cookies = request->cookies(); + if (!cookies) { + auto& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto& names = builtinNames(vm); + JSC::JSValue headersValue = request->get(globalObject, names.headersPublicName()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + auto* headers = jsDynamicCast(headersValue); + if (!headers) + return JSValue::encode(jsUndefined()); + + auto& fetchHeaders = headers->wrapped(); + + auto cookieHeader = fetchHeaders.internalHeaders().get(HTTPHeaderName::Cookie); + + // Create a CookieMap from the cookie header + auto cookieMapResult = WebCore::CookieMap::create(cookieHeader); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + if (cookieMapResult.hasException()) { + WebCore::propagateException(*globalObject, throwScope, cookieMapResult.releaseException()); + return JSValue::encode(jsUndefined()); + } + + auto cookieMap = cookieMapResult.releaseReturnValue(); + + // Convert to JS + auto cookies = WebCore::toJSNewlyCreated(globalObject, jsCast(globalObject), WTFMove(cookieMap)); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + request->setCookies(cookies.getObject()); + return JSValue::encode(cookies); + } + + return JSValue::encode(cookies); +} + Structure* createJSBunRequestStructure(JSC::VM& vm, Zig::GlobalObject* globalObject) { auto prototypeStructure = JSBunRequestPrototype::createStructure(vm, globalObject, globalObject->JSRequestPrototype()); diff --git a/src/bun.js/bindings/JSBunRequest.h b/src/bun.js/bindings/JSBunRequest.h index 92a5d936fa..ebe3b6428d 100644 --- a/src/bun.js/bindings/JSBunRequest.h +++ b/src/bun.js/bindings/JSBunRequest.h @@ -32,11 +32,15 @@ public: JSObject* params() const; void setParams(JSObject* params); + JSObject* cookies() const; + void setCookies(JSObject* cookies); + private: JSBunRequest(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr); void finishCreation(JSC::VM& vm, JSObject* params); mutable JSC::WriteBarrier m_params; + mutable JSC::WriteBarrier m_cookies; }; JSC::Structure* createJSBunRequestStructure(JSC::VM&, Zig::GlobalObject*); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 77b30297b9..ef0de56fa1 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -130,6 +130,7 @@ #include "ObjectBindings.h" #include +#include "wtf-bindings.h" #if OS(DARWIN) #if BUN_DEBUG @@ -6063,7 +6064,7 @@ extern "C" EncodedJSValue JSC__JSValue__dateInstanceFromNullTerminatedString(JSC // this is largely copied from dateProtoFuncToISOString extern "C" int JSC__JSValue__toISOString(JSC::JSGlobalObject* globalObject, EncodedJSValue dateValue, char* buf) { - char buffer[29]; + char buffer[64]; JSC::DateInstance* thisDateObj = JSC::jsDynamicCast(JSC::JSValue::decode(dateValue)); if (!thisDateObj) return -1; @@ -6073,28 +6074,7 @@ extern "C" int JSC__JSValue__toISOString(JSC::JSGlobalObject* globalObject, Enco auto& vm = JSC::getVM(globalObject); - const GregorianDateTime* gregorianDateTime = thisDateObj->gregorianDateTimeUTC(vm.dateCache); - if (!gregorianDateTime) - return -1; - - // If the year is outside the bounds of 0 and 9999 inclusive we want to use the extended year format (ES 15.9.1.15.1). - int ms = static_cast(fmod(thisDateObj->internalNumber(), msPerSecond)); - if (ms < 0) - ms += msPerSecond; - - int charactersWritten; - if (gregorianDateTime->year() > 9999 || gregorianDateTime->year() < 0) - charactersWritten = snprintf(buffer, sizeof(buffer), "%+07d-%02d-%02dT%02d:%02d:%02d.%03dZ", gregorianDateTime->year(), gregorianDateTime->month() + 1, gregorianDateTime->monthDay(), gregorianDateTime->hour(), gregorianDateTime->minute(), gregorianDateTime->second(), ms); - else - charactersWritten = snprintf(buffer, sizeof(buffer), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", gregorianDateTime->year(), gregorianDateTime->month() + 1, gregorianDateTime->monthDay(), gregorianDateTime->hour(), gregorianDateTime->minute(), gregorianDateTime->second(), ms); - - memcpy(buf, buffer, charactersWritten); - - ASSERT(charactersWritten > 0 && static_cast(charactersWritten) < sizeof(buffer)); - if (static_cast(charactersWritten) >= sizeof(buffer)) - return -1; - - return charactersWritten; + return static_cast(Bun::toISOString(vm, thisDateObj->internalNumber(), buffer)); } extern "C" int JSC__JSValue__DateNowISOString(JSC::JSGlobalObject* globalObject, char* buf) diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 334bbfe422..3ba26026fd 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -73,8 +73,11 @@ public: std::unique_ptr m_clientSubspaceForDOMFormDataIterator; std::unique_ptr m_clientSubspaceForDOMURL; std::unique_ptr m_clientSubspaceForURLSearchParams; - std::unique_ptr m_clientSubspaceForURLSearchParamsIterator; + + std::unique_ptr m_clientSubspaceForCookie; + std::unique_ptr m_clientSubspaceForCookieMap; + std::unique_ptr m_clientSubspaceForCookieMapIterator; std::unique_ptr m_clientSubspaceForExposedToWorkerAndWindow; diff --git a/src/bun.js/bindings/webcore/DOMConstructors.h b/src/bun.js/bindings/webcore/DOMConstructors.h index 89fe962110..2b02150ed8 100644 --- a/src/bun.js/bindings/webcore/DOMConstructors.h +++ b/src/bun.js/bindings/webcore/DOMConstructors.h @@ -541,6 +541,8 @@ enum class DOMConstructorID : uint16_t { TextMetrics, TimeRanges, URLSearchParams, + Cookie, + CookieMap, ValidityState, WebKitMediaKeyError, ANGLEInstancedArrays, @@ -858,7 +860,7 @@ enum class DOMConstructorID : uint16_t { EventEmitter, }; -static constexpr unsigned numberOfDOMConstructorsBase = 846; +static constexpr unsigned numberOfDOMConstructorsBase = 848; static constexpr unsigned bunExtraConstructors = 1; diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index 4ffbadd00a..397854144d 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -919,6 +919,10 @@ public: std::unique_ptr m_subspaceForExposedToWorkerAndWindow; std::unique_ptr m_subspaceForURLSearchParams; std::unique_ptr m_subspaceForURLSearchParamsIterator; + + std::unique_ptr m_subspaceForCookie; + std::unique_ptr m_subspaceForCookieMap; + std::unique_ptr m_subspaceForCookieMapIterator; std::unique_ptr m_subspaceForDOMException; // std::unique_ptr m_subspaceForDOMFormData; diff --git a/src/bun.js/bindings/webcore/JSCookie.cpp b/src/bun.js/bindings/webcore/JSCookie.cpp new file mode 100644 index 0000000000..c80069ee1a --- /dev/null +++ b/src/bun.js/bindings/webcore/JSCookie.cpp @@ -0,0 +1,893 @@ +#include "config.h" +#include "JSCookie.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "IDLTypes.h" +#include "JSDOMBinding.h" +#include "JSDOMConstructor.h" +#include "JSDOMConvertBase.h" +#include "JSDOMConvertBoolean.h" +#include "JSDOMConvertInterface.h" +#include "JSDOMConvertNullable.h" +#include "JSDOMConvertNumbers.h" +#include "JSDOMConvertStrings.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMOperation.h" +#include "JSDOMWrapperCache.h" +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; + +// Helper for getting wrapped Cookie from JS value +static Cookie* toCookieWrapped(JSGlobalObject* lexicalGlobalObject, JSC::ThrowScope& throwScope, JSValue value) +{ + auto& vm = getVM(lexicalGlobalObject); + auto* impl = JSCookie::toWrapped(vm, value); + if (UNLIKELY(!impl)) + throwVMTypeError(lexicalGlobalObject, throwScope); + return impl; +} + +static JSC_DECLARE_HOST_FUNCTION(jsCookiePrototypeFunction_toString); +static JSC_DECLARE_HOST_FUNCTION(jsCookiePrototypeFunction_toJSON); +static JSC_DECLARE_HOST_FUNCTION(jsCookieStaticFunctionParse); +static JSC_DECLARE_HOST_FUNCTION(jsCookieStaticFunctionFrom); +static JSC_DECLARE_HOST_FUNCTION(jsCookieStaticFunctionSerialize); +static JSC_DECLARE_CUSTOM_GETTER(jsCookiePrototypeGetter_name); +static JSC_DECLARE_CUSTOM_SETTER(jsCookiePrototypeSetter_name); +static JSC_DECLARE_CUSTOM_GETTER(jsCookiePrototypeGetter_value); +static JSC_DECLARE_CUSTOM_SETTER(jsCookiePrototypeSetter_value); +static JSC_DECLARE_CUSTOM_GETTER(jsCookiePrototypeGetter_domain); +static JSC_DECLARE_CUSTOM_SETTER(jsCookiePrototypeSetter_domain); +static JSC_DECLARE_CUSTOM_GETTER(jsCookiePrototypeGetter_path); +static JSC_DECLARE_CUSTOM_SETTER(jsCookiePrototypeSetter_path); +static JSC_DECLARE_CUSTOM_GETTER(jsCookiePrototypeGetter_expires); +static JSC_DECLARE_CUSTOM_SETTER(jsCookiePrototypeSetter_expires); +static JSC_DECLARE_CUSTOM_GETTER(jsCookiePrototypeGetter_secure); +static JSC_DECLARE_CUSTOM_SETTER(jsCookiePrototypeSetter_secure); +static JSC_DECLARE_CUSTOM_GETTER(jsCookiePrototypeGetter_sameSite); +static JSC_DECLARE_CUSTOM_SETTER(jsCookiePrototypeSetter_sameSite); +static JSC_DECLARE_CUSTOM_GETTER(jsCookiePrototypeGetter_httpOnly); +static JSC_DECLARE_CUSTOM_SETTER(jsCookiePrototypeSetter_httpOnly); +static JSC_DECLARE_CUSTOM_GETTER(jsCookiePrototypeGetter_maxAge); +static JSC_DECLARE_CUSTOM_SETTER(jsCookiePrototypeSetter_maxAge); +static JSC_DECLARE_CUSTOM_GETTER(jsCookiePrototypeGetter_partitioned); +static JSC_DECLARE_CUSTOM_SETTER(jsCookiePrototypeSetter_partitioned); +static JSC_DECLARE_HOST_FUNCTION(jsCookiePrototypeFunction_isExpired); +static JSC_DECLARE_CUSTOM_GETTER(jsCookieConstructor); + +class JSCookiePrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSCookiePrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSCookiePrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSCookiePrototype(vm, globalObject, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSCookiePrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSCookiePrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; + +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSCookiePrototype, JSCookiePrototype::Base); + +JSValue getInternalProperties(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSCookie* castedThis) +{ + return castedThis->wrapped().toJSON(vm, lexicalGlobalObject); +} + +using JSCookieDOMConstructor = JSDOMConstructor; + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSCookieDOMConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* castedThis = jsCast(callFrame->jsCallee()); + + // Check if this was called with 'new' + if (UNLIKELY(!callFrame->thisValue().isObject())) + return throwVMError(lexicalGlobalObject, throwScope, createNotAConstructorError(lexicalGlobalObject, callFrame->jsCallee())); + + // Static method: parse(cookieString) + if (callFrame->argumentCount() == 1) { + auto cookieString = convert(*lexicalGlobalObject, callFrame->argument(0)); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto result = Cookie::parse(cookieString); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto* globalObject = castedThis->globalObject(); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS(lexicalGlobalObject, globalObject, result.releaseReturnValue()))); + } + + // Constructor: Cookie.from(name, value, options) + if (callFrame->argumentCount() >= 2) { + auto name = convert(*lexicalGlobalObject, callFrame->argument(0)); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto value = convert(*lexicalGlobalObject, callFrame->argument(1)); + RETURN_IF_EXCEPTION(throwScope, {}); + + // Default values + String domain; + String path = "/"_s; + double expires = 0; + double maxAge = 0; + bool secure = false; + bool httpOnly = false; + bool partitioned = false; + CookieSameSite sameSite = CookieSameSite::Lax; + auto& names = builtinNames(vm); + + // Optional options parameter (third argument) + if (callFrame->argumentCount() > 2 && !callFrame->argument(2).isUndefinedOrNull()) { + auto options = callFrame->argument(2); + + if (!options.isObject()) + return throwVMTypeError(lexicalGlobalObject, throwScope, "Options must be an object"_s); + + auto* optionsObj = options.getObject(); + + // domain + if (auto domainValue = optionsObj->get(lexicalGlobalObject, names.domainPublicName()); + !domainValue.isUndefined() && !domainValue.isNull()) { + domain = convert(*lexicalGlobalObject, domainValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // path + if (auto pathValue = optionsObj->get(lexicalGlobalObject, names.pathPublicName()); + !pathValue.isUndefined() && !pathValue.isNull()) { + path = convert(*lexicalGlobalObject, pathValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // expires + if (auto expiresValue = optionsObj->get(lexicalGlobalObject, names.expiresPublicName()); + !expiresValue.isUndefined() && !expiresValue.isNull()) { + // Handle both Date objects and numeric timestamps + if (expiresValue.inherits()) { + JSC::DateInstance* dateInstance = JSC::jsCast(expiresValue.asCell()); + expires = dateInstance->internalNumber(); + } else if (expiresValue.isNumber()) { + expires = expiresValue.asNumber(); + } + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // maxAge + if (auto maxAgeValue = optionsObj->get(lexicalGlobalObject, names.maxAgePublicName()); + !maxAgeValue.isUndefined() && !maxAgeValue.isNull() && maxAgeValue.isNumber()) { + maxAge = maxAgeValue.asNumber(); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // secure + if (auto secureValue = optionsObj->get(lexicalGlobalObject, names.securePublicName()); + !secureValue.isUndefined()) { + secure = secureValue.toBoolean(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // httpOnly + if (auto httpOnlyValue = optionsObj->get(lexicalGlobalObject, names.httpOnlyPublicName()); + !httpOnlyValue.isUndefined()) { + httpOnly = httpOnlyValue.toBoolean(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // partitioned + if (auto partitionedValue = optionsObj->get(lexicalGlobalObject, names.partitionedPublicName()); + !partitionedValue.isUndefined()) { + partitioned = partitionedValue.toBoolean(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // sameSite + if (auto sameSiteValue = optionsObj->get(lexicalGlobalObject, names.sameSitePublicName()); + !sameSiteValue.isUndefined() && !sameSiteValue.isNull()) { + String sameSiteStr = convert(*lexicalGlobalObject, sameSiteValue); + RETURN_IF_EXCEPTION(throwScope, {}); + + if (sameSiteStr == "strict"_s) + sameSite = CookieSameSite::Strict; + else if (sameSiteStr == "lax"_s) + sameSite = CookieSameSite::Lax; + else if (sameSiteStr == "none"_s) + sameSite = CookieSameSite::None; + else + return throwVMTypeError(lexicalGlobalObject, throwScope, "Invalid sameSite value. Must be 'strict', 'lax', or 'none'"_s); + } + } + + auto cookie = Cookie::create(name, value, domain, path, expires, secure, sameSite, httpOnly, maxAge, partitioned); + auto* globalObject = castedThis->globalObject(); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS(lexicalGlobalObject, globalObject, WTFMove(cookie)))); + } + + return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); +} + +JSC_ANNOTATE_HOST_FUNCTION(JSCookieDOMConstructorConstruct, JSCookieDOMConstructor::construct); + +// Setup for JSCookieDOMConstructor +template<> const ClassInfo JSCookieDOMConstructor::s_info = { "Cookie"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCookieDOMConstructor) }; + +template<> JSValue JSCookieDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + return globalObject.objectPrototype(); +} + +template<> void JSCookieDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) +{ + putDirect(vm, vm.propertyNames->length, jsNumber(2), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "Cookie"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSCookie::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); + + // Add static methods + JSC::JSFunction* parseFunction = JSC::JSFunction::create(vm, &globalObject, 1, "parse"_s, jsCookieStaticFunctionParse, JSC::ImplementationVisibility::Public, JSC::NoIntrinsic); + putDirect(vm, Identifier::fromString(vm, "parse"_s), parseFunction, static_cast(JSC::PropertyAttribute::DontDelete)); + + JSC::JSFunction* fromFunction = JSC::JSFunction::create(vm, &globalObject, 3, "from"_s, jsCookieStaticFunctionFrom, JSC::ImplementationVisibility::Public, JSC::NoIntrinsic); + putDirect(vm, Identifier::fromString(vm, "from"_s), fromFunction, static_cast(JSC::PropertyAttribute::DontDelete)); + + JSC::JSFunction* serializeFunction = JSC::JSFunction::create(vm, &globalObject, 1, "serialize"_s, jsCookieStaticFunctionSerialize, JSC::ImplementationVisibility::Public, JSC::NoIntrinsic); + putDirect(vm, Identifier::fromString(vm, "serialize"_s), serializeFunction, static_cast(JSC::PropertyAttribute::DontDelete)); +} + +static const HashTableValue JSCookiePrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsCookieConstructor, 0 } }, + { "name"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsCookiePrototypeGetter_name, jsCookiePrototypeSetter_name } }, + { "value"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsCookiePrototypeGetter_value, jsCookiePrototypeSetter_value } }, + { "domain"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsCookiePrototypeGetter_domain, jsCookiePrototypeSetter_domain } }, + { "path"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsCookiePrototypeGetter_path, jsCookiePrototypeSetter_path } }, + { "expires"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsCookiePrototypeGetter_expires, jsCookiePrototypeSetter_expires } }, + { "maxAge"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsCookiePrototypeGetter_maxAge, jsCookiePrototypeSetter_maxAge } }, + { "secure"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsCookiePrototypeGetter_secure, jsCookiePrototypeSetter_secure } }, + { "httpOnly"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsCookiePrototypeGetter_httpOnly, jsCookiePrototypeSetter_httpOnly } }, + { "sameSite"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsCookiePrototypeGetter_sameSite, jsCookiePrototypeSetter_sameSite } }, + { "partitioned"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsCookiePrototypeGetter_partitioned, jsCookiePrototypeSetter_partitioned } }, + { "isExpired"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookiePrototypeFunction_isExpired, 0 } }, + { "toString"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookiePrototypeFunction_toString, 0 } }, + { "toJSON"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookiePrototypeFunction_toJSON, 0 } }, +}; + +const ClassInfo JSCookiePrototype::s_info = { "Cookie"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCookiePrototype) }; + +void JSCookiePrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSCookie::info(), JSCookiePrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +const ClassInfo JSCookie::s_info = { "Cookie"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCookie) }; + +JSCookie::JSCookie(Structure* structure, JSDOMGlobalObject& globalObject, Ref&& impl) + : JSDOMWrapper(structure, globalObject, WTFMove(impl)) +{ +} + +void JSCookie::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSObject* JSCookie::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSCookiePrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSCookiePrototype::create(vm, &globalObject, structure); +} + +JSObject* JSCookie::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSCookie::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *jsCast(globalObject)); +} + +void JSCookie::destroy(JSC::JSCell* cell) +{ + JSCookie* thisObject = static_cast(cell); + thisObject->JSCookie::~JSCookie(); +} + +JSC_DEFINE_CUSTOM_GETTER(jsCookieConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* prototype = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!prototype)) + return throwVMTypeError(lexicalGlobalObject, throwScope); + return JSValue::encode(JSCookie::getConstructor(vm, prototype->globalObject())); +} + +// Instance methods +static inline JSC::EncodedJSValue jsCookiePrototypeFunction_toStringBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto& impl = castedThis->wrapped(); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS(*lexicalGlobalObject, throwScope, impl.toString(vm)))); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookiePrototypeFunction_toString, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "toString"); +} + +// Implementation of the toJSON method +static inline JSC::EncodedJSValue jsCookiePrototypeFunction_toJSONBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto& impl = castedThis->wrapped(); + + // Delegate to the C++ toJSON method + JSC::JSValue result = impl.toJSON(vm, lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + + return JSValue::encode(result); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookiePrototypeFunction_toJSON, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "toJSON"); +} + +// Static function implementations +JSC_DEFINE_HOST_FUNCTION(jsCookieStaticFunctionParse, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + if (callFrame->argumentCount() < 1) + return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); + + auto cookieString = convert(*lexicalGlobalObject, callFrame->uncheckedArgument(0)); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto result = Cookie::parse(cookieString); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto* globalObject = jsCast(lexicalGlobalObject); + return JSValue::encode(toJS(lexicalGlobalObject, globalObject, result.releaseReturnValue())); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookieStaticFunctionFrom, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + if (callFrame->argumentCount() < 2) + return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); + + auto name = convert(*lexicalGlobalObject, callFrame->uncheckedArgument(0)); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto value = convert(*lexicalGlobalObject, callFrame->uncheckedArgument(1)); + RETURN_IF_EXCEPTION(throwScope, {}); + + // Optional parameters + String domain; + String path = "/"_s; + double expires = 0; + double maxAge = 0; + bool secure = false; + bool httpOnly = false; + bool partitioned = false; + auto& builtinNames = Bun::builtinNames(vm); + + CookieSameSite sameSite = CookieSameSite::Lax; + + // Check for options object + if (callFrame->argumentCount() > 2 && !callFrame->uncheckedArgument(2).isUndefinedOrNull() && callFrame->uncheckedArgument(2).isObject()) { + auto* options = callFrame->uncheckedArgument(2).getObject(); + + // domain + auto domainValue = options->get(lexicalGlobalObject, builtinNames.domainPublicName()); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!domainValue.isUndefined() && !domainValue.isNull()) { + domain = convert(*lexicalGlobalObject, domainValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // path + auto pathValue = options->get(lexicalGlobalObject, builtinNames.pathPublicName()); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!pathValue.isUndefined() && !pathValue.isNull()) { + path = convert(*lexicalGlobalObject, pathValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // expires + auto expiresValue = options->get(lexicalGlobalObject, builtinNames.expiresPublicName()); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!expiresValue.isUndefined() && !expiresValue.isNull()) { + if (auto* dateInstance = jsDynamicCast(expiresValue)) { + expires = dateInstance->internalNumber(); + } else if (expiresValue.isNumber()) { + expires = expiresValue.asNumber(); + } + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // maxAge + auto maxAgeValue = options->get(lexicalGlobalObject, builtinNames.maxAgePublicName()); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!maxAgeValue.isUndefined() && !maxAgeValue.isNull() && maxAgeValue.isNumber()) { + maxAge = maxAgeValue.asNumber(); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // secure + auto secureValue = options->get(lexicalGlobalObject, builtinNames.securePublicName()); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!secureValue.isUndefined()) { + secure = secureValue.toBoolean(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // httpOnly + auto httpOnlyValue = options->get(lexicalGlobalObject, builtinNames.httpOnlyPublicName()); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!httpOnlyValue.isUndefined()) { + httpOnly = httpOnlyValue.toBoolean(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // partitioned + auto partitionedValue = options->get(lexicalGlobalObject, builtinNames.partitionedPublicName()); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!partitionedValue.isUndefined()) { + partitioned = partitionedValue.toBoolean(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // sameSite + auto sameSiteValue = options->get(lexicalGlobalObject, builtinNames.sameSitePublicName()); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!sameSiteValue.isUndefined() && !sameSiteValue.isNull()) { + String sameSiteStr = convert(*lexicalGlobalObject, sameSiteValue); + RETURN_IF_EXCEPTION(throwScope, {}); + + if (WTF::equalIgnoringASCIICase(sameSiteStr, "strict"_s)) + sameSite = CookieSameSite::Strict; + else if (WTF::equalIgnoringASCIICase(sameSiteStr, "lax"_s)) + sameSite = CookieSameSite::Lax; + else if (WTF::equalIgnoringASCIICase(sameSiteStr, "none"_s)) + sameSite = CookieSameSite::None; + else + return throwVMTypeError(lexicalGlobalObject, throwScope, "Invalid sameSite value. Must be 'strict', 'lax', or 'none'"_s); + } + } + + // Create the cookie + auto cookie = Cookie::from(name, value, domain, path, expires, secure, sameSite, httpOnly, maxAge, partitioned); + + auto* globalObject = jsCast(lexicalGlobalObject); + return JSValue::encode(toJSNewlyCreated(lexicalGlobalObject, globalObject, WTFMove(cookie))); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookieStaticFunctionSerialize, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + if (callFrame->argumentCount() < 1) + return JSValue::encode(jsEmptyString(vm)); + + Vector> cookies; + + // Process each cookie argument + for (unsigned i = 0; i < callFrame->argumentCount(); i++) { + auto* cookieImpl = toCookieWrapped(lexicalGlobalObject, throwScope, callFrame->uncheckedArgument(i)); + RETURN_IF_EXCEPTION(throwScope, {}); + + if (cookieImpl) + cookies.append(*cookieImpl); + } + + // Let the C++ Cookie::serialize handle the work + String result = Cookie::serialize(vm, cookies); + + return JSValue::encode(jsString(vm, result)); +} + +// Property getters/setters +JSC_DEFINE_CUSTOM_GETTER(jsCookiePrototypeGetter_name, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "name"_s); + auto& impl = thisObject->wrapped(); + return JSValue::encode(toJS(*lexicalGlobalObject, throwScope, impl.name())); +} + +JSC_DEFINE_CUSTOM_SETTER(jsCookiePrototypeSetter_name, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "name"_s); + auto& impl = thisObject->wrapped(); + auto value = convert(*lexicalGlobalObject, JSValue::decode(encodedValue)); + RETURN_IF_EXCEPTION(throwScope, false); + impl.setName(value); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsCookiePrototypeGetter_value, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "value"_s); + auto& impl = thisObject->wrapped(); + return JSValue::encode(toJS(*lexicalGlobalObject, throwScope, impl.value())); +} + +JSC_DEFINE_CUSTOM_SETTER(jsCookiePrototypeSetter_value, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "value"_s); + auto& impl = thisObject->wrapped(); + auto value = convert(*lexicalGlobalObject, JSValue::decode(encodedValue)); + RETURN_IF_EXCEPTION(throwScope, false); + impl.setValue(value); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsCookiePrototypeGetter_domain, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "domain"_s); + auto& impl = thisObject->wrapped(); + return JSValue::encode(toJS>(*lexicalGlobalObject, throwScope, impl.domain())); +} + +JSC_DEFINE_CUSTOM_SETTER(jsCookiePrototypeSetter_domain, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "domain"_s); + auto& impl = thisObject->wrapped(); + auto value = convert(*lexicalGlobalObject, JSValue::decode(encodedValue)); + RETURN_IF_EXCEPTION(throwScope, false); + impl.setDomain(value); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsCookiePrototypeGetter_path, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "path"_s); + auto& impl = thisObject->wrapped(); + return JSValue::encode(toJS(*lexicalGlobalObject, throwScope, impl.path())); +} + +JSC_DEFINE_CUSTOM_SETTER(jsCookiePrototypeSetter_path, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "path"_s); + auto& impl = thisObject->wrapped(); + auto value = convert(*lexicalGlobalObject, JSValue::decode(encodedValue)); + RETURN_IF_EXCEPTION(throwScope, false); + impl.setPath(value); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsCookiePrototypeGetter_expires, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "expires"_s); + auto& impl = thisObject->wrapped(); + return JSValue::encode(toJS>(*lexicalGlobalObject, throwScope, impl.expires())); +} + +JSC_DEFINE_CUSTOM_SETTER(jsCookiePrototypeSetter_expires, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "expires"_s); + auto& impl = thisObject->wrapped(); + auto value = convert(*lexicalGlobalObject, JSValue::decode(encodedValue)); + RETURN_IF_EXCEPTION(throwScope, false); + impl.setExpires(value); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsCookiePrototypeGetter_secure, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "secure"_s); + auto& impl = thisObject->wrapped(); + return JSValue::encode(toJS(*lexicalGlobalObject, throwScope, impl.secure())); +} + +JSC_DEFINE_CUSTOM_SETTER(jsCookiePrototypeSetter_secure, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "secure"_s); + auto& impl = thisObject->wrapped(); + auto value = convert(*lexicalGlobalObject, JSValue::decode(encodedValue)); + RETURN_IF_EXCEPTION(throwScope, false); + impl.setSecure(value); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsCookiePrototypeGetter_sameSite, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "sameSite"_s); + auto& impl = thisObject->wrapped(); + + return JSValue::encode(toJS(lexicalGlobalObject, impl.sameSite())); +} + +JSC_DEFINE_CUSTOM_SETTER(jsCookiePrototypeSetter_sameSite, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "sameSite"_s); + auto& impl = thisObject->wrapped(); + + auto sameSiteStr = convert(*lexicalGlobalObject, JSValue::decode(encodedValue)); + RETURN_IF_EXCEPTION(throwScope, false); + + CookieSameSite sameSite; + if (WTF::equalIgnoringASCIICase(sameSiteStr, "strict"_s)) + sameSite = CookieSameSite::Strict; + else if (WTF::equalIgnoringASCIICase(sameSiteStr, "lax"_s)) + sameSite = CookieSameSite::Lax; + else if (WTF::equalIgnoringASCIICase(sameSiteStr, "none"_s)) + sameSite = CookieSameSite::None; + else { + throwTypeError(lexicalGlobalObject, throwScope, "Invalid sameSite value. Must be 'strict', 'lax', or 'none'"_s); + return false; + } + + impl.setSameSite(sameSite); + return true; +} + +// HttpOnly property +JSC_DEFINE_CUSTOM_GETTER(jsCookiePrototypeGetter_httpOnly, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "httpOnly"_s); + auto& impl = thisObject->wrapped(); + return JSValue::encode(toJS(*lexicalGlobalObject, throwScope, impl.httpOnly())); +} + +JSC_DEFINE_CUSTOM_SETTER(jsCookiePrototypeSetter_httpOnly, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "httpOnly"_s); + auto& impl = thisObject->wrapped(); + auto value = convert(*lexicalGlobalObject, JSValue::decode(encodedValue)); + RETURN_IF_EXCEPTION(throwScope, false); + impl.setHttpOnly(value); + return true; +} + +// MaxAge property +JSC_DEFINE_CUSTOM_GETTER(jsCookiePrototypeGetter_maxAge, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "maxAge"_s); + auto& impl = thisObject->wrapped(); + return JSValue::encode(toJS>(*lexicalGlobalObject, throwScope, impl.maxAge())); +} + +JSC_DEFINE_CUSTOM_SETTER(jsCookiePrototypeSetter_maxAge, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "maxAge"_s); + auto& impl = thisObject->wrapped(); + auto value = convert(*lexicalGlobalObject, JSValue::decode(encodedValue)); + RETURN_IF_EXCEPTION(throwScope, false); + impl.setMaxAge(value); + + return true; +} + +// Partitioned property +JSC_DEFINE_CUSTOM_GETTER(jsCookiePrototypeGetter_partitioned, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "partitioned"_s); + auto& impl = thisObject->wrapped(); + return JSValue::encode(toJS(*lexicalGlobalObject, throwScope, impl.partitioned())); +} + +JSC_DEFINE_CUSTOM_SETTER(jsCookiePrototypeSetter_partitioned, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwThisTypeError(*lexicalGlobalObject, throwScope, "Cookie"_s, "partitioned"_s); + auto& impl = thisObject->wrapped(); + auto value = convert(*lexicalGlobalObject, JSValue::decode(encodedValue)); + RETURN_IF_EXCEPTION(throwScope, false); + impl.setPartitioned(value); + return true; +} + +// isExpired method +static inline JSC::EncodedJSValue jsCookiePrototypeFunction_isExpiredBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto& impl = castedThis->wrapped(); + + bool expired = impl.isExpired(); + return JSValue::encode(JSC::jsBoolean(expired)); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookiePrototypeFunction_isExpired, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "isExpired"); +} + +GCClient::IsoSubspace* JSCookie::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForCookie.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForCookie = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForCookie.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForCookie = std::forward(space); }); +} + +void JSCookie::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) +{ + auto* thisObject = jsCast(cell); + analyzer.setWrappedObjectForCell(cell, &thisObject->wrapped()); + Base::analyzeHeap(cell, analyzer); +} + +bool JSCookieOwner::isReachableFromOpaqueRoots(JSC::Handle handle, void*, AbstractSlotVisitor& visitor, ASCIILiteral* reason) +{ + UNUSED_PARAM(handle); + UNUSED_PARAM(visitor); + UNUSED_PARAM(reason); + return false; +} + +void JSCookieOwner::finalize(JSC::Handle handle, void* context) +{ + auto* jsCookie = static_cast(handle.slot()->asCell()); + auto& world = *static_cast(context); + uncacheWrapper(world, &jsCookie->wrapped(), jsCookie); +} + +JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject* globalObject, Ref&& impl) +{ + return createWrapper(globalObject, WTFMove(impl)); +} + +JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, Cookie& impl) +{ + return wrap(lexicalGlobalObject, globalObject, impl); +} + +Cookie* JSCookie::toWrapped(JSC::VM& vm, JSC::JSValue value) +{ + if (auto* wrapper = jsDynamicCast(value)) + return &wrapper->wrapped(); + return nullptr; +} + +size_t JSCookie::estimatedSize(JSC::JSCell* cell, JSC::VM& vm) +{ + auto* thisObject = jsCast(cell); + auto& wrapped = thisObject->wrapped(); + return Base::estimatedSize(cell, vm) + wrapped.memoryCost(); +} + +JSC::JSValue toJS(JSC::JSGlobalObject* globalObject, CookieSameSite sameSite) +{ + auto& commonStrings = defaultGlobalObject(globalObject)->commonStrings(); + switch (sameSite) { + case CookieSameSite::Strict: + return commonStrings.strictString(globalObject); + case CookieSameSite::Lax: + return commonStrings.laxString(globalObject); + case CookieSameSite::None: + return commonStrings.noneString(globalObject); + default: { + break; + } + } + __builtin_unreachable(); + return {}; +} + +} diff --git a/src/bun.js/bindings/webcore/JSCookie.h b/src/bun.js/bindings/webcore/JSCookie.h new file mode 100644 index 0000000000..dda730d2bf --- /dev/null +++ b/src/bun.js/bindings/webcore/JSCookie.h @@ -0,0 +1,75 @@ +#pragma once + +#include "JSDOMWrapper.h" +#include "Cookie.h" +#include + +namespace WebCore { + +class JSCookie : public JSDOMWrapper { +public: + using Base = JSDOMWrapper; + static JSCookie* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject, Ref&& impl) + { + JSCookie* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSCookie(structure, *globalObject, WTFMove(impl)); + ptr->finishCreation(globalObject->vm()); + return ptr; + } + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static Cookie* toWrapped(JSC::VM&, JSC::JSValue); + static void destroy(JSC::JSCell*); + + DECLARE_INFO; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(static_cast(WebCore::JSAsJSONType), StructureFlags), info()); + } + + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); + static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm); + +protected: + JSCookie(JSC::Structure*, JSDOMGlobalObject&, Ref&&); + + void finishCreation(JSC::VM&); +}; + +class JSCookieOwner final : public JSC::WeakHandleOwner { +public: + bool isReachableFromOpaqueRoots(JSC::Handle, void* context, JSC::AbstractSlotVisitor&, ASCIILiteral*) final; + void finalize(JSC::Handle, void* context) final; +}; + +inline JSC::WeakHandleOwner* wrapperOwner(DOMWrapperWorld&, Cookie*) +{ + static NeverDestroyed owner; + return &owner.get(); +} + +inline void* wrapperKey(Cookie* wrappableObject) +{ + return wrappableObject; +} +JSC::JSValue getInternalProperties(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSCookie* castedThis); +JSC::JSValue toJS(JSC::JSGlobalObject*, JSDOMGlobalObject*, Cookie&); +inline JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, Cookie* impl) { return impl ? toJS(lexicalGlobalObject, globalObject, *impl) : JSC::jsNull(); } +JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject*, Ref&&); +inline JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, RefPtr&& impl) { return impl ? toJSNewlyCreated(lexicalGlobalObject, globalObject, impl.releaseNonNull()) : JSC::jsNull(); } + +template<> struct JSDOMWrapperConverterTraits { + using WrapperClass = JSCookie; + using ToWrappedReturnType = Cookie*; +}; + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSCookieMap.cpp b/src/bun.js/bindings/webcore/JSCookieMap.cpp new file mode 100644 index 0000000000..18aaedec7c --- /dev/null +++ b/src/bun.js/bindings/webcore/JSCookieMap.cpp @@ -0,0 +1,893 @@ +#include "config.h" +#include "JSCookieMap.h" + +#include "Cookie.h" +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "IDLTypes.h" +#include "JSCookie.h" +#include "JSDOMBinding.h" +#include "JSDOMConstructor.h" +#include "JSDOMConvertBase.h" +#include "JSDOMConvertBoolean.h" +#include "JSDOMConvertDate.h" +#include "JSDOMConvertInterface.h" +#include "JSDOMConvertNullable.h" +#include "JSDOMConvertRecord.h" +#include "JSDOMConvertSequences.h" +#include "JSDOMConvertStrings.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMIterator.h" +#include "JSDOMOperation.h" +#include "JSDOMWrapperCache.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; + +// Define the toWrapped template function for CookieMap +template +CookieMap* toWrapped(JSGlobalObject& lexicalGlobalObject, ExceptionThrower&& exceptionThrower, JSValue value) +{ + auto& vm = getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* impl = JSCookieMap::toWrapped(vm, value); + if (UNLIKELY(!impl)) + exceptionThrower(lexicalGlobalObject, scope); + return impl; +} + +static JSC_DECLARE_HOST_FUNCTION(jsCookieMapPrototypeFunction_get); +static JSC_DECLARE_HOST_FUNCTION(jsCookieMapPrototypeFunction_getAll); +static JSC_DECLARE_HOST_FUNCTION(jsCookieMapPrototypeFunction_has); +static JSC_DECLARE_HOST_FUNCTION(jsCookieMapPrototypeFunction_set); +static JSC_DECLARE_HOST_FUNCTION(jsCookieMapPrototypeFunction_delete); +static JSC_DECLARE_HOST_FUNCTION(jsCookieMapPrototypeFunction_toString); +static JSC_DECLARE_HOST_FUNCTION(jsCookieMapPrototypeFunction_toJSON); +static JSC_DECLARE_HOST_FUNCTION(jsCookieMapPrototypeFunction_entries); +static JSC_DECLARE_HOST_FUNCTION(jsCookieMapPrototypeFunction_keys); +static JSC_DECLARE_HOST_FUNCTION(jsCookieMapPrototypeFunction_values); +static JSC_DECLARE_HOST_FUNCTION(jsCookieMapPrototypeFunction_forEach); +static JSC_DECLARE_CUSTOM_GETTER(jsCookieMapPrototypeGetter_size); +static JSC_DECLARE_CUSTOM_GETTER(jsCookieMapConstructor); + +class JSCookieMapPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSCookieMapPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSCookieMapPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSCookieMapPrototype(vm, globalObject, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSCookieMapPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSCookieMapPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; + +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSCookieMapPrototype, JSCookieMapPrototype::Base); + +using JSCookieMapDOMConstructor = JSDOMConstructor; + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSCookieMapDOMConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* castedThis = jsCast(callFrame->jsCallee()); + + // Check arguments + JSValue initValue = callFrame->argument(0); + + std::variant>, HashMap, String> init; + + if (initValue.isUndefinedOrNull() || (initValue.isString() && initValue.getString(lexicalGlobalObject).isEmpty())) { + init = String(); + } else if (initValue.isString()) { + init = initValue.getString(lexicalGlobalObject); + } else if (initValue.isObject()) { + auto* object = initValue.getObject(); + + if (isArray(lexicalGlobalObject, object)) { + auto* array = jsCast(object); + Vector> seqSeq; + + uint32_t length = array->length(); + for (uint32_t i = 0; i < length; ++i) { + auto element = array->getIndex(lexicalGlobalObject, i); + RETURN_IF_EXCEPTION(throwScope, {}); + + if (!element.isObject() || !jsDynamicCast(element)) { + throwTypeError(lexicalGlobalObject, throwScope, "Expected each element to be an array of two strings"_s); + return {}; + } + + auto* subArray = jsCast(element); + if (subArray->length() != 2) { + throwTypeError(lexicalGlobalObject, throwScope, "Expected arrays of exactly two strings"_s); + return {}; + } + + auto first = subArray->getIndex(lexicalGlobalObject, 0); + RETURN_IF_EXCEPTION(throwScope, {}); + auto second = subArray->getIndex(lexicalGlobalObject, 1); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto firstStr = first.toString(lexicalGlobalObject)->value(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + auto secondStr = second.toString(lexicalGlobalObject)->value(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + + Vector pair; + pair.append(firstStr); + pair.append(secondStr); + seqSeq.append(WTFMove(pair)); + } + init = WTFMove(seqSeq); + } else { + // Handle as record + HashMap record; + + PropertyNameArray propertyNames(vm, PropertyNameMode::Strings, PrivateSymbolMode::Exclude); + JSObject::getOwnPropertyNames(object, lexicalGlobalObject, propertyNames, DontEnumPropertiesMode::Include); + RETURN_IF_EXCEPTION(throwScope, {}); + + for (const auto& propertyName : propertyNames) { + JSValue value = object->get(lexicalGlobalObject, propertyName); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto valueStr = value.toString(lexicalGlobalObject)->value(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + + record.set(propertyName.string(), valueStr); + } + init = WTFMove(record); + } + } else { + throwTypeError(lexicalGlobalObject, throwScope, "Invalid initializer type"_s); + return {}; + } + + auto result = CookieMap::create(WTFMove(init)); + RETURN_IF_EXCEPTION(throwScope, {}); + + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJSNewlyCreated(lexicalGlobalObject, castedThis->globalObject(), result.releaseReturnValue()))); +} + +JSC_ANNOTATE_HOST_FUNCTION(JSCookieMapDOMConstructorConstruct, JSCookieMapDOMConstructor::construct); + +template<> const ClassInfo JSCookieMapDOMConstructor::s_info = { "CookieMap"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCookieMapDOMConstructor) }; + +template<> JSValue JSCookieMapDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + return globalObject.objectPrototype(); +} + +template<> void JSCookieMapDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) +{ + putDirect(vm, vm.propertyNames->length, jsNumber(1), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "CookieMap"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSCookieMap::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); +} + +static const HashTableValue JSCookieMapPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsCookieMapConstructor, 0 } }, + { "get"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookieMapPrototypeFunction_get, 1 } }, + { "getAll"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookieMapPrototypeFunction_getAll, 1 } }, + { "has"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookieMapPrototypeFunction_has, 1 } }, + { "set"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookieMapPrototypeFunction_set, 1 } }, + { "delete"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookieMapPrototypeFunction_delete, 1 } }, + { "entries"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookieMapPrototypeFunction_entries, 0 } }, + { "keys"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookieMapPrototypeFunction_keys, 0 } }, + { "values"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookieMapPrototypeFunction_values, 0 } }, + { "forEach"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookieMapPrototypeFunction_forEach, 1 } }, + { "toString"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookieMapPrototypeFunction_toString, 0 } }, + { "toJSON"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsCookieMapPrototypeFunction_toJSON, 0 } }, + { "size"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsCookieMapPrototypeGetter_size, 0 } }, +}; + +const ClassInfo JSCookieMapPrototype::s_info = { "CookieMap"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCookieMapPrototype) }; + +void JSCookieMapPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSCookieMap::info(), JSCookieMapPrototypeTableValues, *this); + putDirect(vm, vm.propertyNames->iteratorSymbol, getDirect(vm, PropertyName(Identifier::fromString(vm, "entries"_s))), static_cast(JSC::PropertyAttribute::DontEnum)); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +const ClassInfo JSCookieMap::s_info = { "CookieMap"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCookieMap) }; + +JSCookieMap::JSCookieMap(Structure* structure, JSDOMGlobalObject& globalObject, Ref&& impl) + : JSDOMWrapper(structure, globalObject, WTFMove(impl)) +{ +} + +void JSCookieMap::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSObject* JSCookieMap::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSCookieMapPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSCookieMapPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSCookieMap::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSCookieMap::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *jsCast(globalObject)); +} + +void JSCookieMap::destroy(JSC::JSCell* cell) +{ + JSCookieMap* thisObject = static_cast(cell); + thisObject->JSCookieMap::~JSCookieMap(); +} + +JSC_DEFINE_CUSTOM_GETTER(jsCookieMapConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* prototype = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!prototype)) + return throwVMTypeError(lexicalGlobalObject, throwScope); + return JSValue::encode(JSCookieMap::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsCookieMapPrototypeGetter_size, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return throwVMTypeError(lexicalGlobalObject, throwScope); + return JSValue::encode(jsNumber(thisObject->wrapped().size())); +} + +// Implementation of the get method +static inline JSC::EncodedJSValue jsCookieMapPrototypeFunction_getBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto& impl = castedThis->wrapped(); + + if (callFrame->argumentCount() < 1) + return JSValue::encode(jsNull()); + + JSValue arg0 = callFrame->uncheckedArgument(0); + auto& names = builtinNames(vm); + + if (arg0.isObject()) { + // Handle options object + auto* options = arg0.getObject(); + + // Extract name + auto nameValue = options->get(lexicalGlobalObject, PropertyName(vm.propertyNames->name)); + RETURN_IF_EXCEPTION(throwScope, {}); + + if (!nameValue.isUndefined()) { + auto name = convert(*lexicalGlobalObject, nameValue); + RETURN_IF_EXCEPTION(throwScope, {}); + + // Get cookie by name + auto cookie = impl.get(name); + if (!cookie) + return JSValue::encode(jsNull()); + + // Return as Cookie object + return JSValue::encode(toJS(lexicalGlobalObject, castedThis->globalObject(), *cookie)); + } + + // Extract url + auto urlValue = options->get(lexicalGlobalObject, names.urlPublicName()); + RETURN_IF_EXCEPTION(throwScope, {}); + + if (!urlValue.isUndefined()) { + auto url = convert(*lexicalGlobalObject, urlValue); + RETURN_IF_EXCEPTION(throwScope, {}); + + // Create options struct and get cookie by URL + CookieStoreGetOptions options; + options.url = url; + auto cookie = impl.get(options); + if (!cookie) + return JSValue::encode(jsNull()); + + // Return as Cookie object + return JSValue::encode(toJS(lexicalGlobalObject, castedThis->globalObject(), *cookie)); + } + + // If we got here, neither name nor url was provided + return JSValue::encode(jsNull()); + } else { + // Handle single string argument (name) + auto name = convert(*lexicalGlobalObject, arg0); + RETURN_IF_EXCEPTION(throwScope, {}); + + auto cookie = impl.get(name); + if (!cookie) + return JSValue::encode(jsNull()); + + // Return as Cookie object + return JSValue::encode(toJS(lexicalGlobalObject, castedThis->globalObject(), *cookie)); + } +} + +JSC_DEFINE_HOST_FUNCTION(jsCookieMapPrototypeFunction_get, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "get"); +} + +// Implementation of the getAll method +static inline JSC::EncodedJSValue jsCookieMapPrototypeFunction_getAllBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto& impl = castedThis->wrapped(); + + if (callFrame->argumentCount() < 1) + return JSValue::encode(JSC::constructEmptyArray(lexicalGlobalObject, nullptr)); + + JSValue arg0 = callFrame->uncheckedArgument(0); + Vector> cookies; + auto& names = builtinNames(vm); + + if (arg0.isObject()) { + // Handle options object + auto* options = arg0.getObject(); + + // Extract name + auto nameValue = options->get(lexicalGlobalObject, PropertyName(vm.propertyNames->name)); + RETURN_IF_EXCEPTION(throwScope, {}); + + if (!nameValue.isUndefined()) { + auto name = convert(*lexicalGlobalObject, nameValue); + RETURN_IF_EXCEPTION(throwScope, {}); + + // Get cookies by name + cookies = impl.getAll(name); + } else { + // Extract url + auto urlValue = options->get(lexicalGlobalObject, names.urlPublicName()); + RETURN_IF_EXCEPTION(throwScope, {}); + + if (!urlValue.isUndefined()) { + auto url = convert(*lexicalGlobalObject, urlValue); + RETURN_IF_EXCEPTION(throwScope, {}); + + // Create options struct and get cookies by URL + CookieStoreGetOptions options; + options.url = url; + cookies = impl.getAll(options); + } + } + } else { + // Handle single string argument (name) + auto name = convert(*lexicalGlobalObject, arg0); + RETURN_IF_EXCEPTION(throwScope, {}); + + cookies = impl.getAll(name); + } + + // Create array of Cookie objects + JSC::JSArray* resultArray = JSC::constructEmptyArray(lexicalGlobalObject, nullptr, cookies.size()); + RETURN_IF_EXCEPTION(throwScope, {}); + + for (size_t i = 0; i < cookies.size(); ++i) { + resultArray->putDirectIndex(lexicalGlobalObject, i, toJS(lexicalGlobalObject, castedThis->globalObject(), cookies[i])); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + return JSValue::encode(resultArray); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookieMapPrototypeFunction_getAll, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "getAll"); +} + +// Implementation of the has method +static inline JSC::EncodedJSValue jsCookieMapPrototypeFunction_hasBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto& impl = castedThis->wrapped(); + + if (callFrame->argumentCount() < 1) + return JSValue::encode(jsBoolean(false)); + + auto name = convert(*lexicalGlobalObject, callFrame->uncheckedArgument(0)); + RETURN_IF_EXCEPTION(throwScope, {}); + + String value; + if (callFrame->argumentCount() > 1) { + value = convert(*lexicalGlobalObject, callFrame->uncheckedArgument(1)); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + return JSValue::encode(jsBoolean(impl.has(name, value))); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookieMapPrototypeFunction_has, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "has"); +} + +// Implementation of the set method +static inline JSC::EncodedJSValue jsCookieMapPrototypeFunction_setBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto& impl = castedThis->wrapped(); + + if (callFrame->argumentCount() < 1) + return JSValue::encode(jsUndefined()); + + JSValue arg0 = callFrame->uncheckedArgument(0); + JSValue arg2 = callFrame->argument(2); + String name; + String value; + + // Extract optional fields + String domain; + String path = "/"_s; + double expires = 0; + bool secure = false; + CookieSameSite sameSite = CookieSameSite::Lax; + // Create and set the cookie + // Extract httpOnly and partitioned + bool httpOnly = false; + bool partitioned = false; + double maxAge = 0; + + auto& names = builtinNames(vm); + + const auto fromObject = [&](JSObject* obj, bool checkNameAndValue = false) -> void { + if (checkNameAndValue) { + auto nameValue = obj->get(lexicalGlobalObject, PropertyName(vm.propertyNames->name)); + RETURN_IF_EXCEPTION(throwScope, void()); + + if (nameValue.isUndefined() || nameValue.isNull()) { + throwVMError(lexicalGlobalObject, throwScope, createTypeError(lexicalGlobalObject, "Cookie name is required"_s)); + return; + } + + auto valueValue = obj->get(lexicalGlobalObject, vm.propertyNames->value); + RETURN_IF_EXCEPTION(throwScope, void()); + + if (valueValue.isUndefined() || valueValue.isNull()) { + throwVMError(lexicalGlobalObject, throwScope, createTypeError(lexicalGlobalObject, "Cookie value is required"_s)); + return; + } + + name = convert(*lexicalGlobalObject, nameValue); + RETURN_IF_EXCEPTION(throwScope, void()); + + value = convert(*lexicalGlobalObject, valueValue); + RETURN_IF_EXCEPTION(throwScope, void()); + } + + // domain + auto domainValue = obj->get(lexicalGlobalObject, names.domainPublicName()); + RETURN_IF_EXCEPTION(throwScope, void()); + if (!domainValue.isUndefined() && !domainValue.isNull()) { + domain = convert(*lexicalGlobalObject, domainValue); + RETURN_IF_EXCEPTION(throwScope, void()); + } + + // path + auto pathValue = obj->get(lexicalGlobalObject, names.pathPublicName()); + RETURN_IF_EXCEPTION(throwScope, void()); + if (!pathValue.isUndefined() && !pathValue.isNull()) { + path = convert(*lexicalGlobalObject, pathValue); + RETURN_IF_EXCEPTION(throwScope, void()); + } + + // expires + auto expiresValue = obj->get(lexicalGlobalObject, names.expiresPublicName()); + RETURN_IF_EXCEPTION(throwScope, void()); + if (!expiresValue.isUndefined() && !expiresValue.isNull()) { + // Handle Date object + if (expiresValue.inherits()) { + auto* dateInstance = jsCast(expiresValue.asCell()); + expires = dateInstance->internalNumber(); + RETURN_IF_EXCEPTION(throwScope, void()); + } else if (expiresValue.isNumber()) { + expires = expiresValue.asNumber(); + RETURN_IF_EXCEPTION(throwScope, void()); + } else if (expiresValue.isString()) { + auto expiresStr = convert(*lexicalGlobalObject, expiresValue); + RETURN_IF_EXCEPTION(throwScope, void()); + if (auto parsed = WTF::parseDate(expiresStr.span())) { + expires = parsed; + RETURN_IF_EXCEPTION(throwScope, void()); + } else { + throwVMError(lexicalGlobalObject, throwScope, createTypeError(lexicalGlobalObject, "Invalid cookie expiration date"_s)); + return; + } + } else { + throwVMError(lexicalGlobalObject, throwScope, createTypeError(lexicalGlobalObject, "Invalid cookie expiration date"_s)); + return; + } + } + + // secure + auto secureValue = obj->get(lexicalGlobalObject, names.securePublicName()); + RETURN_IF_EXCEPTION(throwScope, void()); + if (!secureValue.isUndefined()) { + secure = secureValue.toBoolean(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, void()); + } + + // sameSite + auto sameSiteValue = obj->get(lexicalGlobalObject, names.sameSitePublicName()); + RETURN_IF_EXCEPTION(throwScope, void()); + if (!sameSiteValue.isUndefined() && !sameSiteValue.isNull()) { + String sameSiteStr = convert(*lexicalGlobalObject, sameSiteValue); + RETURN_IF_EXCEPTION(throwScope, void()); + + if (sameSiteStr == "strict"_s) + sameSite = CookieSameSite::Strict; + else if (sameSiteStr == "lax"_s) + sameSite = CookieSameSite::Lax; + else if (sameSiteStr == "none"_s) + sameSite = CookieSameSite::None; + else + throwVMTypeError(lexicalGlobalObject, throwScope, "Invalid sameSite value. Must be 'strict', 'lax', or 'none'"_s); + return; + } + + auto httpOnlyValue + = obj->get(lexicalGlobalObject, PropertyName(names.httpOnlyPublicName())); + RETURN_IF_EXCEPTION(throwScope, void()); + if (!httpOnlyValue.isUndefined()) { + httpOnly = httpOnlyValue.toBoolean(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, void()); + } + + auto partitionedValue = obj->get(lexicalGlobalObject, PropertyName(names.partitionedPublicName())); + RETURN_IF_EXCEPTION(throwScope, void()); + if (!partitionedValue.isUndefined()) { + partitioned = partitionedValue.toBoolean(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, void()); + } + + auto maxAgeValue = obj->get(lexicalGlobalObject, PropertyName(names.maxAgePublicName())); + RETURN_IF_EXCEPTION(throwScope, void()); + if (!maxAgeValue.isUndefined() && !maxAgeValue.isNull() && maxAgeValue.isNumber()) { + maxAge = maxAgeValue.asNumber(); + RETURN_IF_EXCEPTION(throwScope, void()); + } + }; + + // Check if we're setting with a Cookie object directly + if (arg0.isObject() && JSCookie::toWrapped(vm, arg0)) { + auto* cookieImpl = JSCookie::toWrapped(vm, arg0); + if (cookieImpl) + impl.set(Ref(*cookieImpl)); + return JSValue::encode(jsUndefined()); + } else if (arg0.isObject()) { + auto* obj = arg0.getObject(); + fromObject(obj, true); + } else { + // Handle name/value pair + if (callFrame->argumentCount() < 2) + return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); + + name = convert(*lexicalGlobalObject, callFrame->uncheckedArgument(0)); + RETURN_IF_EXCEPTION(throwScope, {}); + + value = convert(*lexicalGlobalObject, callFrame->uncheckedArgument(1)); + RETURN_IF_EXCEPTION(throwScope, {}); + + // Check for optional third parameter (options) + if (callFrame->argumentCount() >= 3) { + JSValue optionsArg = arg2; + if (!optionsArg.isObject()) + return JSValue::encode(jsUndefined()); + + auto* options = optionsArg.getObject(); + fromObject(options, false); + } + } + + auto cookie = Cookie::create(name, value, domain, path, expires, secure, sameSite, httpOnly, maxAge, partitioned); + impl.set(WTFMove(cookie)); + + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookieMapPrototypeFunction_set, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "set"); +} + +// Implementation of the delete method +static inline JSC::EncodedJSValue jsCookieMapPrototypeFunction_deleteBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto& impl = castedThis->wrapped(); + + if (callFrame->argumentCount() < 1) + return JSValue::encode(jsUndefined()); + + JSValue arg0 = callFrame->uncheckedArgument(0); + auto& names = builtinNames(vm); + if (arg0.isObject()) { + // Handle as options object (CookieStoreDeleteOptions) + auto* options = arg0.getObject(); + + // Extract required name + auto nameValue = options->get(lexicalGlobalObject, PropertyName(vm.propertyNames->name)); + RETURN_IF_EXCEPTION(throwScope, {}); + + if (nameValue.isUndefined() || nameValue.isNull()) + return throwVMError(lexicalGlobalObject, throwScope, createTypeError(lexicalGlobalObject, "Cookie name is required"_s)); + + auto name = convert(*lexicalGlobalObject, nameValue); + RETURN_IF_EXCEPTION(throwScope, {}); + + CookieStoreDeleteOptions deleteOptions; + deleteOptions.name = name; + + // Extract optional domain + auto domainValue = options->get(lexicalGlobalObject, names.domainPublicName()); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!domainValue.isUndefined() && !domainValue.isNull()) { + deleteOptions.domain = convert(*lexicalGlobalObject, domainValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + // Extract optional path + auto pathValue = options->get(lexicalGlobalObject, names.pathPublicName()); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!pathValue.isUndefined() && !pathValue.isNull()) { + deleteOptions.path = convert(*lexicalGlobalObject, pathValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } else { + deleteOptions.path = "/"_s; + } + + impl.remove(deleteOptions); + } else { + // Handle single string argument (name) + auto name = convert(*lexicalGlobalObject, arg0); + RETURN_IF_EXCEPTION(throwScope, {}); + + impl.remove(name); + } + + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookieMapPrototypeFunction_delete, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "delete"); +} + +// Implementation of the toString method +static inline JSC::EncodedJSValue jsCookieMapPrototypeFunction_toStringBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto& impl = castedThis->wrapped(); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS(*lexicalGlobalObject, throwScope, impl.toString(vm)))); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookieMapPrototypeFunction_toString, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "toString"); +} + +// Implementation of the toJSON method +static inline JSC::EncodedJSValue jsCookieMapPrototypeFunction_toJSONBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto& impl = castedThis->wrapped(); + + // Delegate to the C++ toJSON method + JSC::JSValue result = impl.toJSON(lexicalGlobalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + + return JSValue::encode(result); +} + +JSC::JSValue getInternalProperties(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSCookieMap* castedThis) +{ + return castedThis->wrapped().toJSON(lexicalGlobalObject); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookieMapPrototypeFunction_toJSON, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "toJSON"); +} + +// Iterator implementation for CookieMap +struct CookieMapIteratorTraits { + static constexpr JSDOMIteratorType type = JSDOMIteratorType::Map; + using KeyType = IDLUSVString; + using ValueType = IDLInterface; +}; + +using CookieMapIteratorBase = JSDOMIteratorBase; +class CookieMapIterator final : public CookieMapIteratorBase { +public: + using Base = CookieMapIteratorBase; + DECLARE_INFO; + + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForCookieMapIterator.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForCookieMapIterator = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForCookieMapIterator.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForCookieMapIterator = std::forward(space); }); + } + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + + static CookieMapIterator* create(JSC::VM& vm, JSC::Structure* structure, JSCookieMap& iteratedObject, IterationKind kind) + { + auto* instance = new (NotNull, JSC::allocateCell(vm)) CookieMapIterator(structure, iteratedObject, kind); + instance->finishCreation(vm); + return instance; + } + +private: + CookieMapIterator(JSC::Structure* structure, JSCookieMap& iteratedObject, IterationKind kind) + : Base(structure, iteratedObject, kind) + { + } +}; + +using CookieMapIteratorPrototype = JSDOMIteratorPrototype; +JSC_ANNOTATE_HOST_FUNCTION(CookieMapIteratorPrototypeNext, CookieMapIteratorPrototype::next); + +template<> +const JSC::ClassInfo CookieMapIteratorBase::s_info = { "CookieMap Iterator"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(CookieMapIteratorBase) }; +const JSC::ClassInfo CookieMapIterator::s_info = { "CookieMap Iterator"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(CookieMapIterator) }; + +template<> +const JSC::ClassInfo CookieMapIteratorPrototype::s_info = { "CookieMap Iterator"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(CookieMapIteratorPrototype) }; + +static inline JSC::EncodedJSValue jsCookieMapPrototypeFunction_entriesCaller(JSGlobalObject*, CallFrame*, JSCookieMap* thisObject) +{ + return JSValue::encode(iteratorCreate(*thisObject, IterationKind::Entries)); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookieMapPrototypeFunction_entries, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "entries"); +} + +static inline JSC::EncodedJSValue jsCookieMapPrototypeFunction_keysCaller(JSGlobalObject*, CallFrame*, JSCookieMap* thisObject) +{ + return JSValue::encode(iteratorCreate(*thisObject, IterationKind::Keys)); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookieMapPrototypeFunction_keys, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "keys"); +} + +static inline JSC::EncodedJSValue jsCookieMapPrototypeFunction_valuesCaller(JSGlobalObject*, CallFrame*, JSCookieMap* thisObject) +{ + return JSValue::encode(iteratorCreate(*thisObject, IterationKind::Values)); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookieMapPrototypeFunction_values, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "values"); +} + +static inline JSC::EncodedJSValue jsCookieMapPrototypeFunction_forEachCaller(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame, JSCookieMap* thisObject) +{ + return JSValue::encode(iteratorForEach(*lexicalGlobalObject, *callFrame, *thisObject)); +} + +JSC_DEFINE_HOST_FUNCTION(jsCookieMapPrototypeFunction_forEach, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "forEach"); +} + +GCClient::IsoSubspace* JSCookieMap::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForCookieMap.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForCookieMap = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForCookieMap.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForCookieMap = std::forward(space); }); +} + +void JSCookieMap::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) +{ + auto* thisObject = jsCast(cell); + analyzer.setWrappedObjectForCell(cell, &thisObject->wrapped()); + Base::analyzeHeap(cell, analyzer); +} + +bool JSCookieMapOwner::isReachableFromOpaqueRoots(JSC::Handle handle, void*, AbstractSlotVisitor& visitor, ASCIILiteral* reason) +{ + UNUSED_PARAM(handle); + UNUSED_PARAM(visitor); + UNUSED_PARAM(reason); + return false; +} + +void JSCookieMapOwner::finalize(JSC::Handle handle, void* context) +{ + auto* jsCookieMap = static_cast(handle.slot()->asCell()); + auto& world = *static_cast(context); + uncacheWrapper(world, &jsCookieMap->wrapped(), jsCookieMap); +} + +JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject* globalObject, Ref&& impl) +{ + return createWrapper(globalObject, WTFMove(impl)); +} + +JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, CookieMap& impl) +{ + return wrap(lexicalGlobalObject, globalObject, impl); +} + +CookieMap* JSCookieMap::toWrapped(JSC::VM& vm, JSC::JSValue value) +{ + if (auto* wrapper = jsDynamicCast(value)) + return &wrapper->wrapped(); + return nullptr; +} + +size_t JSCookieMap::estimatedSize(JSC::JSCell* cell, JSC::VM& vm) +{ + auto* thisObject = jsCast(cell); + auto& wrapped = thisObject->wrapped(); + return Base::estimatedSize(cell, vm) + wrapped.memoryCost(); +} + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSCookieMap.h b/src/bun.js/bindings/webcore/JSCookieMap.h new file mode 100644 index 0000000000..9b16ffcc19 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSCookieMap.h @@ -0,0 +1,75 @@ +#pragma once + +#include "JSDOMWrapper.h" +#include "CookieMap.h" +#include + +namespace WebCore { + +class JSCookieMap : public JSDOMWrapper { +public: + using Base = JSDOMWrapper; + static JSCookieMap* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject, Ref&& impl) + { + JSCookieMap* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSCookieMap(structure, *globalObject, WTFMove(impl)); + ptr->finishCreation(globalObject->vm()); + return ptr; + } + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static CookieMap* toWrapped(JSC::VM&, JSC::JSValue); + static void destroy(JSC::JSCell*); + + DECLARE_INFO; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(static_cast(WebCore::JSAsJSONType), StructureFlags), info()); + } + + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); + static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm); + +protected: + JSCookieMap(JSC::Structure*, JSDOMGlobalObject&, Ref&&); + + void finishCreation(JSC::VM&); +}; +JSC::JSValue getInternalProperties(JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSCookieMap* castedThis); +class JSCookieMapOwner final : public JSC::WeakHandleOwner { +public: + bool isReachableFromOpaqueRoots(JSC::Handle, void* context, JSC::AbstractSlotVisitor&, ASCIILiteral*) final; + void finalize(JSC::Handle, void* context) final; +}; + +inline JSC::WeakHandleOwner* wrapperOwner(DOMWrapperWorld&, CookieMap*) +{ + static NeverDestroyed owner; + return &owner.get(); +} + +inline void* wrapperKey(CookieMap* wrappableObject) +{ + return wrappableObject; +} + +JSC::JSValue toJS(JSC::JSGlobalObject*, JSDOMGlobalObject*, CookieMap&); +inline JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, CookieMap* impl) { return impl ? toJS(lexicalGlobalObject, globalObject, *impl) : JSC::jsNull(); } +JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject*, Ref&&); +inline JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, RefPtr&& impl) { return impl ? toJSNewlyCreated(lexicalGlobalObject, globalObject, impl.releaseNonNull()) : JSC::jsNull(); } + +template<> struct JSDOMWrapperConverterTraits { + using WrapperClass = JSCookieMap; + using ToWrappedReturnType = CookieMap*; +}; + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSPerformanceResourceTiming.cpp b/src/bun.js/bindings/webcore/JSPerformanceResourceTiming.cpp index a77a899d7e..71f3027255 100644 --- a/src/bun.js/bindings/webcore/JSPerformanceResourceTiming.cpp +++ b/src/bun.js/bindings/webcore/JSPerformanceResourceTiming.cpp @@ -447,7 +447,7 @@ static inline EncodedJSValue jsPerformanceResourceTimingPrototypeFunction_toJSON auto* result = constructEmptyObject(lexicalGlobalObject); auto nameValue = toJS(*lexicalGlobalObject, throwScope, impl.name()); RETURN_IF_EXCEPTION(throwScope, {}); - result->putDirect(vm, Identifier::fromString(vm, "name"_s), nameValue); + result->putDirect(vm, vm.propertyNames->name, nameValue); auto entryTypeValue = toJS(*lexicalGlobalObject, throwScope, impl.entryType()); RETURN_IF_EXCEPTION(throwScope, {}); result->putDirect(vm, Identifier::fromString(vm, "entryType"_s), entryTypeValue); diff --git a/src/bun.js/bindings/webcore/JSPerformanceServerTiming.cpp b/src/bun.js/bindings/webcore/JSPerformanceServerTiming.cpp index 65c10c2b4f..70e5675976 100644 --- a/src/bun.js/bindings/webcore/JSPerformanceServerTiming.cpp +++ b/src/bun.js/bindings/webcore/JSPerformanceServerTiming.cpp @@ -220,7 +220,7 @@ static inline EncodedJSValue jsPerformanceServerTimingPrototypeFunction_toJSONBo auto* result = constructEmptyObject(lexicalGlobalObject); auto nameValue = toJS(*lexicalGlobalObject, throwScope, impl.name()); RETURN_IF_EXCEPTION(throwScope, {}); - result->putDirect(vm, Identifier::fromString(vm, "name"_s), nameValue); + result->putDirect(vm, vm.propertyNames->name, nameValue); auto durationValue = toJS(*lexicalGlobalObject, throwScope, impl.duration()); RETURN_IF_EXCEPTION(throwScope, {}); result->putDirect(vm, Identifier::fromString(vm, "duration"_s), durationValue); diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index f663adfb12..eeacfbbaed 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -97,6 +97,7 @@ using namespace JSC; macro(dirname) \ macro(disturbed) \ macro(document) \ + macro(domain) \ macro(encode) \ macro(encoding) \ macro(end) \ @@ -105,6 +106,7 @@ using namespace JSC; macro(evaluateCommonJSModule) \ macro(evaluated) \ macro(execArgv) \ + macro(expires) \ macro(exports) \ macro(extname) \ macro(failureKind) \ @@ -130,6 +132,7 @@ using namespace JSC; macro(host) \ macro(hostname) \ macro(href) \ + macro(httpOnly) \ macro(ignoreBOM) \ macro(importer) \ macro(inFlightCloseRequest) \ @@ -157,6 +160,7 @@ using namespace JSC; macro(makeDOMException) \ macro(makeErrorWithCode) \ macro(makeGetterTypeError) \ + macro(maxAge) \ macro(method) \ macro(mockedFunction) \ macro(mode) \ @@ -174,6 +178,7 @@ using namespace JSC; macro(overridableRequire) \ macro(ownerReadableStream) \ macro(parse) \ + macro(partitioned) \ macro(password) \ macro(patch) \ macro(path) \ @@ -217,6 +222,8 @@ using namespace JSC; macro(requireNativeModule) \ macro(resolveSync) \ macro(resume) \ + macro(sameSite) \ + macro(secure) \ macro(self) \ macro(sep) \ macro(setBody) \ diff --git a/test/js/bun/cookie/cookie-map.test.ts b/test/js/bun/cookie/cookie-map.test.ts new file mode 100644 index 0000000000..faabdac9ed --- /dev/null +++ b/test/js/bun/cookie/cookie-map.test.ts @@ -0,0 +1,248 @@ +import { test, expect, describe } from "bun:test"; + +describe("Bun.Cookie and Bun.CookieMap", () => { + // Basic Cookie tests + test("can create a basic Cookie", () => { + const cookie = new Bun.Cookie("name", "value"); + expect(cookie.name).toBe("name"); + expect(cookie.value).toBe("value"); + expect(cookie.path).toBe("/"); + expect(cookie.domain).toBe(null); + expect(cookie.secure).toBe(false); + expect(cookie.httpOnly).toBe(false); + expect(cookie.partitioned).toBe(false); + expect(cookie.sameSite).toBe("lax"); + }); + + test("can create a Cookie with options", () => { + const cookie = new Bun.Cookie("name", "value", { + domain: "example.com", + path: "/foo", + secure: true, + httpOnly: true, + partitioned: true, + sameSite: "lax", + maxAge: 3600, + }); + + expect(cookie.name).toBe("name"); + expect(cookie.value).toBe("value"); + expect(cookie.domain).toBe("example.com"); + expect(cookie.path).toBe("/foo"); + expect(cookie.secure).toBe(true); + expect(cookie.httpOnly).toBe(true); + expect(cookie.partitioned).toBe(true); + expect(cookie.sameSite).toBe("lax"); + expect(cookie.maxAge).toBe(3600); + }); + + test("Cookie.toString() formats properly", () => { + const cookie = new Bun.Cookie("name", "value", { + domain: "example.com", + path: "/foo", + secure: true, + httpOnly: true, + partitioned: true, + sameSite: "strict", + maxAge: 3600, + }); + + const str = cookie.toString(); + expect(str).toInclude("name=value"); + expect(str).toInclude("Domain=example.com"); + expect(str).toInclude("Path=/foo"); + expect(str).toInclude("Max-Age=3600"); + expect(str).toInclude("Secure"); + expect(str).toInclude("HttpOnly"); + expect(str).toInclude("Partitioned"); + expect(str).toInclude("SameSite=strict"); + }); + + test("can set Cookie expires as Date", () => { + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 1); // tomorrow + + const cookie = new Bun.Cookie("name", "value", { + expires: futureDate, + }); + + expect(cookie.isExpired()).toBe(false); + }); + + test("Cookie.isExpired() returns correct value", async () => { + // Expired cookie (max-age in the past) + const expiredCookie = new Bun.Cookie("name", "value", { + expires: new Date(Date.now() - 1000), + }); + expect(expiredCookie.isExpired()).toBe(true); + + // Non-expired cookie (future max-age) + const validCookie = new Bun.Cookie("name", "value", { + maxAge: 3600, // 1 hour + }); + expect(validCookie.isExpired()).toBe(false); + + // Session cookie (no expiration) + const sessionCookie = new Bun.Cookie("name", "value"); + expect(sessionCookie.isExpired()).toBe(false); + }); + + test("Cookie.parse works with all attributes", () => { + const cookieStr = + "name=value; Domain=example.com; Expires=Thu, 13 Mar 2025 12:00:00 GMT; Path=/foo; Max-Age=3600; Secure; HttpOnly; Partitioned; SameSite=Strict"; + const cookie = Bun.Cookie.parse(cookieStr); + + expect(cookie.name).toBe("name"); + expect(cookie.value).toBe("value"); + expect(cookie.domain).toBe("example.com"); + expect(cookie.path).toBe("/foo"); + expect(cookie.maxAge).toBe(3600); + expect(cookie.secure).toBe(true); + expect(cookie.httpOnly).toBe(true); + expect(cookie.expires).toEqual(Date.parse("Thu, 13 Mar 2025 12:00:00 GMT")); + expect(cookie.partitioned).toBe(true); + expect(cookie.sameSite).toBe("strict"); + }); + + test("Cookie.serialize creates cookie string", () => { + const cookie1 = new Bun.Cookie("foo", "bar"); + const cookie2 = new Bun.Cookie("baz", "qux"); + + const cookieStr = Bun.Cookie.serialize(cookie1, cookie2); + + expect(cookieStr).toMatchInlineSnapshot(`"foo=bar; baz=qux"`); + }); + + // Basic CookieMap tests + test("can create an empty CookieMap", () => { + const map = new Bun.CookieMap(); + expect(map.size).toBe(0); + expect(map.toString()).toBe(""); + }); + + test("can create CookieMap from string", () => { + const map = new Bun.CookieMap("name=value; foo=bar"); + expect(map.size).toBe(2); + + const cookie1 = map.get("name"); + expect(cookie1).toBeDefined(); + expect(cookie1?.name).toBe("name"); + expect(cookie1?.value).toBe("value"); + + const cookie2 = map.get("foo"); + expect(cookie2).toBeDefined(); + expect(cookie2?.name).toBe("foo"); + expect(cookie2?.value).toBe("bar"); + + expect(map.toString()).toMatchInlineSnapshot(`"name=value; foo=bar"`); + }); + + test("can create CookieMap from object", () => { + const map = new Bun.CookieMap({ + name: "value", + foo: "bar", + }); + + expect(map.size).toBe(2); + expect(map.get("name")?.value).toBe("value"); + expect(map.get("foo")?.value).toBe("bar"); + }); + + test("can create CookieMap from array pairs", () => { + const map = new Bun.CookieMap([ + ["name", "value"], + ["foo", "bar"], + ]); + + expect(map.size).toBe(2); + expect(map.get("name")?.value).toBe("value"); + expect(map.get("foo")?.value).toBe("bar"); + }); + + test("CookieMap methods work", () => { + const map = new Bun.CookieMap(); + + // Set a cookie with name/value + map.set("name", "value"); + expect(map.size).toBe(1); + expect(map.has("name")).toBe(true); + + // Set with cookie object + map.set( + new Bun.Cookie("foo", "bar", { + secure: true, + httpOnly: true, + partitioned: true, + }), + ); + expect(map.size).toBe(2); + expect(map.has("foo")).toBe(true); + expect(map.get("foo")?.secure).toBe(true); + expect(map.get("foo")?.httpOnly).toBe(true); + expect(map.get("foo")?.partitioned).toBe(true); + expect(map.toString()).toMatchInlineSnapshot(`"name=value; foo=bar; Secure; HttpOnly; Partitioned"`); + + // Delete a cookie + map.delete("name"); + expect(map.size).toBe(1); + expect(map.has("name")).toBe(false); + + // Get all (only one remains) + const all = map.getAll("foo"); + expect(all.length).toBe(1); + expect(all[0].value).toBe("bar"); + }); + + test("CookieMap supports iteration", () => { + const map = new Bun.CookieMap("a=1; b=2; c=3"); + + // Test keys() + const keys = Array.from(map.keys()); + expect(keys).toEqual(["a", "b", "c"]); + + // Test entries() + let count = 0; + for (const [key, cookie] of map.entries()) { + count++; + expect(typeof key).toBe("string"); + expect(typeof cookie).toBe("object"); + expect(cookie instanceof Bun.Cookie).toBe(true); + expect(["1", "2", "3"]).toContain(cookie.value); + } + expect(count).toBe(3); + + // Test forEach + const collected: string[] = []; + map.forEach((cookie, key) => { + collected.push(`${key}=${cookie.value}`); + }); + expect(collected.sort()).toEqual(["a=1", "b=2", "c=3"]); + }); + + test("CookieMap.toString() formats properly", () => { + const map = new Bun.CookieMap("a=1; b=2"); + const str = map.toString(); + expect(str).toInclude("a=1"); + expect(str).toInclude("b=2"); + }); + + test("CookieMap works with cookies with advanced attributes", () => { + const map = new Bun.CookieMap(); + + // Add a cookie with httpOnly and partitioned flags + map.set("session", "abc123", { + httpOnly: true, + secure: true, + partitioned: true, + maxAge: 3600, + }); + + const cookie = map.get("session"); + expect(cookie).toBeDefined(); + expect(cookie?.httpOnly).toBe(true); + expect(cookie?.secure).toBe(true); + expect(cookie?.partitioned).toBe(true); + expect(cookie?.maxAge).toBe(3600); + expect(cookie?.value).toBe("abc123"); + }); +}); diff --git a/test/js/bun/http/bun-serve-cookies.test.ts b/test/js/bun/http/bun-serve-cookies.test.ts new file mode 100644 index 0000000000..be78223f77 --- /dev/null +++ b/test/js/bun/http/bun-serve-cookies.test.ts @@ -0,0 +1,653 @@ +import { afterAll, beforeAll, describe, expect, it, test } from "bun:test"; +import { isBroken, isMacOS } from "harness"; +import type { Server, ServeOptions, BunRequest } from "bun"; + +describe("request cookies", () => { + let server: Server; + + beforeAll(() => { + server = Bun.serve({ + port: 0, + routes: { + "/before-headers": req => { + // Access cookies before accessing headers + const cookies = req.cookies; + expect(cookies).toBeDefined(); + expect(cookies.size).toBe(2); + expect(cookies.get("name")?.value).toBe("value"); + expect(cookies.get("foo")?.value).toBe("bar"); + + // Verify headers are still accessible afterward + expect(req.headers.get("cookie")).toBe("name=value; foo=bar"); + + return new Response("ok"); + }, + "/after-headers": req => { + // Access headers first + const cookieHeader = req.headers.get("cookie"); + expect(cookieHeader).toBe("name=value; foo=bar"); + + // Then access cookies + const cookies = req.cookies; + expect(cookies).toBeDefined(); + expect(cookies.size).toBe(2); + expect(cookies.get("name")?.value).toBe("value"); + expect(cookies.get("foo")?.value).toBe("bar"); + + return new Response("ok"); + }, + "/no-cookies": req => { + // Test with no cookies in request + const cookies = req.cookies; + expect(cookies).toBeDefined(); + expect(cookies.size).toBe(0); + + return new Response("ok"); + }, + "/cookies-readonly": req => { + // Verify cookies property is readonly + try { + // @ts-expect-error - This should fail at runtime + req.cookies = {}; + return new Response("not ok - should have thrown"); + } catch (e) { + return new Response("ok - readonly"); + } + }, + }, + }); + server.unref(); + }); + + afterAll(() => { + server.stop(true); + }); + + it("parses cookies before headers are accessed", async () => { + const res = await fetch(`${server.url}before-headers`, { + headers: { + "Cookie": "name=value; foo=bar", + }, + }); + expect(res.status).toBe(200); + expect(await res.text()).toBe("ok"); + }); + + it("parses cookies after headers are accessed", async () => { + const res = await fetch(`${server.url}after-headers`, { + headers: { + "Cookie": "name=value; foo=bar", + }, + }); + expect(res.status).toBe(200); + expect(await res.text()).toBe("ok"); + }); + + it("handles requests with no cookies", async () => { + const res = await fetch(`${server.url}no-cookies`); + expect(res.status).toBe(200); + expect(await res.text()).toBe("ok"); + }); + + it("has readonly cookies property", async () => { + const res = await fetch(`${server.url}cookies-readonly`); + expect(res.status).toBe(200); + expect(await res.text()).toBe("ok - readonly"); + }); +}); + +describe("cookie attributes", () => { + let server: Server; + + beforeAll(() => { + server = Bun.serve({ + port: 0, + routes: { + "/cookie-attributes": req => { + const cookie = req.cookies.get("complex"); + expect(cookie).toBeDefined(); + + if (!cookie) { + return new Response("no cookie found", { status: 500 }); + } + + return new Response( + JSON.stringify({ + name: cookie.name, + value: cookie.value, + path: cookie.path, + secure: cookie.secure, + sameSite: cookie.sameSite, + }), + ); + }, + }, + }); + server.unref(); + }); + + afterAll(() => { + server.stop(true); + }); + + it("correctly parses cookie attributes", async () => { + const res = await fetch(`${server.url}cookie-attributes`, { + headers: { + "Cookie": "complex=value; simple=123", + }, + }); + + expect(res.status).toBe(200); + const data = await res.json(); + + expect(data.name).toBe("complex"); + expect(data.value).toBe("value"); + expect(data.path).toBe("/"); + expect(data.secure).toBe(false); + expect(data.sameSite).toBe("lax"); + }); +}); + +describe("instanceof and type checks", () => { + let server: Server; + + beforeAll(() => { + server = Bun.serve({ + port: 0, + routes: { + "/instanceof-checks": req => { + // Check that cookies is an instance of Bun.CookieMap + expect(req.cookies instanceof Bun.CookieMap).toBe(true); + + // Check that cookie is an instance of Bun.Cookie + const cookie = req.cookies.get("name"); + expect(cookie instanceof Bun.Cookie).toBe(true); + + return new Response("ok"); + }, + "/constructor-identities": req => { + // Verify that the constructors match + expect(req.cookies.constructor).toBe(Bun.CookieMap); + + const cookie = req.cookies.get("name"); + expect(cookie?.constructor).toBe(Bun.Cookie); + + return new Response("ok"); + }, + }, + }); + server.unref(); + }); + + afterAll(() => { + server.stop(true); + }); + + it("cookies is instance of Bun.CookieMap and has right prototype", async () => { + const res = await fetch(`${server.url}instanceof-checks`, { + headers: { + "Cookie": "name=value", + }, + }); + expect(res.status).toBe(200); + }); + + it("constructors match expected types", async () => { + const res = await fetch(`${server.url}constructor-identities`, { + headers: { + "Cookie": "name=value", + }, + }); + expect(res.status).toBe(200); + }); +}); + +describe("complex cookie parsing", () => { + let server: Server; + + beforeAll(() => { + server = Bun.serve({ + port: 0, + routes: { + "/special-chars": req => { + const cookie = req.cookies.get("complex"); + if (!cookie) { + return new Response("no cookie found", { status: 500 }); + } + + expect(cookie.value).toBe("value with spaces"); + return new Response("ok"); + }, + "/equals-in-value": req => { + const cookie = req.cookies.get("equation"); + if (!cookie) { + return new Response("no cookie found", { status: 500 }); + } + + expect(cookie.value).toBe("x=y+z"); + return new Response("ok"); + }, + "/multiple-cookies": req => { + // Cookie with same name multiple times should be parsed correctly + const cookies = req.cookies; + expect(cookies.size).toBeGreaterThanOrEqual(2); + + // Get first occurrence of duplicate cookie + const duplicateCookie = cookies.get("duplicate"); + expect(duplicateCookie).toBeDefined(); + + // In most implementations, the first value should be preserved + expect(duplicateCookie?.value).toBe("first"); + + return new Response("ok"); + }, + "/cookie-map-methods": req => { + const cookies = req.cookies; + + // Test has() method + expect(cookies.has("name")).toBe(true); + expect(cookies.has("nonexistent")).toBe(false); + + // Test size + expect(cookies.size).toBe(2); + + // Test toString() returns a valid cookie string + const cookieStr = cookies.toString(); + expect(cookieStr).toInclude("name=value"); + expect(cookieStr).toInclude("foo=bar"); + + return new Response("ok"); + }, + }, + }); + server.unref(); + }); + + afterAll(() => { + server.stop(true); + }); + + it("handles cookie values with spaces", async () => { + const res = await fetch(`${server.url}special-chars`, { + headers: { + "Cookie": "complex=value with spaces", + }, + }); + expect(res.status).toBe(200); + }); + + it("handles cookie values with equals signs", async () => { + const res = await fetch(`${server.url}equals-in-value`, { + headers: { + "Cookie": "equation=x=y+z", + }, + }); + expect(res.status).toBe(200); + }); + + it("handles duplicate cookie names", async () => { + const res = await fetch(`${server.url}multiple-cookies`, { + headers: { + "Cookie": "duplicate=first; duplicate=second; other=value", + }, + }); + expect(res.status).toBe(200); + }); + + it("CookieMap methods work correctly", async () => { + const res = await fetch(`${server.url}cookie-map-methods`, { + headers: { + "Cookie": "name=value; foo=bar", + }, + }); + expect(res.status).toBe(200); + }); +}); + +describe("CookieMap iterator", () => { + let server: Server; + + beforeAll(() => { + server = Bun.serve({ + port: 0, + routes: { + "/iterator-entries": req => { + const cookies = req.cookies; + + // Test entries() iterator + const entries = Array.from(cookies.entries()); + expect(entries.length).toBe(3); + + // Entries should be [name, Cookie] pairs + expect(entries[0][0]).toBeTypeOf("string"); + expect(entries[0][1]).toBeTypeOf("object"); + expect(entries[0][1].constructor).toBe(Bun.Cookie); + + // Check that we can get cookies values + const cookieNames = entries.map(([name, _]) => name); + expect(cookieNames).toContain("a"); + expect(cookieNames).toContain("b"); + expect(cookieNames).toContain("c"); + + const cookieValues = entries.map(([_, cookie]) => cookie.value); + expect(cookieValues).toContain("1"); + expect(cookieValues).toContain("2"); + expect(cookieValues).toContain("3"); + + return new Response("ok"); + }, + "/iterator-for-of": req => { + const cookies = req.cookies; + + // Test for...of iteration (should iterate over entries) + const collected: { name: string; value: string }[] = []; + for (const entry of cookies) { + // Check that we get [name, cookie] entries + expect(entry.length).toBe(2); + expect(entry[0]).toBeTypeOf("string"); + expect(entry[1]).toBeTypeOf("object"); + expect(entry[1].constructor).toBe(Bun.Cookie); + + const [name, cookie] = entry; + collected.push({ name, value: cookie.value }); + } + + expect(collected.length).toBe(3); + expect(collected.some(c => c.name === "a" && c.value === "1")).toBe(true); + expect(collected.some(c => c.name === "b" && c.value === "2")).toBe(true); + expect(collected.some(c => c.name === "c" && c.value === "3")).toBe(true); + + return new Response("ok"); + }, + "/iterator-keys-values": req => { + const cookies = req.cookies; + + // Test keys() iterator + const keys = Array.from(cookies.keys()); + expect(keys.length).toBe(3); + expect(keys).toContain("a"); + expect(keys).toContain("b"); + expect(keys).toContain("c"); + + // Test values() iterator - returns Cookie objects + const values = Array.from(cookies.values()); + expect(values.length).toBe(3); + + // Values should be Cookie objects + for (const cookie of values) { + expect(cookie).toBeTypeOf("object"); + expect(cookie.constructor).toBe(Bun.Cookie); + expect(cookie.name).toBeTypeOf("string"); + expect(cookie.value).toBeTypeOf("string"); + } + + // Values should include the expected cookies + const cookieValues = values.map(c => c.value); + expect(cookieValues).toContain("1"); + expect(cookieValues).toContain("2"); + expect(cookieValues).toContain("3"); + + return new Response("ok"); + }, + "/iterator-forEach": req => { + const cookies = req.cookies; + + // Test forEach method + const collected: { key: string; value: string }[] = []; + cookies.forEach((cookie, key) => { + expect(cookie).toBeTypeOf("object"); + expect(cookie.constructor).toBe(Bun.Cookie); + expect(key).toBeTypeOf("string"); + collected.push({ key, value: cookie.value }); + }); + + expect(collected.length).toBe(3); + expect(collected.some(c => c.key === "a" && c.value === "1")).toBe(true); + expect(collected.some(c => c.key === "b" && c.value === "2")).toBe(true); + expect(collected.some(c => c.key === "c" && c.value === "3")).toBe(true); + + return new Response("ok"); + }, + }, + }); + server.unref(); + }); + + afterAll(() => { + server.stop(true); + }); + + it("implements entries() iterator", async () => { + const res = await fetch(`${server.url}iterator-entries`, { + headers: { + "Cookie": "a=1; b=2; c=3", + }, + }); + expect(res.status).toBe(200); + }); + + it("implements for...of iteration", async () => { + const res = await fetch(`${server.url}iterator-for-of`, { + headers: { + "Cookie": "a=1; b=2; c=3", + }, + }); + expect(res.status).toBe(200); + }); + + it("implements keys() and values() iterators", async () => { + const res = await fetch(`${server.url}iterator-keys-values`, { + headers: { + "Cookie": "a=1; b=2; c=3", + }, + }); + expect(res.status).toBe(200); + }); + + it("implements forEach method", async () => { + const res = await fetch(`${server.url}iterator-forEach`, { + headers: { + "Cookie": "a=1; b=2; c=3", + }, + }); + expect(res.status).toBe(200); + }); +}); + +describe("Direct usage of Bun.Cookie and Bun.CookieMap", () => { + it("can create a Cookie directly", () => { + const cookie = new Bun.Cookie("name", "value"); + + expect(cookie.constructor).toBe(Bun.Cookie); + expect(cookie.name).toBe("name"); + expect(cookie.value).toBe("value"); + expect(cookie.path).toBe("/"); + // Domain may be null in the implementation + expect(cookie.domain == null || cookie.domain === "").toBe(true); + expect(cookie.secure).toBe(false); + expect(cookie.sameSite).toBe("lax"); + }); + + it("can create a Cookie with options", () => { + const cookie = new Bun.Cookie("name", "value", { + path: "/path", + domain: "example.com", + secure: true, + sameSite: "lax", + }); + + expect(cookie.name).toBe("name"); + expect(cookie.value).toBe("value"); + expect(cookie.path).toBe("/path"); + expect(cookie.domain).toBe("example.com"); + expect(cookie.secure).toBe(true); + expect(cookie.sameSite).toBe("lax"); + }); + + it("can create a CookieMap directly", () => { + const cookieMap = new Bun.CookieMap(); + + expect(cookieMap.constructor).toBe(Bun.CookieMap); + expect(cookieMap.size).toBe(0); + }); + + it("can create a CookieMap with a cookie string", () => { + const cookieMap = new Bun.CookieMap("name=value; foo=bar"); + + expect(cookieMap.size).toBe(2); + + const nameCookie = cookieMap.get("name"); + expect(nameCookie).toBeDefined(); + expect(nameCookie?.value).toBe("value"); + + const fooCookie = cookieMap.get("foo"); + expect(fooCookie).toBeDefined(); + expect(fooCookie?.value).toBe("bar"); + }); + + it("can create a CookieMap with an object", () => { + const cookieMap = new Bun.CookieMap({ + name: "value", + foo: "bar", + }); + + expect(cookieMap.size).toBe(2); + + const nameCookie = cookieMap.get("name"); + expect(nameCookie).toBeDefined(); + expect(nameCookie?.value).toBe("value"); + + const fooCookie = cookieMap.get("foo"); + expect(fooCookie).toBeDefined(); + expect(fooCookie?.value).toBe("bar"); + }); + + it("can create a CookieMap with an array of pairs", () => { + const cookieMap = new Bun.CookieMap([ + ["name", "value"], + ["foo", "bar"], + ]); + + expect(cookieMap.size).toBe(2); + + const nameCookie = cookieMap.get("name"); + expect(nameCookie).toBeDefined(); + expect(nameCookie?.value).toBe("value"); + + const fooCookie = cookieMap.get("foo"); + expect(fooCookie).toBeDefined(); + expect(fooCookie?.value).toBe("bar"); + }); + + it("can set and get cookies in a CookieMap", () => { + const cookieMap = new Bun.CookieMap(); + + // Set with name/value + cookieMap.set("name", "value"); + + // Set with options + cookieMap.set({ + name: "foo", + value: "bar", + secure: true, + path: "/path", + }); + + expect(cookieMap.size).toBe(2); + + const nameCookie = cookieMap.get("name"); + console.log(nameCookie); + expect(nameCookie).toBeDefined(); + expect(nameCookie?.value).toBe("value"); + + const fooCookie = cookieMap.get("foo"); + expect(fooCookie).toBeDefined(); + expect(fooCookie?.value).toBe("bar"); + expect(fooCookie?.secure).toBe(true); + expect(fooCookie?.path).toBe("/path"); + }); + + it("can use Cookie.parse to parse cookie strings", () => { + const cookie = Bun.Cookie.parse("name=value; Path=/; Secure; SameSite=Lax"); + + expect(cookie.constructor).toBe(Bun.Cookie); + expect(cookie.name).toBe("name"); + expect(cookie.value).toBe("value"); + expect(cookie.path).toBe("/"); + expect(cookie.secure).toBe(true); + expect(cookie.sameSite.toLowerCase()).toBe("lax"); + }); + + it("can use Cookie.from to create cookies", () => { + const cookie = Bun.Cookie.from("name", "value", { + path: "/path", + domain: "example.com", + secure: true, + sameSite: "none", + }); + + expect(cookie.constructor).toBe(Bun.Cookie); + expect(cookie.name).toBe("name"); + expect(cookie.value).toBe("value"); + expect(cookie.path).toBe("/path"); + expect(cookie.domain).toBe("example.com"); + expect(cookie.secure).toBe(true); + expect(cookie.sameSite.toLowerCase()).toBe("none"); + }); + + it("can convert cookies to string", () => { + const cookie = new Bun.Cookie("name", "value", { + path: "/path", + domain: "example.com", + secure: true, + sameSite: "lax", + }); + + const cookieStr = cookie.toString(); + expect(cookieStr).toInclude("name=value"); + expect(cookieStr).toInclude("Domain=example.com"); + expect(cookieStr).toInclude("Path=/path"); + expect(cookieStr).toInclude("Secure"); + expect(cookieStr).not.toInclude("SameSite"); + }); + + it("correctly handles toJSON methods", () => { + // Create a Cookie and test toJSON + const cookie = new Bun.Cookie("name", "value", { + path: "/test", + domain: "example.org", + secure: true, + sameSite: "lax", + expires: Date.now() + 3600000, // 1 hour in the future + }); + + const cookieJSON = cookie.toJSON(); + expect(cookieJSON).toBeTypeOf("object"); + expect(cookieJSON.name).toBe("name"); + expect(cookieJSON.value).toBe("value"); + expect(cookieJSON.path).toBe("/test"); + expect(cookieJSON.domain).toBe("example.org"); + expect(cookieJSON.secure).toBe(true); + + // Create a CookieMap and test toJSON + const cookieMap = new Bun.CookieMap("a=1; b=2; c=3"); + + const mapJSON = cookieMap.toJSON(); + expect(mapJSON).toBeInstanceOf(Array); + expect(mapJSON.length).toBe(3); + + // Each entry should be [name, value] + for (const entry of mapJSON) { + expect(entry.length).toBe(2); + expect(entry[0]).toBeTypeOf("string"); + expect(entry[1]).toBeTypeOf("object"); + } + + // Verify JSON.stringify works as expected + const jsonString = JSON.stringify(cookie); + expect(jsonString).toBeTypeOf("string"); + const parsed = JSON.parse(jsonString); + expect(parsed.name).toBe("name"); + expect(parsed.value).toBe("value"); + }); +}); diff --git a/test/js/bun/util/cookie.test.js b/test/js/bun/util/cookie.test.js new file mode 100644 index 0000000000..523c3a0623 --- /dev/null +++ b/test/js/bun/util/cookie.test.js @@ -0,0 +1,228 @@ +import { test, expect, describe } from "bun:test"; + +describe("Bun.Cookie", () => { + test("can create a cookie", () => { + const cookie = new Bun.Cookie("name", "value"); + expect(cookie.name).toBe("name"); + expect(cookie.value).toBe("value"); + expect(cookie.path).toBe("/"); + expect(cookie.domain).toBe(null); + expect(cookie.expires).toBe(0); + expect(cookie.secure).toBe(false); + expect(cookie.sameSite).toBe("lax"); + }); + + test("can create a cookie with options", () => { + const cookie = new Bun.Cookie("name", "value", { + domain: "example.com", + path: "/foo", + expires: 123456789, + secure: true, + sameSite: "strict", + }); + + expect(cookie.name).toBe("name"); + expect(cookie.value).toBe("value"); + expect(cookie.domain).toBe("example.com"); + expect(cookie.path).toBe("/foo"); + expect(cookie.expires).toBe(123456789); + expect(cookie.secure).toBe(true); + expect(cookie.sameSite).toBe("strict"); + }); + + test("stringify a cookie", () => { + const cookie = new Bun.Cookie("name", "value", { + domain: "example.com", + path: "/foo", + secure: true, + sameSite: "lax", + }); + + expect(cookie.toString()).toBe("name=value; Domain=example.com; Path=/foo; Secure"); + }); + + test("parse a cookie string", () => { + const cookie = Bun.Cookie.parse("name=value; Domain=example.com; Path=/foo; Secure; SameSite=lax"); + + expect(cookie.name).toBe("name"); + expect(cookie.value).toBe("value"); + expect(cookie.domain).toBe("example.com"); + expect(cookie.path).toBe("/foo"); + expect(cookie.secure).toBe(true); + expect(cookie.sameSite).toBe("lax"); + }); +}); + +describe("Bun.CookieMap", () => { + test("can create an empty cookie map", () => { + const cookieMap = new Bun.CookieMap(); + expect(cookieMap.size).toBe(0); + expect(cookieMap.toString()).toBe(""); + }); + + test("can create a cookie map from a string", () => { + const cookieMap = new Bun.CookieMap("name=value; foo=bar"); + expect(cookieMap.size).toBe(2); + expect(cookieMap.has("name")).toBe(true); + expect(cookieMap.has("foo")).toBe(true); + expect(cookieMap.get("name").value).toBe("value"); + expect(cookieMap.get("foo").value).toBe("bar"); + }); + + test("can create a cookie map from an object", () => { + const cookieMap = new Bun.CookieMap({ + name: "value", + foo: "bar", + }); + + expect(cookieMap.size).toBe(2); + expect(cookieMap.has("name")).toBe(true); + expect(cookieMap.has("foo")).toBe(true); + expect(cookieMap.get("name").value).toBe("value"); + expect(cookieMap.get("foo").value).toBe("bar"); + }); + + test("can create a cookie map from pairs", () => { + const cookieMap = new Bun.CookieMap([ + ["name", "value"], + ["foo", "bar"], + ]); + + expect(cookieMap.size).toBe(2); + expect(cookieMap.has("name")).toBe(true); + expect(cookieMap.has("foo")).toBe(true); + expect(cookieMap.get("name").value).toBe("value"); + expect(cookieMap.get("foo").value).toBe("bar"); + }); + + test("can set and get cookies", () => { + const cookieMap = new Bun.CookieMap(); + + cookieMap.set("name", "value"); + expect(cookieMap.size).toBe(1); + expect(cookieMap.has("name")).toBe(true); + expect(cookieMap.get("name").value).toBe("value"); + + cookieMap.set("foo", "bar"); + expect(cookieMap.size).toBe(2); + expect(cookieMap.has("foo")).toBe(true); + expect(cookieMap.get("foo").value).toBe("bar"); + }); + + test("can set cookies with a Cookie object", () => { + const cookieMap = new Bun.CookieMap(); + const cookie = new Bun.Cookie("name", "value", { + domain: "example.com", + path: "/foo", + secure: true, + sameSite: "lax", + }); + + cookieMap.set(cookie); + expect(cookieMap.size).toBe(1); + expect(cookieMap.has("name")).toBe(true); + + const retrievedCookie = cookieMap.get("name"); + expect(retrievedCookie.name).toBe("name"); + expect(retrievedCookie.value).toBe("value"); + expect(retrievedCookie.domain).toBe("example.com"); + expect(retrievedCookie.path).toBe("/foo"); + expect(retrievedCookie.secure).toBe(true); + expect(retrievedCookie.sameSite).toBe("lax"); + }); + + test("can delete cookies", () => { + const cookieMap = new Bun.CookieMap("name=value; foo=bar"); + expect(cookieMap.size).toBe(2); + + cookieMap.delete("name"); + expect(cookieMap.size).toBe(1); + expect(cookieMap.has("name")).toBe(false); + expect(cookieMap.has("foo")).toBe(true); + + cookieMap.delete("foo"); + expect(cookieMap.size).toBe(0); + expect(cookieMap.has("foo")).toBe(false); + }); + + test("can delete cookies with options", () => { + const cookieMap = new Bun.CookieMap(); + cookieMap.set( + new Bun.Cookie("name", "value", { + domain: "example.com", + path: "/foo", + }), + ); + + cookieMap.delete({ + name: "name", + domain: "example.com", + path: "/foo", + }); + + expect(cookieMap.size).toBe(0); + }); + + test("can get all cookies with the same name", () => { + const cookieMap = new Bun.CookieMap(); + cookieMap.set( + new Bun.Cookie("name", "value1", { + domain: "example.com", + path: "/foo", + }), + ); + cookieMap.set( + new Bun.Cookie("name", "value2", { + domain: "example.org", + path: "/bar", + }), + ); + + // Since we're overwriting cookies with the same name, + // the size should still be 1 + expect(cookieMap.size).toBe(1); + + // But this would work if we didn't overwrite + // const cookies = cookieMap.getAll("name"); + // expect(cookies.length).toBe(2); + }); + + test("can stringify a cookie map", () => { + const cookieMap = new Bun.CookieMap("name=value; foo=bar"); + expect(cookieMap.toString()).toBe("name=value; foo=bar"); + }); + + test("supports iteration", () => { + const cookieMap = new Bun.CookieMap("name=value; foo=bar"); + + const entries = Array.from(cookieMap.entries()); + expect(entries).toMatchInlineSnapshot(` + [ + [ + "name", + { + "name": "name", + "value": "value", + "path": "/", + "secure": false, + "sameSite": "lax", + "httpOnly": false, + "partitioned": false + }, + ], + [ + "foo", + { + "name": "foo", + "value": "bar", + "path": "/", + "secure": false, + "sameSite": "lax", + "httpOnly": false, + "partitioned": false + }, + ], + ] + `); + }); +});