| 1 | const std = @import("std"); |
| 2 | const builtin = @import("builtin"); |
| 3 | const extras = @import("extras"); |
| 4 | const unicode_idna = @import("unicode-idna"); |
| 5 | const nio = @import("nio"); |
| 6 | |
| 7 | pub 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 | |
| 1010 | pub 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 |
| 1232 | fn 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 |
| 1243 | fn 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 |
| 1254 | fn 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 |
| 1263 | fn 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 |
| 1270 | fn 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 |
| 1278 | fn 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 |
| 1286 | fn 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 |
| 1296 | fn 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 |
| 1328 | pub 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 |
| 1476 | fn 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 |
| 1488 | fn 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 |
| 1498 | pub 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 | } |
| 1505 | pub 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 |
| 1522 | fn 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 |
| 1539 | fn 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 |
| 1567 | pub 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 |
| 1619 | fn 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 |
| 1666 | fn 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 |
| 1686 | pub 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 |
| 1691 | pub 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 |
| 1697 | pub 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 |
| 1718 | pub 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 |
| 1726 | pub 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 |
| 1753 | pub 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 |
| 1758 | pub 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 |
| 1767 | pub 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 |
| 1776 | pub 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 |
| 1781 | pub 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 |
| 1790 | pub 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 |
| 1801 | pub 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 |
| 1808 | pub 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 |
| 1816 | fn l(b: u8) u3 { |
| 1817 | return std.unicode.utf8ByteSequenceLength(b) catch unreachable; |
| 1818 | } |
| 1819 | fn lastcpi(haystack: []const u8) usize { |
| 1820 | var i = haystack.len - 1; |
| 1821 | while (haystack[i] & 0xC0 == 0x80) : (i -= 1) {} |
| 1822 | return i; |
| 1823 | } |
| 1824 | pub 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 | } |
| 1834 | pub 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 | } |
| 1847 | pub 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 | } |
| 1860 | fn 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 | } |
| 1870 | fn 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 | } |
| 1883 | fn 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 | } |
| 1933 | fn 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 | } |
| 1950 | fn 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 | |
| 1957 | pub 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 | } |