FastCGI implementation in OCaml
1open Fastcgi
2
3let hex_dump ?(max_bytes=256) content =
4 let len = String.length content in
5 let display_len = min len max_bytes in
6 let bytes_per_line = 16 in
7 let lines_to_show = (display_len - 1) / bytes_per_line + 1 in
8
9 for i = 0 to lines_to_show - 1 do
10 let start = i * bytes_per_line in
11 let end_ = min (start + bytes_per_line) display_len in
12 Printf.printf " %04x: " start;
13
14 (* Print hex bytes *)
15 for j = start to end_ - 1 do
16 Printf.printf "%02x " (Char.code content.[j]);
17 if j = start + 7 then Printf.printf " "
18 done;
19
20 (* Pad remaining space *)
21 for _ = end_ to start + bytes_per_line - 1 do
22 Printf.printf " ";
23 if end_ <= start + 7 then Printf.printf " "
24 done;
25
26 Printf.printf " |";
27
28 (* Print ASCII representation *)
29 for j = start to end_ - 1 do
30 let c = content.[j] in
31 if c >= ' ' && c <= '~' then
32 Printf.printf "%c" c
33 else
34 Printf.printf "."
35 done;
36
37 Printf.printf "|\n%!"
38 done;
39
40 if len > max_bytes then
41 Printf.printf " ... (%d more bytes truncated)\n%!" (len - max_bytes)
42
43
44let test_record_types () =
45 Printf.printf "Testing record type conversions...\n%!";
46
47 let test_type rt expected_int =
48 assert (Record.record_to_int rt = expected_int);
49 assert (Record.record_of_int expected_int = rt)
50 in
51
52 test_type Begin_request 1;
53 test_type Abort_request 2;
54 test_type End_request 3;
55 test_type Params 4;
56 test_type Stdin 5;
57 test_type Stdout 6;
58 test_type Stderr 7;
59 test_type Data 8;
60 test_type Get_values 9;
61 test_type Get_values_result 10;
62 test_type Unknown_type 11;
63
64 (* Test invalid record type *)
65 (try
66 let _ = Record.record_of_int 99 in
67 assert false (* Should not reach here *)
68 with Invalid_argument _ -> ());
69
70 Printf.printf "✓ Record type conversion test passed\n%!"
71
72let test_kv_encoding () =
73 Printf.printf "Testing key-value encoding...\n%!";
74
75 let kv = Record.KV.empty
76 |> Record.KV.add "REQUEST_METHOD" "GET"
77 |> Record.KV.add "SERVER_NAME" "localhost"
78 |> Record.KV.add "SERVER_PORT" "80"
79 in
80
81 let encoded = Record.KV.encode kv in
82 let decoded = Record.KV.decode encoded in
83
84 (* Check that all original pairs are present *)
85 assert (Record.KV.find "REQUEST_METHOD" decoded = "GET");
86 assert (Record.KV.find "SERVER_NAME" decoded = "localhost");
87 assert (Record.KV.find "SERVER_PORT" decoded = "80");
88
89 Printf.printf "✓ Key-value encoding test passed\n%!"
90
91let test_long_key_value () =
92 Printf.printf "Testing long key-value encoding...\n%!";
93
94 (* Create a long key and value to test 4-byte length encoding *)
95 let long_key = String.make 200 'k' in
96 let long_value = String.make 300 'v' in
97
98 let kv = Record.KV.empty
99 |> Record.KV.add long_key long_value
100 |> Record.KV.add "short" "val"
101 in
102
103 let encoded = Record.KV.encode kv in
104 let decoded = Record.KV.decode encoded in
105
106 assert (Record.KV.find long_key decoded = long_value);
107 assert (Record.KV.find "short" decoded = "val");
108
109 Printf.printf "✓ Long key-value encoding test passed\n%!"
110
111let test_with_binary_test_case ~fs filename expected_type expected_request_id =
112 Printf.printf "Testing with binary test case: %s...\n%!" filename;
113
114 let (raw_content, parsed) =
115 Eio.Path.with_open_in (fs, "test_cases/" ^ filename) @@ fun flow ->
116 let buf = Buffer.create 1024 in
117 Eio.Flow.copy flow (Eio.Flow.buffer_sink buf);
118 let raw_content = Buffer.contents buf in
119 let buf_read = Eio.Buf_read.of_string raw_content in
120 let parsed = Record.read buf_read in
121 (raw_content, parsed)
122 in
123
124 Printf.printf "\nRaw file contents (%d bytes):\n" (String.length raw_content);
125 hex_dump raw_content;
126
127 Printf.printf "\nParsed record:\n";
128 Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
129
130 (* If this is a Params record, also show the decoded key-value pairs *)
131 if parsed.record_type = Params && String.length parsed.content > 0 then (
132 let params = Record.KV.decode parsed.content in
133 Printf.printf "\nDecoded parameters:\n";
134 Format.printf "%a\n" Record.KV.pp params
135 );
136
137 assert (parsed.version = 1);
138 assert (parsed.record_type = expected_type);
139 assert (parsed.request_id = expected_request_id);
140
141 Printf.printf "✓ Binary test case %s passed\n%!" filename
142
143let run_tests ~fs =
144 Printf.printf "Running FastCGI Record tests...\n\n%!";
145
146 test_record_types ();
147 test_kv_encoding ();
148 test_long_key_value ();
149
150 (* Test with some of our binary test cases *)
151 test_with_binary_test_case ~fs "begin_request_responder.bin" Begin_request 1;
152 test_with_binary_test_case ~fs "params_empty.bin" Params 1;
153 test_with_binary_test_case ~fs "end_request_success.bin" End_request 1;
154 test_with_binary_test_case ~fs "get_values.bin" Get_values 0;
155 test_with_binary_test_case ~fs "abort_request.bin" Abort_request 1;
156
157 Printf.printf "\n✅ All FastCGI Record tests passed!\n%!"
158
159let () = Eio_main.run @@ fun env ->
160 let fs = Eio.Stdenv.cwd env in
161 run_tests ~fs:(fst fs)