this repo has no description
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

tests

+525 -1
-1
lib/dune
··· 8 8 ) 9 9 (modules_without_implementation 10 10 fastcgi 11 - fastcgi_record 12 11 ) 13 12 )
+243
lib/fastcgi_record.ml
··· 1 + 2 + type version = int 3 + 4 + type record = 5 + | Begin_request 6 + | Abort_request 7 + | End_request 8 + | Params 9 + | Stdin 10 + | Stdout 11 + | Stderr 12 + | Data 13 + | Get_values 14 + | Get_values_result 15 + | Unknown_type 16 + 17 + let record_to_int = function 18 + | Begin_request -> 1 19 + | Abort_request -> 2 20 + | End_request -> 3 21 + | Params -> 4 22 + | Stdin -> 5 23 + | Stdout -> 6 24 + | Stderr -> 7 25 + | Data -> 8 26 + | Get_values -> 9 27 + | Get_values_result -> 10 28 + | Unknown_type -> 11 29 + 30 + let record_of_int = function 31 + | 1 -> Begin_request 32 + | 2 -> Abort_request 33 + | 3 -> End_request 34 + | 4 -> Params 35 + | 5 -> Stdin 36 + | 6 -> Stdout 37 + | 7 -> Stderr 38 + | 8 -> Data 39 + | 9 -> Get_values 40 + | 10 -> Get_values_result 41 + | 11 -> Unknown_type 42 + | n -> invalid_arg (Printf.sprintf "Unknown FastCGI record type: %d" n) 43 + 44 + let pp_record ppf = function 45 + | Begin_request -> Format.pp_print_string ppf "Begin_request" 46 + | Abort_request -> Format.pp_print_string ppf "Abort_request" 47 + | End_request -> Format.pp_print_string ppf "End_request" 48 + | Params -> Format.pp_print_string ppf "Params" 49 + | Stdin -> Format.pp_print_string ppf "Stdin" 50 + | Stdout -> Format.pp_print_string ppf "Stdout" 51 + | Stderr -> Format.pp_print_string ppf "Stderr" 52 + | Data -> Format.pp_print_string ppf "Data" 53 + | Get_values -> Format.pp_print_string ppf "Get_values" 54 + | Get_values_result -> Format.pp_print_string ppf "Get_values_result" 55 + | Unknown_type -> Format.pp_print_string ppf "Unknown_type" 56 + 57 + type request_id = int 58 + 59 + type t = { 60 + version : version; 61 + record_type : record; 62 + request_id : request_id; 63 + content : string; 64 + } 65 + 66 + let pp ppf record = 67 + Format.fprintf ppf 68 + "@[<2>{ version = %d;@ record_type = %a;@ request_id = %d;@ content = %S }@]" 69 + record.version 70 + pp_record record.record_type 71 + record.request_id 72 + record.content 73 + 74 + (* FastCGI constants *) 75 + let fcgi_version_1 = 1 76 + let fcgi_header_len = 8 77 + 78 + let read buf_read = 79 + (* Read the 8-byte header *) 80 + let header = Eio.Buf_read.take fcgi_header_len buf_read in 81 + 82 + (* Parse header fields *) 83 + let version = Char.code header.[0] in 84 + let record_type_int = Char.code header.[1] in 85 + let request_id = (Char.code header.[2] lsl 8) lor (Char.code header.[3]) in 86 + let content_length = (Char.code header.[4] lsl 8) lor (Char.code header.[5]) in 87 + let padding_length = Char.code header.[6] in 88 + let _reserved = Char.code header.[7] in 89 + 90 + (* Validate version *) 91 + if version <> fcgi_version_1 then 92 + invalid_arg (Printf.sprintf "Unsupported FastCGI version: %d" version); 93 + 94 + (* Convert record type *) 95 + let record_type = record_of_int record_type_int in 96 + 97 + (* Read content *) 98 + let content = 99 + if content_length = 0 then 100 + "" 101 + else 102 + Eio.Buf_read.take content_length buf_read 103 + in 104 + 105 + (* Skip padding *) 106 + if padding_length > 0 then 107 + ignore (Eio.Buf_read.take padding_length buf_read); 108 + 109 + { version; record_type; request_id; content } 110 + 111 + let write buf_write record = 112 + let content_length = String.length record.content in 113 + 114 + (* Calculate padding for 8-byte alignment *) 115 + let padding_length = (8 - (content_length land 7)) land 7 in 116 + 117 + (* Create and write header *) 118 + let header = Bytes.create fcgi_header_len in 119 + Bytes.set_uint8 header 0 record.version; 120 + Bytes.set_uint8 header 1 (record_to_int record.record_type); 121 + Bytes.set_uint16_be header 2 record.request_id; 122 + Bytes.set_uint16_be header 4 content_length; 123 + Bytes.set_uint8 header 6 padding_length; 124 + Bytes.set_uint8 header 7 0; (* reserved *) 125 + 126 + Eio.Buf_write.string buf_write (Bytes.to_string header); 127 + 128 + (* Write content *) 129 + if content_length > 0 then 130 + Eio.Buf_write.string buf_write record.content; 131 + 132 + (* Write padding *) 133 + if padding_length > 0 then 134 + Eio.Buf_write.string buf_write (String.make padding_length '\000') 135 + 136 + let create ~version ~record ~request_id ~content = 137 + { version; record_type = record; request_id; content } 138 + 139 + module KV = struct 140 + type t = (string * string) list 141 + 142 + let empty = [] 143 + 144 + let add key value kvs = (key, value) :: kvs 145 + 146 + let remove key kvs = List.filter (fun (k, _) -> k <> key) kvs 147 + 148 + let find key kvs = 149 + try List.assoc key kvs 150 + with Not_found -> raise Not_found 151 + 152 + let find_opt key kvs = 153 + try Some (List.assoc key kvs) 154 + with Not_found -> None 155 + 156 + let to_seq kvs = List.to_seq kvs 157 + 158 + let of_seq seq = List.of_seq seq 159 + 160 + let encode_length len = 161 + if len <= 127 then 162 + String.make 1 (Char.chr len) 163 + else 164 + let b = Bytes.create 4 in 165 + Bytes.set_int32_be b 0 (Int32.logor (Int32.of_int len) 0x80000000l); 166 + Bytes.to_string b 167 + 168 + let decode_length buf pos = 169 + if pos >= String.length buf then 170 + failwith "Unexpected end of buffer while reading length"; 171 + let first_byte = Char.code buf.[pos] in 172 + if first_byte land 0x80 = 0 then 173 + (* Single byte length *) 174 + (first_byte, pos + 1) 175 + else ( 176 + (* Four byte length *) 177 + if pos + 4 > String.length buf then 178 + failwith "Unexpected end of buffer while reading 4-byte length"; 179 + let len = 180 + ((first_byte land 0x7f) lsl 24) lor 181 + ((Char.code buf.[pos + 1]) lsl 16) lor 182 + ((Char.code buf.[pos + 2]) lsl 8) lor 183 + (Char.code buf.[pos + 3]) 184 + in 185 + (len, pos + 4) 186 + ) 187 + 188 + let encode kvs = 189 + let buf = Buffer.create 256 in 190 + List.iter (fun (key, value) -> 191 + let key_len = String.length key in 192 + let value_len = String.length value in 193 + Buffer.add_string buf (encode_length key_len); 194 + Buffer.add_string buf (encode_length value_len); 195 + Buffer.add_string buf key; 196 + Buffer.add_string buf value 197 + ) kvs; 198 + Buffer.contents buf 199 + 200 + let decode content = 201 + let len = String.length content in 202 + let rec loop pos acc = 203 + if pos >= len then 204 + List.rev acc 205 + else 206 + (* Read name length *) 207 + let name_len, pos = decode_length content pos in 208 + (* Read value length *) 209 + let value_len, pos = decode_length content pos in 210 + 211 + (* Check bounds *) 212 + if pos + name_len + value_len > len then 213 + failwith "Unexpected end of buffer while reading key-value data"; 214 + 215 + (* Extract name and value *) 216 + let name = String.sub content pos name_len in 217 + let value = String.sub content (pos + name_len) value_len in 218 + 219 + loop (pos + name_len + value_len) ((name, value) :: acc) 220 + in 221 + loop 0 [] 222 + 223 + let read buf_read = 224 + (* For reading from a stream, we need to determine how much data to read. 225 + Since we're in a record context, we should read all available data 226 + until we hit the end of the record content. *) 227 + let content = Eio.Buf_read.take_all buf_read in 228 + decode content 229 + 230 + let write buf_write kvs = 231 + let encoded = encode kvs in 232 + Eio.Buf_write.string buf_write encoded 233 + 234 + let pp ppf kvs = 235 + Format.fprintf ppf "@[<2>[@["; 236 + let first = ref true in 237 + List.iter (fun (key, value) -> 238 + if not !first then Format.fprintf ppf ";@ "; 239 + first := false; 240 + Format.fprintf ppf "(%S, %S)" key value 241 + ) kvs; 242 + Format.fprintf ppf "@]]@]" 243 + end
+11
test/dune
··· 1 + (test 2 + (name simple_test) 3 + (libraries fastcgi eio) 4 + (deps (source_tree test_cases)) 5 + ) 6 + 7 + (test 8 + (name validate_all_test_cases) 9 + (libraries fastcgi eio) 10 + (deps (source_tree test_cases)) 11 + )
+106
test/simple_test.ml
··· 1 + open Fastcgi 2 + 3 + let test_record_types () = 4 + Printf.printf "Testing record type conversions...\n%!"; 5 + 6 + let test_type rt expected_int = 7 + assert (Record.record_to_int rt = expected_int); 8 + assert (Record.record_of_int expected_int = rt) 9 + in 10 + 11 + test_type Begin_request 1; 12 + test_type Abort_request 2; 13 + test_type End_request 3; 14 + test_type Params 4; 15 + test_type Stdin 5; 16 + test_type Stdout 6; 17 + test_type Stderr 7; 18 + test_type Data 8; 19 + test_type Get_values 9; 20 + test_type Get_values_result 10; 21 + test_type Unknown_type 11; 22 + 23 + (* Test invalid record type *) 24 + (try 25 + let _ = Record.record_of_int 99 in 26 + assert false (* Should not reach here *) 27 + with Invalid_argument _ -> ()); 28 + 29 + Printf.printf "✓ Record type conversion test passed\n%!" 30 + 31 + let test_kv_encoding () = 32 + Printf.printf "Testing key-value encoding...\n%!"; 33 + 34 + let kv = Record.KV.empty 35 + |> Record.KV.add "REQUEST_METHOD" "GET" 36 + |> Record.KV.add "SERVER_NAME" "localhost" 37 + |> Record.KV.add "SERVER_PORT" "80" 38 + in 39 + 40 + let encoded = Record.KV.encode kv in 41 + let decoded = Record.KV.decode encoded in 42 + 43 + (* Check that all original pairs are present *) 44 + assert (Record.KV.find "REQUEST_METHOD" decoded = "GET"); 45 + assert (Record.KV.find "SERVER_NAME" decoded = "localhost"); 46 + assert (Record.KV.find "SERVER_PORT" decoded = "80"); 47 + 48 + Printf.printf "✓ Key-value encoding test passed\n%!" 49 + 50 + let test_long_key_value () = 51 + Printf.printf "Testing long key-value encoding...\n%!"; 52 + 53 + (* Create a long key and value to test 4-byte length encoding *) 54 + let long_key = String.make 200 'k' in 55 + let long_value = String.make 300 'v' in 56 + 57 + let kv = Record.KV.empty 58 + |> Record.KV.add long_key long_value 59 + |> Record.KV.add "short" "val" 60 + in 61 + 62 + let encoded = Record.KV.encode kv in 63 + let decoded = Record.KV.decode encoded in 64 + 65 + assert (Record.KV.find long_key decoded = long_value); 66 + assert (Record.KV.find "short" decoded = "val"); 67 + 68 + Printf.printf "✓ Long key-value encoding test passed\n%!" 69 + 70 + let test_with_binary_test_case filename expected_type expected_request_id = 71 + Printf.printf "Testing with binary test case: %s...\n%!" filename; 72 + 73 + let content = 74 + let ic = open_in_bin ("test_cases/" ^ filename) in 75 + let len = in_channel_length ic in 76 + let content = really_input_string ic len in 77 + close_in ic; 78 + content 79 + in 80 + 81 + let buf_read = Eio.Buf_read.of_string content in 82 + let parsed = Record.read buf_read in 83 + 84 + assert (parsed.version = 1); 85 + assert (parsed.record_type = expected_type); 86 + assert (parsed.request_id = expected_request_id); 87 + 88 + Printf.printf "✓ Binary test case %s passed\n%!" filename 89 + 90 + let run_tests () = 91 + Printf.printf "Running FastCGI Record tests...\n\n%!"; 92 + 93 + test_record_types (); 94 + test_kv_encoding (); 95 + test_long_key_value (); 96 + 97 + (* Test with some of our binary test cases *) 98 + test_with_binary_test_case "begin_request_responder.bin" Begin_request 1; 99 + test_with_binary_test_case "params_empty.bin" Params 1; 100 + test_with_binary_test_case "end_request_success.bin" End_request 1; 101 + test_with_binary_test_case "get_values.bin" Get_values 0; 102 + test_with_binary_test_case "abort_request.bin" Abort_request 1; 103 + 104 + Printf.printf "\n✅ All FastCGI Record tests passed!\n%!" 105 + 106 + let () = run_tests ()
+165
test/validate_all_test_cases.ml
··· 1 + open Fastcgi 2 + open Record 3 + 4 + let test_cases = [ 5 + ("abort_request.bin", Abort_request, 1); 6 + ("begin_request_authorizer.bin", Begin_request, 2); 7 + ("begin_request_filter.bin", Begin_request, 3); 8 + ("begin_request_no_keep.bin", Begin_request, 1); 9 + ("begin_request_responder.bin", Begin_request, 1); 10 + ("data_empty.bin", Data, 3); 11 + ("data_filter.bin", Data, 3); 12 + ("end_request_error.bin", End_request, 1); 13 + ("end_request_success.bin", End_request, 1); 14 + ("get_values.bin", Get_values, 0); 15 + ("get_values_result.bin", Get_values_result, 0); 16 + ("params_empty.bin", Params, 1); 17 + ("params_get.bin", Params, 1); 18 + ("params_post.bin", Params, 1); 19 + ("stderr_empty.bin", Stderr, 1); 20 + ("stderr_message.bin", Stderr, 1); 21 + ("stdin_empty.bin", Stdin, 1); 22 + ("stdin_form_data.bin", Stdin, 1); 23 + ("stdout_empty.bin", Stdout, 1); 24 + ("stdout_response.bin", Stdout, 1); 25 + ("unknown_type.bin", Unknown_type, 0); 26 + ] 27 + 28 + let test_binary_file filename expected_type expected_request_id = 29 + Printf.printf "Testing %s... " filename; 30 + 31 + let content = 32 + let ic = open_in_bin ("test_cases/" ^ filename) in 33 + let len = in_channel_length ic in 34 + let content = really_input_string ic len in 35 + close_in ic; 36 + content 37 + in 38 + 39 + let buf_read = Eio.Buf_read.of_string content in 40 + let parsed = Record.read buf_read in 41 + 42 + assert (parsed.version = 1); 43 + assert (parsed.record_type = expected_type); 44 + assert (parsed.request_id = expected_request_id); 45 + 46 + Printf.printf "✓\n%!" 47 + 48 + let test_params_decoding () = 49 + Printf.printf "Testing params record content decoding... "; 50 + 51 + let content = 52 + let ic = open_in_bin "test_cases/params_get.bin" in 53 + let len = in_channel_length ic in 54 + let content = really_input_string ic len in 55 + close_in ic; 56 + content 57 + in 58 + 59 + let buf_read = Eio.Buf_read.of_string content in 60 + let parsed = Record.read buf_read in 61 + 62 + (* Decode the params content *) 63 + let params = Record.KV.decode parsed.content in 64 + 65 + (* Check some expected environment variables *) 66 + assert (Record.KV.find "REQUEST_METHOD" params = "GET"); 67 + assert (Record.KV.find "SERVER_NAME" params = "localhost"); 68 + assert (Record.KV.find "SERVER_PORT" params = "80"); 69 + 70 + Printf.printf "✓\n%!" 71 + 72 + let test_large_record () = 73 + Printf.printf "Testing large record... "; 74 + 75 + let content = 76 + let ic = open_in_bin "test_cases/large_record.bin" in 77 + let len = in_channel_length ic in 78 + let content = really_input_string ic len in 79 + close_in ic; 80 + content 81 + in 82 + 83 + let buf_read = Eio.Buf_read.of_string content in 84 + let parsed = Record.read buf_read in 85 + 86 + assert (parsed.version = 1); 87 + assert (parsed.record_type = Stdout); 88 + assert (parsed.request_id = 1); 89 + assert (String.length parsed.content = 65000); 90 + 91 + Printf.printf "✓\n%!" 92 + 93 + let test_padded_record () = 94 + Printf.printf "Testing padded record... "; 95 + 96 + let content = 97 + let ic = open_in_bin "test_cases/padded_record.bin" in 98 + let len = in_channel_length ic in 99 + let content = really_input_string ic len in 100 + close_in ic; 101 + content 102 + in 103 + 104 + let buf_read = Eio.Buf_read.of_string content in 105 + let parsed = Record.read buf_read in 106 + 107 + assert (parsed.version = 1); 108 + assert (parsed.record_type = Stdout); 109 + assert (parsed.request_id = 1); 110 + assert (parsed.content = "Hello"); 111 + 112 + Printf.printf "✓\n%!" 113 + 114 + let test_multiplexed_records () = 115 + Printf.printf "Testing multiplexed records... "; 116 + 117 + let content = 118 + let ic = open_in_bin "test_cases/multiplexed_requests.bin" in 119 + let len = in_channel_length ic in 120 + let content = really_input_string ic len in 121 + close_in ic; 122 + content 123 + in 124 + 125 + let buf_read = Eio.Buf_read.of_string content in 126 + let records = ref [] in 127 + 128 + (* Read all records from the multiplexed stream *) 129 + (try 130 + while true do 131 + let record = Record.read buf_read in 132 + records := record :: !records 133 + done 134 + with End_of_file -> ()); 135 + 136 + let records = List.rev !records in 137 + 138 + (* Should have multiple records with different request IDs *) 139 + assert (List.length records > 5); 140 + 141 + (* Check that we have records for both request ID 1 and 2 *) 142 + let request_ids = List.map (fun r -> r.Record.request_id) records in 143 + assert (List.mem 1 request_ids); 144 + assert (List.mem 2 request_ids); 145 + 146 + Printf.printf "✓\n%!" 147 + 148 + let run_all_tests () = 149 + Printf.printf "Validating all FastCGI test case files...\n\n%!"; 150 + 151 + (* Test individual files *) 152 + List.iter (fun (filename, expected_type, expected_request_id) -> 153 + test_binary_file filename expected_type expected_request_id 154 + ) test_cases; 155 + 156 + Printf.printf "\nTesting specific content decoding...\n%!"; 157 + test_params_decoding (); 158 + test_large_record (); 159 + test_padded_record (); 160 + test_multiplexed_records (); 161 + 162 + Printf.printf "\n✅ All %d test case files validated successfully!\n%!" (List.length test_cases); 163 + Printf.printf "✅ FastCGI Record implementation is working correctly!\n%!" 164 + 165 + let () = run_all_tests ()
test_cases/README.md test/test_cases/README.md
test_cases/abort_request.bin test/test_cases/abort_request.bin
test_cases/begin_request_authorizer.bin test/test_cases/begin_request_authorizer.bin
test_cases/begin_request_filter.bin test/test_cases/begin_request_filter.bin
test_cases/begin_request_no_keep.bin test/test_cases/begin_request_no_keep.bin
test_cases/begin_request_responder.bin test/test_cases/begin_request_responder.bin
test_cases/data_empty.bin test/test_cases/data_empty.bin
test_cases/data_filter.bin test/test_cases/data_filter.bin
test_cases/end_request_error.bin test/test_cases/end_request_error.bin
test_cases/end_request_success.bin test/test_cases/end_request_success.bin
test_cases/generate_test_cases.py test/test_cases/generate_test_cases.py
test_cases/get_values.bin test/test_cases/get_values.bin
test_cases/get_values_result.bin test/test_cases/get_values_result.bin
test_cases/large_record.bin test/test_cases/large_record.bin
test_cases/multiplexed_requests.bin test/test_cases/multiplexed_requests.bin
test_cases/padded_record.bin test/test_cases/padded_record.bin
test_cases/params_empty.bin test/test_cases/params_empty.bin
test_cases/params_get.bin test/test_cases/params_get.bin
test_cases/params_post.bin test/test_cases/params_post.bin
test_cases/stderr_empty.bin test/test_cases/stderr_empty.bin
test_cases/stderr_message.bin test/test_cases/stderr_message.bin
test_cases/stdin_empty.bin test/test_cases/stdin_empty.bin
test_cases/stdin_form_data.bin test/test_cases/stdin_form_data.bin
test_cases/stdout_empty.bin test/test_cases/stdout_empty.bin
test_cases/stdout_response.bin test/test_cases/stdout_response.bin
test_cases/test_case_sizes.txt test/test_cases/test_case_sizes.txt
test_cases/unknown_type.bin test/test_cases/unknown_type.bin
test_cases/validate_test_cases.py test/test_cases/validate_test_cases.py
test_cases/validation_results.txt test/test_cases/validation_results.txt