#include "node_url_pattern.h" #include "base_object-inl.h" #include "debug_utils-inl.h" #include "env-inl.h" #include "node.h" #include "node_errors.h" #include "node_mem-inl.h" #include "path.h" #include "util-inl.h" namespace node { using node::url_pattern::URLPatternRegexProvider; template <> struct MemoryRetainerTraits> { using Type = ada::url_pattern; static void MemoryInfo(MemoryTracker* tracker, const Type& value) { tracker->TraitTrackInline(value.protocol_component, "protocol_component"); tracker->TraitTrackInline(value.username_component, "username_component"); tracker->TraitTrackInline(value.password_component, "password_component"); tracker->TraitTrackInline(value.hostname_component, "hostname_component"); tracker->TraitTrackInline(value.port_component, "port_component"); tracker->TraitTrackInline(value.pathname_component, "pathname_component"); tracker->TraitTrackInline(value.search_component, "search_component"); tracker->TraitTrackInline(value.hash_component, "hash_component"); } static const char* MemoryInfoName(const Type& value) { return "ada::url_pattern"; } static size_t SelfSize(const Type& value) { return sizeof(value); } }; template <> struct MemoryRetainerTraits< ada::url_pattern_component> { using Type = ada::url_pattern_component; static void MemoryInfo(MemoryTracker* tracker, const Type& value) { tracker->TrackField("pattern", value.pattern); tracker->TrackField("group_name_list", value.group_name_list); } static const char* MemoryInfoName(const Type& value) { return "ada::url_pattern_component"; } static size_t SelfSize(const Type& value) { return sizeof(value); } }; } // namespace node namespace node::url_pattern { using v8::Array; using v8::Context; using v8::DictionaryTemplate; using v8::DontDelete; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::Global; using v8::Isolate; using v8::Local; using v8::LocalVector; using v8::MaybeLocal; using v8::Name; using v8::NewStringType; using v8::Object; using v8::PropertyAttribute; using v8::ReadOnly; using v8::RegExp; using v8::Signature; using v8::String; using v8::Value; std::optional URLPatternRegexProvider::create_instance(std::string_view pattern, bool ignore_case) { auto isolate = Isolate::GetCurrent(); auto env = Environment::GetCurrent(isolate); int flags = RegExp::Flags::kUnicodeSets | RegExp::Flags::kDotAll; if (ignore_case) { flags |= static_cast(RegExp::Flags::kIgnoreCase); } Local local_pattern; if (!String::NewFromUtf8( isolate, pattern.data(), NewStringType::kNormal, pattern.size()) .ToLocal(&local_pattern)) { return std::nullopt; } Local regexp; if (!RegExp::New( env->context(), local_pattern, static_cast(flags)) .ToLocal(®exp)) { return std::nullopt; } return Global(isolate, regexp); } bool URLPatternRegexProvider::regex_match(std::string_view input, const regex_type& pattern) { auto isolate = Isolate::GetCurrent(); auto env = Environment::GetCurrent(isolate); Local local_input; if (!String::NewFromUtf8( isolate, input.data(), NewStringType::kNormal, input.size()) .ToLocal(&local_input)) { return false; } Local result_object; if (!pattern.Get(isolate) ->Exec(env->context(), local_input) .ToLocal(&result_object)) { return false; } // RegExp::Exec returns null if there is no match. return !result_object->IsNull(); } std::optional>> URLPatternRegexProvider::regex_search(std::string_view input, const regex_type& global_pattern) { auto isolate = Isolate::GetCurrent(); auto env = Environment::GetCurrent(isolate); Local local_input; if (!String::NewFromUtf8( isolate, input.data(), NewStringType::kNormal, input.size()) .ToLocal(&local_input)) { return std::nullopt; } Local exec_result_object; auto pattern = global_pattern.Get(isolate); if (!pattern->Exec(env->context(), local_input) .ToLocal(&exec_result_object) || exec_result_object->IsNull()) { return std::nullopt; } DCHECK(exec_result_object->IsArray()); auto exec_result = exec_result_object.As(); size_t len = exec_result->Length(); std::vector> result; result.reserve(len); for (size_t i = 1; i < len; i++) { Local entry; if (!exec_result->Get(env->context(), i).ToLocal(&entry)) { return std::nullopt; } // V8 checks that the regexp exec result is one of the correct types. DCHECK_IMPLIES(!entry->IsUndefined(), entry->IsString()); if (entry->IsUndefined()) { result.emplace_back(std::nullopt); } else if (entry->IsString()) { Utf8Value utf8_entry(isolate, entry.As()); result.emplace_back(utf8_entry.ToString()); } } return result; } URLPattern::URLPattern(Environment* env, Local object, ada::url_pattern&& url_pattern) : BaseObject(env, object), url_pattern_(std::move(url_pattern)) { MakeWeak(); } void URLPattern::MemoryInfo(MemoryTracker* tracker) const { tracker->TraitTrackInline(url_pattern_, "url_pattern"); #define URL_PATTERN_CACHED_VALUES(_, lowercase) \ tracker->TrackField(#lowercase, lowercase); URL_PATTERN_COMPONENTS(URL_PATTERN_CACHED_VALUES) #undef URL_PATTERN_CACHED_VALUES } void URLPattern::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); if (!args.IsConstructCall()) { THROW_ERR_CONSTRUCT_CALL_REQUIRED(env); return; } // If no arguments are passed, then we parse the empty URLPattern. if (args.Length() == 0) { auto init = ada::url_pattern_init{}; auto url_pattern = ada::parse_url_pattern(init); CHECK(url_pattern); new URLPattern(env, args.This(), std::move(*url_pattern)); return; } std::optional init{}; std::optional input{}; std::optional base_url{}; std::optional options{}; // Following patterns are supported: // - new URLPattern(input) // - new URLPattern(input, baseURL) // - new URLPattern(input, options) // - new URLPattern(input, baseURL, options) if (args[0]->IsString()) { BufferValue input_buffer(env->isolate(), args[0]); CHECK_NOT_NULL(*input_buffer); input = input_buffer.ToString(); } else if (args[0]->IsObject()) { init = URLPatternInit::FromJsObject(env, args[0].As()); // If init does not have a value here, the implication is that an // error was thrown. Let's allow that to be handled now by returning // early. If we don't, the error thrown will be swallowed. if (!init) return; } else { THROW_ERR_INVALID_ARG_TYPE(env, "Input must be an object or a string"); return; } // The next argument can be baseURL or options. if (args.Length() > 1) { if (args[1]->IsString()) { BufferValue base_url_buffer(env->isolate(), args[1]); CHECK_NOT_NULL(*base_url_buffer); base_url = base_url_buffer.ToString(); } else if (args[1]->IsObject()) { CHECK(!options.has_value()); options = URLPatternOptions::FromJsObject(env, args[1].As()); if (!options) { // If options does not have a value, we assume an error was // thrown and scheduled on the isolate. Return early to // propagate it. return; } } else { THROW_ERR_INVALID_ARG_TYPE(env, "second argument must be a string or object"); return; } // Only remaining argument can be options. if (args.Length() > 2) { if (!args[2]->IsObject()) { THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); return; } CHECK(!options.has_value()); options = URLPatternOptions::FromJsObject(env, args[2].As()); if (!options) { // If options does not have a value, we assume an error was // thrown and scheduled on the isolate. Return early to // propagate it. return; } } } // Either url_pattern_init or input as a string must be provided. CHECK_IMPLIES(init.has_value(), !input.has_value()); std::string_view base_url_view{}; if (base_url) base_url_view = {base_url->data(), base_url->size()}; ada::url_pattern_input arg0; if (init.has_value()) { arg0 = std::move(*init); } else { arg0 = std::move(*input); } auto url_pattern = parse_url_pattern( std::move(arg0), base_url ? &base_url_view : nullptr, options.has_value() ? &options.value() : nullptr); if (!url_pattern) { THROW_ERR_INVALID_URL_PATTERN(env, "Failed to construct URLPattern"); return; } new URLPattern(env, args.This(), std::move(*url_pattern)); } MaybeLocal URLPattern::URLPatternInit::ToJsObject( Environment* env, const ada::url_pattern_init& init) { auto isolate = env->isolate(); auto context = env->context(); auto tmpl = env->urlpatterninit_template(); if (tmpl.IsEmpty()) { static constexpr std::string_view namesVec[] = { "protocol", "username", "password", "hostname", "port", "pathname", "search", "hash", "baseURL", }; tmpl = DictionaryTemplate::New(isolate, namesVec); env->set_urlpatterninit_template(tmpl); } MaybeLocal values[] = { Undefined(isolate), // protocol Undefined(isolate), // username Undefined(isolate), // password Undefined(isolate), // hostname Undefined(isolate), // port Undefined(isolate), // pathname Undefined(isolate), // search Undefined(isolate), // hash Undefined(isolate), // baseURL }; int idx = 0; Local temp; const auto trySet = [&](const std::optional& val) { if (val.has_value()) { if (!ToV8Value(context, *val).ToLocal(&temp)) { return false; } values[idx] = temp; } idx++; return true; }; if (!trySet(init.protocol) || !trySet(init.username) || !trySet(init.password) || !trySet(init.hostname) || !trySet(init.port) || !trySet(init.pathname) || !trySet(init.search) || !trySet(init.hash) || !trySet(init.base_url)) { return {}; } return NewDictionaryInstance(env->context(), tmpl, values); } std::optional URLPattern::URLPatternInit::FromJsObject( Environment* env, Local obj) { ada::url_pattern_init init{}; Local components[] = { env->protocol_string(), env->username_string(), env->password_string(), env->hostname_string(), env->port_string(), env->pathname_string(), env->search_string(), env->hash_string(), env->base_url_string(), }; auto isolate = env->isolate(); const auto set_parameter = [&](std::string_view key, std::string_view value) { if (key == "protocol") { init.protocol = std::string(value); } else if (key == "username") { init.username = std::string(value); } else if (key == "password") { init.password = std::string(value); } else if (key == "hostname") { init.hostname = std::string(value); } else if (key == "port") { init.port = std::string(value); } else if (key == "pathname") { init.pathname = std::string(value); } else if (key == "search") { init.search = std::string(value); } else if (key == "hash") { init.hash = std::string(value); } else if (key == "baseURL") { init.base_url = std::string(value); } }; Local value; for (const auto& component : components) { Utf8Value key(isolate, component); if (obj->Get(env->context(), component).ToLocal(&value)) { if (value->IsString()) { Utf8Value utf8_value(isolate, value); set_parameter(key.ToStringView(), utf8_value.ToStringView()); } } else { // If ToLocal failed then we assume an error occurred, // bail out early to propagate the error. return std::nullopt; } } return init; } MaybeLocal URLPattern::URLPatternComponentResult::ToJSObject( Environment* env, const ada::url_pattern_component_result& result) { auto isolate = env->isolate(); auto context = env->context(); LocalVector group_names(isolate); LocalVector group_values(isolate); group_names.reserve(result.groups.size()); group_values.reserve(result.groups.size()); for (const auto& [group_key, group_value] : result.groups) { Local key; if (!ToV8Value(context, group_key).ToLocal(&key)) { return {}; } group_names.push_back(key.As()); Local value; if (group_value) { if (!ToV8Value(env->context(), *group_value).ToLocal(&value)) { return {}; } } else { value = Undefined(isolate); } group_values.push_back(value); } auto parsed_group = Object::New(isolate, Object::New(isolate), group_names.data(), group_values.data(), group_names.size()); Local input; if (!ToV8Value(env->context(), result.input).ToLocal(&input)) { return {}; } auto tmpl = env->urlpatterncomponentresult_template(); if (tmpl.IsEmpty()) { static constexpr std::string_view namesVec[] = { "input", "groups", }; tmpl = DictionaryTemplate::New(isolate, namesVec); env->set_urlpatterncomponentresult_template(tmpl); } MaybeLocal values[] = {input, parsed_group}; return NewDictionaryInstance(env->context(), tmpl, values); } MaybeLocal URLPattern::URLPatternResult::ToJSValue( Environment* env, const ada::url_pattern_result& result) { auto isolate = env->isolate(); auto tmpl = env->urlpatternresult_template(); if (tmpl.IsEmpty()) { static constexpr std::string_view namesVec[] = { "inputs", "protocol", "username", "password", "hostname", "port", "pathname", "search", "hash", }; tmpl = DictionaryTemplate::New(isolate, namesVec); env->set_urlpatternresult_template(tmpl); } size_t index = 0; MaybeLocal vals[] = { Array::New(env->context(), result.inputs.size(), [&index, &inputs = result.inputs, env]() { auto& input = inputs[index++]; if (std::holds_alternative(input)) { auto input_str = std::get(input); return ToV8Value(env->context(), input_str); } else { DCHECK( std::holds_alternative(input)); auto init = std::get(input); return URLPatternInit::ToJsObject(env, init); } }), URLPatternComponentResult::ToJSObject(env, result.protocol), URLPatternComponentResult::ToJSObject(env, result.username), URLPatternComponentResult::ToJSObject(env, result.password), URLPatternComponentResult::ToJSObject(env, result.hostname), URLPatternComponentResult::ToJSObject(env, result.port), URLPatternComponentResult::ToJSObject(env, result.pathname), URLPatternComponentResult::ToJSObject(env, result.search), URLPatternComponentResult::ToJSObject(env, result.hash)}; return NewDictionaryInstanceNullProto(env->context(), tmpl, vals); } std::optional URLPattern::URLPatternOptions::FromJsObject(Environment* env, Local obj) { ada::url_pattern_options options{}; Local ignore_case; if (obj->Get(env->context(), env->ignore_case_string()) .ToLocal(&ignore_case)) { if (!ignore_case->IsBoolean()) { THROW_ERR_INVALID_ARG_TYPE(env, "options.ignoreCase must be a boolean"); return std::nullopt; } options.ignore_case = ignore_case->IsTrue(); } else { // If ToLocal returns false, the assumption is that getting the // ignore_case_string threw an error, let's propagate that now // by returning std::nullopt. return std::nullopt; } return options; } // Perform value lookup and cache the result in a v8::Global. #define URL_PATTERN_COMPONENT_GETTERS(uppercase_name, lowercase_name) \ MaybeLocal URLPattern::uppercase_name() { \ auto isolate = env()->isolate(); \ if (lowercase_name.IsEmpty()) { \ Local value; \ if (ToV8Value(env()->context(), url_pattern_.get_##lowercase_name()) \ .ToLocal(&value)) { \ lowercase_name.Reset(isolate, value); \ return value; \ } \ return {}; \ } \ return lowercase_name.Get(isolate); \ } URL_PATTERN_COMPONENTS(URL_PATTERN_COMPONENT_GETTERS) #undef URL_PATTERN_COMPONENT_GETTERS bool URLPattern::HasRegExpGroups() const { return url_pattern_.has_regexp_groups(); } // Instance methods MaybeLocal URLPattern::Exec(Environment* env, const ada::url_pattern_input& input, std::optional& baseURL) { if (auto result = url_pattern_.exec(input, baseURL ? &*baseURL : nullptr)) { if (result->has_value()) { return URLPatternResult::ToJSValue(env, result->value()); } return Null(env->isolate()); } return {}; } bool URLPattern::Test(Environment* env, const ada::url_pattern_input& input, std::optional& baseURL) { if (auto result = url_pattern_.test(input, baseURL ? &*baseURL : nullptr)) { return *result; } THROW_ERR_OPERATION_FAILED(env, "Failed to test URLPattern"); return false; } // V8 Methods void URLPattern::Exec(const FunctionCallbackInfo& args) { URLPattern* url_pattern; ASSIGN_OR_RETURN_UNWRAP(&url_pattern, args.This()); auto env = Environment::GetCurrent(args); ada::url_pattern_input input; std::optional baseURL{}; std::string input_base; if (args.Length() == 0) { input = ada::url_pattern_init{}; } else if (args[0]->IsString()) { Utf8Value input_value(env->isolate(), args[0].As()); input_base = input_value.ToString(); input = std::string_view(input_base); } else if (args[0]->IsObject()) { auto maybeInput = URLPatternInit::FromJsObject(env, args[0].As()); if (!maybeInput.has_value()) return; input = std::move(*maybeInput); } else { THROW_ERR_INVALID_ARG_TYPE( env, "URLPattern input needs to be a string or an object"); return; } if (args.Length() > 1) { if (!args[1]->IsString()) { THROW_ERR_INVALID_ARG_TYPE(env, "baseURL must be a string"); return; } Utf8Value base_url_value(env->isolate(), args[1].As()); baseURL = base_url_value.ToStringView(); } Local result; std::optional baseURL_opt = baseURL ? std::optional(*baseURL) : std::nullopt; if (!url_pattern->Exec(env, input, baseURL_opt).ToLocal(&result)) { THROW_ERR_OPERATION_FAILED(env, "Failed to exec URLPattern"); return; } args.GetReturnValue().Set(result); } void URLPattern::Test(const FunctionCallbackInfo& args) { URLPattern* url_pattern; ASSIGN_OR_RETURN_UNWRAP(&url_pattern, args.This()); auto env = Environment::GetCurrent(args); ada::url_pattern_input input; std::optional baseURL{}; std::string input_base; if (args.Length() == 0) { input = ada::url_pattern_init{}; } else if (args[0]->IsString()) { Utf8Value input_value(env->isolate(), args[0].As()); input_base = input_value.ToString(); input = std::string_view(input_base); } else if (args[0]->IsObject()) { auto maybeInput = URLPatternInit::FromJsObject(env, args[0].As()); if (!maybeInput.has_value()) return; input = std::move(*maybeInput); } else { THROW_ERR_INVALID_ARG_TYPE( env, "URLPattern input needs to be a string or an object"); return; } if (args.Length() > 1) { if (!args[1]->IsString()) { THROW_ERR_INVALID_ARG_TYPE(env, "baseURL must be a string"); return; } Utf8Value base_url_value(env->isolate(), args[1].As()); baseURL = base_url_value.ToStringView(); } std::optional baseURL_opt = baseURL ? std::optional(*baseURL) : std::nullopt; args.GetReturnValue().Set(url_pattern->Test(env, input, baseURL_opt)); } #define URL_PATTERN_COMPONENT_GETTERS(uppercase_name, lowercase_name) \ void URLPattern::uppercase_name(const FunctionCallbackInfo& info) { \ URLPattern* url_pattern; \ ASSIGN_OR_RETURN_UNWRAP(&url_pattern, info.This()); \ Local result; \ if (url_pattern->uppercase_name().ToLocal(&result)) { \ info.GetReturnValue().Set(result); \ } \ } URL_PATTERN_COMPONENTS(URL_PATTERN_COMPONENT_GETTERS) #undef URL_PATTERN_COMPONENT_GETTERS void URLPattern::HasRegexpGroups(const FunctionCallbackInfo& info) { URLPattern* url_pattern; ASSIGN_OR_RETURN_UNWRAP(&url_pattern, info.This()); info.GetReturnValue().Set(url_pattern->HasRegExpGroups()); } static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(URLPattern::New); #define URL_PATTERN_COMPONENT_GETTERS(uppercase_name, _) \ registry->Register(URLPattern::uppercase_name); URL_PATTERN_COMPONENTS(URL_PATTERN_COMPONENT_GETTERS) #undef URL_PATTERN_COMPONENT_GETTERS registry->Register(URLPattern::HasRegexpGroups); registry->Register(URLPattern::Exec); registry->Register(URLPattern::Test); } static void Initialize(Local target, Local unused, Local context, void* priv) { Environment* env = Environment::GetCurrent(context); Isolate* isolate = env->isolate(); auto attributes = static_cast(ReadOnly | DontDelete); auto ctor_tmpl = NewFunctionTemplate(isolate, URLPattern::New); auto instance_template = ctor_tmpl->InstanceTemplate(); auto prototype_template = ctor_tmpl->PrototypeTemplate(); ctor_tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "URLPattern")); // The signature is used to prevent the property accessors from being // called on the wrong receiver object (`this`) auto signature = Signature::New(isolate, ctor_tmpl); instance_template->SetInternalFieldCount(URLPattern::kInternalFieldCount); #define ENV_GETTER(lowercase_name) env->lowercase_name##_string() #define URL_PATTERN_COMPONENT_GETTERS(uppercase_name, lowercase_name) \ prototype_template->SetAccessorProperty( \ ENV_GETTER(lowercase_name), \ FunctionTemplate::New( \ isolate, URLPattern::uppercase_name, Local(), signature), \ Local(), \ attributes); URL_PATTERN_COMPONENTS(URL_PATTERN_COMPONENT_GETTERS) #undef URL_PATTERN_COMPONENT_GETTERS #undef ENV_GETTER prototype_template->SetAccessorProperty( env->has_regexp_groups_string(), FunctionTemplate::New( isolate, URLPattern::HasRegexpGroups, Local(), signature), Local(), attributes); SetProtoMethodNoSideEffect(isolate, ctor_tmpl, "exec", URLPattern::Exec); SetProtoMethodNoSideEffect(isolate, ctor_tmpl, "test", URLPattern::Test); SetConstructorFunction(context, target, "URLPattern", ctor_tmpl); } } // namespace node::url_pattern NODE_BINDING_CONTEXT_AWARE_INTERNAL(url_pattern, node::url_pattern::Initialize) NODE_BINDING_EXTERNAL_REFERENCE(url_pattern, node::url_pattern::RegisterExternalReferences)