From 994c9e8cf20ea4f1c00b8d7b669ce35b7dd00b22 Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sat, 10 Sep 2022 15:29:08 -0700 Subject: [PATCH] Add Url type --- src/util/Url.zig | 160 +++++++++++++++++++++++++++++++++++++++++++++++ src/util/lib.zig | 1 + 2 files changed, 161 insertions(+) create mode 100644 src/util/Url.zig diff --git a/src/util/Url.zig b/src/util/Url.zig new file mode 100644 index 0000000..515e507 --- /dev/null +++ b/src/util/Url.zig @@ -0,0 +1,160 @@ +const Url = @This(); +const std = @import("std"); + +scheme: []const u8, +hostport: []const u8, +path: []const u8, +query: []const u8, +fragment: []const u8, + +pub fn parse(url: []const u8) !Url { + const scheme_end = for (url) |ch, i| { + if (ch == ':') break i; + } else return error.InvalidUrl; + + if (url.len < scheme_end + 3 or url[scheme_end + 1] != '/' or url[scheme_end + 1] != '/') return error.InvalidUrl; + + const hostport_start = scheme_end + 3; + const hostport_end = for (url[hostport_start..]) |ch, i| { + if (ch == '/' or ch == '?' or ch == '#') break i + hostport_start; + } else url.len; + + const path_end = for (url[hostport_end..]) |ch, i| { + if (ch == '?' or ch == '#') break i + hostport_end; + } else url.len; + + const query_end = if (!(url.len > path_end and url[path_end] == '?')) + path_end + else for (url[path_end..]) |ch, i| { + if (ch == '#') break i + path_end; + } else url.len; + + const query = url[path_end..query_end]; + const fragment = url[query_end..]; + + return Url{ + .scheme = url[0..scheme_end], + .hostport = url[hostport_start..hostport_end], + .path = url[hostport_end..path_end], + .query = if (query.len > 0) query[1..] else query, + .fragment = if (fragment.len > 0) fragment[1..] else fragment, + }; +} + +pub fn getQuery(self: Url, param: []const u8) ?[]const u8 { + var key_start: usize = 0; + while (key_start < self.query.len) { + const key_end = for (self.query[key_start..]) |ch, i| { + if (ch == '=') break key_start + i; + } else return null; + + const val_start = key_end + 1; + const val_end = for (self.query[val_start..]) |ch, i| { + if (ch == '&') break val_start + i; + } else self.query.len; + + const key = self.query[key_start..key_end]; + if (std.mem.eql(u8, key, param)) return self.query[val_start..val_end]; + + key_start = val_end + 1; + } + + return null; +} + +pub fn strDecode(buf: []u8, str: []const u8) ![]u8 { + var str_i: usize = 0; + var buf_i: usize = 0; + while (str_i < str.len) : ({ + str_i += 1; + buf_i += 1; + }) { + if (buf_i >= buf.len) return error.NoSpaceLeft; + const ch = str[str_i]; + if (ch == '%') { + if (str.len < str_i + 2) return error.BadEscape; + + const hi = try std.fmt.charToDigit(str[str_i + 1], 16); + const lo = try std.fmt.charToDIgit(str[str_i + 2], 16); + str_i += 2; + + buf[buf_i] = (hi << 4) | lo; + } else { + buf[buf_i] = str[str_i]; + } + } + + return buf[0..buf_i]; +} + +fn expectEqualUrl(expected: Url, actual: Url) !void { + const t = @import("std").testing; + try t.expectEqualStrings(expected.scheme, actual.scheme); + try t.expectEqualStrings(expected.hostport, actual.hostport); + try t.expectEqualStrings(expected.path, actual.path); + try t.expectEqualStrings(expected.query, actual.query); + try t.expectEqualStrings(expected.fragment, actual.fragment); +} +test "Url" { + try expectEqualUrl(.{ + .scheme = "https", + .hostport = "example.com", + .path = "", + .query = "", + .fragment = "", + }, try Url.parse("https://example.com")); + + try expectEqualUrl(.{ + .scheme = "https", + .hostport = "example.com:1234", + .path = "", + .query = "", + .fragment = "", + }, try Url.parse("https://example.com:1234")); + + try expectEqualUrl(.{ + .scheme = "http", + .hostport = "example.com", + .path = "/home", + .query = "", + .fragment = "", + }, try Url.parse("http://example.com/home")); + + try expectEqualUrl(.{ + .scheme = "https", + .hostport = "example.com", + .path = "", + .query = "query=abc", + .fragment = "", + }, try Url.parse("https://example.com?query=abc")); + + try expectEqualUrl(.{ + .scheme = "https", + .hostport = "example.com", + .path = "", + .query = "query=abc", + .fragment = "", + }, try Url.parse("https://example.com?query=abc")); + + try expectEqualUrl(.{ + .scheme = "https", + .hostport = "example.com", + .path = "/path/to/resource", + .query = "query=abc", + .fragment = "123", + }, try Url.parse("https://example.com/path/to/resource?query=abc#123")); + + const t = @import("std").testing; + try t.expectError(error.InvalidUrl, Url.parse("https:example.com")); + try t.expectError(error.InvalidUrl, Url.parse("example.com")); +} + +test "Url.getQuery" { + const url = try Url.parse("https://example.com?a=xyz&b=jkl"); + const t = @import("std").testing; + + try t.expectEqualStrings("xyz", url.getQuery("a").?); + try t.expectEqualStrings("jkl", url.getQuery("b").?); + try t.expect(url.getQuery("c") == null); + try t.expect(url.getQuery("xyz") == null); +} diff --git a/src/util/lib.zig b/src/util/lib.zig index 0e292a0..bc48935 100644 --- a/src/util/lib.zig +++ b/src/util/lib.zig @@ -4,6 +4,7 @@ pub const ciutf8 = @import("./ciutf8.zig"); pub const Uuid = @import("./Uuid.zig"); pub const DateTime = @import("./DateTime.zig"); pub const PathIter = @import("./PathIter.zig"); +pub const Url = @import("./Url.zig"); pub fn cloneStr(str: []const u8, alloc: std.mem.Allocator) ![]const u8 { var new = try alloc.alloc(u8, str.len);