@@ -144,6 +144,11 @@ const Cursor = struct {
144
144
return cursor .asInteger (u32 ) == @as (u32 , @bitCast ([4 ]u8 { c0 , c1 , c2 , c3 }));
145
145
}
146
146
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
+
147
152
/// Parses the method and the trailing space.
148
153
/// SAFETY: This function doesn't check if out of bounds reachable.
149
154
inline fn parseMethod (cursor : * Cursor , method : * Method ) ParseRequestError ! void {
@@ -384,7 +389,7 @@ const Cursor = struct {
384
389
return error .Invalid ;
385
390
}
386
391
},
387
- inline else = > return error .Invalid , // unroll
392
+ else = > return error .Invalid , // unroll
388
393
}
389
394
}
390
395
@@ -508,7 +513,7 @@ const Cursor = struct {
508
513
// move forward
509
514
cursor .advance (1 );
510
515
},
511
- inline else = > {
516
+ else = > {
512
517
// If we got here we've either;
513
518
// * Found an invalid character that's not colon (58),
514
519
// * Reached end of the buffer so this is likely a partial request.
@@ -551,7 +556,7 @@ const Cursor = struct {
551
556
cursor .advance (1 );
552
557
},
553
558
// Any other character is invalid.
554
- inline else = > {
559
+ else = > {
555
560
if (val_end == cursor .end ) {
556
561
return error .Incomplete ;
557
562
}
@@ -633,6 +638,57 @@ const Cursor = struct {
633
638
},
634
639
}
635
640
}
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
+ }
636
692
};
637
693
638
694
/// Table of valid path characters.
@@ -671,6 +727,18 @@ inline fn isValidValueChar(c: u8) bool {
671
727
return value_map [c ] != 0 ;
672
728
}
673
729
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
+
674
742
/// Returns an integer filled with a given byte.
675
743
inline fn broadcast (comptime T : type , byte : u8 ) T {
676
744
comptime {
@@ -717,7 +785,7 @@ pub fn parseRequest(
717
785
version : * Version ,
718
786
/// Parsed headers will be stored here.
719
787
headers : []Header ,
720
- /// When the function returns, count of parsed headers will be set here.
788
+ /// Count of parsed headers will be set here.
721
789
header_count : * usize ,
722
790
) ParseRequestError ! usize {
723
791
// We expect at least 15 bytes to start processing.
@@ -744,7 +812,122 @@ pub fn parseRequest(
744
812
try cursor .parseHeaders (headers , header_count );
745
813
746
814
// 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 ;
748
931
}
749
932
750
933
const testing = std .testing ;
@@ -875,7 +1058,7 @@ test "cursor: match path" {
875
1058
}
876
1059
}
877
1060
878
- test "parse request" {
1061
+ test parseRequest {
879
1062
const buffer : []const u8 = "OPTIONS /hey-this-is-kinda-long-path HTTP/1.1\r \n Host: localhost\r \n Connection: close\r \n \r \n " ;
880
1063
881
1064
var method : Method = .unknown ;
@@ -898,54 +1081,25 @@ test "parse request" {
898
1081
try testing .expectEqualStrings ("close" , headers [1 ].value );
899
1082
}
900
1083
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 \n Host: localhost\r \n Some-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