| ... | ... | @@ -1,25 +1,1346 @@ |
| 1 | 1 | const std = @import("std"); |
| 2 | const builtin = @import("builtin"); |
| 3 | const net = @import("net"); |
| 4 | const extras = @import("extras"); |
| 2 | 5 | |
| 3 | 6 | pub const URL = struct { |
| 4 | 7 | href: []const u8, |
| 5 | | origin: []const u8, |
| 6 | | protocol: []const u8, |
| 7 | | username: []const u8, |
| 8 | | password: []const u8, |
| 9 | | host: []const u8, |
| 10 | | hostname: []const u8, |
| 11 | | port: []const u8, |
| 12 | | pathname: []const u8, |
| 13 | | search: []const u8, |
| 14 | | hash: []const u8, |
| 8 | scheme: []const u8 = "", |
| 9 | |
| 10 | pub const HostKind = enum { |
| 11 | name, |
| 12 | ipv4, |
| 13 | ipv6, |
| 14 | }; |
| 15 | |
| 16 | const Host = union(HostKind) { |
| 17 | name: []const u8, |
| 18 | ipv4: u32, |
| 19 | ipv6: u128, |
| 20 | }; |
| 15 | 21 | |
| 16 | 22 | /// Caller owns memory and is responsible for freeing `url.href`. |
| 17 | 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 = "" }; |
| 19 | | _ = &u; |
| 20 | | _ = alloc; |
| 21 | | _ = input; |
| 22 | | _ = base; |
| 23 | | return error.SkipZigTest; |
| 24 | const u = try parseBasic(alloc, input, if (base) |b| try parseBasic(alloc, b, null, null) else null, null); |
| 25 | return u; |
| 26 | } |
| 27 | |
| 28 | /// https://url.spec.whatwg.org/#concept-basic-url-parser |
| 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 |
| 796 | fn 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 |
| 807 | fn 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 |
| 818 | fn 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 |
| 827 | fn 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 |
| 834 | fn 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 |
| 866 | fn 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 |
| 1014 | fn 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 |
| 1026 | fn 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 |
| 1036 | fn 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: |
| 1058 | fn 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 |
| 1064 | fn 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 |
| 1093 | fn 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 |
| 1145 | fn 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 |
| 1197 | fn 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 |
| 1202 | fn 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 |
| 1208 | fn 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 |
| 1229 | fn 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 |
| 1256 | fn 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 |
| 1261 | fn 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 |
| 1270 | fn 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 |
| 1278 | fn 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 |
| 1283 | fn 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 |
| 1316 | fn l(b: u8) u3 { |
| 1317 | return std.unicode.utf8ByteSequenceLength(b) catch unreachable; |
| 1318 | } |
| 1319 | fn lastcpi(haystack: []const u8) usize { |
| 1320 | var i = haystack.len - 1; |
| 1321 | while (haystack[i] & 0xC0 == 0x80) : (i -= 1) {} |
| 1322 | return i; |
| 1323 | } |
| 1324 | fn 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 | } |
| 1334 | fn 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 | } |