1const std = @import("std");
2const builtin = @import("builtin");
3const extras = @import("extras");
4const unicode_idna = @import("unicode-idna");
5const nio = @import("nio");
6
7pub const URL = struct {
8 href: []const u8,
9 protocol: []const u8,
10 username: []const u8,
11 password: []const u8,
12 hostname: []const u8,
13 hostname_kind: HostKind,
14 port: []const u8,
15 host: []const u8,
16 pathname: []const u8,
17 search: []const u8,
18 hash: []const u8,
19 has_opaque_path: bool,
20
21 pub const HostKind = enum {
22 unset,
23 name,
24 ipv4,
25 ipv6,
26 };
27
28 const Host = union(HostKind) {
29 unset: void,
30 name: []const u8,
31 ipv4: u32,
32 ipv6: u128,
33 };
34
35 /// Caller owns memory and is responsible for freeing `url.href`.
36 pub fn parse(alloc: std.mem.Allocator, input: []const u8, base: ?[]const u8) !URL {
37 if (base) |b| {
38 const b_url = try parseBasic(alloc, b, null, null);
39 defer alloc.free(b_url.href);
40 return parseBasic(alloc, input, &b_url, null);
41 }
42 return parseBasic(alloc, input, null, null);
43 }
44
45 /// https://url.spec.whatwg.org/#concept-basic-url-parser
46 pub fn parseBasic(alloc: std.mem.Allocator, input: []const u8, base: ?*const URL, state_override: ?BasicParserState) error{ InvalidURL, OutOfMemory }!URL {
47 std.debug.assert(state_override == null); // TODO:
48 // input is a scalar value string
49 if (!std.unicode.utf8ValidateSlice(input)) return error.InvalidURL;
50
51 var inputl = nio.AllocatingWriter.init(alloc);
52 defer inputl.deinit();
53 try inputl.writeAll(input);
54
55 // 1.
56 while (inputl.items.len > 0 and is_c0control_or_space(inputl.items[0])) _ = inputl.orderedRemove(0);
57 while (inputl.items.len > 0 and is_c0control_or_space(inputl.last())) inputl.items.len -= 1;
58
59 // 3.
60 while (std.mem.indexOfScalar(u8, inputl.items, '\t')) |i| _ = inputl.orderedRemove(i);
61 while (std.mem.indexOfScalar(u8, inputl.items, '\n')) |i| _ = inputl.orderedRemove(i);
62 while (std.mem.indexOfScalar(u8, inputl.items, '\r')) |i| _ = inputl.orderedRemove(i);
63
64 const length = inputl.items.len;
65
66 // 4.
67 // this doesn't need to be var since the switch below uses label to jump with the new prong
68 var state = state_override orelse .scheme_start;
69
70 // 5.
71 // we always do utf-8
72
73 // 6.
74 var buffer = nio.AllocatingWriter.init(alloc);
75 defer buffer.deinit();
76
77 // 7.
78 var atSignSeen = false;
79 _ = &atSignSeen;
80 var insideBrackets = false;
81 _ = &insideBrackets;
82 var passwordTokenSeen = false;
83 _ = &passwordTokenSeen;
84
85 // 8.
86 // codepoint index into inputl
87 var pointer: usize = 0;
88 // byte index into inputl
89 // moved at the same time as pointer
90 // if pointer is > length, i must not be read
91 var i: usize = 0;
92 // inputl[i], must change in tandem with i
93 var c: u8 = if (length > 0) inputl.items[i] else 0;
94
95 var href = ManyArrayList(8 + 7, u8).init(alloc);
96 defer href.deinit();
97 var hostname_kind: HostKind = .unset;
98 var has_opaque_path = false;
99 // scheme
100 // :
101 // //
102 // username
103 // :
104 // password
105 // @
106 // hostname
107 // :
108 // port
109 // path
110 // ?
111 // query
112 // #
113 // fragment
114
115 // 9.
116 while (true) {
117 switch (state) {
118 .scheme_start => {
119 std.debug.assert(pointer == 0);
120 // 1. If c is an ASCII alpha, append c, lowercased, to buffer, and set state to scheme state.
121 if (i < length and std.ascii.isAlphabetic(c)) {
122 try buffer.writeAll(&.{std.ascii.toLower(c)});
123 state = .scheme;
124 }
125 // 2. Otherwise, if state override is not given, set state to no scheme state and decrease pointer by 1.
126 else if (state_override == null) {
127 state = .no_scheme;
128 continue; // pointer goes to -1 then +1'd
129 }
130 // 3. Otherwise, return failure.
131 else {
132 return error.InvalidURL;
133 }
134 },
135 .scheme => {
136 // 1. If c is an ASCII alphanumeric, U+002B (+), U+002D (-), or U+002E (.), append c, lowercased, to buffer.
137 if (std.ascii.isAlphanumeric(c) or c == '+' or c == '-' or c == '.') {
138 try buffer.writeAll(&.{std.ascii.toLower(c)});
139 }
140 // 2. Otherwise, if c is U+003A (:), then:
141 else if (c == ':') {
142 try href.set(1, ":");
143 // 1. If state override is given, then:
144 if (state_override != null) {
145 @panic("TODO");
146 // 1. If url’s scheme is a special scheme and buffer is not a special scheme, then return.
147 // 2. If url’s scheme is not a special scheme and buffer is a special scheme, then return.
148 // 3. If url includes credentials or has a non-null port, and buffer is "file", then return.
149 // 4. If url’s scheme is "file" and its host is an empty host, then return.
150 }
151 // 2. Set url’s scheme to buffer.
152 try href.set(0, buffer.items);
153 // 3. If state override is given, then:
154 if (state_override != null) {
155 @panic("TODO");
156 // 1. If url’s port is url’s scheme’s default port, then set url’s port to null.
157 // 2. Return.
158 }
159 // 4. Set buffer to the empty string.
160 buffer.items.len = 0;
161 // 5. If url’s scheme is "file", then:
162 if (std.mem.eql(u8, href.items(0), "file")) {
163 // 1. If remaining does not start with "//", special-scheme-missing-following-solidus validation error.
164 {}
165 // 2. Set state to file state.
166 state = .file;
167 }
168 // 6. Otherwise, if url is special, base is non-null, and base’s scheme is url’s scheme:
169 else if (isSchemeSpecial(href.items(0)) and base != null and std.mem.eql(u8, base.?.scheme(), href.items(0))) {
170 // 1. Assert: base is special (and therefore does not have an opaque path).
171 std.debug.assert(base.?.isSpecial());
172 std.debug.assert(!base.?.has_opaque_path);
173 // 2. Set state to special relative or authority state.
174 state = .special_relative_or_authority;
175 }
176 // 7. Otherwise, if url is special, set state to special authority slashes state.
177 else if (isSchemeSpecial(href.items(0))) {
178 state = .special_authority_slashes;
179 }
180 // 8. Otherwise, if remaining starts with an U+002F (/), set state to path or authority state and increase pointer by 1.
181 else if (std.mem.startsWith(u8, inputl.items[i + l(c) ..], "/")) {
182 state = .path_or_authority;
183 i += l(c);
184 c = inputl.items[i];
185 pointer += 1;
186 }
187 // 9. Otherwise, set url’s path to the empty string and set state to opaque path state.
188 else {
189 href.clear(10);
190 has_opaque_path = true;
191 state = .opaque_path;
192 }
193 }
194 // 3. Otherwise, if state override is not given, set buffer to the empty string, state to no scheme state, and start over (from the first code point in input).
195 else if (state_override == null) {
196 buffer.items.len = 0;
197 state = .no_scheme;
198 pointer = 0;
199 i = 0;
200 c = inputl.items[i];
201 continue;
202 }
203 // 4. Otherwise, return failure.
204 else {
205 return error.InvalidURL;
206 }
207 },
208 .no_scheme => {
209 // 1. If base is null, or base has an opaque path and c is not U+0023 (#), missing-scheme-non-relative-URL validation error, return failure.
210 if (base == null or (base != null and base.?.has_opaque_path and c != '#')) {
211 return error.InvalidURL;
212 }
213 // 2. Otherwise, if base has an opaque path and c is U+0023 (#), set url’s scheme to base’s scheme, url’s path to base’s path, url’s query to base’s query, url’s fragment to the empty string, and set state to fragment state.
214 else if (base.?.has_opaque_path and c == '#') {
215 try href.set(0, base.?.scheme());
216 try href.set(1, ":");
217 try href.set(7, base.?.hostname);
218 hostname_kind = base.?.hostname_kind;
219 try href.set(10, base.?.pathname);
220 has_opaque_path = base.?.has_opaque_path;
221 try href.set(12, base.?.query());
222 try href.set(13, "#");
223 try href.set(14, "");
224 state = .fragment;
225 }
226 // 3. Otherwise, if base’s scheme is not "file", set state to relative state and decrease pointer by 1.
227 else if (!std.mem.eql(u8, base.?.scheme(), "file")) {
228 state = .relative;
229 if (pointer == 0) continue; // pointer goes to -1 then +1'd
230 pointer -= 1;
231 i = lastcpi(inputl.items[0..i]);
232 c = inputl.items[i];
233 }
234 // 4. Otherwise, set state to file state and decrease pointer by 1.
235 else {
236 state = .file;
237 if (pointer == 0) continue; // pointer goes to -1 then +1'd
238 pointer -= 1;
239 i = lastcpi(inputl.items[0..i]);
240 c = inputl.items[i];
241 }
242 },
243 .special_relative_or_authority => {
244 // 1. If c is U+002F (/) and remaining starts with U+002F (/), then set state to special authority ignore slashes state and increase pointer by 1.
245 if (c == '/' and std.mem.startsWith(u8, inputl.items[i + l(c) ..], "/")) {
246 state = .special_authority_ignore_slashes;
247 i += l(c);
248 c = inputl.items[i];
249 pointer += 1;
250 }
251 // 2. Otherwise, special-scheme-missing-following-solidus validation error, set state to relative state and decrease pointer by 1.
252 else {
253 state = .relative;
254 pointer -= 1;
255 i = lastcpi(inputl.items[0..i]);
256 c = inputl.items[i];
257 }
258 },
259 .path_or_authority => {
260 // 1. If c is U+002F (/), then set state to authority state.
261 if (c == '/') {
262 state = .authority;
263 }
264 // 2. Otherwise, set state to path state, and decrease pointer by 1.
265 else {
266 state = .path;
267 pointer -= 1;
268 i = lastcpi(inputl.items[0..i]);
269 c = inputl.items[i];
270 }
271 },
272 .relative => {
273 // 1. Assert: base’s scheme is not "file".
274 std.debug.assert(!std.mem.eql(u8, base.?.scheme(), "file"));
275 // 2. Set url’s scheme to base’s scheme.
276 try href.set(0, base.?.scheme());
277 try href.set(1, ":");
278 // 3. If c is U+002F (/), then set state to relative slash state.
279 if (c == '/') {
280 state = .relative_slash;
281 }
282 // 4. Otherwise, if url is special and c is U+005C (\), invalid-reverse-solidus validation error, set state to relative slash state.
283 else if (isSchemeSpecial(href.items(0)) and c == '\\') {
284 state = .relative_slash;
285 }
286 // 5. Otherwise:
287 else {
288 // 1. Set url’s username to base’s username, url’s password to base’s password, url’s host to base’s host, url’s port to base’s port, url’s path to a clone of base’s path, and url’s query to base’s query.
289 try href.set(3, base.?.username);
290 try href.set(5, base.?.password);
291 try href.set(7, base.?.hostname);
292 hostname_kind = base.?.hostname_kind;
293 try href.set(9, base.?.port);
294 try href.set(10, base.?.pathname);
295 has_opaque_path = base.?.has_opaque_path;
296 try href.set(12, base.?.query());
297 // 2. If c is U+003F (?), then set url’s query to the empty string, and state to query state.
298 if (c == '?') {
299 href.clear(12);
300 state = .query;
301 try href.appendSlice(11, &.{c});
302 }
303 // 3. Otherwise, if c is U+0023 (#), set url’s fragment to the empty string and state to fragment state.
304 else if (c == '#') {
305 href.clear(14);
306 state = .fragment;
307 try href.appendSlice(13, &.{c});
308 }
309 // 4. Otherwise, if c is not the EOF code point:
310 else if (i < length) {
311 // 1. Set url’s query to null.
312 href.clear(12);
313 // 2. Shorten url’s path.
314 shortenUrlPath(&href, has_opaque_path);
315 // 3. Set state to path state and decrease pointer by 1.
316 state = .path;
317 if (pointer == 0) continue; // pointer goes to -1 then +1'd
318 pointer -= 1;
319 i = lastcpi(inputl.items[0..i]);
320 c = inputl.items[i];
321 }
322 }
323 },
324 .relative_slash => {
325 // 1. If url is special and c is U+002F (/) or U+005C (\), then:
326 if (isSchemeSpecial(href.items(0)) and (c == '/' or c == '\\')) {
327 // 1. If c is U+005C (\), invalid-reverse-solidus validation error.
328 // 2. Set state to special authority ignore slashes state.
329 state = .special_authority_ignore_slashes;
330 }
331 // 2. Otherwise, if c is U+002F (/), then set state to authority state.
332 else if (c == '/') {
333 state = .authority;
334 }
335 // 3. Otherwise, set url’s username to base’s username, url’s password to base’s password, url’s host to base’s host, url’s port to base’s port, state to path state, and then, decrease pointer by 1.
336 else {
337 try href.set(3, base.?.username);
338 try href.set(5, base.?.password);
339 try href.set(7, base.?.hostname);
340 hostname_kind = base.?.hostname_kind;
341 try href.set(9, base.?.port);
342 state = .path;
343 if (pointer == 0) continue; // pointer goes to -1 then +1'd
344 pointer -= 1;
345 i = lastcpi(inputl.items[0..i]);
346 c = inputl.items[i];
347 }
348 },
349 .special_authority_slashes => {
350 // 1. If c is U+002F (/) and remaining starts with U+002F (/), then set state to special authority ignore slashes state and increase pointer by 1.
351 if (c == '/' and std.mem.startsWith(u8, inputl.items[i + l(c) ..], "/")) {
352 state = .special_authority_ignore_slashes;
353 i += l(c);
354 c = if (i < length) inputl.items[i] else 0;
355 pointer += 1;
356 }
357 // 2. Otherwise, special-scheme-missing-following-solidus validation error, set state to special authority ignore slashes state and decrease pointer by 1.
358 else {
359 state = .special_authority_ignore_slashes;
360 pointer -= 1;
361 i = lastcpi(inputl.items[0..i]);
362 c = inputl.items[i];
363 }
364 },
365 .special_authority_ignore_slashes => {
366 // 1. If c is neither U+002F (/) nor U+005C (\), then set state to authority state and decrease pointer by 1.
367 if (c != '/' and c != '\\') {
368 state = .authority;
369 pointer -= 1;
370 i = lastcpi(inputl.items[0..i]);
371 c = inputl.items[i];
372 }
373 // 2. Otherwise, special-scheme-missing-following-solidus validation error.
374 else {
375 //
376 }
377 },
378 .authority => {
379 // 1. If c is U+0040 (@), then:
380 if (c == '@') {
381 // 1. Invalid-credentials validation error.
382 {}
383 // 2. If atSignSeen is true, then prepend "%40" to buffer.
384 if (atSignSeen) try buffer.insertAt(0, "%40");
385 // 3. Set atSignSeen to true.
386 atSignSeen = true;
387 // 4. For each codePoint in buffer:
388 var it = std.unicode.Utf8View.initUnchecked(buffer.items).iterator();
389 while (it.nextCodepointSlice()) |sl| {
390 // 1. If codePoint is U+003A (:) and passwordTokenSeen is false, then set passwordTokenSeen to true and continue.
391 if (sl[0] == ':' and !passwordTokenSeen) {
392 passwordTokenSeen = true;
393 continue;
394 }
395 // 2. Let encodedCodePoints be the result of running UTF-8 percent-encode codePoint using the userinfo percent-encode set.
396 // 3. If passwordTokenSeen is true, then append encodedCodePoints to url’s password.
397 // 4. Otherwise, append encodedCodePoints to url’s username.
398 if (passwordTokenSeen) {
399 try percentEncodeScalarML(&href, 5, sl, is_userinfo_percent_char);
400 } else {
401 try percentEncodeScalarML(&href, 3, sl, is_userinfo_percent_char);
402 }
403 }
404 // 5. Set buffer to the empty string.
405 buffer.items.len = 0;
406 }
407 // 2. Otherwise, if one of the following is true:
408 // - c is the EOF code point, U+002F (/), U+003F (?), or U+0023 (#)
409 // - url is special and c is U+005C (\)
410 // then:
411 else if ((i == length or c == '/' or c == '?' or c == '#') or (isSchemeSpecial(href.items(0)) and c == '\\')) {
412 // 1. If atSignSeen is true and buffer is the empty string, host-missing validation error, return failure.
413 if (atSignSeen and buffer.items.len == 0) return error.InvalidURL;
414 // 2. Decrease pointer by buffer’s code point length + 1, set buffer to the empty string, and set state to host state.
415 for (0..(std.unicode.utf8CountCodepoints(buffer.items) catch unreachable) + 1) |_| {
416 pointer -= 1;
417 i = lastcpi(inputl.items[0..i]);
418 c = inputl.items[i];
419 }
420 buffer.items.len = 0;
421 state = .host;
422 }
423 // 3. Otherwise, append c to buffer.
424 else {
425 try buffer.writeAll(inputl.items[i..][0..l(c)]);
426 }
427 },
428 .host, .hostname => {
429 // 1. If state override is given and url’s scheme is "file", then decrease pointer by 1 and set state to file host state.
430 if (state_override != null and std.mem.eql(u8, href.items(0), "file")) {
431 pointer -= 1;
432 i = lastcpi(inputl.items[0..i]);
433 c = inputl.items[i];
434 state = .file_host;
435 }
436 // 2. Otherwise, if c is U+003A (:) and insideBrackets is false:
437 else if (c == ':' and insideBrackets == false) {
438 // 1. If buffer is the empty string, host-missing validation error, return failure.
439 if (buffer.items.len == 0) return error.InvalidURL;
440 // 2. If state override is given and state override is hostname state, then return failure.
441 if (state_override != null and state_override.? == .hostname) return error.InvalidURL;
442 // 3. Let host be the result of host parsing buffer with url is not special.
443 // 4. If host is failure, then return failure.
444 const h = try parseHost(alloc, buffer.items, !isSchemeSpecial(href.items(0)));
445 defer if (h == .name) alloc.free(h.name);
446 // 5. Set url’s host to host, buffer to the empty string, and state to port state.
447 try setHost(&href, h);
448 hostname_kind = h;
449 buffer.items.len = 0;
450 state = .port;
451 }
452 // 3. Otherwise, if one of the following is true:
453 // - c is the EOF code point, U+002F (/), U+003F (?), or U+0023 (#)
454 // - url is special and c is U+005C (\)
455 // then decrease pointer by 1, and:
456 else if ((i == length or c == '/' or c == '?' or c == '#') or (isSchemeSpecial(href.items(0)) and c == '\\')) {
457 pointer -= 1;
458 i = lastcpi(inputl.items[0..i]);
459 c = inputl.items[i];
460 // 1. If url is special and buffer is the empty string, host-missing validation error, return failure.
461 if (isSchemeSpecial(href.items(0)) and buffer.items.len == 0) return error.InvalidURL
462 // 2. Otherwise, if state override is given, buffer is the empty string, and either url includes credentials or url’s port is non-null, then return failure.
463 else if (state_override != null and buffer.items.len == 0 and (href.lengths[3] > 0 or href.lengths[5] > 0)) return error.InvalidURL;
464 // 3. Let host be the result of host parsing buffer with url is not special.
465 // 4. If host is failure, then return failure.
466 const h = try parseHost(alloc, buffer.items, !isSchemeSpecial(href.items(0)));
467 defer if (h == .name) alloc.free(h.name);
468 // 5. Set url’s host to host, buffer to the empty string, and state to path start state.
469 try setHost(&href, h);
470 hostname_kind = h;
471 buffer.items.len = 0;
472 state = .path_start;
473 // 6. If state override is given, then return.
474 if (state_override != null) break;
475 }
476 // 4. Otherwise:
477 else {
478 // 1. If c is U+005B ([), then set insideBrackets to true.
479 if (c == '[') insideBrackets = true;
480 // 2. If c is U+005D (]), then set insideBrackets to false.
481 if (c == ']') insideBrackets = false;
482 // 3. Append c to buffer.
483 try buffer.writeAll(inputl.items[i..][0..l(c)]);
484 }
485 },
486 .port => {
487 // 1. If c is an ASCII digit, append c to buffer.
488 if (std.ascii.isDigit(c)) {
489 try buffer.writeAll(inputl.items[i..][0..l(c)]);
490 }
491 // 2. Otherwise, if one of the following is true:
492 // - c is the EOF code point, U+002F (/), U+003F (?), or U+0023 (#);
493 // - url is special and c is U+005C (\); or
494 // - state override is given,
495 // then:
496 else if ((i == length or c == '/' or c == '?' or c == '#') or (isSchemeSpecial(href.items(0)) and c == '\\') or (state_override != null)) {
497 // 1. If buffer is not the empty string:
498 if (buffer.items.len > 0) {
499 // 1. Let port be the mathematical integer value that is represented by buffer in radix-10 using ASCII digits for digits with values 0 through 9.
500 // 2. If port is not a 16-bit unsigned integer, port-out-of-range validation error, return failure.
501 const p = extras.parseDigits(u16, buffer.items, 10) catch return error.InvalidURL;
502 // 3. Set url’s port to null, if port is url’s scheme’s default port; otherwise to port.
503 if (schemeDefaultPort(href.items(0)) != p) try href.print(9, "{d}", .{p});
504 // 4. Set buffer to the empty string.
505 buffer.items.len = 0;
506 // 5. If state override is given, then return.
507 if (state_override != null) break;
508 }
509 // 2. If state override is given, then return failure.
510 if (state_override != null) return error.InvalidURL;
511 // 3. Set state to path start state and decrease pointer by 1.
512 state = .path_start;
513 pointer -= 1;
514 i = lastcpi(inputl.items[0..i]);
515 c = inputl.items[i];
516 }
517 // 3. Otherwise, port-invalid validation error, return failure.
518 else {
519 return error.InvalidURL;
520 }
521 },
522 .file => {
523 // 1. Set url’s scheme to "file".
524 try href.set(0, "file");
525 try href.set(1, ":");
526 // 2. Set url’s host to the empty string.
527 href.clear(7);
528 hostname_kind = .name;
529 // 3. If c is U+002F (/) or U+005C (\), then:
530 if (c == '/' or c == '\\') {
531 // 1. If c is U+005C (\), invalid-reverse-solidus validation error.
532 {}
533 // 2. Set state to file slash state.
534 state = .file_slash;
535 }
536 // 4. Otherwise, if base is non-null and base’s scheme is "file":
537 else if (base != null and std.mem.eql(u8, base.?.scheme(), "file")) {
538 // 1. Set url’s host to base’s host, url’s path to a clone of base’s path, and url’s query to base’s query.
539 try href.set(7, base.?.hostname);
540 hostname_kind = base.?.hostname_kind;
541 try href.set(10, base.?.pathname);
542 has_opaque_path = base.?.has_opaque_path;
543 try href.set(12, base.?.query());
544 // 2. If c is U+003F (?), then set url’s query to the empty string and state to query state.
545 if (c == '?') {
546 href.clear(12);
547 state = .query;
548 try href.appendSlice(11, &.{c});
549 }
550 // 3. Otherwise, if c is U+0023 (#), set url’s fragment to the empty string and state to fragment state.
551 else if (c == '#') {
552 href.clear(14);
553 state = .fragment;
554 try href.appendSlice(13, &.{c});
555 }
556 // 4. Otherwise, if c is not the EOF code point:
557 else if (i < length) {
558 // 1. Set url’s query to null.
559 href.clear(12);
560 // 2. If the code point substring from pointer to the end of input does not start with a Windows drive letter, then shorten url’s path.
561 if (!startsWithWindowsDriveLetter(inputl.items[i..])) {
562 shortenUrlPath(&href, has_opaque_path);
563 }
564 // 3. Otherwise:
565 // This is a (platform-independent) Windows drive letter quirk.
566 else {
567 // 1. File-invalid-Windows-drive-letter validation error.
568 {}
569 // 2. Set url’s path to « ».
570 href.clear(10);
571 }
572 // 4. Set state to path state and decrease pointer by 1.
573 state = .path;
574 if (pointer == 0) continue; // pointer goes to -1 then +1'd
575 pointer -= 1;
576 i = lastcpi(inputl.items[0..i]);
577 c = inputl.items[i];
578 }
579 }
580 // 5. Otherwise, set state to path state, and decrease pointer by 1.
581 else {
582 state = .path;
583 pointer -= 1;
584 i = lastcpi(inputl.items[0..i]);
585 c = inputl.items[i];
586 }
587 },
588 .file_slash => {
589 // 1. If c is U+002F (/) or U+005C (\), then:
590 if (c == '/' or c == '\\') {
591 // 1. If c is U+005C (\), invalid-reverse-solidus validation error.
592 {}
593 // 2. Set state to file host state.
594 state = .file_host;
595 }
596 // 2. Otherwise:
597 else {
598 // 1. If base is non-null and base’s scheme is "file", then:
599 if (base != null and std.mem.eql(u8, base.?.scheme(), "file")) {
600 // 1. Set url’s host to base’s host.
601 try href.set(7, base.?.hostname);
602 hostname_kind = base.?.hostname_kind;
603 // 2. If the code point substring from pointer to the end of input does not start with a Windows drive letter and base’s path[0] is a normalized Windows drive letter, then append base’s path[0] to url’s path.
604 // > This is a (platform-independent) Windows drive letter quirk.
605 if (!startsWithWindowsDriveLetter(inputl.items[i..])) {
606 const base_path0 = nthScalarItem(u8, base.?.pathname, '/', 1);
607 if (isNormalizedWindowsDriveLetter(base_path0)) {
608 try href.appendSlice(10, "/");
609 try href.appendSlice(10, base_path0);
610 }
611 }
612 }
613 // 2. Set state to path state, and decrease pointer by 1.
614 state = .path;
615 if (pointer == 0) continue; // pointer goes to -1 then +1'd
616 pointer -= 1;
617 i = lastcpi(inputl.items[0..i]);
618 c = inputl.items[i];
619 }
620 },
621 .file_host => {
622 // 1. If c is the EOF code point, U+002F (/), U+005C (\), U+003F (?), or U+0023 (#), then decrease pointer by 1 and then:
623 if (i == length or c == '/' or c == '\\' or c == '?' or c == '#') {
624 pointer -= 1;
625 i = lastcpi(inputl.items[0..i]);
626 c = inputl.items[i];
627 // 1. If state override is not given and buffer is a Windows drive letter, file-invalid-Windows-drive-letter-host validation error, set state to path state.
628 // > This is a (platform-independent) Windows drive letter quirk. buffer is not reset here and instead used in the path state.
629 if (state_override == null and isWindowsDriveLetter(buffer.items)) {
630 state = .path;
631 }
632 // 2. Otherwise, if buffer is the empty string, then:
633 else if (buffer.items.len == 0) {
634 // 1. Set url’s host to the empty string.
635 href.clear(7);
636 hostname_kind = .name;
637 // 2. If state override is given, then return.
638 if (state_override != null) break;
639 // 3. Set state to path start state.
640 state = .path_start;
641 }
642 // 3. Otherwise, run these steps:
643 else {
644 // 1. Let host be the result of host parsing buffer with url is not special.
645 // 2. If host is failure, then return failure.
646 var h = try parseHost(alloc, buffer.items, !isSchemeSpecial(href.items(0)));
647 defer if (h == .name) alloc.free(h.name);
648 // 3. If host is "localhost", then set host to the empty string.
649 if (h == .name and std.mem.eql(u8, h.name, "localhost")) {
650 alloc.free(h.name);
651 h = .{ .name = "" };
652 }
653 // 4. Set url’s host to host.
654 try setHost(&href, h);
655 hostname_kind = h;
656 // 5. If state override is given, then return.
657 if (state_override != null) break;
658 // 6. Set buffer to the empty string and state to path start state.
659 buffer.items.len = 0;
660 state = .path_start;
661 }
662 }
663 // 2. Otherwise, append c to buffer.
664 else {
665 try buffer.writeAll(inputl.items[i..][0..l(c)]);
666 }
667 },
668 .path_start => {
669 // 1. If url is special, then:
670 if (isSchemeSpecial(href.items(0))) {
671 // 1. If c is U+005C (\), invalid-reverse-solidus validation error.
672 {}
673 // 2. Set state to path state.
674 state = .path;
675 // 3. If c is neither U+002F (/) nor U+005C (\), then decrease pointer by 1.
676 if (c != '/' and c != '\\') {
677 pointer -= 1;
678 i = lastcpi(inputl.items[0..i]);
679 c = inputl.items[i];
680 }
681 }
682 // 2. Otherwise, if state override is not given and c is U+003F (?), set url’s query to the empty string and state to query state.
683 else if (state_override == null and c == '?') {
684 href.clear(12);
685 state = .query;
686 try href.appendSlice(11, &.{c});
687 }
688 // 3. Otherwise, if state override is not given and c is U+0023 (#), set url’s fragment to the empty string and state to fragment state.
689 else if (state_override == null and c == '#') {
690 href.clear(14);
691 state = .fragment;
692 try href.appendSlice(13, &.{c});
693 }
694 // 4. Otherwise, if c is not the EOF code point:
695 else if (i < length) {
696 // 1. Set state to path state.
697 state = .path;
698 // 2. If c is not U+002F (/), then decrease pointer by 1.
699 if (c != '/') {
700 pointer -= 1;
701 i = lastcpi(inputl.items[0..i]);
702 c = inputl.items[i];
703 }
704 }
705 // 5. Otherwise, if state override is given and url’s host is null, append the empty string to url’s path.
706 else if (state_override != null) {
707 @panic("TODO");
708 }
709 },
710 .path => {
711 const is_lsep = c == '/';
712 const is_rsep = isSchemeSpecial(href.items(0)) and c == '\\';
713 // 1. If one of the following is true:
714 // - c is the EOF code point or U+002F (/)
715 // - url is special and c is U+005C (\)
716 // - state override is not given and c is U+003F (?) or U+0023 (#)
717 // then:
718 if ((i == length or is_lsep) or (is_rsep) or (state_override == null and (c == '?' or c == '#'))) {
719 // 1. If url is special and c is U+005C (\), invalid-reverse-solidus validation error.
720 {}
721 // 2. If buffer is a double-dot URL path segment, then:
722 if (isDoubleDotPathSeg(buffer.items)) {
723 // 1. Shorten url’s path.
724 shortenUrlPath(&href, has_opaque_path);
725 // 2. If neither c is U+002F (/), nor url is special and c is U+005C (\), append the empty string to url’s path.
726 // > This means that for input /usr/.. the result is / and not a lack of a path.
727 if (!is_lsep and !is_rsep) {
728 try href.appendSlice(10, "/");
729 }
730 }
731 // 3. Otherwise, if buffer is a single-dot URL path segment and if neither c is U+002F (/), nor url is special and c is U+005C (\), append the empty string to url’s path.
732 else if (isSingleDotPathSeg(buffer.items) and !is_lsep and !is_rsep) {
733 try href.appendSlice(10, "/");
734 }
735 // 4. Otherwise, if buffer is not a single-dot URL path segment, then:
736 else if (!isSingleDotPathSeg(buffer.items)) {
737 // 1. If url’s scheme is "file", url’s path is empty, and buffer is a Windows drive letter, then replace the second code point in buffer with U+003A (:).
738 // > This is a (platform-independent) Windows drive letter quirk.
739 if (std.mem.eql(u8, href.items(0), "file") and href.lengths[10] == 0 and isWindowsDriveLetter(buffer.items)) {
740 buffer.items[1] = ':';
741 }
742 // 2. Append buffer to url’s path.
743 try href.appendSlice(10, "/");
744 try href.appendSlice(10, buffer.items);
745 }
746 // 5. Set buffer to the empty string.
747 buffer.items.len = 0;
748 // 6. If c is U+003F (?), then set url’s query to the empty string and state to query state.
749 if (c == '?') {
750 href.clear(12);
751 state = .query;
752 try href.appendSlice(11, &.{c});
753 }
754 // 7. If c is U+0023 (#), then set url’s fragment to the empty string and state to fragment state.
755 if (c == '#') {
756 href.clear(14);
757 state = .fragment;
758 try href.appendSlice(13, &.{c});
759 }
760 }
761 // 2. Otherwise, run these steps:
762 else {
763 // 1. If c is not a URL code point and not U+0025 (%), invalid-URL-unit validation error.
764 {}
765 // 2. If c is U+0025 (%) and remaining does not start with two ASCII hex digits, invalid-URL-unit validation error.
766 {}
767 // 3. UTF-8 percent-encode c using the path percent-encode set and append the result to buffer.
768 if (is_path_percent_char(c)) {
769 const pe = try percentEncode(alloc, inputl.items[i..][0..l(c)], is_path_percent_char);
770 defer alloc.free(pe);
771 try buffer.writeAll(pe);
772 } else {
773 try buffer.writeAll(inputl.items[i..][0..l(c)]);
774 }
775 }
776 },
777 .opaque_path => {
778 // 1. If c is U+003F (?), then set url’s query to the empty string and state to query state.
779 if (c == '?') {
780 href.clear(12);
781 state = .query;
782 try href.appendSlice(11, &.{c});
783 }
784 // 2. Otherwise, if c is U+0023 (#), then set url’s fragment to the empty string and state to fragment state.
785 else if (c == '#') {
786 href.clear(14);
787 state = .fragment;
788 try href.appendSlice(13, &.{c});
789 }
790 // 3. Otherwise, if c is U+0020 SPACE:
791 else if (c == ' ') {
792 // 1. If remaining starts with U+003F (?) or U+003F (#), then append "%20" to url’s path.
793 const rem = inputl.items[i + l(c) ..];
794 if (std.mem.startsWith(u8, rem, "?") or std.mem.startsWith(u8, rem, "#")) {
795 try href.appendSlice(10, "%20");
796 }
797 // 2. Otherwise, append U+0020 SPACE to url’s path.
798 else {
799 try href.appendSlice(10, " ");
800 }
801 }
802 // 4. Otherwise, if c is not the EOF code point:
803 else if (i < length) {
804 // 1. If c is not a URL code point and not U+0025 (%), invalid-URL-unit validation error.
805 {}
806 // 2. If c is U+0025 (%) and remaining does not start with two ASCII hex digits, invalid-URL-unit validation error.
807 {}
808 // 3. UTF-8 percent-encode c using the C0 control percent-encode set and append the result to url’s path.
809 try percentEncodeScalarML(&href, 10, inputl.items[i..][0..l(c)], is_c0control_percent_char);
810 }
811 },
812 .query => {
813 // 1. If encoding is not UTF-8 and one of the following is true:
814 // - url is not special
815 // - url’s scheme is "ws" or "wss"
816 // then set encoding to UTF-8.
817 {}
818 // 2. If one of the following is true:
819 // - state override is not given and c is U+0023 (#)
820 // - c is the EOF code point
821 // then:
822 if ((state_override == null and c == '#') or (i == length)) {
823 // 1. Let queryPercentEncodeSet be the special-query percent-encode set if url is special; otherwise the query percent-encode set.
824 // 2. Percent-encode after encoding, with encoding, buffer, and queryPercentEncodeSet, and append the result to url’s query.
825 // > This operation cannot be invoked code-point-for-code-point due to the stateful ISO-2022-JP encoder.
826 if (isSchemeSpecial(href.items(0))) {
827 try percentEncodeML(&href, 12, buffer.items, is_special_query_percent_char);
828 } else {
829 try percentEncodeML(&href, 12, buffer.items, is_query_percent_char);
830 }
831 // 3. Set buffer to the empty string.
832 buffer.items.len = 0;
833 // 4. If c is U+0023 (#), then set url’s fragment to the empty string and state to fragment state.
834 if (c == '#') {
835 href.clear(14);
836 state = .fragment;
837 try href.appendSlice(13, &.{c});
838 }
839 }
840 // 3. Otherwise, if c is not the EOF code point:
841 else if (i < length) {
842 // 1. If c is not a URL code point and not U+0025 (%), invalid-URL-unit validation error.
843 {}
844 // 2. If c is U+0025 (%) and remaining does not start with two ASCII hex digits, invalid-URL-unit validation error.
845 {}
846 // 3. Append c to buffer.
847 try buffer.writeAll(inputl.items[i..][0..l(c)]);
848 }
849 },
850 .fragment => {
851 // 1. If c is not the EOF code point, then:
852 if (i < length) {
853 // 1. If c is not a URL code point and not U+0025 (%), invalid-URL-unit validation error.
854 {}
855 // 2. If c is U+0025 (%) and remaining does not start with two ASCII hex digits, invalid-URL-unit validation error.
856 {}
857 // 3. UTF-8 percent-encode c using the fragment percent-encode set and append the result to url’s fragment.
858 try percentEncodeScalarML(&href, 14, inputl.items[i..][0..l(c)], is_fragment_percent_char);
859 }
860 },
861 }
862
863 // If after a run pointer points to the EOF code point, go to the next step. Otherwise, increase pointer by 1 and continue with the state machine.
864 if (i == length) break;
865 i += l(c);
866 c = if (i < length) inputl.items[i] else 0;
867 pointer += 1;
868 }
869
870 if (hostname_kind != .unset) {
871 try href.appendSlice(2, "//");
872 }
873 if (href.lengths[5] > 0) {
874 try href.set(4, ":");
875 }
876 if (href.lengths[3] > 0 or href.lengths[5] > 0) {
877 try href.set(6, "@");
878 }
879 if (href.lengths[9] > 0) {
880 try href.set(8, ":");
881 }
882 if (href.lengths[12] > 0) {
883 try href.set(11, "?");
884 }
885 if (href.lengths[14] > 0) {
886 try href.set(13, "#");
887 }
888
889 var path_offset: usize = 0;
890 if (hostname_kind == .unset and std.mem.startsWith(u8, href.items(10), "//")) {
891 try href.replace(10, 0, 0, "/.");
892 path_offset += 2;
893 }
894
895 const _href = try href.list.toOwnedSlice();
896
897 const url: URL = .{
898 .href = _href,
899 .protocol = _href[0..extras.sum(usize, href.lengths[0..2])],
900 .username = _href[extras.sum(usize, href.lengths[0..3])..][0..href.lengths[3]],
901 .password = _href[extras.sum(usize, href.lengths[0..5])..][0..href.lengths[5]],
902 .hostname = _href[extras.sum(usize, href.lengths[0..7])..][0..href.lengths[7]],
903 .hostname_kind = hostname_kind,
904 .port = _href[extras.sum(usize, href.lengths[0..9])..][0..href.lengths[9]],
905 .host = _href[extras.sum(usize, href.lengths[0..7])..][0..extras.sum(usize, href.lengths[7..][0..if (href.lengths[9] == 0) 1 else 3])],
906 .pathname = _href[extras.sum(usize, href.lengths[0..10])..][0..href.lengths[10]][path_offset..],
907 .search = if (href.lengths[12] == 0) "" else _href[extras.sum(usize, href.lengths[0..11])..][0..extras.sum(usize, href.lengths[11..][0..2])],
908 .hash = if (href.lengths[14] == 0) "" else _href[extras.sum(usize, href.lengths[0..13])..][0..extras.sum(usize, href.lengths[13..][0..2])],
909 .has_opaque_path = has_opaque_path,
910 };
911 return url;
912 }
913
914 const BasicParserState = enum {
915 scheme_start,
916 scheme,
917 no_scheme,
918 special_relative_or_authority,
919 path_or_authority,
920 relative,
921 relative_slash,
922 special_authority_slashes,
923 special_authority_ignore_slashes,
924 authority,
925 host,
926 hostname,
927 port,
928 file,
929 file_slash,
930 file_host,
931 path_start,
932 path,
933 opaque_path,
934 query,
935 fragment,
936 };
937
938 pub fn isSpecial(u: *const URL) bool {
939 return isSchemeSpecial(u.scheme());
940 }
941
942 pub fn scheme(u: *const URL) []const u8 {
943 return u.protocol[0 .. u.protocol.len - 1];
944 }
945
946 pub fn query(u: *const URL) []const u8 {
947 if (u.search.len == 0) return "";
948 return u.search[1..]; // search includes '?'
949 }
950
951 pub fn fragment(u: *const URL) []const u8 {
952 if (u.hash.len == 0) return "";
953 return u.hash[1..]; // hash includes '#'
954 }
955
956 pub fn hostFancy(u: *const URL) Host {
957 return switch (u.hostname_kind) {
958 .unset => .{ .unset = {} },
959 .name => .{ .name = u.hostname },
960 .ipv4 => .{ .ipv4 = parseIPv4(u.hostname) catch unreachable },
961 .ipv6 => .{ .ipv6 = parseIPv6(u.hostname[1 .. u.hostname.len - 1]) catch unreachable },
962 };
963 }
964
965 pub fn portFancy(u: *const URL) ?u16 {
966 if (u.port.len > 0) return extras.parseDigits(u16, u.port, 10) catch unreachable;
967 const s = u.scheme();
968 if (std.mem.eql(u8, s, "http")) return 80;
969 if (std.mem.eql(u8, s, "https")) return 443;
970 if (std.mem.eql(u8, s, "file")) return null;
971 if (std.mem.eql(u8, s, "ws")) return 80;
972 if (std.mem.eql(u8, s, "wss")) return 443;
973 if (std.mem.eql(u8, s, "ftp")) return 21;
974 return null;
975 }
976
977 pub fn searchParams(u: *const URL, allocator: std.mem.Allocator) !SearchParams {
978 return .initFromString(allocator, u.query());
979 }
980
981 pub fn isHostLocal(u: *const URL) bool {
982 switch (u.hostFancy()) {
983 .unset => {
984 return false;
985 },
986 .name => |nm| {
987 if (std.mem.eql(u8, nm, "localhost")) return true;
988 if (std.mem.endsWith(u8, nm, ".localhost")) return true;
989 if (std.mem.endsWith(u8, nm, ".test")) return true;
990 if (std.mem.endsWith(u8, nm, ".example")) return true;
991 if (std.mem.endsWith(u8, nm, ".invalid")) return true;
992 if (std.mem.endsWith(u8, nm, ".home.arpa")) return true;
993 if (std.mem.endsWith(u8, nm, ".local")) return true;
994 return false;
995 },
996 .ipv4 => |ip| {
997 const parts: [4]u8 = @bitCast(ip);
998 if (parts[0] == 127) return true; // 127.0.0.0/8
999 return false; // TODO
1000 },
1001 .ipv6 => |ip| {
1002 const parts: [8]u16 = @bitCast(ip);
1003 _ = parts;
1004 return false; // TODO
1005 },
1006 }
1007 }
1008};
1009
1010pub const SearchParams = struct {
1011 const string = []const u8;
1012 const KV = struct { key: string, value: string };
1013
1014 allocator: std.mem.Allocator,
1015 inner: std.MultiArrayList(KV),
1016
1017 pub fn init(alloc: std.mem.Allocator) SearchParams {
1018 return .{
1019 .allocator = alloc,
1020 .inner = std.MultiArrayList(KV){},
1021 };
1022 }
1023
1024 pub fn initFromString(alloc: std.mem.Allocator, input: string) !SearchParams {
1025 var uv = SearchParams.init(alloc);
1026 var iter = RawIterator.init(input, alloc);
1027 while (try iter.next()) |piece| {
1028 const k = piece[0];
1029 const v = piece[1];
1030 try uv.append(k, v);
1031 }
1032 return uv;
1033 }
1034
1035 pub fn deinit(self: *SearchParams) void {
1036 for (self.inner.items(.value)) |x| self.allocator.free(x);
1037 self.inner.clearAndFree(self.allocator);
1038 }
1039
1040 const RawIterator = struct {
1041 input: string,
1042 iter: std.mem.SplitIterator(u8, .scalar),
1043 alloc: std.mem.Allocator,
1044
1045 fn init(input: string, alloc: std.mem.Allocator) RawIterator {
1046 return .{
1047 .input = input,
1048 .iter = std.mem.splitScalar(u8, input, '&'),
1049 .alloc = alloc,
1050 };
1051 }
1052
1053 fn next(ri: *RawIterator) !?struct { string, string } {
1054 while (ri.iter.next()) |piece| {
1055 if (piece.len == 0) continue;
1056 var jter = std.mem.splitScalar(u8, piece, '=');
1057 const k = jter.next().?;
1058 var v = jter.rest();
1059 std.mem.replaceScalar(u8, @constCast(v), '+', ' ');
1060 v = std.Uri.percentDecodeInPlace(try ri.alloc.dupe(u8, v));
1061 return .{ k, v };
1062 }
1063 return null;
1064 }
1065 };
1066
1067 pub fn set(self: *SearchParams, key: string, value: string) !void {
1068 const keys = self.inner.items(.key);
1069 const values = self.inner.items(.value);
1070 var idx = extras.indexOfSlice(u8, keys, key) orelse {
1071 return try self.append(key, value);
1072 };
1073 values[idx] = value;
1074 idx += 1;
1075 while (true) {
1076 if (idx >= self.inner.len) break;
1077 if (!std.mem.eql(u8, keys[idx], key)) {
1078 idx += 1;
1079 continue;
1080 }
1081 self.inner.orderedRemove(idx);
1082 }
1083 }
1084
1085 pub fn append(self: *SearchParams, key: string, value: string) !void {
1086 try self.inner.append(self.allocator, .{ .key = key, .value = value });
1087 }
1088
1089 pub fn get(self: *const SearchParams, key: string) ?string {
1090 const keys = self.inner.items(.key);
1091 const idx = extras.indexOfSlice(u8, keys, key) orelse return null;
1092 const values = self.inner.items(.value);
1093 return values[idx];
1094 }
1095
1096 pub fn getAll(self: *const SearchParams, key: string) !?[]const string {
1097 const keys = self.inner.items(.key);
1098 const values = self.inner.items(.value);
1099 var backer: [256]usize = undefined;
1100 @memset(&backer, 0);
1101 var bset = std.bit_set.DynamicBitSetUnmanaged{ .bit_length = self.inner.len, .masks = &backer };
1102 var prev: bool = false;
1103 var phase_count: usize = 0;
1104 for (keys, 0..) |k, i| {
1105 const this = std.mem.eql(u8, k, key);
1106 defer prev = this;
1107 if (this != prev) phase_count += 1;
1108 if (this == true) bset.set(i);
1109 }
1110 if (prev == true) phase_count += 1;
1111 const count = bset.count();
1112 if (count == 0) return null;
1113 if (count == 1) return values[bset.findFirstSet().?..][0..1];
1114 if (phase_count == 2) return values[bset.findFirstSet().? .. findLastSet(bset).? + 1];
1115 const items = try self.allocator.alloc(string, count);
1116 var iter = bset.iterator(.{});
1117 var i: usize = 0;
1118 while (iter.next()) |idx| : (i += 1) items[i] = values[idx];
1119 return items;
1120 }
1121
1122 pub fn take(self: *SearchParams, key: string) ?string {
1123 const keys = self.inner.items(.key);
1124 const idx = extras.indexOfSlice(u8, keys, key) orelse return null;
1125 const values = self.inner.items(.value);
1126 defer self.inner.orderedRemove(idx);
1127 return values[idx];
1128 }
1129
1130 /// if 'value' is null, will delete all the values with key 'key'
1131 pub fn delete(self: *SearchParams, key: string, value: ?string) void {
1132 var offset: usize = 0;
1133 while (true) {
1134 const keys = self.inner.items(.key);
1135 const values = self.inner.items(.value);
1136 var idx = extras.indexOfSlice(u8, keys[offset..], key) orelse break;
1137 idx += offset;
1138 if (idx >= self.inner.len) break;
1139 if (!std.mem.eql(u8, keys[idx], key)) break;
1140 if (value) |v| {
1141 if (!std.mem.eql(u8, values[idx], v)) {
1142 offset += 1;
1143 continue;
1144 }
1145 }
1146 self.inner.orderedRemove(idx);
1147 }
1148 }
1149
1150 pub fn has(self: *const SearchParams, key: string, value: ?string) bool {
1151 const keys = self.inner.items(.key);
1152 if (value == null) {
1153 return extras.indexOfSlice(u8, keys, key) != null;
1154 }
1155 const values = self.inner.items(.value);
1156 for (keys, 0..) |k, i| {
1157 if (std.mem.eql(u8, key, k)) {
1158 if (std.mem.eql(u8, value.?, values[i])) {
1159 return true;
1160 }
1161 }
1162 }
1163 return false;
1164 }
1165
1166 pub fn size(self: *const SearchParams) usize {
1167 return self.inner.len;
1168 }
1169
1170 pub fn encode(self: *const SearchParams) ![]u8 {
1171 const alloc = self.allocator;
1172 var list = nio.AllocatingWriter.init(alloc);
1173 errdefer list.deinit();
1174 for (self.inner.items(.key), self.inner.items(.value), 0..) |k, v, i| {
1175 if (i > 0) try list.writeAll("&");
1176 try percentEncodeAL(&list, k, is_formurlencoded_percent_char);
1177 try list.writeAll("=");
1178 try percentEncodeAL(&list, v, is_formurlencoded_percent_char);
1179 }
1180 list.items.len -= std.mem.replace(u8, list.items, "%20", "+", list.items) * 2;
1181 return list.toOwnedSlice();
1182 }
1183
1184 pub fn IteratorFor(comptime field: string) type {
1185 return struct {
1186 raw: RawIterator,
1187
1188 const Self = @This();
1189
1190 pub fn init(alloc: std.mem.Allocator, input: string) Self {
1191 return .{
1192 .raw = .{
1193 .input = input,
1194 .iter = std.mem.splitScalar(u8, input, '&'),
1195 .alloc = alloc,
1196 },
1197 };
1198 }
1199
1200 pub fn next(self: *Self) !?string {
1201 while (try self.raw.next()) |piece| {
1202 const k = piece[0];
1203 const v = piece[1];
1204 if (!std.mem.eql(u8, k, field)) continue;
1205 return v;
1206 }
1207 return null;
1208 }
1209 };
1210 }
1211
1212 /// Finds the index of the last set bit.
1213 /// If no bits are set, returns null.
1214 fn findLastSet(self: std.bit_set.DynamicBitSetUnmanaged) ?usize {
1215 if (self.bit_length == 0) return null;
1216 const bs = @bitSizeOf(usize);
1217 var len = self.bit_length / bs;
1218 if (self.bit_length % bs != 0) len += 1;
1219 var offset: usize = len * bs;
1220 var idx: usize = len - 1;
1221 while (self.masks[idx] == 0) : (idx -= 1) {
1222 offset -= bs;
1223 if (idx == 0) return null;
1224 }
1225 offset -= @clz(self.masks[idx]);
1226 offset -= 1;
1227 return offset;
1228 }
1229};
1230
1231/// https://url.spec.whatwg.org/#special-scheme
1232fn isSchemeSpecial(scheme: []const u8) bool {
1233 if (std.mem.eql(u8, scheme, "ftp")) return true;
1234 if (std.mem.eql(u8, scheme, "file")) return true;
1235 if (std.mem.eql(u8, scheme, "http")) return true;
1236 if (std.mem.eql(u8, scheme, "https")) return true;
1237 if (std.mem.eql(u8, scheme, "ws")) return true;
1238 if (std.mem.eql(u8, scheme, "wss")) return true;
1239 return false;
1240}
1241
1242/// https://url.spec.whatwg.org/#default-port
1243fn schemeDefaultPort(scheme: []const u8) ?u16 {
1244 if (std.mem.eql(u8, scheme, "ftp")) return 21;
1245 if (std.mem.eql(u8, scheme, "file")) return null;
1246 if (std.mem.eql(u8, scheme, "http")) return 80;
1247 if (std.mem.eql(u8, scheme, "https")) return 443;
1248 if (std.mem.eql(u8, scheme, "ws")) return 80;
1249 if (std.mem.eql(u8, scheme, "wss")) return 443;
1250 return null;
1251}
1252
1253/// https://url.spec.whatwg.org/#double-dot-path-segment
1254fn isDoubleDotPathSeg(segment: []const u8) bool {
1255 if (std.mem.eql(u8, segment, "..")) return true;
1256 if (std.ascii.eqlIgnoreCase(segment, ".%2e")) return true;
1257 if (std.ascii.eqlIgnoreCase(segment, "%2e.")) return true;
1258 if (std.ascii.eqlIgnoreCase(segment, "%2e%2e")) return true;
1259 return false;
1260}
1261
1262/// https://url.spec.whatwg.org/#single-dot-path-segment
1263fn isSingleDotPathSeg(segment: []const u8) bool {
1264 if (std.mem.eql(u8, segment, ".")) return true;
1265 if (std.ascii.eqlIgnoreCase(segment, "%2e")) return true;
1266 return false;
1267}
1268
1269/// https://url.spec.whatwg.org/#windows-drive-letter
1270fn isWindowsDriveLetter(buffer: []const u8) bool {
1271 if (buffer.len != 2) return false;
1272 if (!std.ascii.isAlphabetic(buffer[0])) return false;
1273 if (!(buffer[1] == ':' or buffer[1] == '|')) return false;
1274 return true;
1275}
1276
1277/// https://url.spec.whatwg.org/#normalized-windows-drive-letter
1278fn isNormalizedWindowsDriveLetter(buffer: []const u8) bool {
1279 if (buffer.len != 2) return false;
1280 if (!std.ascii.isAlphabetic(buffer[0])) return false;
1281 if (!(buffer[1] == ':')) return false;
1282 return true;
1283}
1284
1285/// https://url.spec.whatwg.org/#start-with-a-windows-drive-letter
1286fn startsWithWindowsDriveLetter(buffer: []const u8) bool {
1287 if (buffer.len < 2) return false;
1288 if (!std.ascii.isAlphabetic(buffer[0])) return false;
1289 if (!(buffer[1] == ':' or buffer[1] == '|')) return false;
1290 if (buffer.len == 2) return true;
1291 if (!(buffer[2] == '/' or buffer[2] == '\\' or buffer[2] == '?' or buffer[2] == '#')) return false;
1292 return true;
1293}
1294
1295/// https://url.spec.whatwg.org/#concept-host-parser
1296fn parseHost(allocator: std.mem.Allocator, input: []u8, isOpaque: bool) !URL.Host {
1297 // 1. If input starts with U+005B ([), then:
1298 if (std.mem.startsWith(u8, input, "[")) {
1299 // 1. If input does not end with U+005D (]), IPv6-unclosed validation error, return failure.
1300 if (!std.mem.endsWith(u8, input, "]")) return error.InvalidURL;
1301 // 2. Return the result of IPv6 parsing input with its leading U+005B ([) and trailing U+005D (]) removed.
1302 const adr = try parseIPv6(input[1 .. input.len - 1]);
1303 return .{ .ipv6 = adr };
1304 }
1305 // 2. If isOpaque is true, then return the result of opaque-host parsing input.
1306 if (isOpaque) return .{ .name = try parseHostOpaque(allocator, input) };
1307 // 3. Assert: input is not the empty string.
1308 std.debug.assert(input.len > 0);
1309 // 4. Let domain be the result of running UTF-8 decode without BOM on the percent-decoding of input.
1310 // > Alternatively UTF-8 decode without BOM or fail can be used, coupled with an early return for failure, as domain to ASCII fails on U+FFFD (�).
1311 const domain = try percentDecode(allocator, input);
1312 defer allocator.free(domain);
1313 if (!std.unicode.utf8ValidateSlice(domain)) return error.InvalidURL;
1314 // 5. Let asciiDomain be the result of running domain to ASCII with domain and false.
1315 // 6. If asciiDomain is failure, then return failure.
1316 const asciidomain = try domainToAscii(allocator, domain, false);
1317 // 7. If asciiDomain ends in a number, then return the result of IPv4 parsing asciiDomain.
1318 if (endsInANumber(asciidomain)) {
1319 defer allocator.free(asciidomain);
1320 const adr = try parseIPv4(asciidomain);
1321 return .{ .ipv4 = adr };
1322 }
1323 // 8. Return asciiDomain.
1324 return .{ .name = asciidomain };
1325}
1326
1327/// https://url.spec.whatwg.org/#concept-ipv6-parser
1328pub fn parseIPv6(input: []const u8) !u128 {
1329 // 1. Let address be a new IPv6 address whose pieces are all 0.
1330 var address: [8]u16 = @splat(0);
1331 _ = &address;
1332 // 2. Let pieceIndex be 0.
1333 var pieceIndex: u8 = 0;
1334 _ = &pieceIndex;
1335 // 3. Let compress be null.
1336 var compress: ?u8 = null;
1337 _ = &compress;
1338 // 4. Let pointer be a pointer for input.
1339 std.debug.assert(extras.matchesAll(u8, input, std.ascii.isAscii));
1340 var pointer: usize = 0;
1341 // 5. If c is U+003A (:), then:
1342 if (input.len > 0 and input[pointer] == ':') {
1343 // 1. If remaining does not start with U+003A (:), IPv6-invalid-compression validation error, return failure.
1344 if (!std.mem.startsWith(u8, input[pointer + 1 ..], ":")) return error.InvalidURL;
1345 // 2. Increase pointer by 2.
1346 pointer += 2;
1347 // 3. Increase pieceIndex by 1 and then set compress to pieceIndex.
1348 pieceIndex += 1;
1349 compress = pieceIndex;
1350 }
1351 // 6. While c is not the EOF code point:
1352 while (pointer != input.len) {
1353 // 1. If pieceIndex is 8, IPv6-too-many-pieces validation error, return failure.
1354 if (pieceIndex == 8) return error.InvalidURL;
1355 // 2. If c is U+003A (:), then:
1356 if (input[pointer] == ':') {
1357 // 1. If compress is non-null, IPv6-multiple-compression validation error, return failure.
1358 if (compress != null) return error.InvalidURL;
1359 // 2. Increase pointer and pieceIndex by 1, set compress to pieceIndex, and then continue.
1360 pointer += 1;
1361 pieceIndex += 1;
1362 compress = pieceIndex;
1363 continue;
1364 }
1365 // 3. Let value and length be 0.
1366 var value: u16 = 0;
1367 var length: usize = 0;
1368 // 4. While length is less than 4 and c is an ASCII hex digit, set value to value × 0x10 + c interpreted as hexadecimal number, and increase pointer and length by 1.
1369 const hex_alpha_upper = "0123456789ABCDEF";
1370 const hex_alpha_lower = "0123456789abcdef";
1371 while (length < 4 and pointer < input.len and std.ascii.isHex(input[pointer])) {
1372 const as_hex: u16 = @intCast(std.mem.indexOfScalar(u8, hex_alpha_lower, input[pointer]) orelse std.mem.indexOfScalar(u8, hex_alpha_upper, input[pointer]) orelse unreachable);
1373 value = value * 0x10 + as_hex;
1374 pointer += 1;
1375 length += 1;
1376 }
1377 // 5. If c is U+002E (.), then:
1378 if (pointer < input.len and input[pointer] == '.') {
1379 // 1. If length is 0, IPv4-in-IPv6-invalid-code-point validation error, return failure.
1380 if (length == 0) return error.InvalidURL;
1381 // 2. Decrease pointer by length.
1382 pointer -= length;
1383 // 3. If pieceIndex is greater than 6, IPv4-in-IPv6-too-many-pieces validation error, return failure.
1384 if (pieceIndex > 6) return error.InvalidURL;
1385 // 4. Let numbersSeen be 0.
1386 var numbersSeen: usize = 0;
1387 // 5. While c is not the EOF code point:
1388 while (pointer < input.len) {
1389 // 1. Let ipv4Piece be null.
1390 var ipv4Piece: ?u16 = null;
1391 // 2. If numbersSeen is greater than 0, then:
1392 if (numbersSeen > 0) {
1393 // 1. If c is a U+002E (.) and numbersSeen is less than 4, then increase pointer by 1.
1394 if (input[pointer] == '.' and numbersSeen < 4) {
1395 pointer += 1;
1396 }
1397 // 2. Otherwise, IPv4-in-IPv6-invalid-code-point validation error, return failure.
1398 else {
1399 return error.InvalidURL;
1400 }
1401 }
1402 // 3. If c is not an ASCII digit, IPv4-in-IPv6-invalid-code-point validation error, return failure.
1403 if (pointer == input.len or !std.ascii.isDigit(input[pointer])) return error.InvalidURL;
1404 // 4. While c is an ASCII digit:
1405 while (pointer < input.len and std.ascii.isDigit(input[pointer])) {
1406 // 1. Let number be c interpreted as decimal number.
1407 const dec_alpha = "0123456789";
1408 const number: u8 = @intCast(std.mem.indexOfScalar(u8, dec_alpha, input[pointer]).?);
1409 // 2. If ipv4Piece is null, then set ipv4Piece to number.
1410 if (ipv4Piece == null) {
1411 ipv4Piece = number;
1412 }
1413 // Otherwise, if ipv4Piece is 0, IPv4-in-IPv6-invalid-code-point validation error, return failure.
1414 else if (ipv4Piece == 0) {
1415 return error.InvalidURL;
1416 }
1417 // Otherwise, set ipv4Piece to ipv4Piece × 10 + number.
1418 else {
1419 ipv4Piece = ipv4Piece.? * 10 + number;
1420 }
1421 // 3. If ipv4Piece is greater than 255, IPv4-in-IPv6-out-of-range-part validation error, return failure.
1422 if (ipv4Piece.? > 255) return error.InvalidURL;
1423 // 4. Increase pointer by 1.
1424 pointer += 1;
1425 }
1426 // 5. Set address[pieceIndex] to address[pieceIndex] × 0x100 + ipv4Piece.
1427 address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece.?;
1428 // 6. Increase numbersSeen by 1.
1429 numbersSeen += 1;
1430 // 7. If numbersSeen is 2 or 4, then increase pieceIndex by 1.
1431 if (numbersSeen == 2 or numbersSeen == 4) pieceIndex += 1;
1432 }
1433 // 6. If numbersSeen is not 4, IPv4-in-IPv6-too-few-parts validation error, return failure.
1434 if (numbersSeen != 4) return error.InvalidURL;
1435 // 7. Break.
1436 break;
1437 }
1438 // 6. Otherwise, if c is U+003A (:):
1439 else if (pointer < input.len and input[pointer] == ':') {
1440 // 1. Increase pointer by 1.
1441 pointer += 1;
1442 // 2. If c is the EOF code point, IPv6-invalid-code-point validation error, return failure.
1443 if (pointer == input.len) return error.InvalidURL;
1444 }
1445 // 7. Otherwise, if c is not the EOF code point, IPv6-invalid-code-point validation error, return failure.
1446 else if (pointer < input.len) {
1447 return error.InvalidURL;
1448 }
1449 // 8. Set address[pieceIndex] to value.
1450 address[pieceIndex] = value;
1451 // 9. Increase pieceIndex by 1.
1452 pieceIndex += 1;
1453 }
1454 // 7. If compress is non-null, then:
1455 if (compress != null) {
1456 // 1. Let swaps be pieceIndex − compress.
1457 var swaps = pieceIndex - compress.?;
1458 // 2. Set pieceIndex to 7.
1459 pieceIndex = 7;
1460 // 3. While pieceIndex is not 0 and swaps is greater than 0, swap address[pieceIndex] with address[compress + swaps − 1], and then decrease both pieceIndex and swaps by 1.
1461 while (pieceIndex != 0 and swaps > 0) {
1462 std.mem.swap(u16, &address[pieceIndex], &address[compress.? + swaps - 1]);
1463 pieceIndex -= 1;
1464 swaps -= 1;
1465 }
1466 }
1467 // 8. Otherwise, if compress is null and pieceIndex is not 8, IPv6-too-few-pieces validation error, return failure.
1468 else if (compress == null and pieceIndex != 8) {
1469 return error.InvalidURL;
1470 }
1471 // 9. Return address.
1472 return @bitCast(address);
1473}
1474
1475/// https://url.spec.whatwg.org/#concept-opaque-host-parser
1476fn parseHostOpaque(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
1477 // 1. If input contains a forbidden host code point, host-invalid-code-point validation error, return failure.
1478 if (extras.matchesAny(u8, input, is_forbidden_host_codepoint)) return error.InvalidURL;
1479 // 2. If input contains a code point that is not a URL code point and not U+0025 (%), invalid-URL-unit validation error.
1480 {}
1481 // 3. If input contains a U+0025 (%) and the two code points following it are not ASCII hex digits, invalid-URL-unit validation error.
1482 {}
1483 // 4. Return the result of running UTF-8 percent-encode on input using the C0 control percent-encode set.
1484 return percentEncode(allocator, input, is_c0control_percent_char);
1485}
1486
1487/// https://url.spec.whatwg.org/#utf-8-percent-encode
1488fn percentEncode(allocator: std.mem.Allocator, input: []const u8, comptime set: fn (u8) bool) ![]u8 {
1489 if (!extras.matchesAny(u8, input, set)) return allocator.dupe(u8, input);
1490 var result = nio.AllocatingWriter.init(allocator);
1491 errdefer result.deinit();
1492 try result.ensureUnusedCapacity(input.len);
1493 try percentEncodeAL(&result, input, set);
1494 return result.toOwnedSlice();
1495}
1496
1497/// https://url.spec.whatwg.org/#string-percent-decode
1498pub fn percentDecode(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
1499 var result = nio.AllocatingWriter.init(allocator);
1500 errdefer result.deinit();
1501 try result.ensureUnusedCapacity(input.len);
1502 try percentDecodeW(&result, input);
1503 return result.toOwnedSlice();
1504}
1505pub fn percentDecodeW(writer: anytype, input: []const u8) !void {
1506 var i: usize = 0;
1507 while (i < input.len) : (i += 1) {
1508 if (input[i] == '%') {
1509 if (input.len >= i + 1 + 2) {
1510 if (std.ascii.isHex(input[i + 1]) and std.ascii.isHex(input[i + 2])) {
1511 try writer.writeAll(&.{extras.parseDigits(u8, input[i + 1 ..][0..2], 16) catch unreachable});
1512 i += 2;
1513 continue;
1514 }
1515 }
1516 }
1517 try writer.writeAll(&.{input[i]});
1518 }
1519}
1520
1521/// https://url.spec.whatwg.org/#concept-domain-to-ascii
1522fn domainToAscii(allocator: std.mem.Allocator, domain: []const u8, beStrict: bool) ![]u8 {
1523 const result = unicode_idna.ToASCII(allocator, domain, beStrict, true, true, beStrict, false, beStrict, false) catch |err| switch (err) {
1524 error.IDNAFailure => return error.InvalidURL,
1525 error.OutOfMemory => return error.OutOfMemory,
1526 };
1527 errdefer allocator.free(result);
1528 if (!beStrict) {
1529 if (result.len == 0) return error.InvalidURL;
1530 if (extras.matchesAny(u8, result, is_forbidden_domain_codepoint)) return error.InvalidURL;
1531 return result;
1532 }
1533 std.debug.assert(result.len > 0);
1534 std.debug.assert(!extras.matchesAny(u8, result, is_forbidden_domain_codepoint));
1535 return result;
1536}
1537
1538/// https://url.spec.whatwg.org/#ends-in-a-number-checker
1539fn endsInANumber(input: []const u8) bool {
1540 // 1. Let parts be the result of strictly splitting input on U+002E (.).
1541 // 2. If the last item in parts is the empty string, then:
1542 {
1543 // 1. If parts’s size is 1, then return false.
1544 // 2. Remove the last item from parts.
1545 }
1546 // 3. Let last be the last item in parts.
1547 // 4. If last is non-empty and contains only ASCII digits, then return true.
1548 // > The erroneous input "09" will be caught by the IPv4 parser at a later stage.
1549 // 5. If parsing last as an IPv4 number does not return failure, then return true.
1550 // > This is equivalent to checking that last is "0X" or "0x", followed by zero or more ASCII hex digits.
1551 // 6. Return false.
1552
1553 var end = input.len;
1554 var start: usize = if (std.mem.lastIndexOfScalar(u8, input[0..end], '.')) |i| i + 1 else 0;
1555 if (end - start == 0) {
1556 if (extras.countScalar(u8, input, '.') == 0) return false;
1557 end = start - 1;
1558 start = if (std.mem.lastIndexOfScalar(u8, input[0..end], '.')) |i| i + 1 else 0;
1559 }
1560 const last = input[start..end];
1561 if (last.len > 0 and extras.matchesAll(u8, last, std.ascii.isDigit)) return true;
1562 parseIPv4Number(last, void) catch return false;
1563 return true;
1564}
1565
1566/// https://url.spec.whatwg.org/#concept-ipv4-parser
1567pub fn parseIPv4(input: []const u8) !u32 {
1568 // 1. Let parts be the result of strictly splitting input on U+002E (.).
1569 var end = input.len;
1570 var parts_size = extras.countScalar(u8, input[0..end], '.') + 1;
1571 // 2. If the last item in parts is the empty string, then:
1572 if (std.mem.endsWith(u8, input, ".")) {
1573 // 1. IPv4-empty-part validation error.
1574 // 2. If parts’s size is greater than 1, then remove the last item from parts.
1575 if (parts_size > 1) {
1576 end = std.mem.lastIndexOfScalar(u8, input, '.').?;
1577 parts_size -= 1;
1578 }
1579 }
1580 var iter = std.mem.splitScalar(u8, input[0..end], '.');
1581 // 3. If parts’s size is greater than 4, IPv4-too-many-parts validation error, return failure.
1582 if (parts_size > 4) return error.InvalidURL;
1583 // 4. Let numbers be an empty list.
1584 var numbers: [4]u32 = @splat(0);
1585 var numbers_len: u8 = 0;
1586 // 5. For each part of parts:
1587 while (iter.next()) |part| {
1588 // 1. Let result be the result of parsing part.
1589 // 2. If result is failure, IPv4-non-numeric-part validation error, return failure.
1590 const result = parseIPv4Number(part, u32) catch return error.InvalidURL;
1591 // 3. If result[1] is true, IPv4-non-decimal-part validation error.
1592 {}
1593 // 4. Append result[0] to numbers.
1594 numbers[numbers_len] = result;
1595 numbers_len += 1;
1596 }
1597 // 6. If any item in numbers is greater than 255, IPv4-out-of-range-part validation error.
1598 {}
1599 // 7. If any but the last item in numbers is greater than 255, then return failure.
1600 for (0..numbers_len - 1) |i| if (numbers[i] > 255) return error.InvalidURL;
1601 // 8. If the last item in numbers is greater than or equal to 256^(5 − numbers’s size), then return failure.
1602 if (numbers[numbers_len - 1] >= std.math.pow(u64, 256, 5 - numbers_len)) return error.InvalidURL;
1603 // 9. Let ipv4 be the last item in numbers.
1604 var ipv4 = numbers[numbers_len - 1];
1605 // 10. Remove the last item from numbers.
1606 numbers_len -= 1;
1607 // 11. Let counter be 0.
1608 // 12. For each n of numbers:
1609 for (numbers[0..numbers_len], 0..) |n, counter| {
1610 // 1. Increment ipv4 by n × 256^(3 − counter).
1611 ipv4 += @as(u32, @intCast(n)) * std.math.pow(u32, 256, 3 - @as(u8, @intCast(counter)));
1612 // 2. Increment counter by 1.
1613 }
1614 // 13. Return ipv4.
1615 return @byteSwap(ipv4);
1616}
1617
1618/// https://url.spec.whatwg.org/#ipv4-number-parser
1619fn parseIPv4Number(input_: []const u8, T: type) !T {
1620 var input = input_;
1621 // 1. If input is the empty string, then return failure.
1622 if (input.len == 0) return error.Invalid;
1623 // 2. Let validationError be false.
1624 var validationError = false;
1625 // 3. Let R be 10.
1626 var radix: u8 = 10;
1627 // 4. If input contains at least two code points and the first two code points are either "0X" or "0x", then:
1628 if (std.mem.startsWith(u8, input, "0X") or std.mem.startsWith(u8, input, "0x")) {
1629 // 1. Set validationError to true.
1630 validationError = true;
1631 // 2. Remove the first two code points from input.
1632 input = input[2..];
1633 // 3. Set R to 16.
1634 radix = 16;
1635 }
1636 // 5. Otherwise, if input contains at least two code points and the first code point is U+0030 (0), then:
1637 else if (input.len >= 2 and input[0] == '0') {
1638 // 1. Set validationError to true.
1639 validationError = true;
1640 // 2. Remove the first code point from input.
1641 input = input[1..];
1642 // 3. Set R to 8.
1643 radix = 8;
1644 }
1645 // 6. If input is the empty string, then return (0, true).
1646 if (input.len == 0 and T == void) return;
1647 if (input.len == 0 and T != void) return 0;
1648 // 7. If input contains a code point that is not a radix-R digit, then return failure.
1649 switch (radix) {
1650 8 => for (input) |c| switch (c) {
1651 '0'...'7' => {},
1652 else => return error.Invalid,
1653 },
1654 10 => for (input) |c| if (!std.ascii.isDigit(c)) return error.Invalid,
1655 16 => for (input) |c| if (!std.ascii.isHex(c)) return error.Invalid,
1656 else => unreachable,
1657 }
1658 // 8. Let output be the mathematical integer value that is represented by input in radix-R notation, using ASCII hex digits for digits with values 0 through 15.
1659 // 9. Return (output, validationError).
1660 if (T == void) return;
1661 const output = extras.parseDigits(T, input, radix) catch return error.Invalid;
1662 return output;
1663}
1664
1665/// https://url.spec.whatwg.org/#shorten-a-urls-path
1666fn shortenUrlPath(url: *ManyArrayList(15, u8), has_opaque_path: bool) void {
1667 // 1. Assert: url does not have an opaque path.
1668 std.debug.assert(!has_opaque_path);
1669 // 2. Let path be url’s path.
1670 const path = url.items(10);
1671 // 3. If url’s scheme is "file", path’s size is 1, and path[0] is a normalized Windows drive letter, then return.
1672 if (std.mem.eql(u8, url.items(0), "file") and extras.countScalar(u8, path, '/') == 1 and isNormalizedWindowsDriveLetter(nthScalarItem(u8, path, '/', 1))) {
1673 return;
1674 }
1675 // 4. Remove path’s last item, if any.
1676 const new_len = std.mem.lastIndexOfScalar(u8, path, '/') orelse return;
1677 url.replace(10, new_len, path.len - new_len, "") catch unreachable;
1678}
1679
1680//
1681//
1682//
1683//
1684
1685/// https://infra.spec.whatwg.org/#c0-control
1686pub fn is_c0control(c: u8) bool {
1687 if (c >= 0x00 and c <= 0x1F) return true;
1688 return false;
1689}
1690/// https://infra.spec.whatwg.org/#c0-control-or-space
1691pub fn is_c0control_or_space(c: u8) bool {
1692 if (is_c0control(c)) return true;
1693 if (c == ' ') return true;
1694 return false;
1695}
1696/// https://url.spec.whatwg.org/#forbidden-host-code-point
1697pub fn is_forbidden_host_codepoint(c: u8) bool {
1698 if (c == 0) return true;
1699 if (c == '\t') return true;
1700 if (c == '\n') return true;
1701 if (c == '\r') return true;
1702 if (c == ' ') return true;
1703 if (c == '#') return true;
1704 if (c == '/') return true;
1705 if (c == ':') return true;
1706 if (c == '<') return true;
1707 if (c == '>') return true;
1708 if (c == '?') return true;
1709 if (c == '@') return true;
1710 if (c == '[') return true;
1711 if (c == '\\') return true;
1712 if (c == ']') return true;
1713 if (c == '^') return true;
1714 if (c == '|') return true;
1715 return false;
1716}
1717/// https://url.spec.whatwg.org/#forbidden-domain-code-point
1718pub fn is_forbidden_domain_codepoint(c: u8) bool {
1719 if (is_forbidden_host_codepoint(c)) return true;
1720 if (is_c0control(c)) return true;
1721 if (c == '%') return true;
1722 if (c == 0x7f) return true;
1723 return false;
1724}
1725/// https://url.spec.whatwg.org/#url-code-points
1726pub fn is_url_codepoint(c: u21) bool {
1727 if (c < 128 and std.ascii.isAlphanumeric(@intCast(c))) return true;
1728 if (c == '!') return true;
1729 if (c == '$') return true;
1730 if (c == '&') return true;
1731 if (c == '\'') return true;
1732 if (c == '(') return true;
1733 if (c == ')') return true;
1734 if (c == '*') return true;
1735 if (c == '+') return true;
1736 if (c == ',') return true;
1737 if (c == '-') return true;
1738 if (c == '.') return true;
1739 if (c == '/') return true;
1740 if (c == ':') return true;
1741 if (c == ';') return true;
1742 if (c == '=') return true;
1743 if (c == '?') return true;
1744 if (c == '@') return true;
1745 if (c == '_') return true;
1746 if (c == '~') return true;
1747 // and code points in the range U+00A0 to U+10FFFD, inclusive,
1748 // excluding surrogates and noncharacters.
1749 if (c >= 0x00A0 and c <= 0x10FFFD) return true;
1750 return false;
1751}
1752/// https://url.spec.whatwg.org/#c0-control-percent-encode-set
1753pub fn is_c0control_percent_char(c: u8) bool {
1754 if (c >= 0x00 and c <= 0x1F) return true;
1755 return c > 0x7E;
1756}
1757/// https://url.spec.whatwg.org/#query-percent-encode-set
1758pub fn is_query_percent_char(c: u8) bool {
1759 if (c == ' ') return true;
1760 if (c == '"') return true;
1761 if (c == '#') return true;
1762 if (c == '<') return true;
1763 if (c == '>') return true;
1764 return is_c0control_percent_char(c);
1765}
1766/// https://url.spec.whatwg.org/#path-percent-encode-set
1767pub fn is_path_percent_char(c: u8) bool {
1768 if (c == '?') return true;
1769 if (c == '^') return true;
1770 if (c == '`') return true;
1771 if (c == '{') return true;
1772 if (c == '}') return true;
1773 return is_query_percent_char(c);
1774}
1775/// https://url.spec.whatwg.org/#special-query-percent-encode-set
1776pub fn is_special_query_percent_char(c: u8) bool {
1777 if (c == '\'') return true;
1778 return is_query_percent_char(c);
1779}
1780/// https://url.spec.whatwg.org/#fragment-percent-encode-set
1781pub fn is_fragment_percent_char(c: u8) bool {
1782 if (c == ' ') return true;
1783 if (c == '"') return true;
1784 if (c == '<') return true;
1785 if (c == '>') return true;
1786 if (c == '`') return true;
1787 return is_c0control_percent_char(c);
1788}
1789/// https://url.spec.whatwg.org/#userinfo-percent-encode-set
1790pub fn is_userinfo_percent_char(c: u8) bool {
1791 if (c == '/') return true;
1792 if (c == ':') return true;
1793 if (c == ';') return true;
1794 if (c == '=') return true;
1795 if (c == '@') return true;
1796 if (c >= '[' and c <= ']') return true;
1797 if (c == '|') return true;
1798 return is_path_percent_char(c);
1799}
1800/// https://url.spec.whatwg.org/#component-percent-encode-set
1801pub fn is_component_percent_char(c: u8) bool {
1802 if (c >= '$' and c <= '&') return true;
1803 if (c == '+') return true;
1804 if (c == ',') return true;
1805 return is_userinfo_percent_char(c);
1806}
1807/// https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set
1808pub fn is_formurlencoded_percent_char(c: u8) bool {
1809 if (c == '!') return true;
1810 if (c >= '\'' and c <= ')') return true;
1811 if (c == '~') return true;
1812 return is_component_percent_char(c);
1813}
1814
1815/// Asserts b is a valid UTF-8 codepoint
1816fn l(b: u8) u3 {
1817 return std.unicode.utf8ByteSequenceLength(b) catch unreachable;
1818}
1819fn lastcpi(haystack: []const u8) usize {
1820 var i = haystack.len - 1;
1821 while (haystack[i] & 0xC0 == 0x80) : (i -= 1) {}
1822 return i;
1823}
1824pub fn percentEncodeScalarAL(list: *nio.AllocatingWriter, cp: []const u8, comptime set: fn (u8) bool) !void {
1825 if (set(cp[0])) {
1826 for (cp) |b| {
1827 try list.append('%');
1828 try list.print("{X:0>2}", .{b});
1829 }
1830 } else {
1831 try list.append(cp[0]);
1832 }
1833}
1834pub fn percentEncodeW(writer: anytype, input: []const u8, comptime set: fn (u8) bool) !void {
1835 var it = std.unicode.Utf8View.initUnchecked(input).iterator();
1836 while (it.nextCodepointSlice()) |sl| {
1837 if (set(sl[0])) {
1838 for (sl) |b| {
1839 try writer.writeAll(&.{'%'});
1840 try writer.print("{X:0>2}", .{b});
1841 }
1842 } else {
1843 try writer.writeAll(sl);
1844 }
1845 }
1846}
1847pub fn percentEncodeAL(list: *nio.AllocatingWriter, input: []const u8, comptime set: fn (u8) bool) !void {
1848 var it = std.unicode.Utf8View.initUnchecked(input).iterator();
1849 while (it.nextCodepointSlice()) |sl| {
1850 if (set(sl[0])) {
1851 for (sl) |b| {
1852 try list.writeAll("%");
1853 try list.print("{X:0>2}", .{b});
1854 }
1855 } else {
1856 try list.writeAll(sl);
1857 }
1858 }
1859}
1860fn percentEncodeScalarML(list: *ManyArrayList(15, u8), n: usize, cp: []const u8, comptime set: fn (u8) bool) !void {
1861 if (set(cp[0])) {
1862 for (cp) |b| {
1863 try list.appendSlice(n, &.{'%'});
1864 try list.print(n, "{X:0>2}", .{b});
1865 }
1866 } else {
1867 try list.appendSlice(n, &.{cp[0]});
1868 }
1869}
1870fn percentEncodeML(list: *ManyArrayList(15, u8), n: usize, input: []const u8, comptime set: fn (u8) bool) !void {
1871 var it = std.unicode.Utf8View.initUnchecked(input).iterator();
1872 while (it.nextCodepointSlice()) |sl| {
1873 if (set(sl[0])) {
1874 for (sl) |b| {
1875 try list.appendSlice(n, &.{'%'});
1876 try list.print(n, "{X:0>2}", .{b});
1877 }
1878 } else {
1879 try list.appendSlice(n, sl);
1880 }
1881 }
1882}
1883fn setHost(href: *ManyArrayList(15, u8), h: URL.Host) !void {
1884 switch (h) {
1885 .unset => unreachable,
1886 .name => {
1887 try href.set(7, h.name);
1888 },
1889 .ipv4 => {
1890 const bytes: [4]u8 = @bitCast(h.ipv4);
1891 try href.print(7, "{d}.{d}.{d}.{d}", .{ bytes[0], bytes[1], bytes[2], bytes[3] });
1892 },
1893 .ipv6 => {
1894 const pieces: [8]u16 = @bitCast(h.ipv6);
1895 try href.appendSlice(7, "[");
1896 const compress = blk: {
1897 var longest: struct { ?usize, u8 } = .{ null, 1 };
1898 var found: struct { ?usize, u8 } = .{ null, 0 };
1899 for (&pieces, 0..) |piece, pieceIndex| {
1900 if (piece != 0) {
1901 if (found[1] > longest[1]) {
1902 longest[0] = found[0];
1903 longest[1] = found[1];
1904 }
1905 found[0] = null;
1906 found[1] = 0;
1907 } else {
1908 if (found[0] == null) found[0] = pieceIndex;
1909 found[1] += 1;
1910 }
1911 }
1912 if (found[1] > longest[1]) break :blk found[0];
1913 break :blk longest[0];
1914 };
1915 var ignore0 = false;
1916 for (&pieces, 0..) |piece, pieceIndex| {
1917 if (ignore0) {
1918 if (piece == 0) continue;
1919 ignore0 = false;
1920 }
1921 if (compress == pieceIndex) {
1922 try href.appendSlice(7, if (pieceIndex == 0) "::" else ":");
1923 ignore0 = true;
1924 continue;
1925 }
1926 try href.print(7, "{x}", .{piece});
1927 if (pieceIndex != 7) try href.appendSlice(7, ":");
1928 }
1929 try href.appendSlice(7, "]");
1930 },
1931 }
1932}
1933fn replaceInPlace(comptime T: type, input: []T, needle: []const T, replacement: []const T) usize {
1934 // Empty needle will loop until output buffer overflows.
1935 std.debug.assert(needle.len > 0);
1936 std.debug.assert(needle.len >= replacement.len);
1937
1938 var slide: usize = 0;
1939 var replacements: usize = 0;
1940 while (std.mem.indexOf(u8, input[slide..], needle)) |idx| {
1941 slide += idx;
1942 std.mem.copyForwards(u8, input[slide..], replacement);
1943 slide += replacement.len;
1944 std.mem.copyForwards(u8, input[slide..], input[slide..][needle.len - replacement.len ..]);
1945 slide += needle.len - replacement.len;
1946 replacements += 1;
1947 }
1948 return replacements;
1949}
1950fn nthScalarItem(T: type, haystack: []const u8, needle: T, index: usize) []const T {
1951 var it = std.mem.splitScalar(T, haystack, needle);
1952 var idx: usize = 0;
1953 while (idx < index) : (idx += 1) _ = it.next().?;
1954 return it.next().?;
1955}
1956
1957pub fn ManyArrayList(N: usize, T: type) type {
1958 return struct {
1959 list: std.array_list.Managed(T),
1960 lengths: [N]usize,
1961
1962 pub fn init(allocator: std.mem.Allocator) @This() {
1963 return .{
1964 .list = .init(allocator),
1965 .lengths = @splat(0),
1966 };
1967 }
1968
1969 pub fn deinit(self: *@This()) void {
1970 self.list.deinit();
1971 }
1972
1973 pub fn set(self: *@This(), n: usize, slice: []const T) !void {
1974 const real_n = extras.sum(usize, self.lengths[0..n]);
1975 try self.list.replaceRange(real_n, self.lengths[n], slice);
1976 self.lengths[n] = slice.len;
1977 }
1978
1979 pub fn items(self: *@This(), n: usize) []T {
1980 const real_n = extras.sum(usize, self.lengths[0..n]);
1981 const len = self.lengths[n];
1982 return self.list.items[real_n..][0..len];
1983 }
1984
1985 pub fn clear(self: *@This(), n: usize) void {
1986 const real_n = extras.sum(usize, self.lengths[0..n]);
1987 self.list.replaceRangeAssumeCapacity(real_n, self.lengths[n], &.{});
1988 self.lengths[n] = 0;
1989 }
1990
1991 pub fn print(self: *@This(), n: usize, comptime fmt: []const u8, args: anytype) !void {
1992 var buffer: [64]u8 = undefined;
1993 const slice = std.fmt.bufPrint(&buffer, fmt, args) catch unreachable;
1994 return self.appendSlice(n, slice);
1995 }
1996
1997 pub fn appendSlice(self: *@This(), n: usize, slice: []const T) !void {
1998 const real_n = extras.sum(usize, self.lengths[0 .. n + 1]);
1999 try self.list.insertSlice(real_n, slice);
2000 self.lengths[n] += slice.len;
2001 }
2002
2003 pub fn replace(self: *@This(), n: usize, o: usize, c: usize, slice: []const T) !void {
2004 std.debug.assert(o + c <= self.lengths[n]);
2005 const real_n = extras.sum(usize, self.lengths[0..n]);
2006 try self.list.replaceRange(real_n + o, c, slice);
2007 self.lengths[n] -= c;
2008 self.lengths[n] += slice.len;
2009 }
2010 };
2011}