Skip to content

Commit d4b9ea2

Browse files
committed
Merge branch 'response-parser'
2 parents cadb52c + d903fbb commit d4b9ea2

File tree

1 file changed

+211
-57
lines changed

1 file changed

+211
-57
lines changed

src/root.zig

+211-57
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ const Cursor = struct {
144144
return cursor.asInteger(u32) == @as(u32, @bitCast([4]u8{ c0, c1, c2, c3 }));
145145
}
146146

147+
/// Moves the cursor until no leading spaces there are.
148+
inline fn skipSpaces(cursor: *Cursor) void {
149+
while (cursor.end - cursor.current() > 0 and cursor.char() == ' ') : (cursor.advance(1)) {}
150+
}
151+
147152
/// Parses the method and the trailing space.
148153
/// SAFETY: This function doesn't check if out of bounds reachable.
149154
inline fn parseMethod(cursor: *Cursor, method: *Method) ParseRequestError!void {
@@ -384,7 +389,7 @@ const Cursor = struct {
384389
return error.Invalid;
385390
}
386391
},
387-
inline else => return error.Invalid, // unroll
392+
else => return error.Invalid, // unroll
388393
}
389394
}
390395

@@ -508,7 +513,7 @@ const Cursor = struct {
508513
// move forward
509514
cursor.advance(1);
510515
},
511-
inline else => {
516+
else => {
512517
// If we got here we've either;
513518
// * Found an invalid character that's not colon (58),
514519
// * Reached end of the buffer so this is likely a partial request.
@@ -551,7 +556,7 @@ const Cursor = struct {
551556
cursor.advance(1);
552557
},
553558
// Any other character is invalid.
554-
inline else => {
559+
else => {
555560
if (val_end == cursor.end) {
556561
return error.Incomplete;
557562
}
@@ -633,6 +638,57 @@ const Cursor = struct {
633638
},
634639
}
635640
}
641+
642+
/// Matches status message for valid characters.
643+
inline fn matchStatusMessage(cursor: *Cursor) void {
644+
while (cursor.end - cursor.idx > 0) : (cursor.advance(1)) {
645+
if (!isValidStatusMsgChar(cursor.char())) {
646+
return;
647+
}
648+
}
649+
}
650+
651+
/// Parses the status message in HTTP responses.
652+
inline fn parseStatusMessage(cursor: *Cursor, status_msg: *?[]const u8) ParseRequestError!void {
653+
const msg_start = cursor.current();
654+
cursor.matchStatusMessage();
655+
const msg_end = cursor.current();
656+
657+
// The character that cause `matchStatusMessage` must be either `\r` or `\n`.
658+
switch (cursor.char()) {
659+
'\n' => {
660+
// done
661+
cursor.advance(1);
662+
},
663+
'\r' => {
664+
cursor.advance(1);
665+
666+
// If we've reached the end, return `error.Incomplete`.
667+
if (cursor.current() == cursor.end) {
668+
return error.Incomplete;
669+
}
670+
671+
// We expect `\n` after.
672+
if (cursor.char() != '\n') {
673+
@branchHint(.unlikely);
674+
return error.Invalid;
675+
}
676+
677+
// done
678+
cursor.advance(1);
679+
},
680+
else => {
681+
if (msg_end == cursor.end) {
682+
return error.Incomplete;
683+
}
684+
685+
return error.Invalid;
686+
},
687+
}
688+
689+
// set the status message
690+
status_msg.* = msg_start[0 .. msg_end - msg_start];
691+
}
636692
};
637693

638694
/// Table of valid path characters.
@@ -671,6 +727,18 @@ inline fn isValidValueChar(c: u8) bool {
671727
return value_map[c] != 0;
672728
}
673729

730+
/// Table of valid status message characters.
731+
const status_msg_map = createCharMap(.{
732+
// Invalid characters.
733+
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
734+
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 127,
735+
});
736+
737+
/// Checks if a given character is a valid statuss message character.
738+
inline fn isValidStatusMsgChar(c: u8) bool {
739+
return status_msg_map[c] != 0;
740+
}
741+
674742
/// Returns an integer filled with a given byte.
675743
inline fn broadcast(comptime T: type, byte: u8) T {
676744
comptime {
@@ -717,7 +785,7 @@ pub fn parseRequest(
717785
version: *Version,
718786
/// Parsed headers will be stored here.
719787
headers: []Header,
720-
/// When the function returns, count of parsed headers will be set here.
788+
/// Count of parsed headers will be set here.
721789
header_count: *usize,
722790
) ParseRequestError!usize {
723791
// We expect at least 15 bytes to start processing.
@@ -744,7 +812,122 @@ pub fn parseRequest(
744812
try cursor.parseHeaders(headers, header_count);
745813

746814
// Return the total consumed length to caller.
747-
return cursor.idx - cursor.start;
815+
return cursor.current() - cursor.start;
816+
}
817+
818+
/// Minimum response len.
819+
///
820+
/// `HTTP/1.1 200\n`
821+
/// Status message (OK) is optional.
822+
const min_res_len = 13;
823+
824+
/// Parses an HTTP response.
825+
/// * `error.Incomplete` indicates more data is needed to complete the request.
826+
/// * `error.Invalid` indicates request is invalid/malformed.
827+
pub fn parseResponse(
828+
// Slice we want to parse.
829+
slice: []const u8,
830+
/// Parsed HTTP version will be stored here.
831+
version: *Version,
832+
/// Parsed status code will be stored here.
833+
status_code: *u16,
834+
/// Parsed status message will be stored here.
835+
status_msg: *?[]const u8,
836+
/// Parsed headers will be stored here.
837+
headers: []Header,
838+
/// Count of parsed headers will be set here.
839+
header_count: *usize,
840+
) !usize {
841+
// We need at least `min_res_len` bytes to start parsing.
842+
if (slice.len < min_res_len) {
843+
return error.Incomplete;
844+
}
845+
846+
const slice_start = slice.ptr;
847+
const slice_end = slice.ptr + slice.len;
848+
849+
var cursor = Cursor{ .idx = slice_start, .start = slice_start, .end = slice_end };
850+
851+
// Parse HTTP version.
852+
// Request and response differ in this sense so we can't use `Cursor.parseVersion` here.
853+
{
854+
const chunk = cursor.asInteger(u64);
855+
// advance as much as consumed
856+
cursor.advance(8);
857+
858+
// Match the version with magic integers.
859+
version.* = blk: switch (chunk) {
860+
HTTP_1_0 => break :blk .@"1.0",
861+
HTTP_1_1 => break :blk .@"1.1",
862+
else => return error.Invalid, // Unknown/unsupported HTTP version.
863+
};
864+
865+
// Parse the space afterwards.
866+
if (cursor.char() != ' ') {
867+
@branchHint(.unlikely);
868+
return error.Invalid;
869+
}
870+
871+
// Consume the space.
872+
cursor.advance(1);
873+
}
874+
875+
// Parse status code.
876+
{
877+
// Make sure next 3 bytes are numeric (0-9).
878+
const dirty = cursor.idx[0] > 47 and cursor.idx[0] < 58 and
879+
cursor.idx[1] > 47 and cursor.idx[1] < 58 and
880+
cursor.idx[2] > 47 and cursor.idx[2] < 58;
881+
882+
if (!dirty) {
883+
@branchHint(.unlikely);
884+
return error.Invalid;
885+
}
886+
887+
// Parse the status code.
888+
const hundreds: u16 = @as(u16, @intCast(cursor.idx[0] - '0')) * 100;
889+
const tens: u16 = @as(u16, @intCast(cursor.idx[1] - '0')) * 10;
890+
const ones: u16 = @as(u16, @intCast(cursor.idx[2] - '0'));
891+
892+
// Set the status code.
893+
status_code.* = hundreds + tens + ones;
894+
895+
// eat bytes
896+
cursor.advance(3);
897+
}
898+
899+
// Parse status message if exists, otherwise, parse CRLF and continue.
900+
switch (cursor.char()) {
901+
' ' => {
902+
// Skip spaces if there are more.
903+
cursor.skipSpaces();
904+
// Get status message after.
905+
try cursor.parseStatusMessage(status_msg);
906+
},
907+
// If we got CRLF, it means we won't get a status message.
908+
'\n' => cursor.advance(1),
909+
'\r' => {
910+
cursor.advance(1);
911+
912+
if (cursor.current() == cursor.end) {
913+
return error.Incomplete;
914+
}
915+
916+
if (cursor.char() != '\n') {
917+
@branchHint(.unlikely);
918+
return error.Invalid;
919+
}
920+
921+
cursor.advance(1);
922+
},
923+
else => return error.Invalid,
924+
}
925+
926+
// Parse headers.
927+
try cursor.parseHeaders(headers, header_count);
928+
929+
// Return the total consumed length to caller.
930+
return cursor.current() - cursor.start;
748931
}
749932

750933
const testing = std.testing;
@@ -875,7 +1058,7 @@ test "cursor: match path" {
8751058
}
8761059
}
8771060

878-
test "parse request" {
1061+
test parseRequest {
8791062
const buffer: []const u8 = "OPTIONS /hey-this-is-kinda-long-path HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
8801063

8811064
var method: Method = .unknown;
@@ -898,54 +1081,25 @@ test "parse request" {
8981081
try testing.expectEqualStrings("close", headers[1].value);
8991082
}
9001083

901-
//test parseRequest {
902-
//const buffer: []const u8 = "TRACE /cookies HTTP/1.1\r\nHost: asdjqwdkwfj\r\nConnection: keep-alive\r\n\r\n";
903-
//
904-
//var method: Method = .unknown;
905-
//var path: ?[]const u8 = null;
906-
//var http_version: Version = .@"1.0";
907-
//var headers: [3]Header = undefined;
908-
//var header_count: usize = 0;
909-
//
910-
//const len = parseRequest(buffer[0..], &method, &path, &http_version, &headers, &header_count) catch |err| switch (err) {
911-
// error.Incomplete => @panic("need more bytes"),
912-
// error.Invalid => @panic("invalid!"),
913-
//};
914-
//
915-
//std.debug.print("{}\t{}\n", .{ method, http_version });
916-
//std.debug.print("path: {s}\n", .{path.?});
917-
//
918-
//for (headers[0..header_count]) |header| {
919-
// std.debug.print("{s}\t{s}\n", .{ header.key, header.value });
920-
//}
921-
//
922-
//std.debug.print("len: {any}\n", .{len});
923-
924-
//var tokens: [256]u1 = std.mem.zeroes([256]u1);
925-
//@memset(&tokens, 1);
926-
//
927-
//tokens[58] = 0;
928-
//tokens[127] = 0;
929-
//
930-
//for (0..32) |i| {
931-
// tokens[i] = 0;
932-
//}
933-
//
934-
//std.debug.print("{any}\n", .{tokens});
935-
936-
//const min: @Vector(8, u8) = @splat('A' - 1);
937-
//const max: @Vector(8, u8) = @splat('Z');
938-
//
939-
//const chunk: @Vector(8, u8) = "tEsTINgG".*;
940-
//
941-
//const bits = @intFromBool(chunk <= max) & @intFromBool(chunk > min);
942-
//var res: u8 = @bitCast(bits);
943-
//
944-
//while (res != 0) {
945-
// const t = res & -%res;
946-
// defer res ^= t;
947-
//
948-
// const idx = @ctz(t);
949-
// std.debug.print("{c}\n", .{chunk[idx]});
950-
//}
951-
//}
1084+
test parseResponse {
1085+
const buffer = "HTTP/1.1 418 I'm a teapot\r\nHost: localhost\r\nSome-Number-Sequence: 123291429\r\n\r\n";
1086+
1087+
var version: Version = .@"1.0";
1088+
var status_code: u16 = 0;
1089+
var status_msg: ?[]const u8 = null;
1090+
var headers: [2]Header = undefined;
1091+
var header_count: usize = 0;
1092+
1093+
const len = try parseResponse(buffer[0..], &version, &status_code, &status_msg, &headers, &header_count);
1094+
1095+
try testing.expect(buffer.len == len);
1096+
try testing.expect(version == .@"1.1");
1097+
try testing.expect(status_code == 418);
1098+
try testing.expect(status_msg != null);
1099+
try testing.expectEqualStrings("I'm a teapot", status_msg.?);
1100+
try testing.expect(header_count == 2);
1101+
try testing.expectEqualStrings("Host", headers[0].key);
1102+
try testing.expectEqualStrings("localhost", headers[0].value);
1103+
try testing.expectEqualStrings("Some-Number-Sequence", headers[1].key);
1104+
try testing.expectEqualStrings("123291429", headers[1].value);
1105+
}

0 commit comments

Comments
 (0)