Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/browser/env.zig
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const WebApis = struct {
@import("storage/storage.zig").Interfaces,
@import("url/url.zig").Interfaces,
@import("xhr/xhr.zig").Interfaces,
@import("url/form_data.zig").Interfaces,
@import("xmlserializer/xmlserializer.zig").Interfaces,
});
};
Expand Down
245 changes: 245 additions & 0 deletions src/browser/url/form_data.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <[email protected]>
// Pierre Tachoire <[email protected]>
//
// 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;

const iterator = @import("../iterator/iterator.zig");
const SessionState = @import("../env.zig").SessionState;

pub const Interfaces = .{
FormData,
KeyIterable,
ValueIterable,
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).
pub const FormData = struct {
entries: std.ArrayListUnmanaged(Entry),

pub fn constructor() FormData {
return .{
.entries = .empty,
};
}

pub fn _get(self: *const FormData, key: []const u8) ?[]const u8 {
const result = self.find(key) orelse return null;
return result.entry.value;
}

pub fn _getAll(self: *const FormData, key: []const u8, state: *SessionState) ![][]const u8 {
const arena = state.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 _has(self: *const FormData, key: []const u8) bool {
return self.find(key) != null;
}

// 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, state: *SessionState) !void {
self._delete(key);
return self._append(key, value, state);
}

// 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, state: *SessionState) !void {
const arena = state.arena;
return self.entries.append(arena, .{ .key = try arena.dupe(u8, key), .value = try arena.dupe(u8, 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;
}
}
}

pub fn _keys(self: *const FormData) KeyIterable {
return .{ .inner = .{ .entries = &self.entries } };
}

pub fn _values(self: *const FormData) ValueIterable {
return .{ .inner = .{ .entries = &self.entries } };
}

pub fn _entries(self: *const FormData) EntryIterable {
return .{ .inner = .{ .entries = &self.entries } };
}

pub fn _symbol_iterator(self: *const FormData) EntryIterable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi I can't find this a spec online..

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% sure how to read the spec here, but it does mention: iterable<USVString, FormDataEntryValue>.

MDN lists the iterator here: https://developer.mozilla.org/en-US/docs/Web/API/FormData#browser_compatibility

And, a quick test in browser shows that the form data is iterable:

let f = new FormData()
f.set('a', '1');
for (entry of f) console.log(entry)

will print Array [ "a", "1" ]

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;
}
};

const Entry = struct {
key: []const u8,
value: []const u8,
};

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 std.ArrayListUnmanaged(Entry),

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 std.ArrayListUnmanaged(Entry),

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 std.ArrayListUnmanaged(Entry),

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 testing = @import("../../testing.zig");
test "FormData" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();

try runner.testCases(&.{
.{ "let f = new FormData()", null },
.{ "f.get('a')", "null" },
.{ "f.has('a')", "false" },
.{ "f.getAll('a')", "" },
.{ "f.delete('a')", "undefined" },

.{ "f.set('a', 1)", "undefined" },
.{ "f.has('a')", "true" },
.{ "f.get('a')", "1" },
.{ "f.getAll('a')", "1" },

.{ "f.append('a', 2)", "undefined" },
.{ "f.has('a')", "true" },
.{ "f.get('a')", "1" },
.{ "f.getAll('a')", "1,2" },

.{ "f.append('b', '3')", "undefined" },
.{ "f.has('a')", "true" },
.{ "f.get('a')", "1" },
.{ "f.getAll('a')", "1,2" },
.{ "f.has('b')", "true" },
.{ "f.get('b')", "3" },
.{ "f.getAll('b')", "3" },

.{ "let acc = [];", null },
.{ "for (const key of f.keys()) { acc.push(key) }; acc;", "a,a,b" },

.{ "acc = [];", null },
.{ "for (const value of f.values()) { acc.push(value) }; acc;", "1,2,3" },

.{ "acc = [];", null },
.{ "for (const entry of f.entries()) { acc.push(entry) }; acc;", "a,1,a,2,b,3" },

.{ "acc = [];", null },
.{ "for (const entry of f) { acc.push(entry) }; acc;", "a,1,a,2,b,3" },

.{ "f.delete('a')", "undefined" },
.{ "f.has('a')", "false" },
.{ "f.has('b')", "true" },

.{ "acc = [];", null },
.{ "for (const key of f.keys()) { acc.push(key) }; acc;", "b" },

.{ "acc = [];", null },
.{ "for (const value of f.values()) { acc.push(value) }; acc;", "3" },

.{ "acc = [];", null },
.{ "for (const entry of f.entries()) { acc.push(entry) }; acc;", "b,3" },

.{ "acc = [];", null },
.{ "for (const entry of f) { acc.push(entry) }; acc;", "b,3" },
}, .{});
}