From f7040153cdcee4271f4258562b761f8a90229e17 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 6 May 2024 12:45:14 +0200 Subject: [PATCH] url: implement query parsing --- src/run_tests.zig | 4 ++ src/url/query.zig | 159 ++++++++++++++++++++++++++++++++++++++++++++++ src/url/url.zig | 63 ++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 src/url/query.zig diff --git a/src/run_tests.zig b/src/run_tests.zig index dd424238..fb554cd1 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -11,6 +11,7 @@ const Window = @import("html/window.zig").Window; const xhr = @import("xhr/xhr.zig"); const storage = @import("storage/storage.zig"); const url = @import("url/url.zig"); +const urlquery = @import("url/query.zig"); const documentTestExecFn = @import("dom/document.zig").testExecFn; const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn; @@ -278,6 +279,9 @@ test { const cssLibdomTest = @import("css/libdom_test.zig"); std.testing.refAllDecls(cssLibdomTest); + + const queryTest = @import("url/query.zig"); + std.testing.refAllDecls(queryTest); } fn testJSRuntime(alloc: std.mem.Allocator) !void { diff --git a/src/url/query.zig b/src/url/query.zig new file mode 100644 index 00000000..30d71b51 --- /dev/null +++ b/src/url/query.zig @@ -0,0 +1,159 @@ +const std = @import("std"); + +const Reader = @import("../str/parser.zig").Reader; + +// Values is a map with string key of string values. +pub const Values = struct { + alloc: std.mem.Allocator, + map: std.StringArrayHashMapUnmanaged(List), + + const List = std.ArrayListUnmanaged([]const u8); + + pub fn init(alloc: std.mem.Allocator) Values { + return .{ + .alloc = alloc, + .map = .{}, + }; + } + + pub fn deinit(self: *Values) void { + var it = self.map.iterator(); + while (it.next()) |entry| { + for (entry.value_ptr.items) |v| self.alloc.free(v); + entry.value_ptr.deinit(self.alloc); + self.alloc.free(entry.key_ptr.*); + } + self.map.deinit(self.alloc); + } + + // add the key value couple to the values. + // the key and the value are duplicated. + pub fn append(self: *Values, k: []const u8, v: []const u8) !void { + const vv = try self.alloc.dupe(u8, v); + + if (self.map.getPtr(k)) |list| { + return try list.append(self.alloc, vv); + } + + const kk = try self.alloc.dupe(u8, k); + var list = List{}; + try list.append(self.alloc, vv); + try self.map.put(self.alloc, kk, list); + } + + pub fn get(self: *Values, k: []const u8) [][]const u8 { + if (self.map.get(k)) |list| { + return list.items; + } + + return &[_][]const u8{}; + } + + pub fn first(self: *Values, k: []const u8) []const u8 { + if (self.map.getPtr(k)) |list| { + if (list.items.len == 0) return ""; + return list.items[0]; + } + + return ""; + } + + pub fn delete(self: *Values, k: []const u8) void { + if (self.map.getPtr(k)) |list| { + list.deinit(self.alloc); + _ = self.map.fetchSwapRemove(k); + } + } + + pub fn deleteValue(self: *Values, k: []const u8, v: []const u8) void { + const list = self.map.getPtr(k) orelse return; + + var i: usize = 0; + while (i < list.items.len) { + if (std.mem.eql(u8, v, list.items[i])) { + _ = list.swapRemove(i); + return; + } + i += 1; + } + } + + pub fn count(self: *Values) usize { + return self.map.count(); + } +}; + +// Parse the given query. +pub fn parseQuery(alloc: std.mem.Allocator, s: []const u8) !Values { + var values = Values.init(alloc); + errdefer values.deinit(); + + const ln = s.len; + if (ln == 0) return values; + + var r = Reader{ .s = s }; + while (true) { + const param = r.until('&'); + if (param.len == 0) break; + + var rr = Reader{ .s = param }; + const k = rr.until('='); + if (k.len == 0) continue; + + _ = rr.skip(); + const v = rr.tail(); + + // TODO decode k and v + + try values.append(k, v); + + if (!r.skip()) break; + } + + return values; +} + +test "parse empty query" { + var values = try parseQuery(std.testing.allocator, ""); + defer values.deinit(); + + try std.testing.expect(values.count() == 0); +} + +test "parse empty query &" { + var values = try parseQuery(std.testing.allocator, "&"); + defer values.deinit(); + + try std.testing.expect(values.count() == 0); +} + +test "parse query" { + var values = try parseQuery(std.testing.allocator, "a=b&b=c"); + defer values.deinit(); + + try std.testing.expect(values.count() == 2); + try std.testing.expect(values.get("a").len == 1); + try std.testing.expect(std.mem.eql(u8, values.get("a")[0], "b")); + try std.testing.expect(std.mem.eql(u8, values.first("a"), "b")); + + try std.testing.expect(values.get("b").len == 1); + try std.testing.expect(std.mem.eql(u8, values.get("b")[0], "c")); + try std.testing.expect(std.mem.eql(u8, values.first("b"), "c")); +} + +test "parse query no value" { + var values = try parseQuery(std.testing.allocator, "a"); + defer values.deinit(); + + try std.testing.expect(values.count() == 1); + try std.testing.expect(std.mem.eql(u8, values.first("a"), "")); +} + +test "parse query dup" { + var values = try parseQuery(std.testing.allocator, "a=b&a=c"); + defer values.deinit(); + + try std.testing.expect(values.count() == 1); + try std.testing.expect(std.mem.eql(u8, values.first("a"), "b")); + try std.testing.expect(values.get("a").len == 2); +} diff --git a/src/url/url.zig b/src/url/url.zig index 7a627196..f268493d 100644 --- a/src/url/url.zig +++ b/src/url/url.zig @@ -5,6 +5,8 @@ const Case = jsruntime.test_utils.Case; const checkCases = jsruntime.test_utils.checkCases; const generate = @import("../generate.zig"); +const query = @import("query.zig"); + pub const Interfaces = generate.Tuple(.{ URL, URLSearchParams, @@ -27,6 +29,7 @@ pub const Interfaces = generate.Tuple(.{ pub const URL = struct { rawuri: []const u8, uri: std.Uri, + search_params: URLSearchParams, pub const mem_guarantied = true; @@ -41,10 +44,12 @@ pub const URL = struct { return .{ .rawuri = raw, .uri = uri, + .search_params = try URLSearchParams.constructor(alloc, uri.query), }; } pub fn deinit(self: *URL, alloc: std.mem.Allocator) void { + self.search_params.deinit(); alloc.free(self.rawuri); } @@ -125,14 +130,57 @@ pub const URL = struct { return try std.mem.concat(alloc, u8, &[_][]const u8{ "#", self.uri.fragment.? }); } + pub fn get_searchParams(self: *URL) *URLSearchParams { + return &self.search_params; + } + pub fn _toJSON(self: *URL, alloc: std.mem.Allocator) ![]const u8 { return try self.get_href(alloc); } }; // https://url.spec.whatwg.org/#interface-urlsearchparams +// TODO array like pub const URLSearchParams = struct { + values: query.Values, + pub const mem_guarantied = true; + + pub fn constructor(alloc: std.mem.Allocator, init: ?[]const u8) !URLSearchParams { + return .{ + .values = try query.parseQuery(alloc, init orelse ""), + }; + } + + pub fn deinit(self: *URLSearchParams, _: std.mem.Allocator) void { + self.values.deinit(); + } + + pub fn get_size(self: *URLSearchParams) u32 { + return @intCast(self.values.count()); + } + + pub fn _append(self: *URLSearchParams, name: []const u8, value: []const u8) !void { + try self.values.append(name, value); + } + + pub fn _delete(self: *URLSearchParams, name: []const u8, value: ?[]const u8) !void { + if (value) |v| return self.values.deleteValue(name, v); + + self.values.delete(name); + } + + pub fn _get(self: *URLSearchParams, name: []const u8) ?[]const u8 { + return self.values.first(name); + } + + // TODO return generates an error: caught unexpected error 'TypeLookup' + // pub fn _getAll(self: *URLSearchParams, name: []const u8) [][]const u8 { + // try self.values.get(name); + // } + + // TODO + pub fn _sort(_: *URLSearchParams) void {} }; // Tests @@ -154,6 +202,21 @@ pub fn testExecFn( .{ .src = "url.pathname", .ex = "/path" }, .{ .src = "url.search", .ex = "?query" }, .{ .src = "url.hash", .ex = "#fragment" }, + .{ .src = "url.searchParams.get('query')", .ex = "" }, }; try checkCases(js_env, &url); + + var qs = [_]Case{ + .{ .src = "var url = new URL('https://foo.bar/path?a=~&b=%7E')", .ex = "undefined" }, + .{ .src = "url.searchParams.get('a')", .ex = "~" }, + .{ .src = "url.searchParams.get('b')", .ex = "~" }, + .{ .src = "url.searchParams.append('c', 'foo')", .ex = "undefined" }, + .{ .src = "url.searchParams.get('c')", .ex = "foo" }, + .{ .src = "url.searchParams.size", .ex = "3" }, + .{ .src = "url.searchParams.delete('c', 'foo')", .ex = "undefined" }, + .{ .src = "url.searchParams.get('c')", .ex = "" }, + .{ .src = "url.searchParams.delete('a')", .ex = "undefined" }, + .{ .src = "url.searchParams.get('a')", .ex = "" }, + }; + try checkCases(js_env, &qs); }