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