authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2019-06-27 14:07:27-04:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2019-06-27 14:07:27-04:00
log8a251c411dd9347082d315b197a862ba3cfa06ba
treebc549102d65a12bf661c387fe3c911bf098df2cc
parent1ccf6a2c9e7b42214be07185467e7ae7029a0aa5
parent39112d9052fd93210d4a4164adb7b9c93ad1623d
signaturelock-open Commit is signed but in an unrecognized format.

Merge branch 'daurnimator-http.headers'


4 files changed, 619 insertions(+), 0 deletions(-)

CMakeLists.txt+2
...@@ -525,6 +525,8 @@ set(ZIG_STD_FILES...@@ -525,6 +525,8 @@ set(ZIG_STD_FILES
525 "hash_map.zig"525 "hash_map.zig"
526 "heap.zig"526 "heap.zig"
527 "heap/logging_allocator.zig"527 "heap/logging_allocator.zig"
528 "http.zig"
529 "http/headers.zig"
528 "io.zig"530 "io.zig"
529 "io/c_out_stream.zig"531 "io/c_out_stream.zig"
530 "io/seekable_stream.zig"532 "io/seekable_stream.zig"
std/http.zig created+5
...@@ -0,0 +1,5 @@
1test "std.http" {
2 _ = @import("http/headers.zig");
3}
4
5pub const Headers = @import("http/headers.zig").Headers;
std/http/headers.zig created+610
...@@ -0,0 +1,610 @@
1// HTTP Header data structure/type
2// Based on lua-http's http.header module
3//
4// Design criteria:
5// - the same header field is allowed more than once
6// - must be able to fetch separate occurrences (important for some headers e.g. Set-Cookie)
7// - optionally available as comma separated list
8// - http2 adds flag to headers that they should never be indexed
9// - header order should be recoverable
10//
11// Headers are implemented as an array of entries.
12// An index of field name => array indices is kept.
13
14const std = @import("../std.zig");
15const debug = std.debug;
16const assert = debug.assert;
17const testing = std.testing;
18const mem = std.mem;
19const Allocator = mem.Allocator;
20
21fn never_index_default(name: []const u8) bool {
22 if (mem.eql(u8, "authorization", name)) return true;
23 if (mem.eql(u8, "proxy-authorization", name)) return true;
24 if (mem.eql(u8, "cookie", name)) return true;
25 if (mem.eql(u8, "set-cookie", name)) return true;
26 return false;
27}
28
29const HeaderEntry = struct {
30 allocator: *Allocator,
31 pub name: []const u8,
32 pub value: []u8,
33 pub never_index: bool,
34
35 const Self = @This();
36
37 fn init(allocator: *Allocator, name: []const u8, value: []const u8, never_index: ?bool) !Self {
38 return Self{
39 .allocator = allocator,
40 .name = name, // takes reference
41 .value = try mem.dupe(allocator, u8, value),
42 .never_index = never_index orelse never_index_default(name),
43 };
44 }
45
46 fn deinit(self: Self) void {
47 self.allocator.free(self.value);
48 }
49
50 pub fn modify(self: *Self, value: []const u8, never_index: ?bool) !void {
51 const old_len = self.value.len;
52 if (value.len > old_len) {
53 self.value = try self.allocator.realloc(self.value, value.len);
54 } else if (value.len < old_len) {
55 self.value = self.allocator.shrink(self.value, value.len);
56 }
57 mem.copy(u8, self.value, value);
58 self.never_index = never_index orelse never_index_default(self.name);
59 }
60
61 fn compare(a: HeaderEntry, b: HeaderEntry) bool {
62 if (a.name.ptr != b.name.ptr and a.name.len != b.name.len) {
63 // Things beginning with a colon *must* be before others
64 const a_is_colon = a.name[0] == ':';
65 const b_is_colon = b.name[0] == ':';
66 if (a_is_colon and !b_is_colon) {
67 return true;
68 } else if (!a_is_colon and b_is_colon) {
69 return false;
70 }
71
72 // Sort lexicographically on header name
73 return mem.compare(u8, a.name, b.name) == mem.Compare.LessThan;
74 }
75
76 // Sort lexicographically on header value
77 if (!mem.eql(u8, a.value, b.value)) {
78 return mem.compare(u8, a.value, b.value) == mem.Compare.LessThan;
79 }
80
81 // Doesn't matter here; need to pick something for sort consistency
82 return a.never_index;
83 }
84};
85
86test "HeaderEntry" {
87 var e = try HeaderEntry.init(debug.global_allocator, "foo", "bar", null);
88 defer e.deinit();
89 testing.expectEqualSlices(u8, "foo", e.name);
90 testing.expectEqualSlices(u8, "bar", e.value);
91 testing.expectEqual(false, e.never_index);
92
93 try e.modify("longer value", null);
94 testing.expectEqualSlices(u8, "longer value", e.value);
95
96 // shorter value
97 try e.modify("x", null);
98 testing.expectEqualSlices(u8, "x", e.value);
99}
100
101const HeaderList = std.ArrayList(HeaderEntry);
102const HeaderIndexList = std.ArrayList(usize);
103const HeaderIndex = std.AutoHashMap([]const u8, HeaderIndexList);
104
105pub const Headers = struct {
106 // the owned header field name is stored in the index as part of the key
107 allocator: *Allocator,
108 data: HeaderList,
109 index: HeaderIndex,
110
111 const Self = @This();
112
113 pub fn init(allocator: *Allocator) Self {
114 return Self{
115 .allocator = allocator,
116 .data = HeaderList.init(allocator),
117 .index = HeaderIndex.init(allocator),
118 };
119 }
120
121 pub fn deinit(self: Self) void {
122 {
123 var it = self.index.iterator();
124 while (it.next()) |kv| {
125 var dex = &kv.value;
126 dex.deinit();
127 self.allocator.free(kv.key);
128 }
129 self.index.deinit();
130 }
131 {
132 var it = self.data.iterator();
133 while (it.next()) |entry| {
134 entry.deinit();
135 }
136 self.data.deinit();
137 }
138 }
139
140 pub fn clone(self: Self, allocator: *Allocator) !Self {
141 var other = Headers.init(allocator);
142 errdefer other.deinit();
143 try other.data.ensureCapacity(self.data.count());
144 try other.index.initCapacity(self.index.entries.len);
145 var it = self.data.iterator();
146 while (it.next()) |entry| {
147 try other.append(entry.name, entry.value, entry.never_index);
148 }
149 return other;
150 }
151
152 pub fn count(self: Self) usize {
153 return self.data.count();
154 }
155
156 pub const Iterator = HeaderList.Iterator;
157
158 pub fn iterator(self: Self) Iterator {
159 return self.data.iterator();
160 }
161
162 pub fn append(self: *Self, name: []const u8, value: []const u8, never_index: ?bool) !void {
163 const n = self.data.count() + 1;
164 try self.data.ensureCapacity(n);
165 var entry: HeaderEntry = undefined;
166 if (self.index.get(name)) |kv| {
167 entry = try HeaderEntry.init(self.allocator, kv.key, value, never_index);
168 errdefer entry.deinit();
169 var dex = &kv.value;
170 try dex.append(n - 1);
171 } else {
172 const name_dup = try mem.dupe(self.allocator, u8, name);
173 errdefer self.allocator.free(name_dup);
174 entry = try HeaderEntry.init(self.allocator, name_dup, value, never_index);
175 errdefer entry.deinit();
176 var dex = HeaderIndexList.init(self.allocator);
177 try dex.append(n - 1);
178 errdefer dex.deinit();
179 _ = try self.index.put(name, dex);
180 }
181 self.data.appendAssumeCapacity(entry);
182 }
183
184 /// If the header already exists, replace the current value, otherwise append it to the list of headers.
185 /// If the header has multiple entries then returns an error.
186 pub fn upsert(self: *Self, name: []const u8, value: []const u8, never_index: ?bool) !void {
187 if (self.index.get(name)) |kv| {
188 const dex = kv.value;
189 if (dex.count() != 1)
190 return error.CannotUpsertMultiValuedField;
191 var e = &self.data.at(dex.at(0));
192 try e.modify(value, never_index);
193 } else {
194 try self.append(name, value, never_index);
195 }
196 }
197
198 /// Returns boolean indicating if the field is present.
199 pub fn contains(self: Self, name: []const u8) bool {
200 return self.index.contains(name);
201 }
202
203 /// Returns boolean indicating if something was deleted.
204 pub fn delete(self: *Self, name: []const u8) bool {
205 if (self.index.remove(name)) |kv| {
206 var dex = &kv.value;
207 // iterate backwards
208 var i = dex.count();
209 while (i > 0) {
210 i -= 1;
211 const data_index = dex.at(i);
212 const removed = self.data.orderedRemove(data_index);
213 assert(mem.eql(u8, removed.name, name));
214 removed.deinit();
215 }
216 dex.deinit();
217 self.allocator.free(kv.key);
218 self.rebuild_index();
219 return true;
220 } else {
221 return false;
222 }
223 }
224
225 /// Removes the element at the specified index.
226 /// Moves items down to fill the empty space.
227 pub fn orderedRemove(self: *Self, i: usize) void {
228 const removed = self.data.orderedRemove(i);
229 const kv = self.index.get(removed.name).?;
230 var dex = &kv.value;
231 if (dex.count() == 1) {
232 // was last item; delete the index
233 _ = self.index.remove(kv.key);
234 dex.deinit();
235 removed.deinit();
236 self.allocator.free(kv.key);
237 } else {
238 dex.shrink(dex.count() - 1);
239 removed.deinit();
240 }
241 // if it was the last item; no need to rebuild index
242 if (i != self.data.count()) {
243 self.rebuild_index();
244 }
245 }
246
247 /// Removes the element at the specified index.
248 /// The empty slot is filled from the end of the list.
249 pub fn swapRemove(self: *Self, i: usize) void {
250 const removed = self.data.swapRemove(i);
251 const kv = self.index.get(removed.name).?;
252 var dex = &kv.value;
253 if (dex.count() == 1) {
254 // was last item; delete the index
255 _ = self.index.remove(kv.key);
256 dex.deinit();
257 removed.deinit();
258 self.allocator.free(kv.key);
259 } else {
260 dex.shrink(dex.count() - 1);
261 removed.deinit();
262 }
263 // if it was the last item; no need to rebuild index
264 if (i != self.data.count()) {
265 self.rebuild_index();
266 }
267 }
268
269 /// Access the header at the specified index.
270 pub fn at(self: Self, i: usize) HeaderEntry {
271 return self.data.at(i);
272 }
273
274 /// Returns a list of indices containing headers with the given name.
275 /// The returned list should not be modified by the caller.
276 pub fn getIndices(self: Self, name: []const u8) ?HeaderIndexList {
277 if (self.index.get(name)) |kv| {
278 return kv.value;
279 } else {
280 return null;
281 }
282 }
283
284 /// Returns a slice containing each header with the given name.
285 pub fn get(self: Self, allocator: *Allocator, name: []const u8) !?[]const HeaderEntry {
286 const dex = self.getIndices(name) orelse return null;
287
288 const buf = try allocator.alloc(HeaderEntry, dex.count());
289 var it = dex.iterator();
290 var n: usize = 0;
291 while (it.next()) |idx| {
292 buf[n] = self.data.at(idx);
293 n += 1;
294 }
295 return buf;
296 }
297
298 /// Returns all headers with the given name as a comma seperated string.
299 ///
300 /// Useful for HTTP headers that follow RFC-7230 section 3.2.2:
301 /// A recipient MAY combine multiple header fields with the same field
302 /// name into one "field-name: field-value" pair, without changing the
303 /// semantics of the message, by appending each subsequent field value to
304 /// the combined field value in order, separated by a comma. The order
305 /// in which header fields with the same field name are received is
306 /// therefore significant to the interpretation of the combined field
307 /// value
308 pub fn getCommaSeparated(self: Self, allocator: *Allocator, name: []const u8) !?[]u8 {
309 const dex = self.getIndices(name) orelse return null;
310
311 // adapted from mem.join
312 const total_len = blk: {
313 var sum: usize = dex.count() - 1; // space for separator(s)
314 var it = dex.iterator();
315 while (it.next()) |idx|
316 sum += self.data.at(idx).value.len;
317 break :blk sum;
318 };
319
320 const buf = try allocator.alloc(u8, total_len);
321 errdefer allocator.free(buf);
322
323 const first_value = self.data.at(dex.at(0)).value;
324 mem.copy(u8, buf, first_value);
325 var buf_index: usize = first_value.len;
326 for (dex.toSlice()[1..]) |idx| {
327 const value = self.data.at(idx).value;
328 buf[buf_index] = ',';
329 buf_index += 1;
330 mem.copy(u8, buf[buf_index..], value);
331 buf_index += value.len;
332 }
333
334 // No need for shrink since buf is exactly the correct size.
335 return buf;
336 }
337
338 fn rebuild_index(self: *Self) void {
339 { // clear out the indexes
340 var it = self.index.iterator();
341 while (it.next()) |kv| {
342 var dex = &kv.value;
343 dex.len = 0; // keeps capacity available
344 }
345 }
346 { // fill up indexes again; we know capacity is fine from before
347 var it = self.data.iterator();
348 while (it.next()) |entry| {
349 var dex = &self.index.get(entry.name).?.value;
350 dex.appendAssumeCapacity(it.count);
351 }
352 }
353 }
354
355 pub fn sort(self: *Self) void {
356 std.sort.sort(HeaderEntry, self.data.toSlice(), HeaderEntry.compare);
357 self.rebuild_index();
358 }
359
360 pub fn format(
361 self: Self,
362 comptime fmt: []const u8,
363 options: std.fmt.FormatOptions,
364 context: var,
365 comptime Errors: type,
366 output: fn (@typeOf(context), []const u8) Errors!void,
367 ) Errors!void {
368 var it = self.iterator();
369 while (it.next()) |entry| {
370 try output(context, entry.name);
371 try output(context, ": ");
372 try output(context, entry.value);
373 try output(context, "\n");
374 }
375 }
376};
377
378test "Headers.iterator" {
379 var h = Headers.init(debug.global_allocator);
380 defer h.deinit();
381 try h.append("foo", "bar", null);
382 try h.append("cookie", "somevalue", null);
383
384 var count: i32 = 0;
385 var it = h.iterator();
386 while (it.next()) |e| {
387 if (count == 0) {
388 testing.expectEqualSlices(u8, "foo", e.name);
389 testing.expectEqualSlices(u8, "bar", e.value);
390 testing.expectEqual(false, e.never_index);
391 } else if (count == 1) {
392 testing.expectEqualSlices(u8, "cookie", e.name);
393 testing.expectEqualSlices(u8, "somevalue", e.value);
394 testing.expectEqual(true, e.never_index);
395 }
396 count += 1;
397 }
398 testing.expectEqual(i32(2), count);
399}
400
401test "Headers.contains" {
402 var h = Headers.init(debug.global_allocator);
403 defer h.deinit();
404 try h.append("foo", "bar", null);
405 try h.append("cookie", "somevalue", null);
406
407 testing.expectEqual(true, h.contains("foo"));
408 testing.expectEqual(false, h.contains("flooble"));
409}
410
411test "Headers.delete" {
412 var h = Headers.init(debug.global_allocator);
413 defer h.deinit();
414 try h.append("foo", "bar", null);
415 try h.append("baz", "qux", null);
416 try h.append("cookie", "somevalue", null);
417
418 testing.expectEqual(false, h.delete("not-present"));
419 testing.expectEqual(usize(3), h.count());
420
421 testing.expectEqual(true, h.delete("foo"));
422 testing.expectEqual(usize(2), h.count());
423 {
424 const e = h.at(0);
425 testing.expectEqualSlices(u8, "baz", e.name);
426 testing.expectEqualSlices(u8, "qux", e.value);
427 testing.expectEqual(false, e.never_index);
428 }
429 {
430 const e = h.at(1);
431 testing.expectEqualSlices(u8, "cookie", e.name);
432 testing.expectEqualSlices(u8, "somevalue", e.value);
433 testing.expectEqual(true, e.never_index);
434 }
435
436 testing.expectEqual(false, h.delete("foo"));
437}
438
439test "Headers.orderedRemove" {
440 var h = Headers.init(debug.global_allocator);
441 defer h.deinit();
442 try h.append("foo", "bar", null);
443 try h.append("baz", "qux", null);
444 try h.append("cookie", "somevalue", null);
445
446 h.orderedRemove(0);
447 testing.expectEqual(usize(2), h.count());
448 {
449 const e = h.at(0);
450 testing.expectEqualSlices(u8, "baz", e.name);
451 testing.expectEqualSlices(u8, "qux", e.value);
452 testing.expectEqual(false, e.never_index);
453 }
454 {
455 const e = h.at(1);
456 testing.expectEqualSlices(u8, "cookie", e.name);
457 testing.expectEqualSlices(u8, "somevalue", e.value);
458 testing.expectEqual(true, e.never_index);
459 }
460}
461
462test "Headers.swapRemove" {
463 var h = Headers.init(debug.global_allocator);
464 defer h.deinit();
465 try h.append("foo", "bar", null);
466 try h.append("baz", "qux", null);
467 try h.append("cookie", "somevalue", null);
468
469 h.swapRemove(0);
470 testing.expectEqual(usize(2), h.count());
471 {
472 const e = h.at(0);
473 testing.expectEqualSlices(u8, "cookie", e.name);
474 testing.expectEqualSlices(u8, "somevalue", e.value);
475 testing.expectEqual(true, e.never_index);
476 }
477 {
478 const e = h.at(1);
479 testing.expectEqualSlices(u8, "baz", e.name);
480 testing.expectEqualSlices(u8, "qux", e.value);
481 testing.expectEqual(false, e.never_index);
482 }
483}
484
485test "Headers.at" {
486 var h = Headers.init(debug.global_allocator);
487 defer h.deinit();
488 try h.append("foo", "bar", null);
489 try h.append("cookie", "somevalue", null);
490
491 {
492 const e = h.at(0);
493 testing.expectEqualSlices(u8, "foo", e.name);
494 testing.expectEqualSlices(u8, "bar", e.value);
495 testing.expectEqual(false, e.never_index);
496 }
497 {
498 const e = h.at(1);
499 testing.expectEqualSlices(u8, "cookie", e.name);
500 testing.expectEqualSlices(u8, "somevalue", e.value);
501 testing.expectEqual(true, e.never_index);
502 }
503}
504
505test "Headers.getIndices" {
506 var h = Headers.init(debug.global_allocator);
507 defer h.deinit();
508 try h.append("foo", "bar", null);
509 try h.append("set-cookie", "x=1", null);
510 try h.append("set-cookie", "y=2", null);
511
512 testing.expect(null == h.getIndices("not-present"));
513 testing.expectEqualSlices(usize, [_]usize{0}, h.getIndices("foo").?.toSliceConst());
514 testing.expectEqualSlices(usize, [_]usize{ 1, 2 }, h.getIndices("set-cookie").?.toSliceConst());
515}
516
517test "Headers.get" {
518 var h = Headers.init(debug.global_allocator);
519 defer h.deinit();
520 try h.append("foo", "bar", null);
521 try h.append("set-cookie", "x=1", null);
522 try h.append("set-cookie", "y=2", null);
523
524 {
525 const v = try h.get(debug.global_allocator, "not-present");
526 testing.expect(null == v);
527 }
528 {
529 const v = (try h.get(debug.global_allocator, "foo")).?;
530 defer debug.global_allocator.free(v);
531 const e = v[0];
532 testing.expectEqualSlices(u8, "foo", e.name);
533 testing.expectEqualSlices(u8, "bar", e.value);
534 testing.expectEqual(false, e.never_index);
535 }
536 {
537 const v = (try h.get(debug.global_allocator, "set-cookie")).?;
538 defer debug.global_allocator.free(v);
539 {
540 const e = v[0];
541 testing.expectEqualSlices(u8, "set-cookie", e.name);
542 testing.expectEqualSlices(u8, "x=1", e.value);
543 testing.expectEqual(true, e.never_index);
544 }
545 {
546 const e = v[1];
547 testing.expectEqualSlices(u8, "set-cookie", e.name);
548 testing.expectEqualSlices(u8, "y=2", e.value);
549 testing.expectEqual(true, e.never_index);
550 }
551 }
552}
553
554test "Headers.getCommaSeparated" {
555 var h = Headers.init(debug.global_allocator);
556 defer h.deinit();
557 try h.append("foo", "bar", null);
558 try h.append("set-cookie", "x=1", null);
559 try h.append("set-cookie", "y=2", null);
560
561 {
562 const v = try h.getCommaSeparated(debug.global_allocator, "not-present");
563 testing.expect(null == v);
564 }
565 {
566 const v = (try h.getCommaSeparated(debug.global_allocator, "foo")).?;
567 defer debug.global_allocator.free(v);
568 testing.expectEqualSlices(u8, "bar", v);
569 }
570 {
571 const v = (try h.getCommaSeparated(debug.global_allocator, "set-cookie")).?;
572 defer debug.global_allocator.free(v);
573 testing.expectEqualSlices(u8, "x=1,y=2", v);
574 }
575}
576
577test "Headers.sort" {
578 var h = Headers.init(debug.global_allocator);
579 defer h.deinit();
580 try h.append("foo", "bar", null);
581 try h.append("cookie", "somevalue", null);
582
583 h.sort();
584 {
585 const e = h.at(0);
586 testing.expectEqualSlices(u8, "cookie", e.name);
587 testing.expectEqualSlices(u8, "somevalue", e.value);
588 testing.expectEqual(true, e.never_index);
589 }
590 {
591 const e = h.at(1);
592 testing.expectEqualSlices(u8, "foo", e.name);
593 testing.expectEqualSlices(u8, "bar", e.value);
594 testing.expectEqual(false, e.never_index);
595 }
596}
597
598test "Headers.format" {
599 var h = Headers.init(debug.global_allocator);
600 defer h.deinit();
601 try h.append("foo", "bar", null);
602 try h.append("cookie", "somevalue", null);
603
604 var buf: [100]u8 = undefined;
605 testing.expectEqualSlices(u8,
606 \\foo: bar
607 \\cookie: somevalue
608 \\
609 , try std.fmt.bufPrint(buf[0..], "{}", h));
610}
std/std.zig+2
...@@ -37,6 +37,7 @@ pub const fs = @import("fs.zig");...@@ -37,6 +37,7 @@ pub const fs = @import("fs.zig");
37pub const hash = @import("hash.zig");37pub const hash = @import("hash.zig");
38pub const hash_map = @import("hash_map.zig");38pub const hash_map = @import("hash_map.zig");
39pub const heap = @import("heap.zig");39pub const heap = @import("heap.zig");
40pub const http = @import("http.zig");
40pub const io = @import("io.zig");41pub const io = @import("io.zig");
41pub const json = @import("json.zig");42pub const json = @import("json.zig");
42pub const lazyInit = @import("lazy_init.zig").lazyInit;43pub const lazyInit = @import("lazy_init.zig").lazyInit;
...@@ -89,6 +90,7 @@ test "std" {...@@ -89,6 +90,7 @@ test "std" {
89 _ = @import("fs.zig");90 _ = @import("fs.zig");
90 _ = @import("hash.zig");91 _ = @import("hash.zig");
91 _ = @import("heap.zig");92 _ = @import("heap.zig");
93 _ = @import("http.zig");
92 _ = @import("io.zig");94 _ = @import("io.zig");
93 _ = @import("json.zig");95 _ = @import("json.zig");
94 _ = @import("lazy_init.zig");96 _ = @import("lazy_init.zig");