From 583ad97633f8d261053d0453c69672d0695280a7 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Wed, 24 Apr 2024 17:29:34 -0600 Subject: [PATCH 1/7] fix: move getHeaderCommon to zap.zig --- src/http.zig | 18 ------------------ src/zap.zig | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/http.zig b/src/http.zig index 8c25558..6c2ef45 100644 --- a/src/http.zig +++ b/src/http.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const fio = @import("fio.zig"); /// HTTP Status codes according to `rfc9110` /// https://datatracker.ietf.org/doc/html/rfc9110#name-status-codes @@ -149,20 +148,3 @@ pub fn methodToEnum(method: ?[]const u8) Method { return Method.UNKNOWN; } } - -/// Registers a new mimetype to be used for files ending with the given extension. -pub fn mimetypeRegister(file_extension: []const u8, mime_type_str: []const u8) void { - // NOTE: facil.io is expecting a non-const pointer to u8 values, but it does not - // not appear to actually modify the value. Here we do a const cast so - // that it is easy to pass static strings to http_mimetype_register without - // needing to allocate a buffer on the heap. - const extension = @constCast(file_extension); - const mimetype = fio.fiobj_str_new(mime_type_str.ptr, mime_type_str.len); - - fio.http_mimetype_register(extension.ptr, extension.len, mimetype); -} - -/// Clears the Mime-Type registry (it will be empty after this call). -pub fn mimetypeClear() void { - fio.http_mimetype_clear(); -} diff --git a/src/zap.zig b/src/zap.zig index b8ab289..695c4b5 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -122,6 +122,23 @@ pub fn startWithLogging(args: fio.fio_start_args) void { fio.fio_start(args); } +/// Registers a new mimetype to be used for files ending with the given extension. +pub fn mimetypeRegister(file_extension: []const u8, mime_type_str: []const u8) void { + // NOTE: facil.io is expecting a non-const pointer to u8 values, but it does not + // not appear to actually modify the value. Here we do a const cast so + // that it is easy to pass static strings to http_mimetype_register without + // needing to allocate a buffer on the heap. + const extension = @constCast(file_extension); + const mimetype = fio.fiobj_str_new(mime_type_str.ptr, mime_type_str.len); + + fio.http_mimetype_register(extension.ptr, extension.len, mimetype); +} + +/// Clears the Mime-Type registry (it will be empty after this call). +pub fn mimetypeClear() void { + fio.http_mimetype_clear(); +} + pub const ListenError = error{ AlreadyListening, ListenError, From 698c06d808a163e532b5a0cbd0b2127424bf3484 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Wed, 24 Apr 2024 18:15:04 -0600 Subject: [PATCH 2/7] feat: add parseAccept --- src/request.zig | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/request.zig b/src/request.zig index 24a02f5..88531f6 100644 --- a/src/request.zig +++ b/src/request.zig @@ -584,6 +584,84 @@ pub fn parseCookies(self: *const Self, url_encoded: bool) void { fio.http_parse_cookies(self.h, if (url_encoded) 1 else 0); } +pub const MIMEType = struct { + type: union(enum) { + glob, + value: []const u8, + type: Type, + }, + subtype: union(enum) { + glob, + value: []const u8, + }, + const Type = enum { + application, + audio, + font, + example, + image, + message, + model, + multipart, + text, + video, + }; +}; + +pub const AcceptItem = struct { + mimetype: MIMEType, + q: f64, +}; + +/// Returns an unsorted list of `AcceptItem`s +pub fn parseAcceptAlloc(self: *const Self, allocator: std.mem.Allocator) !std.ArrayList(AcceptItem) { + const accept_str = self.getHeaderCommon(.accept); + + var list = std.ArrayList(AcceptItem).init(allocator); + + var tok_iter = std.mem.tokenize(u8, accept_str, ", "); + while (tok_iter.next()) |tok| { + var split_iter = std.mem.split(u8, tok, ";"); + const mimetype_str = split_iter.next().?; + const q_factor = q_factor: { + const q_factor_str = split_iter.next() orelse break :q_factor 1; + var eq_iter = std.mem.split(u8, q_factor_str, "="); + const q = eq_iter.next().?; + if (q[0] != 'q') break :q_factor 1; + const value = eq_iter.next() orelse break :q_factor 1; + const parsed = std.fmt.parseFloat(f64, value) catch break :q_factor 1; + break :q_factor parsed; + }; + + var type_split_iter = std.mem.split(u8, mimetype_str, '/'); + + const mimetype_type_str = type_split_iter.next() orelse continue; + const mimetype_subtype_str = type_split_iter.next() orelse continue; + + const mimetype_type = if (std.mem.eql(u8, "*", mimetype_type_str)) + .glob + else if (std.meta.stringToEnum(MIMEType.Type, mimetype_type_str)) |t| + .{ .type = t } + else + .{ .value = mimetype_type_str }; + + const mimetype_subtype = if (std.mem.eql(u8, "*", mimetype_subtype_str)) + .glob + else + .{ .value = mimetype_subtype_str }; + + try list.append(.{ + .mimetype = .{ + .type = mimetype_type, + .subtype = mimetype_subtype, + }, + .q = q_factor, + }); + } + + return list; +} + /// Set a response cookie pub fn setCookie(self: *const Self, args: CookieArgs) HttpError!void { const c: fio.http_cookie_args_s = .{ From 6d646b62d6838fea483b68095c0bf9cd512419a8 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Wed, 24 Apr 2024 19:19:11 -0600 Subject: [PATCH 3/7] feat: make example for parseAccept --- build.zig | 1 + src/request.zig | 108 +++++++++++++++++++++++++++++++----------------- 2 files changed, 71 insertions(+), 38 deletions(-) diff --git a/build.zig b/build.zig index 162f4ac..6bc9520 100644 --- a/build.zig +++ b/build.zig @@ -70,6 +70,7 @@ pub fn build(b: *std.Build) !void { .{ .name = "middleware_with_endpoint", .src = "examples/middleware_with_endpoint/middleware_with_endpoint.zig" }, .{ .name = "senderror", .src = "examples/senderror/senderror.zig" }, .{ .name = "bindataformpost", .src = "examples/bindataformpost/bindataformpost.zig" }, + .{ .name = "accept", .src = "examples/accept/accept.zig" }, }) |excfg| { const ex_name = excfg.name; const ex_src = excfg.src; diff --git a/src/request.zig b/src/request.zig index 88531f6..fe071b2 100644 --- a/src/request.zig +++ b/src/request.zig @@ -6,6 +6,8 @@ const fio = @import("fio.zig"); const util = @import("util.zig"); const zap = @import("zap.zig"); +const ContentType = zap.ContentType; + pub const HttpError = error{ HttpSendBody, HttpSetContentType, @@ -16,15 +18,6 @@ pub const HttpError = error{ SendFile, }; -/// Http Content Type enum. -/// Needs some love. -pub const ContentType = enum { - TEXT, - HTML, - JSON, - // TODO: more content types -}; - /// Key value pair of strings from HTTP parameters pub const HttpParamStrKV = struct { key: util.FreeOrNot, @@ -585,39 +578,77 @@ pub fn parseCookies(self: *const Self, url_encoded: bool) void { } pub const MIMEType = struct { - type: union(enum) { - glob, - value: []const u8, - type: Type, - }, - subtype: union(enum) { + type: Fragment, + subtype: Fragment, + + const Fragment = union(enum) { glob, value: []const u8, - }, - const Type = enum { - application, - audio, - font, - example, - image, - message, - model, - multipart, - text, - video, }; }; pub const AcceptItem = struct { mimetype: MIMEType, q: f64, -}; -/// Returns an unsorted list of `AcceptItem`s -pub fn parseAcceptAlloc(self: *const Self, allocator: std.mem.Allocator) !std.ArrayList(AcceptItem) { - const accept_str = self.getHeaderCommon(.accept); + pub fn lessThan(_: void, lhs: AcceptItem, rhs: AcceptItem) bool { + return lhs.q < rhs.q; + } - var list = std.ArrayList(AcceptItem).init(allocator); + pub fn asCommon(item: AcceptItem) ?Common { + if (item.mimetype.type == .glob) { + if (item.mimetype.subtype == .glob) return .@"*/*"; + return null; + } + if (std.mem.eql(u8, "text", item.mimetype.type.value)) { + if (item.mimetype.subtype == .glob) { + return .@"text/*"; + } else if (std.mem.eql(u8, "html", item.mimetype.subtype.value)) { + return .@"text/html"; + } else if (std.mem.eql(u8, "plain", item.mimetype.subtype.value)) { + return .@"text/plain"; + } + } else if (std.mem.eql(u8, "application", item.mimetype.type.value)) { + if (item.mimetype.subtype == .glob) { + return .@"application/*"; + } else if (std.mem.eql(u8, "xml", item.mimetype.subtype.value)) { + return .@"application/xml"; + } else if (std.mem.eql(u8, "json", item.mimetype.subtype.value)) { + return .@"application/json"; + } else if (std.mem.eql(u8, "xhtml+xml", item.mimetype.subtype.value)) { + return .@"application/xhtml+xml"; + } + } else if (std.mem.eql(u8, "image", item.mimetype.type.value)) { + if (item.mimetype.subtype == .glob) { + return .@"image/*"; + } else if (std.mem.eql(u8, "avif", item.mimetype.subtype.value)) { + return .@"image/avif"; + } else if (std.mem.eql(u8, "webp", item.mimetype.subtype.value)) { + return .@"image/webp"; + } + } + + return null; + } + + const Common = enum { + @"*/*", + @"text/*", + @"text/html", + @"text/plain", + @"application/*", + @"application/xhtml+xml", + @"application/xml", + @"application/json", + @"image/*", + @"image/avif", + @"image/webp", + }; +}; + +/// Parses `Accept:` http header into `list`. +pub fn parseAccept(self: *const Self, list: *std.ArrayList(AcceptItem)) !void { + const accept_str = self.getHeaderCommon(.accept) orelse return error.NoAccept; var tok_iter = std.mem.tokenize(u8, accept_str, ", "); while (tok_iter.next()) |tok| { @@ -633,19 +664,17 @@ pub fn parseAcceptAlloc(self: *const Self, allocator: std.mem.Allocator) !std.Ar break :q_factor parsed; }; - var type_split_iter = std.mem.split(u8, mimetype_str, '/'); + var type_split_iter = std.mem.split(u8, mimetype_str, "/"); const mimetype_type_str = type_split_iter.next() orelse continue; const mimetype_subtype_str = type_split_iter.next() orelse continue; - const mimetype_type = if (std.mem.eql(u8, "*", mimetype_type_str)) + const mimetype_type: MIMEType.Fragment = if (std.mem.eql(u8, "*", mimetype_type_str)) .glob - else if (std.meta.stringToEnum(MIMEType.Type, mimetype_type_str)) |t| - .{ .type = t } else .{ .value = mimetype_type_str }; - const mimetype_subtype = if (std.mem.eql(u8, "*", mimetype_subtype_str)) + const mimetype_subtype: MIMEType.Fragment = if (std.mem.eql(u8, "*", mimetype_subtype_str)) .glob else .{ .value = mimetype_subtype_str }; @@ -658,8 +687,11 @@ pub fn parseAcceptAlloc(self: *const Self, allocator: std.mem.Allocator) !std.Ar .q = q_factor, }); } +} - return list; +/// Sorts list from `parseAccept` +pub fn sortAccept(accept_list: []AcceptItem) void { + std.sort.insertion(AcceptItem, accept_list, {}, AcceptItem.lessThan); } /// Set a response cookie From 4988feb3143c3782a053d382525fba7f23b3cbfb Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Wed, 24 Apr 2024 21:53:41 -0600 Subject: [PATCH 4/7] fix: simplify accept header api somewhat --- examples/accept/accept.zig | 72 +++++++++++++++++++++++++++++ src/request.zig | 94 ++++++++------------------------------ src/zap.zig | 10 ++++ 3 files changed, 102 insertions(+), 74 deletions(-) create mode 100644 examples/accept/accept.zig diff --git a/examples/accept/accept.zig b/examples/accept/accept.zig new file mode 100644 index 0000000..4fe6d26 --- /dev/null +++ b/examples/accept/accept.zig @@ -0,0 +1,72 @@ +const std = @import("std"); +const zap = @import("zap"); + +var gpa = std.heap.GeneralPurposeAllocator(.{ + .thread_safe = true, +}){}; + +fn on_request_verbose(r: zap.Request) void { + const allocator = gpa.allocator(); + const content_type: zap.ContentType = content_type: { + var accept_list = std.ArrayList(zap.Request.AcceptItem).init(allocator); + defer accept_list.deinit(); + r.parseAccept(&accept_list) catch break :content_type .HTML; + for (accept_list.items) |accept| { + break :content_type accept.toContentType() orelse continue; + } + break :content_type .HTML; + }; + + r.setContentType(content_type) catch return; + switch (content_type) { + .TEXT => { + r.sendBody("Hello from ZAP!!!") catch return; + }, + .HTML => { + r.sendBody("

