mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 13:54:21 +00:00
438 lines
16 KiB
Zig
438 lines
16 KiB
Zig
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,
|
|
|
|
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 = .{},
|
|
|
|
/// When this is false, names and values will not be duplicated.
|
|
/// Use with caution.
|
|
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;
|
|
}
|
|
|
|
/// 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 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;
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
/// 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];
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
/// 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];
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
}
|
|
|
|
/// Sorts the headers in lexicographical order.
|
|
pub fn sort(headers: *Headers) void {
|
|
std.mem.sort(Field, headers.list.items, {}, Field.lessThan);
|
|
headers.rebuildIndex();
|
|
}
|
|
|
|
/// 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");
|
|
}
|
|
}
|
|
|
|
/// Writes all of the headers with the given name to the given stream, separated by commas.
|
|
///
|
|
/// This is useful for headers like `Set-Cookie` which can have multiple values. RFC 9110, Section 5.2
|
|
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");
|
|
}
|
|
|
|
/// Frees all `HeaderIndexList`s within `index`
|
|
/// Frees names and values of all fields if they are owned.
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Clears and frees the underlying data structures.
|
|
/// Frees names and values if they are owned.
|
|
pub fn clearAndFree(headers: *Headers) void {
|
|
headers.deallocateIndexListsAndFields();
|
|
headers.index.clearAndFree(headers.allocator);
|
|
headers.list.clearAndFree(headers.allocator);
|
|
}
|
|
|
|
/// 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();
|
|
}
|
|
};
|
|
|
|
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());
|
|
// 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());
|
|
}
|