Skip to content
File

Blob: src/workerd/api/urlpattern.c++

11.7 KB
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#include "urlpattern.h"
6 
7#include <workerd/util/own-util.h>
8 
9#include <kj/vector.h>
10 
11namespace workerd::api {
12 
13namespace {
14jsg::JsRef<jsg::JsRegExp> compileRegex(
15 jsg::Lock& js, const jsg::UrlPattern::Component& component, bool ignoreCase) {
16 JSG_TRY(js) {
17 jsg::Lock::RegExpFlags flags = jsg::Lock::RegExpFlags::kUNICODE;
18 if (ignoreCase) {
19 flags = static_cast<jsg::Lock::RegExpFlags>(
20 flags | static_cast<int>(jsg::Lock::RegExpFlags::kIGNORE_CASE));
21 }
22 return jsg::JsRef<jsg::JsRegExp>(js, js.regexp(component.getRegex(), flags));
23 }
24 JSG_CATCH(_) {
25 JSG_FAIL_REQUIRE(TypeError, "Invalid regular expression syntax.");
26 }
27}
28 
29jsg::Ref<URLPattern> create(jsg::Lock& js, jsg::UrlPattern pattern) {
30 bool ignoreCase = pattern.getIgnoreCase();
31 
32 // Might look a bit confusing here. The URL_PATTERN_COMPONENTS macro
33 // is used also to define the constructor for URLPattern so to make
34 // sure things line up right we reuse that pattern here also. Because
35 // we are moving the pattern into the constructor, we need to make sure
36 // the regex patterns are compiled first so we use the macro twice.
37#define V(Name, var) auto var = compileRegex(js, pattern.get##Name(), ignoreCase);
38 URL_PATTERN_COMPONENTS(V)
39#undef V
40 
41#define V(_, var) , kj::mv(var)
42 return js.alloc<URLPattern>(kj::mv(pattern) URL_PATTERN_COMPONENTS(V));
43#undef V
44}
45 
46kj::Maybe<URLPattern::URLPatternComponentResult> execRegex(jsg::Lock& js,
47 jsg::JsRef<jsg::JsRegExp>& regex,
48 kj::ArrayPtr<const kj::String> nameList,
49 kj::StringPtr input) {
50 using Groups = jsg::Dict<kj::String, kj::String>;
51 
52 KJ_IF_SOME(array, regex.getHandle(js)(js, input)) {
53 // Per the URLPattern spec, we iterate over nameList rather than the regex
54 // result array. The regex may contain more capturing groups than there are
55 // named parts (e.g. when a part's value itself contains capturing groups
56 // like "(?<foo>x)"), so using the regex array length would cause an
57 // out-of-bounds access on nameList.
58 uint32_t length = nameList.size();
59 kj::Vector<Groups::Field> fields(length);
60 
61 for (uint32_t index = 0; index < length; index++) {
62 auto value = array.get(js, index + 1);
63 fields.add(Groups::Field{
64 .name = kj::str(nameList[index]),
65 .value = value.isUndefined() ? kj::String() : kj::str(value),
66 });
67 }
68 
69 return URLPattern::URLPatternComponentResult{
70 .input = kj::str(input),
71 .groups = Groups{.fields = fields.releaseAsArray()},
72 };
73 }
74 
75 return kj::none;
76}
77} // namespace
78 
79URLPattern::URLPattern(jsg::UrlPattern inner,
80 jsg::JsRef<jsg::JsRegExp> protocolRegex,
81 jsg::JsRef<jsg::JsRegExp> usernameRegex,
82 jsg::JsRef<jsg::JsRegExp> passwordRegex,
83 jsg::JsRef<jsg::JsRegExp> hostnameRegex,
84 jsg::JsRef<jsg::JsRegExp> portRegex,
85 jsg::JsRef<jsg::JsRegExp> pathnameRegex,
86 jsg::JsRef<jsg::JsRegExp> searchRegex,
87 jsg::JsRef<jsg::JsRegExp> hashRegex)
88 : inner(kj::mv(inner)),
89 protocolRegex(kj::mv(protocolRegex)),
90 usernameRegex(kj::mv(usernameRegex)),
91 passwordRegex(kj::mv(passwordRegex)),
92 hostnameRegex(kj::mv(hostnameRegex)),
93 portRegex(kj::mv(portRegex)),
94 pathnameRegex(kj::mv(pathnameRegex)),
95 searchRegex(kj::mv(searchRegex)),
96 hashRegex(kj::mv(hashRegex)) {}
97 
98void URLPattern::visitForGc(jsg::GcVisitor& visitor) {
99 visitor.visit(protocolRegex, usernameRegex, passwordRegex, hostnameRegex, portRegex,
100 pathnameRegex, searchRegex, hashRegex);
101}
102 
103kj::StringPtr URLPattern::getProtocol() {
104 return inner.getProtocol().getPattern();
105}
106kj::StringPtr URLPattern::getUsername() {
107 return inner.getUsername().getPattern();
108}
109kj::StringPtr URLPattern::getPassword() {
110 return inner.getPassword().getPattern();
111}
112kj::StringPtr URLPattern::getHostname() {
113 return inner.getHostname().getPattern();
114}
115kj::StringPtr URLPattern::getPort() {
116 return inner.getPort().getPattern();
117}
118kj::StringPtr URLPattern::getPathname() {
119 return inner.getPathname().getPattern();
120}
121kj::StringPtr URLPattern::getSearch() {
122 return inner.getSearch().getPattern();
123}
124kj::StringPtr URLPattern::getHash() {
125 return inner.getHash().getPattern();
126}
127 
128URLPattern::URLPatternInit::operator jsg::UrlPattern::Init() {
129 return {
130 .protocol = mapCopyString(this->protocol),
131 .username = mapCopyString(this->username),
132 .password = mapCopyString(this->password),
133 .hostname = mapCopyString(this->hostname),
134 .port = mapCopyString(this->port),
135 .pathname = mapCopyString(this->pathname),
136 .search = mapCopyString(this->search),
137 .hash = mapCopyString(this->hash),
138 .baseUrl = mapCopyString(this->baseURL),
139 };
140}
141 
142jsg::Ref<URLPattern> URLPattern::constructor(jsg::Lock& js,
143 jsg::Optional<URLPatternInput> input,
144 jsg::Optional<kj::OneOf<kj::String, URLPatternOptions>> baseURL,
145 jsg::Optional<URLPatternOptions> patternOptions) {
146 kj::Maybe<kj::String> base;
147 kj::Maybe<bool> ignoreCase;
148 
149 KJ_IF_SOME(b, baseURL) {
150 KJ_SWITCH_ONEOF(b) {
151 KJ_CASE_ONEOF(str, kj::String) {
152 base = kj::str(str);
153 }
154 KJ_CASE_ONEOF(o, URLPatternOptions) {
155 ignoreCase = o.ignoreCase.orDefault(false);
156 }
157 }
158 }
159 
160 if (ignoreCase == kj::none) {
161 KJ_IF_SOME(o, patternOptions) {
162 ignoreCase = o.ignoreCase.orDefault(false);
163 }
164 }
165 
166 KJ_SWITCH_ONEOF(kj::mv(input).orDefault(URLPatternInit{})) {
167 KJ_CASE_ONEOF(str, kj::String) {
168 KJ_SWITCH_ONEOF(jsg::UrlPattern::tryCompile(str.asPtr(),
169 jsg::UrlPattern::CompileOptions{
170 .baseUrl = base.map([](kj::String& str) { return str.asPtr(); }),
171 .ignoreCase = ignoreCase.orDefault(false),
172 })) {
173 KJ_CASE_ONEOF(err, kj::String) {
174 JSG_FAIL_REQUIRE(TypeError, kj::mv(err));
175 }
176 KJ_CASE_ONEOF(pattern, jsg::UrlPattern) {
177 return create(js, kj::mv(pattern));
178 }
179 }
180 }
181 KJ_CASE_ONEOF(init, URLPatternInit) {
182 KJ_SWITCH_ONEOF(jsg::UrlPattern::tryCompile(init,
183 jsg::UrlPattern::CompileOptions{
184 .baseUrl = base.map([](kj::String& str) { return str.asPtr(); }),
185 .ignoreCase = ignoreCase.orDefault(false),
186 })) {
187 KJ_CASE_ONEOF(err, kj::String) {
188 JSG_FAIL_REQUIRE(TypeError, kj::mv(err));
189 }
190 KJ_CASE_ONEOF(pattern, jsg::UrlPattern) {
191 return create(js, kj::mv(pattern));
192 }
193 }
194 }
195 }
196 KJ_UNREACHABLE;
197}
198 
199bool URLPattern::test(
200 jsg::Lock& js, jsg::Optional<URLPatternInput> input, jsg::Optional<kj::String> baseURL) {
201 return exec(js, kj::mv(input), kj::mv(baseURL)) != kj::none;
202}
203 
204kj::Maybe<URLPattern::URLPatternResult> URLPattern::exec(
205 jsg::Lock& js, jsg::Optional<URLPatternInput> maybeInput, jsg::Optional<kj::String> maybeBase) {
206 auto input = kj::mv(maybeInput).orDefault(URLPattern::URLPatternInit());
207 kj::Vector<URLPattern::URLPatternInput> inputs(2);
208 
209 kj::String protocol = nullptr;
210 kj::String username = nullptr;
211 kj::String password = nullptr;
212 kj::String hostname = nullptr;
213 kj::String port = nullptr;
214 kj::String pathname = nullptr;
215 kj::String search = nullptr;
216 kj::String hash = nullptr;
217 
218 KJ_SWITCH_ONEOF(input) {
219 KJ_CASE_ONEOF(string, kj::String) {
220 KJ_IF_SOME(url, jsg::Url::tryParse(string.asPtr(), maybeBase.map([](kj::String& s) {
221 return s.asPtr();
222 }))) {
223 auto p = url.getProtocol();
224 protocol = kj::str(p.first(p.size() - 1));
225 username = kj::str(url.getUsername());
226 password = kj::str(url.getPassword());
227 hostname = kj::str(url.getHostname());
228 port = kj::str(url.getPort());
229 pathname = kj::str(url.getPathname());
230 search = url.getSearch().size() > 0 ? kj::str(url.getSearch().slice(1)) : kj::String();
231 hash = url.getHash().size() > 0 ? kj::str(url.getHash().slice(1)) : kj::String();
232 } else {
233 return kj::none;
234 }
235 inputs.add(kj::mv(string));
236 KJ_IF_SOME(base, maybeBase) {
237 inputs.add(kj::mv(base));
238 }
239 }
240 KJ_CASE_ONEOF(i, URLPattern::URLPatternInit) {
241 JSG_REQUIRE(
242 maybeBase == kj::none, TypeError, "A baseURL is not allowed when input is an object.");
243 inputs.add(URLPattern::URLPatternInit{
244 .protocol = mapCopyString(i.protocol),
245 .username = mapCopyString(i.username),
246 .password = mapCopyString(i.password),
247 .hostname = mapCopyString(i.hostname),
248 .port = mapCopyString(i.port),
249 .pathname = mapCopyString(i.pathname),
250 .search = mapCopyString(i.search),
251 .hash = mapCopyString(i.hash),
252 .baseURL = mapCopyString(i.baseURL),
253 });
254 
255 jsg::UrlPattern::Init init = {
256 .protocol = kj::mv(i.protocol),
257 .username = kj::mv(i.username),
258 .password = kj::mv(i.password),
259 .hostname = kj::mv(i.hostname),
260 .port = kj::mv(i.port),
261 .pathname = kj::mv(i.pathname),
262 .search = kj::mv(i.search),
263 .hash = kj::mv(i.hash),
264 .baseUrl = kj::mv(i.baseURL),
265 };
266 
267 jsg::UrlPattern::ProcessInitOptions options = {
268 .mode = jsg::UrlPattern::ProcessInitOptions::Mode::URL};
269 
270 KJ_SWITCH_ONEOF(jsg::UrlPattern::processInit(kj::mv(init), kj::mv(options))) {
271 KJ_CASE_ONEOF(err, kj::String) {
272 JSG_FAIL_REQUIRE(TypeError, kj::mv(err));
273 }
274 KJ_CASE_ONEOF(init, jsg::UrlPattern::Init) {
275 protocol = kj::mv(init.protocol).orDefault(kj::String());
276 username = kj::mv(init.username).orDefault(kj::String());
277 password = kj::mv(init.password).orDefault(kj::String());
278 hostname = kj::mv(init.hostname).orDefault(kj::String());
279 port = kj::mv(init.port).orDefault(kj::String());
280 pathname = kj::mv(init.pathname).orDefault(kj::String());
281 search = kj::mv(init.search).orDefault(kj::String());
282 hash = kj::mv(init.hash).orDefault(kj::String());
283 }
284 }
285 }
286 }
287 
288 auto protocolExecResult = execRegex(js, protocolRegex, inner.getProtocol().getNames(), protocol);
289 auto usernameExecResult = execRegex(js, usernameRegex, inner.getUsername().getNames(), username);
290 auto passwordExecResult = execRegex(js, passwordRegex, inner.getPassword().getNames(), password);
291 auto hostnameExecResult = execRegex(js, hostnameRegex, inner.getHostname().getNames(), hostname);
292 auto portExecResult = execRegex(js, portRegex, inner.getPort().getNames(), port);
293 auto pathnameExecResult = execRegex(js, pathnameRegex, inner.getPathname().getNames(), pathname);
294 auto searchExecResult = execRegex(js, searchRegex, inner.getSearch().getNames(), search);
295 auto hashExecResult = execRegex(js, hashRegex, inner.getHash().getNames(), hash);
296 
297 if (protocolExecResult == kj::none || usernameExecResult == kj::none ||
298 passwordExecResult == kj::none || hostnameExecResult == kj::none ||
299 portExecResult == kj::none || pathnameExecResult == kj::none ||
300 searchExecResult == kj::none || hashExecResult == kj::none) {
301 return kj::none;
302 }
303 
304 return URLPattern::URLPatternResult{
305 .inputs = inputs.releaseAsArray(),
306 .protocol = kj::mv(KJ_REQUIRE_NONNULL(protocolExecResult)),
307 .username = kj::mv(KJ_REQUIRE_NONNULL(usernameExecResult)),
308 .password = kj::mv(KJ_REQUIRE_NONNULL(passwordExecResult)),
309 .hostname = kj::mv(KJ_REQUIRE_NONNULL(hostnameExecResult)),
310 .port = kj::mv(KJ_REQUIRE_NONNULL(portExecResult)),
311 .pathname = kj::mv(KJ_REQUIRE_NONNULL(pathnameExecResult)),
312 .search = kj::mv(KJ_REQUIRE_NONNULL(searchExecResult)),
313 .hash = kj::mv(KJ_REQUIRE_NONNULL(hashExecResult)),
314 };
315}
316} // namespace workerd::api