diff options
| author | 2021-08-14 02:39:44 -0700 | |
|---|---|---|
| committer | 2021-08-14 02:39:44 -0700 | |
| commit | 16c76743048ef905269e2711cb0148ecc4e57f3f (patch) | |
| tree | a8fb11b55767945a47e92120179f2f762b1f8e99 /src/query_string_map.zig | |
| parent | f59892f647ceef1c05e40c9cdef4f79d0a530c2f (diff) | |
| download | bun-16c76743048ef905269e2711cb0148ecc4e57f3f.tar.gz bun-16c76743048ef905269e2711cb0148ecc4e57f3f.tar.zst bun-16c76743048ef905269e2711cb0148ecc4e57f3f.zip | |
lots
Former-commit-id: 0b8128cb3b4db02f9d33331b4c2c1b595156e6c8
Diffstat (limited to 'src/query_string_map.zig')
| -rw-r--r-- | src/query_string_map.zig | 417 |
1 files changed, 417 insertions, 0 deletions
diff --git a/src/query_string_map.zig b/src/query_string_map.zig index 2640e8a73..00b451e9a 100644 --- a/src/query_string_map.zig +++ b/src/query_string_map.zig @@ -1,7 +1,305 @@ const std = @import("std"); const Api = @import("./api/schema.zig").Api; +const resolve_path = @import("./resolver/resolve_path.zig"); usingnamespace @import("./global.zig"); +// This is close to WHATWG URL, but we don't want the validation errors +pub const URL = struct { + hash: string = "", + host: string = "", + hostname: string = "", + href: string = "", + origin: string = "", + password: string = "", + pathname: string = "/", + path: string = "/", + port: string = "", + protocol: string = "", + search: string = "", + searchParams: ?QueryStringMap = null, + username: string = "", + + port_was_automatically_set: bool = false, + + pub fn hasHTTPLikeProtocol(this: *const URL) bool { + return strings.eqlComptime(this.protocol, "http") or strings.eqlComptime(this.protocol, "https"); + } + + pub fn getPort(this: *const URL) ?u16 { + return std.fmt.parseInt(u16, this.port, 10) catch null; + } + + pub fn hasValidPort(this: *const URL) bool { + return (this.getPort() orelse 0) > 1; + } + + pub fn isEmpty(this: *const URL) bool { + return this.href.len == 0; + } + + pub fn isAbsolute(this: *const URL) bool { + return this.hostname.len > 0 and this.pathname.len > 0; + } + + pub fn joinNormalize(out: []u8, prefix: string, dirname: string, basename: string, extname: string) string { + var buf: [2048]u8 = undefined; + + var path_parts: [10]string = undefined; + var path_end: usize = 0; + + path_parts[0] = "/"; + path_end += 1; + + if (prefix.len > 0) { + path_parts[path_end] = prefix; + path_end += 1; + } + + if (dirname.len > 0) { + path_parts[path_end] = std.mem.trim(u8, dirname, "/\\"); + path_end += 1; + } + + if (basename.len > 0) { + if (dirname.len > 0) { + path_parts[path_end] = "/"; + path_end += 1; + } + + path_parts[path_end] = std.mem.trim(u8, basename, "/\\"); + path_end += 1; + } + + if (extname.len > 0) { + path_parts[path_end] = extname; + path_end += 1; + } + + var buf_i: usize = 0; + for (path_parts[0..path_end]) |part| { + std.mem.copy(u8, buf[buf_i..], part); + buf_i += part.len; + } + return resolve_path.normalizeStringBuf(buf[0..buf_i], out, false, .loose, false); + } + + pub fn joinWrite( + this: *const URL, + comptime Writer: type, + writer: Writer, + prefix: string, + dirname: string, + basename: string, + extname: string, + ) !void { + var out: [2048]u8 = undefined; + const normalized_path = joinNormalize(&out, prefix, dirname, basename, extname); + + try writer.print("{s}/{s}", .{ this.origin, normalized_path }); + } + + pub fn joinAlloc(this: *const URL, allocator: *std.mem.Allocator, prefix: string, dirname: string, basename: string, extname: string) !string { + var out: [2048]u8 = undefined; + const normalized_path = joinNormalize(&out, prefix, dirname, basename, extname); + + return try std.fmt.allocPrint(allocator, "{s}/{s}", .{ this.origin, normalized_path }); + } + + pub fn parse(base_: string) URL { + const base = std.mem.trim(u8, base_, &std.ascii.spaces); + if (base.len == 0) return URL{}; + var url = URL{}; + url.href = base; + var offset: u31 = 0; + switch (base[0]) { + '@' => { + offset += url.parsePassword(base[offset..]) orelse 0; + offset += url.parseHost(base[offset..]) orelse 0; + }, + 'a'...'z', 'A'...'Z', '0'...'9', '-', '_', ':' => { + offset += url.parseProtocol(base[offset..]) orelse 0; + + // if there's no protocol or @, it's ambiguous whether the colon is a port or a username. + if (offset > 0) { + if ((std.mem.indexOfScalar(u8, base[offset..], '@') orelse 0) > (std.mem.indexOfScalar(u8, base[offset..], ':') orelse 0)) { + offset += url.parseUsername(base[offset..]) orelse 0; + offset += url.parsePassword(base[offset..]) orelse 0; + } + } + + offset += url.parseHost(base[offset..]) orelse 0; + }, + else => {}, + } + + url.origin = base[0..offset]; + + if (offset > base.len) { + return url; + } + + const path_offset = offset; + + var can_update_path = true; + if (base.len > offset + 1 and base[offset] == '/' and base[offset..].len > 0) { + url.path = base[offset..]; + url.pathname = url.path; + } + + if (strings.indexOfChar(base[offset..], '?')) |q| { + offset += @intCast(u31, q); + url.path = base[path_offset..][0..q]; + can_update_path = false; + url.search = base[offset..]; + } + + if (strings.indexOfChar(base[offset..], '#')) |hash| { + offset += @intCast(u31, hash); + if (can_update_path) { + url.path = base[path_offset..][0..hash]; + } + url.hash = base[offset..]; + + if (url.search.len > 0) { + url.search = url.search[0 .. url.search.len - url.hash.len]; + } + } + + if (base.len > path_offset and base[path_offset] == '/' and offset > 0) { + url.pathname = base[path_offset..std.math.min(offset, base.len)]; + url.origin = base[0..path_offset]; + } + + if (url.path.len > 1) { + const trimmed = std.mem.trim(u8, url.path, "/"); + if (trimmed.len > 1) { + url.path = url.path[std.math.max(@ptrToInt(trimmed.ptr) - @ptrToInt(url.path.ptr), 1) - 1 ..]; + } else { + url.path = "/"; + } + } else { + url.path = "/"; + } + + if (url.pathname.len == 0) { + url.pathname = "/"; + } + + url.origin = std.mem.trim(u8, url.origin, "/ ?#"); + return url; + } + + pub fn parseProtocol(url: *URL, str: string) ?u31 { + var i: u31 = 0; + if (str.len < "://".len) return null; + while (i < str.len) : (i += 1) { + switch (str[i]) { + '/', '?', '%' => { + return null; + }, + ':' => { + if (i + 3 <= str.len and str[i + 1] == '/' and str[i + 2] == '/') { + url.protocol = str[0..i]; + return i + 3; + } + }, + else => {}, + } + } + + return null; + } + + pub fn parseUsername(url: *URL, str: string) ?u31 { + var i: u31 = 0; + + // reset it + url.username = ""; + + if (str.len < "@".len) return null; + + while (i < str.len) : (i += 1) { + switch (str[i]) { + ':', '@' => { + // we found a username, everything before this point in the slice is a username + url.username = str[0..i]; + return i + 1; + }, + // if we reach a slash, there's no username + '/' => { + return null; + }, + else => {}, + } + } + return null; + } + + pub fn parsePassword(url: *URL, str: string) ?u31 { + var i: u31 = 0; + + // reset it + url.password = ""; + + if (str.len < "@".len) return null; + + while (i < str.len) : (i += 1) { + switch (str[i]) { + '@' => { + // we found a password, everything before this point in the slice is a password + url.password = str[0..i]; + std.debug.assert(str[i..].len < 2 or std.mem.readIntNative(u16, str[i..][0..2]) != std.mem.readIntNative(u16, "//")); + return i + 1; + }, + // if we reach a slash, there's no password + '/' => { + return null; + }, + else => {}, + } + } + return null; + } + + pub fn parseHost(url: *URL, str: string) ?u31 { + var i: u31 = 0; + + // reset it + url.host = ""; + url.hostname = ""; + url.port = ""; + + // look for the first "/" + // if we have a slash, anything before that is the host + // anything before the colon is the hostname + // anything after the colon but before the slash is the port + // the origin is the scheme before the slash + + var colon_i: ?u31 = null; + while (i < str.len) : (i += 1) { + colon_i = if (colon_i == null and str[i] == ':') i else colon_i; + + switch (str[i]) { + // alright, we found the slash + '/' => { + break; + }, + else => {}, + } + } + + url.host = str[0..i]; + if (colon_i) |colon| { + url.hostname = str[0..colon]; + url.port = str[colon + 1 .. i]; + } else { + url.hostname = str[0..i]; + } + + return i; + } +}; + /// QueryString array-backed hash table that does few allocations and preserves the original order pub const QueryStringMap = struct { allocator: *std.mem.Allocator, @@ -825,3 +1123,122 @@ test "QueryStringMap Iterator" { try expect(iter.next(buf) == null); } + +test "URL - parse" { + var url = URL.parse("https://url.spec.whatwg.org/foo#include-credentials"); + try expectString("https", url.protocol); + try expectString("url.spec.whatwg.org", url.host); + try expectString("/foo", url.pathname); + try expectString("#include-credentials", url.hash); + + url = URL.parse("https://url.spec.whatwg.org/#include-credentials"); + try expectString("https", url.protocol); + try expectString("url.spec.whatwg.org", url.host); + try expectString("/", url.pathname); + try expectString("#include-credentials", url.hash); + + url = URL.parse("://url.spec.whatwg.org/#include-credentials"); + try expectString("", url.protocol); + try expectString("url.spec.whatwg.org", url.host); + try expectString("/", url.pathname); + try expectString("#include-credentials", url.hash); + + url = URL.parse("/#include-credentials"); + try expectString("", url.protocol); + try expectString("", url.host); + try expectString("/", url.pathname); + try expectString("#include-credentials", url.hash); + + url = URL.parse("https://username:password@url.spec.whatwg.org/#include-credentials"); + try expectString("https", url.protocol); + try expectString("username", url.username); + try expectString("password", url.password); + try expectString("url.spec.whatwg.org", url.host); + try expectString("/", url.pathname); + try expectString("#include-credentials", url.hash); + + url = URL.parse("https://username:password@url.spec.whatwg.org:3000/#include-credentials"); + try expectString("https", url.protocol); + try expectString("username", url.username); + try expectString("password", url.password); + try expectString("url.spec.whatwg.org:3000", url.host); + try expectString("3000", url.port); + try expectString("/", url.pathname); + try expectString("#include-credentials", url.hash); + + url = URL.parse("example.com/#include-credentials"); + try expectString("", url.protocol); + try expectString("", url.username); + try expectString("", url.password); + try expectString("example.com", url.host); + try expectString("/", url.pathname); + try expectString("#include-credentials", url.hash); + + url = URL.parse("example.com:8080/#include-credentials"); + try expectString("", url.protocol); + try expectString("", url.username); + try expectString("", url.password); + try expectString("example.com:8080", url.host); + try expectString("example.com", url.hostname); + try expectString("8080", url.port); + try expectString("/", url.pathname); + try expectString("#include-credentials", url.hash); + + url = URL.parse("example.com:8080/////#include-credentials"); + try expectString("", url.protocol); + try expectString("", url.username); + try expectString("", url.password); + try expectString("example.com:8080", url.host); + try expectString("example.com", url.hostname); + try expectString("8080", url.port); + try expectString("/////", url.pathname); + try expectString("/", url.path); + try expectString("#include-credentials", url.hash); + url = URL.parse("example.com:8080/////hi?wow#include-credentials"); + try expectString("", url.protocol); + try expectString("", url.username); + try expectString("", url.password); + try expectString("example.com:8080", url.host); + try expectString("example.com", url.hostname); + try expectString("8080", url.port); + try expectString("/////hi?wow", url.pathname); + try expectString("/hi", url.path); + try expectString("#include-credentials", url.hash); + try expectString("?wow", url.search); + + url = URL.parse("/src/index"); + try expectString("", url.protocol); + try expectString("", url.username); + try expectString("", url.password); + try expectString("", url.host); + try expectString("", url.hostname); + try expectString("", url.port); + try expectString("/src/index", url.path); + try expectString("/src/index", url.pathname); + + try expectString("", url.hash); + try expectString("", url.search); + + url = URL.parse("http://localhost:3000/"); + try expectString("http", url.protocol); + try expectString("", url.username); + try expectString("", url.password); + try expectString("localhost:3000", url.host); + try expectString("localhost", url.hostname); + try expectString("3000", url.port); + try expectString("/", url.path); + try expectString("/", url.pathname); +} + +test "URL - joinAlloc" { + var url = URL.parse("http://localhost:3000"); + + var absolute_url = try url.joinAlloc(std.heap.c_allocator, "/_next/", "src/components", "button", ".js"); + try expectString("http://localhost:3000/_next/src/components/button.js", absolute_url); + + absolute_url = try url.joinAlloc(std.heap.c_allocator, "compiled-", "src/components", "button", ".js"); + try expectString("http://localhost:3000/compiled-src/components/button.js", absolute_url); + + absolute_url = try url.joinAlloc(std.heap.c_allocator, "compiled-", "", "button", ".js"); + try expectString("http://localhost:3000/compiled-button.js", absolute_url); +} |
