瀏覽代碼

Re-commiting whole course squashed into one commit

The history is non-sensical and messy (including every dumb typo)
so I squashed it all to avoid the rabbit hole of thinking how to split
it in a meaningful way.
Alois Mahdal 5 天之前
父節點
當前提交
87b6a069c4
共有 2 個文件被更改,包括 765 次插入15 次删除
  1. 764
    14
      src/main.zig
  2. 1
    1
      your_program.sh

+ 764
- 14
src/main.zig 查看文件

@@ -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
 }

+ 1
- 1
your_program.sh 查看文件

@@ -14,7 +14,7 @@ set -e # Exit early if any commands fail
14 14
 # - Edit .codecrafters/compile.sh to change how your program compiles remotely
15 15
 (
16 16
   cd "$(dirname "$0")" # Ensure compile steps are run within the repository directory
17
-  zig build
17
+  zig build -freference-trace
18 18
 )
19 19
 
20 20
 # Copied from .codecrafters/run.sh