| 
   | 
const std = @import("../std.zig");
const Allocator = std.mem.Allocator;
const testing = std.testing;
const ascii = std.ascii;
const assert = std.debug.assert;
 | 
| HeaderList | pub const HeaderList = std.ArrayListUnmanaged(Field); | 
| HeaderIndexList | pub const HeaderIndexList = std.ArrayListUnmanaged(usize); | 
| HeaderIndex | pub const HeaderIndex = std.HashMapUnmanaged([]const u8, HeaderIndexList, CaseInsensitiveStringContext, std.hash_map.default_max_load_percentage); | 
| CaseInsensitiveStringContext | 
pub const CaseInsensitiveStringContext = struct {
 | 
| hash() | 
    pub fn hash(self: @This(), s: []const u8) u64 {
        _ = self;
        var buf: [64]u8 = undefined;
        var i: usize = 0;
        var h = std.hash.Wyhash.init(0);
        while (i + 64 < s.len) : (i += 64) {
            const ret = ascii.lowerString(buf[0..], s[i..][0..64]);
            h.update(ret);
        }
        const left = @min(64, s.len - i);
        const ret = ascii.lowerString(buf[0..], s[i..][0..left]);
        h.update(ret);
        return h.final();
    }
 | 
| eql() | 
    pub fn eql(self: @This(), a: []const u8, b: []const u8) bool {
        _ = self;
        return ascii.eqlIgnoreCase(a, b);
    }
};
 | 
| Field | 
pub const Field = struct {
    name: []const u8,
    value: []const u8,
    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);
    }
};
 | 
| Headers | 
pub const Headers = struct {
    allocator: Allocator,
    list: HeaderList = .{},
    index: HeaderIndex = .{},
    owned: bool = true,
 | 
| init()When this is false, names and values will not be duplicated. Use with caution. | 
    pub fn init(allocator: Allocator) Headers {
        return .{ .allocator = allocator };
    }
 | 
| initList() | 
    pub fn initList(allocator: Allocator, list: []const Field) !Headers {
        var new = Headers.init(allocator);
        try new.list.ensureTotalCapacity(allocator, list.len);
        try new.index.ensureTotalCapacity(allocator, @intCast(list.len));
        for (list) |field| {
            try new.append(field.name, field.value);
        }
        return new;
    }
 | 
| deinit() | 
    pub fn deinit(headers: *Headers) void {
        headers.deallocateIndexListsAndFields();
        headers.index.deinit(headers.allocator);
        headers.list.deinit(headers.allocator);
        headers.* = undefined;
    }
 | 
| append()Appends a header to the list. Both name and value are copied. | 
    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 std.ascii.allocLowerString(headers.allocator, 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);
    }
 | 
| contains() | 
    pub fn contains(headers: Headers, name: []const u8) bool {
        return headers.index.contains(name);
    }
 | 
| delete()Removes all headers with the given name. | 
    pub fn delete(headers: *Headers, name: []const u8) bool {
        if (headers.index.fetchRemove(name)) |kv| {
            var index = kv.value;
            // iterate backwards
            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)); // ensure the index hasn't been corrupted
                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;
        }
    }
 | 
| firstIndexOf()Returns the index of the first occurrence of a header with the given name. | 
    pub fn firstIndexOf(headers: Headers, name: []const u8) ?usize {
        const index = headers.index.get(name) orelse return null;
        return index.items[0];
    }
 | 
| getIndices()Returns a list of indices containing headers with the given name. | 
    pub fn getIndices(headers: Headers, name: []const u8) ?[]const usize {
        const index = headers.index.get(name) orelse return null;
        return index.items;
    }
 | 
| getFirstEntry()Returns the entry of the first occurrence of a header with the given name. | 
    pub fn getFirstEntry(headers: Headers, name: []const u8) ?Field {
        const first_index = headers.firstIndexOf(name) orelse return null;
        return headers.list.items[first_index];
    }
 | 
| getEntries()Returns a slice containing each header with the given name. The caller owns the returned slice, but NOT the values in the slice. | 
    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;
    }
 | 
| getFirstValue()Returns the value in the entry of the first occurrence of a header with the given name. | 
    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;
    }
 | 
| getValues()Returns a slice containing the value of each header with the given name. The caller owns the returned slice, but NOT the values in the slice. | 
    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 {
        // clear out the indexes
        var it = headers.index.iterator();
        while (it.next()) |entry| {
            entry.value_ptr.shrinkRetainingCapacity(0);
        }
        // fill up indexes again; we know capacity is fine from before
        for (headers.list.items, 0..) |entry, i| {
            headers.index.getEntry(entry.name).?.value_ptr.appendAssumeCapacity(i);
        }
    }
 | 
| sort()Sorts the headers in lexicographical order. | 
    pub fn sort(headers: *Headers) void {
        std.mem.sort(Field, headers.list.items, {}, Field.lessThan);
        headers.rebuildIndex();
    }
 | 
| format()Writes the headers to the given stream. | 
    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");
        }
    }
 | 
| formatCommaSeparated() Writes all of the headers with the given name to the given stream, separated by commas. | 
    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);
            }
        }
    }
 | 
| clearAndFree() Frees all  | 
    pub fn clearAndFree(headers: *Headers) void {
        headers.deallocateIndexListsAndFields();
        headers.index.clearAndFree(headers.allocator);
        headers.list.clearAndFree(headers.allocator);
    }
 | 
| clearRetainingCapacity()Clears the underlying data structures while retaining their capacities. Frees names and values if they are owned. | 
    pub fn clearRetainingCapacity(headers: *Headers) void {
        headers.deallocateIndexListsAndFields();
        headers.index.clearRetainingCapacity();
        headers.list.clearRetainingCapacity();
    }
 | 
| clone() | 
    pub fn clone(headers: Headers, allocator: Allocator) !Headers {
        var new = Headers.init(allocator);
        try new.list.ensureTotalCapacity(allocator, headers.list.capacity);
        try new.index.ensureTotalCapacity(allocator, headers.index.capacity());
        for (headers.list.items) |field| {
            try new.append(field.name, field.value);
        }
        return new;
    }
};
 | 
| Test:Headers.append | 
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 | 
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 | 
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 | 
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());
    // Capacity should still be the same since we shouldn't have needed to grow
    // when adding back the same fields
    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());
}
 | 
| Test:Headers.initList | 
test "Headers.initList" {
    var h = try Headers.initList(std.testing.allocator, &.{
        .{ .name = "Accept-Encoding", .value = "gzip" },
        .{ .name = "Authorization", .value = "it's over 9000!" },
    });
    defer h.deinit();
    const encoding_values = (try h.getValues(testing.allocator, "Accept-Encoding")).?;
    defer testing.allocator.free(encoding_values);
    try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"gzip"}), encoding_values);
    const authorization_values = (try h.getValues(testing.allocator, "Authorization")).?;
    defer testing.allocator.free(authorization_values);
    try testing.expectEqualDeep(@as([]const []const u8, &[_][]const u8{"it's over 9000!"}), authorization_values);
}
 | 
| Generated by zstd-browse2 on 2023-11-04 14:12:33 -0400. |