authorgravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-01-11 18:08:07 -08:00
committergravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-01-11 18:08:28 -08:00
logc40a330df43887587589574b8c5d980875e7508a
tree6dedc1b50bf2db12ed83373bace4d20e1e6f89d8
parentc9cbfe30abf66754104105db6775ca0df03381e9

└─ run test 93/214 passed, 113 failed, 8 skipped


5 files changed, 1560 insertions(+), 18 deletions(-)

generate.ts-2
...@@ -80,8 +80,6 @@ pub fn parseIDNAPass(input: []const u8, output: []const u8) !void {...@@ -80,8 +80,6 @@ pub fn parseIDNAPass(input: []const u8, output: []const u8) !void {
80`);80`);
8181
82w.write(`\n`);82w.write(`\n`);
83// prettier-ignore
84if (false)
85for (const c of cases.filter((v) => v.base == null && !!v.failure)) {83for (const c of cases.filter((v) => v.base == null && !!v.failure)) {
86 w.write(`test { try parseFail("${stringEscape(c.input)}", null); }\n`);84 w.write(`test { try parseFail("${stringEscape(c.input)}", null); }\n`);
87}85}
licenses.txt+7
...@@ -1,7 +1,14 @@...@@ -1,7 +1,14 @@
1MPL-2.0:1MPL-2.0:
2= https://spdx.org/licenses/MPL-2.02= https://spdx.org/licenses/MPL-2.0
3- This3- This
4- git https://github.com/nektro/zig-net
5- git https://github.com/nektro/zig-nio
46
5MIT:7MIT:
6= https://spdx.org/licenses/MIT8= https://spdx.org/licenses/MIT
7- git https://github.com/nektro/zig-expect9- git https://github.com/nektro/zig-expect
10- git https://github.com/nektro/zig-extras
11- git https://github.com/nektro/zig-sys-linux
12
13Unspecified:
14- system_lib c
url.zig+1337-16
...@@ -1,25 +1,1346 @@...@@ -1,25 +1,1346 @@
1const std = @import("std");1const std = @import("std");
2const builtin = @import("builtin");
3const net = @import("net");
4const extras = @import("extras");
25
3pub const URL = struct {6pub const URL = struct {
4 href: []const u8,7 href: []const u8,
5 origin: []const u8,8 scheme: []const u8 = "",
6 protocol: []const u8,9
7 username: []const u8,10 pub const HostKind = enum {
8 password: []const u8,11 name,
9 host: []const u8,12 ipv4,
10 hostname: []const u8,13 ipv6,
11 port: []const u8,14 };
12 pathname: []const u8,15
13 search: []const u8,16 const Host = union(HostKind) {
14 hash: []const u8,17 name: []const u8,
18 ipv4: u32,
19 ipv6: u128,
20 };
1521
16 /// Caller owns memory and is responsible for freeing `url.href`.22 /// Caller owns memory and is responsible for freeing `url.href`.
17 pub fn parse(alloc: std.mem.Allocator, input: []const u8, base: ?[]const u8) !URL {23 pub fn parse(alloc: std.mem.Allocator, input: []const u8, base: ?[]const u8) !URL {
18 var u: URL = .{ .href = "", .origin = "", .protocol = "", .username = "", .password = "", .host = "", .hostname = "", .port = "", .pathname = "", .search = "", .hash = "" };24 const u = try parseBasic(alloc, input, if (base) |b| try parseBasic(alloc, b, null, null) else null, null);
19 _ = &u;25 return u;
20 _ = alloc;26 }
21 _ = input;27
22 _ = base;28 /// https://url.spec.whatwg.org/#concept-basic-url-parser
23 return error.SkipZigTest;29 fn parseBasic(alloc: std.mem.Allocator, input: []const u8, base: ?URL, state_override: ?BasicParserState) error{ SkipZigTest, InvalidURL, OutOfMemory }!URL {
30 // input is a scalar value string
31 if (!std.unicode.utf8ValidateSlice(input)) return error.InvalidURL;
32
33 var inputl = std.ArrayList(u8).init(alloc);
34 defer inputl.deinit();
35 try inputl.appendSlice(input);
36
37 // 1.
38 while (inputl.items.len > 0 and is_c0control_or_space(inputl.items[0])) _ = inputl.orderedRemove(0);
39 while (inputl.items.len > 0 and is_c0control_or_space(inputl.getLast())) inputl.items.len -= 1;
40
41 // 3.
42 while (std.mem.indexOfScalar(u8, inputl.items, '\t')) |i| _ = inputl.orderedRemove(i);
43 while (std.mem.indexOfScalar(u8, inputl.items, '\n')) |i| _ = inputl.orderedRemove(i);
44 while (std.mem.indexOfScalar(u8, inputl.items, '\r')) |i| _ = inputl.orderedRemove(i);
45
46 const length = inputl.items.len;
47
48 // 4.
49 // this doesn't need to be var since the switch below uses label to jump with the new prong
50 var state = state_override orelse .scheme_start;
51
52 // 5.
53 // we always do utf-8
54
55 // 6.
56 var buffer = std.ArrayList(u8).init(alloc);
57 defer buffer.deinit();
58
59 // 7.
60 var atSignSeen = false;
61 _ = &atSignSeen;
62 var insideBrackets = false;
63 _ = &insideBrackets;
64 var passwordTokenSeen = false;
65 _ = &passwordTokenSeen;
66
67 // 8.
68 // codepoint index into inputl
69 var pointer: isize = 0;
70 // byte index into inputl
71 // moved at the same time as pointer
72 // if pointer is -1, i must not be read
73 var i: usize = 0;
74 // inputl[i], must change in tandem with i
75 var c: u8 = if (length > 0) inputl.items[i] else 0;
76
77 var href = std.ArrayList(u8).init(alloc);
78 defer href.deinit();
79
80 var scheme = std.ArrayList(u8).init(alloc);
81 defer scheme.deinit();
82 var username = std.ArrayList(u8).init(alloc);
83 defer username.deinit();
84 var password = std.ArrayList(u8).init(alloc);
85 defer password.deinit();
86 var host: Host = .{ .name = "" };
87 defer if (host == .name) alloc.free(host.name);
88 var port = std.ArrayList(u8).init(alloc);
89 defer port.deinit();
90 var path = std.ArrayList(u8).init(alloc);
91 defer path.deinit();
92 var query = std.ArrayList(u8).init(alloc);
93 defer query.deinit();
94 var fragment = std.ArrayList(u8).init(alloc);
95 defer fragment.deinit();
96
97 // 9.
98 while (true) {
99 switch (state) {
100 .scheme_start => {
101 std.debug.assert(pointer == 0);
102 // 1. If c is an ASCII alpha, append c, lowercased, to buffer, and set state to scheme state.
103 if (i != length and std.ascii.isAlphabetic(c)) {
104 try buffer.append(std.ascii.toLower(c));
105 state = .scheme;
106 }
107 // 2. Otherwise, if state override is not given, set state to no scheme state and decrease pointer by 1.
108 else if (state_override == null) {
109 state = .no_scheme;
110 continue; // pointer goes to -1 then +1'd
111 }
112 // 3. Otherwise, return failure.
113 else {
114 return error.InvalidURL;
115 }
116 },
117 .scheme => {
118 // 1. If c is an ASCII alphanumeric, U+002B (+), U+002D (-), or U+002E (.), append c, lowercased, to buffer.
119 if (std.ascii.isAlphanumeric(c) or c == '+' or c == '-' or c == '.') {
120 try buffer.append(std.ascii.toLower(c));
121 }
122 // 2. Otherwise, if c is U+003A (:), then:
123 else if (c == ':') {
124 // 1. If state override is given, then:
125 if (state_override != null) {
126 @panic("TODO");
127 // 1. If url’s scheme is a special scheme and buffer is not a special scheme, then return.
128 // 2. If url’s scheme is not a special scheme and buffer is a special scheme, then return.
129 // 3. If url includes credentials or has a non-null port, and buffer is "file", then return.
130 // 4. If url’s scheme is "file" and its host is an empty host, then return.
131 }
132 // 2. Set url’s scheme to buffer.
133 scheme.clearRetainingCapacity();
134 try scheme.appendSlice(buffer.items);
135 // 3. If state override is given, then:
136 if (state_override != null) {
137 @panic("TODO");
138 // 1. If url’s port is url’s scheme’s default port, then set url’s port to null.
139 // 2. Return.
140 }
141 // 4. Set buffer to the empty string.
142 buffer.clearRetainingCapacity();
143 // 5. If url’s scheme is "file", then:
144 if (std.mem.eql(u8, scheme.items, "file")) {
145 // 1. If remaining does not start with "//", special-scheme-missing-following-solidus validation error.
146 {}
147 // 2. Set state to file state.
148 state = .file;
149 }
150 // 6. Otherwise, if url is special, base is non-null, and base’s scheme is url’s scheme:
151 else if (isSchemeSpecial(scheme.items) and base != null and std.mem.eql(u8, base.?.scheme, scheme.items)) {
152 @panic("TODO");
153 // 1. Assert: base is special (and therefore does not have an opaque path).
154 // 2. Set state to special relative or authority state.
155 }
156 // 7. Otherwise, if url is special, set state to special authority slashes state.
157 else if (isSchemeSpecial(scheme.items)) {
158 state = .special_authority_slashes;
159 }
160 // 8. Otherwise, if remaining starts with an U+002F (/), set state to path or authority state and increase pointer by 1.
161 else if (std.mem.startsWith(u8, inputl.items[i + l(inputl.items[i]) ..], "/")) {
162 state = .path_or_authority;
163 i += l(c);
164 c = inputl.items[i];
165 pointer += 1;
166 }
167 // 9. Otherwise, set url’s path to the empty string and set state to opaque path state.
168 else {
169 path.clearRetainingCapacity();
170 state = .opaque_path;
171 }
172 }
173 // 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).
174 else if (state_override == null) {
175 buffer.clearRetainingCapacity();
176 state = .no_scheme;
177 pointer = 0;
178 i = 0;
179 c = inputl.items[i];
180 }
181 // 4. Otherwise, return failure.
182 else {
183 return error.InvalidURL;
184 }
185 },
186 .no_scheme => {
187 // 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.
188 // 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.
189 // 3. Otherwise, if base’s scheme is not "file", set state to relative state and decrease pointer by 1.
190 // 4. Otherwise, set state to file state and decrease pointer by 1.
191 return error.SkipZigTest;
192 },
193 .special_relative_or_authority => {
194 // 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.
195 if (c == '/' and std.mem.startsWith(u8, inputl.items[i + l(c) ..], "/")) {
196 state = .special_authority_ignore_slashes;
197 i += l(c);
198 c = inputl.items[i];
199 pointer += 1;
200 }
201 // 2. Otherwise, special-scheme-missing-following-solidus validation error, set state to relative state and decrease pointer by 1.
202 else {
203 state = .relative;
204 @panic("TODO");
205 }
206 },
207 .path_or_authority => {
208 // 1. If c is U+002F (/), then set state to authority state.
209 if (c == '/') {
210 state = .authority;
211 }
212 // 2. Otherwise, set state to path state, and decrease pointer by 1.
213 else {
214 state = .path;
215 @panic("TODO");
216 }
217 },
218 .relative => {
219 // 1. Assert: base’s scheme is not "file".
220 std.debug.assert(!std.mem.eql(u8, base.?.scheme, "file"));
221 // 2. Set url’s scheme to base’s scheme.
222 try scheme.appendSlice(base.?.scheme);
223 // 3. If c is U+002F (/), then set state to relative slash state.
224 if (c == '/') {
225 state = .relative_slash;
226 }
227 // 4. Otherwise, if url is special and c is U+005C (\), invalid-reverse-solidus validation error, set state to relative slash state.
228 else if (isSchemeSpecial(scheme.items) and c == '\\') {
229 state = .relative_slash;
230 }
231 // 5. Otherwise:
232 else {
233 // 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.
234 // try username.appendSlice(base.?.username);
235 // try password.appendSlice(base.?.password);
236 // host = base.?.host;
237 // try port.writer().print("{d}", .{base.?.port.?});
238 // try path.appendSlice(base.?.path);
239 // try query.appendSlice(base.?.query.?);
240 // 2. If c is U+003F (?), then set url’s query to the empty string, and state to query state.
241 if (c == '?') {
242 query.clearRetainingCapacity();
243 state = .query;
244 }
245 // 3. Otherwise, if c is U+0023 (#), set url’s fragment to the empty string and state to fragment state.
246 else if (c == '#') {
247 fragment.clearRetainingCapacity();
248 state = .fragment;
249 }
250 // 4. Otherwise, if c is not the EOF code point:
251 else if (i < length) {
252 // 1. Set url’s query to null.
253 query.clearRetainingCapacity();
254 // 2. Shorten url’s path.
255 @panic("TODO");
256 // 3. Set state to path state and decrease pointer by 1.
257 // state = .path;
258 // pointer -= 1;
259 // i = lastcpi(inputl.items[0..i]);
260 // c = inputl.items[i];
261 }
262 }
263 },
264 .relative_slash => {
265 // 1. If url is special and c is U+002F (/) or U+005C (\), then:
266 if (isSchemeSpecial(scheme.items) and (c == '/' or c == '\\')) {
267 // 1. If c is U+005C (\), invalid-reverse-solidus validation error.
268 // 2. Set state to special authority ignore slashes state.
269 state = .special_authority_ignore_slashes;
270 }
271 // 2. Otherwise, if c is U+002F (/), then set state to authority state.
272 else if (c == '/') {
273 state = .authority;
274 }
275 // 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.
276 else {
277 // try username.appendSlice(base.?.username);
278 // try password.appendSlice(base.?.password);
279 // host = base.?.host;
280 // try port.writer().print("{d}", .{base.?.port.?});
281 state = .path;
282 pointer -= 1;
283 i = lastcpi(inputl.items[0..i]);
284 c = inputl.items[i];
285 @panic("TODO");
286 }
287 },
288 .special_authority_slashes => {
289 // 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.
290 if (c == '/' and std.mem.startsWith(u8, inputl.items[i + l(c) ..], "/")) {
291 state = .special_authority_ignore_slashes;
292 i += l(c);
293 c = if (i < length) inputl.items[i] else 0;
294 pointer += 1;
295 }
296 // 2. Otherwise, special-scheme-missing-following-solidus validation error, set state to special authority ignore slashes state and decrease pointer by 1.
297 else {
298 state = .special_authority_ignore_slashes;
299 pointer -= 1;
300 i = lastcpi(inputl.items[0..i]);
301 c = inputl.items[i];
302 }
303 },
304 .special_authority_ignore_slashes => {
305 // 1. If c is neither U+002F (/) nor U+005C (\), then set state to authority state and decrease pointer by 1.
306 if (c != '/' and c != '\\') {
307 state = .authority;
308 pointer -= 1;
309 i = lastcpi(inputl.items[0..i]);
310 c = inputl.items[i];
311 }
312 // 2. Otherwise, special-scheme-missing-following-solidus validation error.
313 else {
314 //
315 }
316 },
317 .authority => {
318 // 1. If c is U+0040 (@), then:
319 if (c == '@') {
320 // 1. Invalid-credentials validation error.
321 // 2. If atSignSeen is true, then prepend "%40" to buffer.
322 if (atSignSeen) try buffer.insertSlice(0, "%40");
323 // 3. Set atSignSeen to true.
324 atSignSeen = true;
325 // 4. For each codePoint in buffer:
326 {
327 // 1. If codePoint is U+003A (:) and passwordTokenSeen is false, then set passwordTokenSeen to true and continue.
328 // 2. Let encodedCodePoints be the result of running UTF-8 percent-encode codePoint using the userinfo percent-encode set.
329 // 3. If passwordTokenSeen is true, then append encodedCodePoints to url’s password.
330 // 4. Otherwise, append encodedCodePoints to url’s username.
331 }
332 // 5. Set buffer to the empty string.
333 buffer.clearRetainingCapacity();
334 }
335 // 2. Otherwise, if one of the following is true:
336 // - c is the EOF code point, U+002F (/), U+003F (?), or U+0023 (#)
337 // - url is special and c is U+005C (\)
338 // then:
339 else if ((i == length or c == '/' or c == '?' or c == '#') or (isSchemeSpecial(scheme.items) and c == '\\')) {
340 // 1. If atSignSeen is true and buffer is the empty string, host-missing validation error, return failure.
341 if (atSignSeen and buffer.items.len == 0) return error.InvalidURL;
342 // 2. Decrease pointer by buffer’s code point length + 1, set buffer to the empty string, and set state to host state.
343 for (0..(std.unicode.utf8CountCodepoints(buffer.items) catch unreachable) + 1) |_| {
344 pointer -= 1;
345 i = lastcpi(inputl.items[0..i]);
346 c = inputl.items[i];
347 }
348 buffer.clearRetainingCapacity();
349 state = .host;
350 }
351 // 3. Otherwise, append c to buffer.
352 else {
353 try buffer.appendSlice(inputl.items[i..][0..l(c)]);
354 }
355 },
356 .host, .hostname => {
357 // 1. If state override is given and url’s scheme is "file", then decrease pointer by 1 and set state to file host state.
358 if (state_override != null and std.mem.eql(u8, scheme.items, "file")) {
359 pointer -= 1;
360 i = lastcpi(inputl.items[0..i]);
361 c = inputl.items[i];
362 state = .file_host;
363 }
364 // 2. Otherwise, if c is U+003A (:) and insideBrackets is false:
365 else if (c == ':' and insideBrackets == false) {
366 // 1. If buffer is the empty string, host-missing validation error, return failure.
367 if (buffer.items.len == 0) return error.InvalidURL;
368 // 2. If state override is given and state override is hostname state, then return failure.
369 if (state_override != null and state_override.? == .hostname) return error.InvalidURL;
370 // 3. Let host be the result of host parsing buffer with url is not special.
371 // 4. If host is failure, then return failure.
372 const h = try parseHost(alloc, buffer.items, !isSchemeSpecial(scheme.items));
373 // 5. Set url’s host to host, buffer to the empty string, and state to port state.
374 host = h;
375 buffer.clearRetainingCapacity();
376 state = .port;
377 }
378 // 3. Otherwise, if one of the following is true:
379 // - c is the EOF code point, U+002F (/), U+003F (?), or U+0023 (#)
380 // - url is special and c is U+005C (\)
381 // then decrease pointer by 1, and:
382 else if ((i == length or c == '/' or c == '?' or c == '#') or (isSchemeSpecial(scheme.items) and c == '\\')) {
383 pointer -= 1;
384 i = lastcpi(inputl.items[0..i]);
385 c = inputl.items[i];
386 // 1. If url is special and buffer is the empty string, host-missing validation error, return failure.
387 if (isSchemeSpecial(scheme.items) and buffer.items.len == 0) return error.InvalidURL
388 // 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.
389 else if (state_override != null and buffer.items.len == 0 and (username.items.len > 0 or password.items.len > 0)) return error.InvalidURL;
390 // 3. Let host be the result of host parsing buffer with url is not special.
391 // 4. If host is failure, then return failure.
392 const h = try parseHost(alloc, buffer.items, !isSchemeSpecial(scheme.items));
393 // 5. Set url’s host to host, buffer to the empty string, and state to path start state.
394 host = h;
395 buffer.clearRetainingCapacity();
396 state = .path_start;
397 // 6. If state override is given, then return.
398 if (state_override != null) break;
399 }
400 // 4. Otherwise:
401 else {
402 // 1. If c is U+005B ([), then set insideBrackets to true.
403 if (c == '[') insideBrackets = true;
404 // 2. If c is U+005D (]), then set insideBrackets to false.
405 if (c == ']') insideBrackets = false;
406 // 3. Append c to buffer.
407 try buffer.appendSlice(inputl.items[i..][0..l(c)]);
408 }
409 },
410 .port => {
411 // 1. If c is an ASCII digit, append c to buffer.
412 if (std.ascii.isDigit(c)) {
413 try buffer.appendSlice(inputl.items[i..][0..l(c)]);
414 }
415 // 2. Otherwise, if one of the following is true:
416 // - c is the EOF code point, U+002F (/), U+003F (?), or U+0023 (#);
417 // - url is special and c is U+005C (\); or
418 // - state override is given,
419 // then:
420 else if ((i == length or c == '/' or c == '?' or c == '#') or (isSchemeSpecial(scheme.items) and c == '\\') or (state_override != null)) {
421 // 1. If buffer is not the empty string:
422 if (buffer.items.len > 0) {
423 // 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.
424 // 2. If port is not a 16-bit unsigned integer, port-out-of-range validation error, return failure.
425 const p = std.fmt.parseInt(u16, buffer.items, 10) catch return error.InvalidURL;
426 // 3. Set url’s port to null, if port is url’s scheme’s default port; otherwise to port.
427 if (schemeDefaultPort(scheme.items) != p) try port.writer().print("{d}", .{p});
428 // 4. Set buffer to the empty string.
429 buffer.clearRetainingCapacity();
430 // 5. If state override is given, then return.
431 if (state_override != null) break;
432 }
433 // 2. If state override is given, then return failure.
434 if (state_override != null) return error.InvalidURL;
435 // 3. Set state to path start state and decrease pointer by 1.
436 state = .path_start;
437 pointer -= 1;
438 i = lastcpi(inputl.items[0..i]);
439 c = inputl.items[i];
440 }
441 // 3. Otherwise, port-invalid validation error, return failure.
442 else {
443 return error.InvalidURL;
444 }
445 },
446 .file => {
447 // 1. Set url’s scheme to "file".
448 try scheme.appendSlice("file");
449 // 2. Set url’s host to the empty string.
450 host = .{ .name = "" };
451 // 3. If c is U+002F (/) or U+005C (\), then:
452 if (c == '/' or c == '\\') {
453 // 1. If c is U+005C (\), invalid-reverse-solidus validation error.
454 {}
455 // 2. Set state to file slash state.
456 state = .file_slash;
457 }
458 // 4. Otherwise, if base is non-null and base’s scheme is "file":
459 else if (base != null and std.mem.eql(u8, base.?.scheme, "file")) {
460 // 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.
461 // host = base.?.host;
462 // try path.appendSlice(base.?.path.?);
463 // try query.appendSlice(base.?.query.?);
464 // 2. If c is U+003F (?), then set url’s query to the empty string and state to query state.
465 if (c == '?') {
466 query.clearRetainingCapacity();
467 state = .query;
468 }
469 // 3. Otherwise, if c is U+0023 (#), set url’s fragment to the empty string and state to fragment state.
470 else if (c == '#') {
471 fragment.clearRetainingCapacity();
472 state = .fragment;
473 }
474 // 4. Otherwise, if c is not the EOF code point:
475 else if (i < length) {
476 // 1. Set url’s query to null.
477 query.clearRetainingCapacity();
478 // 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.
479 // 3. Otherwise:
480 // This is a (platform-independent) Windows drive letter quirk.
481 if (builtin.target.os.tag == .windows) @compileError("TODO");
482 {
483 // 1. File-invalid-Windows-drive-letter validation error.
484 // 2. Set url’s path to « ».
485 }
486 // 4. Set state to path state and decrease pointer by 1.
487 state = .path;
488 pointer -= 1;
489 i = lastcpi(inputl.items[0..i]);
490 c = inputl.items[i];
491 }
492 }
493 // 5. Otherwise, set state to path state, and decrease pointer by 1.
494 else {
495 state = .path;
496 pointer -= 1;
497 i = lastcpi(inputl.items[0..i]);
498 c = inputl.items[i];
499 }
500 },
501 .file_slash => {
502 // 1. If c is U+002F (/) or U+005C (\), then:
503 if (c == '/' or c == '\\') {
504 // 1. If c is U+005C (\), invalid-reverse-solidus validation error.
505 {}
506 // 2. Set state to file host state.
507 state = .file_host;
508 }
509 // 2. Otherwise:
510 else {
511 // 1. If base is non-null and base’s scheme is "file", then:
512 if (base != null and std.mem.eql(u8, base.?.scheme, "file")) {
513 // 1. Set url’s host to base’s host.
514 // host = base.?.host;
515 // 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.
516 // > This is a (platform-independent) Windows drive letter quirk.
517 if (builtin.target.os.tag == .windows) @compileError("TODO");
518 }
519 // 2. Set state to path state, and decrease pointer by 1.
520 state = .path;
521 pointer -= 1;
522 i = lastcpi(inputl.items[0..i]);
523 c = inputl.items[i];
524 }
525 },
526 .file_host => {
527 // 1. If c is the EOF code point, U+002F (/), U+005C (\), U+003F (?), or U+0023 (#), then decrease pointer by 1 and then:
528 if (i == length or c == '/' or c == '\\' or c == '?' or c == '#') {
529 pointer -= 1;
530 i = lastcpi(inputl.items[0..i]);
531 c = inputl.items[i];
532 // 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.
533 // > This is a (platform-independent) Windows drive letter quirk. buffer is not reset here and instead used in the path state.
534 if (builtin.target.os.tag == .windows) {
535 @compileError("TODO");
536 }
537 // 2. Otherwise, if buffer is the empty string, then:
538 else if (buffer.items.len == 0) {
539 // 1. Set url’s host to the empty string.
540 host = .{ .name = "" };
541 // 2. If state override is given, then return.
542 if (state_override != null) break;
543 // 3. Set state to path start state.
544 state = .path_start;
545 }
546 // 3. Otherwise, run these steps:
547 else {
548 // 1. Let host be the result of host parsing buffer with url is not special.
549 // 2. If host is failure, then return failure.
550 var h = try parseHost(alloc, buffer.items, !isSchemeSpecial(scheme.items));
551 // 3. If host is "localhost", then set host to the empty string.
552 if (h == .name and std.mem.eql(u8, h.name, "localhost")) {
553 alloc.free(h.name);
554 h = .{ .name = "" };
555 }
556 // 4. Set url’s host to host.
557 host = h;
558 // 5. If state override is given, then return.
559 if (state_override != null) break;
560 // 6. Set buffer to the empty string and state to path start state.
561 buffer.clearRetainingCapacity();
562 state = .path_start;
563 }
564 }
565 // 2. Otherwise, append c to buffer.
566 try buffer.appendSlice(inputl.items[i..][0..l(c)]);
567 },
568 .path_start => {
569 // 1. If url is special, then:
570 if (isSchemeSpecial(scheme.items)) {
571 // 1. If c is U+005C (\), invalid-reverse-solidus validation error.
572 {}
573 // 2. Set state to path state.
574 state = .path;
575 // 3. If c is neither U+002F (/) nor U+005C (\), then decrease pointer by 1.
576 if (c != '/' and c != '\\') {
577 pointer -= 1;
578 i = lastcpi(inputl.items[0..i]);
579 c = inputl.items[i];
580 }
581 }
582 // 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.
583 else if (state_override == null and c == '?') {
584 query.clearRetainingCapacity();
585 state = .query;
586 }
587 // 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.
588 else if (state_override == null and c == '#') {
589 fragment.clearRetainingCapacity();
590 state = .fragment;
591 }
592 // 4. Otherwise, if c is not the EOF code point:
593 else if (i < length) {
594 // 1. Set state to path state.
595 state = .path;
596 // 2. If c is not U+002F (/), then decrease pointer by 1.
597 if (c != '/') {
598 pointer -= 1;
599 i = lastcpi(inputl.items[0..i]);
600 c = inputl.items[i];
601 }
602 }
603 // 5. Otherwise, if state override is given and url’s host is null, append the empty string to url’s path.
604 else if (state_override != null) {
605 @panic("TODO");
606 }
607 },
608 .path => {
609 // 1. If one of the following is true:
610 // - c is the EOF code point or U+002F (/)
611 // - url is special and c is U+005C (\)
612 // - state override is not given and c is U+003F (?) or U+0023 (#)
613 // then:
614 if ((i == length or c == '/') or (isSchemeSpecial(scheme.items) and c == '\\') or (state_override == null and (c == '?' or c == '#'))) {
615 // 1. If url is special and c is U+005C (\), invalid-reverse-solidus validation error.
616 {}
617 // 2. If buffer is a double-dot URL path segment, then:
618 if (isDoubleDotPathSeg(buffer.items)) {
619 @panic("TODO");
620 // 1. Shorten url’s path.
621 // 2. If neither c is U+002F (/), nor url is special and c is U+005C (\), append the empty string to url’s path.
622 // > This means that for input /usr/.. the result is / and not a lack of a path.
623 }
624 // 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.
625 else if (isSingleDotPathSeg(buffer.items)) {
626 @panic("TODO");
627 }
628 // 4. Otherwise, if buffer is not a single-dot URL path segment, then:
629 else if (isSingleDotPathSeg(buffer.items)) {
630 // 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 (:).
631 // > This is a (platform-independent) Windows drive letter quirk.
632 if (builtin.target.os.tag == .windows) @compileError("TODO");
633 // 2. Append buffer to url’s path.
634 try path.appendSlice(buffer.items);
635 }
636 // 5. Set buffer to the empty string.
637 buffer.clearRetainingCapacity();
638 // 6. If c is U+003F (?), then set url’s query to the empty string and state to query state.
639 if (c == '?') {
640 query.clearRetainingCapacity();
641 state = .query;
642 }
643 // 7. If c is U+0023 (#), then set url’s fragment to the empty string and state to fragment state.
644 if (c == '#') {
645 fragment.clearRetainingCapacity();
646 state = .fragment;
647 }
648 }
649 // 2. Otherwise, run these steps:
650 else {
651 // 1. If c is not a URL code point and not U+0025 (%), invalid-URL-unit validation error.
652 {}
653 // 2. If c is U+0025 (%) and remaining does not start with two ASCII hex digits, invalid-URL-unit validation error.
654 {}
655 // 3. UTF-8 percent-encode c using the path percent-encode set and append the result to buffer.
656 if (is_path_percent_char(c)) {
657 const pe = try percentEncode(alloc, inputl.items[i..][0..l(c)], is_path_percent_char);
658 defer alloc.free(pe);
659 try buffer.appendSlice(pe);
660 } else {
661 try buffer.appendSlice(inputl.items[i..][0..l(c)]);
662 }
663 }
664 },
665 .opaque_path => {
666 // 1. If c is U+003F (?), then set url’s query to the empty string and state to query state.
667 if (c == '?') {
668 query.clearRetainingCapacity();
669 state = .query;
670 }
671 // 2. Otherwise, if c is U+0023 (#), then set url’s fragment to the empty string and state to fragment state.
672 else if (c == '#') {
673 fragment.clearRetainingCapacity();
674 state = .fragment;
675 }
676 // 3. Otherwise, if c is U+0020 SPACE:
677 else if (c == ' ') {
678 // 1. If remaining starts with U+003F (?) or U+003F (#), then append "%20" to url’s path.
679 const rem = inputl.items[i + l(c) ..];
680 if (std.mem.startsWith(u8, rem, "?") or std.mem.startsWith(u8, rem, "#")) {
681 try path.appendSlice("%20");
682 }
683 // 2. Otherwise, append U+0020 SPACE to url’s path.
684 else {
685 try path.append(' ');
686 }
687 }
688 // 4. Otherwise, if c is not the EOF code point:
689 else if (i < length) {
690 // 1. If c is not a URL code point and not U+0025 (%), invalid-URL-unit validation error.
691 {}
692 // 2. If c is U+0025 (%) and remaining does not start with two ASCII hex digits, invalid-URL-unit validation error.
693 {}
694 // 3. UTF-8 percent-encode c using the C0 control percent-encode set and append the result to url’s path.
695 try percentEncodeScalarAL(&path, inputl.items[i..][0..l(c)], is_c0control_percent_char);
696 }
697 },
698 .query => {
699 // 1. If encoding is not UTF-8 and one of the following is true:
700 // - url is not special
701 // - url’s scheme is "ws" or "wss"
702 // then set encoding to UTF-8.
703 // 2. If one of the following is true:
704 // - state override is not given and c is U+0023 (#)
705 // - c is the EOF code point
706 // then:
707 if ((state_override == null and c == '#') or (i == length)) {
708 // 1. Let queryPercentEncodeSet be the special-query percent-encode set if url is special; otherwise the query percent-encode set.
709 // 2. Percent-encode after encoding, with encoding, buffer, and queryPercentEncodeSet, and append the result to url’s query.
710 // > This operation cannot be invoked code-point-for-code-point due to the stateful ISO-2022-JP encoder.
711 if (isSchemeSpecial(scheme.items)) {
712 try percentEncodeAL(&query, buffer.items, is_special_query_percent_char);
713 } else {
714 try percentEncodeAL(&query, buffer.items, is_query_percent_char);
715 }
716 // 3. Set buffer to the empty string.
717 buffer.clearRetainingCapacity();
718 // 4. If c is U+0023 (#), then set url’s fragment to the empty string and state to fragment state.
719 if (c == '#') {
720 fragment.clearRetainingCapacity();
721 state = .fragment;
722 }
723 }
724 // 3. Otherwise, if c is not the EOF code point:
725 else if (i < length) {
726 // 1. If c is not a URL code point and not U+0025 (%), invalid-URL-unit validation error.
727 {}
728 // 2. If c is U+0025 (%) and remaining does not start with two ASCII hex digits, invalid-URL-unit validation error.
729 {}
730 // 3. Append c to buffer.
731 try buffer.appendSlice(inputl.items[i..][0..l(c)]);
732 }
733 },
734 .fragment => {
735 // 1. If c is not the EOF code point, then:
736 if (i < length) {
737 // 1. If c is not a URL code point and not U+0025 (%), invalid-URL-unit validation error.
738 {}
739 // 2. If c is U+0025 (%) and remaining does not start with two ASCII hex digits, invalid-URL-unit validation error.
740 {}
741 // 3. UTF-8 percent-encode c using the fragment percent-encode set and append the result to url’s fragment.
742 try percentEncodeScalarAL(&fragment, inputl.items[i..][0..l(c)], is_fragment_percent_char);
743 }
744 },
745 }
746
747 // 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.
748 if (pointer == length) {
749 if (state == .fragment) break;
750 state = @enumFromInt(@intFromEnum(state) + 1);
751 } else {
752 i += l(c);
753 c = if (i < length) inputl.items[i] else 0;
754 pointer += 1;
755 }
756 }
757
758 const _href = try href.toOwnedSlice();
759
760 const url: URL = .{
761 .href = _href,
762 };
763 return url;
764 }
765
766 const BasicParserState = enum {
767 scheme_start,
768 scheme,
769 no_scheme,
770 special_relative_or_authority,
771 path_or_authority,
772 relative,
773 relative_slash,
774 special_authority_slashes,
775 special_authority_ignore_slashes,
776 authority,
777 host,
778 hostname,
779 port,
780 file,
781 file_slash,
782 file_host,
783 path_start,
784 path,
785 opaque_path,
786 query,
787 fragment,
788 };
789
790 pub fn isSpecial(u: URL) bool {
791 return isSchemeSpecial(u.scheme);
24 }792 }
25};793};
794
795/// https://url.spec.whatwg.org/#special-scheme
796fn isSchemeSpecial(scheme: []const u8) bool {
797 if (std.mem.eql(u8, scheme, "ftp")) return true;
798 if (std.mem.eql(u8, scheme, "file")) return true;
799 if (std.mem.eql(u8, scheme, "http")) return true;
800 if (std.mem.eql(u8, scheme, "https")) return true;
801 if (std.mem.eql(u8, scheme, "ws")) return true;
802 if (std.mem.eql(u8, scheme, "wss")) return true;
803 return false;
804}
805
806/// https://url.spec.whatwg.org/#default-port
807fn schemeDefaultPort(scheme: []const u8) ?u16 {
808 if (std.mem.eql(u8, scheme, "ftp")) return 21;
809 if (std.mem.eql(u8, scheme, "file")) return null;
810 if (std.mem.eql(u8, scheme, "http")) return 80;
811 if (std.mem.eql(u8, scheme, "https")) return 443;
812 if (std.mem.eql(u8, scheme, "ws")) return 80;
813 if (std.mem.eql(u8, scheme, "wss")) return 443;
814 return null;
815}
816
817/// https://url.spec.whatwg.org/#double-dot-path-segment
818fn isDoubleDotPathSeg(segment: []const u8) bool {
819 if (std.mem.eql(u8, segment, "..")) return true;
820 if (std.ascii.eqlIgnoreCase(segment, ".%2e")) return true;
821 if (std.ascii.eqlIgnoreCase(segment, "%2e.")) return true;
822 if (std.ascii.eqlIgnoreCase(segment, "%2e%2e")) return true;
823 return false;
824}
825
826/// https://url.spec.whatwg.org/#single-dot-path-segment
827fn isSingleDotPathSeg(segment: []const u8) bool {
828 if (std.mem.eql(u8, segment, ".")) return true;
829 if (std.ascii.eqlIgnoreCase(segment, "%2e")) return true;
830 return false;
831}
832
833/// https://url.spec.whatwg.org/#concept-host-parser
834fn parseHost(allocator: std.mem.Allocator, input: []u8, isOpaque: bool) !URL.Host {
835 // 1. If input starts with U+005B ([), then:
836 if (std.mem.startsWith(u8, input, "[")) {
837 // 1. If input does not end with U+005D (]), IPv6-unclosed validation error, return failure.
838 if (!std.mem.endsWith(u8, input, "]")) return error.InvalidURL;
839 // 2. Return the result of IPv6 parsing input with its leading U+005B ([) and trailing U+005D (]) removed.
840 const adr = try parseIPv6(input[1 .. input.len - 1 - 1]);
841 return .{ .ipv6 = adr };
842 }
843 // 2. If isOpaque is true, then return the result of opaque-host parsing input.
844 if (isOpaque) return .{ .name = try parseHostOpaque(allocator, input) };
845 // 3. Assert: input is not the empty string.
846 std.debug.assert(input.len > 0);
847 // 4. Let domain be the result of running UTF-8 decode without BOM on the percent-decoding of input.
848 // > 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 (�).
849 const domain = try percentDecode(allocator, input);
850 defer allocator.free(domain);
851 if (!std.unicode.utf8ValidateSlice(domain)) return error.InvalidURL;
852 // 5. Let asciiDomain be the result of running domain to ASCII with domain and false.
853 // 6. If asciiDomain is failure, then return failure.
854 const asciidomain = try domainToAscii(allocator, domain, false);
855 // 7. If asciiDomain ends in a number, then return the result of IPv4 parsing asciiDomain.
856 if (endsInANumber(asciidomain)) {
857 defer allocator.free(asciidomain);
858 const adr = try parseIPv4(input);
859 return .{ .ipv4 = adr };
860 }
861 // 8. Return asciiDomain.
862 return .{ .name = asciidomain };
863}
864
865/// https://url.spec.whatwg.org/#concept-ipv6-parser
866fn parseIPv6(input: []const u8) !u128 {
867 // 1. Let address be a new IPv6 address whose pieces are all 0.
868 var address: [8]u16 = @splat(0);
869 _ = &address;
870 // 2. Let pieceIndex be 0.
871 var pieceIndex: u8 = 0;
872 _ = &pieceIndex;
873 // 3. Let compress be null.
874 var compress: ?u8 = null;
875 _ = &compress;
876 // 4. Let pointer be a pointer for input.
877 std.debug.assert(extras.matchesAll(u8, input, std.ascii.isAscii));
878 var pointer: usize = 0;
879 // 5. If c is U+003A (:), then:
880 if (input[pointer] == ':') {
881 // 1. If remaining does not start with U+003A (:), IPv6-invalid-compression validation error, return failure.
882 if (!std.mem.startsWith(u8, input[pointer + 1 ..], ":")) return error.InvalidURL;
883 // 2. Increase pointer by 2.
884 pointer += 2;
885 // 3. Increase pieceIndex by 1 and then set compress to pieceIndex.
886 pieceIndex += 1;
887 compress = pieceIndex;
888 }
889 // 6. While c is not the EOF code point:
890 while (pointer != input.len) {
891 // 1. If pieceIndex is 8, IPv6-too-many-pieces validation error, return failure.
892 if (pieceIndex == 8) return error.InvalidURL;
893 // 2. If c is U+003A (:), then:
894 if (input[pointer] == ':') {
895 // 1. If compress is non-null, IPv6-multiple-compression validation error, return failure.
896 if (compress != null) return error.InvalidURL;
897 // 2. Increase pointer and pieceIndex by 1, set compress to pieceIndex, and then continue.
898 pointer += 1;
899 pieceIndex += 1;
900 compress = pieceIndex;
901 continue;
902 }
903 // 3. Let value and length be 0.
904 var value: u16 = 0;
905 var length: usize = 0;
906 // 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.
907 const hex_alpha_upper = "0123456789ABCDEF";
908 const hex_alpha_lower = "0123456789abcdef";
909 while (length < 4 and pointer < input.len and std.ascii.isHex(input[pointer])) {
910 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);
911 value = value * 0x10 + as_hex;
912 pointer += 1;
913 length += 1;
914 }
915 // 5. If c is U+002E (.), then:
916 if (pointer < input.len and input[pointer] == '.') {
917 // 1. If length is 0, IPv4-in-IPv6-invalid-code-point validation error, return failure.
918 if (length == 0) return error.InvalidURL;
919 // 2. Decrease pointer by length.
920 pointer -= length;
921 // 3. If pieceIndex is greater than 6, IPv4-in-IPv6-too-many-pieces validation error, return failure.
922 if (pieceIndex > 6) return error.InvalidURL;
923 // 4. Let numbersSeen be 0.
924 var numbersSeen: usize = 0;
925 // 5. While c is not the EOF code point:
926 if (pointer < input.len) {
927 // 1. Let ipv4Piece be null.
928 var ipv4Piece: ?u16 = null;
929 // 2. If numbersSeen is greater than 0, then:
930 if (numbersSeen > 0) {
931 // 1. If c is a U+002E (.) and numbersSeen is less than 4, then increase pointer by 1.
932 if (input[pointer] == '.' and numbersSeen < 4) {
933 pointer += 1;
934 }
935 // 2. Otherwise, IPv4-in-IPv6-invalid-code-point validation error, return failure.
936 else {
937 return error.InvalidURL;
938 }
939 }
940 // 3. If c is not an ASCII digit, IPv4-in-IPv6-invalid-code-point validation error, return failure.
941 if (!std.ascii.isDigit(input[pointer])) return error.InvalidURL;
942 // 4. While c is an ASCII digit:
943 while (std.ascii.isDigit(input[pointer])) {
944 // 1. Let number be c interpreted as decimal number.
945 const dec_alpha = "0123456789";
946 const number: u8 = @intCast(std.mem.indexOfScalar(u8, dec_alpha, input[pointer]).?);
947 // 2. If ipv4Piece is null, then set ipv4Piece to number.
948 if (ipv4Piece == null) {
949 ipv4Piece = number;
950 }
951 // Otherwise, if ipv4Piece is 0, IPv4-in-IPv6-invalid-code-point validation error, return failure.
952 else if (ipv4Piece == 0) {
953 return error.InvalidURL;
954 }
955 // Otherwise, set ipv4Piece to ipv4Piece × 10 + number.
956 else {
957 ipv4Piece = ipv4Piece.? * 10 + number;
958 }
959 // 3. If ipv4Piece is greater than 255, IPv4-in-IPv6-out-of-range-part validation error, return failure.
960 if (ipv4Piece.? > 255) return error.InvalidURL;
961 // 4. Increase pointer by 1.
962 pointer += 1;
963 }
964 // 5. Set address[pieceIndex] to address[pieceIndex] × 0x100 + ipv4Piece.
965 address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece.?;
966 // 6. Increase numbersSeen by 1.
967 numbersSeen += 1;
968 // 7. If numbersSeen is 2 or 4, then increase pieceIndex by 1.
969 if (numbersSeen == 2 or numbersSeen == 4) pieceIndex += 1;
970 }
971 // 6. If numbersSeen is not 4, IPv4-in-IPv6-too-few-parts validation error, return failure.
972 if (numbersSeen != 4) return error.InvalidURL;
973 // 7. Break.
974 break;
975 }
976 // 6. Otherwise, if c is U+003A (:):
977 else if (pointer < input.len and input[pointer] == ':') {
978 // 1. Increase pointer by 1.
979 pointer += 1;
980 // 2. If c is the EOF code point, IPv6-invalid-code-point validation error, return failure.
981 if (pointer == input.len) return error.InvalidURL;
982 }
983 // 7. Otherwise, if c is not the EOF code point, IPv6-invalid-code-point validation error, return failure.
984 else if (pointer < input.len) {
985 return error.InvalidURL;
986 }
987 // 8. Set address[pieceIndex] to value.
988 address[pieceIndex] = value;
989 // 9. Increase pieceIndex by 1.
990 pieceIndex += 1;
991 }
992 // 7. If compress is non-null, then:
993 if (compress != null) {
994 // 1. Let swaps be pieceIndex − compress.
995 var swaps = pieceIndex - compress.?;
996 // 2. Set pieceIndex to 7.
997 pieceIndex = 7;
998 // 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.
999 while (pieceIndex != 0 and swaps > 0) {
1000 std.mem.swap(u16, &address[pieceIndex], &address[compress.? + swaps - 1]);
1001 pieceIndex -= 1;
1002 swaps -= 1;
1003 }
1004 }
1005 // 8. Otherwise, if compress is null and pieceIndex is not 8, IPv6-too-few-pieces validation error, return failure.
1006 else if (compress == null and pieceIndex != 8) {
1007 return error.InvalidURL;
1008 }
1009 // 9. Return address.
1010 return @bitCast(address);
1011}
1012
1013/// https://url.spec.whatwg.org/#concept-opaque-host-parser
1014fn parseHostOpaque(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
1015 // 1. If input contains a forbidden host code point, host-invalid-code-point validation error, return failure.
1016 if (extras.matchesAny(u8, input, is_forbidden_host_codepoint)) return error.InvalidURL;
1017 // 2. If input contains a code point that is not a URL code point and not U+0025 (%), invalid-URL-unit validation error.
1018 {}
1019 // 3. If input contains a U+0025 (%) and the two code points following it are not ASCII hex digits, invalid-URL-unit validation error.
1020 {}
1021 // 4. Return the result of running UTF-8 percent-encode on input using the C0 control percent-encode set.
1022 return percentEncode(allocator, input, is_c0control);
1023}
1024
1025/// https://url.spec.whatwg.org/#utf-8-percent-encode
1026fn percentEncode(allocator: std.mem.Allocator, input: []const u8, comptime set: fn (u8) bool) ![]u8 {
1027 if (!extras.matchesAny(u8, input, set)) return allocator.dupe(u8, input);
1028 var result = std.ArrayList(u8).init(allocator);
1029 errdefer result.deinit();
1030 try result.ensureUnusedCapacity(input.len);
1031 try percentEncodeAL(&result, input, set);
1032 return result.toOwnedSlice();
1033}
1034
1035/// https://url.spec.whatwg.org/#string-percent-decode
1036fn percentDecode(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
1037 var result = std.ArrayList(u8).init(allocator);
1038 errdefer result.deinit();
1039 try result.ensureUnusedCapacity(input.len);
1040 var i: usize = 0;
1041 while (i < input.len) : (i += 1) {
1042 if (input[i] == '%') {
1043 if (input.len >= i + 1 + 2) {
1044 if (std.ascii.isHex(input[i + 1]) and std.ascii.isHex(input[i + 2])) {
1045 try result.append(std.fmt.parseInt(u8, input[i + 1 ..][0..2], 16) catch unreachable);
1046 i += 2;
1047 continue;
1048 }
1049 }
1050 }
1051 try result.append(input[i]);
1052 }
1053 return result.toOwnedSlice();
1054}
1055
1056/// https://url.spec.whatwg.org/#concept-domain-to-ascii
1057// TODO:
1058fn domainToAscii(allocator: std.mem.Allocator, domain: []const u8, beStrict: bool) ![]u8 {
1059 _ = beStrict;
1060 return allocator.dupe(u8, domain);
1061}
1062
1063// https://url.spec.whatwg.org/#ends-in-a-number-checker
1064fn endsInANumber(input: []const u8) bool {
1065 // 1. Let parts be the result of strictly splitting input on U+002E (.).
1066 // 2. If the last item in parts is the empty string, then:
1067 {
1068 // 1. If parts’s size is 1, then return false.
1069 // 2. Remove the last item from parts.
1070 }
1071 // 3. Let last be the last item in parts.
1072 // 4. If last is non-empty and contains only ASCII digits, then return true.
1073 // > The erroneous input "09" will be caught by the IPv4 parser at a later stage.
1074 // 5. If parsing last as an IPv4 number does not return failure, then return true.
1075 // > This is equivalent to checking that last is "0X" or "0x", followed by zero or more ASCII hex digits.
1076 // 6. Return false.
1077
1078 _ = std.mem.splitBackwardsScalar;
1079 var end = input.len;
1080 var start: usize = if (std.mem.lastIndexOfScalar(u8, input[0..end], '.')) |i| i + 1 else 0;
1081 if (end - start == 0) {
1082 if (std.mem.count(u8, input, ".") == 0) return false;
1083 end = start;
1084 start = if (std.mem.lastIndexOfScalar(u8, input[0..end], '.')) |i| i + 1 else 0;
1085 }
1086 const last = input[start..end];
1087 if (last.len > 0 and extras.matchesAll(u8, last, std.ascii.isDigit)) return true;
1088 parseIPv4Number(last, void) catch return false;
1089 return true;
1090}
1091
1092/// https://url.spec.whatwg.org/#concept-ipv4-parser
1093fn parseIPv4(input: []const u8) !u32 {
1094 // 1. Let parts be the result of strictly splitting input on U+002E (.).
1095 var end = input.len;
1096 var parts_size = std.mem.count(u8, input[0..end], ".") + 1;
1097 // 2. If the last item in parts is the empty string, then:
1098 if (std.mem.endsWith(u8, input, ".")) {
1099 // 1. IPv4-empty-part validation error.
1100 // 2. If parts’s size is greater than 1, then remove the last item from parts.
1101 if (parts_size > 1) {
1102 end = std.mem.lastIndexOfScalar(u8, input, '.').?;
1103 parts_size -= 1;
1104 }
1105 }
1106 var iter = std.mem.splitScalar(u8, input[0..end], '.');
1107 // 3. If parts’s size is greater than 4, IPv4-too-many-parts validation error, return failure.
1108 if (parts_size > 4) return error.InvalidURL;
1109 // 4. Let numbers be an empty list.
1110 var numbers: [4]u32 = @splat(0);
1111 var numbers_len: u8 = 0;
1112 // 5. For each part of parts:
1113 while (iter.next()) |part| {
1114 // 1. Let result be the result of parsing part.
1115 // 2. If result is failure, IPv4-non-numeric-part validation error, return failure.
1116 const result = parseIPv4Number(part, u32) catch return error.InvalidURL;
1117 // 3. If result[1] is true, IPv4-non-decimal-part validation error.
1118 {}
1119 // 4. Append result[0] to numbers.
1120 numbers[numbers_len] = result;
1121 numbers_len += 1;
1122 }
1123 // 6. If any item in numbers is greater than 255, IPv4-out-of-range-part validation error.
1124 {}
1125 // 7. If any but the last item in numbers is greater than 255, then return failure.
1126 for (0..numbers_len - 1) |i| if (numbers[i] > 255) return error.InvalidURL;
1127 // 8. If the last item in numbers is greater than or equal to 256^(5 − numbers’s size), then return failure.
1128 if (numbers[numbers_len - 1] >= std.math.pow(u32, 256, 5 - numbers_len)) return error.InvalidURL;
1129 // 9. Let ipv4 be the last item in numbers.
1130 var ipv4 = numbers[numbers_len - 1];
1131 // 10. Remove the last item from numbers.
1132 numbers_len -= 1;
1133 // 11. Let counter be 0.
1134 // 12. For each n of numbers:
1135 for (0..numbers_len, 0..) |counter, n| {
1136 // 1. Increment ipv4 by n × 256^(3 − counter).
1137 ipv4 += @as(u32, @intCast(n)) * std.math.pow(u32, 256, 3 - @as(u8, @intCast(counter)));
1138 // 2. Increment counter by 1.
1139 }
1140 // 13. Return ipv4.
1141 return ipv4;
1142}
1143
1144// https://url.spec.whatwg.org/#ipv4-number-parser
1145fn parseIPv4Number(input_: []const u8, T: type) !T {
1146 var input = input_;
1147 // 1. If input is the empty string, then return failure.
1148 if (input.len == 0) return error.Invalid;
1149 // 2. Let validationError be false.
1150 var validationError = false;
1151 // 3. Let R be 10.
1152 var radix: u8 = 10;
1153 // 4. If input contains at least two code points and the first two code points are either "0X" or "0x", then:
1154 if (std.mem.startsWith(u8, input, "0X") or std.mem.startsWith(u8, input, "0x")) {
1155 // 1. Set validationError to true.
1156 validationError = true;
1157 // 2. Remove the first two code points from input.
1158 input = input[2..];
1159 // 3. Set R to 16.
1160 radix = 16;
1161 }
1162 // 5. Otherwise, if input contains at least two code points and the first code point is U+0030 (0), then:
1163 else if (input.len >= 2 and input[0] == '0') {
1164 // 1. Set validationError to true.
1165 validationError = true;
1166 // 2. Remove the first code point from input.
1167 input = input[1..];
1168 // 3. Set R to 8.
1169 radix = 8;
1170 }
1171 // 6. If input is the empty string, then return (0, true).
1172 if (input.len == 0 and T == void) return;
1173 if (input.len == 0 and T != void) return 0;
1174 // 7. If input contains a code point that is not a radix-R digit, then return failure.
1175 switch (radix) {
1176 8 => for (input) |c| switch (c) {
1177 '0'...'7' => {},
1178 else => return error.Invalid,
1179 },
1180 10 => for (input) |c| if (!std.ascii.isDigit(c)) return error.Invalid,
1181 16 => for (input) |c| if (!std.ascii.isHex(c)) return error.Invalid,
1182 else => unreachable,
1183 }
1184 // 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.
1185 // 9. Return (output, validationError).
1186 if (T == void) return;
1187 const output = std.fmt.parseInt(T, input, radix) catch return error.Invalid;
1188 return output;
1189}
1190
1191//
1192//
1193//
1194//
1195
1196/// https://infra.spec.whatwg.org/#c0-control
1197fn is_c0control(c: u8) bool {
1198 if (c >= 0x00 and c <= 0x1F) return true;
1199 return false;
1200}
1201/// https://infra.spec.whatwg.org/#c0-control-or-space
1202fn is_c0control_or_space(c: u8) bool {
1203 if (is_c0control(c)) return true;
1204 if (c == ' ') return true;
1205 return false;
1206}
1207/// https://url.spec.whatwg.org/#forbidden-host-code-point
1208fn is_forbidden_host_codepoint(c: u8) bool {
1209 if (c == 0) return true;
1210 if (c == '\t') return true;
1211 if (c == '\n') return true;
1212 if (c == '\r') return true;
1213 if (c == ' ') return true;
1214 if (c == '#') return true;
1215 if (c == '/') return true;
1216 if (c == ':') return true;
1217 if (c == '<') return true;
1218 if (c == '>') return true;
1219 if (c == '?') return true;
1220 if (c == '@') return true;
1221 if (c == '[') return true;
1222 if (c == '\\') return true;
1223 if (c == ']') return true;
1224 if (c == '^') return true;
1225 if (c == '|') return true;
1226 return false;
1227}
1228/// https://url.spec.whatwg.org/#url-code-points
1229fn is_url_codepoint(c: u21) bool {
1230 if (c < 128 and std.ascii.isAlphanumeric(@intCast(c))) return true;
1231 if (c == '!') return true;
1232 if (c == '$') return true;
1233 if (c == '&') return true;
1234 if (c == '\'') return true;
1235 if (c == '(') return true;
1236 if (c == ')') return true;
1237 if (c == '*') return true;
1238 if (c == '+') return true;
1239 if (c == ',') return true;
1240 if (c == '-') return true;
1241 if (c == '.') return true;
1242 if (c == '/') return true;
1243 if (c == ':') return true;
1244 if (c == ';') return true;
1245 if (c == '=') return true;
1246 if (c == '?') return true;
1247 if (c == '@') return true;
1248 if (c == '_') return true;
1249 if (c == '~') return true;
1250 // and code points in the range U+00A0 to U+10FFFD, inclusive,
1251 // excluding surrogates and noncharacters.
1252 if (c >= 0x00A0 and c <= 0x10FFFD) return true;
1253 return false;
1254}
1255/// https://url.spec.whatwg.org/#c0-control-percent-encode-set
1256fn is_c0control_percent_char(c: u8) bool {
1257 if (c >= 0x00 and c <= 0x1F) return true;
1258 return c > 0x7E;
1259}
1260/// https://url.spec.whatwg.org/#query-percent-encode-set
1261fn is_query_percent_char(c: u8) bool {
1262 if (c == ' ') return true;
1263 if (c == '"') return true;
1264 if (c == '#') return true;
1265 if (c == '<') return true;
1266 if (c == '>') return true;
1267 return is_c0control_percent_char(c);
1268}
1269/// https://url.spec.whatwg.org/#path-percent-encode-set
1270fn is_path_percent_char(c: u8) bool {
1271 if (c == '?') return true;
1272 if (c == '`') return true;
1273 if (c == '{') return true;
1274 if (c == '}') return true;
1275 return is_query_percent_char(c);
1276}
1277/// https://url.spec.whatwg.org/#special-query-percent-encode-set
1278fn is_special_query_percent_char(c: u8) bool {
1279 if (c == '\'') return true;
1280 return is_query_percent_char(c);
1281}
1282/// https://url.spec.whatwg.org/#fragment-percent-encode-set
1283fn is_fragment_percent_char(c: u8) bool {
1284 if (c == ' ') return true;
1285 if (c == '"') return true;
1286 if (c == '<') return true;
1287 if (c == '>') return true;
1288 if (c == '`') return true;
1289 return is_c0control_percent_char(c);
1290}
1291
1292// fn is_userinfo_percent_char(c: u8) bool {
1293// if (c == '/') return true;
1294// if (c == ':') return true;
1295// if (c == ';') return true;
1296// if (c == '=') return true;
1297// if (c == '@') return true;
1298// if (c >= '[' and c <= '^') return true;
1299// if (c == '|') return true;
1300// return is_path_percent_char(c);
1301// }
1302// fn is_component_percent_char(c: u8) bool {
1303// if (c >= '$' and c <= '&') return true;
1304// if (c == '+') return true;
1305// if (c == ',') return true;
1306// return is_userinfo_percent_char(c);
1307// }
1308// fn is_formurlencoded_percent_char(c: u8) bool {
1309// if (c == '!') return true;
1310// if (c >= '\'' and c <= ')') return true;
1311// if (c == '~') return true;
1312// return is_component_percent_char(c);
1313// }
1314
1315/// Asserts b is a valid UTF-8 codepoint
1316fn l(b: u8) u3 {
1317 return std.unicode.utf8ByteSequenceLength(b) catch unreachable;
1318}
1319fn lastcpi(haystack: []const u8) usize {
1320 var i = haystack.len - 1;
1321 while (haystack[i] & 0xC0 == 0x80) : (i -= 1) {}
1322 return i;
1323}
1324fn percentEncodeScalarAL(list: *std.ArrayList(u8), cp: []const u8, comptime set: fn (u8) bool) !void {
1325 if (!set(cp[0])) {
1326 for (cp) |b| {
1327 try list.append('%');
1328 try list.writer().print("{d:0<2}", .{b});
1329 }
1330 } else {
1331 try list.append(cp[0]);
1332 }
1333}
1334fn percentEncodeAL(list: *std.ArrayList(u8), input: []const u8, comptime set: fn (u8) bool) !void {
1335 var it = std.unicode.Utf8View.initUnchecked(input).iterator();
1336 while (it.nextCodepointSlice()) |sl| {
1337 if (!set(sl[0])) {
1338 for (sl) |b| {
1339 try list.append('%');
1340 try list.writer().print("{d:0<2}", .{b});
1341 }
1342 } else {
1343 try list.appendSlice(sl);
1344 }
1345 }
1346}
zig-out/test.zig+213
...@@ -47,6 +47,219 @@ pub fn parseIDNAPass(input: []const u8, output: []const u8) !void {...@@ -47,6 +47,219 @@ pub fn parseIDNAPass(input: []const u8, output: []const u8) !void {
47 return error.SkipZigTest;47 return error.SkipZigTest;
48}48}
4949
50test { try parseFail("file://example:1/", null); }
51test { try parseFail("file://example:test/", null); }
52test { try parseFail("file://example%/", null); }
53test { try parseFail("file://[example]/", null); }
54test { try parseFail("http://user:pass@/", null); }
55test { try parseFail("http://foo:-80/", null); }
56test { try parseFail("http:/:@/www.example.com", null); }
57test { try parseFail("http://user@/www.example.com", null); }
58test { try parseFail("http:@/www.example.com", null); }
59test { try parseFail("http:/@/www.example.com", null); }
60test { try parseFail("http://@/www.example.com", null); }
61test { try parseFail("https:@/www.example.com", null); }
62test { try parseFail("http:a:b@/www.example.com", null); }
63test { try parseFail("http:/a:b@/www.example.com", null); }
64test { try parseFail("http://a:b@/www.example.com", null); }
65test { try parseFail("http::@/www.example.com", null); }
66test { try parseFail("http:@:www.example.com", null); }
67test { try parseFail("http:/@:www.example.com", null); }
68test { try parseFail("http://@:www.example.com", null); }
69test { try parseFail("https://\xfffd", null); }
70test { try parseFail("https://%EF%BF%BD", null); }
71test { try parseFail("http://a.b.c.xn--pokxncvks", null); }
72test { try parseFail("http://10.0.0.xn--pokxncvks", null); }
73test { try parseFail("http://a.b.c.XN--pokxncvks", null); }
74test { try parseFail("http://a.b.c.Xn--pokxncvks", null); }
75test { try parseFail("http://10.0.0.XN--pokxncvks", null); }
76test { try parseFail("http://10.0.0.xN--pokxncvks", null); }
77test { try parseFail("https://x x:12", null); }
78test { try parseFail("http://[www.google.com]/", null); }
79test { try parseFail("sc://@/", null); }
80test { try parseFail("sc://te@s:t@/", null); }
81test { try parseFail("sc://:/", null); }
82test { try parseFail("sc://:12/", null); }
83test { try parseFail("sc://a\x00b/", null); }
84test { try parseFail("sc://a b/", null); }
85test { try parseFail("sc://a<b", null); }
86test { try parseFail("sc://a>b", null); }
87test { try parseFail("sc://a[b/", null); }
88test { try parseFail("sc://a\\b/", null); }
89test { try parseFail("sc://a]b/", null); }
90test { try parseFail("sc://a^b", null); }
91test { try parseFail("sc://a|b/", null); }
92test { try parseFail("http://a\x00b/", null); }
93test { try parseFail("http://a\x01b/", null); }
94test { try parseFail("http://a\x02b/", null); }
95test { try parseFail("http://a\x03b/", null); }
96test { try parseFail("http://a\x04b/", null); }
97test { try parseFail("http://a\x05b/", null); }
98test { try parseFail("http://a\x06b/", null); }
99test { try parseFail("http://a\x07b/", null); }
100test { try parseFail("http://a\x08b/", null); }
101test { try parseFail("http://a\x0bb/", null); }
102test { try parseFail("http://a\x0cb/", null); }
103test { try parseFail("http://a\x0eb/", null); }
104test { try parseFail("http://a\x0fb/", null); }
105test { try parseFail("http://a\x10b/", null); }
106test { try parseFail("http://a\x11b/", null); }
107test { try parseFail("http://a\x12b/", null); }
108test { try parseFail("http://a\x13b/", null); }
109test { try parseFail("http://a\x14b/", null); }
110test { try parseFail("http://a\x15b/", null); }
111test { try parseFail("http://a\x16b/", null); }
112test { try parseFail("http://a\x17b/", null); }
113test { try parseFail("http://a\x18b/", null); }
114test { try parseFail("http://a\x19b/", null); }
115test { try parseFail("http://a\x1ab/", null); }
116test { try parseFail("http://a\x1bb/", null); }
117test { try parseFail("http://a\x1cb/", null); }
118test { try parseFail("http://a\x1db/", null); }
119test { try parseFail("http://a\x1eb/", null); }
120test { try parseFail("http://a\x1fb/", null); }
121test { try parseFail("http://a b/", null); }
122test { try parseFail("http://a%b/", null); }
123test { try parseFail("http://a<b", null); }
124test { try parseFail("http://a>b", null); }
125test { try parseFail("http://a[b/", null); }
126test { try parseFail("http://a]b/", null); }
127test { try parseFail("http://a^b", null); }
128test { try parseFail("http://a|b/", null); }
129test { try parseFail("http://a\x7fb/", null); }
130test { try parseFail("http://ho%00st/", null); }
131test { try parseFail("http://ho%01st/", null); }
132test { try parseFail("http://ho%02st/", null); }
133test { try parseFail("http://ho%03st/", null); }
134test { try parseFail("http://ho%04st/", null); }
135test { try parseFail("http://ho%05st/", null); }
136test { try parseFail("http://ho%06st/", null); }
137test { try parseFail("http://ho%07st/", null); }
138test { try parseFail("http://ho%08st/", null); }
139test { try parseFail("http://ho%09st/", null); }
140test { try parseFail("http://ho%0Ast/", null); }
141test { try parseFail("http://ho%0Bst/", null); }
142test { try parseFail("http://ho%0Cst/", null); }
143test { try parseFail("http://ho%0Dst/", null); }
144test { try parseFail("http://ho%0Est/", null); }
145test { try parseFail("http://ho%0Fst/", null); }
146test { try parseFail("http://ho%10st/", null); }
147test { try parseFail("http://ho%11st/", null); }
148test { try parseFail("http://ho%12st/", null); }
149test { try parseFail("http://ho%13st/", null); }
150test { try parseFail("http://ho%14st/", null); }
151test { try parseFail("http://ho%15st/", null); }
152test { try parseFail("http://ho%16st/", null); }
153test { try parseFail("http://ho%17st/", null); }
154test { try parseFail("http://ho%18st/", null); }
155test { try parseFail("http://ho%19st/", null); }
156test { try parseFail("http://ho%1Ast/", null); }
157test { try parseFail("http://ho%1Bst/", null); }
158test { try parseFail("http://ho%1Cst/", null); }
159test { try parseFail("http://ho%1Dst/", null); }
160test { try parseFail("http://ho%1Est/", null); }
161test { try parseFail("http://ho%1Fst/", null); }
162test { try parseFail("http://ho%20st/", null); }
163test { try parseFail("http://ho%23st/", null); }
164test { try parseFail("http://ho%25st/", null); }
165test { try parseFail("http://ho%2Fst/", null); }
166test { try parseFail("http://ho%3Ast/", null); }
167test { try parseFail("http://ho%3Cst/", null); }
168test { try parseFail("http://ho%3Est/", null); }
169test { try parseFail("http://ho%3Fst/", null); }
170test { try parseFail("http://ho%40st/", null); }
171test { try parseFail("http://ho%5Bst/", null); }
172test { try parseFail("http://ho%5Cst/", null); }
173test { try parseFail("http://ho%5Dst/", null); }
174test { try parseFail("http://ho%7Cst/", null); }
175test { try parseFail("http://ho%7Fst/", null); }
176test { try parseFail("ftp://example.com%80/", null); }
177test { try parseFail("ftp://example.com%A0/", null); }
178test { try parseFail("https://example.com%80/", null); }
179test { try parseFail("https://example.com%A0/", null); }
180test { try parseFail("https://0x100000000/test", null); }
181test { try parseFail("https://256.0.0.1/test", null); }
182test { try parseFail("file://%43%3A", null); }
183test { try parseFail("file://%43%7C", null); }
184test { try parseFail("file://%43|", null); }
185test { try parseFail("file://C%7C", null); }
186test { try parseFail("file://%43%7C/", null); }
187test { try parseFail("https://%43%7C/", null); }
188test { try parseFail("asdf://%43|/", null); }
189test { try parseFail("\\\\\\.\\Y:", null); }
190test { try parseFail("\\\\\\.\\y:", null); }
191test { try parseFail("https://[0::0::0]", null); }
192test { try parseFail("https://[0:.0]", null); }
193test { try parseFail("https://[0:0:]", null); }
194test { try parseFail("https://[0:1:2:3:4:5:6:7.0.0.0.1]", null); }
195test { try parseFail("https://[0:1.00.0.0.0]", null); }
196test { try parseFail("https://[0:1.290.0.0.0]", null); }
197test { try parseFail("https://[0:1.23.23]", null); }
198test { try parseFail("http://?", null); }
199test { try parseFail("http://#", null); }
200test { try parseFail("non-special://[:80/", null); }
201test { try parseFail("http://[::127.0.0.0.1]", null); }
202test { try parseFail("a", null); }
203test { try parseFail("a/", null); }
204test { try parseFail("a//", null); }
205test { try parseFail("file://\xad/p", null); }
206test { try parseFail("file://%C2%AD/p", null); }
207test { try parseFail("file://xn--/p", null); }
208test { try parseFail("#", null); }
209test { try parseFail("?", null); }
210test { try parseFail("http://0..0x300/", null); }
211test { try parseFail("http://0..0x300./", null); }
212test { try parseFail("http://1.2.3.08", null); }
213test { try parseFail("http://1.2.3.08.", null); }
214test { try parseFail("http://1.2.3.09", null); }
215test { try parseFail("http://09.2.3.4", null); }
216test { try parseFail("http://09.2.3.4.", null); }
217test { try parseFail("http://01.2.3.4.5", null); }
218test { try parseFail("http://01.2.3.4.5.", null); }
219test { try parseFail("http://0x100.2.3.4", null); }
220test { try parseFail("http://0x100.2.3.4.", null); }
221test { try parseFail("http://0x1.2.3.4.5", null); }
222test { try parseFail("http://0x1.2.3.4.5.", null); }
223test { try parseFail("http://foo.1.2.3.4", null); }
224test { try parseFail("http://foo.1.2.3.4.", null); }
225test { try parseFail("http://foo.2.3.4", null); }
226test { try parseFail("http://foo.2.3.4.", null); }
227test { try parseFail("http://foo.09", null); }
228test { try parseFail("http://foo.09.", null); }
229test { try parseFail("http://foo.0x4", null); }
230test { try parseFail("http://foo.0x4.", null); }
231test { try parseFail("http://0999999999999999999/", null); }
232test { try parseFail("http://foo.0x", null); }
233test { try parseFail("http://foo.0XFfFfFfFfFfFfFfFfFfAcE123", null); }
234test { try parseFail("http://\xd83d\xdca9.123/", null); }
235test { try parseFail("https://\x00y", null); }
236test { try parseFail("https://\xffffy", null); }
237test { try parseFail("", null); }
238test { try parseFail("https://\xad/", null); }
239test { try parseFail("https://%C2%AD/", null); }
240test { try parseFail("https://xn--/", null); }
241test { try parseFail("data://:443", null); }
242test { try parseFail("data://test:test", null); }
243test { try parseFail("data://[:1]", null); }
244test { try parseFail("javascript://:443", null); }
245test { try parseFail("javascript://test:test", null); }
246test { try parseFail("javascript://[:1]", null); }
247test { try parseFail("mailto://:443", null); }
248test { try parseFail("mailto://test:test", null); }
249test { try parseFail("mailto://[:1]", null); }
250test { try parseFail("intent://:443", null); }
251test { try parseFail("intent://test:test", null); }
252test { try parseFail("intent://[:1]", null); }
253test { try parseFail("urn://:443", null); }
254test { try parseFail("urn://test:test", null); }
255test { try parseFail("urn://[:1]", null); }
256test { try parseFail("turn://:443", null); }
257test { try parseFail("turn://test:test", null); }
258test { try parseFail("turn://[:1]", null); }
259test { try parseFail("stun://:443", null); }
260test { try parseFail("stun://test:test", null); }
261test { try parseFail("stun://[:1]", null); }
262test { try parseFail("non-special://host\\a", null); }
50263
51264
52265
zigmod.yml+3
...@@ -3,5 +3,8 @@ name: url...@@ -3,5 +3,8 @@ name: url
3main: url.zig3main: url.zig
4license: MPL-2.04license: MPL-2.0
5description: A WHATWG URL-compatible parser for Zig5description: A WHATWG URL-compatible parser for Zig
6dependencies:
7 - src: git https://github.com/nektro/zig-net
8 - src: git https://github.com/nektro/zig-extras
6root_dependencies:9root_dependencies:
7 - src: git https://github.com/nektro/zig-expect10 - src: git https://github.com/nektro/zig-expect