authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2019-06-27 13:37:40-04:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2019-06-27 13:37:40-04:00
logba29435f67176e2a93609c6bd0ca173315551714
tree025ed5c388299e5b0b3a010c8080eea47da43050
parent1ccf6a2c9e7b42214be07185467e7ae7029a0aa5
parent43d52fa4c5dd3dac36f83c0494286db280d592bc
signaturelock-open Commit is signed but in an unrecognized format.

Merge branch 'http.headers' of https://github.com/daurnimator/zig into daurnimator-http.headers


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

CMakeLists.txt+2
......@@ -525,6 +525,8 @@ set(ZIG_STD_FILES
525525 "hash_map.zig"
526526 "heap.zig"
527527 "heap/logging_allocator.zig"
528 "http.zig"
529 "http/headers.zig"
528530 "io.zig"
529531 "io/c_out_stream.zig"
530532 "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+603
......@@ -0,0 +1,603 @@
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(self: Self, comptime fmt: []const u8, context: var, comptime Errors: type, output: fn (@typeOf(context), []const u8) Errors!void) Errors!void {
361 var it = self.iterator();
362 while (it.next()) |entry| {
363 try output(context, entry.name);
364 try output(context, ": ");
365 try output(context, entry.value);
366 try output(context, "\n");
367 }
368 }
369};
370
371test "Headers.iterator" {
372 var h = Headers.init(debug.global_allocator);
373 defer h.deinit();
374 try h.append("foo", "bar", null);
375 try h.append("cookie", "somevalue", null);
376
377 var count: i32 = 0;
378 var it = h.iterator();
379 while (it.next()) |e| {
380 if (count == 0) {
381 testing.expectEqualSlices(u8, "foo", e.name);
382 testing.expectEqualSlices(u8, "bar", e.value);
383 testing.expectEqual(false, e.never_index);
384 } else if (count == 1) {
385 testing.expectEqualSlices(u8, "cookie", e.name);
386 testing.expectEqualSlices(u8, "somevalue", e.value);
387 testing.expectEqual(true, e.never_index);
388 }
389 count += 1;
390 }
391 testing.expectEqual(i32(2), count);
392}
393
394test "Headers.contains" {
395 var h = Headers.init(debug.global_allocator);
396 defer h.deinit();
397 try h.append("foo", "bar", null);
398 try h.append("cookie", "somevalue", null);
399
400 testing.expectEqual(true, h.contains("foo"));
401 testing.expectEqual(false, h.contains("flooble"));
402}
403
404test "Headers.delete" {
405 var h = Headers.init(debug.global_allocator);
406 defer h.deinit();
407 try h.append("foo", "bar", null);
408 try h.append("baz", "qux", null);
409 try h.append("cookie", "somevalue", null);
410
411 testing.expectEqual(false, h.delete("not-present"));
412 testing.expectEqual(usize(3), h.count());
413
414 testing.expectEqual(true, h.delete("foo"));
415 testing.expectEqual(usize(2), h.count());
416 {
417 const e = h.at(0);
418 testing.expectEqualSlices(u8, "baz", e.name);
419 testing.expectEqualSlices(u8, "qux", e.value);
420 testing.expectEqual(false, e.never_index);
421 }
422 {
423 const e = h.at(1);
424 testing.expectEqualSlices(u8, "cookie", e.name);
425 testing.expectEqualSlices(u8, "somevalue", e.value);
426 testing.expectEqual(true, e.never_index);
427 }
428
429 testing.expectEqual(false, h.delete("foo"));
430}
431
432test "Headers.orderedRemove" {
433 var h = Headers.init(debug.global_allocator);
434 defer h.deinit();
435 try h.append("foo", "bar", null);
436 try h.append("baz", "qux", null);
437 try h.append("cookie", "somevalue", null);
438
439 h.orderedRemove(0);
440 testing.expectEqual(usize(2), h.count());
441 {
442 const e = h.at(0);
443 testing.expectEqualSlices(u8, "baz", e.name);
444 testing.expectEqualSlices(u8, "qux", e.value);
445 testing.expectEqual(false, e.never_index);
446 }
447 {
448 const e = h.at(1);
449 testing.expectEqualSlices(u8, "cookie", e.name);
450 testing.expectEqualSlices(u8, "somevalue", e.value);
451 testing.expectEqual(true, e.never_index);
452 }
453}
454
455test "Headers.swapRemove" {
456 var h = Headers.init(debug.global_allocator);
457 defer h.deinit();
458 try h.append("foo", "bar", null);
459 try h.append("baz", "qux", null);
460 try h.append("cookie", "somevalue", null);
461
462 h.swapRemove(0);
463 testing.expectEqual(usize(2), h.count());
464 {
465 const e = h.at(0);
466 testing.expectEqualSlices(u8, "cookie", e.name);
467 testing.expectEqualSlices(u8, "somevalue", e.value);
468 testing.expectEqual(true, e.never_index);
469 }
470 {
471 const e = h.at(1);
472 testing.expectEqualSlices(u8, "baz", e.name);
473 testing.expectEqualSlices(u8, "qux", e.value);
474 testing.expectEqual(false, e.never_index);
475 }
476}
477
478test "Headers.at" {
479 var h = Headers.init(debug.global_allocator);
480 defer h.deinit();
481 try h.append("foo", "bar", null);
482 try h.append("cookie", "somevalue", null);
483
484 {
485 const e = h.at(0);
486 testing.expectEqualSlices(u8, "foo", e.name);
487 testing.expectEqualSlices(u8, "bar", e.value);
488 testing.expectEqual(false, e.never_index);
489 }
490 {
491 const e = h.at(1);
492 testing.expectEqualSlices(u8, "cookie", e.name);
493 testing.expectEqualSlices(u8, "somevalue", e.value);
494 testing.expectEqual(true, e.never_index);
495 }
496}
497
498test "Headers.getIndices" {
499 var h = Headers.init(debug.global_allocator);
500 defer h.deinit();
501 try h.append("foo", "bar", null);
502 try h.append("set-cookie", "x=1", null);
503 try h.append("set-cookie", "y=2", null);
504
505 testing.expect(null == h.getIndices("not-present"));
506 testing.expectEqualSlices(usize, [_]usize{0}, h.getIndices("foo").?.toSliceConst());
507 testing.expectEqualSlices(usize, [_]usize{ 1, 2 }, h.getIndices("set-cookie").?.toSliceConst());
508}
509
510test "Headers.get" {
511 var h = Headers.init(debug.global_allocator);
512 defer h.deinit();
513 try h.append("foo", "bar", null);
514 try h.append("set-cookie", "x=1", null);
515 try h.append("set-cookie", "y=2", null);
516
517 {
518 const v = try h.get(debug.global_allocator, "not-present");
519 testing.expect(null == v);
520 }
521 {
522 const v = (try h.get(debug.global_allocator, "foo")).?;
523 defer debug.global_allocator.free(v);
524 const e = v[0];
525 testing.expectEqualSlices(u8, "foo", e.name);
526 testing.expectEqualSlices(u8, "bar", e.value);
527 testing.expectEqual(false, e.never_index);
528 }
529 {
530 const v = (try h.get(debug.global_allocator, "set-cookie")).?;
531 defer debug.global_allocator.free(v);
532 {
533 const e = v[0];
534 testing.expectEqualSlices(u8, "set-cookie", e.name);
535 testing.expectEqualSlices(u8, "x=1", e.value);
536 testing.expectEqual(true, e.never_index);
537 }
538 {
539 const e = v[1];
540 testing.expectEqualSlices(u8, "set-cookie", e.name);
541 testing.expectEqualSlices(u8, "y=2", e.value);
542 testing.expectEqual(true, e.never_index);
543 }
544 }
545}
546
547test "Headers.getCommaSeparated" {
548 var h = Headers.init(debug.global_allocator);
549 defer h.deinit();
550 try h.append("foo", "bar", null);
551 try h.append("set-cookie", "x=1", null);
552 try h.append("set-cookie", "y=2", null);
553
554 {
555 const v = try h.getCommaSeparated(debug.global_allocator, "not-present");
556 testing.expect(null == v);
557 }
558 {
559 const v = (try h.getCommaSeparated(debug.global_allocator, "foo")).?;
560 defer debug.global_allocator.free(v);
561 testing.expectEqualSlices(u8, "bar", v);
562 }
563 {
564 const v = (try h.getCommaSeparated(debug.global_allocator, "set-cookie")).?;
565 defer debug.global_allocator.free(v);
566 testing.expectEqualSlices(u8, "x=1,y=2", v);
567 }
568}
569
570test "Headers.sort" {
571 var h = Headers.init(debug.global_allocator);
572 defer h.deinit();
573 try h.append("foo", "bar", null);
574 try h.append("cookie", "somevalue", null);
575
576 h.sort();
577 {
578 const e = h.at(0);
579 testing.expectEqualSlices(u8, "cookie", e.name);
580 testing.expectEqualSlices(u8, "somevalue", e.value);
581 testing.expectEqual(true, e.never_index);
582 }
583 {
584 const e = h.at(1);
585 testing.expectEqualSlices(u8, "foo", e.name);
586 testing.expectEqualSlices(u8, "bar", e.value);
587 testing.expectEqual(false, e.never_index);
588 }
589}
590
591test "Headers.format" {
592 var h = Headers.init(debug.global_allocator);
593 defer h.deinit();
594 try h.append("foo", "bar", null);
595 try h.append("cookie", "somevalue", null);
596
597 var buf: [100]u8 = undefined;
598 testing.expectEqualSlices(u8,
599 \\foo: bar
600 \\cookie: somevalue
601 \\
602 , try std.fmt.bufPrint(buf[0..], "{}", h));
603}
std/std.zig+2
......@@ -37,6 +37,7 @@ pub const fs = @import("fs.zig");
3737pub const hash = @import("hash.zig");
3838pub const hash_map = @import("hash_map.zig");
3939pub const heap = @import("heap.zig");
40pub const http = @import("http.zig");
4041pub const io = @import("io.zig");
4142pub const json = @import("json.zig");
4243pub const lazyInit = @import("lazy_init.zig").lazyInit;
......@@ -89,6 +90,7 @@ test "std" {
8990 _ = @import("fs.zig");
9091 _ = @import("hash.zig");
9192 _ = @import("heap.zig");
93 _ = @import("http.zig");
9294 _ = @import("io.zig");
9395 _ = @import("json.zig");
9496 _ = @import("lazy_init.zig");