|
@@ -1,19 +1,769 @@
|
1
|
1
|
const std = @import("std");
|
|
2
|
+const LOG = std.log;
|
2
|
3
|
const net = std.net;
|
3
|
4
|
|
|
5
|
+const _KB = 1024;
|
|
6
|
+const _MB = 1024 * _KB;
|
|
7
|
+const THREAD_BUF_SIZE = 3 * _MB;
|
|
8
|
+const MAX_READ_BYTES = 512 * _KB;
|
|
9
|
+
|
|
10
|
+const HELP_TEXT =
|
|
11
|
+ \\usage: codecrafters-http-server [options]
|
|
12
|
+ \\
|
|
13
|
+ \\options:
|
|
14
|
+ \\ --address ADDR Bind to IP address ADDR.
|
|
15
|
+ \\ --port PORT Bind to port PORT.
|
|
16
|
+ \\ --max-threads NUM Don't create more than NUM threads.
|
|
17
|
+ \\ --debug Enable debugging output.
|
|
18
|
+ \\ --verbose Enable verbose output.
|
|
19
|
+ \\ --directory PATH Serve files from PATH directory.
|
|
20
|
+ \\
|
|
21
|
+ \\
|
|
22
|
+;
|
|
23
|
+
|
|
24
|
+const AppError = error{
|
|
25
|
+ UnsupportedMethod,
|
|
26
|
+};
|
|
27
|
+
|
|
28
|
+const ParsingError = error{
|
|
29
|
+ InvalidHeader,
|
|
30
|
+ InvalidRequestLine,
|
|
31
|
+ InvalidRequest,
|
|
32
|
+ OutOfMemory,
|
|
33
|
+};
|
|
34
|
+
|
|
35
|
+const FormattingError = error{
|
|
36
|
+ BufferOverflow,
|
|
37
|
+};
|
|
38
|
+
|
|
39
|
+const RequestMethod = enum {
|
|
40
|
+ GET,
|
|
41
|
+ POST,
|
|
42
|
+};
|
|
43
|
+
|
|
44
|
+const ContentType = enum {
|
|
45
|
+ plain,
|
|
46
|
+ binary,
|
|
47
|
+};
|
|
48
|
+
|
|
49
|
+const ContentEncoding = enum {
|
|
50
|
+ _none,
|
|
51
|
+ gzip,
|
|
52
|
+};
|
|
53
|
+
|
|
54
|
+const Request = struct {
|
|
55
|
+ method: RequestMethod,
|
|
56
|
+ target: []const u8,
|
|
57
|
+ proto_version: []const u8,
|
|
58
|
+ body: []const u8,
|
|
59
|
+ accept_encoding: []ContentEncoding,
|
|
60
|
+ user_agent: []const u8,
|
|
61
|
+};
|
|
62
|
+
|
|
63
|
+const ResponseStatus = enum(u16) {
|
|
64
|
+ OK = 200,
|
|
65
|
+ CREATED = 201,
|
|
66
|
+ BAD = 400,
|
|
67
|
+ NOT_FOUND = 404,
|
|
68
|
+ ERROR = 500,
|
|
69
|
+};
|
|
70
|
+
|
|
71
|
+const Response = struct {
|
|
72
|
+ status: ResponseStatus,
|
|
73
|
+ content: []const u8,
|
|
74
|
+ content_type: ContentType,
|
|
75
|
+ content_encoding: ContentEncoding = ._none,
|
|
76
|
+ content_length: usize,
|
|
77
|
+
|
|
78
|
+ fn _compressed_content(self: @This(), allocator: std.mem.Allocator) ![]const u8 {
|
|
79
|
+ var compressed_al = std.ArrayList(u8).init(allocator);
|
|
80
|
+ var content_fbs = std.io.fixedBufferStream(self.content);
|
|
81
|
+ try std.compress.gzip.compress(content_fbs.reader(), compressed_al.writer(), .{});
|
|
82
|
+ return @as([]const u8, compressed_al.items);
|
|
83
|
+ }
|
|
84
|
+
|
|
85
|
+ fn format(self: @This(), allocator: std.mem.Allocator) ![]u8 {
|
|
86
|
+ var out_al = std.ArrayList(u8).init(allocator);
|
|
87
|
+ try out_al.appendSlice(switch (self.status) {
|
|
88
|
+ .OK => "HTTP/1.1 200 OK\r\n",
|
|
89
|
+ .CREATED => "HTTP/1.1 201 Created\r\n",
|
|
90
|
+ .BAD => "HTTP/1.1 400 Bad Request\r\n",
|
|
91
|
+ .NOT_FOUND => "HTTP/1.1 404 Not Found\r\n",
|
|
92
|
+ .ERROR => "HTTP/1.1 500 Internal Server Error\r\n",
|
|
93
|
+ });
|
|
94
|
+ try out_al.appendSlice(switch (self.content_type) {
|
|
95
|
+ .plain => "Content-Type: text/plain\r\n",
|
|
96
|
+ .binary => "Content-Type: application/octet-stream\r\n",
|
|
97
|
+ });
|
|
98
|
+ const content = switch (self.content_encoding) {
|
|
99
|
+ .gzip => try self._compressed_content(allocator),
|
|
100
|
+ ._none => self.content,
|
|
101
|
+ };
|
|
102
|
+ if (self.content_encoding == .gzip) try out_al.appendSlice("Content-Encoding: gzip\r\n");
|
|
103
|
+ try std.fmt.format(out_al.writer(), "Content-Length: {d}\r\n", .{content.len});
|
|
104
|
+ try out_al.appendSlice("\r\n");
|
|
105
|
+ try out_al.appendSlice(content);
|
|
106
|
+ return out_al.items;
|
|
107
|
+ }
|
|
108
|
+};
|
|
109
|
+
|
|
110
|
+const Header = struct {
|
|
111
|
+ name: []const u8,
|
|
112
|
+ value: []const u8,
|
|
113
|
+};
|
|
114
|
+
|
|
115
|
+fn parse_request(buff: []const u8, logger: ThreadLogger, allocator: std.mem.Allocator) ParsingError!Request {
|
|
116
|
+ var end: usize = 0;
|
|
117
|
+ var pos: usize = 0;
|
|
118
|
+
|
|
119
|
+ // generic checks
|
|
120
|
+ if (!std.mem.containsAtLeast(u8, buff, 2, "\r\n")) return ParsingError.InvalidRequest;
|
|
121
|
+
|
|
122
|
+ // method
|
|
123
|
+ end = std.mem.indexOfPos(u8, buff, pos, " ") orelse return ParsingError.InvalidRequestLine;
|
|
124
|
+ const method: RequestMethod = blk: {
|
|
125
|
+ if (std.mem.eql(u8, buff[pos..end], "GET")) break :blk .GET;
|
|
126
|
+ if (std.mem.eql(u8, buff[pos..end], "POST")) break :blk .POST;
|
|
127
|
+ return ParsingError.InvalidRequestLine;
|
|
128
|
+ };
|
|
129
|
+ pos = end + 1;
|
|
130
|
+
|
|
131
|
+ // target
|
|
132
|
+ end = std.mem.indexOfPos(u8, buff, pos, " ") orelse return ParsingError.InvalidRequestLine;
|
|
133
|
+ const target = buff[pos..end];
|
|
134
|
+ if (target.len < 1) return ParsingError.InvalidRequestLine;
|
|
135
|
+ pos = end + 1;
|
|
136
|
+
|
|
137
|
+ // proto_version
|
|
138
|
+ end = std.mem.indexOfPos(u8, buff, pos, "\r\n") orelse return ParsingError.InvalidRequest;
|
|
139
|
+ const proto_version = buff[pos..end];
|
|
140
|
+ pos = end + 2;
|
|
141
|
+
|
|
142
|
+ // headers
|
|
143
|
+ var accept_encoding_al = std.ArrayList(ContentEncoding).init(allocator);
|
|
144
|
+ var user_agent_al = std.ArrayList(u8).init(allocator);
|
|
145
|
+ var headerline_iter = std.mem.split(u8, buff[pos..], "\r\n");
|
|
146
|
+ var headerline_delim_pos: usize = 0;
|
|
147
|
+ var header_name: []const u8 = undefined;
|
|
148
|
+ var header_value: []const u8 = undefined;
|
|
149
|
+ while (headerline_iter.next()) |headerline| {
|
|
150
|
+ pos = pos + headerline.len + 2;
|
|
151
|
+ if (headerline.len == 0) break;
|
|
152
|
+ headerline_delim_pos = std.mem.indexOf(u8, headerline, ": ") orelse return ParsingError.InvalidHeader;
|
|
153
|
+ header_name = headerline[0..headerline_delim_pos];
|
|
154
|
+ header_value = headerline[headerline_delim_pos + 2 ..];
|
|
155
|
+
|
|
156
|
+ if (eql_i(header_name, "Accept-Encoding")) {
|
|
157
|
+ var ae_iter = std.mem.split(u8, header_value, ", ");
|
|
158
|
+ while (ae_iter.next()) |ae_word| {
|
|
159
|
+ if (eql_i(ae_word, "gzip")) {
|
|
160
|
+ accept_encoding_al.append(.gzip) catch return ParsingError.OutOfMemory;
|
|
161
|
+ } else {
|
|
162
|
+ logger.warn("ignoring unknown Accept-Encoding: {s}", .{ae_word});
|
|
163
|
+ }
|
|
164
|
+ }
|
|
165
|
+ } else if (eql_i(header_name, "User-Agent")) {
|
|
166
|
+ user_agent_al.appendSlice(header_value) catch return ParsingError.OutOfMemory;
|
|
167
|
+ } else {
|
|
168
|
+ logger.warn("ignoring unknown header: {s}", .{header_name});
|
|
169
|
+ }
|
|
170
|
+ }
|
|
171
|
+
|
|
172
|
+ return Request{
|
|
173
|
+ .method = method,
|
|
174
|
+ .target = target,
|
|
175
|
+ .proto_version = proto_version,
|
|
176
|
+ .user_agent = user_agent_al.items,
|
|
177
|
+ .accept_encoding = accept_encoding_al.items,
|
|
178
|
+ .body = buff[pos..],
|
|
179
|
+ };
|
|
180
|
+}
|
|
181
|
+
|
|
182
|
+const Task = struct {
|
|
183
|
+ request: Request,
|
|
184
|
+ app_options: AppOptions,
|
|
185
|
+ allocator: std.mem.Allocator,
|
|
186
|
+ logger: ThreadLogger,
|
|
187
|
+
|
|
188
|
+ fn _create_response(self: @This(), status: ResponseStatus, content: []const u8, content_type: ContentType) !Response {
|
|
189
|
+ const content_encoding: ContentEncoding = blk: {
|
|
190
|
+ if (self.request.accept_encoding.len == 0) break :blk ._none;
|
|
191
|
+ if (content.len == 0) break :blk ._none;
|
|
192
|
+ break :blk self.request.accept_encoding[0];
|
|
193
|
+ };
|
|
194
|
+ return Response{
|
|
195
|
+ .status = status,
|
|
196
|
+ .content = content,
|
|
197
|
+ .content_type = content_type,
|
|
198
|
+ .content_encoding = content_encoding,
|
|
199
|
+ .content_length = content.len,
|
|
200
|
+ };
|
|
201
|
+ }
|
|
202
|
+
|
|
203
|
+ fn _process(self: @This(), endpoint: Endpoint) !Response {
|
|
204
|
+ switch (endpoint) {
|
|
205
|
+ ._not_found => {
|
|
206
|
+ return self._create_response(.NOT_FOUND, "", .plain);
|
|
207
|
+ },
|
|
208
|
+
|
|
209
|
+ ._method_unsupported => {
|
|
210
|
+ return self._create_response(.BAD, "method not supported", .plain);
|
|
211
|
+ },
|
|
212
|
+
|
|
213
|
+ .get_echo_void => {
|
|
214
|
+ return self._create_response(.OK, "", .plain);
|
|
215
|
+ },
|
|
216
|
+
|
|
217
|
+ .get_echo_str => {
|
|
218
|
+ std.debug.assert(std.mem.startsWith(u8, self.request.target, "/echo/"));
|
|
219
|
+ const str: []const u8 = self.request.target[6..];
|
|
220
|
+ return self._create_response(.OK, str, .plain);
|
|
221
|
+ },
|
|
222
|
+
|
|
223
|
+ .get_echo_ua => {
|
|
224
|
+ std.debug.assert(std.mem.eql(u8, self.request.target, "/user-agent"));
|
|
225
|
+ return self._create_response(.OK, self.request.user_agent, .plain);
|
|
226
|
+ },
|
|
227
|
+
|
|
228
|
+ .get_files_path => {
|
|
229
|
+ std.debug.assert(std.mem.startsWith(u8, self.request.target, "/files/"));
|
|
230
|
+ const filename = self.request.target[7..];
|
|
231
|
+ if (contains(filename, "/")) return self._create_response(.BAD, "filename .plain not contain slash", .plain);
|
|
232
|
+ if (self.app_options.directory == null) return self._create_response(.BAD, "storage .plain", .plain);
|
|
233
|
+ const root = try std.fs.openDirAbsolute(self.app_options.directory orelse unreachable, .{});
|
|
234
|
+ const fh = root.openFile(filename, .{ .mode = .read_only }) catch |e| switch (e) {
|
|
235
|
+ error.FileNotFound => return self._create_response(.NOT_FOUND, "", .plain),
|
|
236
|
+ error.AccessDenied => return self._create_response(.NOT_FOUND, "", .plain),
|
|
237
|
+ else => return e,
|
|
238
|
+ };
|
|
239
|
+ const content = try fh.readToEndAlloc(self.allocator, MAX_READ_BYTES);
|
|
240
|
+ return self._create_response(.OK, content, .binary);
|
|
241
|
+ },
|
|
242
|
+
|
|
243
|
+ .post_files_path => {
|
|
244
|
+ std.debug.assert(std.mem.startsWith(u8, self.request.target, "/files/"));
|
|
245
|
+ const filename = self.request.target[7..];
|
|
246
|
+ if (contains(filename, "/")) return self._create_response(.BAD, "filename .plain not contain slash", .plain);
|
|
247
|
+ if (self.app_options.directory == null) return self._create_response(.BAD, "storage disabled", .plain);
|
|
248
|
+ const root = try std.fs.openDirAbsolute(self.app_options.directory orelse unreachable, .{});
|
|
249
|
+ const fh = try root.createFile(filename, .{});
|
|
250
|
+ try fh.writeAll(self.request.body);
|
|
251
|
+ return self._create_response(.CREATED, "", .plain);
|
|
252
|
+ },
|
|
253
|
+
|
|
254
|
+ ._error => {
|
|
255
|
+ return self._create_response(.ERROR, "", .plain);
|
|
256
|
+ },
|
|
257
|
+ }
|
|
258
|
+ }
|
|
259
|
+
|
|
260
|
+ fn respond(self: @This()) Response {
|
|
261
|
+ const endpoint = Endpoint.select(self.request);
|
|
262
|
+ self.logger.debug("Task.respond():selected endpoint: {}", .{endpoint});
|
|
263
|
+ return self._process(endpoint) catch |e| {
|
|
264
|
+ self.logger.err("Endpoint.respond():e={}", .{e});
|
|
265
|
+ return Response{
|
|
266
|
+ .status = .ERROR,
|
|
267
|
+ .content = "error when creating response",
|
|
268
|
+ .content_type = .plain,
|
|
269
|
+ .content_encoding = ._none,
|
|
270
|
+ .content_length = 0,
|
|
271
|
+ };
|
|
272
|
+ };
|
|
273
|
+ }
|
|
274
|
+};
|
|
275
|
+
|
|
276
|
+fn contains(haystack: []const u8, needle: []const u8) bool {
|
|
277
|
+ const idx = std.mem.indexOf(u8, haystack, needle);
|
|
278
|
+ return idx != null;
|
|
279
|
+}
|
|
280
|
+
|
|
281
|
+const Endpoint = union(enum) {
|
|
282
|
+ get_echo_void,
|
|
283
|
+ get_echo_str,
|
|
284
|
+ get_echo_ua,
|
|
285
|
+ get_files_path,
|
|
286
|
+ post_files_path,
|
|
287
|
+ _not_found,
|
|
288
|
+ _method_unsupported,
|
|
289
|
+ _error,
|
|
290
|
+
|
|
291
|
+ fn _match(request: Request, method: RequestMethod, op: enum { eq, sw }, needle: []const u8) bool {
|
|
292
|
+ if (request.method != method) return false;
|
|
293
|
+ return switch (op) {
|
|
294
|
+ .eq => std.mem.eql(u8, request.target, needle),
|
|
295
|
+ .sw => std.mem.startsWith(u8, request.target, needle),
|
|
296
|
+ };
|
|
297
|
+ }
|
|
298
|
+
|
|
299
|
+ fn select(request: Request) Endpoint {
|
|
300
|
+ if (Endpoint._match(request, .GET, .eq, "/")) return .get_echo_void;
|
|
301
|
+ if (Endpoint._match(request, .GET, .sw, "/echo/")) return .get_echo_str;
|
|
302
|
+ if (Endpoint._match(request, .GET, .eq, "/user-agent")) return .get_echo_ua;
|
|
303
|
+ if (Endpoint._match(request, .GET, .sw, "/files/")) return .get_files_path;
|
|
304
|
+ if (Endpoint._match(request, .POST, .sw, "/files/")) return .post_files_path;
|
|
305
|
+ return ._not_found;
|
|
306
|
+ }
|
|
307
|
+};
|
|
308
|
+
|
|
309
|
+// True if ASCII strings *a* and *b* are equal with case-insensitive comparison.
|
|
310
|
+fn eql_i(a: []const u8, b: []const u8) bool {
|
|
311
|
+ if (a.len != b.len) return false;
|
|
312
|
+ for (a, b) |ac, bc| {
|
|
313
|
+ std.debug.assert(std.ascii.isASCII(ac));
|
|
314
|
+ std.debug.assert(std.ascii.isASCII(bc));
|
|
315
|
+ if (ac == bc) continue;
|
|
316
|
+ if (std.ascii.eqlIgnoreCase(a, b)) continue;
|
|
317
|
+ return false;
|
|
318
|
+ }
|
|
319
|
+ return true;
|
|
320
|
+}
|
|
321
|
+
|
|
322
|
+fn append(buff: []u8, pos: usize, val: []const u8) FormattingError!usize {
|
|
323
|
+ if (pos >= buff.len) return FormattingError.BufferOverflow;
|
|
324
|
+ const dest = buff[pos..];
|
|
325
|
+ if (dest.len < val.len) return FormattingError.BufferOverflow;
|
|
326
|
+ std.mem.copyForwards(u8, dest, val);
|
|
327
|
+ return pos + val.len;
|
|
328
|
+}
|
|
329
|
+
|
|
330
|
+fn append_fmt(buff: []u8, pos: usize, comptime fmt: []const u8, args: anytype) FormattingError!usize {
|
|
331
|
+ if (pos >= buff.len) return FormattingError.BufferOverflow;
|
|
332
|
+ if (buff.len < fmt.len) return FormattingError.BufferOverflow;
|
|
333
|
+ const dest = buff[pos..];
|
|
334
|
+ const written = std.fmt.bufPrint(dest, fmt, args) catch |err| switch (err) {
|
|
335
|
+ error.NoSpaceLeft => return FormattingError.BufferOverflow,
|
|
336
|
+ else => unreachable,
|
|
337
|
+ };
|
|
338
|
+ return pos + written.len;
|
|
339
|
+}
|
|
340
|
+
|
|
341
|
+fn upto(buff: []const u8, len: usize) []const u8 {
|
|
342
|
+ if (buff.len <= len) return buff;
|
|
343
|
+ return buff[0..len];
|
|
344
|
+}
|
|
345
|
+
|
|
346
|
+const ThreadLogger = struct {
|
|
347
|
+ conn: std.net.Server.Connection,
|
|
348
|
+ thread_id: std.Thread.Id,
|
|
349
|
+
|
|
350
|
+ fn init(conn: std.net.Server.Connection) ThreadLogger {
|
|
351
|
+ return ThreadLogger{
|
|
352
|
+ .conn = conn,
|
|
353
|
+ .thread_id = std.Thread.getCurrentId(),
|
|
354
|
+ };
|
|
355
|
+ }
|
|
356
|
+
|
|
357
|
+ fn debug(self: @This(), comptime fmt: []const u8, args: anytype) void {
|
|
358
|
+ var arr: [1000]u8 = undefined;
|
|
359
|
+ const msg = std.fmt.bufPrint(@as([]u8, &arr), fmt, args) catch return;
|
|
360
|
+ LOG.debug("{} thread[{}]: {s}", .{ self.conn.address, self.thread_id, msg });
|
|
361
|
+ }
|
|
362
|
+
|
|
363
|
+ fn err(self: @This(), comptime fmt: []const u8, args: anytype) void {
|
|
364
|
+ var arr: [1000]u8 = undefined;
|
|
365
|
+ const msg = std.fmt.bufPrint(@as([]u8, &arr), fmt, args) catch return;
|
|
366
|
+ LOG.err("{} thread[{}]: {s}", .{ self.conn.address, self.thread_id, msg });
|
|
367
|
+ }
|
|
368
|
+
|
|
369
|
+ fn info(self: @This(), comptime fmt: []const u8, args: anytype) void {
|
|
370
|
+ var arr: [1000]u8 = undefined;
|
|
371
|
+ const msg = std.fmt.bufPrint(@as([]u8, &arr), fmt, args) catch return;
|
|
372
|
+ LOG.info("{} thread[{}]: {s}", .{ self.conn.address, self.thread_id, msg });
|
|
373
|
+ }
|
|
374
|
+
|
|
375
|
+ fn warn(self: @This(), comptime fmt: []const u8, args: anytype) void {
|
|
376
|
+ var arr: [1000]u8 = undefined;
|
|
377
|
+ const msg = std.fmt.bufPrint(@as([]u8, &arr), fmt, args) catch return;
|
|
378
|
+ LOG.warn("{} thread[{}]: {s}", .{ self.conn.address, self.thread_id, msg });
|
|
379
|
+ }
|
|
380
|
+};
|
|
381
|
+
|
|
382
|
+fn safe_handle_connection(conn: std.net.Server.Connection, app_options: AppOptions) void {
|
|
383
|
+ const logger = ThreadLogger.init(conn);
|
|
384
|
+ logger.info("safe_handle_connection():processing connection", .{});
|
|
385
|
+ handle_connection(conn, logger, app_options) catch |err| {
|
|
386
|
+ logger.err("safe_handle_connection():aborting thread: processing request failed: {}", .{err});
|
|
387
|
+ return;
|
|
388
|
+ };
|
|
389
|
+ logger.info("safe_handle_connection():done", .{});
|
|
390
|
+}
|
|
391
|
+
|
|
392
|
+/// Helper struct to aid with learning about and debugging ThreadMem
|
|
393
|
+const ThreadMemDebug = struct {
|
|
394
|
+ buff: []u8,
|
|
395
|
+ inner_fba_ptr: *std.heap.FixedBufferAllocator,
|
|
396
|
+ allocator_ptr: *std.mem.Allocator,
|
|
397
|
+ arena_ptr: *std.heap.ArenaAllocator,
|
|
398
|
+ outer_sky: []u8,
|
|
399
|
+ inner_sky: []u8,
|
|
400
|
+
|
|
401
|
+ /// Write to *buff* a unique four-character symbol of *ptr*.
|
|
402
|
+ ///
|
|
403
|
+ /// Value of the symbol bears no useul relation to value or type of the pointer
|
|
404
|
+ /// but different pointers will likely produce different symbol. The symbol is
|
|
405
|
+ /// only useful for visually distinguishing two distinct pointers.
|
|
406
|
+ fn _write_ptr_sym(buff: *[4]u8, ptr: anytype) void {
|
|
407
|
+ buff.*[0] = '_';
|
|
408
|
+ buff.*[1] = '_';
|
|
409
|
+ var desc_buff: [1000]u8 = undefined;
|
|
410
|
+ const desc = std.fmt.bufPrint(&desc_buff, "{*}", .{ptr}) catch return;
|
|
411
|
+ var hash: [16]u8 = undefined;
|
|
412
|
+ std.crypto.hash.Md5.hash(desc, &hash, .{});
|
|
413
|
+ _ = std.fmt.bufPrint(buff, "{x}{x}", .{ hash[0], hash[1] }) catch return;
|
|
414
|
+ }
|
|
415
|
+
|
|
416
|
+ /// Describe pointer *item_ptr* in relation to our main buffer.
|
|
417
|
+ ///
|
|
418
|
+ /// *item_ptr* must be pointer to an object previously allocated using
|
|
419
|
+ /// ThreadMem.allocator.
|
|
420
|
+ ///
|
|
421
|
+ /// Send to debug log a description of main buffer and *item_ptr* prefixed by
|
|
422
|
+ /// simplified information about where the *item_ptr* points within our buffer.
|
|
423
|
+ fn describe_ptr(self: @This(), logger: ThreadLogger, label: []const u8, item_ptr: anytype) void {
|
|
424
|
+ const buff_addr = @intFromPtr(&self.buff);
|
|
425
|
+ var buff_sym: [4]u8 = undefined;
|
|
426
|
+ _write_ptr_sym(&buff_sym, &self.buff);
|
|
427
|
+ var item_sym: [4]u8 = undefined;
|
|
428
|
+ _write_ptr_sym(&item_sym, item_ptr);
|
|
429
|
+ const item_addr = @intFromPtr(item_ptr);
|
|
430
|
+ std.debug.assert(item_addr > buff_addr);
|
|
431
|
+ const item_off = item_addr - buff_addr;
|
|
432
|
+ logger.debug("{s: <50}: {s} + {d: >6} {*}: [...{*}]", .{ label, buff_sym, item_off, &self.buff, item_ptr });
|
|
433
|
+ }
|
|
434
|
+
|
|
435
|
+ fn debug_dump(self: @This(), logger: ThreadLogger) void {
|
|
436
|
+ self.describe_ptr(logger, "ThreadMemDebug.debug_dump():inner_fba_ptr", self.inner_fba_ptr);
|
|
437
|
+ self.describe_ptr(logger, "ThreadMemDebug.debug_dump():arena_ptr", self.arena_ptr);
|
|
438
|
+ self.describe_ptr(logger, "ThreadMemDebug.debug_dump():allocator_ptr", self.allocator_ptr);
|
|
439
|
+ self.describe_ptr(logger, "ThreadMemDebug.debug_dump():&outer_sky", &self.outer_sky);
|
|
440
|
+ self.describe_ptr(logger, "ThreadMemDebug.debug_dump():&inner_sky", &self.inner_sky);
|
|
441
|
+ logger.debug("ThreadMemDebug.debug_dump():inner_fba_ptr.end_index={d}", .{self.inner_fba_ptr.end_index});
|
|
442
|
+ logger.debug("ThreadMemDebug.debug_dump():outer_sky.len={d}", .{self.outer_sky.len});
|
|
443
|
+ logger.debug("ThreadMemDebug.debug_dump():inner_sky.len={d}", .{self.inner_sky.len});
|
|
444
|
+ }
|
|
445
|
+};
|
|
446
|
+
|
|
447
|
+/// Memory management helper which is created using fixed array
|
|
448
|
+/// and contains embedded ArenaAllocator which can be used to further
|
|
449
|
+/// allocate memory from the rest of the array.
|
|
450
|
+///
|
|
451
|
+/// You only need to call .deinit() on ThreadMem in order to release
|
|
452
|
+/// the whole structure.
|
|
453
|
+const ThreadMem = struct {
|
|
454
|
+ allocator: std.mem.Allocator,
|
|
455
|
+ arena: std.heap.ArenaAllocator,
|
|
456
|
+ debug: ThreadMemDebug,
|
|
457
|
+
|
|
458
|
+ /// WARNING: buff must be at least 1024 bytes long.
|
|
459
|
+ fn init(buff: []u8) !ThreadMem {
|
|
460
|
+ std.debug.assert(buff.len > 1024);
|
|
461
|
+ // first, create FBA from buff and allocate memory for all supporting objects
|
|
462
|
+ var outer_fba = std.heap.FixedBufferAllocator.init(buff);
|
|
463
|
+ var arena_ptr = try outer_fba.allocator().create(std.heap.ArenaAllocator);
|
|
464
|
+ const allocator_ptr = try outer_fba.allocator().create(std.mem.Allocator);
|
|
465
|
+ var inner_fba_ptr = try outer_fba.allocator().create(std.heap.FixedBufferAllocator);
|
|
466
|
+ const outer_sky = buff[outer_fba.end_index..];
|
|
467
|
+ // now, ^^ outer_sky is the actual free remaining memory
|
|
468
|
+ inner_fba_ptr.* = std.heap.FixedBufferAllocator.init(outer_sky);
|
|
469
|
+ arena_ptr.* = std.heap.ArenaAllocator.init(inner_fba_ptr.allocator());
|
|
470
|
+ allocator_ptr.* = arena_ptr.allocator();
|
|
471
|
+ const inner_sky = buff[inner_fba_ptr.end_index..];
|
|
472
|
+ return ThreadMem{
|
|
473
|
+ .allocator = allocator_ptr.*,
|
|
474
|
+ .arena = arena_ptr.*,
|
|
475
|
+ .debug = ThreadMemDebug{
|
|
476
|
+ .buff = buff,
|
|
477
|
+ .inner_fba_ptr = inner_fba_ptr,
|
|
478
|
+ .arena_ptr = arena_ptr,
|
|
479
|
+ .allocator_ptr = allocator_ptr,
|
|
480
|
+ .outer_sky = outer_sky,
|
|
481
|
+ .inner_sky = inner_sky,
|
|
482
|
+ },
|
|
483
|
+ };
|
|
484
|
+ }
|
|
485
|
+
|
|
486
|
+ fn deinit(self: @This()) void {
|
|
487
|
+ self.arena.deinit();
|
|
488
|
+ }
|
|
489
|
+};
|
|
490
|
+
|
|
491
|
+fn handle_connection(conn: std.net.Server.Connection, logger: ThreadLogger, app_options: AppOptions) !void {
|
|
492
|
+ var thread_buf: [THREAD_BUF_SIZE]u8 = undefined;
|
|
493
|
+ const mem = try ThreadMem.init(&thread_buf);
|
|
494
|
+ defer mem.deinit();
|
|
495
|
+ mem.debug.debug_dump(logger);
|
|
496
|
+ defer conn.stream.close();
|
|
497
|
+ const request_buff = try mem.allocator.alloc(u8, app_options.request_buff_size);
|
|
498
|
+ const response_buff = try mem.allocator.alloc(u8, app_options.response_buff_size);
|
|
499
|
+ mem.debug.describe_ptr(logger, "handle_connection():&request_buff", &request_buff);
|
|
500
|
+ mem.debug.describe_ptr(logger, "handle_connection():&response_buff", &response_buff);
|
|
501
|
+ var bytes_read: usize = undefined;
|
|
502
|
+ bytes_read = try conn.stream.read(request_buff);
|
|
503
|
+ logger.info("handle_connection():read: {d} bytes", .{bytes_read});
|
|
504
|
+ const request = try parse_request(request_buff[0..bytes_read], logger, mem.allocator);
|
|
505
|
+ logger.info("handle_connection():parsed request: proto_version={s}, method={}, target={s}, user_agent={s}, accept_encoding={any}, body.len={d}", .{
|
|
506
|
+ request.proto_version,
|
|
507
|
+ request.method,
|
|
508
|
+ request.target,
|
|
509
|
+ request.user_agent,
|
|
510
|
+ request.accept_encoding,
|
|
511
|
+ request.body.len,
|
|
512
|
+ });
|
|
513
|
+ const task = Task{
|
|
514
|
+ .request = request,
|
|
515
|
+ .allocator = mem.allocator,
|
|
516
|
+ .app_options = app_options,
|
|
517
|
+ .logger = logger,
|
|
518
|
+ };
|
|
519
|
+ const response = task.respond();
|
|
520
|
+ logger.debug("handle_connection():formed response: status={}, content[0..32]=|{s}|", .{ response.status, upto(response.content, 32) });
|
|
521
|
+ // mem.debug.describe_ptr(logger, "handle_connection():response.headers", &response.headers);
|
|
522
|
+ const response_bytes = try response.format(mem.allocator);
|
|
523
|
+ logger.info("handle_connection():sending response: {d} bytes", .{response_bytes.len});
|
|
524
|
+ try conn.stream.writeAll(response_bytes);
|
|
525
|
+ logger.info("handle_connection():wrote response to stream: {*}", .{&conn.stream});
|
|
526
|
+}
|
|
527
|
+
|
|
528
|
+const ArgParsingResult = union(enum) {
|
|
529
|
+ options: AppOptions,
|
|
530
|
+ usage_error: []const u8,
|
|
531
|
+
|
|
532
|
+ fn unknown_arg(arg: []const u8) ArgParsingResult {
|
|
533
|
+ var arr: [100]u8 = undefined;
|
|
534
|
+ const msg = std.fmt.bufPrint(@as([]u8, &arr), "unknown argument: {s}", .{arg}) catch "unknown argument";
|
|
535
|
+ return ArgParsingResult{ .usage_error = msg };
|
|
536
|
+ }
|
|
537
|
+};
|
|
538
|
+
|
|
539
|
+const AppOptions = struct {
|
|
540
|
+ directory: ?[]const u8,
|
|
541
|
+ address: []const u8,
|
|
542
|
+ port: u16,
|
|
543
|
+ debug: bool,
|
|
544
|
+ verbose: bool,
|
|
545
|
+ response_buff_size: usize,
|
|
546
|
+ request_buff_size: usize,
|
|
547
|
+ max_threads: usize,
|
|
548
|
+
|
|
549
|
+ fn init() AppOptions {
|
|
550
|
+ const parsing_result = AppOptions.parse_args();
|
|
551
|
+ switch (parsing_result) {
|
|
552
|
+ .options => return parsing_result.options,
|
|
553
|
+ .usage_error => {
|
|
554
|
+ _ = std.io.getStdErr().write(HELP_TEXT) catch {};
|
|
555
|
+ _ = std.io.getStdErr().write(parsing_result.usage_error) catch {};
|
|
556
|
+ _ = std.io.getStdErr().write("\n") catch {};
|
|
557
|
+ std.process.exit(2);
|
|
558
|
+ },
|
|
559
|
+ }
|
|
560
|
+ }
|
|
561
|
+
|
|
562
|
+ fn parse_args() ArgParsingResult {
|
|
563
|
+ var options = AppOptions{
|
|
564
|
+ .debug = false,
|
|
565
|
+ .directory = null,
|
|
566
|
+ .address = "127.0.0.1",
|
|
567
|
+ .port = 4221,
|
|
568
|
+ .verbose = false,
|
|
569
|
+ .request_buff_size = 1024 * 1024,
|
|
570
|
+ .response_buff_size = 1024 * 1024,
|
|
571
|
+ .max_threads = std.Thread.getCpuCount() catch 1,
|
|
572
|
+ };
|
|
573
|
+ var this: []const u8 = undefined;
|
|
574
|
+ var it = std.process.args();
|
|
575
|
+ var param: ?[]const u8 = null;
|
|
576
|
+ _ = it.next(); // drop program name
|
|
577
|
+ while (true) {
|
|
578
|
+ this = @as(?[]const u8, it.next()) orelse break;
|
|
579
|
+ if (std.mem.eql(u8, this, "--directory")) {
|
|
580
|
+ param = @as(?[]const u8, it.next());
|
|
581
|
+ options.directory = param orelse return ArgParsingResult{ .usage_error = "no DIRECTORY?" };
|
|
582
|
+ } else if (std.mem.eql(u8, this, "--address")) {
|
|
583
|
+ param = @as(?[]const u8, it.next());
|
|
584
|
+ options.address = param orelse return ArgParsingResult{ .usage_error = "no ADDRESS?" };
|
|
585
|
+ } else if (std.mem.eql(u8, this, "--port")) {
|
|
586
|
+ param = @as(?[]const u8, it.next());
|
|
587
|
+ options.port = std.fmt.parseInt(u16, param orelse return ArgParsingResult{ .usage_error = "no PORT?" }, 10) catch {
|
|
588
|
+ return ArgParsingResult{ .usage_error = "PORT must be integer" };
|
|
589
|
+ };
|
|
590
|
+ } else if (std.mem.eql(u8, this, "--max-threads")) {
|
|
591
|
+ param = @as(?[]const u8, it.next());
|
|
592
|
+ options.max_threads = std.fmt.parseInt(usize, param orelse return ArgParsingResult{ .usage_error = "no NUM?" }, 10) catch {
|
|
593
|
+ return ArgParsingResult{ .usage_error = "NUM must be integer" };
|
|
594
|
+ };
|
|
595
|
+ } else if (std.mem.eql(u8, this, "--debug")) {
|
|
596
|
+ options.debug = true;
|
|
597
|
+ } else if (std.mem.eql(u8, this, "--verbose")) {
|
|
598
|
+ options.verbose = true;
|
|
599
|
+ } else {
|
|
600
|
+ return ArgParsingResult.unknown_arg(this);
|
|
601
|
+ }
|
|
602
|
+ }
|
|
603
|
+ return ArgParsingResult{ .options = options };
|
|
604
|
+ }
|
|
605
|
+};
|
|
606
|
+
|
4
|
607
|
pub fn main() !void {
|
5
|
|
- const stdout = std.io.getStdOut().writer();
|
6
|
|
-
|
7
|
|
- // You can use print statements as follows for debugging, they'll be visible when running tests.
|
8
|
|
- try stdout.print("Logs from your program will appear here!\n", .{});
|
9
|
|
-
|
10
|
|
- // Uncomment this block to pass the first stage
|
11
|
|
- // const address = try net.Address.resolveIp("127.0.0.1", 4221);
|
12
|
|
- // var listener = try address.listen(.{
|
13
|
|
- // .reuse_address = true,
|
14
|
|
- // });
|
15
|
|
- // defer listener.deinit();
|
16
|
|
- //
|
17
|
|
- // _ = try listener.accept();
|
18
|
|
- // try stdout.print("client connected!", .{});
|
|
608
|
+ const app_options = AppOptions.init();
|
|
609
|
+ LOG.info("app_options.address: {str}", .{app_options.address});
|
|
610
|
+ LOG.info("app_options.port: {d}", .{app_options.port});
|
|
611
|
+ LOG.info("app_options.directory: {any}", .{app_options.directory});
|
|
612
|
+ LOG.info("app_options.debug: {}", .{app_options.debug});
|
|
613
|
+ LOG.info("app_options.verbose: {}", .{app_options.verbose});
|
|
614
|
+ LOG.info("app_options.request_buff_size: {d}", .{app_options.request_buff_size});
|
|
615
|
+ LOG.info("app_options.response_buff_size: {d}", .{app_options.response_buff_size});
|
|
616
|
+ LOG.info("app_options.max_threads: {d}", .{app_options.max_threads});
|
|
617
|
+
|
|
618
|
+ // Server
|
|
619
|
+ const address = try net.Address.resolveIp(app_options.address, @as(u16, app_options.port));
|
|
620
|
+ var listener = try address.listen(.{
|
|
621
|
+ .reuse_address = true,
|
|
622
|
+ });
|
|
623
|
+ defer listener.deinit();
|
|
624
|
+ LOG.info("listening: {}", .{listener.listen_address});
|
|
625
|
+
|
|
626
|
+ // Worker thread pool
|
|
627
|
+ var pool: std.Thread.Pool = undefined;
|
|
628
|
+ try pool.init(std.Thread.Pool.Options{
|
|
629
|
+ .allocator = std.heap.page_allocator,
|
|
630
|
+ .n_jobs = @intCast(app_options.max_threads),
|
|
631
|
+ });
|
|
632
|
+
|
|
633
|
+ // Connections
|
|
634
|
+ var conn: net.Server.Connection = undefined;
|
|
635
|
+ while (true) {
|
|
636
|
+ conn = try listener.accept();
|
|
637
|
+ LOG.info("{any}: accepted connection", .{conn.address});
|
|
638
|
+ try pool.spawn(safe_handle_connection, .{ conn, app_options });
|
|
639
|
+ }
|
|
640
|
+}
|
|
641
|
+
|
|
642
|
+test parse_request {
|
|
643
|
+ var poor_buf: [10]u8 = undefined;
|
|
644
|
+ var tiny_buf: [100]u8 = undefined;
|
|
645
|
+ var rich_buf: [10_000]u8 = undefined;
|
|
646
|
+ var rich_fba = std.heap.FixedBufferAllocator.init(&rich_buf);
|
|
647
|
+ var poor_fba = std.heap.FixedBufferAllocator.init(&poor_buf);
|
|
648
|
+ var tiny_fba = std.heap.FixedBufferAllocator.init(&tiny_buf);
|
|
649
|
+ const poor_allocator = poor_fba.allocator();
|
|
650
|
+ const tiny_allocator = tiny_fba.allocator();
|
|
651
|
+ const rich_allocator = rich_fba.allocator();
|
|
652
|
+
|
|
653
|
+ // bad request
|
|
654
|
+ try std.testing.expectEqual(ParsingError.InvalidRequest, parse_request("", rich_allocator));
|
|
655
|
+ try std.testing.expectEqual(ParsingError.InvalidRequest, parse_request(" ", rich_allocator));
|
|
656
|
+ try std.testing.expectEqual(ParsingError.InvalidRequest, parse_request("\r\n", rich_allocator));
|
|
657
|
+ try std.testing.expectEqual(ParsingError.InvalidRequest, parse_request("\n", rich_allocator));
|
|
658
|
+ try std.testing.expectEqual(ParsingError.InvalidRequest, parse_request("GET / HTTP/1.1\n\n", rich_allocator));
|
|
659
|
+ try std.testing.expectEqual(ParsingError.InvalidRequest, parse_request(" ", rich_allocator));
|
|
660
|
+ try std.testing.expectEqual(ParsingError.InvalidRequest, parse_request("\r\n", rich_allocator));
|
|
661
|
+ try std.testing.expectEqual(ParsingError.InvalidRequest, parse_request("\n", rich_allocator));
|
|
662
|
+
|
|
663
|
+ // bad request line
|
|
664
|
+ try std.testing.expectEqual(ParsingError.InvalidRequestLine, parse_request("GET / HTTP/1.1\r\n\r\n", rich_allocator));
|
|
665
|
+ try std.testing.expectEqual(ParsingError.InvalidRequestLine, parse_request("GET HTTP/1.1\r\n\r\n", rich_allocator));
|
|
666
|
+ try std.testing.expectEqual(ParsingError.InvalidRequestLine, parse_request("GET HTTP/1.1\r\n\r\n", rich_allocator));
|
|
667
|
+ try std.testing.expectEqual(ParsingError.InvalidRequestLine, parse_request("GET\r\n\r\n", rich_allocator));
|
|
668
|
+ try std.testing.expectEqual(ParsingError.InvalidRequestLine, parse_request("\r\n\r\n", rich_allocator));
|
|
669
|
+
|
|
670
|
+ const poor_request = ("GET / HTTP/1.1\r\n" ++ "\r\n");
|
|
671
|
+ const tiny_request = ("GET / HTTP/1.1\r\n" ++ "Foo: bar\r\n" ++ "\r\n");
|
|
672
|
+ const rich_request = ("GET /hey HTTP/1.1\r\n" ++ "Jane: 3\r\n" ++ "Joe: 42\r\n" ++ "\r\n" ++ "abc");
|
|
673
|
+
|
|
674
|
+ // out of memory
|
|
675
|
+ try std.testing.expectEqual(ParsingError.OutOfMemory, parse_request(tiny_request, poor_allocator));
|
|
676
|
+ try std.testing.expectEqual(ParsingError.OutOfMemory, parse_request(rich_request, tiny_allocator));
|
|
677
|
+
|
|
678
|
+ // small memory, comply
|
|
679
|
+ const poor_result = try parse_request(poor_request, poor_allocator);
|
|
680
|
+ try std.testing.expectEqual(RequestMethod.GET, poor_result.method);
|
|
681
|
+ try std.testing.expectEqualSlices(u8, "HTTP/1.1", poor_result.proto_version);
|
|
682
|
+ try std.testing.expectEqualSlices(u8, "/", poor_result.target);
|
|
683
|
+ try std.testing.expectEqual(0, poor_result.headers.len);
|
|
684
|
+ try std.testing.expectEqualSlices(u8, "", poor_result.body);
|
|
685
|
+
|
|
686
|
+ // correct request, tiny one
|
|
687
|
+ const tiny_result = try parse_request(tiny_request, rich_allocator);
|
|
688
|
+ try std.testing.expectEqual(RequestMethod.GET, tiny_result.method);
|
|
689
|
+ try std.testing.expectEqualSlices(u8, "/", tiny_result.target);
|
|
690
|
+ try std.testing.expectEqualSlices(u8, "HTTP/1.1", tiny_result.proto_version);
|
|
691
|
+ try std.testing.expectEqual(1, tiny_result.headers.len);
|
|
692
|
+ try std.testing.expectEqualDeep(Header{ .name = "Foo", .value = "bar" }, tiny_result.headers[0]);
|
|
693
|
+ try std.testing.expectEqualSlices(u8, "", tiny_result.body);
|
|
694
|
+
|
|
695
|
+ // correct request
|
|
696
|
+ const rich_result = try parse_request(rich_request, rich_allocator);
|
|
697
|
+ try std.testing.expectEqual(RequestMethod.GET, rich_result.method);
|
|
698
|
+ try std.testing.expectEqualSlices(u8, "/hey", rich_result.target);
|
|
699
|
+ try std.testing.expectEqualSlices(u8, "HTTP/1.1", rich_result.proto_version);
|
|
700
|
+ try std.testing.expectEqual(2, rich_result.headers.len);
|
|
701
|
+ try std.testing.expectEqualDeep(Header{ .name = "Jane", .value = "3" }, rich_result.headers[0]);
|
|
702
|
+ try std.testing.expectEqualDeep(Header{ .name = "Joe", .value = "42" }, rich_result.headers[1]);
|
|
703
|
+ try std.testing.expectEqualSlices(u8, "abc", rich_result.body);
|
|
704
|
+ try std.testing.expectEqual(3, rich_result.body.len);
|
|
705
|
+}
|
|
706
|
+
|
|
707
|
+test append {
|
|
708
|
+ const buff = try std.testing.allocator.alloc(u8, 8);
|
|
709
|
+ defer std.testing.allocator.free(buff);
|
|
710
|
+
|
|
711
|
+ // empty buff
|
|
712
|
+ try std.testing.expectEqual(FormattingError.BufferOverflow, append(buff[0..0], 0, "foo"));
|
|
713
|
+
|
|
714
|
+ // small buff: value is longer
|
|
715
|
+ try std.testing.expectEqual(FormattingError.BufferOverflow, append(buff[0..7], 0, "foo-bar-baz"));
|
|
716
|
+
|
|
717
|
+ // small buff: pos is outside
|
|
718
|
+ try std.testing.expectEqual(FormattingError.BufferOverflow, append(buff, 8, "foo"));
|
|
719
|
+
|
|
720
|
+ // small buff: pos is too far
|
|
721
|
+ try std.testing.expectEqual(FormattingError.BufferOverflow, append(buff, 7, "foo"));
|
|
722
|
+
|
|
723
|
+ // bad value: empty but outside
|
|
724
|
+ try std.testing.expectEqual(FormattingError.BufferOverflow, append(buff, 8, ""));
|
|
725
|
+
|
|
726
|
+ // ok: empty left
|
|
727
|
+ try std.testing.expectEqual(0, append(buff, 0, ""));
|
|
728
|
+
|
|
729
|
+ // ok: empty right
|
|
730
|
+ try std.testing.expectEqual(7, append(buff, 7, ""));
|
|
731
|
+
|
|
732
|
+ // ok: aligned left
|
|
733
|
+ try std.testing.expectEqual(3, append(buff, 0, "foo"));
|
|
734
|
+
|
|
735
|
+ // ok: aligned right
|
|
736
|
+ try std.testing.expectEqual(7, append(buff, 4, "foo"));
|
|
737
|
+}
|
|
738
|
+
|
|
739
|
+test append_fmt {
|
|
740
|
+ const buff = try std.testing.allocator.alloc(u8, 8);
|
|
741
|
+ defer std.testing.allocator.free(buff);
|
|
742
|
+
|
|
743
|
+ // empty buff
|
|
744
|
+ try std.testing.expectEqual(FormattingError.BufferOverflow, append_fmt(buff[0..0], 0, "foo", .{}));
|
|
745
|
+
|
|
746
|
+ // small buff: value is longer
|
|
747
|
+ try std.testing.expectEqual(FormattingError.BufferOverflow, append_fmt(buff[0..7], 0, "foo-{s}", .{"bar-baz"}));
|
|
748
|
+
|
|
749
|
+ // small buff: pos is outside
|
|
750
|
+ try std.testing.expectEqual(FormattingError.BufferOverflow, append_fmt(buff, 8, "foo", .{}));
|
|
751
|
+
|
|
752
|
+ // small buff: pos is too far (empty fmt)
|
|
753
|
+ try std.testing.expectEqual(FormattingError.BufferOverflow, append_fmt(buff, 7, "foo", .{}));
|
|
754
|
+
|
|
755
|
+ // small buff: pos is too far (expanded fmt)
|
|
756
|
+ try std.testing.expectEqual(FormattingError.BufferOverflow, append_fmt(buff, 2, "foo-{s}", .{"bar"}));
|
|
757
|
+
|
|
758
|
+ // ok: empty left
|
|
759
|
+ try std.testing.expectEqual(0, append_fmt(buff, 0, "", .{}));
|
|
760
|
+
|
|
761
|
+ // ok: empty right
|
|
762
|
+ try std.testing.expectEqual(7, append_fmt(buff, 7, "", .{}));
|
|
763
|
+
|
|
764
|
+ // ok: aligned left
|
|
765
|
+ try std.testing.expectEqual(5, append_fmt(buff, 0, "foo-{d}", .{1}));
|
|
766
|
+
|
|
767
|
+ // ok: aligned right
|
|
768
|
+ try std.testing.expectEqual(7, append_fmt(buff, 2, "foo-{d}", .{1}));
|
19
|
769
|
}
|