|
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. |