Skip to content

Commit 9efc1a1

Browse files
authored
Merge pull request #752 from lightpanda-io/url_search_params
Rework/fix URLSearchParams
2 parents 234e7af + 13d602a commit 9efc1a1

File tree

4 files changed

+701
-291
lines changed

4 files changed

+701
-291
lines changed

src/browser/key_value.zig

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
2+
//
3+
// Francis Bouvier <[email protected]>
4+
// Pierre Tachoire <[email protected]>
5+
//
6+
// This program is free software: you can redistribute it and/or modify
7+
// it under the terms of the GNU Affero General Public License as
8+
// published by the Free Software Foundation, either version 3 of the
9+
// License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU Affero General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU Affero General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
const std = @import("std");
20+
21+
const Allocator = std.mem.Allocator;
22+
23+
// Used by FormDAta and URLSearchParams.
24+
//
25+
// We store the values in an ArrayList rather than a an
26+
// StringArrayHashMap([]const u8) because of the way the iterators (i.e., keys(),
27+
// values() and entries()) work. The FormData can contain duplicate keys, and
28+
// each iteration yields 1 key=>value pair. So, given:
29+
//
30+
// let f = new FormData();
31+
// f.append('a', '1');
32+
// f.append('a', '2');
33+
//
34+
// Then we'd expect f.keys(), f.values() and f.entries() to yield 2 results:
35+
// ['a', '1']
36+
// ['a', '2']
37+
//
38+
// This is much easier to do with an ArrayList than a HashMap, especially given
39+
// that the FormData could be mutated while iterating.
40+
// The downside is that most of the normal operations are O(N).
41+
pub const List = struct {
42+
entries: std.ArrayListUnmanaged(KeyValue) = .{},
43+
44+
pub fn init(entries: std.ArrayListUnmanaged(KeyValue)) List {
45+
return .{ .entries = entries };
46+
}
47+
48+
pub fn clone(self: *const List, arena: Allocator) !List {
49+
const entries = self.entries.items;
50+
51+
var c: std.ArrayListUnmanaged(KeyValue) = .{};
52+
try c.ensureTotalCapacity(arena, entries.len);
53+
for (entries) |kv| {
54+
c.appendAssumeCapacity(kv);
55+
}
56+
57+
return .{ .entries = c };
58+
}
59+
60+
pub fn fromOwnedSlice(entries: []KeyValue) List {
61+
return .{
62+
.entries = std.ArrayListUnmanaged(KeyValue).fromOwnedSlice(entries),
63+
};
64+
}
65+
66+
pub fn count(self: *const List) usize {
67+
return self.entries.items.len;
68+
}
69+
70+
pub fn get(self: *const List, key: []const u8) ?[]const u8 {
71+
const result = self.find(key) orelse return null;
72+
return result.entry.value;
73+
}
74+
75+
pub fn getAll(self: *const List, arena: Allocator, key: []const u8) ![]const []const u8 {
76+
var arr: std.ArrayListUnmanaged([]const u8) = .empty;
77+
for (self.entries.items) |entry| {
78+
if (std.mem.eql(u8, key, entry.key)) {
79+
try arr.append(arena, entry.value);
80+
}
81+
}
82+
return arr.items;
83+
}
84+
85+
pub fn has(self: *const List, key: []const u8) bool {
86+
return self.find(key) != null;
87+
}
88+
89+
pub fn set(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void {
90+
self.delete(key);
91+
return self.append(arena, key, value);
92+
}
93+
94+
pub fn append(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void {
95+
return self.appendOwned(arena, try arena.dupe(u8, key), try arena.dupe(u8, value));
96+
}
97+
98+
pub fn appendOwned(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void {
99+
return self.entries.append(arena, .{
100+
.key = key,
101+
.value = value,
102+
});
103+
}
104+
105+
pub fn delete(self: *List, key: []const u8) void {
106+
var i: usize = 0;
107+
while (i < self.entries.items.len) {
108+
const entry = self.entries.items[i];
109+
if (std.mem.eql(u8, key, entry.key)) {
110+
_ = self.entries.swapRemove(i);
111+
} else {
112+
i += 1;
113+
}
114+
}
115+
}
116+
117+
pub fn deleteKeyValue(self: *List, key: []const u8, value: []const u8) void {
118+
var i: usize = 0;
119+
while (i < self.entries.items.len) {
120+
const entry = self.entries.items[i];
121+
if (std.mem.eql(u8, key, entry.key) and std.mem.eql(u8, value, entry.value)) {
122+
_ = self.entries.swapRemove(i);
123+
} else {
124+
i += 1;
125+
}
126+
}
127+
}
128+
129+
pub fn keyIterator(self: *const List) KeyIterator {
130+
return .{ .entries = &self.entries };
131+
}
132+
133+
pub fn valueIterator(self: *const List) ValueIterator {
134+
return .{ .entries = &self.entries };
135+
}
136+
137+
pub fn entryIterator(self: *const List) EntryIterator {
138+
return .{ .entries = &self.entries };
139+
}
140+
141+
pub fn ensureTotalCapacity(self: *List, arena: Allocator, len: usize) !void {
142+
return self.entries.ensureTotalCapacity(arena, len);
143+
}
144+
145+
const FindResult = struct {
146+
index: usize,
147+
entry: KeyValue,
148+
};
149+
150+
fn find(self: *const List, key: []const u8) ?FindResult {
151+
for (self.entries.items, 0..) |entry, i| {
152+
if (std.mem.eql(u8, key, entry.key)) {
153+
return .{ .index = i, .entry = entry };
154+
}
155+
}
156+
return null;
157+
}
158+
};
159+
160+
pub const KeyValue = struct {
161+
key: []const u8,
162+
value: []const u8,
163+
};
164+
165+
pub const KeyIterator = struct {
166+
index: usize = 0,
167+
entries: *const std.ArrayListUnmanaged(KeyValue),
168+
169+
pub fn _next(self: *KeyIterator) ?[]const u8 {
170+
const entries = self.entries.items;
171+
172+
const index = self.index;
173+
if (index == entries.len) {
174+
return null;
175+
}
176+
self.index += 1;
177+
return entries[index].key;
178+
}
179+
};
180+
181+
pub const ValueIterator = struct {
182+
index: usize = 0,
183+
entries: *const std.ArrayListUnmanaged(KeyValue),
184+
185+
pub fn _next(self: *ValueIterator) ?[]const u8 {
186+
const entries = self.entries.items;
187+
188+
const index = self.index;
189+
if (index == entries.len) {
190+
return null;
191+
}
192+
self.index += 1;
193+
return entries[index].value;
194+
}
195+
};
196+
197+
pub const EntryIterator = struct {
198+
index: usize = 0,
199+
entries: *const std.ArrayListUnmanaged(KeyValue),
200+
201+
pub fn _next(self: *EntryIterator) ?struct { []const u8, []const u8 } {
202+
const entries = self.entries.items;
203+
204+
const index = self.index;
205+
if (index == entries.len) {
206+
return null;
207+
}
208+
self.index += 1;
209+
const entry = entries[index];
210+
return .{ entry.key, entry.value };
211+
}
212+
};
213+
214+
const URLEncodeMode = enum {
215+
form,
216+
query,
217+
};
218+
219+
pub fn urlEncode(list: List, mode: URLEncodeMode, writer: anytype) !void {
220+
const entries = list.entries.items;
221+
if (entries.len == 0) {
222+
return;
223+
}
224+
225+
try urlEncodeEntry(entries[0], mode, writer);
226+
for (entries[1..]) |entry| {
227+
try writer.writeByte('&');
228+
try urlEncodeEntry(entry, mode, writer);
229+
}
230+
}
231+
232+
fn urlEncodeEntry(entry: KeyValue, mode: URLEncodeMode, writer: anytype) !void {
233+
try urlEncodeValue(entry.key, mode, writer);
234+
235+
// for a form, for an empty value, we'll do "spice="
236+
// but for a query, we do "spice"
237+
if (mode == .query and entry.value.len == 0) {
238+
return;
239+
}
240+
241+
try writer.writeByte('=');
242+
try urlEncodeValue(entry.value, mode, writer);
243+
}
244+
245+
fn urlEncodeValue(value: []const u8, mode: URLEncodeMode, writer: anytype) !void {
246+
if (!urlEncodeShouldEscape(value, mode)) {
247+
return writer.writeAll(value);
248+
}
249+
250+
for (value) |b| {
251+
if (urlEncodeUnreserved(b, mode)) {
252+
try writer.writeByte(b);
253+
} else if (b == ' ' and mode == .form) {
254+
// for form submission, space should be encoded as '+', not '%20'
255+
try writer.writeByte('+');
256+
} else {
257+
try writer.print("%{X:0>2}", .{b});
258+
}
259+
}
260+
}
261+
262+
fn urlEncodeShouldEscape(value: []const u8, mode: URLEncodeMode) bool {
263+
for (value) |b| {
264+
if (!urlEncodeUnreserved(b, mode)) {
265+
return true;
266+
}
267+
}
268+
return false;
269+
}
270+
271+
fn urlEncodeUnreserved(b: u8, mode: URLEncodeMode) bool {
272+
return switch (b) {
273+
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_' => true,
274+
'~' => mode == .query,
275+
else => false,
276+
};
277+
}

0 commit comments

Comments
 (0)