mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-29 15:13:28 +00:00 
			
		
		
		
	Rework/fix URLSearchParams
Extracts the FormData logic, which is both more complete and more correct and reuses it between FormData and URLSearchParams. This includes the additional iterator behavior, `set` and URLSearchParams constructor from FormData.
This commit is contained in:
		
							
								
								
									
										277
									
								
								src/browser/key_value.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								src/browser/key_value.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,277 @@ | ||||
| // Copyright (C) 2023-2024  Lightpanda (Selecy SAS) | ||||
| // | ||||
| // Francis Bouvier <francis@lightpanda.io> | ||||
| // Pierre Tachoire <pierre@lightpanda.io> | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as | ||||
| // published by the Free Software Foundation, either version 3 of the | ||||
| // License, or (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| const std = @import("std"); | ||||
|  | ||||
| const Allocator = std.mem.Allocator; | ||||
|  | ||||
| // Used by FormDAta and URLSearchParams. | ||||
| // | ||||
| // We store the values in an ArrayList rather than a an | ||||
| // StringArrayHashMap([]const u8) because of the way the iterators (i.e., keys(), | ||||
| // values() and entries()) work. The FormData can contain duplicate keys, and | ||||
| // each iteration yields 1 key=>value pair. So, given: | ||||
| // | ||||
| //  let f = new FormData(); | ||||
| //  f.append('a', '1'); | ||||
| //  f.append('a', '2'); | ||||
| // | ||||
| // Then we'd expect f.keys(), f.values() and f.entries() to yield 2 results: | ||||
| //  ['a', '1'] | ||||
| //  ['a', '2'] | ||||
| // | ||||
| // This is much easier to do with an ArrayList than a HashMap, especially given | ||||
| // that the FormData could be mutated while iterating. | ||||
| // The downside is that most of the normal operations are O(N). | ||||
| pub const List = struct { | ||||
|     entries: std.ArrayListUnmanaged(KeyValue) = .{}, | ||||
|  | ||||
|     pub fn init(entries: std.ArrayListUnmanaged(KeyValue)) List { | ||||
|         return .{ .entries = entries }; | ||||
|     } | ||||
|  | ||||
|     pub fn clone(self: *const List, arena: Allocator) !List { | ||||
|         const entries = self.entries.items; | ||||
|  | ||||
|         var c: std.ArrayListUnmanaged(KeyValue) = .{}; | ||||
|         try c.ensureTotalCapacity(arena, entries.len); | ||||
|         for (entries) |kv| { | ||||
|             c.appendAssumeCapacity(kv); | ||||
|         } | ||||
|  | ||||
|         return .{ .entries = c }; | ||||
|     } | ||||
|  | ||||
|     pub fn fromOwnedSlice(entries: []KeyValue) List { | ||||
|         return .{ | ||||
|             .entries = std.ArrayListUnmanaged(KeyValue).fromOwnedSlice(entries), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     pub fn count(self: *const List) usize { | ||||
|         return self.entries.items.len; | ||||
|     } | ||||
|  | ||||
|     pub fn get(self: *const List, key: []const u8) ?[]const u8 { | ||||
|         const result = self.find(key) orelse return null; | ||||
|         return result.entry.value; | ||||
|     } | ||||
|  | ||||
|     pub fn getAll(self: *const List, arena: Allocator, key: []const u8) ![]const []const u8 { | ||||
|         var arr: std.ArrayListUnmanaged([]const u8) = .empty; | ||||
|         for (self.entries.items) |entry| { | ||||
|             if (std.mem.eql(u8, key, entry.key)) { | ||||
|                 try arr.append(arena, entry.value); | ||||
|             } | ||||
|         } | ||||
|         return arr.items; | ||||
|     } | ||||
|  | ||||
|     pub fn has(self: *const List, key: []const u8) bool { | ||||
|         return self.find(key) != null; | ||||
|     } | ||||
|  | ||||
|     pub fn set(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void { | ||||
|         self.delete(key); | ||||
|         return self.append(arena, key, value); | ||||
|     } | ||||
|  | ||||
|     pub fn append(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void { | ||||
|         return self.appendOwned(arena, try arena.dupe(u8, key), try arena.dupe(u8, value)); | ||||
|     } | ||||
|  | ||||
|     pub fn appendOwned(self: *List, arena: Allocator, key: []const u8, value: []const u8) !void { | ||||
|         return self.entries.append(arena, .{ | ||||
|             .key = key, | ||||
|             .value = value, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     pub fn delete(self: *List, key: []const u8) void { | ||||
|         var i: usize = 0; | ||||
|         while (i < self.entries.items.len) { | ||||
|             const entry = self.entries.items[i]; | ||||
|             if (std.mem.eql(u8, key, entry.key)) { | ||||
|                 _ = self.entries.swapRemove(i); | ||||
|             } else { | ||||
|                 i += 1; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn deleteKeyValue(self: *List, key: []const u8, value: []const u8) void { | ||||
|         var i: usize = 0; | ||||
|         while (i < self.entries.items.len) { | ||||
|             const entry = self.entries.items[i]; | ||||
|             if (std.mem.eql(u8, key, entry.key) and std.mem.eql(u8, value, entry.value)) { | ||||
|                 _ = self.entries.swapRemove(i); | ||||
|             } else { | ||||
|                 i += 1; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn keyIterator(self: *const List) KeyIterator { | ||||
|         return .{ .entries = &self.entries }; | ||||
|     } | ||||
|  | ||||
|     pub fn valueIterator(self: *const List) ValueIterator { | ||||
|         return .{ .entries = &self.entries }; | ||||
|     } | ||||
|  | ||||
|     pub fn entryIterator(self: *const List) EntryIterator { | ||||
|         return .{ .entries = &self.entries }; | ||||
|     } | ||||
|  | ||||
|     pub fn ensureTotalCapacity(self: *List, arena: Allocator, len: usize) !void { | ||||
|         return self.entries.ensureTotalCapacity(arena, len); | ||||
|     } | ||||
|  | ||||
|     const FindResult = struct { | ||||
|         index: usize, | ||||
|         entry: KeyValue, | ||||
|     }; | ||||
|  | ||||
|     fn find(self: *const List, key: []const u8) ?FindResult { | ||||
|         for (self.entries.items, 0..) |entry, i| { | ||||
|             if (std.mem.eql(u8, key, entry.key)) { | ||||
|                 return .{ .index = i, .entry = entry }; | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| pub const KeyValue = struct { | ||||
|     key: []const u8, | ||||
|     value: []const u8, | ||||
| }; | ||||
|  | ||||
| pub const KeyIterator = struct { | ||||
|     index: usize = 0, | ||||
|     entries: *const std.ArrayListUnmanaged(KeyValue), | ||||
|  | ||||
|     pub fn _next(self: *KeyIterator) ?[]const u8 { | ||||
|         const entries = self.entries.items; | ||||
|  | ||||
|         const index = self.index; | ||||
|         if (index == entries.len) { | ||||
|             return null; | ||||
|         } | ||||
|         self.index += 1; | ||||
|         return entries[index].key; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| pub const ValueIterator = struct { | ||||
|     index: usize = 0, | ||||
|     entries: *const std.ArrayListUnmanaged(KeyValue), | ||||
|  | ||||
|     pub fn _next(self: *ValueIterator) ?[]const u8 { | ||||
|         const entries = self.entries.items; | ||||
|  | ||||
|         const index = self.index; | ||||
|         if (index == entries.len) { | ||||
|             return null; | ||||
|         } | ||||
|         self.index += 1; | ||||
|         return entries[index].value; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| pub const EntryIterator = struct { | ||||
|     index: usize = 0, | ||||
|     entries: *const std.ArrayListUnmanaged(KeyValue), | ||||
|  | ||||
|     pub fn _next(self: *EntryIterator) ?struct { []const u8, []const u8 } { | ||||
|         const entries = self.entries.items; | ||||
|  | ||||
|         const index = self.index; | ||||
|         if (index == entries.len) { | ||||
|             return null; | ||||
|         } | ||||
|         self.index += 1; | ||||
|         const entry = entries[index]; | ||||
|         return .{ entry.key, entry.value }; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| const URLEncodeMode = enum { | ||||
|     form, | ||||
|     query, | ||||
| }; | ||||
|  | ||||
| pub fn urlEncode(list: List, mode: URLEncodeMode, writer: anytype) !void { | ||||
|     const entries = list.entries.items; | ||||
|     if (entries.len == 0) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     try urlEncodeEntry(entries[0], mode, writer); | ||||
|     for (entries[1..]) |entry| { | ||||
|         try writer.writeByte('&'); | ||||
|         try urlEncodeEntry(entry, mode, writer); | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn urlEncodeEntry(entry: KeyValue, mode: URLEncodeMode, writer: anytype) !void { | ||||
|     try urlEncodeValue(entry.key, mode, writer); | ||||
|  | ||||
|     // for a form, for an empty value, we'll do "spice=" | ||||
|     // but for a query, we do "spice" | ||||
|     if (mode == .query and entry.value.len == 0) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     try writer.writeByte('='); | ||||
|     try urlEncodeValue(entry.value, mode, writer); | ||||
| } | ||||
|  | ||||
| fn urlEncodeValue(value: []const u8, mode: URLEncodeMode, writer: anytype) !void { | ||||
|     if (!urlEncodeShouldEscape(value, mode)) { | ||||
|         return writer.writeAll(value); | ||||
|     } | ||||
|  | ||||
|     for (value) |b| { | ||||
|         if (urlEncodeUnreserved(b, mode)) { | ||||
|             try writer.writeByte(b); | ||||
|         } else if (b == ' ' and mode == .form) { | ||||
|             // for form submission, space should be encoded as '+', not '%20' | ||||
|             try writer.writeByte('+'); | ||||
|         } else { | ||||
|             try writer.print("%{X:0>2}", .{b}); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn urlEncodeShouldEscape(value: []const u8, mode: URLEncodeMode) bool { | ||||
|     for (value) |b| { | ||||
|         if (!urlEncodeUnreserved(b, mode)) { | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| fn urlEncodeUnreserved(b: u8, mode: URLEncodeMode) bool { | ||||
|     return switch (b) { | ||||
|         'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_' => true, | ||||
|         '~' => mode == .query, | ||||
|         else => false, | ||||
|     }; | ||||
| } | ||||
| @@ -18,56 +18,46 @@ | ||||
|  | ||||
| const std = @import("std"); | ||||
|  | ||||
| const Reader = @import("../../str/parser.zig").Reader; | ||||
| const asUint = @import("../../str/parser.zig").asUint; | ||||
| const Allocator = std.mem.Allocator; | ||||
|  | ||||
| // Values is a map with string key of string values. | ||||
| pub const Values = struct { | ||||
|     arena: std.heap.ArenaAllocator, | ||||
|     map: std.StringArrayHashMapUnmanaged(List), | ||||
|     map: std.StringArrayHashMapUnmanaged(List) = .{}, | ||||
|  | ||||
|     const List = std.ArrayListUnmanaged([]const u8); | ||||
|  | ||||
|     pub fn init(allocator: std.mem.Allocator) Values { | ||||
|         return .{ | ||||
|             .map = .{}, | ||||
|             .arena = std.heap.ArenaAllocator.init(allocator), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     pub fn deinit(self: *Values) void { | ||||
|         self.arena.deinit(); | ||||
|     } | ||||
|  | ||||
|     // 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 allocator = self.arena.allocator(); | ||||
|     pub fn append(self: *Values, arena: Allocator, k: []const u8, v: []const u8) !void { | ||||
|         const owned_value = try arena.dupe(u8, v); | ||||
|  | ||||
|         var gop = try self.map.getOrPut(arena, k); | ||||
|         errdefer _ = self.map.orderedRemove(k); | ||||
|  | ||||
|         if (gop.found_existing) { | ||||
|             return gop.value_ptr.append(arena, owned_value); | ||||
|         } | ||||
|  | ||||
|         gop.key_ptr.* = try arena.dupe(u8, k); | ||||
|  | ||||
|         var list = List{}; | ||||
|         try list.append(arena, owned_value); | ||||
|         gop.value_ptr.* = list; | ||||
|     } | ||||
|  | ||||
|     pub fn set(self: *Values, arena: Allocator, k: []const u8, v: []const u8) !void { | ||||
|         const owned_value = try allocator.dupe(u8, v); | ||||
|  | ||||
|         var gop = try self.map.getOrPut(allocator, k); | ||||
|         errdefer _ = self.map.remove(k); | ||||
|  | ||||
|         if (gop.found_existing) { | ||||
|             return gop.value_ptr.append(allocator, owned_value); | ||||
|             gop.value_ptr.clearRetainingCapacity(); | ||||
|         } else { | ||||
|             gop._key_ptr.* = try arena.dupe(u8, k); | ||||
|             gop.value_ptr.* = .empty; | ||||
|         } | ||||
|  | ||||
|         gop.key_ptr.* = try allocator.dupe(u8, k); | ||||
|  | ||||
|         var list = List{}; | ||||
|         try list.append(allocator, owned_value); | ||||
|         gop.value_ptr.* = list; | ||||
|     } | ||||
|  | ||||
|     // append by taking the ownership of the key and the value | ||||
|     fn appendOwned(self: *Values, k: []const u8, v: []const u8) !void { | ||||
|         const allocator = self.arena.allocator(); | ||||
|         var gop = try self.map.getOrPut(allocator, k); | ||||
|         if (gop.found_existing) { | ||||
|             return gop.value_ptr.append(allocator, v); | ||||
|         } | ||||
|  | ||||
|         var list = List{}; | ||||
|         try list.append(allocator, v); | ||||
|         gop.value_ptr.* = list; | ||||
|         try gop.value_ptr.append(arena, owned_value); | ||||
|     } | ||||
|  | ||||
|     pub fn get(self: *const Values, k: []const u8) []const []const u8 { | ||||
| @@ -80,13 +70,16 @@ pub const Values = struct { | ||||
|  | ||||
|     pub fn first(self: *const Values, k: []const u8) []const u8 { | ||||
|         if (self.map.getPtr(k)) |list| { | ||||
|             if (list.items.len == 0) return ""; | ||||
|             std.debug.assert(liste.items.len > 0); | ||||
|             return list.items[0]; | ||||
|         } | ||||
|  | ||||
|         return ""; | ||||
|     } | ||||
|  | ||||
|     pub fn has(self: *const Values, k: []const u8) bool { | ||||
|         return self.map.contains(k); | ||||
|     } | ||||
|  | ||||
|     pub fn delete(self: *Values, k: []const u8) void { | ||||
|         _ = self.map.fetchSwapRemove(k); | ||||
|     } | ||||
| @@ -97,6 +90,9 @@ pub const Values = struct { | ||||
|         for (list.items, 0..) |vv, i| { | ||||
|             if (std.mem.eql(u8, v, vv)) { | ||||
|                 _ = list.swapRemove(i); | ||||
|                 if (i == 0) { | ||||
|                     _ = self.map.orderedRemove(k); | ||||
|                 } | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
| @@ -252,27 +248,27 @@ fn unescape(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { | ||||
|                 const encoded = input[input_pos + 1 .. input_pos + 3]; | ||||
|                 const encoded_as_uint = @as(u16, @bitCast(encoded[0..2].*)); | ||||
|                 unescaped[unescaped_pos] = switch (encoded_as_uint) { | ||||
|                     asUint("20") => ' ', | ||||
|                     asUint("21") => '!', | ||||
|                     asUint("22") => '"', | ||||
|                     asUint("23") => '#', | ||||
|                     asUint("24") => '$', | ||||
|                     asUint("25") => '%', | ||||
|                     asUint("26") => '&', | ||||
|                     asUint("27") => '\'', | ||||
|                     asUint("28") => '(', | ||||
|                     asUint("29") => ')', | ||||
|                     asUint("2A") => '*', | ||||
|                     asUint("2B") => '+', | ||||
|                     asUint("2C") => ',', | ||||
|                     asUint("2F") => '/', | ||||
|                     asUint("3A") => ':', | ||||
|                     asUint("3B") => ';', | ||||
|                     asUint("3D") => '=', | ||||
|                     asUint("3F") => '?', | ||||
|                     asUint("40") => '@', | ||||
|                     asUint("5B") => '[', | ||||
|                     asUint("5D") => ']', | ||||
|                     asUint(u16, "20") => ' ', | ||||
|                     asUint(u16, "21") => '!', | ||||
|                     asUint(u16, "22") => '"', | ||||
|                     asUint(u16, "23") => '#', | ||||
|                     asUint(u16, "24") => '$', | ||||
|                     asUint(u16, "25") => '%', | ||||
|                     asUint(u16, "26") => '&', | ||||
|                     asUint(u16, "27") => '\'', | ||||
|                     asUint(u16, "28") => '(', | ||||
|                     asUint(u16, "29") => ')', | ||||
|                     asUint(u16, "2A") => '*', | ||||
|                     asUint(u16, "2B") => '+', | ||||
|                     asUint(u16, "2C") => ',', | ||||
|                     asUint(u16, "2F") => '/', | ||||
|                     asUint(u16, "3A") => ':', | ||||
|                     asUint(u16, "3B") => ';', | ||||
|                     asUint(u16, "3D") => '=', | ||||
|                     asUint(u16, "3F") => '?', | ||||
|                     asUint(u16, "40") => '@', | ||||
|                     asUint(u16, "5B") => '[', | ||||
|                     asUint(u16, "5D") => ']', | ||||
|                     else => HEX_DECODE[encoded[0]] << 4 | HEX_DECODE[encoded[1]], | ||||
|                 }; | ||||
|                 input_pos += 3; | ||||
| @@ -286,7 +282,11 @@ fn unescape(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { | ||||
|     return unescaped; | ||||
| } | ||||
|  | ||||
| const testing = std.testing; | ||||
| pub fn asUint(comptime T: type, comptime string: []const u8) T { | ||||
|     return @bitCast(string[0..string.len].*); | ||||
| } | ||||
|  | ||||
| const testing = @import("../../testing.zig"); | ||||
| test "url.Query: unescape" { | ||||
|     const allocator = testing.allocator; | ||||
|     const cases = [_]struct { expected: []const u8, input: []const u8, free: bool }{ | ||||
| @@ -304,7 +304,7 @@ test "url.Query: unescape" { | ||||
|         defer if (case.free) { | ||||
|             allocator.free(value); | ||||
|         }; | ||||
|         try testing.expectEqualStrings(case.expected, value); | ||||
|         try testing.expectEqual(case.expected, value); | ||||
|     } | ||||
|  | ||||
|     try testing.expectError(error.EscapeError, unescape(undefined, "%")); | ||||
| @@ -346,21 +346,22 @@ test "url.Query: parseQuery" { | ||||
| } | ||||
|  | ||||
| test "url.Query.Values: get/first/count" { | ||||
|     var values = Values.init(testing.allocator); | ||||
|     defer values.deinit(); | ||||
|     defer testing.reset(); | ||||
|     const arena = testing.arena_allocator; | ||||
|  | ||||
|     var values = Values{}; | ||||
|     { | ||||
|         // empty | ||||
|         try testing.expectEqual(0, values.count()); | ||||
|         try testing.expectEqual(0, values.get("").len); | ||||
|         try testing.expectEqualStrings("", values.first("")); | ||||
|         try testing.expectEqual("", values.first("")); | ||||
|         try testing.expectEqual(0, values.get("key").len); | ||||
|         try testing.expectEqualStrings("", values.first("key")); | ||||
|         try testing.expectEqual("", values.first("key")); | ||||
|     } | ||||
|  | ||||
|     { | ||||
|         // add 1 value => key | ||||
|         try values.appendOwned("key", "value"); | ||||
|         try values.append(arena, "key", "value"); | ||||
|         try testing.expectEqual(1, values.count()); | ||||
|         try testing.expectEqual(1, values.get("key").len); | ||||
|         try testing.expectEqualSlices( | ||||
| @@ -368,12 +369,12 @@ test "url.Query.Values: get/first/count" { | ||||
|             &.{"value"}, | ||||
|             values.get("key"), | ||||
|         ); | ||||
|         try testing.expectEqualStrings("value", values.first("key")); | ||||
|         try testing.expectEqual("value", values.first("key")); | ||||
|     } | ||||
|  | ||||
|     { | ||||
|         // add another value for the same key | ||||
|         try values.appendOwned("key", "another"); | ||||
|         try values.append(arena, "key", "another"); | ||||
|         try testing.expectEqual(1, values.count()); | ||||
|         try testing.expectEqual(2, values.get("key").len); | ||||
|         try testing.expectEqualSlices( | ||||
| @@ -381,12 +382,12 @@ test "url.Query.Values: get/first/count" { | ||||
|             &.{ "value", "another" }, | ||||
|             values.get("key"), | ||||
|         ); | ||||
|         try testing.expectEqualStrings("value", values.first("key")); | ||||
|         try testing.expectEqual("value", values.first("key")); | ||||
|     } | ||||
|  | ||||
|     { | ||||
|         // add a new key (and value) | ||||
|         try values.appendOwned("over", "9000!"); | ||||
|         try values.append(arena, "over", "9000!"); | ||||
|         try testing.expectEqual(2, values.count()); | ||||
|         try testing.expectEqual(2, values.get("key").len); | ||||
|         try testing.expectEqual(1, values.get("over").len); | ||||
| @@ -395,7 +396,20 @@ test "url.Query.Values: get/first/count" { | ||||
|             &.{"9000!"}, | ||||
|             values.get("over"), | ||||
|         ); | ||||
|         try testing.expectEqualStrings("9000!", values.first("over")); | ||||
|         try testing.expectEqual("9000!", values.first("over")); | ||||
|     } | ||||
|  | ||||
|     { | ||||
|         // set (override) | ||||
|         try values.append(arena, "key", "9000!"); | ||||
|         try testing.expectEqual(1, values.count()); | ||||
|         try testing.expectEqual(1, values.get("key").len); | ||||
|         try testing.expectEqualSlices( | ||||
|             []const u8, | ||||
|             &.{"9000!"}, | ||||
|             values.get("key"), | ||||
|         ); | ||||
|         try testing.expectEqual("9000!", values.first("key")); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -409,7 +423,7 @@ test "url.Query.Values: encode" { | ||||
|     var buf: std.ArrayListUnmanaged(u8) = .{}; | ||||
|     defer buf.deinit(testing.allocator); | ||||
|     try values.encode(buf.writer(testing.allocator)); | ||||
|     try testing.expectEqualStrings( | ||||
|     try testing.expectEqual( | ||||
|         "hello=world&i%20will%20not%20fear=%3E%3E&a=b&a=c", | ||||
|         buf.items, | ||||
|     ); | ||||
| @@ -425,7 +439,7 @@ fn testParseQuery(expected: anytype, query: []const u8) !void { | ||||
|         const expect = @field(expected, f.name); | ||||
|         try testing.expectEqual(expect.len, actual.len); | ||||
|         for (expect, actual) |e, a| { | ||||
|             try testing.expectEqualStrings(e, a); | ||||
|             try testing.expectEqual(e, a); | ||||
|         } | ||||
|         count += 1; | ||||
|     } | ||||
|   | ||||
| @@ -17,13 +17,20 @@ | ||||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| const std = @import("std"); | ||||
| const Page = @import("../page.zig").Page; | ||||
| const Allocator = std.mem.Allocator; | ||||
|  | ||||
| const query = @import("query.zig"); | ||||
| const Page = @import("../page.zig").Page; | ||||
| const FormData = @import("../xhr/form_data.zig").FormData; | ||||
|  | ||||
| const kv = @import("../key_value.zig"); | ||||
| const iterator = @import("../iterator/iterator.zig"); | ||||
|  | ||||
| pub const Interfaces = .{ | ||||
|     URL, | ||||
|     URLSearchParams, | ||||
|     KeyIterable, | ||||
|     ValueIterable, | ||||
|     EntryIterable, | ||||
| }; | ||||
|  | ||||
| // https://url.spec.whatwg.org/#url | ||||
| @@ -61,7 +68,7 @@ pub const URL = struct { | ||||
|         return init(arena, uri); | ||||
|     } | ||||
|  | ||||
|     pub fn init(arena: std.mem.Allocator, uri: std.Uri) !URL { | ||||
|     pub fn init(arena: Allocator, uri: std.Uri) !URL { | ||||
|         return .{ | ||||
|             .uri = uri, | ||||
|             .search_params = try URLSearchParams.init( | ||||
| @@ -93,15 +100,15 @@ pub const URL = struct { | ||||
|         const cur = self.uri.query; | ||||
|         defer self.uri.query = cur; | ||||
|         var q = std.ArrayList(u8).init(arena); | ||||
|         try self.search_params.values.encode(q.writer()); | ||||
|         try self.search_params.encode(q.writer()); | ||||
|         self.uri.query = .{ .percent_encoded = q.items }; | ||||
|  | ||||
|         return try self.toString(arena); | ||||
|     } | ||||
|  | ||||
|     // format the url with all its components. | ||||
|     pub fn toString(self: *URL, arena: std.mem.Allocator) ![]const u8 { | ||||
|         var buf = std.ArrayList(u8).init(arena); | ||||
|     pub fn toString(self: *URL, arena: Allocator) ![]const u8 { | ||||
|         var buf: std.ArrayListUnmanaged(u8) = .empty; | ||||
|  | ||||
|         try self.uri.writeToStream(.{ | ||||
|             .scheme = true, | ||||
| @@ -110,7 +117,8 @@ pub const URL = struct { | ||||
|             .path = uriComponentNullStr(self.uri.path).len > 0, | ||||
|             .query = uriComponentNullStr(self.uri.query).len > 0, | ||||
|             .fragment = uriComponentNullStr(self.uri.fragment).len > 0, | ||||
|         }, buf.writer()); | ||||
|         }, buf.writer(arena)); | ||||
|  | ||||
|         return buf.items; | ||||
|     } | ||||
|  | ||||
| @@ -165,7 +173,7 @@ pub const URL = struct { | ||||
|         var buf: std.ArrayListUnmanaged(u8) = .{}; | ||||
|  | ||||
|         try buf.append(arena, '?'); | ||||
|         try self.search_params.values.encode(buf.writer(arena)); | ||||
|         try self.search_params.encode(buf.writer(arena)); | ||||
|         return buf.items; | ||||
|     } | ||||
|  | ||||
| @@ -201,47 +209,227 @@ fn uriComponentStr(c: std.Uri.Component) []const u8 { | ||||
| } | ||||
|  | ||||
| // https://url.spec.whatwg.org/#interface-urlsearchparams | ||||
| // TODO array like | ||||
| pub const URLSearchParams = struct { | ||||
|     values: query.Values, | ||||
|     entries: kv.List, | ||||
|  | ||||
|     pub fn constructor(qs: ?[]const u8, page: *Page) !URLSearchParams { | ||||
|         return init(page.arena, qs); | ||||
|     } | ||||
|  | ||||
|     pub fn init(arena: std.mem.Allocator, qs: ?[]const u8) !URLSearchParams { | ||||
|         return .{ | ||||
|             .values = try query.parseQuery(arena, qs orelse ""), | ||||
|     const URLSearchParamsOpts = union(enum) { | ||||
|         qs: []const u8, | ||||
|         form_data: *const FormData, | ||||
|     }; | ||||
|     pub fn constructor(opts_: ?URLSearchParamsOpts, page: *Page) !URLSearchParams { | ||||
|         const opts = opts_ orelse return .{ .entries = .{} }; | ||||
|         return switch (opts) { | ||||
|             .qs => |qs| init(page.arena, qs), | ||||
|             .form_data => |fd| .{ .entries = try fd.entries.clone(page.arena) }, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     pub fn get_size(self: *URLSearchParams) u32 { | ||||
|         return @intCast(self.values.count()); | ||||
|     pub fn init(arena: Allocator, qs_: ?[]const u8) !URLSearchParams { | ||||
|         return .{ | ||||
|             .entries = if (qs_) |qs| try parseQuery(arena, qs) else .{}, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     pub fn _append(self: *URLSearchParams, name: []const u8, value: []const u8) !void { | ||||
|         try self.values.append(name, value); | ||||
|     pub fn get_size(self: *const URLSearchParams) u32 { | ||||
|         return @intCast(self.entries.count()); | ||||
|     } | ||||
|  | ||||
|     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 _append(self: *URLSearchParams, name: []const u8, value: []const u8, page: *Page) !void { | ||||
|         return self.entries.append(page.arena, name, value); | ||||
|     } | ||||
|  | ||||
|     pub fn _get(self: *URLSearchParams, name: []const u8) ?[]const u8 { | ||||
|         return self.values.first(name); | ||||
|     pub fn _set(self: *URLSearchParams, name: []const u8, value: []const u8, page: *Page) !void { | ||||
|         return self.entries.set(page.arena, name, value); | ||||
|     } | ||||
|  | ||||
|     // TODO return generates an error: caught unexpected error 'TypeLookup' | ||||
|     // pub fn _getAll(self: *URLSearchParams, name: []const u8) [][]const u8 { | ||||
|     //     try self.values.get(name); | ||||
|     // } | ||||
|     pub fn _delete(self: *URLSearchParams, name: []const u8, value_: ?[]const u8) void { | ||||
|         if (value_) |value| { | ||||
|             return self.entries.deleteKeyValue(name, value); | ||||
|         } | ||||
|         return self.entries.delete(name); | ||||
|     } | ||||
|  | ||||
|     pub fn _get(self: *const URLSearchParams, name: []const u8) ?[]const u8 { | ||||
|         return self.entries.get(name); | ||||
|     } | ||||
|  | ||||
|     pub fn _getAll(self: *const URLSearchParams, name: []const u8, page: *Page) ![]const []const u8 { | ||||
|         return self.entries.getAll(page.call_arena, name); | ||||
|     } | ||||
|  | ||||
|     pub fn _has(self: *const URLSearchParams, name: []const u8) bool { | ||||
|         return self.entries.has(name); | ||||
|     } | ||||
|  | ||||
|     pub fn _keys(self: *const URLSearchParams) KeyIterable { | ||||
|         return .{ .inner = self.entries.keyIterator() }; | ||||
|     } | ||||
|  | ||||
|     pub fn _values(self: *const URLSearchParams) ValueIterable { | ||||
|         return .{ .inner = self.entries.valueIterator() }; | ||||
|     } | ||||
|  | ||||
|     pub fn _entries(self: *const URLSearchParams) EntryIterable { | ||||
|         return .{ .inner = self.entries.entryIterator() }; | ||||
|     } | ||||
|  | ||||
|     pub fn _symbol_iterator(self: *const URLSearchParams) EntryIterable { | ||||
|         return self._entries(); | ||||
|     } | ||||
|  | ||||
|     fn _toString(self: *const URLSearchParams, page: *Page) ![]const u8 { | ||||
|         var arr: std.ArrayListUnmanaged(u8) = .empty; | ||||
|         try kv.urlEncode(self.entries, .query, arr.writer(page.call_arena)); | ||||
|         return arr.items; | ||||
|     } | ||||
|  | ||||
|     // TODO | ||||
|     pub fn _sort(_: *URLSearchParams) void {} | ||||
|  | ||||
|     fn encode(self: *const URLSearchParams, writer: anytype) !void { | ||||
|         return kv.urlEncode(self.entries, .query, writer); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| // Parse the given query. | ||||
| fn parseQuery(arena: Allocator, s: []const u8) !kv.List { | ||||
|     var list = kv.List{}; | ||||
|  | ||||
|     const ln = s.len; | ||||
|     if (ln == 0) { | ||||
|         return list; | ||||
|     } | ||||
|  | ||||
|     var query = if (s[0] == '?') s[1..] else s; | ||||
|     while (query.len > 0) { | ||||
|         const i = std.mem.indexOfScalarPos(u8, query, 0, '=') orelse query.len; | ||||
|         const name = query[0..i]; | ||||
|  | ||||
|         var value: ?[]const u8 = null; | ||||
|         if (i < query.len) { | ||||
|             query = query[i + 1 ..]; | ||||
|             const j = std.mem.indexOfScalarPos(u8, query, 0, '&') orelse query.len; | ||||
|             value = query[0..j]; | ||||
|  | ||||
|             query = if (j < query.len) query[j + 1 ..] else ""; | ||||
|         } else { | ||||
|             query = ""; | ||||
|         } | ||||
|  | ||||
|         try list.appendOwned( | ||||
|             arena, | ||||
|             try unescape(arena, name), | ||||
|             if (value) |v| try unescape(arena, v) else "", | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     return list; | ||||
| } | ||||
|  | ||||
| fn unescape(arena: Allocator, input: []const u8) ![]const u8 { | ||||
|     const HEX_CHAR = comptime blk: { | ||||
|         var all = std.mem.zeroes([256]bool); | ||||
|         for ('a'..('f' + 1)) |b| all[b] = true; | ||||
|         for ('A'..('F' + 1)) |b| all[b] = true; | ||||
|         for ('0'..('9' + 1)) |b| all[b] = true; | ||||
|         break :blk all; | ||||
|     }; | ||||
|  | ||||
|     const HEX_DECODE = comptime blk: { | ||||
|         var all = std.mem.zeroes([256]u8); | ||||
|         for ('a'..('z' + 1)) |b| all[b] = b - 'a' + 10; | ||||
|         for ('A'..('Z' + 1)) |b| all[b] = b - 'A' + 10; | ||||
|         for ('0'..('9' + 1)) |b| all[b] = b - '0'; | ||||
|         break :blk all; | ||||
|     }; | ||||
|  | ||||
|     var has_plus = false; | ||||
|     var unescaped_len = input.len; | ||||
|  | ||||
|     { | ||||
|         // Figure out if we have any spaces and what the final unescaped length | ||||
|         // will be (which will let us know if we have anything to unescape in | ||||
|         // the first place) | ||||
|         var i: usize = 0; | ||||
|         while (i < input.len) { | ||||
|             const c = input[i]; | ||||
|             if (c == '%') { | ||||
|                 if (i + 2 >= input.len or !HEX_CHAR[input[i + 1]] or !HEX_CHAR[input[i + 2]]) { | ||||
|                     return error.EscapeError; | ||||
|                 } | ||||
|                 i += 3; | ||||
|                 unescaped_len -= 2; | ||||
|             } else if (c == '+') { | ||||
|                 has_plus = true; | ||||
|                 i += 1; | ||||
|             } else { | ||||
|                 i += 1; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // no encoding, and no plus. nothing to unescape | ||||
|     if (unescaped_len == input.len and has_plus == false) { | ||||
|         // we always dupe, because we know our caller wants it always duped. | ||||
|         return arena.dupe(u8, input); | ||||
|     } | ||||
|  | ||||
|     var unescaped = try arena.alloc(u8, unescaped_len); | ||||
|     errdefer arena.free(unescaped); | ||||
|  | ||||
|     var input_pos: usize = 0; | ||||
|     for (0..unescaped_len) |unescaped_pos| { | ||||
|         switch (input[input_pos]) { | ||||
|             '+' => { | ||||
|                 unescaped[unescaped_pos] = ' '; | ||||
|                 input_pos += 1; | ||||
|             }, | ||||
|             '%' => { | ||||
|                 const encoded = input[input_pos + 1 .. input_pos + 3]; | ||||
|                 const encoded_as_uint = @as(u16, @bitCast(encoded[0..2].*)); | ||||
|                 unescaped[unescaped_pos] = switch (encoded_as_uint) { | ||||
|                     asUint(u16, "20") => ' ', | ||||
|                     asUint(u16, "21") => '!', | ||||
|                     asUint(u16, "22") => '"', | ||||
|                     asUint(u16, "23") => '#', | ||||
|                     asUint(u16, "24") => '$', | ||||
|                     asUint(u16, "25") => '%', | ||||
|                     asUint(u16, "26") => '&', | ||||
|                     asUint(u16, "27") => '\'', | ||||
|                     asUint(u16, "28") => '(', | ||||
|                     asUint(u16, "29") => ')', | ||||
|                     asUint(u16, "2A") => '*', | ||||
|                     asUint(u16, "2B") => '+', | ||||
|                     asUint(u16, "2C") => ',', | ||||
|                     asUint(u16, "2F") => '/', | ||||
|                     asUint(u16, "3A") => ':', | ||||
|                     asUint(u16, "3B") => ';', | ||||
|                     asUint(u16, "3D") => '=', | ||||
|                     asUint(u16, "3F") => '?', | ||||
|                     asUint(u16, "40") => '@', | ||||
|                     asUint(u16, "5B") => '[', | ||||
|                     asUint(u16, "5D") => ']', | ||||
|                     else => HEX_DECODE[encoded[0]] << 4 | HEX_DECODE[encoded[1]], | ||||
|                 }; | ||||
|                 input_pos += 3; | ||||
|             }, | ||||
|             else => |c| { | ||||
|                 unescaped[unescaped_pos] = c; | ||||
|                 input_pos += 1; | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
|     return unescaped; | ||||
| } | ||||
|  | ||||
| fn asUint(comptime T: type, comptime string: []const u8) T { | ||||
|     return @bitCast(string[0..string.len].*); | ||||
| } | ||||
|  | ||||
| const KeyIterable = iterator.Iterable(kv.KeyIterator, "URLSearchParamsKeyIterator"); | ||||
| const ValueIterable = iterator.Iterable(kv.ValueIterator, "URLSearchParamsValueIterator"); | ||||
| const EntryIterable = iterator.Iterable(kv.EntryIterator, "URLSearchParamsEntryIterator"); | ||||
|  | ||||
| const testing = @import("../../testing.zig"); | ||||
| test "Browser.URL" { | ||||
|     var runner = try testing.jsRunner(testing.tracking_allocator, .{}); | ||||
| @@ -269,17 +457,19 @@ test "Browser.URL" { | ||||
|         .{ "url.searchParams.get('b')", "~" }, | ||||
|         .{ "url.searchParams.append('c', 'foo')", "undefined" }, | ||||
|         .{ "url.searchParams.get('c')", "foo" }, | ||||
|         .{ "url.searchParams.getAll('c').length", "1" }, | ||||
|         .{ "url.searchParams.getAll('c')[0]", "foo" }, | ||||
|         .{ "url.searchParams.size", "3" }, | ||||
|  | ||||
|         // search is dynamic | ||||
|         .{ "url.search", "?a=%7E&b=%7E&c=foo" }, | ||||
|         .{ "url.search", "?a=~&b=~&c=foo" }, | ||||
|         // href is dynamic | ||||
|         .{ "url.href", "https://foo.bar/path?a=%7E&b=%7E&c=foo#fragment" }, | ||||
|         .{ "url.href", "https://foo.bar/path?a=~&b=~&c=foo#fragment" }, | ||||
|  | ||||
|         .{ "url.searchParams.delete('c', 'foo')", "undefined" }, | ||||
|         .{ "url.searchParams.get('c')", "" }, | ||||
|         .{ "url.searchParams.get('c')", "null" }, | ||||
|         .{ "url.searchParams.delete('a')", "undefined" }, | ||||
|         .{ "url.searchParams.get('a')", "" }, | ||||
|         .{ "url.searchParams.get('a')", "null" }, | ||||
|     }, .{}); | ||||
|  | ||||
|     try runner.testCases(&.{ | ||||
| @@ -287,3 +477,84 @@ test "Browser.URL" { | ||||
|         .{ "url.href", "https://lightpanda.io/over?9000" }, | ||||
|     }, .{}); | ||||
| } | ||||
|  | ||||
| test "Browser.URLSearchParams" { | ||||
|     var runner = try testing.jsRunner(testing.tracking_allocator, .{}); | ||||
|     defer runner.deinit(); | ||||
|     try runner.testCases(&.{ | ||||
|         .{ "let usp = new URLSearchParams()", null }, | ||||
|         .{ "usp.get('a')", "null" }, | ||||
|         .{ "usp.has('a')", "false" }, | ||||
|         .{ "usp.getAll('a')", "" }, | ||||
|         .{ "usp.delete('a')", "undefined" }, | ||||
|  | ||||
|         .{ "usp.set('a', 1)", "undefined" }, | ||||
|         .{ "usp.has('a')", "true" }, | ||||
|         .{ "usp.get('a')", "1" }, | ||||
|         .{ "usp.getAll('a')", "1" }, | ||||
|  | ||||
|         .{ "usp.append('a', 2)", "undefined" }, | ||||
|         .{ "usp.has('a')", "true" }, | ||||
|         .{ "usp.get('a')", "1" }, | ||||
|         .{ "usp.getAll('a')", "1,2" }, | ||||
|  | ||||
|         .{ "usp.append('b', '3')", "undefined" }, | ||||
|         .{ "usp.has('a')", "true" }, | ||||
|         .{ "usp.get('a')", "1" }, | ||||
|         .{ "usp.getAll('a')", "1,2" }, | ||||
|         .{ "usp.has('b')", "true" }, | ||||
|         .{ "usp.get('b')", "3" }, | ||||
|         .{ "usp.getAll('b')", "3" }, | ||||
|  | ||||
|         .{ "let acc = [];", null }, | ||||
|         .{ "for (const key of usp.keys()) { acc.push(key) }; acc;", "a,a,b" }, | ||||
|  | ||||
|         .{ "acc = [];", null }, | ||||
|         .{ "for (const value of usp.values()) { acc.push(value) }; acc;", "1,2,3" }, | ||||
|  | ||||
|         .{ "acc = [];", null }, | ||||
|         .{ "for (const entry of usp.entries()) { acc.push(entry) }; acc;", "a,1,a,2,b,3" }, | ||||
|  | ||||
|         .{ "acc = [];", null }, | ||||
|         .{ "for (const entry of usp) { acc.push(entry) }; acc;", "a,1,a,2,b,3" }, | ||||
|  | ||||
|         .{ "usp.delete('a')", "undefined" }, | ||||
|         .{ "usp.has('a')", "false" }, | ||||
|         .{ "usp.has('b')", "true" }, | ||||
|  | ||||
|         .{ "acc = [];", null }, | ||||
|         .{ "for (const key of usp.keys()) { acc.push(key) }; acc;", "b" }, | ||||
|  | ||||
|         .{ "acc = [];", null }, | ||||
|         .{ "for (const value of usp.values()) { acc.push(value) }; acc;", "3" }, | ||||
|  | ||||
|         .{ "acc = [];", null }, | ||||
|         .{ "for (const entry of usp.entries()) { acc.push(entry) }; acc;", "b,3" }, | ||||
|  | ||||
|         .{ "acc = [];", null }, | ||||
|         .{ "for (const entry of usp) { acc.push(entry) }; acc;", "b,3" }, | ||||
|     }, .{}); | ||||
|  | ||||
|     try runner.testCases(&.{ | ||||
|         .{ "usp = new URLSearchParams('?hello')", null }, | ||||
|         .{ "usp.get('hello')", "" }, | ||||
|  | ||||
|         .{ "usp = new URLSearchParams('?abc=')", null }, | ||||
|         .{ "usp.get('abc')", "" }, | ||||
|  | ||||
|         .{ "usp = new URLSearchParams('?abc=123&')", null }, | ||||
|         .{ "usp.get('abc')", "123" }, | ||||
|         .{ "usp.size", "1" }, | ||||
|  | ||||
|         .{ "var fd = new FormData()", null }, | ||||
|         .{ "fd.append('a', '1')", null }, | ||||
|         .{ "fd.append('a', '2')", null }, | ||||
|         .{ "fd.append('b', '3')", null }, | ||||
|         .{ "ups = new URLSearchParams(fd)", null }, | ||||
|         .{ "ups.size", "3" }, | ||||
|         .{ "ups.getAll('a')", "1,2" }, | ||||
|         .{ "ups.getAll('b')", "3" }, | ||||
|         .{ "fd.delete('a')", null }, // the two aren't linked, it created a copy | ||||
|         .{ "ups.size", "3" }, | ||||
|     }, .{}); | ||||
| } | ||||
|   | ||||
| @@ -22,8 +22,9 @@ const Allocator = std.mem.Allocator; | ||||
|  | ||||
| const log = @import("../../log.zig"); | ||||
| const parser = @import("../netsurf.zig"); | ||||
| const iterator = @import("../iterator/iterator.zig"); | ||||
| const Page = @import("../page.zig").Page; | ||||
| const kv = @import("../key_value.zig"); | ||||
| const iterator = @import("../iterator/iterator.zig"); | ||||
|  | ||||
| pub const Interfaces = .{ | ||||
|     FormData, | ||||
| @@ -32,29 +33,12 @@ pub const Interfaces = .{ | ||||
|     EntryIterable, | ||||
| }; | ||||
|  | ||||
| // We store the values in an ArrayList rather than a an | ||||
| // StringArrayHashMap([]const u8) because of the way the iterators (i.e., keys(), | ||||
| // values() and entries()) work. The FormData can contain duplicate keys, and | ||||
| // each iteration yields 1 key=>value pair. So, given: | ||||
| // | ||||
| //  let f = new FormData(); | ||||
| //  f.append('a', '1'); | ||||
| //  f.append('a', '2'); | ||||
| // | ||||
| // Then we'd expect f.keys(), f.values() and f.entries() to yield 2 results: | ||||
| //  ['a', '1'] | ||||
| //  ['a', '2'] | ||||
| // | ||||
| // This is much easier to do with an ArrayList than a HashMap, especially given | ||||
| // that the FormData could be mutated while iterating. | ||||
| // The downside is that most of the normal operations are O(N). | ||||
|  | ||||
| // https://xhr.spec.whatwg.org/#interface-formdata | ||||
| pub const FormData = struct { | ||||
|     entries: Entry.List, | ||||
|     entries: kv.List, | ||||
|  | ||||
|     pub fn constructor(form_: ?*parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !FormData { | ||||
|         const form = form_ orelse return .{ .entries = .empty }; | ||||
|         const form = form_ orelse return .{ .entries = .{} }; | ||||
|         return fromForm(form, submitter_, page); | ||||
|     } | ||||
|  | ||||
| @@ -64,208 +48,77 @@ pub const FormData = struct { | ||||
|     } | ||||
|  | ||||
|     pub fn _get(self: *const FormData, key: []const u8) ?[]const u8 { | ||||
|         const result = self.find(key) orelse return null; | ||||
|         return result.entry.value; | ||||
|         return self.entries.get(key); | ||||
|     } | ||||
|  | ||||
|     pub fn _getAll(self: *const FormData, key: []const u8, page: *Page) ![][]const u8 { | ||||
|         const arena = page.call_arena; | ||||
|         var arr: std.ArrayListUnmanaged([]const u8) = .empty; | ||||
|         for (self.entries.items) |entry| { | ||||
|             if (std.mem.eql(u8, key, entry.key)) { | ||||
|                 try arr.append(arena, entry.value); | ||||
|             } | ||||
|         } | ||||
|         return arr.items; | ||||
|     pub fn _getAll(self: *const FormData, key: []const u8, page: *Page) ![]const []const u8 { | ||||
|         return self.entries.getAll(page.call_arena, key); | ||||
|     } | ||||
|  | ||||
|     pub fn _has(self: *const FormData, key: []const u8) bool { | ||||
|         return self.find(key) != null; | ||||
|         return self.entries.has(key); | ||||
|     } | ||||
|  | ||||
|     // TODO: value should be a string or blog | ||||
|     // TODO: another optional parameter for the filename | ||||
|     pub fn _set(self: *FormData, key: []const u8, value: []const u8, page: *Page) !void { | ||||
|         self._delete(key); | ||||
|         return self._append(key, value, page); | ||||
|         return self.entries.set(page.arena, key, value); | ||||
|     } | ||||
|  | ||||
|     // TODO: value should be a string or blog | ||||
|     // TODO: another optional parameter for the filename | ||||
|     pub fn _append(self: *FormData, key: []const u8, value: []const u8, page: *Page) !void { | ||||
|         const arena = page.arena; | ||||
|         return self.entries.append(arena, .{ .key = try arena.dupe(u8, key), .value = try arena.dupe(u8, value) }); | ||||
|         return self.entries.append(page.arena, key, value); | ||||
|     } | ||||
|  | ||||
|     pub fn _delete(self: *FormData, key: []const u8) void { | ||||
|         var i: usize = 0; | ||||
|         while (i < self.entries.items.len) { | ||||
|             const entry = self.entries.items[i]; | ||||
|             if (std.mem.eql(u8, key, entry.key)) { | ||||
|                 _ = self.entries.swapRemove(i); | ||||
|             } else { | ||||
|                 i += 1; | ||||
|             } | ||||
|         } | ||||
|         return self.entries.delete(key); | ||||
|     } | ||||
|  | ||||
|     pub fn _keys(self: *const FormData) KeyIterable { | ||||
|         return .{ .inner = .{ .entries = &self.entries } }; | ||||
|         return .{ .inner = self.entries.keyIterator() }; | ||||
|     } | ||||
|  | ||||
|     pub fn _values(self: *const FormData) ValueIterable { | ||||
|         return .{ .inner = .{ .entries = &self.entries } }; | ||||
|         return .{ .inner = self.entries.valueIterator() }; | ||||
|     } | ||||
|  | ||||
|     pub fn _entries(self: *const FormData) EntryIterable { | ||||
|         return .{ .inner = .{ .entries = &self.entries } }; | ||||
|         return .{ .inner = self.entries.entryIterator() }; | ||||
|     } | ||||
|  | ||||
|     pub fn _symbol_iterator(self: *const FormData) EntryIterable { | ||||
|         return self._entries(); | ||||
|     } | ||||
|  | ||||
|     const FindResult = struct { | ||||
|         index: usize, | ||||
|         entry: Entry, | ||||
|     }; | ||||
|  | ||||
|     fn find(self: *const FormData, key: []const u8) ?FindResult { | ||||
|         for (self.entries.items, 0..) |entry, i| { | ||||
|             if (std.mem.eql(u8, key, entry.key)) { | ||||
|                 return .{ .index = i, .entry = entry }; | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     pub fn write(self: *const FormData, encoding_: ?[]const u8, writer: anytype) !void { | ||||
|         const encoding = encoding_ orelse { | ||||
|             return urlEncode(self, writer); | ||||
|             return kv.urlEncode(self.entries, .form, writer); | ||||
|         }; | ||||
|  | ||||
|         if (std.ascii.eqlIgnoreCase(encoding, "application/x-www-form-urlencoded")) { | ||||
|             return urlEncode(self, writer); | ||||
|             return kv.urlEncode(self.entries, .form, writer); | ||||
|         } | ||||
|  | ||||
|         log.warn(.web_api, "not implemented", .{ .feature = "form data encoding", .encoding = encoding }); | ||||
|         log.warn(.web_api, "not implemented", .{ | ||||
|             .feature = "form data encoding", | ||||
|             .encoding = encoding, | ||||
|         }); | ||||
|         return error.EncodingNotSupported; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| fn urlEncode(data: *const FormData, writer: anytype) !void { | ||||
|     const entries = data.entries.items; | ||||
|     if (entries.len == 0) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     try urlEncodeEntry(entries[0], writer); | ||||
|     for (entries[1..]) |entry| { | ||||
|         try writer.writeByte('&'); | ||||
|         try urlEncodeEntry(entry, writer); | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn urlEncodeEntry(entry: Entry, writer: anytype) !void { | ||||
|     try urlEncodeValue(entry.key, writer); | ||||
|     try writer.writeByte('='); | ||||
|     try urlEncodeValue(entry.value, writer); | ||||
| } | ||||
|  | ||||
| fn urlEncodeValue(value: []const u8, writer: anytype) !void { | ||||
|     if (!urlEncodeShouldEscape(value)) { | ||||
|         return writer.writeAll(value); | ||||
|     } | ||||
|  | ||||
|     for (value) |b| { | ||||
|         if (urlEncodeUnreserved(b)) { | ||||
|             try writer.writeByte(b); | ||||
|         } else if (b == ' ') { | ||||
|             // for form submission, space should be encoded as '+', not '%20' | ||||
|             try writer.writeByte('+'); | ||||
|         } else { | ||||
|             try writer.print("%{X:0>2}", .{b}); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn urlEncodeShouldEscape(value: []const u8) bool { | ||||
|     for (value) |b| { | ||||
|         if (!urlEncodeUnreserved(b)) { | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| fn urlEncodeUnreserved(b: u8) bool { | ||||
|     return switch (b) { | ||||
|         'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true, | ||||
|         else => false, | ||||
|     }; | ||||
| } | ||||
|  | ||||
| const Entry = struct { | ||||
|     key: []const u8, | ||||
|     value: []const u8, | ||||
|  | ||||
|     pub const List = std.ArrayListUnmanaged(Entry); | ||||
| }; | ||||
|  | ||||
| const KeyIterable = iterator.Iterable(KeyIterator, "FormDataKeyIterator"); | ||||
| const ValueIterable = iterator.Iterable(ValueIterator, "FormDataValueIterator"); | ||||
| const EntryIterable = iterator.Iterable(EntryIterator, "FormDataEntryIterator"); | ||||
|  | ||||
| const KeyIterator = struct { | ||||
|     index: usize = 0, | ||||
|     entries: *const Entry.List, | ||||
|  | ||||
|     pub fn _next(self: *KeyIterator) ?[]const u8 { | ||||
|         const index = self.index; | ||||
|         if (index == self.entries.items.len) { | ||||
|             return null; | ||||
|         } | ||||
|         self.index += 1; | ||||
|         return self.entries.items[index].key; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| const ValueIterator = struct { | ||||
|     index: usize = 0, | ||||
|     entries: *const Entry.List, | ||||
|  | ||||
|     pub fn _next(self: *ValueIterator) ?[]const u8 { | ||||
|         const index = self.index; | ||||
|         if (index == self.entries.items.len) { | ||||
|             return null; | ||||
|         } | ||||
|         self.index += 1; | ||||
|         return self.entries.items[index].value; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| const EntryIterator = struct { | ||||
|     index: usize = 0, | ||||
|     entries: *const Entry.List, | ||||
|  | ||||
|     pub fn _next(self: *EntryIterator) ?struct { []const u8, []const u8 } { | ||||
|         const index = self.index; | ||||
|         if (index == self.entries.items.len) { | ||||
|             return null; | ||||
|         } | ||||
|         self.index += 1; | ||||
|         const entry = self.entries.items[index]; | ||||
|         return .{ entry.key, entry.value }; | ||||
|     } | ||||
| }; | ||||
| const KeyIterable = iterator.Iterable(kv.KeyIterator, "FormDataKeyIterator"); | ||||
| const ValueIterable = iterator.Iterable(kv.ValueIterator, "FormDataValueIterator"); | ||||
| const EntryIterable = iterator.Iterable(kv.EntryIterator, "FormDataEntryIterator"); | ||||
|  | ||||
| // TODO: handle disabled fieldsets | ||||
| fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !Entry.List { | ||||
| fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !kv.List { | ||||
|     const arena = page.arena; | ||||
|     const collection = try parser.formGetCollection(form); | ||||
|     const len = try parser.htmlCollectionGetLength(collection); | ||||
|  | ||||
|     var entries: Entry.List = .empty; | ||||
|     var entries: kv.List = .{}; | ||||
|     try entries.ensureTotalCapacity(arena, len); | ||||
|  | ||||
|     var submitter_included = false; | ||||
| @@ -288,15 +141,10 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page | ||||
|                 if (std.ascii.eqlIgnoreCase(tpe, "image")) { | ||||
|                     if (submitter_name_) |submitter_name| { | ||||
|                         if (std.mem.eql(u8, submitter_name, name)) { | ||||
|                             try entries.append(arena, .{ | ||||
|                                 .key = try std.fmt.allocPrint(arena, "{s}.x", .{name}), | ||||
|                                 .value = "0", | ||||
|                             }); | ||||
|                             try entries.append(arena, .{ | ||||
|                                 .key = try std.fmt.allocPrint(arena, "{s}.y", .{name}), | ||||
|                                 .value = "0", | ||||
|                             }); | ||||
|  | ||||
|                             const key_x = try std.fmt.allocPrint(arena, "{s}.x", .{name}); | ||||
|                             const key_y = try std.fmt.allocPrint(arena, "{s}.y", .{name}); | ||||
|                             try entries.appendOwned(arena, key_x, "0"); | ||||
|                             try entries.appendOwned(arena, key_y, "0"); | ||||
|                             submitter_included = true; | ||||
|                         } | ||||
|                     } | ||||
| @@ -315,7 +163,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page | ||||
|                     submitter_included = true; | ||||
|                 } | ||||
|                 const value = (try parser.elementGetAttribute(element, "value")) orelse ""; | ||||
|                 try entries.append(arena, .{ .key = name, .value = value }); | ||||
|                 try entries.appendOwned(arena, name, value); | ||||
|             }, | ||||
|             .select => { | ||||
|                 const select: *parser.Select = @ptrCast(node); | ||||
| @@ -324,12 +172,12 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page | ||||
|             .textarea => { | ||||
|                 const textarea: *parser.TextArea = @ptrCast(node); | ||||
|                 const value = try parser.textareaGetValue(textarea); | ||||
|                 try entries.append(arena, .{ .key = name, .value = value }); | ||||
|                 try entries.appendOwned(arena, name, value); | ||||
|             }, | ||||
|             .button => if (submitter_name_) |submitter_name| { | ||||
|                 if (std.mem.eql(u8, submitter_name, name)) { | ||||
|                     const value = (try parser.elementGetAttribute(element, "value")) orelse ""; | ||||
|                     try entries.append(arena, .{ .key = name, .value = value }); | ||||
|                     try entries.appendOwned(arena, name, value); | ||||
|                     submitter_included = true; | ||||
|                 } | ||||
|             }, | ||||
| @@ -345,14 +193,14 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page | ||||
|             // this can happen if the submitter is outside the form, but associated | ||||
|             // with the form via a form=ID attribute | ||||
|             const value = (try parser.elementGetAttribute(@ptrCast(submitter), "value")) orelse ""; | ||||
|             try entries.append(arena, .{ .key = submitter_name_.?, .value = value }); | ||||
|             try entries.appendOwned(arena, submitter_name_.?, value); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return entries; | ||||
| } | ||||
|  | ||||
| fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u8, entries: *Entry.List, page: *Page) !void { | ||||
| fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u8, entries: *kv.List, page: *Page) !void { | ||||
|     const HTMLSelectElement = @import("../html/select.zig").HTMLSelectElement; | ||||
|  | ||||
|     // Go through the HTMLSelectElement because it has specific logic for handling | ||||
| @@ -372,7 +220,7 @@ fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u | ||||
|             return; | ||||
|         } | ||||
|         const value = try parser.optionGetValue(option); | ||||
|         return entries.append(arena, .{ .key = name, .value = value }); | ||||
|         return entries.appendOwned(arena, name, value); | ||||
|     } | ||||
|  | ||||
|     const len = try parser.optionCollectionGetLength(options); | ||||
| @@ -386,7 +234,7 @@ fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u | ||||
|  | ||||
|         if (try parser.optionGetSelected(option)) { | ||||
|             const value = try parser.optionGetValue(option); | ||||
|             try entries.append(arena, .{ .key = name, .value = value }); | ||||
|             try entries.appendOwned(arena, name, value); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -420,7 +268,7 @@ test "Browser.FormData" { | ||||
|         \\   <input id="is_disabled" disabled value="nope2"> | ||||
|         \\ | ||||
|         \\   <input name="txt-1" value="txt-1-v"> | ||||
|         \\   <input name="txt-2" value="txt-2-v" type=password> | ||||
|         \\   <input name="txt-2" value="txt-~-v" type=password> | ||||
|         \\ | ||||
|         \\   <input name="chk-3" value="chk-3-va" type=checkbox> | ||||
|         \\   <input name="chk-3" value="chk-3-vb" type=checkbox checked> | ||||
| @@ -518,7 +366,7 @@ test "Browser.FormData" { | ||||
|             \\ acc.slice(0, -1) | ||||
|             , | ||||
|             \\txt-1=txt-1-v | ||||
|             \\txt-2=txt-2-v | ||||
|             \\txt-2=txt-~-v | ||||
|             \\chk-3=chk-3-vb | ||||
|             \\chk-3=chk-3-vc | ||||
|             \\rdi-1=rdi-1-vc | ||||
| @@ -539,7 +387,7 @@ test "Browser.FormData: urlEncode" { | ||||
|     defer arr.deinit(testing.allocator); | ||||
|  | ||||
|     { | ||||
|         var fd = FormData{ .entries = .empty }; | ||||
|         var fd = FormData{ .entries = .{} }; | ||||
|         try testing.expectError(error.EncodingNotSupported, fd.write("unknown", arr.writer(testing.allocator))); | ||||
|  | ||||
|         try fd.write(null, arr.writer(testing.allocator)); | ||||
| @@ -550,12 +398,12 @@ test "Browser.FormData: urlEncode" { | ||||
|     } | ||||
|  | ||||
|     { | ||||
|         var fd = FormData{ .entries = Entry.List.fromOwnedSlice(@constCast(&[_]Entry{ | ||||
|         var fd = FormData{ .entries = kv.List.fromOwnedSlice(@constCast(&[_]kv.KeyValue{ | ||||
|             .{ .key = "a", .value = "1" }, | ||||
|             .{ .key = "it's over", .value = "9000 !!!" }, | ||||
|             .{ .key = "emot", .value = "ok: ☺" }, | ||||
|             .{ .key = "em~ot", .value = "ok: ☺" }, | ||||
|         })) }; | ||||
|         const expected = "a=1&it%27s+over=9000+%21%21%21&emot=ok%3A+%E2%98%BA"; | ||||
|         const expected = "a=1&it%27s+over=9000+%21%21%21&em%7Eot=ok%3A+%E2%98%BA"; | ||||
|         try fd.write(null, arr.writer(testing.allocator)); | ||||
|         try testing.expectEqual(expected, arr.items); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Karl Seguin
					Karl Seguin