fediglam/src/http/fields.zig

227 lines
6.9 KiB
Zig

const std = @import("std");
pub const ParamIter = struct {
str: []const u8,
index: usize = 0,
const Param = struct {
name: []const u8,
value: []const u8,
};
pub fn from(str: []const u8) ParamIter {
return .{ .str = str, .index = std.mem.indexOfScalar(u8, str, ';') orelse str.len };
}
pub fn fieldValue(self: *ParamIter) []const u8 {
return std.mem.sliceTo(self.str, ';');
}
pub fn next(self: *ParamIter) ?Param {
if (self.index >= self.str.len) return null;
const start = self.index + 1;
const new_start = std.mem.indexOfScalarPos(u8, self.str, start, ';') orelse self.str.len;
self.index = new_start;
const param = std.mem.trim(u8, self.str[start..new_start], " \t");
var split = std.mem.split(u8, param, "=");
const name = split.first();
const value = std.mem.trimLeft(u8, split.rest(), " \t");
// TODO: handle quoted values
// TODO: handle parse errors
return Param{
.name = name,
.value = value,
};
}
};
pub fn getParam(field: []const u8, name: ?[]const u8) ?[]const u8 {
var iter = ParamIter.from(field);
if (name) |param| {
while (iter.next()) |p| {
if (std.ascii.eqlIgnoreCase(param, p.name)) {
const trimmed = std.mem.trim(u8, p.value, " \t");
if (trimmed.len >= 2 and trimmed[0] == '"' and trimmed[trimmed.len - 1] == '"') {
return trimmed[1 .. trimmed.len - 1];
}
return trimmed;
}
}
return null;
} else return iter.fieldValue();
}
pub const Fields = struct {
const HashContext = struct {
const hash_seed = 1;
pub fn eql(_: @This(), lhs: []const u8, rhs: []const u8, _: usize) bool {
return std.ascii.eqlIgnoreCase(lhs, rhs);
}
pub fn hash(_: @This(), s: []const u8) u32 {
var h = std.hash.Wyhash.init(hash_seed);
for (s) |ch| {
const c = [1]u8{std.ascii.toLower(ch)};
h.update(&c);
}
return @truncate(u32, h.final());
}
};
const HashMap = std.ArrayHashMapUnmanaged(
[]const u8,
[]const u8,
HashContext,
true,
);
unmanaged: HashMap,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) Fields {
return Fields{
.unmanaged = .{},
.allocator = allocator,
};
}
pub fn deinit(self: *Fields) void {
var hash_iter = self.unmanaged.iterator();
while (hash_iter.next()) |entry| {
self.allocator.free(entry.key_ptr.*);
self.allocator.free(entry.value_ptr.*);
}
self.unmanaged.deinit(self.allocator);
}
pub fn iterator(self: Fields) HashMap.Iterator {
return self.unmanaged.iterator();
}
pub fn get(self: Fields, key: []const u8) ?[]const u8 {
return self.unmanaged.get(key);
}
pub const ListIterator = struct {
remaining: []const u8,
fn extractElement(self: *ListIterator) ?[]const u8 {
if (self.remaining.len == 0) return null;
var start: usize = 0;
var is_quoted = false;
const end = for (self.remaining) |ch, i| {
if (start == i and std.ascii.isWhitespace(ch)) {
start += 1;
} else if (ch == '"') {
is_quoted = !is_quoted;
}
if (ch == ',' and !is_quoted) {
break i;
}
} else self.remaining.len;
const str = self.remaining[start..end];
if (end == self.remaining.len) {
self.remaining = "";
} else {
self.remaining = self.remaining[end + 1 ..];
}
return std.mem.trim(u8, str, " \t");
}
pub fn next(self: *ListIterator) ?[]const u8 {
while (self.extractElement()) |elem| {
if (elem.len != 0) return elem;
}
return null;
}
};
pub fn getList(self: Fields, key: []const u8) ListIterator {
return if (self.unmanaged.get(key)) |hdr| ListIterator{ .remaining = hdr } else ListIterator{ .remaining = "" };
}
pub fn put(self: *Fields, key: []const u8, val: []const u8) !void {
const key_clone = try self.allocator.alloc(u8, key.len);
std.mem.copy(u8, key_clone, key);
errdefer self.allocator.free(key_clone);
const val_clone = try self.allocator.alloc(u8, val.len);
std.mem.copy(u8, val_clone, val);
errdefer self.allocator.free(val_clone);
if (try self.unmanaged.fetchPut(self.allocator, key_clone, val_clone)) |entry| {
self.allocator.free(key_clone);
//self.allocator.free(entry.key);
self.allocator.free(entry.value);
}
}
pub fn append(self: *Fields, key: []const u8, val: []const u8) !void {
if (self.unmanaged.getEntry(key)) |entry| {
const new_val = try std.mem.join(self.allocator, ", ", &.{ entry.value_ptr.*, val });
self.allocator.free(entry.value_ptr.*);
entry.value_ptr.* = new_val;
} else {
try self.put(key, val);
}
}
pub fn count(self: Fields) usize {
return self.unmanaged.count();
}
pub const CookieOptions = struct {
Secure: bool = true,
HttpOnly: bool = true,
SameSite: ?enum {
Strict,
Lax,
None,
} = null,
};
// TODO: Escape cookie values
pub fn setCookie(self: *Fields, name: []const u8, value: []const u8, opt: CookieOptions) !void {
const cookie = try std.fmt.allocPrint(
self.allocator,
"{s}={s}{s}{s}{s}{s}",
.{
name,
value,
if (opt.Secure) "; Secure" else "",
if (opt.HttpOnly) "; HttpOnly" else "",
if (opt.SameSite) |_| "; SameSite=" else "",
if (opt.SameSite) |same_site| @tagName(same_site) else "",
},
);
defer self.allocator.free(cookie);
// TODO: reduce unnecessary allocations
try self.append("Set-Cookie", cookie);
}
// TODO: perform validation at request parse time?
pub fn getCookie(self: *Fields, name: []const u8) !?[]const u8 {
const hdr = self.get("Cookie") orelse return null;
var iter = std.mem.split(u8, hdr, ";");
while (iter.next()) |cookie| {
const trimmed = std.mem.trimLeft(u8, cookie, " ");
const cookie_name = std.mem.sliceTo(trimmed, '=');
if (std.mem.eql(u8, name, cookie_name)) {
const rest = trimmed[cookie_name.len..];
if (rest.len == 0) return error.InvalidCookie;
return rest[1..];
}
}
return null;
}
};