diff --git a/bench/snippets/urlpattern.js b/bench/snippets/urlpattern.js new file mode 100644 index 0000000000..b5e4908c89 --- /dev/null +++ b/bench/snippets/urlpattern.js @@ -0,0 +1,48 @@ +import { bench, group, run } from "../runner.mjs"; + +const patterns = [ + { name: "string pattern", input: "https://(sub.)?example(.com/)foo" }, + { name: "hostname IDN", input: { hostname: "xn--caf-dma.com" } }, + { + name: "pathname + search + hash + baseURL", + input: { + pathname: "/foo", + search: "bar", + hash: "baz", + baseURL: "https://example.com:8080", + }, + }, + { name: "pathname with regex", input: { pathname: "/([[a-z]--a])" } }, + { name: "named groups", input: { pathname: "/users/:id/posts/:postId" } }, + { name: "wildcard", input: { pathname: "/files/*" } }, +]; + +const testURL = "https://sub.example.com/foo"; + +group("URLPattern parse (constructor)", () => { + for (const { name, input } of patterns) { + bench(name, () => { + return new URLPattern(input); + }); + } +}); + +group("URLPattern.test()", () => { + for (const { name, input } of patterns) { + const pattern = new URLPattern(input); + bench(name, () => { + return pattern.test(testURL); + }); + } +}); + +group("URLPattern.exec()", () => { + for (const { name, input } of patterns) { + const pattern = new URLPattern(input); + bench(name, () => { + return pattern.exec(testURL); + }); + } +}); + +await run(); diff --git a/src/bun.js/bindings/URLDecomposition.cpp b/src/bun.js/bindings/URLDecomposition.cpp index 8655358b58..d1e5bb1185 100644 --- a/src/bun.js/bindings/URLDecomposition.cpp +++ b/src/bun.js/bindings/URLDecomposition.cpp @@ -166,7 +166,7 @@ String URLDecomposition::port() const } // Outer optional is whether we could parse at all. Inner optional is "no port specified". -static std::optional> parsePort(StringView string, StringView protocol) +std::optional> URLDecomposition::parsePort(StringView string, StringView protocol) { // https://url.spec.whatwg.org/#port-state with state override given. uint32_t port { 0 }; diff --git a/src/bun.js/bindings/URLDecomposition.h b/src/bun.js/bindings/URLDecomposition.h index bf9473dcb2..47f88b5f41 100644 --- a/src/bun.js/bindings/URLDecomposition.h +++ b/src/bun.js/bindings/URLDecomposition.h @@ -35,6 +35,10 @@ namespace WebCore { class URLDecomposition { public: + // Parse a port string with optional protocol for default port detection + // Returns nullopt on parse error, or optional (nullopt means empty/default port) + static std::optional> parsePort(StringView port, StringView protocol); + String origin() const; WEBCORE_EXPORT String protocol() const; diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index a47cb93a25..a1e862064b 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -130,6 +130,7 @@ #include "JSTextDecoderStream.h" #include "JSTransformStream.h" #include "JSTransformStreamDefaultController.h" +#include "JSURLPattern.h" #include "JSURLSearchParams.h" #include "JSWasmStreamingCompiler.h" #include "JSWebSocket.h" @@ -1009,6 +1010,7 @@ WEBCORE_GENERATED_CONSTRUCTOR_GETTER(TextEncoderStream); WEBCORE_GENERATED_CONSTRUCTOR_GETTER(TextDecoderStream); WEBCORE_GENERATED_CONSTRUCTOR_GETTER(TransformStream) WEBCORE_GENERATED_CONSTRUCTOR_GETTER(TransformStreamDefaultController) +WEBCORE_GENERATED_CONSTRUCTOR_GETTER(URLPattern); WEBCORE_GENERATED_CONSTRUCTOR_GETTER(URLSearchParams); WEBCORE_GENERATED_CONSTRUCTOR_GETTER(WebSocket); WEBCORE_GENERATED_CONSTRUCTOR_GETTER(Worker); diff --git a/src/bun.js/bindings/ZigGlobalObject.lut.txt b/src/bun.js/bindings/ZigGlobalObject.lut.txt index 9fbf2e00f0..9ccc124f25 100644 --- a/src/bun.js/bindings/ZigGlobalObject.lut.txt +++ b/src/bun.js/bindings/ZigGlobalObject.lut.txt @@ -84,6 +84,7 @@ TransformStream TransformStreamConstructorCallback PropertyCallback TransformStreamDefaultController TransformStreamDefaultControllerConstructorCallback PropertyCallback URL DOMURLConstructorCallback DontEnum|PropertyCallback + URLPattern URLPatternConstructorCallback PropertyCallback URLSearchParams URLSearchParamsConstructorCallback DontEnum|PropertyCallback WebSocket WebSocketConstructorCallback PropertyCallback Worker WorkerConstructorCallback PropertyCallback diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index b19ae4faa9..316f0848a7 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -81,6 +81,7 @@ public: std::unique_ptr m_clientSubspaceForDOMFormData; std::unique_ptr m_clientSubspaceForDOMFormDataIterator; std::unique_ptr m_clientSubspaceForDOMURL; + std::unique_ptr m_clientSubspaceForURLPattern; std::unique_ptr m_clientSubspaceForURLSearchParams; std::unique_ptr m_clientSubspaceForURLSearchParamsIterator; diff --git a/src/bun.js/bindings/webcore/DOMConstructors.h b/src/bun.js/bindings/webcore/DOMConstructors.h index 78b38e131a..fa626592be 100644 --- a/src/bun.js/bindings/webcore/DOMConstructors.h +++ b/src/bun.js/bindings/webcore/DOMConstructors.h @@ -860,11 +860,12 @@ enum class DOMConstructorID : uint16_t { Cookie, CookieMap, EventEmitter, + URLPattern, }; static constexpr unsigned numberOfDOMConstructorsBase = 848; -static constexpr unsigned bunExtraConstructors = 3; +static constexpr unsigned bunExtraConstructors = 4; static constexpr unsigned numberOfDOMConstructors = numberOfDOMConstructorsBase + bunExtraConstructors; diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index 049b161b8f..b44973cb53 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -938,6 +938,7 @@ public: // std::unique_ptr m_subspaceForDOMFormData; // std::unique_ptr m_subspaceForDOMFormDataIterator; std::unique_ptr m_subspaceForDOMURL; + std::unique_ptr m_subspaceForURLPattern; std::unique_ptr m_subspaceForJSSign; std::unique_ptr m_subspaceForJSVerify; std::unique_ptr m_subspaceForJSHmac; diff --git a/src/bun.js/bindings/webcore/JSURLPattern.cpp b/src/bun.js/bindings/webcore/JSURLPattern.cpp new file mode 100644 index 0000000000..b6b531ca04 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSURLPattern.cpp @@ -0,0 +1,545 @@ +/* + This file is part of the WebKit open source project. + This file has been generated by generate-bindings.pl. DO NOT MODIFY! + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "config.h" +#include "JSURLPattern.h" + +#include "ActiveDOMObject.h" +#include "ExtendedDOMClientIsoSubspaces.h" +#include "ExtendedDOMIsoSubspaces.h" +#include "JSDOMAttribute.h" +#include "JSDOMBinding.h" +#include "JSDOMConstructor.h" +#include "JSDOMConvertBoolean.h" +#include "JSDOMConvertDictionary.h" +#include "JSDOMConvertInterface.h" +#include "JSDOMConvertNullable.h" +#include "JSDOMConvertOptional.h" +#include "JSDOMConvertStrings.h" +#include "JSDOMConvertUnion.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMOperation.h" +#include "JSDOMWrapperCache.h" +#include "JSURLPatternInit.h" +#include "JSURLPatternOptions.h" +#include "JSURLPatternResult.h" +#include "ScriptExecutionContext.h" +#include "WebCoreJSClientData.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { +using namespace JSC; + +// Helper to convert from IDL's std::variant to WTF's Variant +static URLPattern::URLPatternInput convertToWTFVariant(std::variant&& input) +{ + if (std::holds_alternative(input)) + return URLPattern::URLPatternInput(std::get(std::move(input))); + return URLPattern::URLPatternInput(std::get(std::move(input))); +} + +static std::optional convertToOptionalWTFVariant(std::optional>&& input) +{ + if (!input) + return std::nullopt; + return convertToWTFVariant(std::move(*input)); +} + +// Functions + +static JSC_DECLARE_HOST_FUNCTION(jsURLPatternPrototypeFunction_test); +static JSC_DECLARE_HOST_FUNCTION(jsURLPatternPrototypeFunction_exec); + +// Attributes + +static JSC_DECLARE_CUSTOM_GETTER(jsURLPatternConstructor); +static JSC_DECLARE_CUSTOM_GETTER(jsURLPattern_protocol); +static JSC_DECLARE_CUSTOM_GETTER(jsURLPattern_username); +static JSC_DECLARE_CUSTOM_GETTER(jsURLPattern_password); +static JSC_DECLARE_CUSTOM_GETTER(jsURLPattern_hostname); +static JSC_DECLARE_CUSTOM_GETTER(jsURLPattern_port); +static JSC_DECLARE_CUSTOM_GETTER(jsURLPattern_pathname); +static JSC_DECLARE_CUSTOM_GETTER(jsURLPattern_search); +static JSC_DECLARE_CUSTOM_GETTER(jsURLPattern_hash); +static JSC_DECLARE_CUSTOM_GETTER(jsURLPattern_hasRegExpGroups); + +class JSURLPatternPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSURLPatternPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSURLPatternPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSURLPatternPrototype(vm, globalObject, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSURLPatternPrototype, 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: + JSURLPatternPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSURLPatternPrototype, JSURLPatternPrototype::Base); + +using JSURLPatternDOMConstructor = JSDOMConstructor; + +static inline EncodedJSValue constructJSURLPattern1(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = lexicalGlobalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* castedThis = jsCast(callFrame->jsCallee()); + ASSERT(castedThis); + RefPtr context = castedThis->scriptExecutionContext(); + if (!context) [[unlikely]] + return throwConstructorScriptExecutionContextUnavailableError(*lexicalGlobalObject, throwScope, "URLPattern"_s); + EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); + auto input = convert>>(*lexicalGlobalObject, argument0.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + EnsureStillAliveScope argument1 = callFrame->uncheckedArgument(1); + auto baseURL = convert(*lexicalGlobalObject, argument1.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + EnsureStillAliveScope argument2 = callFrame->argument(2); + auto options = convert>(*lexicalGlobalObject, argument2.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + auto object = URLPattern::create(*context, convertToWTFVariant(WTFMove(input)), WTFMove(baseURL), WTFMove(options)); + if constexpr (IsExceptionOr) + RETURN_IF_EXCEPTION(throwScope, {}); + static_assert(TypeOrExceptionOrUnderlyingType::isRef); + auto jsValue = toJSNewlyCreated>(*lexicalGlobalObject, *castedThis->globalObject(), throwScope, WTFMove(object)); + if constexpr (IsExceptionOr) + RETURN_IF_EXCEPTION(throwScope, {}); + setSubclassStructureIfNeeded(lexicalGlobalObject, callFrame, asObject(jsValue)); + RETURN_IF_EXCEPTION(throwScope, {}); + return JSValue::encode(jsValue); +} + +static inline EncodedJSValue constructJSURLPattern2(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = lexicalGlobalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* castedThis = jsCast(callFrame->jsCallee()); + ASSERT(castedThis); + RefPtr context = castedThis->scriptExecutionContext(); + if (!context) [[unlikely]] + return throwConstructorScriptExecutionContextUnavailableError(*lexicalGlobalObject, throwScope, "URLPattern"_s); + EnsureStillAliveScope argument0 = callFrame->argument(0); + auto input = convert>>>(*lexicalGlobalObject, argument0.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + EnsureStillAliveScope argument1 = callFrame->argument(1); + auto options = convert>(*lexicalGlobalObject, argument1.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + auto object = URLPattern::create(*context, convertToOptionalWTFVariant(WTFMove(input)), WTFMove(options)); + if constexpr (IsExceptionOr) + RETURN_IF_EXCEPTION(throwScope, {}); + static_assert(TypeOrExceptionOrUnderlyingType::isRef); + auto jsValue = toJSNewlyCreated>(*lexicalGlobalObject, *castedThis->globalObject(), throwScope, WTFMove(object)); + if constexpr (IsExceptionOr) + RETURN_IF_EXCEPTION(throwScope, {}); + setSubclassStructureIfNeeded(lexicalGlobalObject, callFrame, asObject(jsValue)); + RETURN_IF_EXCEPTION(throwScope, {}); + return JSValue::encode(jsValue); +} + +template<> EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSURLPatternDOMConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + SUPPRESS_UNCOUNTED_LOCAL auto& vm = lexicalGlobalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + size_t argsCount = std::min(3, callFrame->argumentCount()); + if (argsCount == 0) { + RELEASE_AND_RETURN(throwScope, (constructJSURLPattern2(lexicalGlobalObject, callFrame))); + } + if (argsCount == 1) { + RELEASE_AND_RETURN(throwScope, (constructJSURLPattern2(lexicalGlobalObject, callFrame))); + } + if (argsCount == 2) { + JSValue distinguishingArg = callFrame->uncheckedArgument(1); + if (distinguishingArg.isUndefined()) + RELEASE_AND_RETURN(throwScope, (constructJSURLPattern2(lexicalGlobalObject, callFrame))); + if (distinguishingArg.isUndefinedOrNull()) + RELEASE_AND_RETURN(throwScope, (constructJSURLPattern2(lexicalGlobalObject, callFrame))); + if (distinguishingArg.isObject()) + RELEASE_AND_RETURN(throwScope, (constructJSURLPattern2(lexicalGlobalObject, callFrame))); + RELEASE_AND_RETURN(throwScope, (constructJSURLPattern1(lexicalGlobalObject, callFrame))); + } + if (argsCount == 3) { + RELEASE_AND_RETURN(throwScope, (constructJSURLPattern1(lexicalGlobalObject, callFrame))); + } + return throwVMTypeError(lexicalGlobalObject, throwScope); +} +JSC_ANNOTATE_HOST_FUNCTION(JSURLPatternConstructorConstruct, JSURLPatternDOMConstructor::construct); + +template<> const ClassInfo JSURLPatternDOMConstructor::s_info = { "URLPattern"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSURLPatternDOMConstructor) }; + +template<> JSValue JSURLPatternDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + UNUSED_PARAM(vm); + return globalObject.functionPrototype(); +} + +template<> void JSURLPatternDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) +{ + putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "URLPattern"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSURLPattern::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); +} + +/* Hash table for prototype */ + +static const std::array JSURLPatternPrototypeTableValues { + HashTableValue { "constructor"_s, static_cast(PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsURLPatternConstructor, 0 } }, + HashTableValue { "protocol"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute, NoIntrinsic, { HashTableValue::GetterSetterType, jsURLPattern_protocol, 0 } }, + HashTableValue { "username"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute, NoIntrinsic, { HashTableValue::GetterSetterType, jsURLPattern_username, 0 } }, + HashTableValue { "password"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute, NoIntrinsic, { HashTableValue::GetterSetterType, jsURLPattern_password, 0 } }, + HashTableValue { "hostname"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute, NoIntrinsic, { HashTableValue::GetterSetterType, jsURLPattern_hostname, 0 } }, + HashTableValue { "port"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute, NoIntrinsic, { HashTableValue::GetterSetterType, jsURLPattern_port, 0 } }, + HashTableValue { "pathname"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute, NoIntrinsic, { HashTableValue::GetterSetterType, jsURLPattern_pathname, 0 } }, + HashTableValue { "search"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute, NoIntrinsic, { HashTableValue::GetterSetterType, jsURLPattern_search, 0 } }, + HashTableValue { "hash"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute, NoIntrinsic, { HashTableValue::GetterSetterType, jsURLPattern_hash, 0 } }, + HashTableValue { "hasRegExpGroups"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute, NoIntrinsic, { HashTableValue::GetterSetterType, jsURLPattern_hasRegExpGroups, 0 } }, + HashTableValue { "test"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsURLPatternPrototypeFunction_test, 0 } }, + HashTableValue { "exec"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsURLPatternPrototypeFunction_exec, 0 } }, +}; + +const ClassInfo JSURLPatternPrototype::s_info = { "URLPattern"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSURLPatternPrototype) }; + +void JSURLPatternPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSURLPattern::info(), JSURLPatternPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +const ClassInfo JSURLPattern::s_info = { "URLPattern"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSURLPattern) }; + +JSURLPattern::JSURLPattern(Structure* structure, JSDOMGlobalObject& globalObject, Ref&& impl) + : JSDOMWrapper(structure, globalObject, WTFMove(impl)) +{ +} + +JSObject* JSURLPattern::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSURLPatternPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSURLPatternPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSURLPattern::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSURLPattern::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *jsCast(globalObject)); +} + +void JSURLPattern::destroy(JSC::JSCell* cell) +{ + SUPPRESS_MEMORY_UNSAFE_CAST JSURLPattern* thisObject = static_cast(cell); + thisObject->JSURLPattern::~JSURLPattern(); +} + +JSC_DEFINE_CUSTOM_GETTER(jsURLPatternConstructor, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName)) +{ + SUPPRESS_UNCOUNTED_LOCAL auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* prototype = jsDynamicCast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, throwScope); + return JSValue::encode(JSURLPattern::getConstructor(vm, prototype->globalObject())); +} + +static inline JSValue jsURLPattern_protocolGetter(JSGlobalObject& lexicalGlobalObject, JSURLPattern& thisObject) +{ + SUPPRESS_UNCOUNTED_LOCAL auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + SUPPRESS_UNCOUNTED_LOCAL auto& impl = thisObject.wrapped(); + RELEASE_AND_RETURN(throwScope, (toJS(lexicalGlobalObject, throwScope, impl.protocol()))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsURLPattern_protocol, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + return IDLAttribute::get(*lexicalGlobalObject, thisValue, attributeName); +} + +static inline JSValue jsURLPattern_usernameGetter(JSGlobalObject& lexicalGlobalObject, JSURLPattern& thisObject) +{ + SUPPRESS_UNCOUNTED_LOCAL auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + SUPPRESS_UNCOUNTED_LOCAL auto& impl = thisObject.wrapped(); + RELEASE_AND_RETURN(throwScope, (toJS(lexicalGlobalObject, throwScope, impl.username()))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsURLPattern_username, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + return IDLAttribute::get(*lexicalGlobalObject, thisValue, attributeName); +} + +static inline JSValue jsURLPattern_passwordGetter(JSGlobalObject& lexicalGlobalObject, JSURLPattern& thisObject) +{ + SUPPRESS_UNCOUNTED_LOCAL auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + SUPPRESS_UNCOUNTED_LOCAL auto& impl = thisObject.wrapped(); + RELEASE_AND_RETURN(throwScope, (toJS(lexicalGlobalObject, throwScope, impl.password()))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsURLPattern_password, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + return IDLAttribute::get(*lexicalGlobalObject, thisValue, attributeName); +} + +static inline JSValue jsURLPattern_hostnameGetter(JSGlobalObject& lexicalGlobalObject, JSURLPattern& thisObject) +{ + SUPPRESS_UNCOUNTED_LOCAL auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + SUPPRESS_UNCOUNTED_LOCAL auto& impl = thisObject.wrapped(); + RELEASE_AND_RETURN(throwScope, (toJS(lexicalGlobalObject, throwScope, impl.hostname()))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsURLPattern_hostname, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + return IDLAttribute::get(*lexicalGlobalObject, thisValue, attributeName); +} + +static inline JSValue jsURLPattern_portGetter(JSGlobalObject& lexicalGlobalObject, JSURLPattern& thisObject) +{ + SUPPRESS_UNCOUNTED_LOCAL auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + SUPPRESS_UNCOUNTED_LOCAL auto& impl = thisObject.wrapped(); + RELEASE_AND_RETURN(throwScope, (toJS(lexicalGlobalObject, throwScope, impl.port()))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsURLPattern_port, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + return IDLAttribute::get(*lexicalGlobalObject, thisValue, attributeName); +} + +static inline JSValue jsURLPattern_pathnameGetter(JSGlobalObject& lexicalGlobalObject, JSURLPattern& thisObject) +{ + SUPPRESS_UNCOUNTED_LOCAL auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + SUPPRESS_UNCOUNTED_LOCAL auto& impl = thisObject.wrapped(); + RELEASE_AND_RETURN(throwScope, (toJS(lexicalGlobalObject, throwScope, impl.pathname()))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsURLPattern_pathname, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + return IDLAttribute::get(*lexicalGlobalObject, thisValue, attributeName); +} + +static inline JSValue jsURLPattern_searchGetter(JSGlobalObject& lexicalGlobalObject, JSURLPattern& thisObject) +{ + SUPPRESS_UNCOUNTED_LOCAL auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + SUPPRESS_UNCOUNTED_LOCAL auto& impl = thisObject.wrapped(); + RELEASE_AND_RETURN(throwScope, (toJS(lexicalGlobalObject, throwScope, impl.search()))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsURLPattern_search, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + return IDLAttribute::get(*lexicalGlobalObject, thisValue, attributeName); +} + +static inline JSValue jsURLPattern_hashGetter(JSGlobalObject& lexicalGlobalObject, JSURLPattern& thisObject) +{ + SUPPRESS_UNCOUNTED_LOCAL auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + SUPPRESS_UNCOUNTED_LOCAL auto& impl = thisObject.wrapped(); + RELEASE_AND_RETURN(throwScope, (toJS(lexicalGlobalObject, throwScope, impl.hash()))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsURLPattern_hash, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + return IDLAttribute::get(*lexicalGlobalObject, thisValue, attributeName); +} + +static inline JSValue jsURLPattern_hasRegExpGroupsGetter(JSGlobalObject& lexicalGlobalObject, JSURLPattern& thisObject) +{ + SUPPRESS_UNCOUNTED_LOCAL auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + SUPPRESS_UNCOUNTED_LOCAL auto& impl = thisObject.wrapped(); + RELEASE_AND_RETURN(throwScope, (toJS(lexicalGlobalObject, throwScope, impl.hasRegExpGroups()))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsURLPattern_hasRegExpGroups, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + return IDLAttribute::get(*lexicalGlobalObject, thisValue, attributeName); +} + +static inline JSC::EncodedJSValue jsURLPatternPrototypeFunction_testBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + RefPtr context = jsCast(lexicalGlobalObject)->scriptExecutionContext(); + if (!context) [[unlikely]] + return JSValue::encode(jsUndefined()); + EnsureStillAliveScope argument0 = callFrame->argument(0); + auto input = convert>>>(*lexicalGlobalObject, argument0.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + EnsureStillAliveScope argument1 = callFrame->argument(1); + auto baseURL = argument1.value().isUndefined() ? String() : convert(*lexicalGlobalObject, argument1.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS(*lexicalGlobalObject, throwScope, impl.test(*context, convertToOptionalWTFVariant(WTFMove(input)), WTFMove(baseURL))))); +} + +JSC_DEFINE_HOST_FUNCTION(jsURLPatternPrototypeFunction_test, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "test"); +} + +static inline JSC::EncodedJSValue jsURLPatternPrototypeFunction_execBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(throwScope); + UNUSED_PARAM(callFrame); + auto& impl = castedThis->wrapped(); + RefPtr context = jsCast(lexicalGlobalObject)->scriptExecutionContext(); + if (!context) [[unlikely]] + return JSValue::encode(jsUndefined()); + EnsureStillAliveScope argument0 = callFrame->argument(0); + auto input = convert>>>(*lexicalGlobalObject, argument0.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + EnsureStillAliveScope argument1 = callFrame->argument(1); + auto baseURL = argument1.value().isUndefined() ? String() : convert(*lexicalGlobalObject, argument1.value()); + RETURN_IF_EXCEPTION(throwScope, encodedJSValue()); + RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS>>(*lexicalGlobalObject, *castedThis->globalObject(), throwScope, impl.exec(*context, convertToOptionalWTFVariant(WTFMove(input)), WTFMove(baseURL))))); +} + +JSC_DEFINE_HOST_FUNCTION(jsURLPatternPrototypeFunction_exec, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + return IDLOperation::call(*lexicalGlobalObject, *callFrame, "exec"); +} + +JSC::GCClient::IsoSubspace* JSURLPattern::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl(vm, [](auto& spaces) { return spaces.m_clientSubspaceForURLPattern.get(); }, [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForURLPattern = std::forward(space); }, [](auto& spaces) { return spaces.m_subspaceForURLPattern.get(); }, [](auto& spaces, auto&& space) { spaces.m_subspaceForURLPattern = std::forward(space); }); +} + +void JSURLPattern::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) +{ + auto* thisObject = jsCast(cell); + analyzer.setWrappedObjectForCell(cell, &thisObject->wrapped()); + if (RefPtr context = thisObject->scriptExecutionContext()) + analyzer.setLabelForCell(cell, makeString("url "_s, context->url().string())); + Base::analyzeHeap(cell, analyzer); +} + +bool JSURLPatternOwner::isReachableFromOpaqueRoots(JSC::Handle handle, void*, AbstractSlotVisitor& visitor, ASCIILiteral* reason) +{ + UNUSED_PARAM(handle); + UNUSED_PARAM(visitor); + UNUSED_PARAM(reason); + return false; +} + +void JSURLPatternOwner::finalize(JSC::Handle handle, void* context) +{ + SUPPRESS_MEMORY_UNSAFE_CAST auto* jsURLPattern = static_cast(handle.slot()->asCell()); + auto& world = *static_cast(context); + uncacheWrapper(world, jsURLPattern->protectedWrapped().ptr(), jsURLPattern); +} + +WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN +#if ENABLE(BINDING_INTEGRITY) +#if PLATFORM(WIN) +#pragma warning(disable : 4483) +extern "C" { +extern void (*const __identifier("??_7URLPattern@WebCore@@6B@")[])(); +} +#else +extern "C" { +extern void* _ZTVN7WebCore10URLPatternE[]; +} +#endif +template T> +static inline void verifyVTable(URLPattern* ptr) +{ + if constexpr (std::is_polymorphic_v) { + const void* actualVTablePointer = getVTablePointer(ptr); +#if PLATFORM(WIN) + void* expectedVTablePointer = __identifier("??_7URLPattern@WebCore@@6B@"); +#else + void* expectedVTablePointer = &_ZTVN7WebCore10URLPatternE[2]; +#endif + + // If you hit this assertion you either have a use after free bug, or + // URLPattern has subclasses. If URLPattern has subclasses that get passed + // to toJS() we currently require URLPattern you to opt out of binding hardening + // by adding the SkipVTableValidation attribute to the interface IDL definition + RELEASE_ASSERT(actualVTablePointer == expectedVTablePointer); + } +} +#endif +WTF_ALLOW_UNSAFE_BUFFER_USAGE_END + +JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject* globalObject, Ref&& impl) +{ +#if ENABLE(BINDING_INTEGRITY) + verifyVTable(impl.ptr()); +#endif + return createWrapper(globalObject, WTFMove(impl)); +} + +JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, URLPattern& impl) +{ + return wrap(lexicalGlobalObject, globalObject, impl); +} + +URLPattern* JSURLPattern::toWrapped(JSC::VM&, JSC::JSValue value) +{ + if (auto* wrapper = jsDynamicCast(value)) + return &wrapper->wrapped(); + return nullptr; +} + +} diff --git a/src/bun.js/bindings/webcore/JSURLPattern.h b/src/bun.js/bindings/webcore/JSURLPattern.h new file mode 100644 index 0000000000..1f7b6b520e --- /dev/null +++ b/src/bun.js/bindings/webcore/JSURLPattern.h @@ -0,0 +1,96 @@ +/* + This file is part of the WebKit open source project. + This file has been generated by generate-bindings.pl. DO NOT MODIFY! + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include "URLPattern.h" +#include "JSDOMWrapper.h" +#include + +namespace WebCore { + +class JSURLPattern : public JSDOMWrapper { +public: + using Base = JSDOMWrapper; + static JSURLPattern* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject, Ref&& impl) + { + SUPPRESS_UNCOUNTED_LOCAL auto& vm = globalObject->vm(); + JSURLPattern* ptr = new (NotNull, JSC::allocateCell(vm)) JSURLPattern(structure, *globalObject, WTFMove(impl)); + ptr->finishCreation(vm); + return ptr; + } + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static URLPattern* 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(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); + } + + 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&); + +protected: + JSURLPattern(JSC::Structure*, JSDOMGlobalObject&, Ref&&); + + DECLARE_DEFAULT_FINISH_CREATION; +}; + +class JSURLPatternOwner 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&, URLPattern*) +{ + static NeverDestroyed owner; + return &owner.get(); +} + +inline void* wrapperKey(URLPattern* wrappableObject) +{ + return wrappableObject; +} + +JSC::JSValue toJS(JSC::JSGlobalObject*, JSDOMGlobalObject*, URLPattern&); +inline JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, URLPattern* impl) { return impl ? toJS(lexicalGlobalObject, globalObject, *impl) : JSC::jsNull(); } +JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject*, Ref&&); +ALWAYS_INLINE JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, URLPattern& impl) { return toJSNewlyCreated(lexicalGlobalObject, globalObject, Ref { impl }); } +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 = JSURLPattern; + using ToWrappedReturnType = URLPattern*; +}; + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSURLPatternInit.cpp b/src/bun.js/bindings/webcore/JSURLPatternInit.cpp new file mode 100644 index 0000000000..625af5be84 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSURLPatternInit.cpp @@ -0,0 +1,200 @@ +/* + This file is part of the WebKit open source project. + This file has been generated by generate-bindings.pl. DO NOT MODIFY! + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "config.h" +#include "JSURLPatternInit.h" + +#include "JSDOMConvertStrings.h" +#include "JSDOMGlobalObject.h" +#include +#include + +namespace WebCore { +using namespace JSC; + +template<> URLPatternInit convertDictionary(JSGlobalObject& lexicalGlobalObject, JSValue value) +{ + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + bool isNullOrUndefined = value.isUndefinedOrNull(); + auto* object = isNullOrUndefined ? nullptr : value.getObject(); + if (!isNullOrUndefined && !object) [[unlikely]] { + throwTypeError(&lexicalGlobalObject, throwScope); + return {}; + } + URLPatternInit result; + JSValue baseURLValue; + if (isNullOrUndefined) + baseURLValue = jsUndefined(); + else { + baseURLValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "baseURL"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + } + if (!baseURLValue.isUndefined()) { + result.baseURL = convert(lexicalGlobalObject, baseURLValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + JSValue hashValue; + if (isNullOrUndefined) + hashValue = jsUndefined(); + else { + hashValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "hash"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + } + if (!hashValue.isUndefined()) { + result.hash = convert(lexicalGlobalObject, hashValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + JSValue hostnameValue; + if (isNullOrUndefined) + hostnameValue = jsUndefined(); + else { + hostnameValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "hostname"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + } + if (!hostnameValue.isUndefined()) { + result.hostname = convert(lexicalGlobalObject, hostnameValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + JSValue passwordValue; + if (isNullOrUndefined) + passwordValue = jsUndefined(); + else { + passwordValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "password"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + } + if (!passwordValue.isUndefined()) { + result.password = convert(lexicalGlobalObject, passwordValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + JSValue pathnameValue; + if (isNullOrUndefined) + pathnameValue = jsUndefined(); + else { + pathnameValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "pathname"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + } + if (!pathnameValue.isUndefined()) { + result.pathname = convert(lexicalGlobalObject, pathnameValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + JSValue portValue; + if (isNullOrUndefined) + portValue = jsUndefined(); + else { + portValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "port"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + } + if (!portValue.isUndefined()) { + result.port = convert(lexicalGlobalObject, portValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + JSValue protocolValue; + if (isNullOrUndefined) + protocolValue = jsUndefined(); + else { + protocolValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "protocol"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + } + if (!protocolValue.isUndefined()) { + result.protocol = convert(lexicalGlobalObject, protocolValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + JSValue searchValue; + if (isNullOrUndefined) + searchValue = jsUndefined(); + else { + searchValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "search"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + } + if (!searchValue.isUndefined()) { + result.search = convert(lexicalGlobalObject, searchValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + JSValue usernameValue; + if (isNullOrUndefined) + usernameValue = jsUndefined(); + else { + usernameValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "username"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + } + if (!usernameValue.isUndefined()) { + result.username = convert(lexicalGlobalObject, usernameValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } + return result; +} + +JSC::JSObject* convertDictionaryToJS(JSC::JSGlobalObject& lexicalGlobalObject, JSDOMGlobalObject& globalObject, const URLPatternInit& dictionary) +{ + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + auto result = constructEmptyObject(&lexicalGlobalObject, globalObject.objectPrototype()); + + if (!IDLUSVString::isNullValue(dictionary.baseURL)) { + auto baseURLValue = toJS(lexicalGlobalObject, throwScope, IDLUSVString::extractValueFromNullable(dictionary.baseURL)); + RETURN_IF_EXCEPTION(throwScope, {}); + result->putDirect(vm, JSC::Identifier::fromString(vm, "baseURL"_s), baseURLValue); + } + if (!IDLUSVString::isNullValue(dictionary.hash)) { + auto hashValue = toJS(lexicalGlobalObject, throwScope, IDLUSVString::extractValueFromNullable(dictionary.hash)); + RETURN_IF_EXCEPTION(throwScope, {}); + result->putDirect(vm, JSC::Identifier::fromString(vm, "hash"_s), hashValue); + } + if (!IDLUSVString::isNullValue(dictionary.hostname)) { + auto hostnameValue = toJS(lexicalGlobalObject, throwScope, IDLUSVString::extractValueFromNullable(dictionary.hostname)); + RETURN_IF_EXCEPTION(throwScope, {}); + result->putDirect(vm, JSC::Identifier::fromString(vm, "hostname"_s), hostnameValue); + } + if (!IDLUSVString::isNullValue(dictionary.password)) { + auto passwordValue = toJS(lexicalGlobalObject, throwScope, IDLUSVString::extractValueFromNullable(dictionary.password)); + RETURN_IF_EXCEPTION(throwScope, {}); + result->putDirect(vm, JSC::Identifier::fromString(vm, "password"_s), passwordValue); + } + if (!IDLUSVString::isNullValue(dictionary.pathname)) { + auto pathnameValue = toJS(lexicalGlobalObject, throwScope, IDLUSVString::extractValueFromNullable(dictionary.pathname)); + RETURN_IF_EXCEPTION(throwScope, {}); + result->putDirect(vm, JSC::Identifier::fromString(vm, "pathname"_s), pathnameValue); + } + if (!IDLUSVString::isNullValue(dictionary.port)) { + auto portValue = toJS(lexicalGlobalObject, throwScope, IDLUSVString::extractValueFromNullable(dictionary.port)); + RETURN_IF_EXCEPTION(throwScope, {}); + result->putDirect(vm, JSC::Identifier::fromString(vm, "port"_s), portValue); + } + if (!IDLUSVString::isNullValue(dictionary.protocol)) { + auto protocolValue = toJS(lexicalGlobalObject, throwScope, IDLUSVString::extractValueFromNullable(dictionary.protocol)); + RETURN_IF_EXCEPTION(throwScope, {}); + result->putDirect(vm, JSC::Identifier::fromString(vm, "protocol"_s), protocolValue); + } + if (!IDLUSVString::isNullValue(dictionary.search)) { + auto searchValue = toJS(lexicalGlobalObject, throwScope, IDLUSVString::extractValueFromNullable(dictionary.search)); + RETURN_IF_EXCEPTION(throwScope, {}); + result->putDirect(vm, JSC::Identifier::fromString(vm, "search"_s), searchValue); + } + if (!IDLUSVString::isNullValue(dictionary.username)) { + auto usernameValue = toJS(lexicalGlobalObject, throwScope, IDLUSVString::extractValueFromNullable(dictionary.username)); + RETURN_IF_EXCEPTION(throwScope, {}); + result->putDirect(vm, JSC::Identifier::fromString(vm, "username"_s), usernameValue); + } + return result; +} + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSURLPatternInit.h b/src/bun.js/bindings/webcore/JSURLPatternInit.h new file mode 100644 index 0000000000..7bb0ac061d --- /dev/null +++ b/src/bun.js/bindings/webcore/JSURLPatternInit.h @@ -0,0 +1,32 @@ +/* + This file is part of the WebKit open source project. + This file has been generated by generate-bindings.pl. DO NOT MODIFY! + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include "JSDOMConvertDictionary.h" +#include "URLPatternInit.h" + +namespace WebCore { + +template<> URLPatternInit convertDictionary(JSC::JSGlobalObject&, JSC::JSValue); + +JSC::JSObject* convertDictionaryToJS(JSC::JSGlobalObject&, JSDOMGlobalObject&, const URLPatternInit&); + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSURLPatternOptions.cpp b/src/bun.js/bindings/webcore/JSURLPatternOptions.cpp new file mode 100644 index 0000000000..43b07abe21 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSURLPatternOptions.cpp @@ -0,0 +1,56 @@ +/* + This file is part of the WebKit open source project. + This file has been generated by generate-bindings.pl. DO NOT MODIFY! + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "config.h" +#include "JSURLPatternOptions.h" + +#include "JSDOMConvertBoolean.h" +#include + +namespace WebCore { +using namespace JSC; + +template<> URLPatternOptions convertDictionary(JSGlobalObject& lexicalGlobalObject, JSValue value) +{ + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + bool isNullOrUndefined = value.isUndefinedOrNull(); + auto* object = isNullOrUndefined ? nullptr : value.getObject(); + if (!isNullOrUndefined && !object) [[unlikely]] { + throwTypeError(&lexicalGlobalObject, throwScope); + return {}; + } + URLPatternOptions result; + JSValue ignoreCaseValue; + if (isNullOrUndefined) + ignoreCaseValue = jsUndefined(); + else { + ignoreCaseValue = object->get(&lexicalGlobalObject, Identifier::fromString(vm, "ignoreCase"_s)); + RETURN_IF_EXCEPTION(throwScope, {}); + } + if (!ignoreCaseValue.isUndefined()) { + result.ignoreCase = convert(lexicalGlobalObject, ignoreCaseValue); + RETURN_IF_EXCEPTION(throwScope, {}); + } else + result.ignoreCase = false; + return result; +} + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSURLPatternOptions.h b/src/bun.js/bindings/webcore/JSURLPatternOptions.h new file mode 100644 index 0000000000..7ea3f8de33 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSURLPatternOptions.h @@ -0,0 +1,30 @@ +/* + This file is part of the WebKit open source project. + This file has been generated by generate-bindings.pl. DO NOT MODIFY! + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include "JSDOMConvertDictionary.h" +#include "URLPatternOptions.h" + +namespace WebCore { + +template<> URLPatternOptions convertDictionary(JSC::JSGlobalObject&, JSC::JSValue); + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSURLPatternResult.cpp b/src/bun.js/bindings/webcore/JSURLPatternResult.cpp new file mode 100644 index 0000000000..87e2e77934 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSURLPatternResult.cpp @@ -0,0 +1,175 @@ +/* + This file is part of the WebKit open source project. + This file has been generated by generate-bindings.pl. DO NOT MODIFY! + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "config.h" +#include "root.h" +#include "JSURLPatternResult.h" + +#include "IDLTypes.h" +#include "JSDOMConvertBase.h" +#include "JSDOMConvertStrings.h" +#include "JSDOMGlobalObject.h" +#include "JSURLPatternInit.h" +#include +#include +#include + +namespace WebCore { +using namespace JSC; + +// URLPatternResult and URLPatternComponentResult are output-only dictionaries that are +// returned from exec() but never accepted as input from JavaScript. These convertDictionary +// template specializations are required to satisfy template instantiation in the binding +// infrastructure. They intentionally throw TypeErrors to catch any invalid JS→native +// conversion attempts, as these types should never be constructed from JavaScript values. +template<> URLPatternResult convertDictionary(JSGlobalObject& lexicalGlobalObject, JSValue value) +{ + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(value); + throwTypeError(&lexicalGlobalObject, throwScope, "URLPatternResult cannot be converted from JavaScript"_s); + return {}; +} + +template<> URLPatternComponentResult convertDictionary(JSGlobalObject& lexicalGlobalObject, JSValue value) +{ + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + UNUSED_PARAM(value); + throwTypeError(&lexicalGlobalObject, throwScope, "URLPatternComponentResult cannot be converted from JavaScript"_s); + return {}; +} + +// Helper to convert the groups record to JS +static JSC::JSObject* convertGroupsToJS(JSC::JSGlobalObject& lexicalGlobalObject, JSDOMGlobalObject& globalObject, const URLPatternComponentResult::GroupsRecord& groups) +{ + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + auto result = constructEmptyObject(&lexicalGlobalObject, globalObject.objectPrototype()); + + for (const auto& pair : groups) { + JSValue jsValue = WTF::switchOn(pair.value, [&](std::monostate) -> JSValue { return jsUndefined(); }, [&](const String& str) -> JSValue { return toJS(lexicalGlobalObject, throwScope, str); }); + RETURN_IF_EXCEPTION(throwScope, nullptr); + + // Check if the key is an array index + auto identifier = Identifier::fromString(vm, pair.key); + if (auto index = parseIndex(identifier)) { + result->putDirectIndex(&lexicalGlobalObject, index.value(), jsValue); + RETURN_IF_EXCEPTION(throwScope, nullptr); + } else { + result->putDirect(vm, identifier, jsValue); + } + } + + return result; +} + +// Helper to convert URLPatternInput (variant) to JS +static JSC::JSValue convertURLPatternInputToJS(JSC::JSGlobalObject& lexicalGlobalObject, JSDOMGlobalObject& globalObject, const URLPattern::URLPatternInput& input) +{ + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + return WTF::switchOn(input, [&](const String& str) -> JSValue { return toJS(lexicalGlobalObject, throwScope, str); }, [&](const URLPatternInit& init) -> JSValue { return convertDictionaryToJS(lexicalGlobalObject, globalObject, init); }); +} + +JSC::JSObject* convertDictionaryToJS(JSC::JSGlobalObject& lexicalGlobalObject, JSDOMGlobalObject& globalObject, const URLPatternComponentResult& dictionary) +{ + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + auto result = constructEmptyObject(&lexicalGlobalObject, globalObject.objectPrototype()); + + // Output input + auto inputValue = toJS(lexicalGlobalObject, throwScope, dictionary.input); + RETURN_IF_EXCEPTION(throwScope, nullptr); + result->putDirect(vm, Identifier::fromString(vm, "input"_s), inputValue); + + // Output groups - record + auto groupsValue = convertGroupsToJS(lexicalGlobalObject, globalObject, dictionary.groups); + RETURN_IF_EXCEPTION(throwScope, nullptr); + result->putDirect(vm, Identifier::fromString(vm, "groups"_s), groupsValue); + + return result; +} + +JSC::JSObject* convertDictionaryToJS(JSC::JSGlobalObject& lexicalGlobalObject, JSDOMGlobalObject& globalObject, const URLPatternResult& dictionary) +{ + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + auto result = constructEmptyObject(&lexicalGlobalObject, globalObject.objectPrototype()); + + // Output inputs - sequence<(USVString or URLPatternInit)> + auto inputsArray = JSC::constructEmptyArray(&lexicalGlobalObject, nullptr, dictionary.inputs.size()); + RETURN_IF_EXCEPTION(throwScope, nullptr); + for (size_t i = 0; i < dictionary.inputs.size(); ++i) { + auto inputValue = convertURLPatternInputToJS(lexicalGlobalObject, globalObject, dictionary.inputs[i]); + RETURN_IF_EXCEPTION(throwScope, nullptr); + inputsArray->putDirectIndex(&lexicalGlobalObject, i, inputValue); + RETURN_IF_EXCEPTION(throwScope, nullptr); + } + result->putDirect(vm, Identifier::fromString(vm, "inputs"_s), inputsArray); + + // Output protocol + auto protocolValue = convertDictionaryToJS(lexicalGlobalObject, globalObject, dictionary.protocol); + RETURN_IF_EXCEPTION(throwScope, nullptr); + result->putDirect(vm, Identifier::fromString(vm, "protocol"_s), protocolValue); + + // Output username + auto usernameValue = convertDictionaryToJS(lexicalGlobalObject, globalObject, dictionary.username); + RETURN_IF_EXCEPTION(throwScope, nullptr); + result->putDirect(vm, Identifier::fromString(vm, "username"_s), usernameValue); + + // Output password + auto passwordValue = convertDictionaryToJS(lexicalGlobalObject, globalObject, dictionary.password); + RETURN_IF_EXCEPTION(throwScope, nullptr); + result->putDirect(vm, Identifier::fromString(vm, "password"_s), passwordValue); + + // Output hostname + auto hostnameValue = convertDictionaryToJS(lexicalGlobalObject, globalObject, dictionary.hostname); + RETURN_IF_EXCEPTION(throwScope, nullptr); + result->putDirect(vm, Identifier::fromString(vm, "hostname"_s), hostnameValue); + + // Output port + auto portValue = convertDictionaryToJS(lexicalGlobalObject, globalObject, dictionary.port); + RETURN_IF_EXCEPTION(throwScope, nullptr); + result->putDirect(vm, Identifier::fromString(vm, "port"_s), portValue); + + // Output pathname + auto pathnameValue = convertDictionaryToJS(lexicalGlobalObject, globalObject, dictionary.pathname); + RETURN_IF_EXCEPTION(throwScope, nullptr); + result->putDirect(vm, Identifier::fromString(vm, "pathname"_s), pathnameValue); + + // Output search + auto searchValue = convertDictionaryToJS(lexicalGlobalObject, globalObject, dictionary.search); + RETURN_IF_EXCEPTION(throwScope, nullptr); + result->putDirect(vm, Identifier::fromString(vm, "search"_s), searchValue); + + // Output hash + auto hashValue = convertDictionaryToJS(lexicalGlobalObject, globalObject, dictionary.hash); + RETURN_IF_EXCEPTION(throwScope, nullptr); + result->putDirect(vm, Identifier::fromString(vm, "hash"_s), hashValue); + + return result; +} + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSURLPatternResult.h b/src/bun.js/bindings/webcore/JSURLPatternResult.h new file mode 100644 index 0000000000..50da4eae23 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSURLPatternResult.h @@ -0,0 +1,36 @@ +/* + This file is part of the WebKit open source project. + This file has been generated by generate-bindings.pl. DO NOT MODIFY! + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include "JSDOMConvertDictionary.h" +#include "URLPatternResult.h" + +namespace WebCore { + +template<> URLPatternResult convertDictionary(JSC::JSGlobalObject&, JSC::JSValue); + +JSC::JSObject* convertDictionaryToJS(JSC::JSGlobalObject&, JSDOMGlobalObject&, const URLPatternResult&); + +template<> URLPatternComponentResult convertDictionary(JSC::JSGlobalObject&, JSC::JSValue); + +JSC::JSObject* convertDictionaryToJS(JSC::JSGlobalObject&, JSDOMGlobalObject&, const URLPatternComponentResult&); + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/URLPattern.cpp b/src/bun.js/bindings/webcore/URLPattern.cpp new file mode 100644 index 0000000000..705ca93cd0 --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPattern.cpp @@ -0,0 +1,493 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "URLPattern.h" + +#include "ExceptionOr.h" +#include "ScriptExecutionContext.h" +#include "URLPatternCanonical.h" +#include "URLPatternConstructorStringParser.h" +#include "URLPatternInit.h" +#include "URLPatternOptions.h" +#include "URLPatternParser.h" +#include "URLPatternResult.h" +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { +using namespace JSC; + +WTF_MAKE_TZONE_OR_ISO_ALLOCATED_IMPL(URLPattern); + +// https://urlpattern.spec.whatwg.org/#process-a-base-url-string +static String processBaseURLString(StringView input, BaseURLStringType type) +{ + if (type != BaseURLStringType::Pattern) + return input.toString(); + + return URLPatternUtilities::escapePatternString(input); +} + +// https://urlpattern.spec.whatwg.org/#hostname-pattern-is-an-ipv6-address +static bool isHostnamePatternIPv6(StringView hostname) +{ + if (hostname.length() < 2) + return false; + if (hostname[0] == '[') + return true; + if (hostname[0] == '{' && hostname[1] == '[') + return true; + if (hostname[0] == '\\' && hostname[1] == '[') + return true; + return false; +} + +URLPattern::URLPattern() = default; + +// https://urlpattern.spec.whatwg.org/#process-a-urlpatterninit +static ExceptionOr processInit(URLPatternInit&& init, BaseURLStringType type, String&& protocol = {}, String&& username = {}, String&& password = {}, String&& hostname = {}, String&& port = {}, String&& pathname = {}, String&& search = {}, String&& hash = {}) +{ + URLPatternInit result { WTFMove(protocol), WTFMove(username), WTFMove(password), WTFMove(hostname), WTFMove(port), WTFMove(pathname), WTFMove(search), WTFMove(hash), {} }; + + URL baseURL; + + if (!init.baseURL.isNull()) { + baseURL = URL(init.baseURL); + + if (!baseURL.isValid()) + return Exception { ExceptionCode::TypeError, "Invalid baseURL."_s }; + + if (init.protocol.isNull()) + result.protocol = processBaseURLString(baseURL.protocol(), type); + + if (type != BaseURLStringType::Pattern + && init.protocol.isNull() + && init.hostname.isNull() + && init.port.isNull() + && init.username.isNull()) + result.username = processBaseURLString(baseURL.user(), type); + + if (type != BaseURLStringType::Pattern + && init.protocol.isNull() + && init.hostname.isNull() + && init.port.isNull() + && init.username.isNull() + && init.password.isNull()) + result.password = processBaseURLString(baseURL.password(), type); + + if (init.protocol.isNull() + && init.hostname.isNull()) { + result.hostname = processBaseURLString(!baseURL.host().isNull() ? baseURL.host() : StringView { emptyString() }, type); + } + + if (init.protocol.isNull() + && init.hostname.isNull() + && init.port.isNull()) { + auto port = baseURL.port(); + result.port = port ? String::number(*port) : emptyString(); + } + + if (init.protocol.isNull() + && init.hostname.isNull() + && init.port.isNull() + && init.pathname.isNull()) { + result.pathname = processBaseURLString(baseURL.path(), type); + } + + if (init.protocol.isNull() + && init.hostname.isNull() + && init.port.isNull() + && init.pathname.isNull() + && init.search.isNull()) { + result.search = processBaseURLString(baseURL.hasQuery() ? baseURL.query() : StringView { emptyString() }, type); + } + + if (init.protocol.isNull() + && init.hostname.isNull() + && init.port.isNull() + && init.pathname.isNull() + && init.search.isNull() + && init.hash.isNull()) { + result.hash = processBaseURLString(baseURL.hasFragmentIdentifier() ? baseURL.fragmentIdentifier() : StringView { emptyString() }, type); + } + } + + if (!init.protocol.isNull()) { + auto protocolResult = canonicalizeProtocol(init.protocol, type); + + if (protocolResult.hasException()) + return protocolResult.releaseException(); + + result.protocol = protocolResult.releaseReturnValue(); + } + + if (!init.username.isNull()) + result.username = canonicalizeUsername(init.username, type); + + if (!init.password.isNull()) + result.password = canonicalizePassword(init.password, type); + + if (!init.hostname.isNull()) { + auto hostResult = canonicalizeHostname(init.hostname, type); + + if (hostResult.hasException()) + return hostResult.releaseException(); + + result.hostname = hostResult.releaseReturnValue(); + } + + if (!init.port.isNull()) { + auto portResult = canonicalizePort(init.port, result.protocol, type); + + if (portResult.hasException()) + return portResult.releaseException(); + + result.port = portResult.releaseReturnValue(); + } + + if (!init.pathname.isNull()) { + result.pathname = init.pathname; + + if (!baseURL.isNull() && !baseURL.hasOpaquePath() && !isAbsolutePathname(result.pathname, type)) { + auto baseURLPath = processBaseURLString(baseURL.path(), type); + size_t slashIndex = baseURLPath.reverseFind('/'); + + if (slashIndex != notFound) + result.pathname = makeString(StringView { baseURLPath }.left(slashIndex + 1), result.pathname); + } + auto pathResult = processPathname(result.pathname, result.protocol, type); + + if (pathResult.hasException()) + return pathResult.releaseException(); + + result.pathname = pathResult.releaseReturnValue(); + } + + if (!init.search.isNull()) { + auto queryResult = canonicalizeSearch(init.search, type); + + if (queryResult.hasException()) + return queryResult.releaseException(); + + result.search = queryResult.releaseReturnValue(); + } + + if (!init.hash.isNull()) { + auto fragmentResult = canonicalizeHash(init.hash, type); + + if (fragmentResult.hasException()) + return fragmentResult.releaseException(); + + result.hash = fragmentResult.releaseReturnValue(); + } + + return result; +} + +// https://urlpattern.spec.whatwg.org/#url-pattern-create +ExceptionOr> URLPattern::create(ScriptExecutionContext& context, URLPatternInput&& input, String&& baseURL, URLPatternOptions&& options) +{ + URLPatternInit init; + + if (std::holds_alternative(input) && !std::get(input).isNull()) { + auto maybeInit = URLPatternConstructorStringParser(WTFMove(std::get(input))).parse(context); + if (maybeInit.hasException()) + return maybeInit.releaseException(); + init = maybeInit.releaseReturnValue(); + + if (baseURL.isNull() && init.protocol.isEmpty()) + return Exception { ExceptionCode::TypeError, "Relative constructor string must have additional baseURL argument."_s }; + init.baseURL = WTFMove(baseURL); + } else if (std::holds_alternative(input)) { + if (!baseURL.isNull()) + return Exception { ExceptionCode::TypeError, "Constructor with a URLPatternInit should have a null baseURL argument."_s }; + init = std::get(input); + } + + auto maybeProcessedInit = processInit(WTFMove(init), BaseURLStringType::Pattern); + + if (maybeProcessedInit.hasException()) + return maybeProcessedInit.releaseException(); + + auto processedInit = maybeProcessedInit.releaseReturnValue(); + if (!processedInit.protocol) + processedInit.protocol = "*"_s; + if (!processedInit.username) + processedInit.username = "*"_s; + if (!processedInit.password) + processedInit.password = "*"_s; + if (!processedInit.hostname) + processedInit.hostname = "*"_s; + if (!processedInit.pathname) + processedInit.pathname = "*"_s; + if (!processedInit.search) + processedInit.search = "*"_s; + if (!processedInit.hash) + processedInit.hash = "*"_s; + if (!processedInit.port) + processedInit.port = "*"_s; + + if (auto parsedPort = parseInteger(processedInit.port, 10, WTF::ParseIntegerWhitespacePolicy::Disallow)) { + if (WTF::URLParser::isSpecialScheme(processedInit.protocol) && isDefaultPortForProtocol(*parsedPort, processedInit.protocol)) + processedInit.port = emptyString(); + } + + Ref result = adoptRef(*new URLPattern); + + auto maybeCompileException = result->compileAllComponents(context, WTFMove(processedInit), options); + if (maybeCompileException.hasException()) + return maybeCompileException.releaseException(); + + return result; +} + +// https://urlpattern.spec.whatwg.org/#urlpattern-initialize +ExceptionOr> URLPattern::create(ScriptExecutionContext& context, std::optional&& input, URLPatternOptions&& options) +{ + if (!input) + input = URLPatternInit {}; + + return create(context, WTFMove(*input), String {}, WTFMove(options)); +} + +// https://urlpattern.spec.whatwg.org/#build-a-url-pattern-from-a-web-idl-value +ExceptionOr> URLPattern::create(ScriptExecutionContext& context, Compatible&& value, const String& baseURL) +{ + return switchOn(WTFMove(value), [&](RefPtr&& pattern) -> ExceptionOr> { return pattern.releaseNonNull(); }, [&](URLPatternInit&& init) -> ExceptionOr> { return URLPattern::create(context, WTFMove(init), {}, {}); }, [&](String&& string) -> ExceptionOr> { return URLPattern::create(context, WTFMove(string), String { baseURL }, {}); }); +} + +URLPattern::~URLPattern() = default; + +// https://urlpattern.spec.whatwg.org/#dom-urlpattern-test +ExceptionOr URLPattern::test(ScriptExecutionContext& context, std::optional&& input, String&& baseURL) const +{ + if (!input) + input = URLPatternInit {}; + + auto maybeResult = match(context, WTFMove(*input), WTFMove(baseURL)); + if (maybeResult.hasException()) + return maybeResult.releaseException(); + + return !!maybeResult.returnValue(); +} + +// https://urlpattern.spec.whatwg.org/#dom-urlpattern-exec +ExceptionOr> URLPattern::exec(ScriptExecutionContext& context, std::optional&& input, String&& baseURL) const +{ + if (!input) + input = URLPatternInit {}; + + return match(context, WTFMove(*input), WTFMove(baseURL)); +} + +ExceptionOr URLPattern::compileAllComponents(ScriptExecutionContext& context, URLPatternInit&& processedInit, const URLPatternOptions& options) +{ + Ref vm = context.vm(); + JSC::JSLockHolder lock(vm); + + auto maybeProtocolComponent = URLPatternUtilities::URLPatternComponent::compile(vm, processedInit.protocol, EncodingCallbackType::Protocol, URLPatternUtilities::URLPatternStringOptions {}); + if (maybeProtocolComponent.hasException()) + return maybeProtocolComponent.releaseException(); + m_protocolComponent = maybeProtocolComponent.releaseReturnValue(); + + auto maybeUsernameComponent = URLPatternUtilities::URLPatternComponent::compile(vm, processedInit.username, EncodingCallbackType::Username, URLPatternUtilities::URLPatternStringOptions {}); + if (maybeUsernameComponent.hasException()) + return maybeUsernameComponent.releaseException(); + m_usernameComponent = maybeUsernameComponent.releaseReturnValue(); + + auto maybePasswordComponent = URLPatternUtilities::URLPatternComponent::compile(vm, processedInit.password, EncodingCallbackType::Password, URLPatternUtilities::URLPatternStringOptions {}); + if (maybePasswordComponent.hasException()) + return maybePasswordComponent.releaseException(); + m_passwordComponent = maybePasswordComponent.releaseReturnValue(); + + auto hostnameEncodingCallbackType = isHostnamePatternIPv6(processedInit.hostname) ? EncodingCallbackType::IPv6Host : EncodingCallbackType::Host; + auto maybeHostnameComponent = URLPatternUtilities::URLPatternComponent::compile(vm, processedInit.hostname, hostnameEncodingCallbackType, URLPatternUtilities::URLPatternStringOptions { .delimiterCodepoint = "."_s }); + if (maybeHostnameComponent.hasException()) + return maybeHostnameComponent.releaseException(); + m_hostnameComponent = maybeHostnameComponent.releaseReturnValue(); + + auto maybePortComponent = URLPatternUtilities::URLPatternComponent::compile(vm, processedInit.port, EncodingCallbackType::Port, URLPatternUtilities::URLPatternStringOptions {}); + if (maybePortComponent.hasException()) + return maybePortComponent.releaseException(); + m_portComponent = maybePortComponent.releaseReturnValue(); + + URLPatternUtilities::URLPatternStringOptions compileOptions { .ignoreCase = options.ignoreCase }; + + auto maybePathnameComponent = m_protocolComponent.matchSpecialSchemeProtocol(context) + ? URLPatternUtilities::URLPatternComponent::compile(vm, processedInit.pathname, EncodingCallbackType::Path, URLPatternUtilities::URLPatternStringOptions { "/"_s, "/"_s, options.ignoreCase }) + : URLPatternUtilities::URLPatternComponent::compile(vm, processedInit.pathname, EncodingCallbackType::OpaquePath, compileOptions); + if (maybePathnameComponent.hasException()) + return maybePathnameComponent.releaseException(); + m_pathnameComponent = maybePathnameComponent.releaseReturnValue(); + + auto maybeSearchComponent = URLPatternUtilities::URLPatternComponent::compile(vm, processedInit.search, EncodingCallbackType::Search, compileOptions); + if (maybeSearchComponent.hasException()) + return maybeSearchComponent.releaseException(); + m_searchComponent = maybeSearchComponent.releaseReturnValue(); + + auto maybeHashComponent = URLPatternUtilities::URLPatternComponent::compile(vm, processedInit.hash, EncodingCallbackType::Hash, compileOptions); + if (maybeHashComponent.hasException()) + return maybeHashComponent.releaseException(); + m_hashComponent = maybeHashComponent.releaseReturnValue(); + + return {}; +} + +static inline void matchHelperAssignInputsFromURL(const URL& input, String& protocol, String& username, String& password, String& hostname, String& port, String& pathname, String& search, String& hash) +{ + protocol = input.protocol().toString(); + username = input.user(); + password = input.password(); + hostname = input.host().toString(); + port = input.port() ? String::number(*input.port()) : emptyString(); + pathname = input.path().toString(); + search = input.query().toString(); + hash = input.fragmentIdentifier().toString(); +} + +static inline void matchHelperAssignInputsFromInit(const URLPatternInit& input, String& protocol, String& username, String& password, String& hostname, String& port, String& pathname, String& search, String& hash) +{ + protocol = input.protocol; + username = input.username; + password = input.password; + hostname = input.hostname; + port = input.port; + pathname = input.pathname; + search = input.search; + hash = input.hash; +} + +// https://urlpattern.spec.whatwg.org/#url-pattern-match +ExceptionOr> URLPattern::match(ScriptExecutionContext& context, Variant&& input, String&& baseURLString) const +{ + URLPatternResult result; + String protocol, username, password, hostname, port, pathname, search, hash; + + if (URL* inputURL = std::get_if(&input)) { + ASSERT(!inputURL->isEmpty() && inputURL->isValid()); + matchHelperAssignInputsFromURL(*inputURL, protocol, username, password, hostname, port, pathname, search, hash); + result.inputs = Vector { String { inputURL->string() } }; + } else { + URLPatternInput* inputPattern = std::get_if(&input); + result.inputs.append(*inputPattern); + + auto hasError = WTF::switchOn(*inputPattern, [&](const URLPatternInit& value) -> ExceptionOr { + if (!baseURLString.isNull()) + return Exception { ExceptionCode::TypeError, "Base URL string is provided with a URLPatternInit. If URLPatternInit is provided, please use URLPatternInit.baseURL property instead"_s }; + + URLPatternInit initCopy = value; + auto maybeResult = processInit(WTFMove(initCopy), BaseURLStringType::URL); + if (maybeResult.hasException()) + return true; + + matchHelperAssignInputsFromInit(maybeResult.releaseReturnValue(), protocol, username, password, hostname, port, pathname, search, hash); + return false; }, [&](const String& value) -> ExceptionOr { + URL baseURL; + if (!baseURLString.isNull()) { + baseURL = URL { baseURLString }; + if (!baseURL.isValid()) + return true; + result.inputs.append(baseURLString); + } + URL url { baseURL, value }; + if (!url.isValid()) + return true; + + matchHelperAssignInputsFromURL(url, protocol, username, password, hostname, port, pathname, search, hash); + return false; }); + + if (hasError.hasException()) + return hasError.releaseException(); + if (hasError.returnValue()) + return { std::nullopt }; + } + + auto protocolExecResult = m_protocolComponent.componentExec(context, protocol); + if (protocolExecResult.isNull() || protocolExecResult.isUndefined()) + return { std::nullopt }; + + auto* globalObject = context.globalObject(); + if (!globalObject) + return { std::nullopt }; + result.protocol = m_protocolComponent.createComponentMatchResult(globalObject, WTFMove(protocol), protocolExecResult); + + auto usernameExecResult = m_usernameComponent.componentExec(context, username); + if (usernameExecResult.isNull() || usernameExecResult.isUndefined()) + return { std::nullopt }; + result.username = m_usernameComponent.createComponentMatchResult(globalObject, WTFMove(username), usernameExecResult); + + auto passwordExecResult = m_passwordComponent.componentExec(context, password); + if (passwordExecResult.isNull() || passwordExecResult.isUndefined()) + return { std::nullopt }; + result.password = m_passwordComponent.createComponentMatchResult(globalObject, WTFMove(password), passwordExecResult); + + auto hostnameExecResult = m_hostnameComponent.componentExec(context, hostname); + if (hostnameExecResult.isNull() || hostnameExecResult.isUndefined()) + return { std::nullopt }; + result.hostname = m_hostnameComponent.createComponentMatchResult(globalObject, WTFMove(hostname), hostnameExecResult); + + auto pathnameExecResult = m_pathnameComponent.componentExec(context, pathname); + if (pathnameExecResult.isNull() || pathnameExecResult.isUndefined()) + return { std::nullopt }; + result.pathname = m_pathnameComponent.createComponentMatchResult(globalObject, WTFMove(pathname), pathnameExecResult); + + auto portExecResult = m_portComponent.componentExec(context, port); + if (portExecResult.isNull() || portExecResult.isUndefined()) + return { std::nullopt }; + result.port = m_portComponent.createComponentMatchResult(globalObject, WTFMove(port), portExecResult); + + auto searchExecResult = m_searchComponent.componentExec(context, search); + if (searchExecResult.isNull() || searchExecResult.isUndefined()) + return { std::nullopt }; + result.search = m_searchComponent.createComponentMatchResult(globalObject, WTFMove(search), searchExecResult); + + auto hashExecResult = m_hashComponent.componentExec(context, hash); + if (hashExecResult.isNull() || hashExecResult.isUndefined()) + return { std::nullopt }; + result.hash = m_hashComponent.createComponentMatchResult(globalObject, WTFMove(hash), hashExecResult); + + return { result }; +} + +// https://urlpattern.spec.whatwg.org/#url-pattern-has-regexp-groups +bool URLPattern::hasRegExpGroups() const +{ + return m_protocolComponent.hasRegexGroupsFromPartList() + || m_usernameComponent.hasRegexGroupsFromPartList() + || m_passwordComponent.hasRegexGroupsFromPartList() + || m_hostnameComponent.hasRegexGroupsFromPartList() + || m_pathnameComponent.hasRegexGroupsFromPartList() + || m_portComponent.hasRegexGroupsFromPartList() + || m_searchComponent.hasRegexGroupsFromPartList() + || m_hashComponent.hasRegexGroupsFromPartList(); +} + +} diff --git a/src/bun.js/bindings/webcore/URLPattern.h b/src/bun.js/bindings/webcore/URLPattern.h new file mode 100644 index 0000000000..838eb24596 --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPattern.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "root.h" +#include "URLPatternComponent.h" +#include "URLPatternInit.h" +#include +#include +#include +#include +#include + +namespace WebCore { + +class ScriptExecutionContext; +struct URLPatternOptions; +struct URLPatternResult; +template class ExceptionOr; + +enum class BaseURLStringType : bool { Pattern, + URL }; + +namespace URLPatternUtilities { +class URLPatternComponent; +} + +class URLPattern final : public RefCounted { + WTF_MAKE_TZONE_OR_ISO_ALLOCATED(URLPattern); + +public: + using URLPatternInput = Variant; + + static ExceptionOr> create(ScriptExecutionContext&, URLPatternInput&&, String&& baseURL, URLPatternOptions&&); + static ExceptionOr> create(ScriptExecutionContext&, std::optional&&, URLPatternOptions&&); + + using Compatible = Variant>; + static ExceptionOr> create(ScriptExecutionContext&, Compatible&&, const String&); + + ~URLPattern(); + + ExceptionOr test(ScriptExecutionContext&, std::optional&&, String&& baseURL) const; + + ExceptionOr> exec(ScriptExecutionContext&, std::optional&&, String&& baseURL) const; + + const String& protocol() const { return m_protocolComponent.patternString(); } + const String& username() const { return m_usernameComponent.patternString(); } + const String& password() const { return m_passwordComponent.patternString(); } + const String& hostname() const { return m_hostnameComponent.patternString(); } + const String& port() const { return m_portComponent.patternString(); } + const String& pathname() const { return m_pathnameComponent.patternString(); } + const String& search() const { return m_searchComponent.patternString(); } + const String& hash() const { return m_hashComponent.patternString(); } + + bool hasRegExpGroups() const; + +private: + URLPattern(); + ExceptionOr compileAllComponents(ScriptExecutionContext&, URLPatternInit&&, const URLPatternOptions&); + ExceptionOr> match(ScriptExecutionContext&, Variant&&, String&& baseURLString) const; + + URLPatternUtilities::URLPatternComponent m_protocolComponent; + URLPatternUtilities::URLPatternComponent m_usernameComponent; + URLPatternUtilities::URLPatternComponent m_passwordComponent; + URLPatternUtilities::URLPatternComponent m_hostnameComponent; + URLPatternUtilities::URLPatternComponent m_pathnameComponent; + URLPatternUtilities::URLPatternComponent m_portComponent; + URLPatternUtilities::URLPatternComponent m_searchComponent; + URLPatternUtilities::URLPatternComponent m_hashComponent; +}; + +} diff --git a/src/bun.js/bindings/webcore/URLPattern.idl b/src/bun.js/bindings/webcore/URLPattern.idl new file mode 100644 index 0000000000..d3570cc859 --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPattern.idl @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of Apple Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// https://urlpattern.spec.whatwg.org/#urlpattern + +typedef (USVString or URLPatternInit) URLPatternInput; + +[ + EnabledBySetting=URLPatternAPIEnabled, + Exposed=(Window,Worker) +] interface URLPattern { + [CallWith=CurrentScriptExecutionContext] constructor(URLPatternInput input, USVString baseURL, optional URLPatternOptions options); + [CallWith=CurrentScriptExecutionContext] constructor(optional URLPatternInput input, optional URLPatternOptions options); + + [CallWith=CurrentScriptExecutionContext] boolean test(optional URLPatternInput input, optional USVString baseURL); + + [CallWith=CurrentScriptExecutionContext] URLPatternResult? exec(optional URLPatternInput input, optional USVString baseURL); + + readonly attribute USVString protocol; + readonly attribute USVString username; + readonly attribute USVString password; + readonly attribute USVString hostname; + readonly attribute USVString port; + readonly attribute USVString pathname; + readonly attribute USVString search; + readonly attribute USVString hash; + + readonly attribute boolean hasRegExpGroups; +}; diff --git a/src/bun.js/bindings/webcore/URLPatternCanonical.cpp b/src/bun.js/bindings/webcore/URLPatternCanonical.cpp new file mode 100644 index 0000000000..35a2f56f7c --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternCanonical.cpp @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "URLPatternCanonical.h" + +#include "ExceptionOr.h" +#include "URLDecomposition.h" +#include "URLPattern.h" +#include +#include +#include + +namespace WebCore { + +static constexpr auto dummyURLCharacters { "https://w/"_s }; + +static bool isValidIPv6HostCodePoint(auto codepoint) +{ + static constexpr std::array validSpecialCodepoints { '[', ']', ':' }; + return isASCIIHexDigit(codepoint) || std::find(validSpecialCodepoints.begin(), validSpecialCodepoints.end(), codepoint) != validSpecialCodepoints.end(); +} + +// https://urlpattern.spec.whatwg.org/#is-an-absolute-pathname +bool isAbsolutePathname(StringView input, BaseURLStringType inputType) +{ + if (input.isEmpty()) + return false; + + if (input[0] == '/') + return true; + + if (inputType == BaseURLStringType::URL) + return false; + + if (input.length() < 2) + return false; + + if (input.startsWith("\\/"_s)) + return true; + + if (input.startsWith("{/"_s)) + return true; + + return false; +} + +// https://urlpattern.spec.whatwg.org/#canonicalize-a-protocol, combined with https://urlpattern.spec.whatwg.org/#process-protocol-for-init +ExceptionOr canonicalizeProtocol(StringView value, BaseURLStringType valueType) +{ + if (value.isEmpty()) + return value.toString(); + + auto strippedValue = value.endsWith(':') ? value.left(value.length() - 1) : value; + + if (valueType == BaseURLStringType::Pattern) + return strippedValue.toString(); + + URL dummyURL(makeString(strippedValue, "://w/"_s)); + + if (!dummyURL.isValid()) + return Exception { ExceptionCode::TypeError, "Invalid input to canonicalize a URL protocol string."_s }; + + return dummyURL.protocol().toString(); +} + +// https://urlpattern.spec.whatwg.org/#canonicalize-a-username, combined with https://urlpattern.spec.whatwg.org/#process-username-for-init +String canonicalizeUsername(StringView value, BaseURLStringType valueType) +{ + if (value.isEmpty()) + return value.toString(); + + if (valueType == BaseURLStringType::Pattern) + return value.toString(); + + URL dummyURL(dummyURLCharacters); + dummyURL.setUser(value); + + return dummyURL.encodedUser().toString(); +} + +// https://urlpattern.spec.whatwg.org/#canonicalize-a-password, combined with https://urlpattern.spec.whatwg.org/#process-password-for-init +String canonicalizePassword(StringView value, BaseURLStringType valueType) +{ + if (value.isEmpty()) + return value.toString(); + + if (valueType == BaseURLStringType::Pattern) + return value.toString(); + + URL dummyURL(dummyURLCharacters); + dummyURL.setPassword(value); + + return dummyURL.encodedPassword().toString(); +} + +// https://urlpattern.spec.whatwg.org/#canonicalize-a-hostname, combined with https://urlpattern.spec.whatwg.org/#process-hostname-for-init +ExceptionOr canonicalizeHostname(StringView value, BaseURLStringType valueType) +{ + if (value.isEmpty()) + return value.toString(); + + if (valueType == BaseURLStringType::Pattern) + return value.toString(); + + URL dummyURL(dummyURLCharacters); + if (!dummyURL.setHost(value)) + return Exception { ExceptionCode::TypeError, "Invalid input to canonicalize a URL host string."_s }; + + return dummyURL.host().toString(); +} + +// https://urlpattern.spec.whatwg.org/#canonicalize-an-ipv6-hostname +ExceptionOr canonicalizeIPv6Hostname(StringView value, BaseURLStringType valueType) +{ + if (valueType == BaseURLStringType::Pattern) + return value.toString(); + + StringBuilder result; + result.reserveCapacity(value.length()); + + for (auto codepoint : value.codePoints()) { + if (!isValidIPv6HostCodePoint(codepoint)) + return Exception { ExceptionCode::TypeError, "Invalid input to canonicalize a URL IPv6 host string."_s }; + + result.append(toASCIILower(codepoint)); + } + + return String { result.toString() }; +} + +// https://urlpattern.spec.whatwg.org/#canonicalize-a-port, combined with https://urlpattern.spec.whatwg.org/#process-port-for-init +ExceptionOr canonicalizePort(StringView portValue, StringView protocolValue, BaseURLStringType portValueType) +{ + if (portValue.isEmpty()) + return portValue.toString(); + + if (portValueType == BaseURLStringType::Pattern) + return portValue.toString(); + + auto maybePort = URLDecomposition::parsePort(portValue, protocolValue); + if (!maybePort) + return Exception { ExceptionCode::TypeError, "Invalid input to canonicalize a URL port string."_s }; + + auto maybePortNumber = *maybePort; + if (!maybePortNumber) + return String { emptyString() }; + + return String::number(*maybePortNumber); +} + +// https://urlpattern.spec.whatwg.org/#canonicalize-an-opaque-pathname +ExceptionOr canonicalizeOpaquePathname(StringView value) +{ + if (value.isEmpty()) + return value.toString(); + + URL dummyURL(makeString("a:"_s, value)); + + if (!dummyURL.isValid()) + return Exception { ExceptionCode::TypeError, "Invalid input to canonicalize a URL opaque path string."_s }; + + return dummyURL.path().toString(); +} + +// https://urlpattern.spec.whatwg.org/#canonicalize-a-pathname +ExceptionOr canonicalizePathname(StringView pathnameValue) +{ + if (pathnameValue.isEmpty()) + return pathnameValue.toString(); + + bool hasLeadingSlash = pathnameValue[0] == '/'; + String maybeAddSlashPrefix = hasLeadingSlash ? pathnameValue.toString() : makeString("/-"_s, pathnameValue); + + // FIXME: Set state override to State::PathStart after URLParser supports state override. + URL dummyURL(dummyURLCharacters); + dummyURL.setPath(maybeAddSlashPrefix); + ASSERT(dummyURL.isValid()); + + auto result = dummyURL.path(); + if (!hasLeadingSlash) + result = result.substring(2); + + return result.toString(); +} + +// https://urlpattern.spec.whatwg.org/#process-pathname-for-init +ExceptionOr processPathname(StringView pathnameValue, const StringView protocolValue, BaseURLStringType pathnameValueType) +{ + if (pathnameValue.isEmpty()) + return pathnameValue.toString(); + + if (pathnameValueType == BaseURLStringType::Pattern) + return pathnameValue.toString(); + + if (WTF::URLParser::isSpecialScheme(protocolValue) || protocolValue.isEmpty()) + return canonicalizePathname(pathnameValue); + + return canonicalizeOpaquePathname(pathnameValue); +} + +// https://urlpattern.spec.whatwg.org/#canonicalize-a-search, combined with https://urlpattern.spec.whatwg.org/#process-search-for-init +ExceptionOr canonicalizeSearch(StringView value, BaseURLStringType valueType) +{ + if (value.isEmpty()) + return value.toString(); + + auto strippedValue = value[0] == '?' ? value.substring(1) : value; + + if (valueType == BaseURLStringType::Pattern) + return strippedValue.toString(); + + URL dummyURL(dummyURLCharacters); + dummyURL.setQuery(strippedValue); + ASSERT(dummyURL.isValid()); + + return dummyURL.query().toString(); +} + +// https://urlpattern.spec.whatwg.org/#canonicalize-a-hash, combined with https://urlpattern.spec.whatwg.org/#process-hash-for-init +ExceptionOr canonicalizeHash(StringView value, BaseURLStringType valueType) +{ + if (value.isEmpty()) + return value.toString(); + + auto strippedValue = value[0] == '#' ? value.substring(1) : value; + + if (valueType == BaseURLStringType::Pattern) + return strippedValue.toString(); + + URL dummyURL(dummyURLCharacters); + dummyURL.setFragmentIdentifier(strippedValue); + ASSERT(dummyURL.isValid()); + + return dummyURL.fragmentIdentifier().toString(); +} + +ExceptionOr callEncodingCallback(EncodingCallbackType type, StringView input) +{ + switch (type) { + case EncodingCallbackType::Protocol: + return canonicalizeProtocol(input, BaseURLStringType::URL); + case EncodingCallbackType::Username: + return canonicalizeUsername(input, BaseURLStringType::URL); + case EncodingCallbackType::Password: + return canonicalizePassword(input, BaseURLStringType::URL); + case EncodingCallbackType::Host: + return canonicalizeHostname(input, BaseURLStringType::URL); + case EncodingCallbackType::IPv6Host: + return canonicalizeIPv6Hostname(input, BaseURLStringType::URL); + case EncodingCallbackType::Port: + return canonicalizePort(input, {}, BaseURLStringType::URL); + case EncodingCallbackType::Path: + return canonicalizePathname(input); + case EncodingCallbackType::OpaquePath: + return canonicalizeOpaquePathname(input); + case EncodingCallbackType::Search: + return canonicalizeSearch(input, BaseURLStringType::URL); + case EncodingCallbackType::Hash: + return canonicalizeHash(input, BaseURLStringType::URL); + default: + ASSERT_NOT_REACHED(); + return Exception { ExceptionCode::TypeError, "Invalid input type for encoding callback."_s }; + } +} + +} diff --git a/src/bun.js/bindings/webcore/URLPatternCanonical.h b/src/bun.js/bindings/webcore/URLPatternCanonical.h new file mode 100644 index 0000000000..690fc5b6bf --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternCanonical.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include + +namespace WebCore { + +template class ExceptionOr; + +enum class BaseURLStringType : bool; +enum class EncodingCallbackType : uint8_t { Protocol, + Username, + Password, + Host, + IPv6Host, + Port, + Path, + OpaquePath, + Search, + Hash }; + +bool isAbsolutePathname(StringView input, BaseURLStringType inputType); +ExceptionOr canonicalizeProtocol(StringView, BaseURLStringType valueType); +String canonicalizeUsername(StringView value, BaseURLStringType valueType); +String canonicalizePassword(StringView value, BaseURLStringType valueType); +ExceptionOr canonicalizeHostname(StringView value, BaseURLStringType valueType); +ExceptionOr canonicalizeIPv6Hostname(StringView value, BaseURLStringType valueType); +ExceptionOr canonicalizePort(StringView portValue, StringView protocolValue, BaseURLStringType portValueType); +ExceptionOr processPathname(StringView pathnameValue, const StringView protocolValue, BaseURLStringType pathnameValueType); +ExceptionOr canonicalizePathname(StringView pathnameValue); +ExceptionOr canonicalizeOpaquePathname(StringView value); +ExceptionOr canonicalizeSearch(StringView value, BaseURLStringType valueType); +ExceptionOr canonicalizeHash(StringView value, BaseURLStringType valueType); +ExceptionOr callEncodingCallback(EncodingCallbackType, StringView input); +} diff --git a/src/bun.js/bindings/webcore/URLPatternComponent.cpp b/src/bun.js/bindings/webcore/URLPatternComponent.cpp new file mode 100644 index 0000000000..cb2c139aa6 --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternComponent.cpp @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "URLPatternComponent.h" + +#include "ExceptionOr.h" +#include "ScriptExecutionContext.h" +#include "URLPatternCanonical.h" +#include "URLPatternParser.h" +#include "URLPatternResult.h" +#include +#include +#include +#include + +namespace WebCore { +using namespace JSC; +namespace URLPatternUtilities { + +URLPatternComponent::URLPatternComponent(String&& patternString, JSC::Strong&& regex, Vector&& groupNameList, bool hasRegexpGroupsFromPartsList) + : m_patternString(WTFMove(patternString)) + , m_regularExpression(WTFMove(regex)) + , m_groupNameList(WTFMove(groupNameList)) + , m_hasRegexGroupsFromPartList(hasRegexpGroupsFromPartsList) +{ +} + +// https://urlpattern.spec.whatwg.org/#compile-a-component +ExceptionOr URLPatternComponent::compile(Ref vm, StringView input, EncodingCallbackType type, const URLPatternStringOptions& options) +{ + auto maybePartList = URLPatternParser::parse(input, options, type); + if (maybePartList.hasException()) + return maybePartList.releaseException(); + Vector partList = maybePartList.releaseReturnValue(); + + auto [regularExpressionString, nameList] = generateRegexAndNameList(partList, options); + + OptionSet flags = { JSC::Yarr::Flags::UnicodeSets }; + if (options.ignoreCase) + flags.add(JSC::Yarr::Flags::IgnoreCase); + + JSC::RegExp* regularExpression = JSC::RegExp::create(vm, regularExpressionString, flags); + if (!regularExpression->isValid()) + return Exception { ExceptionCode::TypeError, "Unable to create RegExp object regular expression from provided URLPattern string."_s }; + + String patternString = generatePatternString(partList, options); + + bool hasRegexGroups = partList.containsIf([](auto& part) { + return part.type == PartType::Regexp; + }); + + return URLPatternComponent { WTFMove(patternString), JSC::Strong { vm, regularExpression }, WTFMove(nameList), hasRegexGroups }; +} + +// https://urlpattern.spec.whatwg.org/#protocol-component-matches-a-special-scheme +bool URLPatternComponent::matchSpecialSchemeProtocol(ScriptExecutionContext& context) const +{ + Ref vm = context.vm(); + JSC::JSLockHolder lock(vm); + + static constexpr std::array specialSchemeList { "ftp"_s, "file"_s, "http"_s, "https"_s, "ws"_s, "wss"_s }; + auto contextObject = context.globalObject(); + if (!contextObject) + return false; + auto protocolRegex = JSC::RegExpObject::create(vm, contextObject->regExpStructure(), m_regularExpression.get(), true); + + auto isSchemeMatch = std::ranges::find_if(specialSchemeList, [context = Ref { context }, &vm, &protocolRegex](const String& scheme) { + auto maybeMatch = protocolRegex->exec(context->globalObject(), JSC::jsString(vm, scheme)); + return !maybeMatch.isNull(); + }); + + return isSchemeMatch != specialSchemeList.end(); +} + +JSC::JSValue URLPatternComponent::componentExec(ScriptExecutionContext& context, StringView comparedString) const +{ + Ref vm = context.vm(); + JSC::JSLockHolder lock(vm); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + auto contextObject = context.globalObject(); + if (!contextObject) { + throwTypeError(contextObject, throwScope, "URLPattern execution requires a valid execution context"_s); + return {}; + } + auto regex = JSC::RegExpObject::create(vm, contextObject->regExpStructure(), m_regularExpression.get(), true); + return regex->exec(contextObject, JSC::jsString(vm, comparedString)); +} + +// https://urlpattern.spec.whatwg.org/#create-a-component-match-result +URLPatternComponentResult URLPatternComponent::createComponentMatchResult(JSC::JSGlobalObject* globalObject, String&& input, const JSC::JSValue& execResult) const +{ + URLPatternComponentResult::GroupsRecord groups; + + Ref vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + + auto lengthValue = execResult.get(globalObject, vm->propertyNames->length); + RETURN_IF_EXCEPTION(throwScope, {}); + auto length = lengthValue.toIntegerOrInfinity(globalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + ASSERT(length >= 0 && std::isfinite(length)); + + for (unsigned index = 1; index < length; ++index) { + auto match = execResult.get(globalObject, index); + RETURN_IF_EXCEPTION(throwScope, {}); + + Variant value; + if (!match.isNull() && !match.isUndefined()) { + value = match.toWTFString(globalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + } + + size_t groupIndex = index - 1; + String groupName = groupIndex < m_groupNameList.size() ? m_groupNameList[groupIndex] : emptyString(); + groups.append(URLPatternComponentResult::NameMatchPair { WTFMove(groupName), WTFMove(value) }); + } + + return URLPatternComponentResult { !input.isEmpty() ? WTFMove(input) : emptyString(), WTFMove(groups) }; +} + +} +} diff --git a/src/bun.js/bindings/webcore/URLPatternComponent.h b/src/bun.js/bindings/webcore/URLPatternComponent.h new file mode 100644 index 0000000000..6a4d7a5e04 --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternComponent.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include + +namespace JSC { +class RegExp; +class VM; +class JSValue; +} + +namespace WebCore { + +class ScriptExecutionContext; +struct URLPatternComponentResult; +enum class EncodingCallbackType : uint8_t; +template class ExceptionOr; + +namespace URLPatternUtilities { +struct URLPatternStringOptions; + +class URLPatternComponent { +public: + static ExceptionOr compile(Ref, StringView, EncodingCallbackType, const URLPatternStringOptions&); + const String& patternString() const { return m_patternString; } + bool hasRegexGroupsFromPartList() const { return m_hasRegexGroupsFromPartList; } + bool matchSpecialSchemeProtocol(ScriptExecutionContext&) const; + JSC::JSValue componentExec(ScriptExecutionContext&, StringView) const; + URLPatternComponentResult createComponentMatchResult(JSC::JSGlobalObject*, String&& input, const JSC::JSValue& execResult) const; + URLPatternComponent() = default; + +private: + URLPatternComponent(String&&, JSC::Strong&&, Vector&&, bool); + + String m_patternString; + JSC::Strong m_regularExpression; + Vector m_groupNameList; + bool m_hasRegexGroupsFromPartList { false }; +}; + +} +} diff --git a/src/bun.js/bindings/webcore/URLPatternConstructorStringParser.cpp b/src/bun.js/bindings/webcore/URLPatternConstructorStringParser.cpp new file mode 100644 index 0000000000..80d405e820 --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternConstructorStringParser.cpp @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "URLPatternConstructorStringParser.h" + +#include "ExceptionOr.h" +#include "URLPatternCanonical.h" +#include "URLPatternComponent.h" +#include "URLPatternInit.h" +#include "URLPatternParser.h" +#include "URLPatternTokenizer.h" + +namespace WebCore { +using namespace JSC; + +URLPatternConstructorStringParser::URLPatternConstructorStringParser(String&& input) + : m_input(WTFMove(input)) +{ +} + +// https://urlpattern.spec.whatwg.org/#rewind +void URLPatternConstructorStringParser::rewind() +{ + m_tokenIndex = m_componentStart; + m_tokenIncrement = 0; +} + +// https://urlpattern.spec.whatwg.org/#get-a-safe-token +const URLPatternUtilities::Token& URLPatternConstructorStringParser::getSafeToken(size_t index) const +{ + if (index < m_tokenList.size()) + return m_tokenList[index]; + + ASSERT(m_tokenList.last().type == URLPatternUtilities::TokenType::End); + return m_tokenList.last(); +} + +// https://urlpattern.spec.whatwg.org/#is-a-non-special-pattern-char +bool URLPatternConstructorStringParser::isNonSpecialPatternCharacter(size_t index, char value) const +{ + auto token = getSafeToken(index); + + return token.value.length() == 1 && token.value[0] == value + && (token.type == URLPatternUtilities::TokenType::Char + || token.type == URLPatternUtilities::TokenType::EscapedChar + || token.type == URLPatternUtilities::TokenType::InvalidChar); +} + +// https://urlpattern.spec.whatwg.org/#is-a-search-prefix +bool URLPatternConstructorStringParser::isSearchPrefix() const +{ + if (isNonSpecialPatternCharacter(m_tokenIndex, '?')) + return true; + if (m_tokenList[m_tokenIndex].value != "?"_s) + return false; + + if (m_tokenIndex == 0) + return true; + + size_t previousIndex = m_tokenIndex - 1; + auto previousToken = getSafeToken(previousIndex); + if (previousToken.type == URLPatternUtilities::TokenType::Name + || previousToken.type == URLPatternUtilities::TokenType::Regexp + || previousToken.type == URLPatternUtilities::TokenType::Close + || previousToken.type == URLPatternUtilities::TokenType::Asterisk) { + return false; + } + return true; +} + +// https://urlpattern.spec.whatwg.org/#next-is-authority-slashes +bool URLPatternConstructorStringParser::isAuthoritySlashesNext() const +{ + if (!isNonSpecialPatternCharacter(m_tokenIndex + 1, '/')) + return false; + if (!isNonSpecialPatternCharacter(m_tokenIndex + 2, '/')) + return false; + return true; +} + +// https://urlpattern.spec.whatwg.org/#make-a-component-string +String URLPatternConstructorStringParser::makeComponentString() const +{ + const auto& token = m_tokenList[m_tokenIndex]; + + auto componentStartToken = getSafeToken(m_componentStart); + auto componentStartIndex = *componentStartToken.index; + + return m_input.substring(componentStartIndex, *token.index - componentStartIndex).toString(); +} + +static inline void setInitComponentFromState(URLPatternInit& init, URLPatternConstructorStringParserState state, String&& componentString) +{ + switch (state) { + case URLPatternConstructorStringParserState::Protocol: + init.protocol = WTFMove(componentString); + break; + case URLPatternConstructorStringParserState::Username: + init.username = WTFMove(componentString); + break; + case URLPatternConstructorStringParserState::Password: + init.password = WTFMove(componentString); + break; + case URLPatternConstructorStringParserState::Hostname: + init.hostname = WTFMove(componentString); + break; + case URLPatternConstructorStringParserState::Port: + init.port = WTFMove(componentString); + break; + case URLPatternConstructorStringParserState::Pathname: + init.pathname = WTFMove(componentString); + break; + case URLPatternConstructorStringParserState::Search: + init.search = WTFMove(componentString); + break; + case URLPatternConstructorStringParserState::Hash: + init.hash = WTFMove(componentString); + break; + default: + break; + } +} + +// https://urlpattern.spec.whatwg.org/#compute-protocol-matches-a-special-scheme-flag +ExceptionOr URLPatternConstructorStringParser::computeProtocolMatchSpecialSchemeFlag(ScriptExecutionContext& context) +{ + Ref vm = context.vm(); + JSC::JSLockHolder lock(vm); + + auto maybeProtocolComponent = URLPatternUtilities::URLPatternComponent::compile(vm, makeComponentString(), EncodingCallbackType::Protocol, URLPatternUtilities::URLPatternStringOptions {}); + if (maybeProtocolComponent.hasException()) + return maybeProtocolComponent.releaseException(); + + auto protocolComponent = maybeProtocolComponent.releaseReturnValue(); + m_protocolMatchesSpecialSchemeFlag = protocolComponent.matchSpecialSchemeProtocol(context); + + return {}; +} + +// https://urlpattern.spec.whatwg.org/#change-state +void URLPatternConstructorStringParser::changeState(URLPatternConstructorStringParserState newState, size_t skip) +{ + if (m_state != URLPatternConstructorStringParserState::Init + && m_state != URLPatternConstructorStringParserState::Authority + && m_state != URLPatternConstructorStringParserState::Done) + setInitComponentFromState(m_result, m_state, makeComponentString()); + + if (m_state != URLPatternConstructorStringParserState::Init && newState != URLPatternConstructorStringParserState::Done) { + // Set init's hostname to empty if conditions are met. + static constexpr std::array validStateConditionsForEmptyHostname { URLPatternConstructorStringParserState::Protocol, URLPatternConstructorStringParserState::Authority, URLPatternConstructorStringParserState::Username, URLPatternConstructorStringParserState::Password }; + static constexpr std::array validNewStateConditionsForEmptyHostname { URLPatternConstructorStringParserState::Port, URLPatternConstructorStringParserState::Pathname, URLPatternConstructorStringParserState::Search, URLPatternConstructorStringParserState::Hash }; + if (std::ranges::find(validStateConditionsForEmptyHostname, m_state) != validStateConditionsForEmptyHostname.end() + && std::ranges::find(validNewStateConditionsForEmptyHostname, newState) != validNewStateConditionsForEmptyHostname.end() + && m_result.hostname.isNull()) { + m_result.hostname = emptyString(); + } + // Set init's pathname to empty if conditions are met. + static constexpr std::array validStateConditionsForEmptyPathname { URLPatternConstructorStringParserState::Protocol, URLPatternConstructorStringParserState::Authority, URLPatternConstructorStringParserState::Username, URLPatternConstructorStringParserState::Password, URLPatternConstructorStringParserState::Hostname, URLPatternConstructorStringParserState::Port }; + static constexpr std::array validNewStateConditionsForEmptyPathname { URLPatternConstructorStringParserState::Search, URLPatternConstructorStringParserState::Hash }; + if (std::ranges::find(validStateConditionsForEmptyPathname, m_state) != validStateConditionsForEmptyPathname.end() + && std::ranges::find(validNewStateConditionsForEmptyPathname, newState) != validNewStateConditionsForEmptyPathname.end() + && m_result.pathname.isNull()) { + m_result.pathname = m_protocolMatchesSpecialSchemeFlag ? "/"_s : emptyString(); + } + // Set init's search to empty if conditions are met. + static constexpr std::array validStateConditionsForEmptySearch { URLPatternConstructorStringParserState::Protocol, URLPatternConstructorStringParserState::Authority, URLPatternConstructorStringParserState::Username, URLPatternConstructorStringParserState::Password, URLPatternConstructorStringParserState::Hostname, URLPatternConstructorStringParserState::Port, URLPatternConstructorStringParserState::Pathname }; + if (std::ranges::find(validStateConditionsForEmptySearch, m_state) != validStateConditionsForEmptySearch.end() + && newState == URLPatternConstructorStringParserState::Hash + && m_result.search.isNull()) { + m_result.search = emptyString(); + } + } + + m_state = newState; + m_tokenIndex += skip; + m_componentStart = m_tokenIndex; + m_tokenIncrement = 0; +} + +void URLPatternConstructorStringParser::updateState(ScriptExecutionContext& context) +{ + switch (m_state) { + case URLPatternConstructorStringParserState::Init: + // Look for protocol prefix. + if (isNonSpecialPatternCharacter(m_tokenIndex, ':')) { + rewind(); + m_state = URLPatternConstructorStringParserState::Protocol; + } + break; + case URLPatternConstructorStringParserState::Protocol: + // Look for protocol prefix. + if (isNonSpecialPatternCharacter(m_tokenIndex, ':')) { + auto maybeMatchesSpecialSchemeProtocol = computeProtocolMatchSpecialSchemeFlag(context); + if (maybeMatchesSpecialSchemeProtocol.hasException()) + break; // FIXME: Return exceptions. + auto nextState = URLPatternConstructorStringParserState::Pathname; + auto skip = 1; + if (isAuthoritySlashesNext()) { + nextState = URLPatternConstructorStringParserState::Authority; + skip = 3; + } else if (m_protocolMatchesSpecialSchemeFlag) + nextState = URLPatternConstructorStringParserState::Authority; + changeState(nextState, skip); + } + break; + case URLPatternConstructorStringParserState::Authority: + // Look for identity terminator. + if (isNonSpecialPatternCharacter(m_tokenIndex, '@')) { + rewind(); + m_state = URLPatternConstructorStringParserState::Username; + } else if (isNonSpecialPatternCharacter(m_tokenIndex, '/') || isSearchPrefix() || isNonSpecialPatternCharacter(m_tokenIndex, '#')) { // Look for pathname start, search prefix or hash prefix. + rewind(); + m_state = URLPatternConstructorStringParserState::Hostname; + } + break; + case URLPatternConstructorStringParserState::Username: + // Look for password prefix. + if (isNonSpecialPatternCharacter(m_tokenIndex, ':')) + changeState(URLPatternConstructorStringParserState::Password, 1); + // Look for identity terminator. + else if (isNonSpecialPatternCharacter(m_tokenIndex, '@')) + changeState(URLPatternConstructorStringParserState::Hostname, 1); + break; + case URLPatternConstructorStringParserState::Password: + // Look for identity terminator. + if (isNonSpecialPatternCharacter(m_tokenIndex, '@')) + changeState(URLPatternConstructorStringParserState::Hostname, 1); + break; + case URLPatternConstructorStringParserState::Hostname: + // Look for an IPv6 open. + if (isNonSpecialPatternCharacter(m_tokenIndex, '[')) + ++m_hostnameIPv6BracketDepth; + // Look for an IPv6 close. + else if (isNonSpecialPatternCharacter(m_tokenIndex, ']') && m_hostnameIPv6BracketDepth > 0) + --m_hostnameIPv6BracketDepth; + // Look for port prefix. + else if (isNonSpecialPatternCharacter(m_tokenIndex, ':') && !m_hostnameIPv6BracketDepth) + changeState(URLPatternConstructorStringParserState::Port, 1); + // Look for pathname start. + else if (isNonSpecialPatternCharacter(m_tokenIndex, '/')) + changeState(URLPatternConstructorStringParserState::Pathname, 0); + // Look for search prefix. + else if (isSearchPrefix()) + changeState(URLPatternConstructorStringParserState::Search, 1); + // Look for hash prefix. + else if (isNonSpecialPatternCharacter(m_tokenIndex, '#')) + changeState(URLPatternConstructorStringParserState::Hash, 1); + break; + case URLPatternConstructorStringParserState::Port: + // Look for pathname start. + if (isNonSpecialPatternCharacter(m_tokenIndex, '/')) + changeState(URLPatternConstructorStringParserState::Pathname, 0); + else if (isSearchPrefix()) + changeState(URLPatternConstructorStringParserState::Search, 1); + // Look for hash prefix. + else if (isNonSpecialPatternCharacter(m_tokenIndex, '#')) + changeState(URLPatternConstructorStringParserState::Hash, 1); + break; + case URLPatternConstructorStringParserState::Pathname: + if (isSearchPrefix()) + changeState(URLPatternConstructorStringParserState::Search, 1); + // Look for hash prefix. + else if (isNonSpecialPatternCharacter(m_tokenIndex, '#')) + changeState(URLPatternConstructorStringParserState::Hash, 1); + break; + case URLPatternConstructorStringParserState::Search: + // Look for hash prefix. + if (isNonSpecialPatternCharacter(m_tokenIndex, '#')) + changeState(URLPatternConstructorStringParserState::Hash, 1); + break; + case URLPatternConstructorStringParserState::Hash: + break; + case URLPatternConstructorStringParserState::Done: + ASSERT_NOT_REACHED(); + break; + default: + break; + } +} + +void URLPatternConstructorStringParser::performParse(ScriptExecutionContext& context) +{ + while (m_tokenIndex < m_tokenList.size()) { + m_tokenIncrement = 1; + + if (m_tokenList[m_tokenIndex].type == URLPatternUtilities::TokenType::End) { + if (m_state == URLPatternConstructorStringParserState::Init) { + rewind(); + if (isNonSpecialPatternCharacter(m_tokenIndex, '#')) + changeState(URLPatternConstructorStringParserState::Hash, 1); + else if (isSearchPrefix()) + changeState(URLPatternConstructorStringParserState::Search, 1); + else + changeState(URLPatternConstructorStringParserState::Pathname, 0); + + m_tokenIndex += m_tokenIncrement; + continue; + } + if (m_state == URLPatternConstructorStringParserState::Authority) { + rewind(); + m_state = URLPatternConstructorStringParserState::Hostname; + m_tokenIndex += m_tokenIncrement; + continue; + } + + changeState(URLPatternConstructorStringParserState::Done, 0); + break; + } + + if (m_tokenList[m_tokenIndex].type == URLPatternUtilities::TokenType::Open) { + ++m_groupDepth; + ++m_tokenIndex; + continue; + } + + if (m_groupDepth) { + if (m_tokenList[m_tokenIndex].type == URLPatternUtilities::TokenType::Close) + --m_groupDepth; + else { + m_tokenIndex += m_tokenIncrement; + continue; + } + } + + updateState(context); + m_tokenIndex += m_tokenIncrement; + } + if (!m_result.hostname.isNull() && m_result.port.isNull()) + m_result.port = emptyString(); +} + +// https://urlpattern.spec.whatwg.org/#parse-a-constructor-string +ExceptionOr URLPatternConstructorStringParser::parse(ScriptExecutionContext& context) +{ + auto maybeTokenList = URLPatternUtilities::Tokenizer(m_input, URLPatternUtilities::TokenizePolicy::Lenient).tokenize(); + if (maybeTokenList.hasException()) + return maybeTokenList.releaseException(); + m_tokenList = maybeTokenList.releaseReturnValue(); + + performParse(context); + + return URLPatternInit { m_result }; +} + +} diff --git a/src/bun.js/bindings/webcore/URLPatternConstructorStringParser.h b/src/bun.js/bindings/webcore/URLPatternConstructorStringParser.h new file mode 100644 index 0000000000..925284707f --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternConstructorStringParser.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "ScriptExecutionContext.h" +#include "URLPatternInit.h" + +namespace WebCore { + +template class ExceptionOr; + +enum class EncodingCallbackType : uint8_t; + +namespace URLPatternUtilities { +struct Token; +enum class TokenType : uint8_t; +struct URLPatternStringOptions; +struct URLPatternInit; +} + +enum class URLPatternConstructorStringParserState : uint8_t { Init, + Protocol, + Authority, + Username, + Password, + Hostname, + Port, + Pathname, + Search, + Hash, + Done }; + +class URLPatternConstructorStringParser { +public: + explicit URLPatternConstructorStringParser(String&& input); + ExceptionOr parse(ScriptExecutionContext&); + +private: + void performParse(ScriptExecutionContext&); + void rewind(); + const URLPatternUtilities::Token& getSafeToken(size_t index) const; + bool isNonSpecialPatternCharacter(size_t index, char value) const; + bool isSearchPrefix() const; + bool isAuthoritySlashesNext() const; + String makeComponentString() const; + void changeState(URLPatternConstructorStringParserState, size_t skip); + void updateState(ScriptExecutionContext&); + ExceptionOr computeProtocolMatchSpecialSchemeFlag(ScriptExecutionContext&); + + StringView m_input; + Vector m_tokenList; + URLPatternInit m_result; + size_t m_componentStart { 0 }; + size_t m_tokenIndex { 0 }; + size_t m_tokenIncrement { 1 }; + size_t m_groupDepth { 0 }; + int m_hostnameIPv6BracketDepth { 0 }; + bool m_protocolMatchesSpecialSchemeFlag { false }; + URLPatternConstructorStringParserState m_state { URLPatternConstructorStringParserState::Init }; +}; + +} diff --git a/src/bun.js/bindings/webcore/URLPatternInit.h b/src/bun.js/bindings/webcore/URLPatternInit.h new file mode 100644 index 0000000000..786da7bfb0 --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternInit.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include + +namespace WebCore { + +struct URLPatternInit { + String protocol; + String username; + String password; + String hostname; + String port; + String pathname; + String search; + String hash; + String baseURL; +}; + +} diff --git a/src/bun.js/bindings/webcore/URLPatternInit.idl b/src/bun.js/bindings/webcore/URLPatternInit.idl new file mode 100644 index 0000000000..df4d10632b --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternInit.idl @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + + // https://urlpattern.spec.whatwg.org/#dictdef-urlpatterninit + +[ + JSGenerateToJSObject, + JSGenerateToNativeObject +] dictionary URLPatternInit { + USVString protocol; + USVString username; + USVString password; + USVString hostname; + USVString port; + USVString pathname; + USVString search; + USVString hash; + USVString baseURL; +}; diff --git a/src/bun.js/bindings/webcore/URLPatternOptions.h b/src/bun.js/bindings/webcore/URLPatternOptions.h new file mode 100644 index 0000000000..6b91a48054 --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternOptions.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +namespace WebCore { + +struct URLPatternOptions { + bool ignoreCase { false }; +}; + +} diff --git a/src/bun.js/bindings/webcore/URLPatternOptions.idl b/src/bun.js/bindings/webcore/URLPatternOptions.idl new file mode 100644 index 0000000000..77cb6cf4aa --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternOptions.idl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +// https://urlpattern.spec.whatwg.org/#dictdef-urlpatternoptions + +dictionary URLPatternOptions { + boolean ignoreCase = false; +}; diff --git a/src/bun.js/bindings/webcore/URLPatternParser.cpp b/src/bun.js/bindings/webcore/URLPatternParser.cpp new file mode 100644 index 0000000000..9224932aae --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternParser.cpp @@ -0,0 +1,553 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "URLPatternParser.h" + +#include "ExceptionOr.h" +#include "URLPatternCanonical.h" +#include "URLPatternTokenizer.h" +#include +#include + +namespace WebCore { +namespace URLPatternUtilities { + +URLPatternParser::URLPatternParser(EncodingCallbackType type, String&& segmentWildcardRegexp) + : m_callbackType(type) + , m_segmentWildcardRegexp(WTFMove(segmentWildcardRegexp)) +{ +} + +ExceptionOr URLPatternParser::performParse(const URLPatternStringOptions& options) +{ + ExceptionOr maybeFunctionException; + + while (m_index < m_tokenList.size()) { + auto charToken = tryToConsumeToken(TokenType::Char); + auto nameToken = tryToConsumeToken(TokenType::Name); + auto regexOrWildcardToken = tryToConsumeRegexOrWildcardToken(nameToken); + + if (!nameToken.isNull() || !regexOrWildcardToken.isNull()) { + String prefix; + + if (!charToken.isNull()) + prefix = charToken.value.toString(); + + if (!prefix.isEmpty() && prefix != options.prefixCodepoint) + m_pendingFixedValue.append(std::exchange(prefix, {})); + + maybeFunctionException = maybeAddPartFromPendingFixedValue(); + if (maybeFunctionException.hasException()) + return maybeFunctionException.releaseException(); + + auto modifierToken = tryToConsumeModifierToken(); + + maybeFunctionException = addPart(WTFMove(prefix), nameToken, regexOrWildcardToken, {}, modifierToken); + if (maybeFunctionException.hasException()) + return maybeFunctionException.releaseException(); + + continue; + } + + auto fixedToken = charToken; + + if (fixedToken.isNull()) + fixedToken = tryToConsumeToken(TokenType::EscapedChar); + + if (!fixedToken.isNull()) { + m_pendingFixedValue.append(WTFMove(fixedToken.value)); + + continue; + } + + auto openToken = tryToConsumeToken(TokenType::Open); + if (!openToken.isNull()) { + String prefix = consumeText(); + nameToken = tryToConsumeToken(TokenType::Name); + regexOrWildcardToken = tryToConsumeRegexOrWildcardToken(nameToken); + String suffix = consumeText(); + auto maybeCloseError = consumeRequiredToken(TokenType::Close); + if (maybeCloseError.hasException()) + return maybeCloseError.releaseException(); + auto modifierToken = tryToConsumeModifierToken(); + + maybeFunctionException = addPart(WTFMove(prefix), nameToken, regexOrWildcardToken, WTFMove(suffix), modifierToken); + if (maybeFunctionException.hasException()) + return maybeFunctionException.releaseException(); + + continue; + } + + maybeFunctionException = maybeAddPartFromPendingFixedValue(); + if (maybeFunctionException.hasException()) + return maybeFunctionException.releaseException(); + + auto maybeSyntaxError = consumeRequiredToken(TokenType::End); + if (maybeSyntaxError.hasException()) + return maybeSyntaxError.releaseException(); + } + + return {}; +} + +// https://urlpattern.spec.whatwg.org/#try-to-consume-a-token +Token URLPatternParser::tryToConsumeToken(TokenType type) +{ + if (m_index >= m_tokenList.size()) + return {}; + + auto& nextToken = m_tokenList[m_index]; + + if (nextToken.type != type) + return {}; + + ++m_index; + + return nextToken; +} + +// https://urlpattern.spec.whatwg.org/#try-to-consume-a-regexp-or-wildcard-token +Token URLPatternParser::tryToConsumeRegexOrWildcardToken(const Token& token) +{ + auto tokenResult = tryToConsumeToken(TokenType::Regexp); + + if (tokenResult.isNull() && token.isNull()) + tokenResult = tryToConsumeToken(TokenType::Asterisk); + + return tokenResult; +} + +// https://urlpattern.spec.whatwg.org/#try-to-consume-a-modifier-token +Token URLPatternParser::tryToConsumeModifierToken() +{ + auto token = tryToConsumeToken(TokenType::OtherModifier); + if (!token.isNull()) + return token; + + return tryToConsumeToken(TokenType::Asterisk); +} + +// https://urlpattern.spec.whatwg.org/#consume-text +String URLPatternParser::consumeText() +{ + StringBuilder result; + + while (true) { + auto consumedToken = tryToConsumeToken(TokenType::Char); + + if (consumedToken.isNull()) + consumedToken = tryToConsumeToken(TokenType::EscapedChar); + + if (consumedToken.isNull()) + break; + + result.append(consumedToken.value); + } + + return result.toString(); +} + +// https://urlpattern.spec.whatwg.org/#consume-a-required-token +ExceptionOr URLPatternParser::consumeRequiredToken(TokenType type) +{ + auto result = tryToConsumeToken(type); + + if (result.isNull()) + return Exception { ExceptionCode::TypeError, "Null token was produced when consuming a required token."_s }; + + return result; +} + +// https://urlpattern.spec.whatwg.org/#maybe-add-a-part-from-the-pending-fixed-value +ExceptionOr URLPatternParser::maybeAddPartFromPendingFixedValue() +{ + if (m_pendingFixedValue.isEmpty()) + return {}; + + auto encodedValue = callEncodingCallback(m_callbackType, m_pendingFixedValue.toString()); + m_pendingFixedValue.clear(); + + if (encodedValue.hasException()) + return encodedValue.releaseException(); + + m_partList.append(Part { .type = PartType::FixedText, .value = encodedValue.releaseReturnValue(), .modifier = Modifier::None }); + + return {}; +} + +// https://urlpattern.spec.whatwg.org/#add-a-part +ExceptionOr URLPatternParser::addPart(String&& prefix, const Token& nameToken, const Token& regexpOrWildcardToken, String&& suffix, const Token& modifierToken) +{ + Modifier modifier = Modifier::None; + + if (!modifierToken.isNull()) { + if (modifierToken.value == "?"_s) + modifier = Modifier::Optional; + else if (modifierToken.value == "*"_s) + modifier = Modifier::ZeroOrMore; + else if (modifierToken.value == "+"_s) + modifier = Modifier::OneOrMore; + } + + if (nameToken.isNull() && regexpOrWildcardToken.isNull() && modifier == Modifier::None) { + m_pendingFixedValue.append(WTFMove(prefix)); + + return {}; + } + + auto maybeFunctionException = maybeAddPartFromPendingFixedValue(); + if (maybeFunctionException.hasException()) + return maybeFunctionException.releaseException(); + + if (nameToken.isNull() && regexpOrWildcardToken.isNull()) { + ASSERT(suffix.isEmpty()); + + if (prefix.isEmpty()) + return {}; + + auto encodedValue = callEncodingCallback(m_callbackType, WTFMove(prefix)); + if (encodedValue.hasException()) + return encodedValue.releaseException(); + + m_partList.append(Part { .type = PartType::FixedText, .value = encodedValue.releaseReturnValue(), .modifier = modifier }); + + return {}; + } + + String regexValue; + + if (regexpOrWildcardToken.isNull()) + regexValue = m_segmentWildcardRegexp; + else if (regexpOrWildcardToken.type == TokenType::Asterisk) + regexValue = ".*"_s; + else + regexValue = regexpOrWildcardToken.value.toString(); + + PartType type = PartType::Regexp; + + if (regexValue == m_segmentWildcardRegexp) { + type = PartType::SegmentWildcard; + regexValue = {}; + } else if (regexValue == ".*"_s) { + type = PartType::FullWildcard; + regexValue = {}; + } + + String name; + + if (!nameToken.isNull()) + name = nameToken.value.toString(); + else if (!regexpOrWildcardToken.isNull()) { + name = String::number(m_nextNumericName); + ++m_nextNumericName; + } + + if (isDuplicateName(name)) + return Exception { ExceptionCode::TypeError, "Duplicate name token produced when adding to parser part list."_s }; + + auto encodedPrefix = callEncodingCallback(m_callbackType, WTFMove(prefix)); + if (encodedPrefix.hasException()) + return encodedPrefix.releaseException(); + + auto encodedSuffix = callEncodingCallback(m_callbackType, WTFMove(suffix)); + if (encodedSuffix.hasException()) + return encodedSuffix.releaseException(); + + m_partList.append(Part { type, WTFMove(regexValue), modifier, WTFMove(name), encodedPrefix.releaseReturnValue(), encodedSuffix.releaseReturnValue() }); + + return {}; +} + +// https://urlpattern.spec.whatwg.org/#is-a-duplicate-name +bool URLPatternParser::isDuplicateName(StringView name) const +{ + return m_partList.containsIf([&](auto& part) { + return part.name == name; + }); +} + +// https://urlpattern.spec.whatwg.org/#parse-a-pattern-string +ExceptionOr> URLPatternParser::parse(StringView patternStringInput, const URLPatternStringOptions& options, EncodingCallbackType type) +{ + URLPatternParser tokenParser { type, generateSegmentWildcardRegexp(options) }; + + auto maybeParserTokenList = Tokenizer(patternStringInput, TokenizePolicy::Strict).tokenize(); + if (maybeParserTokenList.hasException()) + return maybeParserTokenList.releaseException(); + tokenParser.setTokenList(maybeParserTokenList.releaseReturnValue()); + + auto maybePerformParseError = tokenParser.performParse(options); + if (maybePerformParseError.hasException()) + return maybePerformParseError.releaseException(); + + return tokenParser.takePartList(); +} + +// https://urlpattern.spec.whatwg.org/#generate-a-segment-wildcard-regexp +String generateSegmentWildcardRegexp(const URLPatternStringOptions& options) +{ + return makeString("[^"_s, escapeRegexString(options.delimiterCodepoint), "]+?"_s); +} + +template +static String escapeRegexStringForCharacters(std::span characters) +{ + static constexpr auto regexEscapeCharacters = std::to_array({ '.', '+', '*', '?', '^', '$', '{', '}', '(', ')', '[', ']', '|', '/', '\\' }); // NOLINT + + StringBuilder result; + result.reserveCapacity(characters.size()); + + for (auto character : characters) { + if (std::ranges::find(regexEscapeCharacters, character) != regexEscapeCharacters.end()) + result.append('\\'); + + result.append(character); + } + + return result.toString(); +} + +// https://urlpattern.spec.whatwg.org/#escape-a-regexp-string +String escapeRegexString(StringView input) +{ + // FIXME: Ensure input only contains ASCII based on spec after the parser (or tokenizer) knows to filter non-ASCII input. + + if (input.is8Bit()) + return escapeRegexStringForCharacters(input.span8()); + + return escapeRegexStringForCharacters(input.span16()); +} + +// https://urlpattern.spec.whatwg.org/#convert-a-modifier-to-a-string +ASCIILiteral convertModifierToString(Modifier modifier) +{ + switch (modifier) { + case Modifier::ZeroOrMore: + return "*"_s; + case Modifier::Optional: + return "?"_s; + case Modifier::OneOrMore: + return "+"_s; + default: + return {}; + } +} + +// https://urlpattern.spec.whatwg.org/#generate-a-regular-expression-and-name-list +std::pair> generateRegexAndNameList(const Vector& partList, const URLPatternStringOptions& options) +{ + StringBuilder result; + result.append('^'); + + Vector nameList; + + for (auto& part : partList) { + if (part.type == PartType::FixedText) { + if (part.modifier == Modifier::None) + result.append(escapeRegexString(part.value)); + else + result.append("(?:"_s, escapeRegexString(part.value), ')', convertModifierToString(part.modifier)); + + continue; + } + + ASSERT(!part.name.isEmpty()); + + nameList.append(part.name); + + String regexpValue; + + if (part.type == PartType::SegmentWildcard) + regexpValue = generateSegmentWildcardRegexp(options); + else if (part.type == PartType::FullWildcard) + regexpValue = ".*"_s; + else + regexpValue = part.value; + + if (part.prefix.isEmpty() && part.suffix.isEmpty()) { + if (part.modifier == Modifier::None || part.modifier == Modifier::Optional) + result.append('(', regexpValue, ')', convertModifierToString(part.modifier)); + else + result.append("((?:"_s, regexpValue, ')', convertModifierToString(part.modifier), ')'); + + continue; + } + + if (part.modifier == Modifier::None || part.modifier == Modifier::Optional) { + result.append("(?:"_s, escapeRegexString(part.prefix), '(', regexpValue, ')', escapeRegexString(part.suffix), ')', convertModifierToString(part.modifier)); + + continue; + } + + ASSERT(part.modifier == Modifier::ZeroOrMore || part.modifier == Modifier::OneOrMore); + ASSERT(!part.prefix.isEmpty() || !part.suffix.isEmpty()); + + result.append("(?:"_s, + escapeRegexString(part.prefix), + "((?:"_s, + regexpValue, + ")(?:"_s, + escapeRegexString(part.suffix), + escapeRegexString(part.prefix), + "(?:"_s, + regexpValue, + "))*)"_s, + escapeRegexString(part.suffix), + ')'); + + if (part.modifier == Modifier::ZeroOrMore) + result.append('?'); + } + + result.append('$'); + + return { result.toString(), WTFMove(nameList) }; +} + +// https://urlpattern.spec.whatwg.org/#generate-a-pattern-string +String generatePatternString(const Vector& partList, const URLPatternStringOptions& options) +{ + StringBuilder result; + + for (size_t index = 0; index < partList.size(); ++index) { + auto& part = partList[index]; + + std::optional previousPart; + if (index > 0) + previousPart = partList[index - 1]; + + std::optional nextPart; + if (index < partList.size() - 1) + nextPart = partList[index + 1]; + + if (part.type == PartType::FixedText) { + if (part.modifier == Modifier::None) { + result.append(escapePatternString(part.value)); + + continue; + } + result.append('{', escapePatternString(part.value), '}', convertModifierToString(part.modifier)); + + continue; + } + + bool hasCustomName = !part.name.isEmpty() && !isASCIIDigit(part.name[0]); + + bool needsGrouping = !part.suffix.isEmpty() || (!part.prefix.isEmpty() && part.prefix != options.prefixCodepoint); + + if (!needsGrouping && hasCustomName + && part.type == PartType::SegmentWildcard && part.modifier == Modifier::None + && nextPart && nextPart->prefix.isEmpty() && nextPart->suffix.isEmpty()) { + if (nextPart->type == PartType::FixedText) { + if (!nextPart->value.isEmpty()) + needsGrouping = isValidNameCodepoint(*StringView(nextPart->value).codePoints().begin(), IsFirst::No); + } else + needsGrouping = !nextPart->name.isEmpty() && isASCIIDigit(nextPart->name[0]); + } + + if (!needsGrouping && part.prefix.isEmpty() && previousPart && previousPart->type == PartType::FixedText && !previousPart->value.isEmpty()) { + if (options.prefixCodepoint.length() == 1 + && options.prefixCodepoint.startsWith(*StringView(previousPart->value).codePoints().codePointAt(previousPart->value.length() - 1))) + needsGrouping = true; + } + + ASSERT(!part.name.isEmpty()); + + if (needsGrouping) + result.append('{'); + + result.append(escapePatternString(part.prefix)); + + if (hasCustomName) + result.append(':', part.name); + + if (part.type == PartType::Regexp) + result.append('(', part.value, ')'); + else if (part.type == PartType::SegmentWildcard && !hasCustomName) + result.append('(', generateSegmentWildcardRegexp(options), ')'); + else if (part.type == PartType::FullWildcard) { + if (!hasCustomName + && (!previousPart || previousPart->type == PartType::FixedText || previousPart->modifier != Modifier::None + || needsGrouping || !part.prefix.isEmpty())) + result.append('*'); + else + result.append("(.*)"_s); + } + + if (part.type == PartType::SegmentWildcard && hasCustomName && !part.suffix.isEmpty() && isValidNameCodepoint(*StringView(part.suffix).codePoints().begin(), IsFirst::Yes)) + result.append('\\'); + + result.append(escapePatternString(part.suffix)); + + if (needsGrouping) + result.append('}'); + + result.append(convertModifierToString(part.modifier)); + } + + return result.toString(); +} + +template +static String escapePatternStringForCharacters(std::span characters) +{ + static constexpr auto escapeCharacters = std::to_array({ '+', '*', '?', ':', '(', ')', '\\', '{', '}' }); // NOLINT + + StringBuilder result; + result.reserveCapacity(characters.size()); + + for (auto character : characters) { + if (std::ranges::find(escapeCharacters, character) != escapeCharacters.end()) + result.append('\\'); + + result.append(character); + } + + return result.toString(); +} + +// https://urlpattern.spec.whatwg.org/#escape-a-pattern-string +String escapePatternString(StringView input) +{ + // FIXME: Ensure input only contains ASCII based on spec after the parser (or tokenizer) knows to filter non-ASCII input. + + if (input.is8Bit()) + return escapePatternStringForCharacters(input.span8()); + + return escapePatternStringForCharacters(input.span16()); +} + +// https://urlpattern.spec.whatwg.org/#is-a-valid-name-code-point +bool isValidNameCodepoint(char16_t codepoint, URLPatternUtilities::IsFirst first) +{ + if (first == URLPatternUtilities::IsFirst::Yes) + return u_hasBinaryProperty(codepoint, UCHAR_ID_START) || codepoint == '_' || codepoint == '$'; + + return u_hasBinaryProperty(codepoint, UCHAR_ID_CONTINUE) || codepoint == '_' || codepoint == '$' || codepoint == 0x200c || codepoint == 0x200d; +} + +} // namespace URLPatternUtilities +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/URLPatternParser.h b/src/bun.js/bindings/webcore/URLPatternParser.h new file mode 100644 index 0000000000..3d942e454c --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternParser.h @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "URLPatternTokenizer.h" +#include +#include + +namespace WebCore { + +enum class EncodingCallbackType : uint8_t; +template class ExceptionOr; + +namespace URLPatternUtilities { + +struct Token; +enum class TokenType : uint8_t; + +enum class PartType : uint8_t { FixedText, + Regexp, + SegmentWildcard, + FullWildcard }; +enum class Modifier : uint8_t { None, + Optional, + ZeroOrMore, + OneOrMore }; +enum class IsFirst : bool { No, + Yes }; + +struct Part { + PartType type; + String value; + Modifier modifier; + String name {}; + String prefix {}; + String suffix {}; +}; + +struct URLPatternStringOptions { + String delimiterCodepoint {}; + String prefixCodepoint {}; + bool ignoreCase { false }; +}; + +class URLPatternParser { +public: + URLPatternParser(EncodingCallbackType, String&& segmentWildcardRegexp); + ExceptionOr performParse(const URLPatternStringOptions&); + + void setTokenList(Vector&& tokenList) { m_tokenList = WTFMove(tokenList); } + static ExceptionOr> parse(StringView, const URLPatternStringOptions&, EncodingCallbackType); + +private: + Token tryToConsumeToken(TokenType); + Token tryToConsumeRegexOrWildcardToken(const Token&); + Token tryToConsumeModifierToken(); + + String consumeText(); + ExceptionOr consumeRequiredToken(TokenType); + + ExceptionOr maybeAddPartFromPendingFixedValue(); + ExceptionOr addPart(String&& prefix, const Token& nameToken, const Token& regexpOrWildcardToken, String&& suffix, const Token& modifierToken); + + bool isDuplicateName(StringView) const; + + Vector takePartList() { return std::exchange(m_partList, {}); } + + Vector m_tokenList; + Vector m_partList; + EncodingCallbackType m_callbackType; + String m_segmentWildcardRegexp; + StringBuilder m_pendingFixedValue; + size_t m_index { 0 }; + int m_nextNumericName { 0 }; +}; + +// FIXME: Consider moving functions to somewhere generic, perhaps refactor Part to its own class. +String generateSegmentWildcardRegexp(const URLPatternStringOptions&); +String escapeRegexString(StringView); +ASCIILiteral convertModifierToString(Modifier); +std::pair> generateRegexAndNameList(const Vector& partList, const URLPatternStringOptions&); +String generatePatternString(const Vector& partList, const URLPatternStringOptions&); +String escapePatternString(StringView input); +bool isValidNameCodepoint(char16_t codepoint, URLPatternUtilities::IsFirst); + +} // namespace URLPatternUtilities +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/URLPatternResult.h b/src/bun.js/bindings/webcore/URLPatternResult.h new file mode 100644 index 0000000000..7f86feb2c1 --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternResult.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "URLPattern.h" + +namespace WebCore { + +struct URLPatternComponentResult { + using NameMatchPair = KeyValuePair>; + using GroupsRecord = Vector; + String input; + GroupsRecord groups; +}; + +struct URLPatternResult { + Vector inputs; + + URLPatternComponentResult protocol; + URLPatternComponentResult username; + URLPatternComponentResult password; + URLPatternComponentResult hostname; + URLPatternComponentResult port; + URLPatternComponentResult pathname; + URLPatternComponentResult search; + URLPatternComponentResult hash; +}; + +} diff --git a/src/bun.js/bindings/webcore/URLPatternResult.idl b/src/bun.js/bindings/webcore/URLPatternResult.idl new file mode 100644 index 0000000000..1521879bd5 --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternResult.idl @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + + // https://urlpattern.spec.whatwg.org/#dictdef-urlpatterncomponentresult + // https://urlpattern.spec.whatwg.org/#dictdef-urlpatternresult + + typedef (USVString or URLPatternInit) URLPatternInput; + +[ + JSGenerateToJSObject, + JSGenerateToNativeObject, + ImplementedAs=URLPatternComponentResult +] dictionary URLPatternComponentResult { + USVString input; + record groups; +}; + +[ + JSGenerateToJSObject, + JSGenerateToNativeObject +] dictionary URLPatternResult { + sequence inputs; + + URLPatternComponentResult protocol; + URLPatternComponentResult username; + URLPatternComponentResult password; + URLPatternComponentResult hostname; + URLPatternComponentResult port; + URLPatternComponentResult pathname; + URLPatternComponentResult search; + URLPatternComponentResult hash; +}; diff --git a/src/bun.js/bindings/webcore/URLPatternTokenizer.cpp b/src/bun.js/bindings/webcore/URLPatternTokenizer.cpp new file mode 100644 index 0000000000..d505f740d0 --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternTokenizer.cpp @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "URLPatternTokenizer.h" + +#include "ExceptionOr.h" +#include "URLPatternParser.h" +#include +#include + +namespace WebCore { +namespace URLPatternUtilities { + +bool Token::isNull() const +{ + if (!index) { + ASSERT(value.isNull()); + return true; + } + return false; +} + +// https://urlpattern.spec.whatwg.org/#get-the-next-code-point +void Tokenizer::getNextCodePoint() +{ + m_codepoint = m_input[m_nextIndex++]; + + if (m_input.is8Bit() || !U16_IS_LEAD(m_codepoint) || m_nextIndex >= m_input.length()) + return; + + auto next = m_input[m_nextIndex]; + if (!U16_IS_TRAIL(next)) + return; + + m_nextIndex++; + m_codepoint = U16_GET_SUPPLEMENTARY(m_codepoint, next); +} + +// https://urlpattern.spec.whatwg.org/#seek-and-get-the-next-code-point +void Tokenizer::seekNextCodePoint(size_t index) +{ + m_nextIndex = index; + getNextCodePoint(); +} + +// https://urlpattern.spec.whatwg.org/#add-a-token +void Tokenizer::addToken(TokenType currentType, size_t nextPosition, size_t valuePosition, size_t valueLength) +{ + m_tokenList.append(Token { currentType, m_index, m_input.substring(valuePosition, valueLength) }); + m_index = nextPosition; +} + +// https://urlpattern.spec.whatwg.org/#add-a-token-with-default-length +void Tokenizer::addToken(TokenType currentType, size_t nextPosition, size_t valuePosition) +{ + addToken(currentType, nextPosition, valuePosition, nextPosition - valuePosition); +} + +// https://urlpattern.spec.whatwg.org/#add-a-token-with-default-position-and-length +void Tokenizer::addToken(TokenType currentType) +{ + addToken(currentType, m_nextIndex, m_index); +} + +// https://urlpattern.spec.whatwg.org/#process-a-tokenizing-error +ExceptionOr Tokenizer::processTokenizingError(size_t nextPosition, size_t valuePosition, const String& callerErrorInfo) +{ + if (m_policy == TokenizePolicy::Strict) + return Exception { ExceptionCode::TypeError, callerErrorInfo }; + + ASSERT(m_policy == TokenizePolicy::Lenient); + + addToken(TokenType::InvalidChar, nextPosition, valuePosition); + + return {}; +} + +Tokenizer::Tokenizer(StringView input, TokenizePolicy tokenizerPolicy) + : m_input(input) + , m_policy(tokenizerPolicy) +{ +} + +// https://urlpattern.spec.whatwg.org/#tokenize +ExceptionOr> Tokenizer::tokenize() +{ + ExceptionOr maybeException; + + while (m_index < m_input.length()) { + if (m_policy == TokenizePolicy::Strict && maybeException.hasException()) + return maybeException.releaseException(); + + seekNextCodePoint(m_index); + + if (m_codepoint == '*') { + addToken(TokenType::Asterisk); + continue; + } + + if (m_codepoint == '+' || m_codepoint == '?') { + addToken(TokenType::OtherModifier); + continue; + } + + if (m_codepoint == '\\') { + if (m_index == m_input.length() - 1) { + maybeException = processTokenizingError(m_nextIndex, m_index, "No character is provided after escape."_s); + continue; + } + + auto escapedIndex = m_nextIndex; + getNextCodePoint(); + + addToken(TokenType::EscapedChar, m_nextIndex, escapedIndex); + continue; + } + + if (m_codepoint == '{') { + addToken(TokenType::Open); + continue; + } + + if (m_codepoint == '}') { + addToken(TokenType::Close); + continue; + } + + if (m_codepoint == ':') { + auto namePosition = m_nextIndex; + auto nameStart = namePosition; + + while (namePosition < m_input.length()) { + seekNextCodePoint(namePosition); + + bool isValidCodepoint = isValidNameCodepoint(m_codepoint, namePosition == nameStart ? IsFirst::Yes : IsFirst::No); + + if (!isValidCodepoint) + break; + + namePosition = m_nextIndex; + } + + if (namePosition <= nameStart) { + maybeException = processTokenizingError(nameStart, m_index, makeString("Name position "_s, String::number(namePosition), " is less than name start "_s, String::number(nameStart))); + continue; + } + + addToken(TokenType::Name, namePosition, nameStart); + continue; + } + + if (m_codepoint == '(') { + int depth = 1; + auto regexPosition = m_nextIndex; + auto regexStart = regexPosition; + bool hasError = false; + + while (regexPosition < m_input.length()) { + seekNextCodePoint(regexPosition); + + if (!isASCII(m_codepoint)) { + maybeException = processTokenizingError(regexStart, m_index, "Current codepoint is not ascii"_s); + hasError = true; + break; + } + + if (regexPosition == regexStart && m_codepoint == '?') { + maybeException = processTokenizingError(regexStart, m_index, "Regex cannot start with modifier."_s); + hasError = true; + break; + } + + if (m_codepoint == '\\') { + if (regexPosition == m_input.length() - 1) { + maybeException = processTokenizingError(regexStart, m_index, "No character is provided after escape."_s); + hasError = true; + break; + } + + getNextCodePoint(); + + if (!isASCII(m_codepoint)) { + maybeException = processTokenizingError(regexStart, m_index, "Current codepoint is not ascii"_s); + hasError = true; + break; + } + + regexPosition = m_nextIndex; + continue; + } + + if (m_codepoint == ')') { + depth = depth - 1; + + if (!depth) { + regexPosition = m_nextIndex; + break; + } + } + + if (m_codepoint == '(') { + depth = depth + 1; + + if (regexPosition == m_input.length() - 1) { + maybeException = processTokenizingError(regexStart, m_index, "No closing token is provided by end of string."_s); + hasError = true; + break; + } + + int temporaryPosition = m_nextIndex; + getNextCodePoint(); + + if (m_codepoint != '?') { + maybeException = processTokenizingError(regexStart, m_index, "Required OtherModifier token is not provided in regex."_s); + hasError = true; + break; + } + + m_nextIndex = temporaryPosition; + } + + regexPosition = m_nextIndex; + } + + if (hasError) + continue; + + if (depth) { + maybeException = processTokenizingError(regexStart, m_index, "Current open token does not have a corresponding close token."_s); + continue; + } + + auto regexLength = regexPosition - regexStart - 1; + + if (!regexLength) + maybeException = processTokenizingError(regexStart, m_index, "Regex length is zero."_s); + + addToken(TokenType::Regexp, regexPosition, regexStart, regexLength); + continue; + } + + addToken(TokenType::Char); + } + + addToken(TokenType::End, m_index, m_index); + return WTFMove(m_tokenList); +} + +} // namespace URLPatternUtilities +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/URLPatternTokenizer.h b/src/bun.js/bindings/webcore/URLPatternTokenizer.h new file mode 100644 index 0000000000..fb5a790616 --- /dev/null +++ b/src/bun.js/bindings/webcore/URLPatternTokenizer.h @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include + +namespace WebCore { + +template class ExceptionOr; + +namespace URLPatternUtilities { + +enum class TokenType : uint8_t { Open, + Close, + Regexp, + Name, + Char, + EscapedChar, + OtherModifier, + Asterisk, + End, + InvalidChar }; +enum class TokenizePolicy : bool { Strict, + Lenient }; + +struct Token { + TokenType type; + std::optional index; + StringView value; + + bool isNull() const; +}; + +class Tokenizer { +public: + Tokenizer(StringView input, TokenizePolicy tokenizerPolicy); + + ExceptionOr> tokenize(); + +private: + StringView m_input; + TokenizePolicy m_policy { TokenizePolicy::Strict }; + Vector m_tokenList; + size_t m_index { 0 }; + size_t m_nextIndex { 0 }; + char32_t m_codepoint; + + void getNextCodePoint(); + void seekNextCodePoint(size_t index); + + void addToken(TokenType currentType, size_t nextPosition, size_t valuePosition, size_t valueLength); + void addToken(TokenType currentType, size_t nextPosition, size_t valuePosition); + void addToken(TokenType currentType); + + ExceptionOr processTokenizingError(size_t nextPosition, size_t valuePosition, const String&); +}; + +} // namespace URLPatternUtilities +} // namespace WebCore diff --git a/test/js/web/urlpattern/urlpattern.test.ts b/test/js/web/urlpattern/urlpattern.test.ts new file mode 100644 index 0000000000..c2a727ab5b --- /dev/null +++ b/test/js/web/urlpattern/urlpattern.test.ts @@ -0,0 +1,209 @@ +// Test data from Web Platform Tests +// https://github.com/web-platform-tests/wpt/blob/master/LICENSE.md +import { describe, expect, test } from "bun:test"; +import testData from "./urlpatterntestdata.json"; + +const kComponents = ["protocol", "username", "password", "hostname", "port", "pathname", "search", "hash"] as const; + +type Component = (typeof kComponents)[number]; + +interface TestEntry { + pattern: any[]; + inputs?: any[]; + expected_obj?: Record | "error"; + expected_match?: Record | null | "error"; + exactly_empty_components?: string[]; +} + +function getExpectedPatternString(entry: TestEntry, component: Component): string { + // If the test case explicitly provides an expected pattern string, use that + if (entry.expected_obj && typeof entry.expected_obj === "object" && entry.expected_obj[component] !== undefined) { + return entry.expected_obj[component]; + } + + // Determine if there is a baseURL present + let baseURL: URL | null = null; + if (entry.pattern.length > 0 && entry.pattern[0].baseURL) { + baseURL = new URL(entry.pattern[0].baseURL); + } else if (entry.pattern.length > 1 && typeof entry.pattern[1] === "string") { + baseURL = new URL(entry.pattern[1]); + } + + const EARLIER_COMPONENTS: Record = { + protocol: [], + hostname: ["protocol"], + port: ["protocol", "hostname"], + username: [], + password: [], + pathname: ["protocol", "hostname", "port"], + search: ["protocol", "hostname", "port", "pathname"], + hash: ["protocol", "hostname", "port", "pathname", "search"], + }; + + if (entry.exactly_empty_components?.includes(component)) { + return ""; + } else if (typeof entry.pattern[0] === "object" && entry.pattern[0][component]) { + return entry.pattern[0][component]; + } else if (typeof entry.pattern[0] === "object" && EARLIER_COMPONENTS[component].some(c => c in entry.pattern[0])) { + return "*"; + } else if (baseURL && component !== "username" && component !== "password") { + let base_value = (baseURL as any)[component] as string; + if (component === "protocol") base_value = base_value.substring(0, base_value.length - 1); + else if (component === "search" || component === "hash") base_value = base_value.substring(1); + return base_value; + } else { + return "*"; + } +} + +function getExpectedComponentResult( + entry: TestEntry, + component: Component, +): { input: string; groups: Record } { + let expected_obj = entry.expected_match?.[component]; + + if (!expected_obj) { + expected_obj = { input: "", groups: {} as Record }; + if (!entry.exactly_empty_components?.includes(component)) { + expected_obj.groups["0"] = ""; + } + } + + // Convert null to undefined in groups + for (const key in expected_obj.groups) { + if (expected_obj.groups[key] === null) { + expected_obj.groups[key] = undefined; + } + } + + return expected_obj; +} + +describe("URLPattern", () => { + describe("WPT tests", () => { + for (const entry of testData as TestEntry[]) { + const testName = `Pattern: ${JSON.stringify(entry.pattern)} Inputs: ${JSON.stringify(entry.inputs)}`; + + test(testName, () => { + // Test construction error + if (entry.expected_obj === "error") { + expect(() => new URLPattern(...entry.pattern)).toThrow(TypeError); + return; + } + + const pattern = new URLPattern(...entry.pattern); + + // Verify compiled pattern properties + for (const component of kComponents) { + const expected = getExpectedPatternString(entry, component); + expect(pattern[component]).toBe(expected); + } + + // Test match error + if (entry.expected_match === "error") { + expect(() => pattern.test(...(entry.inputs ?? []))).toThrow(TypeError); + expect(() => pattern.exec(...(entry.inputs ?? []))).toThrow(TypeError); + return; + } + + // Test test() method + expect(pattern.test(...(entry.inputs ?? []))).toBe(!!entry.expected_match); + + // Test exec() method + const exec_result = pattern.exec(...(entry.inputs ?? [])); + + if (!entry.expected_match || typeof entry.expected_match !== "object") { + expect(exec_result).toBe(entry.expected_match); + return; + } + + const expected_inputs = entry.expected_match.inputs ?? entry.inputs; + + // Verify inputs + expect(exec_result!.inputs.length).toBe(expected_inputs!.length); + for (let i = 0; i < exec_result!.inputs.length; i++) { + const input = exec_result!.inputs[i]; + const expected_input = expected_inputs![i]; + if (typeof input === "string") { + expect(input).toBe(expected_input); + } else { + for (const component of kComponents) { + expect(input[component]).toBe(expected_input[component]); + } + } + } + + // Verify component results + for (const component of kComponents) { + const expected = getExpectedComponentResult(entry, component); + expect(exec_result![component]).toEqual(expected); + } + }); + } + }); + + describe("constructor edge cases", () => { + test("unclosed token with URL object - %(", () => { + expect(() => new URLPattern(new URL("https://example.org/%("))).toThrow(TypeError); + }); + + test("unclosed token with URL object - %((", () => { + expect(() => new URLPattern(new URL("https://example.org/%(("))).toThrow(TypeError); + }); + + test("unclosed token with string - (\\", () => { + expect(() => new URLPattern("(\\")).toThrow(TypeError); + }); + + test("constructor with undefined arguments", () => { + // Should not throw + new URLPattern(undefined, undefined); + }); + }); + + describe("hasRegExpGroups", () => { + test("match-everything pattern", () => { + expect(new URLPattern({}).hasRegExpGroups).toBe(false); + }); + + for (const component of kComponents) { + test(`wildcard in ${component}`, () => { + expect(new URLPattern({ [component]: "*" }).hasRegExpGroups).toBe(false); + }); + + test(`segment wildcard in ${component}`, () => { + expect(new URLPattern({ [component]: ":foo" }).hasRegExpGroups).toBe(false); + }); + + test(`optional segment wildcard in ${component}`, () => { + expect(new URLPattern({ [component]: ":foo?" }).hasRegExpGroups).toBe(false); + }); + + test(`named regexp group in ${component}`, () => { + expect(new URLPattern({ [component]: ":foo(hi)" }).hasRegExpGroups).toBe(true); + }); + + test(`anonymous regexp group in ${component}`, () => { + expect(new URLPattern({ [component]: "(hi)" }).hasRegExpGroups).toBe(true); + }); + + if (component !== "protocol" && component !== "port") { + test(`wildcards mixed with fixed text in ${component}`, () => { + expect(new URLPattern({ [component]: "a-{:hello}-z-*-a" }).hasRegExpGroups).toBe(false); + }); + + test(`regexp groups mixed with fixed text in ${component}`, () => { + expect(new URLPattern({ [component]: "a-(hi)-z-(lo)-a" }).hasRegExpGroups).toBe(true); + }); + } + } + + test("complex pathname with no regexp", () => { + expect(new URLPattern({ pathname: "/a/:foo/:baz?/b/*" }).hasRegExpGroups).toBe(false); + }); + + test("complex pathname with regexp", () => { + expect(new URLPattern({ pathname: "/a/:foo/:baz([a-z]+)?/b/*" }).hasRegExpGroups).toBe(true); + }); + }); +}); diff --git a/test/js/web/urlpattern/urlpatterntestdata.json b/test/js/web/urlpattern/urlpatterntestdata.json new file mode 100644 index 0000000000..75f1cf8694 --- /dev/null +++ b/test/js/web/urlpattern/urlpatterntestdata.json @@ -0,0 +1,2967 @@ +[ + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/ba" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/bar/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [ "https://example.com/foo/bar" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": { "0": "example.com" } }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": { "0": "https" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [ "https://example.com/foo/bar/baz" ], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "hostname": "example.com", "pathname": "/foo/bar" }], + "expected_match": { + "hostname": { "input": "example.com", "groups": { "0": "example.com" } }, + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "hostname": "example.com", "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/bar", "baseURL": "https://example.com" }], + "expected_match": { + "hostname": { "input": "example.com", "groups": { "0": "example.com" } }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": { "0": "https" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/bar/baz", + "baseURL": "https://example.com" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "hostname": "example.com", "pathname": "/foo/bar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "protocol": "https", "hostname": "example.com", + "pathname": "/foo/bar" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com" }], + "inputs": [{ "protocol": "https", "hostname": "example.com", + "pathname": "/foo/bar" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com" }], + "inputs": [{ "protocol": "https", "hostname": "example.com", + "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "protocol": "https", "hostname": "example.com", + "pathname": "/foo/bar", "search": "otherquery", + "hash": "otherhash" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hash": { "input": "otherhash", "groups": { "0": "otherhash" } }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "otherquery", "groups": { "0": "otherquery" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com" }], + "inputs": [{ "protocol": "https", "hostname": "example.com", + "pathname": "/foo/bar", "search": "otherquery", + "hash": "otherhash" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hash": { "input": "otherhash", "groups": { "0": "otherhash" } }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "otherquery", "groups": { "0": "otherquery" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?otherquery#otherhash" }], + "inputs": [{ "protocol": "https", "hostname": "example.com", + "pathname": "/foo/bar", "search": "otherquery", + "hash": "otherhash" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hash": { "input": "otherhash", "groups": { "0": "otherhash" } }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "otherquery", "groups": { "0": "otherquery" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [ "https://example.com/foo/bar" ], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [ "https://example.com/foo/bar?otherquery#otherhash" ], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hash": { "input": "otherhash", "groups": { "0": "otherhash" } }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "otherquery", "groups": { "0": "otherquery" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [ "https://example.com/foo/bar?query#hash" ], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hash": { "input": "hash", "groups": { "0": "hash" } }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "query", "groups": { "0": "query" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [ "https://example.com/foo/bar/baz" ], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [ "https://other.com/foo/bar" ], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [ "http://other.com/foo/bar" ], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "pathname": "/foo/bar", "baseURL": "https://example.com" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "pathname": "/foo/bar/baz", + "baseURL": "https://example.com" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "pathname": "/foo/bar", "baseURL": "https://other.com" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar", + "baseURL": "https://example.com?query#hash" }], + "inputs": [{ "pathname": "/foo/bar", "baseURL": "http://example.com" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/([^\\/]+?)" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar" }], + "inputs": [{ "pathname": "/foo/index.html" }], + "expected_match": { + "pathname": { "input": "/foo/index.html", "groups": { "bar": "index.html" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar" }], + "inputs": [{ "pathname": "/foo/bar/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_obj": { + "pathname": "/foo/*" + }, + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_obj": { + "pathname": "/foo/*" + }, + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_obj": { + "pathname": "/foo/*" + }, + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)" }], + "inputs": [{ "pathname": "/foo" }], + "expected_obj": { + "pathname": "/foo/*" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/*" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar(.*)" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar(.*)" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "bar": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar(.*)" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "bar": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar(.*)" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar?" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar?" }], + "inputs": [{ "pathname": "/foo" }], + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "/foo", "groups": { "bar": null } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar?" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar?" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar?" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar+" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar+" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "bar": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar+" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar+" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar+" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar*" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "bar": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar*" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "bar": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar*" }], + "inputs": [{ "pathname": "/foo" }], + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "/foo", "groups": { "bar": null } } + } + }, + { + "pattern": [{ "pathname": "/foo/:bar*" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/:bar*" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)?" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_obj": { + "pathname": "/foo/*?" + }, + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*?" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)?" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_obj": { + "pathname": "/foo/*?" + }, + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*?" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)?" }], + "inputs": [{ "pathname": "/foo" }], + "expected_obj": { + "pathname": "/foo/*?" + }, + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "/foo", "groups": { "0": null } } + } + }, + { + "pattern": [{ "pathname": "/foo/*?" }], + "inputs": [{ "pathname": "/foo" }], + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "/foo", "groups": { "0": null } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)?" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_obj": { + "pathname": "/foo/*?" + }, + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*?" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)?" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_obj": { + "pathname": "/foo/*?" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/*?" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)?" }], + "inputs": [{ "pathname": "/fo" }], + "expected_obj": { + "pathname": "/foo/*?" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/*?" }], + "inputs": [{ "pathname": "/fo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)+" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_obj": { + "pathname": "/foo/*+" + }, + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*+" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)+" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_obj": { + "pathname": "/foo/*+" + }, + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*+" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)+" }], + "inputs": [{ "pathname": "/foo" }], + "expected_obj": { + "pathname": "/foo/*+" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/*+" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)+" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_obj": { + "pathname": "/foo/*+" + }, + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/*+" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)+" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_obj": { + "pathname": "/foo/*+" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/*+" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)+" }], + "inputs": [{ "pathname": "/fo" }], + "expected_obj": { + "pathname": "/foo/*+" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/*+" }], + "inputs": [{ "pathname": "/fo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)*" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_obj": { + "pathname": "/foo/**" + }, + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/**" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": { "0": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)*" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_obj": { + "pathname": "/foo/**" + }, + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/**" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": { + "pathname": { "input": "/foo/bar/baz", "groups": { "0": "bar/baz" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)*" }], + "inputs": [{ "pathname": "/foo" }], + "expected_obj": { + "pathname": "/foo/**" + }, + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "/foo", "groups": { "0": null } } + } + }, + { + "pattern": [{ "pathname": "/foo/**" }], + "inputs": [{ "pathname": "/foo" }], + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "/foo", "groups": { "0": null } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)*" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_obj": { + "pathname": "/foo/**" + }, + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/**" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": { + "pathname": { "input": "/foo/", "groups": { "0": "" } } + } + }, + { + "pattern": [{ "pathname": "/foo/(.*)*" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_obj": { + "pathname": "/foo/**" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/**" }], + "inputs": [{ "pathname": "/foobar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/(.*)*" }], + "inputs": [{ "pathname": "/fo" }], + "expected_obj": { + "pathname": "/foo/**" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/**" }], + "inputs": [{ "pathname": "/fo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_obj": { + "pathname": "/foo/bar" + }, + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_obj": { + "pathname": "/foo/bar" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}" }], + "inputs": [{ "pathname": "/foo" }], + "expected_obj": { + "pathname": "/foo/bar" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_obj": { + "pathname": "/foo/bar" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}?" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}?" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}?" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": { + "pathname": { "input": "/foo", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}?" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}+" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}+" }], + "inputs": [{ "pathname": "/foo/bar/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}+" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}+" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}+" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}*" }], + "inputs": [{ "pathname": "/foo/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}*" }], + "inputs": [{ "pathname": "/foo/bar/bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar/bar", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}*" }], + "inputs": [{ "pathname": "/foo/bar/baz" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo{/bar}*" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": { + "pathname": { "input": "/foo", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/foo{/bar}*" }], + "inputs": [{ "pathname": "/foo/" }], + "expected_match": null + }, + { + "pattern": [{ "protocol": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "username": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "password": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "pathname": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "search": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hash": "(café)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "protocol": ":café" }], + "inputs": [{ "protocol": "foo" }], + "expected_match": { + "protocol": { "input": "foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "username": ":café" }], + "inputs": [{ "username": "foo" }], + "expected_match": { + "username": { "input": "foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "password": ":café" }], + "inputs": [{ "password": "foo" }], + "expected_match": { + "password": { "input": "foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "hostname": ":café" }], + "inputs": [{ "hostname": "foo" }], + "expected_match": { + "hostname": { "input": "foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "pathname": "/:café" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": { + "pathname": { "input": "/foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "search": ":café" }], + "inputs": [{ "search": "foo" }], + "expected_match": { + "search": { "input": "foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "hash": ":café" }], + "inputs": [{ "hash": "foo" }], + "expected_match": { + "hash": { "input": "foo", "groups": { "café": "foo" } } + } + }, + { + "pattern": [{ "protocol": ":\u2118" }], + "inputs": [{ "protocol": "foo" }], + "expected_match": { + "protocol": { "input": "foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "username": ":\u2118" }], + "inputs": [{ "username": "foo" }], + "expected_match": { + "username": { "input": "foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "password": ":\u2118" }], + "inputs": [{ "password": "foo" }], + "expected_match": { + "password": { "input": "foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "hostname": ":\u2118" }], + "inputs": [{ "hostname": "foo" }], + "expected_match": { + "hostname": { "input": "foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "pathname": "/:\u2118" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": { + "pathname": { "input": "/foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "search": ":\u2118" }], + "inputs": [{ "search": "foo" }], + "expected_match": { + "search": { "input": "foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "hash": ":\u2118" }], + "inputs": [{ "hash": "foo" }], + "expected_match": { + "hash": { "input": "foo", "groups": { "\u2118": "foo" } } + } + }, + { + "pattern": [{ "protocol": ":\u3400" }], + "inputs": [{ "protocol": "foo" }], + "expected_match": { + "protocol": { "input": "foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "username": ":\u3400" }], + "inputs": [{ "username": "foo" }], + "expected_match": { + "username": { "input": "foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "password": ":\u3400" }], + "inputs": [{ "password": "foo" }], + "expected_match": { + "password": { "input": "foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "hostname": ":\u3400" }], + "inputs": [{ "hostname": "foo" }], + "expected_match": { + "hostname": { "input": "foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "pathname": "/:\u3400" }], + "inputs": [{ "pathname": "/foo" }], + "expected_match": { + "pathname": { "input": "/foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "search": ":\u3400" }], + "inputs": [{ "search": "foo" }], + "expected_match": { + "search": { "input": "foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "hash": ":\u3400" }], + "inputs": [{ "hash": "foo" }], + "expected_match": { + "hash": { "input": "foo", "groups": { "\u3400": "foo" } } + } + }, + { + "pattern": [{ "protocol": "(.*)" }], + "inputs": [{ "protocol" : "café" }], + "expected_obj": { + "protocol": "*" + }, + "expected_match": null + }, + { + "pattern": [{ "protocol": "(.*)" }], + "inputs": [{ "protocol": "cafe" }], + "expected_obj": { + "protocol": "*" + }, + "expected_match": { + "protocol": { "input": "cafe", "groups": { "0": "cafe" }} + } + }, + { + "pattern": [{ "protocol": "foo-bar" }], + "inputs": [{ "protocol": "foo-bar" }], + "expected_match": { + "protocol": { "input": "foo-bar", "groups": {} } + } + }, + { + "pattern": [{ "username": "caf%C3%A9" }], + "inputs": [{ "username" : "café" }], + "expected_match": { + "username": { "input": "caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "username": "café" }], + "inputs": [{ "username" : "café" }], + "expected_obj": { + "username": "caf%C3%A9" + }, + "expected_match": { + "username": { "input": "caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "username": "caf%c3%a9" }], + "inputs": [{ "username" : "café" }], + "expected_match": null + }, + { + "pattern": [{ "password": "caf%C3%A9" }], + "inputs": [{ "password" : "café" }], + "expected_match": { + "password": { "input": "caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "password": "café" }], + "inputs": [{ "password" : "café" }], + "expected_obj": { + "password": "caf%C3%A9" + }, + "expected_match": { + "password": { "input": "caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "password": "caf%c3%a9" }], + "inputs": [{ "password" : "café" }], + "expected_match": null + }, + { + "pattern": [{ "hostname": "xn--caf-dma.com" }], + "inputs": [{ "hostname" : "café.com" }], + "expected_match": { + "hostname": { "input": "xn--caf-dma.com", "groups": {}} + } + }, + { + "pattern": [{ "hostname": "café.com" }], + "inputs": [{ "hostname" : "café.com" }], + "expected_obj": { + "hostname": "xn--caf-dma.com" + }, + "expected_match": { + "hostname": { "input": "xn--caf-dma.com", "groups": {}} + } + }, + { + "pattern": ["http://\uD83D\uDEB2.com/"], + "inputs": ["http://\uD83D\uDEB2.com/"], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "http", + "hostname": "xn--h78h.com", + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {}}, + "hostname": { "input": "xn--h78h.com", "groups": {}}, + "pathname": { "input": "/", "groups": {}} + } + }, + { + "pattern": ["http://\uD83D \uDEB2"], + "expected_obj": "error" + }, + { + "pattern": [{"hostname":"\uD83D \uDEB2"}], + "expected_obj": "error" + }, + { + "pattern": [{"pathname":"\uD83D \uDEB2"}], + "inputs": [], + "expected_obj": { + "pathname": "%EF%BF%BD%20%EF%BF%BD" + }, + "expected_match": null + }, + { + "pattern": [{"pathname":":\uD83D \uDEB2"}], + "expected_obj": "error" + }, + { + "pattern": [{"pathname":":a\uDB40\uDD00b"}], + "inputs": [], + "expected_obj": { + "pathname": ":a\uDB40\uDD00b" + }, + "expected_match": null + }, + { + "pattern": [{"pathname":"test/:a\uD801\uDC50b"}], + "inputs": [{"pathname":"test/foo"}], + "expected_obj": { + "pathname": "test/:a\uD801\uDC50b" + }, + "expected_match": { + "pathname": { "input": "test/foo", "groups": { "a\uD801\uDC50b": "foo" }} + } + }, + { + "pattern": [{"pathname":":\uD83D\uDEB2"}], + "expected_obj": "error" + }, + { + "pattern": [{ "port": "" }], + "inputs": [{ "protocol": "http", "port": "80" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "protocol": { "input": "http", "groups": { "0": "http" }} + } + }, + { + "pattern": [{ "protocol": "http", "port": "80" }], + "inputs": [{ "protocol": "http", "port": "80" }], + "exactly_empty_components": [ "port" ], + "expected_match": { + "protocol": { "input": "http", "groups": {}} + } + }, + { + "pattern": [{ "protocol": "http", "port": "80{20}?" }], + "inputs": [{ "protocol": "http", "port": "80" }], + "expected_match": null + }, + { + "pattern": [{ "protocol": "http", "port": "80 " }], + "inputs": [{ "protocol": "http", "port": "80" }], + "expected_obj": { + "protocol": "http", + "port": "80" + }, + "expected_match": null + }, + { + "pattern": [{ "protocol": "http", "port": "100000" }], + "inputs": [{ "protocol": "http", "port": "100000" }], + "expected_obj": "error" + }, + { + "pattern": [{ "port": "80" }], + "inputs": [{ "protocol": "http", "port": "80" }], + "expected_match": null + }, + { + "pattern": [{ "protocol": "http{s}?", "port": "80" }], + "inputs": [{ "protocol": "http", "port": "80" }], + "expected_match": null + }, + { + "pattern": [{ "port": "80" }], + "inputs": [{ "port": "80" }], + "expected_match": { + "port": { "input": "80", "groups": {}} + } + }, + { + "pattern": [{ "port": "(.*)" }], + "inputs": [{ "port": "invalid80" }], + "expected_obj": { + "port": "*" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "/foo/./bar" }], + "expected_match": { + "pathname": { "input": "/foo/bar", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/foo/baz" }], + "inputs": [{ "pathname": "/foo/bar/../baz" }], + "expected_match": { + "pathname": { "input": "/foo/baz", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/caf%C3%A9" }], + "inputs": [{ "pathname": "/café" }], + "expected_match": { + "pathname": { "input": "/caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/café" }], + "inputs": [{ "pathname": "/café" }], + "expected_obj": { + "pathname": "/caf%C3%A9" + }, + "expected_match": { + "pathname": { "input": "/caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/caf%c3%a9" }], + "inputs": [{ "pathname": "/café" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "foo/bar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [{ "pathname": "foo/bar", "baseURL": "https://example.com" }], + "expected_match": { + "protocol": { "input": "https", "groups": { "0": "https" }}, + "hostname": { "input": "example.com", "groups": { "0": "example.com" }}, + "pathname": { "input": "/foo/bar", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/foo/../bar" }], + "inputs": [{ "pathname": "/bar" }], + "expected_obj": { + "pathname": "/bar" + }, + "expected_match": { + "pathname": { "input": "/bar", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "./foo/bar", "baseURL": "https://example.com" }], + "inputs": [{ "pathname": "foo/bar", "baseURL": "https://example.com" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/foo/bar" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {}}, + "hostname": { "input": "example.com", "groups": {}}, + "pathname": { "input": "/foo/bar", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "", "baseURL": "https://example.com" }], + "inputs": [{ "pathname": "/", "baseURL": "https://example.com" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {}}, + "hostname": { "input": "example.com", "groups": {}}, + "pathname": { "input": "/", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "{/bar}", "baseURL": "https://example.com/foo/" }], + "inputs": [{ "pathname": "./bar", "baseURL": "https://example.com/foo/" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/bar" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "\\/bar", "baseURL": "https://example.com/foo/" }], + "inputs": [{ "pathname": "./bar", "baseURL": "https://example.com/foo/" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/bar" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "b", "baseURL": "https://example.com/foo/" }], + "inputs": [{ "pathname": "./b", "baseURL": "https://example.com/foo/" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/foo/b" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {}}, + "hostname": { "input": "example.com", "groups": {}}, + "pathname": { "input": "/foo/b", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "foo/bar" }], + "inputs": [ "https://example.com/foo/bar" ], + "expected_match": null + }, + { + "pattern": [{ "pathname": "foo/bar", "baseURL": "https://example.com" }], + "inputs": [ "https://example.com/foo/bar" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/foo/bar" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {}}, + "hostname": { "input": "example.com", "groups": {}}, + "pathname": { "input": "/foo/bar", "groups": {}} + } + }, + { + "pattern": [{ "pathname": ":name.html", "baseURL": "https://example.com" }], + "inputs": [ "https://example.com/foo.html"] , + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/:name.html" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {}}, + "hostname": { "input": "example.com", "groups": {}}, + "pathname": { "input": "/foo.html", "groups": { "name": "foo" }} + } + }, + { + "pattern": [{ "search": "q=caf%C3%A9" }], + "inputs": [{ "search": "q=café" }], + "expected_match": { + "search": { "input": "q=caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "search": "q=café" }], + "inputs": [{ "search": "q=café" }], + "expected_obj": { + "search": "q=caf%C3%A9" + }, + "expected_match": { + "search": { "input": "q=caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "search": "q=caf%c3%a9" }], + "inputs": [{ "search": "q=café" }], + "expected_match": null + }, + { + "pattern": [{ "hash": "caf%C3%A9" }], + "inputs": [{ "hash": "café" }], + "expected_match": { + "hash": { "input": "caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "hash": "café" }], + "inputs": [{ "hash": "café" }], + "expected_obj": { + "hash": "caf%C3%A9" + }, + "expected_match": { + "hash": { "input": "caf%C3%A9", "groups": {}} + } + }, + { + "pattern": [{ "hash": "caf%c3%a9" }], + "inputs": [{ "hash": "café" }], + "expected_match": null + }, + { + "pattern": [{ "protocol": "about", "pathname": "(blank|sourcedoc)" }], + "inputs": [ "about:blank" ], + "expected_match": { + "protocol": { "input": "about", "groups": {}}, + "pathname": { "input": "blank", "groups": { "0": "blank" }} + } + }, + { + "pattern": [{ "protocol": "data", "pathname": ":number([0-9]+)" }], + "inputs": [ "data:8675309" ], + "expected_match": { + "protocol": { "input": "data", "groups": {}}, + "pathname": { "input": "8675309", "groups": { "number": "8675309" }} + } + }, + { + "pattern": [{ "pathname": "/(\\m)" }], + "expected_obj": "error" + }, + { + "pattern": [{ "pathname": "/foo!" }], + "inputs": [{ "pathname": "/foo!" }], + "expected_match": { + "pathname": { "input": "/foo!", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/foo\\:" }], + "inputs": [{ "pathname": "/foo:" }], + "expected_match": { + "pathname": { "input": "/foo:", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/foo\\{" }], + "inputs": [{ "pathname": "/foo{" }], + "expected_obj": { + "pathname": "/foo%7B" + }, + "expected_match": { + "pathname": { "input": "/foo%7B", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/foo\\(" }], + "inputs": [{ "pathname": "/foo(" }], + "expected_match": { + "pathname": { "input": "/foo(", "groups": {}} + } + }, + { + "pattern": [{ "protocol": "javascript", "pathname": "var x = 1;" }], + "inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }], + "expected_match": { + "protocol": { "input": "javascript", "groups": {}}, + "pathname": { "input": "var x = 1;", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "var x = 1;" }], + "inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }], + "expected_obj": { + "pathname": "var%20x%20=%201;" + }, + "expected_match": null + }, + { + "pattern": [{ "protocol": "javascript", "pathname": "var x = 1;" }], + "inputs": [{ "baseURL": "javascript:var x = 1;" }], + "expected_match": { + "protocol": { "input": "javascript", "groups": {}}, + "pathname": { "input": "var x = 1;", "groups": {}} + } + }, + { + "pattern": [{ "protocol": "(data|javascript)", "pathname": "var x = 1;" }], + "inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }], + "expected_match": { + "protocol": { "input": "javascript", "groups": {"0": "javascript"}}, + "pathname": { "input": "var x = 1;", "groups": {}} + } + }, + { + "pattern": [{ "protocol": "(https|javascript)", "pathname": "var x = 1;" }], + "inputs": [{ "protocol": "javascript", "pathname": "var x = 1;" }], + "expected_obj": { + "pathname": "var%20x%20=%201;" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "var x = 1;" }], + "inputs": [{ "pathname": "var x = 1;" }], + "expected_obj": { + "pathname": "var%20x%20=%201;" + }, + "expected_match": { + "pathname": { "input": "var%20x%20=%201;", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [ "./foo/bar", "https://example.com" ], + "expected_match": { + "hostname": { "input": "example.com", "groups": { "0": "example.com" } }, + "pathname": { "input": "/foo/bar", "groups": {} }, + "protocol": { "input": "https", "groups": { "0": "https" } } + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }], + "inputs": [ { "pathname": "/foo/bar" }, "https://example.com" ], + "expected_match": "error" + }, + { + "pattern": [ "https://example.com:8080/foo?bar#baz" ], + "inputs": [{ "pathname": "/foo", "search": "bar", "hash": "baz", + "baseURL": "https://example.com:8080" }], + "expected_obj": { + "protocol": "https", + "username": "*", + "password": "*", + "hostname": "example.com", + "port": "8080", + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} }, + "search": { "input": "bar", "groups": {} }, + "hash": { "input": "baz", "groups": {} } + } + }, + { + "pattern": [ "/foo?bar#baz", "https://example.com:8080" ], + "inputs": [{ "pathname": "/foo", "search": "bar", "hash": "baz", + "baseURL": "https://example.com:8080" }], + "expected_obj": { + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} }, + "search": { "input": "bar", "groups": {} }, + "hash": { "input": "baz", "groups": {} } + } + }, + { + "pattern": [ "/foo" ], + "expected_obj": "error" + }, + { + "pattern": [ "example.com/foo" ], + "expected_obj": "error" + }, + { + "pattern": [ "http{s}?://{*.}?example.com/:product/:endpoint" ], + "inputs": [ "https://sub.example.com/foo/bar" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "http{s}?", + "hostname": "{*.}?example.com", + "pathname": "/:product/:endpoint" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "sub.example.com", "groups": { "0": "sub" } }, + "pathname": { "input": "/foo/bar", "groups": { "product": "foo", + "endpoint": "bar" } } + } + }, + { + "pattern": [ "https://example.com?foo" ], + "inputs": [ "https://example.com/?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com#foo" ], + "inputs": [ "https://example.com/#foo" ], + "exactly_empty_components": [ "port", "search" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/", + "hash": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "hash": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com:8080?foo" ], + "inputs": [ "https://example.com:8080/?foo" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "port": "8080", + "pathname": "/", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com:8080#foo" ], + "inputs": [ "https://example.com:8080/#foo" ], + "exactly_empty_components": [ "search" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "port": "8080", + "pathname": "/", + "hash": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "hash": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/?foo" ], + "inputs": [ "https://example.com/?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/#foo" ], + "inputs": [ "https://example.com/#foo" ], + "exactly_empty_components": [ "port", "search" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/", + "hash": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "hash": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/*?foo" ], + "inputs": [ "https://example.com/?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/*?foo" + }, + "expected_match": null + }, + { + "pattern": [ "https://example.com/*\\?foo" ], + "inputs": [ "https://example.com/?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/*", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "" } }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/:name?foo" ], + "inputs": [ "https://example.com/bar?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/:name?foo" + }, + "expected_match": null + }, + { + "pattern": [ "https://example.com/:name\\?foo" ], + "inputs": [ "https://example.com/bar?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/:name", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/bar", "groups": { "name": "bar" } }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/(bar)?foo" ], + "inputs": [ "https://example.com/bar?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/(bar)?foo" + }, + "expected_match": null + }, + { + "pattern": [ "https://example.com/(bar)\\?foo" ], + "inputs": [ "https://example.com/bar?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/(bar)", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/bar", "groups": { "0": "bar" } }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/{bar}?foo" ], + "inputs": [ "https://example.com/bar?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/{bar}?foo" + }, + "expected_match": null + }, + { + "pattern": [ "https://example.com/{bar}\\?foo" ], + "inputs": [ "https://example.com/bar?foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/bar", + "search": "foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/bar", "groups": {} }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "https://example.com/" ], + "inputs": [ "https://example.com:8080/" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "port": "", + "pathname": "/" + }, + "expected_match": null + }, + { + "pattern": [ "data:foobar" ], + "inputs": [ "data:foobar" ], + "expected_obj": "error" + }, + { + "pattern": [ "data\\:foobar" ], + "inputs": [ "data:foobar" ], + "exactly_empty_components": [ "hostname", "port" ], + "expected_obj": { + "protocol": "data", + "pathname": "foobar" + }, + "expected_match": { + "protocol": { "input": "data", "groups": {} }, + "pathname": { "input": "foobar", "groups": {} } + } + }, + { + "pattern": [ "https://{sub.}?example.com/foo" ], + "inputs": [ "https://example.com/foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "{sub.}?example.com", + "pathname": "/foo" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} } + } + }, + { + "pattern": [ "https://{sub.}?example{.com/}foo" ], + "inputs": [ "https://example.com/foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "{sub.}?example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo", "groups": { "0": "/foo" } } + } + }, + { + "pattern": [ "{https://}example.com/foo" ], + "inputs": [ "https://example.com/foo" ], + "expected_obj": "error" + }, + { + "pattern": [ "https://(sub.)?example.com/foo" ], + "inputs": [ "https://example.com/foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "(sub.)?example.com", + "pathname": "/foo" + }, + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": { "0": null } }, + "pathname": { "input": "/foo", "groups": {} } + } + }, + { + "pattern": [ "https://(sub.)?example(.com/)foo" ], + "inputs": [ "https://example.com/foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "(sub.)?example(.com/)foo", + "pathname": "*" + }, + "expected_match": null + }, + { + "pattern": [ "(https://)example.com/foo" ], + "inputs": [ "https://example.com/foo" ], + "expected_obj": "error" + }, + { + "pattern": [ "https://{sub{.}}example.com/foo" ], + "inputs": [ "https://example.com/foo" ], + "expected_obj": "error" + }, + { + "pattern": [ "https://(sub(?:.))?example.com/foo" ], + "inputs": [ "https://example.com/foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "(sub(?:.))?example.com", + "pathname": "/foo" + }, + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": { "0": null } }, + "pathname": { "input": "/foo", "groups": {} } + } + }, + { + "pattern": [ "file:///foo/bar" ], + "inputs": [ "file:///foo/bar" ], + "exactly_empty_components": [ "hostname", "port" ], + "expected_obj": { + "protocol": "file", + "pathname": "/foo/bar" + }, + "expected_match": { + "protocol": { "input": "file", "groups": {} }, + "pathname": { "input": "/foo/bar", "groups": {} } + } + }, + { + "pattern": [ "data:" ], + "inputs": [ "data:" ], + "exactly_empty_components": [ "hostname", "port", "pathname" ], + "expected_obj": { + "protocol": "data" + }, + "expected_match": { + "protocol": { "input": "data", "groups": {} } + } + }, + { + "pattern": [ "foo://bar" ], + "inputs": [ "foo://bad_url_browser_interop" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "foo", + "hostname": "bar" + }, + "expected_match": null + }, + { + "pattern": [ "(café)://foo" ], + "expected_obj": "error" + }, + { + "pattern": [ "https://example.com/foo?bar#baz" ], + "inputs": [{ "protocol": "https:", + "search": "?bar", + "hash": "#baz", + "baseURL": "http://example.com/foo" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": null + }, + { + "pattern": [{ "protocol": "http{s}?:", + "search": "?bar", + "hash": "#baz" }], + "inputs": [ "http://example.com/foo?bar#baz" ], + "expected_obj": { + "protocol": "http{s}?", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "example.com", "groups": { "0": "example.com" }}, + "pathname": { "input": "/foo", "groups": { "0": "/foo" }}, + "search": { "input": "bar", "groups": {} }, + "hash": { "input": "baz", "groups": {} } + } + }, + { + "pattern": [ "?bar#baz", "https://example.com/foo" ], + "inputs": [ "?bar#baz", "https://example.com/foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} }, + "search": { "input": "bar", "groups": {} }, + "hash": { "input": "baz", "groups": {} } + } + }, + { + "pattern": [ "?bar", "https://example.com/foo#baz" ], + "inputs": [ "?bar", "https://example.com/foo#snafu" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/foo", + "search": "bar", + "hash": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} }, + "search": { "input": "bar", "groups": {} } + } + }, + { + "pattern": [ "#baz", "https://example.com/foo?bar" ], + "inputs": [ "#baz", "https://example.com/foo?bar" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} }, + "search": { "input": "bar", "groups": {} }, + "hash": { "input": "baz", "groups": {} } + } + }, + { + "pattern": [ "#baz", "https://example.com/foo" ], + "inputs": [ "#baz", "https://example.com/foo" ], + "exactly_empty_components": [ "port", "search" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/foo", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/foo", "groups": {} }, + "hash": { "input": "baz", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "*" }], + "inputs": [ "foo", "data:data-urls-cannot-be-base-urls" ], + "expected_match": null + }, + { + "pattern": [{ "pathname": "*" }], + "inputs": [ "foo", "not|a|valid|url" ], + "expected_match": null + }, + { + "pattern": [ "https://foo\\:bar@example.com" ], + "inputs": [ "https://foo:bar@example.com" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "username": "foo", + "password": "bar", + "hostname": "example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "username": { "input": "foo", "groups": {} }, + "password": { "input": "bar", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [ "https://foo@example.com" ], + "inputs": [ "https://foo@example.com" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "username": "foo", + "hostname": "example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "username": { "input": "foo", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [ "https://\\:bar@example.com" ], + "inputs": [ "https://:bar@example.com" ], + "exactly_empty_components": [ "username", "port" ], + "expected_obj": { + "protocol": "https", + "password": "bar", + "hostname": "example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "password": { "input": "bar", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [ "https://:user::pass@example.com" ], + "inputs": [ "https://foo:bar@example.com" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "username": ":user", + "password": ":pass", + "hostname": "example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "username": { "input": "foo", "groups": { "user": "foo" } }, + "password": { "input": "bar", "groups": { "pass": "bar" } }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [ "https\\:foo\\:bar@example.com" ], + "inputs": [ "https:foo:bar@example.com" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "username": "foo", + "password": "bar", + "hostname": "example.com", + "pathname": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "username": { "input": "foo", "groups": {} }, + "password": { "input": "bar", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": { "0": "/" } } + } + }, + { + "pattern": [ "data\\:foo\\:bar@example.com" ], + "inputs": [ "data:foo:bar@example.com" ], + "exactly_empty_components": [ "hostname", "port" ], + "expected_obj": { + "protocol": "data", + "pathname": "foo\\:bar@example.com" + }, + "expected_match": { + "protocol": { "input": "data", "groups": {} }, + "pathname": { "input": "foo:bar@example.com", "groups": {} } + } + }, + { + "pattern": [ "https://foo{\\:}bar@example.com" ], + "inputs": [ "https://foo:bar@example.com" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "username": "foo%3Abar", + "hostname": "example.com" + }, + "expected_match": null + }, + { + "pattern": [ "data{\\:}channel.html", "https://example.com" ], + "inputs": [ "https://example.com/data:channel.html" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "pathname": "/data\\:channel.html", + "search": "*", + "hash": "*" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/data:channel.html", "groups": {} } + } + }, + { + "pattern": [ "http://[\\:\\:1]/" ], + "inputs": [ "http://[::1]/" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "http", + "hostname": "[\\:\\:1]", + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "[::1]", "groups": {} }, + "pathname": { "input": "/", "groups": {} } + } + }, + { + "pattern": [ "http://[\\:\\:1]:8080/" ], + "inputs": [ "http://[::1]:8080/" ], + "expected_obj": { + "protocol": "http", + "hostname": "[\\:\\:1]", + "port": "8080", + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "[::1]", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/", "groups": {} } + } + }, + { + "pattern": [ "http://[\\:\\:a]/" ], + "inputs": [ "http://[::a]/" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "http", + "hostname": "[\\:\\:a]", + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "[::a]", "groups": {} }, + "pathname": { "input": "/", "groups": {} } + } + }, + { + "pattern": [ "http://[:address]/" ], + "inputs": [ "http://[::1]/" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "http", + "hostname": "[:address]", + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "[::1]", "groups": { "address": "::1" }}, + "pathname": { "input": "/", "groups": {} } + } + }, + { + "pattern": [ "http://[\\:\\:AB\\::num]/" ], + "inputs": [ "http://[::ab:1]/" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "protocol": "http", + "hostname": "[\\:\\:ab\\::num]", + "pathname": "/" + }, + "expected_match": { + "protocol": { "input": "http", "groups": {} }, + "hostname": { "input": "[::ab:1]", "groups": { "num": "1" }}, + "pathname": { "input": "/", "groups": {} } + } + }, + { + "pattern": [{ "hostname": "[\\:\\:AB\\::num]" }], + "inputs": [{ "hostname": "[::ab:1]" }], + "expected_obj": { + "hostname": "[\\:\\:ab\\::num]" + }, + "expected_match": { + "hostname": { "input": "[::ab:1]", "groups": { "num": "1" }} + } + }, + { + "pattern": [{ "hostname": "[\\:\\:xY\\::num]" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "{[\\:\\:ab\\::num]}" }], + "inputs": [{ "hostname": "[::ab:1]" }], + "expected_match": { + "hostname": { "input": "[::ab:1]", "groups": { "num": "1" }} + } + }, + { + "pattern": [{ "hostname": "{[\\:\\:fé\\::num]}" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "{[\\:\\::num\\:1]}" }], + "inputs": [{ "hostname": "[::ab:1]" }], + "expected_match": { + "hostname": { "input": "[::ab:1]", "groups": { "num": "ab" }} + } + }, + { + "pattern": [{ "hostname": "{[\\:\\::num\\:fé]}" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "[*\\:1]" }], + "inputs": [{ "hostname": "[::ab:1]" }], + "expected_match": { + "hostname": { "input": "[::ab:1]", "groups": { "0": "::ab" }} + } + }, + { + "pattern": [{ "hostname": "*\\:1]" }], + "expected_obj": "error" + }, + { + "pattern": [ "https://foo{{@}}example.com" ], + "inputs": [ "https://foo@example.com" ], + "expected_obj": "error" + }, + { + "pattern": [ "https://foo{@example.com" ], + "inputs": [ "https://foo@example.com" ], + "expected_obj": "error" + }, + { + "pattern": [ "data\\:text/javascript,let x = 100/:tens?5;" ], + "inputs": [ "data:text/javascript,let x = 100/5;" ], + "exactly_empty_components": [ "hostname", "port" ], + "expected_obj": { + "protocol": "data", + "pathname": "text/javascript,let x = 100/:tens?5;" + }, + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "protocol": { "input": "data", "groups": {} }, + "pathname": { "input": "text/javascript,let x = 100/5;", "groups": { "tens": null } } + } + }, + { + "pattern": [{ "pathname": "/:id/:id" }], + "expected_obj": "error" + }, + { + "pattern": [{ "pathname": "/foo", "baseURL": "" }], + "expected_obj": "error" + }, + { + "pattern": [ "/foo", "" ], + "expected_obj": "error" + }, + { + "pattern": [{ "pathname": "/foo" }, "https://example.com" ], + "expected_obj": "error" + }, + { + "pattern": [{ "pathname": ":name*" }], + "inputs": [{ "pathname": "foobar" }], + "expected_match": { + "pathname": { "input": "foobar", "groups": { "name": "foobar" }} + } + }, + { + "pattern": [{ "pathname": ":name+" }], + "inputs": [{ "pathname": "foobar" }], + "expected_match": { + "pathname": { "input": "foobar", "groups": { "name": "foobar" }} + } + }, + { + "pattern": [{ "pathname": ":name" }], + "inputs": [{ "pathname": "foobar" }], + "expected_match": { + "pathname": { "input": "foobar", "groups": { "name": "foobar" }} + } + }, + { + "pattern": [{ "protocol": ":name*" }], + "inputs": [{ "protocol": "foobar" }], + "expected_match": { + "protocol": { "input": "foobar", "groups": { "name": "foobar" }} + } + }, + { + "pattern": [{ "protocol": ":name+" }], + "inputs": [{ "protocol": "foobar" }], + "expected_match": { + "protocol": { "input": "foobar", "groups": { "name": "foobar" }} + } + }, + { + "pattern": [{ "protocol": ":name" }], + "inputs": [{ "protocol": "foobar" }], + "expected_match": { + "protocol": { "input": "foobar", "groups": { "name": "foobar" }} + } + }, + { + "pattern": [{ "hostname": "bad hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad#hostname" }], + "inputs": [{ "hostname": "bad" }], + "expected_obj": { + "hostname": "bad" + }, + "expected_match": { + "hostname": { "input": "bad", "groups": {} } + } + }, + { + "pattern": [{ "hostname": "bad%hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad/hostname" }], + "inputs": [{ "hostname": "bad" }], + "expected_obj": { + "hostname": "bad" + }, + "expected_match": { + "hostname": { "input": "bad", "groups": {} } + } + }, + { + "pattern": [{ "hostname": "bad\\:hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "badhostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad?hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad@hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad[hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad]hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad\\\\hostname" }], + "inputs": [{ "hostname": "badhostname" }], + "expected_obj": { + "hostname": "bad" + }, + "expected_match": null + }, + { + "pattern": [{ "hostname": "bad^hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad|hostname" }], + "expected_obj": "error" + }, + { + "pattern": [{ "hostname": "bad\nhostname" }], + "inputs": [{ "hostname": "badhostname" }], + "expected_obj": { + "hostname": "badhostname" + }, + "expected_match": { + "hostname": { "input": "badhostname", "groups": {} } + } + }, + { + "pattern": [{ "hostname": "bad\rhostname" }], + "inputs": [{ "hostname": "badhostname" }], + "expected_obj": { + "hostname": "badhostname" + }, + "expected_match": { + "hostname": { "input": "badhostname", "groups": {} } + } + }, + { + "pattern": [{ "hostname": "bad\thostname" }], + "inputs": [{ "hostname": "badhostname" }], + "expected_obj": { + "hostname": "badhostname" + }, + "expected_match": { + "hostname": { "input": "badhostname", "groups": {} } + } + }, + { + "pattern": [{}], + "inputs": ["https://example.com/"], + "expected_match": { + "protocol": { "input": "https", "groups": { "0": "https" }}, + "hostname": { "input": "example.com", "groups": { "0": "example.com" }}, + "pathname": { "input": "/", "groups": { "0": "/" }} + } + }, + { + "pattern": [], + "inputs": ["https://example.com/"], + "expected_match": { + "protocol": { "input": "https", "groups": { "0": "https" }}, + "hostname": { "input": "example.com", "groups": { "0": "example.com" }}, + "pathname": { "input": "/", "groups": { "0": "/" }} + } + }, + { + "pattern": [], + "inputs": [{}], + "expected_match": {} + }, + { + "pattern": [], + "inputs": [], + "expected_match": { "inputs": [{}] } + }, + { + "pattern": [{ "pathname": "(foo)(.*)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "0": "foo", "1": "barbaz" }} + } + }, + { + "pattern": [{ "pathname": "{(foo)bar}(.*)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "0": "foo", "1": "baz" }} + } + }, + { + "pattern": [{ "pathname": "(foo)?(.*)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_obj": { + "pathname": "(foo)?*" + }, + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "0": "foo", "1": "barbaz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}(.*)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "foo": "f", "0": "oobarbaz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}(barbaz)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "foo": "foo", "0": "barbaz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}{(.*)}" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_obj": { + "pathname": "{:foo}(.*)" + }, + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "foo": "f", "0": "oobarbaz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}{(.*)bar}" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_obj": { + "pathname": ":foo{*bar}" + }, + "expected_match": null + }, + { + "pattern": [{ "pathname": "{:foo}{bar(.*)}" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_obj": { + "pathname": ":foo{bar*}" + }, + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "foo": "foo", "0": "baz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}:bar(.*)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_obj": { + "pathname": ":foo:bar(.*)" + }, + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "foo": "f", "bar": "oobarbaz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}?(.*)" }], + "inputs": [{ "pathname": "foobarbaz" }], + "expected_obj": { + "pathname": ":foo?*" + }, + "expected_match": { + "pathname": { "input": "foobarbaz", "groups": { "foo": "f", "0": "oobarbaz" }} + } + }, + { + "pattern": [{ "pathname": "{:foo\\bar}" }], + "inputs": [{ "pathname": "foobar" }], + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": "{:foo\\.bar}" }], + "inputs": [{ "pathname": "foo.bar" }], + "expected_obj": { + "pathname": "{:foo.bar}" + }, + "expected_match": { + "pathname": { "input": "foo.bar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": "{:foo(foo)bar}" }], + "inputs": [{ "pathname": "foobar" }], + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": "{:foo}bar" }], + "inputs": [{ "pathname": "foobar" }], + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": ":foo\\bar" }], + "inputs": [{ "pathname": "foobar" }], + "expected_obj": { + "pathname": "{:foo}bar" + }, + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": ":foo{}(.*)" }], + "inputs": [{ "pathname": "foobar" }], + "expected_obj": { + "pathname": "{:foo}(.*)" + }, + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "f", "0": "oobar" }} + } + }, + { + "pattern": [{ "pathname": ":foo{}bar" }], + "inputs": [{ "pathname": "foobar" }], + "expected_obj": { + "pathname": "{:foo}bar" + }, + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": ":foo{}?bar" }], + "inputs": [{ "pathname": "foobar" }], + "expected_obj": { + "pathname": "{:foo}bar" + }, + "expected_match": { + "pathname": { "input": "foobar", "groups": { "foo": "foo" }} + } + }, + { + "pattern": [{ "pathname": "*{}**?" }], + "inputs": [{ "pathname": "foobar" }], + "expected_obj": { + "pathname": "*(.*)?" + }, + "//": "The `null` below is translated to undefined in the test harness.", + "expected_match": { + "pathname": { "input": "foobar", "groups": { "0": "foobar", "1": null }} + } + }, + { + "pattern": [{ "pathname": ":foo(baz)(.*)" }], + "inputs": [{ "pathname": "bazbar" }], + "expected_match": { + "pathname": { "input": "bazbar", "groups": { "foo": "baz", "0": "bar" }} + } + }, + { + "pattern": [{ "pathname": ":foo(baz)bar" }], + "inputs": [{ "pathname": "bazbar" }], + "expected_match": { + "pathname": { "input": "bazbar", "groups": { "foo": "baz" }} + } + }, + { + "pattern": [{ "pathname": "*/*" }], + "inputs": [{ "pathname": "foo/bar" }], + "expected_match": { + "pathname": { "input": "foo/bar", "groups": { "0": "foo", "1": "bar" }} + } + }, + { + "pattern": [{ "pathname": "*\\/*" }], + "inputs": [{ "pathname": "foo/bar" }], + "expected_obj": { + "pathname": "*/{*}" + }, + "expected_match": { + "pathname": { "input": "foo/bar", "groups": { "0": "foo", "1": "bar" }} + } + }, + { + "pattern": [{ "pathname": "*/{*}" }], + "inputs": [{ "pathname": "foo/bar" }], + "expected_match": { + "pathname": { "input": "foo/bar", "groups": { "0": "foo", "1": "bar" }} + } + }, + { + "pattern": [{ "pathname": "*//*" }], + "inputs": [{ "pathname": "foo/bar" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/:foo." }], + "inputs": [{ "pathname": "/bar." }], + "expected_match": { + "pathname": { "input": "/bar.", "groups": { "foo": "bar" } } + } + }, + { + "pattern": [{ "pathname": "/:foo.." }], + "inputs": [{ "pathname": "/bar.." }], + "expected_match": { + "pathname": { "input": "/bar..", "groups": { "foo": "bar" } } + } + }, + { + "pattern": [{ "pathname": "./foo" }], + "inputs": [{ "pathname": "./foo" }], + "expected_match": { + "pathname": { "input": "./foo", "groups": {}} + } + }, + { + "pattern": [{ "pathname": "../foo" }], + "inputs": [{ "pathname": "../foo" }], + "expected_match": { + "pathname": { "input": "../foo", "groups": {}} + } + }, + { + "pattern": [{ "pathname": ":foo./" }], + "inputs": [{ "pathname": "bar./" }], + "expected_match": { + "pathname": { "input": "bar./", "groups": { "foo": "bar" }} + } + }, + { + "pattern": [{ "pathname": ":foo../" }], + "inputs": [{ "pathname": "bar../" }], + "expected_match": { + "pathname": { "input": "bar../", "groups": { "foo": "bar" }} + } + }, + { + "pattern": [{ "pathname": "/:foo\\bar" }], + "inputs": [{ "pathname": "/bazbar" }], + "expected_obj": { + "pathname": "{/:foo}bar" + }, + "expected_match": { + "pathname": { "input": "/bazbar", "groups": { "foo": "baz" }} + } + }, + { + "pattern": [{ "pathname": "/foo/bar" }, { "ignoreCase": true }], + "inputs": [{ "pathname": "/FOO/BAR" }], + "expected_match": { + "pathname": { "input": "/FOO/BAR", "groups": {} } + } + }, + { + "pattern": [{ "ignoreCase": true }], + "inputs": [{ "pathname": "/FOO/BAR" }], + "expected_match": { + "pathname": { "input": "/FOO/BAR", "groups": { "0": "/FOO/BAR" } } + } + }, + { + "pattern": [ "https://example.com:8080/foo?bar#baz", + { "ignoreCase": true }], + "inputs": [{ "pathname": "/FOO", "search": "BAR", "hash": "BAZ", + "baseURL": "https://example.com:8080" }], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "port": "8080", + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/FOO", "groups": {} }, + "search": { "input": "BAR", "groups": {} }, + "hash": { "input": "BAZ", "groups": {} } + } + }, + { + "pattern": [ "/foo?bar#baz", "https://example.com:8080", + { "ignoreCase": true }], + "inputs": [{ "pathname": "/FOO", "search": "BAR", "hash": "BAZ", + "baseURL": "https://example.com:8080" }], + "expected_obj": { + "protocol": "https", + "hostname": "example.com", + "port": "8080", + "pathname": "/foo", + "search": "bar", + "hash": "baz" + }, + "expected_match": { + "protocol": { "input": "https", "groups": {} }, + "hostname": { "input": "example.com", "groups": {} }, + "port": { "input": "8080", "groups": {} }, + "pathname": { "input": "/FOO", "groups": {} }, + "search": { "input": "BAR", "groups": {} }, + "hash": { "input": "BAZ", "groups": {} } + } + }, + { + "pattern": [ "/foo?bar#baz", { "ignoreCase": true }, + "https://example.com:8080" ], + "inputs": [{ "pathname": "/FOO", "search": "BAR", "hash": "BAZ", + "baseURL": "https://example.com:8080" }], + "expected_obj": "error" + }, + { + "pattern": [{ "search": "foo", "baseURL": "https://example.com/a/+/b" }], + "inputs": [{ "search": "foo", "baseURL": "https://example.com/a/+/b" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "pathname": "/a/\\+/b" + }, + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/a/+/b", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [{ "hash": "foo", "baseURL": "https://example.com/?q=*&v=?&hmm={}&umm=()" }], + "inputs": [{ "hash": "foo", "baseURL": "https://example.com/?q=*&v=?&hmm={}&umm=()" }], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "search": "q=\\*&v=\\?&hmm=\\{\\}&umm=\\(\\)" + }, + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "q=*&v=?&hmm={}&umm=()", "groups": {} }, + "hash": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [ "#foo", "https://example.com/?q=*&v=?&hmm={}&umm=()" ], + "inputs": [ "https://example.com/?q=*&v=?&hmm={}&umm=()#foo" ], + "exactly_empty_components": [ "port" ], + "expected_obj": { + "search": "q=\\*&v=\\?&hmm=\\{\\}&umm=\\(\\)", + "hash": "foo" + }, + "expected_match": { + "hostname": { "input": "example.com", "groups": {} }, + "pathname": { "input": "/", "groups": {} }, + "protocol": { "input": "https", "groups": {} }, + "search": { "input": "q=*&v=?&hmm={}&umm=()", "groups": {} }, + "hash": { "input": "foo", "groups": {} } + } + }, + { + "pattern": [{ "pathname": "/([[a-z]--a])" }], + "inputs": [{ "pathname": "/a" }], + "expected_match": null + }, + { + "pattern": [{ "pathname": "/([[a-z]--a])" }], + "inputs": [{ "pathname": "/z" }], + "expected_match": { + "pathname": { "input": "/z", "groups": { "0": "z" } } + } + }, + { + "pattern": [{ "pathname": "/([\\d&&[0-1]])" }], + "inputs": [{ "pathname": "/0" }], + "expected_match": { + "pathname": { "input": "/0", "groups": { "0": "0" } } + } + }, + { + "pattern": [{ "pathname": "/([\\d&&[0-1]])" }], + "inputs": [{ "pathname": "/3" }], + "expected_match": null + } +] diff --git a/test/no-validate-exceptions.txt b/test/no-validate-exceptions.txt index 7ca32272e1..875b3e3adc 100644 --- a/test/no-validate-exceptions.txt +++ b/test/no-validate-exceptions.txt @@ -155,3 +155,6 @@ vendor/elysia/test/validator/body.test.ts vendor/elysia/test/ws/message.test.ts test/js/node/test/parallel/test-worker-abort-on-uncaught-exception.js + +# TODO: WebCore fixes +test/js/web/urlpattern/urlpattern.test.ts \ No newline at end of file