forked from
karlseguin.tngl.sh/http.zig
An HTTP/1.1 server for zig
1const std = @import("std");
2const builtin = @import("builtin");
3const metrics = @import("metrics.zig");
4
5const Allocator = std.mem.Allocator;
6
7const backend_supports_vectors = switch (builtin.zig_backend) {
8 .stage2_llvm, .stage2_c => true,
9 else => false,
10};
11
12pub const Url = struct {
13 raw: []const u8 = "",
14 path: []const u8 = "",
15 query: []const u8 = "",
16
17 pub fn parse(raw: []const u8) Url {
18 var path = raw;
19 var query: []const u8 = "";
20
21 if (std.mem.indexOfScalar(u8, raw, '?')) |index| {
22 path = raw[0..index];
23 query = raw[index + 1 ..];
24 }
25
26 return .{
27 .raw = raw,
28 .path = path,
29 .query = query,
30 };
31 }
32
33 // the special "*" url, which is valid in HTTP OPTIONS request.
34 pub fn star() Url {
35 return .{
36 .raw = "*",
37 .path = "*",
38 .query = "",
39 };
40 }
41
42 pub const UnescapeResult = struct {
43 // Set to the value, whether or not it required unescaped.
44 value: []const u8,
45
46 // true if the value WAS unescaped AND placed in buffer
47 buffered: bool,
48 };
49 // std.Url.unescapeString has 2 problems
50 // First, it doesn't convert '+' -> ' '
51 // Second, it _always_ allocates a new string even if nothing needs to
52 // be unescaped
53 // When we _have_ to unescape a key or value, we'll try to store the new
54 // value in our static buffer (if we have space), else we'll fallback to
55 // allocating memory in the arena.
56 pub fn unescape(allocator: Allocator, buffer: []u8, input: []const u8) !UnescapeResult {
57 var has_plus = false;
58 var unescaped_len = input.len;
59
60 var in_i: usize = 0;
61 while (in_i < input.len) {
62 const b = input[in_i];
63 if (b == '%') {
64 if (in_i + 2 >= input.len or !std.ascii.isHex(input[in_i + 1]) or !std.ascii.isHex(input[in_i + 2])) {
65 return error.InvalidEscapeSequence;
66 }
67 in_i += 3;
68 unescaped_len -= 2;
69 } else if (b == '+') {
70 has_plus = true;
71 in_i += 1;
72 } else {
73 in_i += 1;
74 }
75 }
76
77 // no encoding, and no plus. nothing to unescape
78 if (unescaped_len == input.len and !has_plus) {
79 return .{ .value = input, .buffered = false };
80 }
81
82 var out = buffer;
83 var buffered = true;
84 if (buffer.len < unescaped_len) {
85 out = try allocator.alloc(u8, unescaped_len);
86 metrics.allocUnescape(unescaped_len);
87 buffered = false;
88 }
89
90 in_i = 0;
91 for (0..unescaped_len) |i| {
92 const b = input[in_i];
93 if (b == '%') {
94 out[i] = decodeHex(input[in_i + 1]) << 4 | decodeHex(input[in_i + 2]);
95 in_i += 3;
96 } else if (b == '+') {
97 out[i] = ' ';
98 in_i += 1;
99 } else {
100 out[i] = b;
101 in_i += 1;
102 }
103 }
104
105 return .{ .value = out[0..unescaped_len], .buffered = buffered };
106 }
107
108 pub fn isValid(url: []const u8) bool {
109 var rest = url;
110
111 if (comptime backend_supports_vectors) {
112 if (comptime std.simd.suggestVectorLength(u8)) |vector_len| {
113 while (rest.len >= vector_len) {
114 const block: @Vector(vector_len, u8) = rest[0..vector_len].*;
115 if (@reduce(.Min, block) < 32 or @reduce(.Max, block) > 126) {
116 return false;
117 }
118 rest = rest[vector_len..];
119 }
120 }
121 }
122
123 for (rest) |c| {
124 if (c < 32 or c > 126) {
125 return false;
126 }
127 }
128
129 return true;
130 }
131};
132
133/// converts ascii to unsigned int of appropriate size
134pub fn asUint(comptime string: anytype) std.meta.Int(
135 .unsigned,
136 @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
137) {
138 const byteLength = @sizeOf(@TypeOf(string.*)) - 1;
139 const expectedType = *const [byteLength:0]u8;
140 if (@TypeOf(string) != expectedType) {
141 @compileError("expected : " ++ @typeName(expectedType) ++ ", got: " ++ @typeName(@TypeOf(string)));
142 }
143
144 return @bitCast(@as(*const [byteLength]u8, string).*);
145}
146
147const HEX_DECODE_ARRAY = blk: {
148 var all: ['f' - '0' + 1]u8 = undefined;
149 for ('0'..('9' + 1)) |b| all[b - '0'] = b - '0';
150 for ('A'..('F' + 1)) |b| all[b - '0'] = b - 'A' + 10;
151 for ('a'..('f' + 1)) |b| all[b - '0'] = b - 'a' + 10;
152 break :blk all;
153};
154
155inline fn decodeHex(char: u8) u8 {
156 return @as([*]const u8, @ptrFromInt((@intFromPtr(&HEX_DECODE_ARRAY) - @as(usize, '0'))))[char];
157}
158
159const t = @import("t.zig");
160test "url: parse" {
161 {
162 // absolute root
163 const url = Url.parse("/");
164 try t.expectString("/", url.raw);
165 try t.expectString("/", url.path);
166 try t.expectString("", url.query);
167 }
168
169 {
170 // absolute path
171 const url = Url.parse("/a/bc/def");
172 try t.expectString("/a/bc/def", url.raw);
173 try t.expectString("/a/bc/def", url.path);
174 try t.expectString("", url.query);
175 }
176
177 {
178 // absolute root with query
179 const url = Url.parse("/?over=9000");
180 try t.expectString("/?over=9000", url.raw);
181 try t.expectString("/", url.path);
182 try t.expectString("over=9000", url.query);
183 }
184
185 {
186 // absolute root with empty query
187 const url = Url.parse("/?");
188 try t.expectString("/?", url.raw);
189 try t.expectString("/", url.path);
190 try t.expectString("", url.query);
191 }
192
193 {
194 // absolute path with query
195 const url = Url.parse("/hello/teg?duncan=idaho&ghanima=atreides");
196 try t.expectString("/hello/teg?duncan=idaho&ghanima=atreides", url.raw);
197 try t.expectString("/hello/teg", url.path);
198 try t.expectString("duncan=idaho&ghanima=atreides", url.query);
199 }
200}
201
202test "url: unescape" {
203 var arena = std.heap.ArenaAllocator.init(t.allocator);
204 const allocator = arena.allocator();
205 defer arena.deinit();
206
207 var buffer: [10]u8 = undefined;
208
209 try t.expectError(error.InvalidEscapeSequence, Url.unescape(t.allocator, &buffer, "%"));
210 try t.expectError(error.InvalidEscapeSequence, Url.unescape(t.allocator, &buffer, "%a"));
211 try t.expectError(error.InvalidEscapeSequence, Url.unescape(t.allocator, &buffer, "%1"));
212 try t.expectError(error.InvalidEscapeSequence, Url.unescape(t.allocator, &buffer, "123%45%6"));
213 try t.expectError(error.InvalidEscapeSequence, Url.unescape(t.allocator, &buffer, "%zzzzz"));
214 try t.expectError(error.InvalidEscapeSequence, Url.unescape(t.allocator, &buffer, "%0\xff"));
215
216 var res = try Url.unescape(allocator, &buffer, "a+b");
217 try t.expectString("a b", res.value);
218 try t.expectEqual(true, res.buffered);
219
220 res = try Url.unescape(allocator, &buffer, "a%20b");
221 try t.expectString("a b", res.value);
222 try t.expectEqual(true, res.buffered);
223
224 const input = "%5C%C3%B6%2F%20%C3%A4%C3%B6%C3%9F%20~~.adas-https%3A%2F%2Fcanvas%3A123%2F%23ads%26%26sad";
225 const expected = "\\ö/ äöß ~~.adas-https://canvas:123/#ads&&sad";
226 res = try Url.unescape(allocator, &buffer, input);
227 try t.expectString(expected, res.value);
228 try t.expectEqual(false, res.buffered);
229}
230
231test "url: isValid" {
232 var input: [600]u8 = undefined;
233 for ([_]u8{ ' ', 'a', 'Z', '~' }) |c| {
234 @memset(&input, c);
235 for (0..input.len) |i| {
236 try t.expectEqual(true, Url.isValid(input[0..i]));
237 }
238 }
239
240 var r = t.getRandom();
241 const random = r.random();
242
243 for ([_]u8{ 31, 128, 0, 255 }) |c| {
244 for (1..input.len) |i| {
245 var slice = input[0..i];
246 const idx = random.uintAtMost(usize, slice.len - 1);
247 slice[idx] = c;
248 try t.expectEqual(false, Url.isValid(slice));
249 slice[idx] = 'a'; // revert this index to a valid value
250 }
251 }
252}
253
254test "toUint" {
255 const ASCII_x = @as(u8, @bitCast([1]u8{'x'}));
256 const ASCII_ab = @as(u16, @bitCast([2]u8{ 'a', 'b' }));
257 const ASCII_xyz = @as(u24, @bitCast([3]u8{ 'x', 'y', 'z' }));
258 const ASCII_abcd = @as(u32, @bitCast([4]u8{ 'a', 'b', 'c', 'd' }));
259
260 try t.expectEqual(ASCII_x, asUint("x"));
261 try t.expectEqual(ASCII_ab, asUint("ab"));
262 try t.expectEqual(ASCII_xyz, asUint("xyz"));
263 try t.expectEqual(ASCII_abcd, asUint("abcd"));
264
265 try t.expectEqual(u8, @TypeOf(asUint("x")));
266 try t.expectEqual(u16, @TypeOf(asUint("ab")));
267 try t.expectEqual(u24, @TypeOf(asUint("xyz")));
268 try t.expectEqual(u32, @TypeOf(asUint("abcd")));
269}