web: Secondary utils and helpers
This commit is contained in:
407
packages/web/src/http/Parser.zig
Normal file
407
packages/web/src/http/Parser.zig
Normal file
@@ -0,0 +1,407 @@
|
||||
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 Error = error{
|
||||
MethodNotSupported,
|
||||
HttpVersionNotSupported,
|
||||
MissingLineFeed,
|
||||
InvalidContentLength,
|
||||
};
|
||||
|
||||
const Vec = @Vector(32, u8);
|
||||
|
||||
const Pattern = struct {
|
||||
value: Vec,
|
||||
mask: Vec,
|
||||
len: u6,
|
||||
|
||||
pub fn init(comptime prefix: []const u8) Pattern {
|
||||
if (prefix.len > 32) {
|
||||
@compileError("Prefix length is too high");
|
||||
}
|
||||
|
||||
var value: [32]u8 = undefined;
|
||||
var mask: [32]u8 = undefined;
|
||||
for (0..32) |i| {
|
||||
if (i < prefix.len) {
|
||||
value[i] = prefix[i];
|
||||
mask[i] = 0xFF;
|
||||
} else {
|
||||
value[i] = 0x00;
|
||||
mask[i] = 0x00;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fn check(self: Pattern, vec: Vec) bool {
|
||||
return @reduce(.And, vec & self.mask == self.value);
|
||||
}
|
||||
};
|
||||
|
||||
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 "),
|
||||
|
||||
@"version_http/1.1": Pattern.init("HTTP/1.1\r\n"),
|
||||
};
|
||||
|
||||
inline fn hasSpace(vec: Vec) bool {
|
||||
const has_space = vec == @as(Vec, @splat(' '));
|
||||
return @reduce(.Or, has_space);
|
||||
}
|
||||
|
||||
inline fn hasCRLF(vec: Vec) bool {
|
||||
const has_cr = vec == @as(Vec, @splat('\r'));
|
||||
const has_lf = vec == @as(Vec, @splat('\n'));
|
||||
return @reduce(.Or, has_cr | has_lf);
|
||||
}
|
||||
|
||||
const State = union(enum) {
|
||||
pub fn methodComplete(method: Method) State {
|
||||
return .{
|
||||
.method_complete = .{
|
||||
.method = method,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn pathname(method: Method, p: []const u8) State {
|
||||
return .{
|
||||
.pathname_state = .{
|
||||
.method = method,
|
||||
.pathname = p,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn headerValue(name: []const u8, value: []const u8) State {
|
||||
return .{
|
||||
.header_value = .{
|
||||
.name = name,
|
||||
.value = value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
init: void,
|
||||
method_d: void,
|
||||
method_g: void,
|
||||
method_h: void,
|
||||
method_p: void,
|
||||
method_de: void,
|
||||
method_ge: void,
|
||||
method_he: void,
|
||||
method_pa: void,
|
||||
method_po: void,
|
||||
method_pu: void,
|
||||
method_del: void,
|
||||
method_hea: void,
|
||||
method_pat: void,
|
||||
method_pos: void,
|
||||
method_dele: void,
|
||||
method_patc: void,
|
||||
method_delet: void,
|
||||
method_complete: struct { method: Method },
|
||||
pathname_state: struct { method: Method, pathname: []const u8 },
|
||||
pathname_complete: void,
|
||||
version_h: void,
|
||||
version_ht: void,
|
||||
version_htt: void,
|
||||
version_http: void,
|
||||
@"version_http/": void,
|
||||
@"version_http/1": void,
|
||||
@"version_http/1.": void,
|
||||
version_complete: void,
|
||||
start_line_end: void,
|
||||
header_name_start: void,
|
||||
header_name: []const u8,
|
||||
header_value: struct { name: []const u8, value: []const u8 },
|
||||
header_line_end: void,
|
||||
headers_end: void,
|
||||
body: []const u8,
|
||||
};
|
||||
|
||||
const ConsumeResult = struct {
|
||||
consumed: usize,
|
||||
done: bool,
|
||||
};
|
||||
|
||||
const ConsumeCharResult = enum {
|
||||
not_done,
|
||||
done,
|
||||
};
|
||||
|
||||
pub const Method = enum {
|
||||
DELETE,
|
||||
GET,
|
||||
HEAD,
|
||||
PATCH,
|
||||
POST,
|
||||
PUT,
|
||||
};
|
||||
|
||||
pub const Route = struct {
|
||||
method: Method,
|
||||
pathname: []const u8,
|
||||
};
|
||||
|
||||
callbacks: Callbacks,
|
||||
state: State,
|
||||
current_header_is_content_length: bool,
|
||||
content_length: usize,
|
||||
|
||||
pub fn init(callbacks: Callbacks) Parser {
|
||||
return .{
|
||||
.callbacks = callbacks,
|
||||
.state = .init,
|
||||
.current_header_is_content_length = false,
|
||||
.content_length = 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult {
|
||||
var i: usize = 0;
|
||||
while (i < chars.len) {
|
||||
switch (self.state) {
|
||||
.body => |body| {
|
||||
const to_consume = @min(chars.len - i, self.content_length - body.len);
|
||||
const new_body = body.ptr[0 .. body.len + to_consume];
|
||||
self.state = .{ .body = new_body };
|
||||
i += to_consume;
|
||||
|
||||
const done = new_body.len >= self.content_length;
|
||||
|
||||
if (done) {
|
||||
if (self.callbacks.body) |bodyCallback| {
|
||||
bodyCallback(self.callbacks.self, new_body);
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.consumed = i,
|
||||
.done = done,
|
||||
};
|
||||
},
|
||||
else => {
|
||||
const res = try self.consumeChar(&chars[i]);
|
||||
i += 1;
|
||||
if (res == .done) return .{
|
||||
.consumed = i,
|
||||
.done = true,
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.consumed = chars.len,
|
||||
.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];
|
||||
switch (self.state) {
|
||||
.init => switch (c) {
|
||||
'D' => self.state = .method_d,
|
||||
'G' => self.state = .method_g,
|
||||
'H' => self.state = .method_h,
|
||||
'P' => self.state = .method_p,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_d => switch (c) {
|
||||
'E' => self.state = .method_de,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_g => switch (c) {
|
||||
'E' => self.state = .method_ge,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_h => switch (c) {
|
||||
'E' => self.state = .method_he,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_p => switch (c) {
|
||||
'A' => self.state = .method_pa,
|
||||
'O' => self.state = .method_po,
|
||||
'U' => self.state = .method_pu,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_de => switch (c) {
|
||||
'L' => self.state = .method_del,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_ge => switch (c) {
|
||||
'T' => self.state = .methodComplete(.GET),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_he => switch (c) {
|
||||
'A' => self.state = .method_hea,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_pa => switch (c) {
|
||||
'T' => self.state = .method_pat,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_po => switch (c) {
|
||||
'S' => self.state = .method_pos,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_pu => switch (c) {
|
||||
'T' => self.state = .methodComplete(.PUT),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_del => switch (c) {
|
||||
'E' => self.state = .method_dele,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_hea => switch (c) {
|
||||
'D' => self.state = .methodComplete(.HEAD),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_pat => switch (c) {
|
||||
'C' => self.state = .method_patc,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_pos => switch (c) {
|
||||
'T' => self.state = .methodComplete(.POST),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_dele => switch (c) {
|
||||
'T' => self.state = .method_delet,
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_patc => switch (c) {
|
||||
'H' => self.state = .methodComplete(.PATCH),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_delet => switch (c) {
|
||||
'E' => self.state = .methodComplete(.DELETE),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.method_complete => |s| switch (c) {
|
||||
' ' => self.state = .pathname(s.method, @as([*]const u8, @ptrCast(c_ptr))[1..1]),
|
||||
else => return error.MethodNotSupported,
|
||||
},
|
||||
.pathname_state => |s| switch (c) {
|
||||
' ' => {
|
||||
self.state = .pathname_complete;
|
||||
if (self.callbacks.route) |routeCallback| {
|
||||
routeCallback(self.callbacks.self, .{
|
||||
.method = s.method,
|
||||
.pathname = s.pathname,
|
||||
});
|
||||
}
|
||||
},
|
||||
else => self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + 1]),
|
||||
},
|
||||
.pathname_complete => switch (c) {
|
||||
'H' => self.state = .version_h,
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.version_h => switch (c) {
|
||||
'T' => self.state = .version_ht,
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.version_ht => switch (c) {
|
||||
'T' => self.state = .version_htt,
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.version_htt => switch (c) {
|
||||
'P' => self.state = .version_http,
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.version_http => switch (c) {
|
||||
'/' => self.state = .@"version_http/",
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.@"version_http/" => switch (c) {
|
||||
'1' => self.state = .@"version_http/1",
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.@"version_http/1" => switch (c) {
|
||||
'.' => self.state = .@"version_http/1.",
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.@"version_http/1." => switch (c) {
|
||||
'1' => self.state = .version_complete,
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.version_complete => switch (c) {
|
||||
'\r' => self.state = .start_line_end,
|
||||
else => return error.HttpVersionNotSupported,
|
||||
},
|
||||
.start_line_end => switch (c) {
|
||||
'\n' => self.state = .header_name_start,
|
||||
else => return error.MissingLineFeed,
|
||||
},
|
||||
.header_name_start => switch (c) {
|
||||
'\r' => self.state = .headers_end,
|
||||
else => self.state = .{ .header_name = c_slice },
|
||||
},
|
||||
.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);
|
||||
}
|
||||
},
|
||||
else => self.state = .headerValue(s.name, s.value.ptr[0 .. s.value.len + 1]),
|
||||
},
|
||||
.header_line_end => switch (c) {
|
||||
'\n' => self.state = .header_name_start,
|
||||
else => return error.MissingLineFeed,
|
||||
},
|
||||
.headers_end => switch (c) {
|
||||
'\n' => {
|
||||
if (self.content_length == 0) {
|
||||
if (self.callbacks.body) |bodyCallback| {
|
||||
bodyCallback(self.callbacks.self, &.{});
|
||||
}
|
||||
return .done;
|
||||
}
|
||||
self.state = .{ .body = @as([*]const u8, @ptrCast(c_ptr))[1..1] };
|
||||
},
|
||||
else => return error.MissingLineFeed,
|
||||
},
|
||||
.body => |body| {
|
||||
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);
|
||||
}
|
||||
return .done;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return .not_done;
|
||||
}
|
||||
Reference in New Issue
Block a user