Hello from ZAP!!!

") catch return; + }, + .XML => { + r.sendBody( + \\ + \\ + \\ + \\ Hello from ZAP!!! + \\ + \\ + ) catch return; + }, + .JSON => { + var buffer: [128]u8 = undefined; + const json = zap.stringifyBuf(&buffer, .{ .message = "Hello from ZAP!!!" }, .{}) orelse return; + r.sendJson(json) catch return; + }, + .XHTML => { + r.sendBody( + \\ + \\ + \\ + \\

Hello from ZAP!!!

+ \\ + \\ + ) catch return; + }, + } +} + +pub fn main() !void { + var listener = zap.HttpListener.init(.{ + .port = 3000, + .on_request = on_request_verbose, + .log = true, + .max_clients = 100000, + }); + try listener.listen(); + + std.debug.print("Listening on 0.0.0.0:3000\n", .{}); + + // start worker threads + zap.start(.{ + .threads = 2, + .workers = 2, + }); +} diff --git a/src/request.zig b/src/request.zig index fe071b2..0f97877 100644 --- a/src/request.zig +++ b/src/request.zig @@ -577,76 +577,30 @@ pub fn parseCookies(self: *const Self, url_encoded: bool) void { fio.http_parse_cookies(self.h, if (url_encoded) 1 else 0); } -pub const MIMEType = struct { +pub const AcceptItem = struct { + raw: []const u8, type: Fragment, subtype: Fragment, + q: f64, const Fragment = union(enum) { glob, value: []const u8, }; -}; - -pub const AcceptItem = struct { - mimetype: MIMEType, - q: f64, pub fn lessThan(_: void, lhs: AcceptItem, rhs: AcceptItem) bool { return lhs.q < rhs.q; } - pub fn asCommon(item: AcceptItem) ?Common { - if (item.mimetype.type == .glob) { - if (item.mimetype.subtype == .glob) return .@"*/*"; - return null; + pub fn toContentType(item: AcceptItem) ?ContentType { + if (ContentType.string_map.get(item.raw)) |common| { + return common; } - if (std.mem.eql(u8, "text", item.mimetype.type.value)) { - if (item.mimetype.subtype == .glob) { - return .@"text/*"; - } else if (std.mem.eql(u8, "html", item.mimetype.subtype.value)) { - return .@"text/html"; - } else if (std.mem.eql(u8, "plain", item.mimetype.subtype.value)) { - return .@"text/plain"; - } - } else if (std.mem.eql(u8, "application", item.mimetype.type.value)) { - if (item.mimetype.subtype == .glob) { - return .@"application/*"; - } else if (std.mem.eql(u8, "xml", item.mimetype.subtype.value)) { - return .@"application/xml"; - } else if (std.mem.eql(u8, "json", item.mimetype.subtype.value)) { - return .@"application/json"; - } else if (std.mem.eql(u8, "xhtml+xml", item.mimetype.subtype.value)) { - return .@"application/xhtml+xml"; - } - } else if (std.mem.eql(u8, "image", item.mimetype.type.value)) { - if (item.mimetype.subtype == .glob) { - return .@"image/*"; - } else if (std.mem.eql(u8, "avif", item.mimetype.subtype.value)) { - return .@"image/avif"; - } else if (std.mem.eql(u8, "webp", item.mimetype.subtype.value)) { - return .@"image/webp"; - } - } - return null; } - - const Common = enum { - @"*/*", - @"text/*", - @"text/html", - @"text/plain", - @"application/*", - @"application/xhtml+xml", - @"application/xml", - @"application/json", - @"image/*", - @"image/avif", - @"image/webp", - }; }; -/// Parses `Accept:` http header into `list`. +/// Parses `Accept:` http header into `list`, ordered from highest q factor to lowest pub fn parseAccept(self: *const Self, list: *std.ArrayList(AcceptItem)) !void { const accept_str = self.getHeaderCommon(.accept) orelse return error.NoAccept; @@ -669,31 +623,23 @@ pub fn parseAccept(self: *const Self, list: *std.ArrayList(AcceptItem)) !void { const mimetype_type_str = type_split_iter.next() orelse continue; const mimetype_subtype_str = type_split_iter.next() orelse continue; - const mimetype_type: MIMEType.Fragment = if (std.mem.eql(u8, "*", mimetype_type_str)) - .glob - else - .{ .value = mimetype_type_str }; - - const mimetype_subtype: MIMEType.Fragment = if (std.mem.eql(u8, "*", mimetype_subtype_str)) - .glob - else - .{ .value = mimetype_subtype_str }; - - try list.append(.{ - .mimetype = .{ - .type = mimetype_type, - .subtype = mimetype_subtype, - }, + const new_item: AcceptItem = .{ + .raw = mimetype_str, + .type = if (std.mem.eql(u8, "*", mimetype_type_str)) .glob else .{ .value = mimetype_type_str }, + .subtype = if (std.mem.eql(u8, "*", mimetype_subtype_str)) .glob else .{ .value = mimetype_subtype_str }, .q = q_factor, - }); + }; + for (list.items, 1..) |item, i| { + if (AcceptItem.lessThan({}, new_item, item)) { + try list.insert(i, new_item); + break; + } + } else { + try list.append(new_item); + } } } -/// Sorts list from `parseAccept` -pub fn sortAccept(accept_list: []AcceptItem) void { - std.sort.insertion(AcceptItem, accept_list, {}, AcceptItem.lessThan); -} - /// Set a response cookie pub fn setCookie(self: *const Self, args: CookieArgs) HttpError!void { const c: fio.http_cookie_args_s = .{ diff --git a/src/zap.zig b/src/zap.zig index 695c4b5..d733cd8 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -159,8 +159,18 @@ pub const HttpError = error{ pub const ContentType = enum { TEXT, HTML, + XML, JSON, + XHTML, // TODO: more content types + + pub const string_map = std.ComptimeStringMap(ContentType, .{ + .{ "text/plain", .TEXT }, + .{ "text/html", .HTML }, + .{ "application/xml", .XML }, + .{ "application/json", .JSON }, + .{ "application/xhtml+xml", .XHTML }, + }); }; /// Used internally: facilio Http request callback function type From c79d62ca2c52d58475eaeeb85d2aa8ce5043f3ad Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Sat, 27 Apr 2024 21:29:32 +0200 Subject: [PATCH 5/7] proposed change to parseAccept API --- examples/accept/accept.zig | 10 +++++++--- src/request.zig | 9 ++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/examples/accept/accept.zig b/examples/accept/accept.zig index 4fe6d26..477e973 100644 --- a/examples/accept/accept.zig +++ b/examples/accept/accept.zig @@ -6,11 +6,15 @@ var gpa = std.heap.GeneralPurposeAllocator(.{ }){}; fn on_request_verbose(r: zap.Request) void { - const allocator = gpa.allocator(); + // use a local buffer for the parsed accept headers + var accept_buffer: [1024]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&accept_buffer); + const accept_allocator = fba.allocator(); + const content_type: zap.ContentType = content_type: { - var accept_list = std.ArrayList(zap.Request.AcceptItem).init(allocator); + var accept_list = r.parseAcceptHeaders(accept_allocator) catch break :content_type .HTML; defer accept_list.deinit(); - r.parseAccept(&accept_list) catch break :content_type .HTML; + for (accept_list.items) |accept| { break :content_type accept.toContentType() orelse continue; } diff --git a/src/request.zig b/src/request.zig index 0f97877..c4b9338 100644 --- a/src/request.zig +++ b/src/request.zig @@ -600,8 +600,14 @@ pub const AcceptItem = struct { } }; +/// List holding access headers parsed by parseAcceptHeaders() +const AcceptHeaderList = std.ArrayList(AcceptItem); + /// Parses `Accept:` http header into `list`, ordered from highest q factor to lowest -pub fn parseAccept(self: *const Self, list: *std.ArrayList(AcceptItem)) !void { +pub fn parseAcceptHeaders(self: *const Self, allocator: std.mem.Allocator) !AcceptHeaderList { + var list = AcceptHeaderList.init(allocator); + errdefer list.deinit(); + const accept_str = self.getHeaderCommon(.accept) orelse return error.NoAccept; var tok_iter = std.mem.tokenize(u8, accept_str, ", "); @@ -638,6 +644,7 @@ pub fn parseAccept(self: *const Self, list: *std.ArrayList(AcceptItem)) !void { try list.append(new_item); } } + return list; } /// Set a response cookie From 5e347ce649130a416cd30869d9df121611c61b83 Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Sat, 27 Apr 2024 21:32:14 +0200 Subject: [PATCH 6/7] Proactively up the version number in master for next release --- build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig.zon b/build.zig.zon index a355b3b..950014e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = "zap", - .version = "0.7.0", + .version = "0.7.1", .paths = .{ "build.zig", "build.zig.zon", From 31ba05d6e93c4d5f7c0bc88f98f4323169a77c16 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Sat, 27 Apr 2024 21:09:20 -0600 Subject: [PATCH 7/7] feat: pre-allocate enough space for accept items --- src/request.zig | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/request.zig b/src/request.zig index c4b9338..a209531 100644 --- a/src/request.zig +++ b/src/request.zig @@ -605,10 +605,12 @@ const AcceptHeaderList = std.ArrayList(AcceptItem); /// Parses `Accept:` http header into `list`, ordered from highest q factor to lowest pub fn parseAcceptHeaders(self: *const Self, allocator: std.mem.Allocator) !AcceptHeaderList { - var list = AcceptHeaderList.init(allocator); - errdefer list.deinit(); + const accept_str = self.getHeaderCommon(.accept) orelse return error.NoAcceptHeader; + + const comma_count = std.mem.count(u8, accept_str, ","); - const accept_str = self.getHeaderCommon(.accept) orelse return error.NoAccept; + var list = try AcceptHeaderList.initCapacity(allocator, comma_count + 1); + errdefer list.deinit(); var tok_iter = std.mem.tokenize(u8, accept_str, ", "); while (tok_iter.next()) |tok| { @@ -637,11 +639,11 @@ pub fn parseAcceptHeaders(self: *const Self, allocator: std.mem.Allocator) !Acce }; for (list.items, 1..) |item, i| { if (AcceptItem.lessThan({}, new_item, item)) { - try list.insert(i, new_item); + list.insertAssumeCapacity(i, new_item); break; } } else { - try list.append(new_item); + list.appendAssumeCapacity(new_item); } } return list;