web: some stuff

This commit is contained in:
2026-03-07 21:08:22 +01:00
parent f02ece22fa
commit 66d49ea8d5
14 changed files with 38964 additions and 101 deletions

View File

@@ -1,24 +1,23 @@
const std = @import("std");
const Parser = @This();
const Callbacks = struct {
self: ?*anyopaque = null,
route: ?*const fn (self: ?*anyopaque, route: Route) void = null,
header: ?*const fn (self: ?*anyopaque, name: []const u8, value: []const u8) void = null,
body: ?*const fn (self: ?*anyopaque, body: []const u8) void = null,
pub const null_callbacks: Callbacks = .{};
};
const Header = @import("Header.zig");
const Method = @import("Method.zig").Method;
const Response = @import("../Response.zig");
const RequestHandler = @import("../RequestHandler.zig");
const RequestRouter = @import("../RequestRouter.zig");
const Error = error{
MethodNotSupported,
HttpVersionNotSupported,
MissingLineFeed,
InvalidContentLength,
RouterError,
HandlerError,
};
const Vec = @Vector(32, u8);
const Vec: type = @Vector(std.simd.suggestVectorLength(u8).?, u8);
const vec_len = @typeInfo(Vec).vector.len;
const Pattern = struct {
value: Vec,
@@ -26,13 +25,13 @@ const Pattern = struct {
len: u6,
pub fn init(comptime prefix: []const u8) Pattern {
if (prefix.len > 32) {
if (prefix.len > vec_len) {
@compileError("Prefix length is too high");
}
var value: [32]u8 = undefined;
var mask: [32]u8 = undefined;
for (0..32) |i| {
var value: [vec_len]u8 = undefined;
var mask: [vec_len]u8 = undefined;
for (0..vec_len) |i| {
if (i < prefix.len) {
value[i] = prefix[i];
mask[i] = 0xFF;
@@ -49,14 +48,24 @@ const Pattern = struct {
};
const patterns = struct {
method_delete: Pattern.init("DELETE "),
method_get: Pattern.init("GET "),
method_head: Pattern.init("HEAD "),
method_patch: Pattern.init("PATCH "),
method_post: Pattern.init("POST "),
method_put: Pattern.init("PUT "),
pub const methods = struct {
// NOTE These patterns are arranged in a specific order, such that the
// first ones are the most common (based on vibes only).
@"version_http/1.1": Pattern.init("HTTP/1.1\r\n"),
pub const GET = Pattern.init("GET ");
pub const POST = Pattern.init("POST ");
pub const HEAD = Pattern.init("HEAD ");
pub const PUT = Pattern.init("PUT ");
pub const DELETE = Pattern.init("DELETE ");
pub const PATCH = Pattern.init("PATCH ");
pub const OPTIONS = Pattern.init("OPTIONS ");
pub const CONNECT = Pattern.init("CONNECT ");
pub const TRACE = Pattern.init("TRACE ");
};
pub const @"version_http/1.1" = Pattern.init("HTTP/1.1\r\n");
};
inline fn hasSpace(vec: Vec) bool {
@@ -98,6 +107,8 @@ const State = union(enum) {
}
init: void,
// TODO Add all methods here and in `consumeChar` (they are covered by
// `consumeVec`, though)
method_d: void,
method_g: void,
method_h: void,
@@ -145,30 +156,20 @@ const ConsumeCharResult = enum {
done,
};
pub const Method = enum {
DELETE,
GET,
HEAD,
PATCH,
POST,
PUT,
};
pub const Route = struct {
method: Method,
pathname: []const u8,
};
callbacks: Callbacks,
request_router: RequestRouter,
response: *Response,
state: State,
current_header_is_content_length: bool,
content_length: usize,
pub fn init(callbacks: Callbacks) Parser {
request_handler: ?RequestHandler = null,
last_router_error: anyerror = undefined,
last_handler_error: anyerror = undefined,
pub fn init(request_router: RequestRouter, response: *Response) Parser {
return .{
.callbacks = callbacks,
.request_router = request_router,
.response = response,
.state = .init,
.current_header_is_content_length = false,
.content_length = 0,
};
}
@@ -186,9 +187,10 @@ pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult {
const done = new_body.len >= self.content_length;
if (done) {
if (self.callbacks.body) |bodyCallback| {
bodyCallback(self.callbacks.self, new_body);
}
self.request_handler.?.rawBody(self.response, new_body) catch |err| {
self.last_handler_error = err;
return error.HandlerError;
};
}
return .{
@@ -197,9 +199,25 @@ pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult {
};
},
else => {
const res = try self.consumeChar(&chars[i]);
if (chars.len - i >= vec_len) {
const vec_res = try self.consumeVec(chars[i..][0..vec_len]);
i += vec_res.consumed;
if (vec_res.done) {
return .{
.consumed = i,
.done = true,
};
}
if (vec_res.consumed > 0) {
continue;
}
}
const char_res = try self.consumeChar(&chars[i]);
i += 1;
if (res == .done) return .{
if (char_res == .done) return .{
.consumed = i,
.done = true,
};
@@ -213,6 +231,65 @@ pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult {
};
}
pub fn consumeVec(self: *Parser, vec: *const [vec_len]u8) Error!ConsumeResult {
switch (self.state) {
.init => {
inline for (@typeInfo(patterns.methods).@"struct".decls) |decl| {
const pattern: Pattern = @field(patterns.methods, decl.name);
if (pattern.check(vec)) {
self.state = .methodComplete(@field(Method, decl.name));
return .{
.consumed = pattern.len,
.done = false,
};
}
}
return error.MethodNotSupported;
},
.pathname_state => |s| {
if (hasSpace(vec)) {
// Delegate to `consumeChar`.
return .{
.consumed = 0,
.done = false,
};
}
self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + vec_len]);
},
.pathname_complete => {
if (patterns.@"version_http/1.1".check(vec)) {
self.state = .header_name_start;
return .{
.consumed = patterns.@"version_http/1.1".len,
.done = false,
};
} else {
return error.HttpVersionNotSupported;
}
},
.header_value => |s| {
if (hasCRLF(vec)) {
// Delegate to `consumeChar`.
return .{
.consumed = 0,
.done = false,
};
}
self.state = .headerValue(s.name, s.value.ptr[0 .. s.value.len + vec_len]);
},
else => {
// Delegate to `consumeChar`.
return .{
.consumed = 0,
.done = false,
};
},
}
}
pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
const c = c_ptr.*;
const c_slice = @as([*]const u8, @ptrCast(c_ptr))[0..1];
@@ -301,12 +378,10 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
.pathname_state => |s| switch (c) {
' ' => {
self.state = .pathname_complete;
if (self.callbacks.route) |routeCallback| {
routeCallback(self.callbacks.self, .{
.method = s.method,
.pathname = s.pathname,
});
}
self.request_handler = self.request_router.rawRoute(.init(s.method, s.pathname)) catch |err| {
self.last_router_error = err;
return error.RouterError;
};
},
else => self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + 1]),
},
@@ -357,21 +432,22 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
.header_name => |name| switch (c) {
':' => {
self.state = .headerValue(name, @as([*]const u8, @ptrCast(c_ptr))[1..1]);
self.current_header_is_content_length = std.ascii.eqlIgnoreCase(name, "Content-Length");
},
else => self.state = .{ .header_name = name.ptr[0 .. name.len + 1] },
},
.header_value => |s| switch (c) {
'\r' => {
self.state = .header_line_end;
const value_trimmed = std.mem.trim(u8, s.value, " \t");
if (self.current_header_is_content_length) {
self.content_length = std.fmt.parseInt(usize, value_trimmed, 10) catch return error.InvalidContentLength;
self.current_header_is_content_length = false;
}
if (self.callbacks.header) |headerCallback| {
headerCallback(self.callbacks.self, s.name, value_trimmed);
const header: Header = .init(s.name, std.mem.trim(u8, s.value, " \t"));
if (header.isKnown(.@"Content-Length")) {
self.content_length = std.fmt.parseInt(usize, header.value, 10) catch return error.InvalidContentLength;
}
self.request_handler.rawHeader(header) catch |err| {
self.last_handler_error = err;
return error.HandlerError;
};
},
else => self.state = .headerValue(s.name, s.value.ptr[0 .. s.value.len + 1]),
},
@@ -382,9 +458,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
.headers_end => switch (c) {
'\n' => {
if (self.content_length == 0) {
if (self.callbacks.body) |bodyCallback| {
bodyCallback(self.callbacks.self, &.{});
}
self.handler.rawBody(self.request, &.{});
return .done;
}
self.state = .{ .body = @as([*]const u8, @ptrCast(c_ptr))[1..1] };
@@ -395,9 +469,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
const new_body = body.ptr[0 .. body.len + 1];
self.state = .{ .body = new_body };
if (new_body.len >= self.content_length) {
if (self.callbacks.body) |bodyCallback| {
bodyCallback(self.callbacks.self, new_body);
}
self.handler.rawBody(self.request, new_body);
return .done;
}
},