WebSocket frame codec (RFC 6455)
0
fork

Configure Feed

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

websocket: extract test_websocket suite, polish lib

- Move all alcotest cases out of test/test.ml into test/test_websocket.ml
exporting a single suite, dropping the redundant Test. module prefix
from each case name.
- Rename Websocket.get_byte -> byte_at (drop redundant get_ prefix).
- Document pp_opcode.
- Convert fuzz/dune to executable + alias rule layout matching the rest
of the repo.
- Add .ocamlformat at the repo's pinned 0.29.0.

+288 -294
+1
.ocamlformat
··· 1 + version = 0.29.0
+20 -1
fuzz/dune
··· 1 - (test 1 + (executable 2 2 (name fuzz) 3 + (modules fuzz fuzz_websocket) 3 4 (libraries websocket alcobar)) 5 + 6 + (rule 7 + (alias runtest) 8 + (enabled_if 9 + (<> %{profile} afl)) 10 + (deps fuzz.exe) 11 + (action 12 + (run %{exe:fuzz.exe}))) 13 + 14 + (rule 15 + (alias fuzz) 16 + (enabled_if 17 + (= %{profile} afl)) 18 + (deps fuzz.exe) 19 + (action 20 + (progn 21 + (run %{exe:fuzz.exe} --gen-corpus corpus) 22 + (run afl-fuzz -V 60 -i corpus -o _fuzz -- %{exe:fuzz.exe} @@))))
+5 -5
lib/websocket.ml
··· 97 97 98 98 (* Decode *) 99 99 100 - let get_byte s i = Char.code (String.get s i) 100 + let byte_at s i = Char.code (String.get s i) 101 101 102 102 let decode buf = 103 103 let blen = String.length buf in 104 104 if blen < 2 then Error `Need_more 105 105 else 106 - let b0 = get_byte buf 0 in 107 - let b1 = get_byte buf 1 in 106 + let b0 = byte_at buf 0 in 107 + let b1 = byte_at buf 1 in 108 108 let fin = b0 land 0x80 <> 0 in 109 109 let opcode_n = b0 land 0x0F in 110 110 let masked = b1 land 0x80 <> 0 in ··· 116 116 if payload_len_7 < 126 then (2, payload_len_7) 117 117 else if payload_len_7 = 126 then 118 118 if blen < 4 then (4, -1) 119 - else (4, (get_byte buf 2 lsl 8) lor get_byte buf 3) 119 + else (4, (byte_at buf 2 lsl 8) lor byte_at buf 3) 120 120 else if blen < 10 then (10, -1) 121 121 else 122 122 let len = ref 0 in 123 123 for i = 0 to 7 do 124 - len := (!len lsl 8) lor get_byte buf (2 + i) 124 + len := (!len lsl 8) lor byte_at buf (2 + i) 125 125 done; 126 126 (10, !len) 127 127 in
+1
lib/websocket.mli
··· 17 17 type opcode = Continuation | Text | Binary | Close | Ping | Pong 18 18 19 19 val pp_opcode : opcode Fmt.t 20 + (** [pp_opcode ppf op] pretty-prints the opcode name. *) 20 21 21 22 (** {1 Frames} *) 22 23
+1 -288
test/test.ml
··· 1 - (** Tests for WebSocket frame codec. 2 - 3 - Test vectors derived from RFC 6455 §5.7 examples and adversarial edge cases. 4 - Follows the same structure as ocaml-sse tests: spec vectors, incremental 5 - parsing, and degenerate inputs. *) 6 - 7 - let ws = 8 - Alcotest.testable Websocket.pp (fun a b -> 9 - a.Websocket.fin = b.fin && a.opcode = b.opcode && a.payload = b.payload) 10 - 11 - (* {1 RFC 6455 §5.7 Examples} *) 12 - 13 - (* Source: RFC 6455 §5.7 "A single-frame unmasked text message" *) 14 - let test_rfc_unmasked_text () = 15 - let bytes = "\x81\x05Hello" in 16 - match Websocket.decode bytes with 17 - | Ok (f, rest) -> 18 - Alcotest.(check ws) 19 - "unmasked Hello" 20 - { fin = true; opcode = Text; payload = "Hello" } 21 - f; 22 - Alcotest.(check string) "no rest" "" rest 23 - | Error _ -> Alcotest.fail "decode failed" 24 - 25 - (* Source: RFC 6455 §5.7 "A single-frame masked text message" *) 26 - let test_rfc_masked_text () = 27 - (* Mask key: 37 fa 21 3d, masked "Hello" *) 28 - let bytes = "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58" in 29 - match Websocket.decode bytes with 30 - | Ok (f, _) -> 31 - Alcotest.(check ws) 32 - "masked Hello" 33 - { fin = true; opcode = Text; payload = "Hello" } 34 - f 35 - | Error _ -> Alcotest.fail "decode failed" 36 - 37 - (* Source: RFC 6455 §5.7 "A fragmented unmasked text message" *) 38 - let test_rfc_fragmented () = 39 - let frag1 = "\x01\x03Hel" in 40 - let frag2 = "\x80\x02lo" in 41 - match Websocket.decode frag1 with 42 - | Ok (f1, _) -> ( 43 - Alcotest.(check bool) "not fin" false f1.fin; 44 - Alcotest.(check string) "first fragment" "Hel" f1.payload; 45 - match Websocket.decode frag2 with 46 - | Ok (f2, _) -> 47 - Alcotest.(check bool) "fin" true f2.fin; 48 - Alcotest.(check string) "second fragment" "lo" f2.payload 49 - | Error _ -> Alcotest.fail "decode frag2 failed") 50 - | Error _ -> Alcotest.fail "decode frag1 failed" 51 - 52 - (* Source: RFC 6455 §5.7 "Unmasked Ping request and masked Ping response" *) 53 - let test_rfc_ping () = 54 - let bytes = "\x89\x05Hello" in 55 - match Websocket.decode bytes with 56 - | Ok (f, _) -> 57 - Alcotest.(check ws) 58 - "ping" 59 - { fin = true; opcode = Ping; payload = "Hello" } 60 - f 61 - | Error _ -> Alcotest.fail "decode failed" 62 - 63 - (* Source: RFC 6455 §5.7 "256 bytes binary message in a single unmasked frame" *) 64 - let test_rfc_256_bytes () = 65 - let payload = String.init 256 (fun i -> Char.chr (i land 0xFF)) in 66 - (* 126 = extended 16-bit length *) 67 - let header = "\x82\x7e\x01\x00" in 68 - let bytes = header ^ payload in 69 - match Websocket.decode bytes with 70 - | Ok (f, _) -> 71 - Alcotest.(check int) "payload length" 256 (String.length f.payload); 72 - Alcotest.(check string) "payload" payload f.payload 73 - | Error _ -> Alcotest.fail "decode failed" 74 - 75 - (* Source: RFC 6455 §5.7 "64KiB binary message" — uses 8-byte extended length *) 76 - let test_rfc_64k () = 77 - let payload = String.make 65536 '\xAA' in 78 - let header = "\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x00" in 79 - let bytes = header ^ payload in 80 - match Websocket.decode bytes with 81 - | Ok (f, _) -> 82 - Alcotest.(check int) "64K payload" 65536 (String.length f.payload) 83 - | Error _ -> Alcotest.fail "decode failed" 84 - 85 - (* {1 Roundtrip tests} *) 86 - 87 - let test_roundtrip_unmasked () = 88 - let frame = Websocket.text "roundtrip" in 89 - let encoded = Websocket.encode ~mask:false frame in 90 - match Websocket.decode encoded with 91 - | Ok (decoded, rest) -> 92 - Alcotest.(check ws) "roundtrip" frame decoded; 93 - Alcotest.(check string) "no rest" "" rest 94 - | Error _ -> Alcotest.fail "decode failed" 95 - 96 - let test_roundtrip_masked () = 97 - let frame = Websocket.binary "\x00\x01\x02\xFF" in 98 - let encoded = Websocket.encode ~mask:true frame in 99 - match Websocket.decode encoded with 100 - | Ok (decoded, _) -> Alcotest.(check ws) "masked roundtrip" frame decoded 101 - | Error _ -> Alcotest.fail "decode failed" 102 - 103 - let test_roundtrip_close () = 104 - let frame = Websocket.close ~code:1001 ~reason:"going away" () in 105 - let encoded = Websocket.encode ~mask:false frame in 106 - match Websocket.decode encoded with 107 - | Ok (decoded, _) -> 108 - Alcotest.(check bool) "fin" true decoded.fin; 109 - let code = Bytes.get_uint16_be (Bytes.of_string decoded.payload) 0 in 110 - Alcotest.(check int) "close code" 1001 code; 111 - let reason = 112 - String.sub decoded.payload 2 (String.length decoded.payload - 2) 113 - in 114 - Alcotest.(check string) "reason" "going away" reason 115 - | Error _ -> Alcotest.fail "decode failed" 116 - 117 - (* {1 Incremental / chunked parsing} *) 118 - 119 - let test_byte_at_a_time () = 120 - let frame = Websocket.text "byte by byte" in 121 - let encoded = Websocket.encode ~mask:false frame in 122 - let len = String.length encoded in 123 - (* Feed all but last byte — should need more *) 124 - let partial = String.sub encoded 0 (len - 1) in 125 - (match Websocket.decode partial with 126 - | Error `Need_more -> () 127 - | _ -> Alcotest.fail "expected Need_more for partial frame"); 128 - (* Feed full frame *) 129 - match Websocket.decode encoded with 130 - | Ok (decoded, _) -> Alcotest.(check ws) "full frame" frame decoded 131 - | Error _ -> Alcotest.fail "decode failed" 132 - 133 - let test_concatenated_frames () = 134 - let f1 = Websocket.text "one" in 135 - let f2 = Websocket.binary "two" in 136 - let f3 = Websocket.ping "" in 137 - let buf = 138 - Websocket.encode ~mask:false f1 139 - ^ Websocket.encode ~mask:false f2 140 - ^ Websocket.encode ~mask:false f3 141 - in 142 - match Websocket.decode buf with 143 - | Ok (d1, rest1) -> ( 144 - Alcotest.(check ws) "first" f1 d1; 145 - match Websocket.decode rest1 with 146 - | Ok (d2, rest2) -> ( 147 - Alcotest.(check ws) "second" f2 d2; 148 - match Websocket.decode rest2 with 149 - | Ok (d3, rest3) -> 150 - Alcotest.(check ws) "third" f3 d3; 151 - Alcotest.(check string) "empty" "" rest3 152 - | Error _ -> Alcotest.fail "third decode failed") 153 - | Error _ -> Alcotest.fail "second decode failed") 154 - | Error _ -> Alcotest.fail "first decode failed" 155 - 156 - (* {1 Adversarial / edge cases} *) 157 - 158 - let test_empty_payload () = 159 - let frame = Websocket.text "" in 160 - let encoded = Websocket.encode ~mask:false frame in 161 - match Websocket.decode encoded with 162 - | Ok (decoded, _) -> Alcotest.(check ws) "empty payload" frame decoded 163 - | Error _ -> Alcotest.fail "decode failed" 164 - 165 - let test_125_byte_payload () = 166 - (* Maximum payload for 7-bit length field *) 167 - let payload = String.make 125 'x' in 168 - let frame = Websocket.text payload in 169 - let encoded = Websocket.encode ~mask:false frame in 170 - match Websocket.decode encoded with 171 - | Ok (decoded, _) -> Alcotest.(check ws) "125 bytes" frame decoded 172 - | Error _ -> Alcotest.fail "decode failed" 173 - 174 - let test_126_byte_payload () = 175 - (* Minimum payload for 16-bit extended length *) 176 - let payload = String.make 126 'y' in 177 - let frame = Websocket.text payload in 178 - let encoded = Websocket.encode ~mask:false frame in 179 - match Websocket.decode encoded with 180 - | Ok (decoded, _) -> Alcotest.(check ws) "126 bytes" frame decoded 181 - | Error _ -> Alcotest.fail "decode failed" 182 - 183 - let test_empty_buffer () = 184 - match Websocket.decode "" with 185 - | Error `Need_more -> () 186 - | _ -> Alcotest.fail "expected Need_more for empty buffer" 187 - 188 - let test_single_byte () = 189 - match Websocket.decode "\x81" with 190 - | Error `Need_more -> () 191 - | _ -> Alcotest.fail "expected Need_more for single byte" 192 - 193 - let test_truncated_extended_length () = 194 - (* 126 = 16-bit extended, but only 1 of 2 length bytes present *) 195 - match Websocket.decode "\x82\x7e\x01" with 196 - | Error `Need_more -> () 197 - | _ -> Alcotest.fail "expected Need_more for truncated extended length" 198 - 199 - let test_truncated_64bit_length () = 200 - (* 127 = 64-bit extended, but only 4 of 8 length bytes present *) 201 - match Websocket.decode "\x82\x7f\x00\x00\x00\x01" with 202 - | Error `Need_more -> () 203 - | _ -> Alcotest.fail "expected Need_more for truncated 64-bit length" 204 - 205 - let test_unknown_opcode () = 206 - match Websocket.decode "\x83\x00" with 207 - | Error (`Invalid _) -> () 208 - | _ -> Alcotest.fail "expected Invalid for reserved opcode 3" 209 - 210 - let test_all_control_opcodes () = 211 - (* Close *) 212 - (match Websocket.decode "\x88\x00" with 213 - | Ok (f, _) -> Alcotest.(check bool) "close opcode" (f.opcode = Close) true 214 - | Error _ -> Alcotest.fail "close decode failed"); 215 - (* Ping *) 216 - (match Websocket.decode "\x89\x00" with 217 - | Ok (f, _) -> Alcotest.(check bool) "ping opcode" (f.opcode = Ping) true 218 - | Error _ -> Alcotest.fail "ping decode failed"); 219 - (* Pong *) 220 - match Websocket.decode "\x8A\x00" with 221 - | Ok (f, _) -> Alcotest.(check bool) "pong opcode" (f.opcode = Pong) true 222 - | Error _ -> Alcotest.fail "pong decode failed" 223 - 224 - let test_binary_payload_all_bytes () = 225 - (* Every byte value 0x00-0xFF *) 226 - let payload = String.init 256 (fun i -> Char.chr i) in 227 - let frame = Websocket.binary payload in 228 - let encoded = Websocket.encode ~mask:false frame in 229 - match Websocket.decode encoded with 230 - | Ok (decoded, _) -> Alcotest.(check ws) "all byte values" frame decoded 231 - | Error _ -> Alcotest.fail "decode failed" 232 - 233 - let test_mask_determinism () = 234 - (* Two masked encodings of same frame should decode to same payload *) 235 - let frame = Websocket.text "deterministic" in 236 - let e1 = Websocket.encode ~mask:true frame in 237 - let e2 = Websocket.encode ~mask:true frame in 238 - (* Encoded bytes differ (different random masks) *) 239 - Alcotest.(check bool) "different wire bytes" true (e1 <> e2); 240 - (* But decoded payloads match *) 241 - let d1 = Result.get_ok (Websocket.decode e1) |> fst in 242 - let d2 = Result.get_ok (Websocket.decode e2) |> fst in 243 - Alcotest.(check ws) "same payload" d1 d2 244 - 245 - let () = 246 - Alcotest.run "websocket" 247 - [ 248 - ( "rfc-6455", 249 - [ 250 - Alcotest.test_case "§5.7 unmasked text" `Quick test_rfc_unmasked_text; 251 - Alcotest.test_case "§5.7 masked text" `Quick test_rfc_masked_text; 252 - Alcotest.test_case "§5.7 fragmented" `Quick test_rfc_fragmented; 253 - Alcotest.test_case "§5.7 ping" `Quick test_rfc_ping; 254 - Alcotest.test_case "§5.7 256 bytes" `Quick test_rfc_256_bytes; 255 - Alcotest.test_case "§5.7 64K" `Quick test_rfc_64k; 256 - ] ); 257 - ( "roundtrip", 258 - [ 259 - Alcotest.test_case "unmasked" `Quick test_roundtrip_unmasked; 260 - Alcotest.test_case "masked" `Quick test_roundtrip_masked; 261 - Alcotest.test_case "close" `Quick test_roundtrip_close; 262 - ] ); 263 - ( "incremental", 264 - [ 265 - Alcotest.test_case "byte at a time" `Quick test_byte_at_a_time; 266 - Alcotest.test_case "concatenated" `Quick test_concatenated_frames; 267 - ] ); 268 - ( "adversarial", 269 - [ 270 - Alcotest.test_case "empty payload" `Quick test_empty_payload; 271 - Alcotest.test_case "125 bytes (max 7-bit)" `Quick 272 - test_125_byte_payload; 273 - Alcotest.test_case "126 bytes (min 16-bit)" `Quick 274 - test_126_byte_payload; 275 - Alcotest.test_case "empty buffer" `Quick test_empty_buffer; 276 - Alcotest.test_case "single byte" `Quick test_single_byte; 277 - Alcotest.test_case "truncated 16-bit len" `Quick 278 - test_truncated_extended_length; 279 - Alcotest.test_case "truncated 64-bit len" `Quick 280 - test_truncated_64bit_length; 281 - Alcotest.test_case "unknown opcode" `Quick test_unknown_opcode; 282 - Alcotest.test_case "all control opcodes" `Quick 283 - test_all_control_opcodes; 284 - Alcotest.test_case "all byte values" `Quick 285 - test_binary_payload_all_bytes; 286 - Alcotest.test_case "mask determinism" `Quick test_mask_determinism; 287 - ] ); 288 - ] 1 + let () = Alcotest.run "websocket" [ Test_websocket.suite ]
+255
test/test_websocket.ml
··· 1 + (** Tests for WebSocket frame codec. 2 + 3 + Test vectors derived from RFC 6455 §5.7 examples and adversarial edge cases. 4 + Follows the same structure as ocaml-sse tests: spec vectors, incremental 5 + parsing, and degenerate inputs. *) 6 + 7 + let ws = 8 + Alcotest.testable Websocket.pp (fun a b -> 9 + a.Websocket.fin = b.fin && a.opcode = b.opcode && a.payload = b.payload) 10 + 11 + (* {1 RFC 6455 §5.7 Examples} *) 12 + 13 + (* Source: RFC 6455 §5.7 "A single-frame unmasked text message" *) 14 + let rfc_unmasked_text () = 15 + let bytes = "\x81\x05Hello" in 16 + match Websocket.decode bytes with 17 + | Ok (f, rest) -> 18 + Alcotest.(check ws) 19 + "unmasked Hello" 20 + { fin = true; opcode = Text; payload = "Hello" } 21 + f; 22 + Alcotest.(check string) "no rest" "" rest 23 + | Error _ -> Alcotest.fail "decode failed" 24 + 25 + (* Source: RFC 6455 §5.7 "A single-frame masked text message" *) 26 + let rfc_masked_text () = 27 + let bytes = "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58" in 28 + match Websocket.decode bytes with 29 + | Ok (f, _) -> 30 + Alcotest.(check ws) 31 + "masked Hello" 32 + { fin = true; opcode = Text; payload = "Hello" } 33 + f 34 + | Error _ -> Alcotest.fail "decode failed" 35 + 36 + (* Source: RFC 6455 §5.7 "A fragmented unmasked text message" *) 37 + let rfc_fragmented () = 38 + let frag1 = "\x01\x03Hel" in 39 + let frag2 = "\x80\x02lo" in 40 + match Websocket.decode frag1 with 41 + | Ok (f1, _) -> ( 42 + Alcotest.(check bool) "not fin" false f1.fin; 43 + Alcotest.(check string) "first fragment" "Hel" f1.payload; 44 + match Websocket.decode frag2 with 45 + | Ok (f2, _) -> 46 + Alcotest.(check bool) "fin" true f2.fin; 47 + Alcotest.(check string) "second fragment" "lo" f2.payload 48 + | Error _ -> Alcotest.fail "decode frag2 failed") 49 + | Error _ -> Alcotest.fail "decode frag1 failed" 50 + 51 + (* Source: RFC 6455 §5.7 "Unmasked Ping request and masked Ping response" *) 52 + let rfc_ping () = 53 + let bytes = "\x89\x05Hello" in 54 + match Websocket.decode bytes with 55 + | Ok (f, _) -> 56 + Alcotest.(check ws) 57 + "ping" 58 + { fin = true; opcode = Ping; payload = "Hello" } 59 + f 60 + | Error _ -> Alcotest.fail "decode failed" 61 + 62 + (* Source: RFC 6455 §5.7 "256 bytes binary message in a single unmasked frame" *) 63 + let rfc_256_bytes () = 64 + let payload = String.init 256 (fun i -> Char.chr (i land 0xFF)) in 65 + let header = "\x82\x7e\x01\x00" in 66 + let bytes = header ^ payload in 67 + match Websocket.decode bytes with 68 + | Ok (f, _) -> 69 + Alcotest.(check int) "payload length" 256 (String.length f.payload); 70 + Alcotest.(check string) "payload" payload f.payload 71 + | Error _ -> Alcotest.fail "decode failed" 72 + 73 + (* Source: RFC 6455 §5.7 "64KiB binary message" — uses 8-byte extended length *) 74 + let rfc_64k () = 75 + let payload = String.make 65536 '\xAA' in 76 + let header = "\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x00" in 77 + let bytes = header ^ payload in 78 + match Websocket.decode bytes with 79 + | Ok (f, _) -> 80 + Alcotest.(check int) "64K payload" 65536 (String.length f.payload) 81 + | Error _ -> Alcotest.fail "decode failed" 82 + 83 + (* {1 Roundtrip tests} *) 84 + 85 + let roundtrip_unmasked () = 86 + let frame = Websocket.text "roundtrip" in 87 + let encoded = Websocket.encode ~mask:false frame in 88 + match Websocket.decode encoded with 89 + | Ok (decoded, rest) -> 90 + Alcotest.(check ws) "roundtrip" frame decoded; 91 + Alcotest.(check string) "no rest" "" rest 92 + | Error _ -> Alcotest.fail "decode failed" 93 + 94 + let roundtrip_masked () = 95 + let frame = Websocket.binary "\x00\x01\x02\xFF" in 96 + let encoded = Websocket.encode ~mask:true frame in 97 + match Websocket.decode encoded with 98 + | Ok (decoded, _) -> Alcotest.(check ws) "masked roundtrip" frame decoded 99 + | Error _ -> Alcotest.fail "decode failed" 100 + 101 + let roundtrip_close () = 102 + let frame = Websocket.close ~code:1001 ~reason:"going away" () in 103 + let encoded = Websocket.encode ~mask:false frame in 104 + match Websocket.decode encoded with 105 + | Ok (decoded, _) -> 106 + Alcotest.(check bool) "fin" true decoded.fin; 107 + let code = Bytes.get_uint16_be (Bytes.of_string decoded.payload) 0 in 108 + Alcotest.(check int) "close code" 1001 code; 109 + let reason = 110 + String.sub decoded.payload 2 (String.length decoded.payload - 2) 111 + in 112 + Alcotest.(check string) "reason" "going away" reason 113 + | Error _ -> Alcotest.fail "decode failed" 114 + 115 + (* {1 Incremental / chunked parsing} *) 116 + 117 + let byte_at_a_time () = 118 + let frame = Websocket.text "byte by byte" in 119 + let encoded = Websocket.encode ~mask:false frame in 120 + let len = String.length encoded in 121 + let partial = String.sub encoded 0 (len - 1) in 122 + (match Websocket.decode partial with 123 + | Error `Need_more -> () 124 + | _ -> Alcotest.fail "expected Need_more for partial frame"); 125 + match Websocket.decode encoded with 126 + | Ok (decoded, _) -> Alcotest.(check ws) "full frame" frame decoded 127 + | Error _ -> Alcotest.fail "decode failed" 128 + 129 + let concatenated_frames () = 130 + let f1 = Websocket.text "one" in 131 + let f2 = Websocket.binary "two" in 132 + let f3 = Websocket.ping "" in 133 + let buf = 134 + Websocket.encode ~mask:false f1 135 + ^ Websocket.encode ~mask:false f2 136 + ^ Websocket.encode ~mask:false f3 137 + in 138 + match Websocket.decode buf with 139 + | Ok (d1, rest1) -> ( 140 + Alcotest.(check ws) "first" f1 d1; 141 + match Websocket.decode rest1 with 142 + | Ok (d2, rest2) -> ( 143 + Alcotest.(check ws) "second" f2 d2; 144 + match Websocket.decode rest2 with 145 + | Ok (d3, rest3) -> 146 + Alcotest.(check ws) "third" f3 d3; 147 + Alcotest.(check string) "empty" "" rest3 148 + | Error _ -> Alcotest.fail "third decode failed") 149 + | Error _ -> Alcotest.fail "second decode failed") 150 + | Error _ -> Alcotest.fail "first decode failed" 151 + 152 + (* {1 Adversarial / edge cases} *) 153 + 154 + let empty_payload () = 155 + let frame = Websocket.text "" in 156 + let encoded = Websocket.encode ~mask:false frame in 157 + match Websocket.decode encoded with 158 + | Ok (decoded, _) -> Alcotest.(check ws) "empty payload" frame decoded 159 + | Error _ -> Alcotest.fail "decode failed" 160 + 161 + let payload_125_bytes () = 162 + let payload = String.make 125 'x' in 163 + let frame = Websocket.text payload in 164 + let encoded = Websocket.encode ~mask:false frame in 165 + match Websocket.decode encoded with 166 + | Ok (decoded, _) -> Alcotest.(check ws) "125 bytes" frame decoded 167 + | Error _ -> Alcotest.fail "decode failed" 168 + 169 + let payload_126_bytes () = 170 + let payload = String.make 126 'y' in 171 + let frame = Websocket.text payload in 172 + let encoded = Websocket.encode ~mask:false frame in 173 + match Websocket.decode encoded with 174 + | Ok (decoded, _) -> Alcotest.(check ws) "126 bytes" frame decoded 175 + | Error _ -> Alcotest.fail "decode failed" 176 + 177 + let empty_buffer () = 178 + match Websocket.decode "" with 179 + | Error `Need_more -> () 180 + | _ -> Alcotest.fail "expected Need_more for empty buffer" 181 + 182 + let single_byte () = 183 + match Websocket.decode "\x81" with 184 + | Error `Need_more -> () 185 + | _ -> Alcotest.fail "expected Need_more for single byte" 186 + 187 + let truncated_extended_length () = 188 + match Websocket.decode "\x82\x7e\x01" with 189 + | Error `Need_more -> () 190 + | _ -> Alcotest.fail "expected Need_more for truncated extended length" 191 + 192 + let truncated_64bit_length () = 193 + match Websocket.decode "\x82\x7f\x00\x00\x00\x01" with 194 + | Error `Need_more -> () 195 + | _ -> Alcotest.fail "expected Need_more for truncated 64-bit length" 196 + 197 + let unknown_opcode () = 198 + match Websocket.decode "\x83\x00" with 199 + | Error (`Invalid _) -> () 200 + | _ -> Alcotest.fail "expected Invalid for reserved opcode 3" 201 + 202 + let all_control_opcodes () = 203 + (match Websocket.decode "\x88\x00" with 204 + | Ok (f, _) -> Alcotest.(check bool) "close opcode" (f.opcode = Close) true 205 + | Error _ -> Alcotest.fail "close decode failed"); 206 + (match Websocket.decode "\x89\x00" with 207 + | Ok (f, _) -> Alcotest.(check bool) "ping opcode" (f.opcode = Ping) true 208 + | Error _ -> Alcotest.fail "ping decode failed"); 209 + match Websocket.decode "\x8A\x00" with 210 + | Ok (f, _) -> Alcotest.(check bool) "pong opcode" (f.opcode = Pong) true 211 + | Error _ -> Alcotest.fail "pong decode failed" 212 + 213 + let binary_all_bytes () = 214 + let payload = String.init 256 (fun i -> Char.chr i) in 215 + let frame = Websocket.binary payload in 216 + let encoded = Websocket.encode ~mask:false frame in 217 + match Websocket.decode encoded with 218 + | Ok (decoded, _) -> Alcotest.(check ws) "all byte values" frame decoded 219 + | Error _ -> Alcotest.fail "decode failed" 220 + 221 + let mask_determinism () = 222 + let frame = Websocket.text "deterministic" in 223 + let e1 = Websocket.encode ~mask:true frame in 224 + let e2 = Websocket.encode ~mask:true frame in 225 + Alcotest.(check bool) "different wire bytes" true (e1 <> e2); 226 + let d1 = Result.get_ok (Websocket.decode e1) |> fst in 227 + let d2 = Result.get_ok (Websocket.decode e2) |> fst in 228 + Alcotest.(check ws) "same payload" d1 d2 229 + 230 + let suite = 231 + ( "websocket", 232 + [ 233 + Alcotest.test_case "rfc §5.7 unmasked text" `Quick rfc_unmasked_text; 234 + Alcotest.test_case "rfc §5.7 masked text" `Quick rfc_masked_text; 235 + Alcotest.test_case "rfc §5.7 fragmented" `Quick rfc_fragmented; 236 + Alcotest.test_case "rfc §5.7 ping" `Quick rfc_ping; 237 + Alcotest.test_case "rfc §5.7 256 bytes" `Quick rfc_256_bytes; 238 + Alcotest.test_case "rfc §5.7 64K" `Quick rfc_64k; 239 + Alcotest.test_case "roundtrip unmasked" `Quick roundtrip_unmasked; 240 + Alcotest.test_case "roundtrip masked" `Quick roundtrip_masked; 241 + Alcotest.test_case "roundtrip close" `Quick roundtrip_close; 242 + Alcotest.test_case "byte at a time" `Quick byte_at_a_time; 243 + Alcotest.test_case "concatenated" `Quick concatenated_frames; 244 + Alcotest.test_case "empty payload" `Quick empty_payload; 245 + Alcotest.test_case "125 bytes (max 7-bit)" `Quick payload_125_bytes; 246 + Alcotest.test_case "126 bytes (min 16-bit)" `Quick payload_126_bytes; 247 + Alcotest.test_case "empty buffer" `Quick empty_buffer; 248 + Alcotest.test_case "single byte" `Quick single_byte; 249 + Alcotest.test_case "truncated 16-bit len" `Quick truncated_extended_length; 250 + Alcotest.test_case "truncated 64-bit len" `Quick truncated_64bit_length; 251 + Alcotest.test_case "unknown opcode" `Quick unknown_opcode; 252 + Alcotest.test_case "all control opcodes" `Quick all_control_opcodes; 253 + Alcotest.test_case "all byte values" `Quick binary_all_bytes; 254 + Alcotest.test_case "mask determinism" `Quick mask_determinism; 255 + ] )
+5
test/test_websocket.mli
··· 1 + (** Tests for the [websocket] frame codec. *) 2 + 3 + val suite : string * unit Alcotest.test_case list 4 + (** [suite] is the alcotest suite covering RFC 6455 vectors, roundtrip, 5 + incremental parsing, and adversarial cases. *)