From 2e9575197c909d8acf128ed5627db2a53765abd5 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Tue, 3 Mar 2026 01:42:15 -0800 Subject: [PATCH] add URLSearchParams --- test.SearchParams.zig | 546 ++++++++++++++++++++++++++++++++++++++++++ test.zig | 4 + url.zig | 244 +++++++++++++++++-- 3 files changed, 780 insertions(+), 14 deletions(-) create mode 100644 test.SearchParams.zig diff --git a/test.SearchParams.zig b/test.SearchParams.zig new file mode 100644 index 0000000000000000000000000000000000000000..c17744aa4eb8b43f5d43a717d9399d11bdbc5142 --- /dev/null +++ b/test.SearchParams.zig @@ -0,0 +1,546 @@ +const std = @import("std"); +const url = @import("url"); +const expect = @import("expect").expect; + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=b&c=d"); + try expect(x.get("a")).toEqualString("b"); + try expect(x.get("c")).toEqualString("d"); + try expect(x.get("e")).toBeNull(); + + try expect(try x.encode()).toEqualString("a=b&c=d"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "e=f&g=h"); + try expect(try x.encode()).toEqualString("e=f&g=h"); + + try x.set("i", " j "); + try expect(x.get("i")).toEqualString(" j "); + try expect(try x.encode()).toEqualString("e=f&g=h&i=+j+"); + + try x.set("e", "updated"); + try expect(x.get("e")).toEqualString("updated"); + try expect(try x.encode()).toEqualString("e=updated&g=h&i=+j+"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("a", "b"); + try expect(try x.encode()).toEqualString("a=b"); + try x.append("a", "b"); + try expect(try x.encode()).toEqualString("a=b&a=b"); + try x.append("a", "c"); + try expect(try x.encode()).toEqualString("a=b&a=b&a=c"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("", ""); + try expect(try x.encode()).toEqualString("="); + try x.append("", ""); + try expect(try x.encode()).toEqualString("=&="); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("first", "1"); + try x.append("second", "2"); + try x.append("third", ""); + try x.append("first", "10"); + try expect(x.get("first")).not().toBeNull(); + try expect(x.get("first")).toEqualString("1"); + try expect(x.get("second")).toEqualString("2"); + try expect(x.get("third")).toEqualString(""); + try x.append("first", "10"); + try expect(x.get("first")).toEqualString("1"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try expect(try x.encode()).toEqualString(""); + try x.append("a", "b"); + try expect(try x.encode()).toEqualString("a=b"); + try x.append("a", "c"); + try expect(try x.encode()).toEqualString("a=b&a=c"); + try expect(x.take("a")).toEqualString("b"); + try expect(try x.encode()).toEqualString("a=c"); + try expect(x.take("a")).toEqualString("c"); + try expect(try x.encode()).toEqualString(""); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=b&c=d"); + x.delete("a", null); + try expect(try x.encode()).toEqualString("c=d"); + x = try url.SearchParams.initFromString(alloc, "a=a&b=b&a=a&c=c"); + x.delete("a", null); + try expect(try x.encode()).toEqualString("b=b&c=c"); + x = try url.SearchParams.initFromString(alloc, "a=a&=&b=b&c=c"); + x.delete("", null); + try expect(try x.encode()).toEqualString("a=a&b=b&c=c"); + x = try url.SearchParams.initFromString(alloc, "a=a&null=null&b=b"); + x.delete("null", null); + try expect(try x.encode()).toEqualString("a=a&b=b"); + x = try url.SearchParams.initFromString(alloc, "a=a&undefined=undefined&b=b"); + x.delete("undefined", null); + try expect(try x.encode()).toEqualString("a=a&b=b"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("first", "1"); + try expect(x.has("first", null)).toEqual(true); + try expect(x.get("first")).toEqualSlice("1"); + x.delete("first", null); + try expect(x.has("first", null)).toEqual(false); + try x.append("first", "1"); + try x.append("first", "10"); + x.delete("first", null); + try expect(x.has("first", null)).toEqual(false); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("a", "b"); + try x.append("a", "c"); + try x.append("a", "d"); + x.delete("a", "c"); + try expect(try x.encode()).toEqualString("a=b&a=d"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("a", "b"); + try x.append("a", "c"); + try x.append("b", "c"); + try x.append("b", "d"); + x.delete("b", "c"); + x.delete("a", null); + try expect(try x.encode()).toEqualString("b=d"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=b&c=d"); + try expect(x.get("a")).toEqualString("b"); + try expect(x.get("c")).toEqualString("d"); + try expect(x.get("e")).toBeNull(); + x = try url.SearchParams.initFromString(alloc, "a=b&c=d&a=e"); + try expect(x.get("a")).toEqualString("b"); + x = try url.SearchParams.initFromString(alloc, "=b&c=d"); + try expect(x.get("")).toEqualString("b"); + x = try url.SearchParams.initFromString(alloc, "a=&c=d&a=e"); + try expect(x.get("a")).toEqualString(""); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "first=second&third&&"); + try expect(x.has("first", null)).toEqual(true); + try expect(x.get("first")).toEqualString("second"); + try expect(x.get("third")).toEqualString(""); + try expect(x.get("fourth")).toBeNull(); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=b&c=d"); + try expect(try x.getAll("a")).toEqualStringSlice(&.{"b"}); + try expect(try x.getAll("c")).toEqualStringSlice(&.{"d"}); + try expect(try x.getAll("e")).toEqualStringSlice(&.{}); + x = try url.SearchParams.initFromString(alloc, "a=b&c=d&a=e"); + try expect(try x.getAll("a")).toEqualStringSlice(&.{ "b", "e" }); + x = try url.SearchParams.initFromString(alloc, "=b&c=d"); + try expect(try x.getAll("")).toEqualStringSlice(&.{"b"}); + x = try url.SearchParams.initFromString(alloc, "a=&c=d&a=e"); + try expect(try x.getAll("a")).toEqualStringSlice(&.{ "", "e" }); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=1&a=2&a=3&a"); + try expect(x.has("a", null)).toEqual(true); + var matches = try x.getAll("a"); + try expect(matches).not().toBeNull(); + try expect(matches.?.len).toEqual(4); + try expect(matches).toEqualStringSlice(&.{ "1", "2", "3", "" }); + try x.set("a", "one"); + try expect(x.get("a")).toEqualString("one"); + try expect(try x.encode()).toEqualString("a=one"); + matches = try x.getAll("a"); + try expect(matches).not().toBeNull(); + try expect(matches.?.len).toEqual(1); + try expect(matches).toEqualStringSlice(&.{"one"}); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=b&c=d"); + try expect(x.has("a", null)).toEqual(true); + try expect(x.has("c", null)).toEqual(true); + try expect(x.has("e", null)).toEqual(false); + x = try url.SearchParams.initFromString(alloc, "a=b&c=d&a=e"); + try expect(x.has("a", null)).toEqual(true); + x = try url.SearchParams.initFromString(alloc, "=b&c=d"); + try expect(x.has("", null)).toEqual(true); + x = try url.SearchParams.initFromString(alloc, "null=a"); + try expect(x.has("null", null)).toEqual(true); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=b&c=d&&"); + try x.append("first", "1"); + try x.append("first", "2"); + try expect(x.has("a", null)).toEqual(true); + try expect(x.has("c", null)).toEqual(true); + try expect(x.has("first", null)).toEqual(true); + try expect(x.has("d", null)).toEqual(false); + x.delete("first", null); + try expect(x.has("first", null)).toEqual(false); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=b&a=d&c&e&"); + try expect(x.has("a", "b")).toEqual(true); + try expect(x.has("a", "c")).toEqual(false); + try expect(x.has("a", "d")).toEqual(true); + try expect(x.has("e", "")).toEqual(true); + try x.append("first", "null"); + try expect(x.has("first", "")).toEqual(false); + try expect(x.has("first", "null")).toEqual(true); + x.delete("a", "b"); + try expect(x.has("a", "d")).toEqual(true); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=b&a=d&c&e&"); + try expect(x.has("a", "b")).toEqual(true); + try expect(x.has("a", "c")).toEqual(false); + try expect(x.has("a", "d")).toEqual(true); + try expect(x.has("a", null)).toEqual(true); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=b&c=d"); + try x.set("a", "B"); + try expect(try x.encode()).toEqualString("a=B&c=d"); + x = try url.SearchParams.initFromString(alloc, "a=b&c=d&a=e"); + try x.set("a", "B"); + try expect(try x.encode()).toEqualString("a=B&c=d"); + try x.set("e", "f"); + try expect(try x.encode()).toEqualString("a=B&c=d&e=f"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=1&a=2&a=3"); + try expect(x.has("a", null)).toEqual(true); + try expect(x.get("a")).toEqualString("1"); + try x.set("first", "4"); + try expect(x.has("a", null)).toEqual(true); + try expect(x.get("a")).toEqualString("1"); + try x.set("a", "4"); + try expect(x.has("a", null)).toEqual(true); + try expect(x.get("a")).toEqualString("4"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=1&b=2&a=3"); + try expect(x.size()).toEqual(3); + x.delete("a", null); + try expect(x.size()).toEqual(1); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=1&b=2&a=3"); + try expect(x.size()).toEqual(3); + try x.append("b", "4"); + try expect(x.size()).toEqual(4); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=1&b=2&a=3"); + try expect(x.size()).toEqual(3); + x.delete("a", null); + try expect(x.size()).toEqual(1); + try x.append("b", "4"); + try expect(x.size()).toEqual(2); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("a", "b c"); + try expect(try x.encode()).toEqualString("a=b+c"); + x.delete("a", null); + try x.append("a b", "c"); + try expect(try x.encode()).toEqualString("a+b=c"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("a", ""); + try expect(try x.encode()).toEqualString("a="); + try x.append("a", ""); + try expect(try x.encode()).toEqualString("a=&a="); + try x.append("", "b"); + try expect(try x.encode()).toEqualString("a=&a=&=b"); + try x.append("", ""); + try expect(try x.encode()).toEqualString("a=&a=&=b&="); + try x.append("", ""); + try expect(try x.encode()).toEqualString("a=&a=&=b&=&="); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("", "b"); + try expect(try x.encode()).toEqualString("=b"); + try x.append("", "b"); + try expect(try x.encode()).toEqualString("=b&=b"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("", ""); + try expect(try x.encode()).toEqualString("="); + try x.append("", ""); + try expect(try x.encode()).toEqualString("=&="); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("a", "b+c"); + try expect(try x.encode()).toEqualString("a=b%2Bc"); + x.delete("a", null); + try x.append("a+b", "c"); + try expect(try x.encode()).toEqualString("a%2Bb=c"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("=", "a"); + try expect(try x.encode()).toEqualString("%3D=a"); + try x.append("b", "="); + try expect(try x.encode()).toEqualString("%3D=a&b=%3D"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("&", "a"); + try expect(try x.encode()).toEqualString("%26=a"); + try x.append("b", "&"); + try expect(try x.encode()).toEqualString("%26=a&b=%26"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("a", "*-._"); + try expect(try x.encode()).toEqualString("a=*-._"); + x.delete("a", null); + try x.append("*-._", "c"); + try expect(try x.encode()).toEqualString("*-._=c"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("a", "b%c"); + try expect(try x.encode()).toEqualString("a=b%25c"); + x.delete("a", null); + try x.append("a%b", "c"); + try expect(try x.encode()).toEqualString("a%25b=c"); + x = try url.SearchParams.initFromString(alloc, "id=0&value=%"); + try expect(try x.encode()).toEqualString("id=0&value=%25"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("a", "b\x00c"); + try expect(try x.encode()).toEqualString("a=b%00c"); + x.delete("a", null); + try x.append("a\x00b", "c"); + try expect(try x.encode()).toEqualString("a%00b=c"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + // try x.append("a", "b\uD83D\uDCA9c"); + try x.append("a", "b\xf0\x9f\x92\xa9c"); + try expect(try x.encode()).toEqualString("a=b%F0%9F%92%A9c"); + x.delete("a", null); + // try x.append("a\uD83D\uDCA9b", "c"); + try x.append("a\xf0\x9f\x92\xa9b", "c"); + try expect(try x.encode()).toEqualString("a%F0%9F%92%A9b=c"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + x = try url.SearchParams.initFromString(alloc, "a=b&c=d&&e&&"); + try expect(try x.encode()).toEqualString("a=b&c=d&e="); + x = try url.SearchParams.initFromString(alloc, "a = b &a=b&c=d%20"); + try expect(try x.encode()).toEqualString("a+=+b+&a=b&c=d+"); + // The lone "=" _does_ survive the roundtrip. + x = try url.SearchParams.initFromString(alloc, "a=&a=b"); + try expect(try x.encode()).toEqualString("a=&a=b"); + x = try url.SearchParams.initFromString(alloc, "b=%2sf%2a"); + try expect(try x.encode()).toEqualString("b=%252sf*"); + x = try url.SearchParams.initFromString(alloc, "b=%2%2af%2a"); + try expect(try x.encode()).toEqualString("b=%252*f*"); + x = try url.SearchParams.initFromString(alloc, "b=%%2a"); + try expect(try x.encode()).toEqualString("b=%25*"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = try url.SearchParams.initFromString(alloc, "a=b,c"); + try expect(try x.encode()).toEqualString("a=b%2Cc"); + try x.append("x", "y"); + try expect(try x.encode()).toEqualString("a=b%2Cc&x=y"); +} + +test { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var x = url.SearchParams.init(alloc); + try x.append("a\nb", "c\rd"); + try x.append("e\n\rf", "g\r\nh"); + try expect(try x.encode()).toEqualString("a%0Ab=c%0Dd&e%0A%0Df=g%0D%0Ah"); +} diff --git a/test.zig b/test.zig index c6c99f551c716ef2b57b34bb7293cefd298b4683..d8f3b4026161a9b87ddfa3aeae5d88e324e2aed2 100644 --- a/test.zig +++ b/test.zig @@ -4,3 +4,7 @@ const url = @import("url"); test { _ = @import("./zig-out/test.zig"); } + +test { + _ = @import("./test.SearchParams.zig"); +} diff --git a/url.zig b/url.zig index 13c6986794cfef7ef21c3588506efc59f36ac416..72f78d5940e8f14d6fa989c4c8a7396cb95ef30e 100644 --- a/url.zig +++ b/url.zig @@ -973,6 +973,222 @@ pub const URL = struct { } }; +pub const SearchParams = struct { + const string = []const u8; + const KV = struct { key: string, value: string }; + + allocator: std.mem.Allocator, + inner: std.MultiArrayList(KV), + + pub fn init(alloc: std.mem.Allocator) SearchParams { + return .{ + .allocator = alloc, + .inner = std.MultiArrayList(KV){}, + }; + } + + pub fn initFromString(alloc: std.mem.Allocator, input: string) !SearchParams { + var uv = SearchParams.init(alloc); + var iter = RawIterator.init(input, alloc); + while (try iter.next()) |piece| { + const k = piece[0]; + const v = piece[1]; + try uv.append(k, v); + } + return uv; + } + + const RawIterator = struct { + input: string, + iter: std.mem.SplitIterator(u8, .scalar), + alloc: std.mem.Allocator, + + fn init(input: string, alloc: std.mem.Allocator) RawIterator { + return .{ + .input = input, + .iter = std.mem.splitScalar(u8, input, '&'), + .alloc = alloc, + }; + } + + fn next(ri: *RawIterator) !?struct { string, string } { + while (ri.iter.next()) |piece| { + if (piece.len == 0) continue; + var jter = std.mem.splitScalar(u8, piece, '='); + const k = jter.next().?; + var v = jter.rest(); + std.mem.replaceScalar(u8, @constCast(v), '+', ' '); + v = std.Uri.percentDecodeInPlace(try ri.alloc.dupe(u8, v)); + return .{ k, v }; + } + return null; + } + }; + + pub fn set(self: *SearchParams, key: string, value: string) !void { + const keys = self.inner.items(.key); + const values = self.inner.items(.value); + var idx = extras.indexOfSlice(u8, keys, key) orelse { + return try self.append(key, value); + }; + values[idx] = value; + idx += 1; + while (true) { + if (idx >= self.inner.len) break; + if (!std.mem.eql(u8, keys[idx], key)) { + idx += 1; + continue; + } + self.inner.orderedRemove(idx); + } + } + + pub fn append(self: *SearchParams, key: string, value: string) !void { + try self.inner.append(self.allocator, .{ .key = key, .value = value }); + } + + pub fn get(self: *const SearchParams, key: string) ?string { + const keys = self.inner.items(.key); + const idx = extras.indexOfSlice(u8, keys, key) orelse return null; + const values = self.inner.items(.value); + return values[idx]; + } + + pub fn getAll(self: *const SearchParams, key: string) !?[]const string { + const keys = self.inner.items(.key); + const values = self.inner.items(.value); + var backer: [256]usize = undefined; + @memset(&backer, 0); + var bset = std.bit_set.DynamicBitSetUnmanaged{ .bit_length = self.inner.len, .masks = &backer }; + var prev: bool = false; + var phase_count: usize = 0; + for (keys, 0..) |k, i| { + const this = std.mem.eql(u8, k, key); + defer prev = this; + if (this != prev) phase_count += 1; + if (this == true) bset.set(i); + } + if (prev == true) phase_count += 1; + const count = bset.count(); + if (count == 0) return null; + if (count == 1) return values[bset.findFirstSet().?..][0..1]; + if (phase_count == 2) return values[bset.findFirstSet().? .. findLastSet(bset).? + 1]; + const items = try self.allocator.alloc(string, count); + var iter = bset.iterator(.{}); + var i: usize = 0; + while (iter.next()) |idx| : (i += 1) items[i] = values[idx]; + return items; + } + + pub fn take(self: *SearchParams, key: string) ?string { + const keys = self.inner.items(.key); + const idx = extras.indexOfSlice(u8, keys, key) orelse return null; + const values = self.inner.items(.value); + defer self.inner.orderedRemove(idx); + return values[idx]; + } + + /// if 'value' is null, will delete all the values with key 'key' + pub fn delete(self: *SearchParams, key: string, value: ?string) void { + var offset: usize = 0; + while (true) { + const keys = self.inner.items(.key); + const values = self.inner.items(.value); + var idx = extras.indexOfSlice(u8, keys[offset..], key) orelse break; + idx += offset; + if (idx >= self.inner.len) break; + if (!std.mem.eql(u8, keys[idx], key)) break; + if (value) |v| { + if (!std.mem.eql(u8, values[idx], v)) { + offset += 1; + continue; + } + } + self.inner.orderedRemove(idx); + } + } + + pub fn has(self: *const SearchParams, key: string, value: ?string) bool { + const keys = self.inner.items(.key); + if (value == null) { + return extras.indexOfSlice(u8, keys, key) != null; + } + const values = self.inner.items(.value); + for (keys, 0..) |k, i| { + if (std.mem.eql(u8, key, k)) { + if (std.mem.eql(u8, value.?, values[i])) { + return true; + } + } + } + return false; + } + + pub fn size(self: *const SearchParams) usize { + return self.inner.len; + } + + pub fn encode(self: *const SearchParams) !string { + const alloc = self.allocator; + var list = std.ArrayList(u8).init(alloc); + errdefer list.deinit(); + for (self.inner.items(.key), self.inner.items(.value), 0..) |k, v, i| { + if (i > 0) try list.writer().writeAll("&"); + try percentEncodeAL(&list, k, is_formurlencoded_percent_char); + try list.append('='); + try percentEncodeAL(&list, v, is_formurlencoded_percent_char); + } + list.items.len -= std.mem.replace(u8, list.items, "%20", "+", list.items) * 2; + return list.toOwnedSlice(); + } + + fn IteratorFor(comptime field: string) type { + return struct { + raw: RawIterator, + + const Self = @This(); + + pub fn init(alloc: std.mem.Allocator, input: string) Self { + return .{ + .raw = .{ + .input = input, + .iter = std.mem.splitScalar(u8, input, '&'), + .alloc = alloc, + }, + }; + } + + pub fn next(self: *Self) !?string { + while (try self.raw.next()) |piece| { + const k = piece[0]; + const v = piece[1]; + if (!std.mem.eql(u8, k, field)) continue; + return v; + } + return null; + } + }; + } + + /// Finds the index of the last set bit. + /// If no bits are set, returns null. + fn findLastSet(self: std.bit_set.DynamicBitSetUnmanaged) ?usize { + if (self.bit_length == 0) return null; + const bs = @bitSizeOf(usize); + var len = self.bit_length / bs; + if (self.bit_length % bs != 0) len += 1; + var offset: usize = len * bs; + var idx: usize = len - 1; + while (self.masks[idx] == 0) : (idx -= 1) { + offset -= bs; + if (idx == 0) return null; + } + offset -= @clz(self.masks[idx]); + offset -= 1; + return offset; + } +}; + /// https://url.spec.whatwg.org/#special-scheme fn isSchemeSpecial(scheme: []const u8) bool { if (std.mem.eql(u8, scheme, "ftp")) return true; @@ -1539,20 +1755,20 @@ pub fn is_userinfo_percent_char(c: u8) bool { if (c == '|') return true; return is_path_percent_char(c); } -// /// https://url.spec.whatwg.org/#component-percent-encode-set -// fn is_component_percent_char(c: u8) bool { -// if (c >= '$' and c <= '&') return true; -// if (c == '+') return true; -// if (c == ',') return true; -// return is_userinfo_percent_char(c); -// } -// /// https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set -// fn is_formurlencoded_percent_char(c: u8) bool { -// if (c == '!') return true; -// if (c >= '\'' and c <= ')') return true; -// if (c == '~') return true; -// return is_component_percent_char(c); -// } +/// https://url.spec.whatwg.org/#component-percent-encode-set +pub fn is_component_percent_char(c: u8) bool { + if (c >= '$' and c <= '&') return true; + if (c == '+') return true; + if (c == ',') return true; + return is_userinfo_percent_char(c); +} +/// https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set +pub fn is_formurlencoded_percent_char(c: u8) bool { + if (c == '!') return true; + if (c >= '\'' and c <= ')') return true; + if (c == '~') return true; + return is_component_percent_char(c); +} /// Asserts b is a valid UTF-8 codepoint fn l(b: u8) u3 { -- 2.54.0