const std = @import("../std.zig");
const Allocator = std.mem.Allocator;
const testing = std.testing;
const ascii = std.ascii;
const assert = std.debug.assert;
pub const HeaderList = std.ArrayListUnmanaged(Field);
pub const HeaderIndexList = std.ArrayListUnmanaged(usize);
pub const HeaderIndex = std.HashMapUnmanaged([]const u8, HeaderIndexList, CaseInsensitiveStringContext, std.hash_map.default_max_load_percentage);
pub const CaseInsensitiveStringContext = struct {
pub fn hash(self: @This(), s: []const u8) u64 {
_ = self;
var buf: [64]u8 = undefined;
var i: u8 = 0;
var h = std.hash.Wyhash.init(0);
while (i < s.len) : (i += 64) {
const left = @min(64, s.len - i);
const ret = ascii.lowerString(buf[0..], s[i..][0..left]);
h.update(ret);
}
return h.final();
}
pub fn eql(self: @This(), a: []const u8, b: []const u8) bool {
_ = self;
return ascii.eqlIgnoreCase(a, b);
}
};
pub const Field = struct {
name: []const u8,
value: []const u8,
pub fn modify(entry: *Field, allocator: Allocator, new_value: []const u8) !void {
if (entry.value.len <= new_value.len) {
std.mem.copy(u8, @constCast(entry.value), new_value);
} else {
allocator.free(entry.value);
entry.value = try allocator.dupe(u8, new_value);
}
}
fn lessThan(ctx: void, a: Field, b: Field) bool {
_ = ctx;
if (a.name.ptr == b.name.ptr) return false;
return ascii.lessThanIgnoreCase(a.name, b.name);
}
};
pub const Headers = struct {
allocator: Allocator,
list: HeaderList = .{},
index: HeaderIndex = .{},
owned: bool = true,
pub fn init(allocator: Allocator) Headers {
return .{ .allocator = allocator };
}
pub fn deinit(headers: *Headers) void {
headers.deallocateIndexListsAndFields();
headers.index.deinit(headers.allocator);
headers.list.deinit(headers.allocator);
headers.* = undefined;
}
pub fn append(headers: *Headers, name: []const u8, value: []const u8) !void {
const n = headers.list.items.len;
const value_duped = if (headers.owned) try headers.allocator.dupe(u8, value) else value;
errdefer if (headers.owned) headers.allocator.free(value_duped);
var entry = Field{ .name = undefined, .value = value_duped };
if (headers.index.getEntry(name)) |kv| {
entry.name = kv.key_ptr.*;
try kv.value_ptr.append(headers.allocator, n);
} else {
const name_duped = if (headers.owned) try headers.allocator.dupe(u8, name) else name;
errdefer if (headers.owned) headers.allocator.free(name_duped);
entry.name = name_duped;
var new_index = try HeaderIndexList.initCapacity(headers.allocator, 1);
errdefer new_index.deinit(headers.allocator);
new_index.appendAssumeCapacity(n);
try headers.index.put(headers.allocator, name_duped, new_index);
}
try headers.list.append(headers.allocator, entry);
}
pub fn contains(headers: Headers, name: []const u8) bool {
return headers.index.contains(name);
}
pub fn delete(headers: *Headers, name: []const u8) bool {
if (headers.index.fetchRemove(name)) |kv| {
var index = kv.value;
var i = index.items.len;
while (i > 0) {
i -= 1;
const data_index = index.items[i];
const removed = headers.list.orderedRemove(data_index);
assert(ascii.eqlIgnoreCase(removed.name, name));
if (headers.owned) headers.allocator.free(removed.value);
}
if (headers.owned) headers.allocator.free(kv.key);
index.deinit(headers.allocator);
headers.rebuildIndex();
return true;
} else {
return false;
}
}
pub fn firstIndexOf(headers: Headers, name: []const u8) ?usize {
const index = headers.index.get(name) orelse return null;
return index.items[0];
}
pub fn getIndices(headers: Headers, name: []const u8) ?[]const usize {
const index = headers.index.get(name) orelse return null;
return index.items;
}
pub fn getFirstEntry(headers: Headers, name: []const u8) ?Field {
const first_index = headers.firstIndexOf(name) orelse return null;
return headers.list.items[first_index];
}
pub fn getEntries(headers: Headers, allocator: Allocator, name: []const u8) !?[]const Field {
const indices = headers.getIndices(name) orelse return null;
const buf = try allocator.alloc(Field, indices.len);
for (indices, 0..) |idx, n| {
buf[n] = headers.list.items[idx];
}
return buf;
}
pub fn getFirstValue(headers: Headers, name: []const u8) ?[]const u8 {
const first_index = headers.firstIndexOf(name) orelse return null;
return headers.list.items[first_index].value;
}
pub fn getValues(headers: Headers, allocator: Allocator, name: []const u8) !?[]const []const u8 {
const indices = headers.getIndices(name) orelse return null;
const buf = try allocator.alloc([]const u8, indices.len);
for (indices, 0..) |idx, n| {
buf[n] = headers.list.items[idx].value;
}
return buf;
}
fn rebuildIndex(headers: *Headers) void {
var it = headers.index.iterator();
while (it.next()) |entry| {
entry.value_ptr.shrinkRetainingCapacity(0);
}
for (headers.list.items, 0..) |entry, i| {
headers.index.getEntry(entry.name).?.value_ptr.appendAssumeCapacity(i);
}
}
pub fn sort(headers: *Headers) void {
std.sort.sort(Field, headers.list.items, {}, Field.lessThan);
headers.rebuildIndex();
}
pub fn format(
headers: Headers,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
out_stream: anytype,
) !void {
_ = fmt;
_ = options;
for (headers.list.items) |entry| {
if (entry.value.len == 0) continue;
try out_stream.writeAll(entry.name);
try out_stream.writeAll(": ");
try out_stream.writeAll(entry.value);
try out_stream.writeAll("\r\n");
}
}
pub fn formatCommaSeparated(
headers: Headers,
name: []const u8,
out_stream: anytype,
) !void {
const indices = headers.getIndices(name) orelse return;
try out_stream.writeAll(name);
try out_stream.writeAll(": ");
for (indices, 0..) |idx, n| {
if (n != 0) try out_stream.writeAll(", ");
try out_stream.writeAll(headers.list.items[idx].value);
}
try out_stream.writeAll("\r\n");
}
fn deallocateIndexListsAndFields(headers: *Headers) void {
var it = headers.index.iterator();
while (it.next()) |entry| {
entry.value_ptr.deinit(headers.allocator);
if (headers.owned) headers.allocator.free(entry.key_ptr.*);
}
if (headers.owned) {
for (headers.list.items) |entry| {
headers.allocator.free(entry.value);
}
}
}
pub fn clearAndFree(headers: *Headers) void {
headers.deallocateIndexListsAndFields();
headers.index.clearAndFree(headers.allocator);
headers.list.clearAndFree(headers.allocator);
}
pub fn clearRetainingCapacity(headers: *Headers) void {
headers.deallocateIndexListsAndFields();
headers.index.clearRetainingCapacity();
headers.list.clearRetainingCapacity();
}
};
test "Headers.append" {
var h = Headers{ .allocator = std.testing.allocator };
defer h.deinit();
try h.append("foo", "bar");
try h.append("hello", "world");
try testing.expect(h.contains("Foo"));
try testing.expect(!h.contains("Bar"));
}
test "Headers.delete" {
var h = Headers{ .allocator = std.testing.allocator };
defer h.deinit();
try h.append("foo", "bar");
try h.append("hello", "world");
try testing.expect(h.contains("Foo"));
_ = h.delete("Foo");
try testing.expect(!h.contains("foo"));
}
test "Headers consistency" {
var h = Headers{ .allocator = std.testing.allocator };
defer h.deinit();
try h.append("foo", "bar");
try h.append("hello", "world");
_ = h.delete("Foo");
try h.append("foo", "bar");
try h.append("bar", "world");
try h.append("foo", "baz");
try h.append("baz", "hello");
try testing.expectEqual(@as(?usize, 0), h.firstIndexOf("hello"));
try testing.expectEqual(@as(?usize, 1), h.firstIndexOf("foo"));
try testing.expectEqual(@as(?usize, 2), h.firstIndexOf("bar"));
try testing.expectEqual(@as(?usize, 4), h.firstIndexOf("baz"));
try testing.expectEqual(@as(?usize, null), h.firstIndexOf("pog"));
try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("hello").?);
try testing.expectEqualSlices(usize, &[_]usize{ 1, 3 }, h.getIndices("foo").?);
try testing.expectEqualSlices(usize, &[_]usize{2}, h.getIndices("bar").?);
try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("baz").?);
try testing.expectEqual(@as(?[]const usize, null), h.getIndices("pog"));
try testing.expectEqualStrings("world", h.getFirstEntry("hello").?.value);
try testing.expectEqualStrings("bar", h.getFirstEntry("foo").?.value);
try testing.expectEqualStrings("world", h.getFirstEntry("bar").?.value);
try testing.expectEqualStrings("hello", h.getFirstEntry("baz").?.value);
const hello_entries = (try h.getEntries(testing.allocator, "hello")).?;
defer testing.allocator.free(hello_entries);
try testing.expectEqualDeep(@as([]const Field, &[_]Field{
.{ .name = "hello", .value = "world" },
}), hello_entries);
const foo_entries = (try h.getEntries(testing.allocator, "foo")).?;
defer testing.allocator.free(foo_entries);
try testing.expectEqualDeep(@as([]const Field, &[_]Field{
.{ .name = "foo", .value = "bar" },
.{ .name = "foo", .value = "baz" },
}), foo_entries);
const bar_entries = (try h.getEntries(testing.allocator, "bar")).?;
defer testing.allocator.free(bar_entries);
try testing.expectEqualDeep(@as([]const Field, &[_]Field{
.{ .name = "bar", .value = "world" },
}), bar_entries);
const baz_entries = (try h.getEntries(testing.allocator, "baz")).?;
defer testing.allocator.free(baz_entries);
try testing.expectEqualDeep(@as([]const Field, &[_]Field{
.{ .name = "baz", .value = "hello" },
}), baz_entries);
const pog_entries = (try h.getEntries(testing.allocator, "pog"));
try testing.expectEqual(@as(?[]const Field, null), pog_entries);
try testing.expectEqualStrings("world", h.getFirstValue("hello").?);
try testing.expectEqualStrings("bar", h.getFirstValue("foo").?);
try testing.expectEqualStrings("world", h.getFirstValue("bar").?);
try testing.expectEqualStrings("hello", h.getFirstValue("baz").?);
try testing.expectEqual(@as(?[]const u8, null), h.getFirstValue("pog"));
const hello_values = (try h.getValues(testing.allocator, "hello")).?;
defer testing.allocator.free(hello_values);
try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), hello_values);
const foo_values = (try h.getValues(testing.allocator, "foo")).?;
defer testing.allocator.free(foo_values);
try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{ "bar", "baz" }), foo_values);
const bar_values = (try h.getValues(testing.allocator, "bar")).?;
defer testing.allocator.free(bar_values);
try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"world"}), bar_values);
const baz_values = (try h.getValues(testing.allocator, "baz")).?;
defer testing.allocator.free(baz_values);
try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"hello"}), baz_values);
const pog_values = (try h.getValues(testing.allocator, "pog"));
try testing.expectEqual(@as(?[]const []const u8, null), pog_values);
h.sort();
try testing.expectEqualSlices(usize, &[_]usize{0}, h.getIndices("bar").?);
try testing.expectEqualSlices(usize, &[_]usize{1}, h.getIndices("baz").?);
try testing.expectEqualSlices(usize, &[_]usize{ 2, 3 }, h.getIndices("foo").?);
try testing.expectEqualSlices(usize, &[_]usize{4}, h.getIndices("hello").?);
const formatted_values = try std.fmt.allocPrint(testing.allocator, "{}", .{h});
defer testing.allocator.free(formatted_values);
try testing.expectEqualStrings("bar: world\r\nbaz: hello\r\nfoo: bar\r\nfoo: baz\r\nhello: world\r\n", formatted_values);
var buf: [128]u8 = undefined;
var fbs = std.io.fixedBufferStream(&buf);
const writer = fbs.writer();
try h.formatCommaSeparated("foo", writer);
try testing.expectEqualStrings("foo: bar, baz\r\n", fbs.getWritten());
}
test "Headers.clearRetainingCapacity and clearAndFree" {
var h = Headers.init(std.testing.allocator);
defer h.deinit();
h.clearRetainingCapacity();
try h.append("foo", "bar");
try h.append("bar", "world");
try h.append("foo", "baz");
try h.append("baz", "hello");
try testing.expectEqual(@as(usize, 4), h.list.items.len);
try testing.expectEqual(@as(usize, 3), h.index.count());
const list_capacity = h.list.capacity;
const index_capacity = h.index.capacity();
h.clearRetainingCapacity();
try testing.expectEqual(@as(usize, 0), h.list.items.len);
try testing.expectEqual(@as(usize, 0), h.index.count());
try testing.expectEqual(list_capacity, h.list.capacity);
try testing.expectEqual(index_capacity, h.index.capacity());
try h.append("foo", "bar");
try h.append("bar", "world");
try h.append("foo", "baz");
try h.append("baz", "hello");
try testing.expectEqual(@as(usize, 4), h.list.items.len);
try testing.expectEqual(@as(usize, 3), h.index.count());
try testing.expectEqual(list_capacity, h.list.capacity);
try testing.expectEqual(index_capacity, h.index.capacity());
h.clearAndFree();
try testing.expectEqual(@as(usize, 0), h.list.items.len);
try testing.expectEqual(@as(usize, 0), h.index.count());
try testing.expectEqual(@as(usize, 0), h.list.capacity);
try testing.expectEqual(@as(usize, 0), h.index.capacity());
}