File Coverage

lib/Hypersonic/Protocol/WebSocket/Frame.pm
Criterion Covered Total %
statement 160 160 100.0
branch 34 38 89.4
condition 7 14 50.0
subroutine 18 18 100.0
pod 0 13 0.0
total 219 243 90.1


line stmt bran cond sub pod time code
1             package Hypersonic::Protocol::WebSocket::Frame;
2 3     3   214557 use strict;
  3         3  
  3         93  
3 3     3   9 use warnings;
  3         3  
  3         110  
4 3     3   1427 use Hypersonic::JIT::Util;
  3         11  
  3         145  
5              
6             # Hypersonic::Protocol::WebSocket::Frame - WebSocket frame encoding/decoding
7             #
8             # Implements RFC 6455 Section 5 binary frame protocol:
9             # - Frame encoding (server->client, unmasked)
10             # - Frame decoding (client->server, masked)
11             # - Control frame handling (ping/pong/close)
12             # - Message fragmentation
13             #
14             # All C code generated at compile time for JIT compilation.
15              
16             our $VERSION = '0.15';
17              
18             # WebSocket opcodes (RFC 6455 Section 5.2)
19             use constant {
20 3         363 OP_CONTINUATION => 0x0,
21             OP_TEXT => 0x1,
22             OP_BINARY => 0x2,
23             OP_CLOSE => 0x8,
24             OP_PING => 0x9,
25             OP_PONG => 0xA,
26 3     3   20 };
  3         4  
27              
28             # Close codes (RFC 6455 Section 7.4.1)
29             use constant {
30 3         7425 CLOSE_NORMAL => 1000,
31             CLOSE_GOING_AWAY => 1001,
32             CLOSE_PROTOCOL_ERROR => 1002,
33             CLOSE_UNSUPPORTED_DATA => 1003,
34             CLOSE_NO_STATUS => 1005,
35             CLOSE_ABNORMAL => 1006,
36             CLOSE_INVALID_PAYLOAD => 1007,
37             CLOSE_POLICY_VIOLATION => 1008,
38             CLOSE_MESSAGE_TOO_BIG => 1009,
39             CLOSE_MANDATORY_EXT => 1010,
40             CLOSE_INTERNAL_ERROR => 1011,
41 3     3   17 };
  3         6  
42              
43             # Export constants
44             sub opcodes {
45             return {
46 1     1 0 2086 continuation => OP_CONTINUATION,
47             text => OP_TEXT,
48             binary => OP_BINARY,
49             close => OP_CLOSE,
50             ping => OP_PING,
51             pong => OP_PONG,
52             };
53             }
54              
55             sub close_codes {
56             return {
57 1     1 0 6570 normal => CLOSE_NORMAL,
58             going_away => CLOSE_GOING_AWAY,
59             protocol_error => CLOSE_PROTOCOL_ERROR,
60             unsupported_data => CLOSE_UNSUPPORTED_DATA,
61             invalid_payload => CLOSE_INVALID_PAYLOAD,
62             policy_violation => CLOSE_POLICY_VIOLATION,
63             message_too_big => CLOSE_MESSAGE_TOO_BIG,
64             internal_error => CLOSE_INTERNAL_ERROR,
65             };
66             }
67              
68             # Generate C constants and structures
69             sub gen_frame_constants {
70 3     3 0 6501 my ($class, $builder) = @_;
71            
72 3         184 $builder->comment('WebSocket opcodes (RFC 6455 Section 5.2)')
73             ->line('#define WS_OP_CONTINUATION 0x0')
74             ->line('#define WS_OP_TEXT 0x1')
75             ->line('#define WS_OP_BINARY 0x2')
76             ->line('#define WS_OP_CLOSE 0x8')
77             ->line('#define WS_OP_PING 0x9')
78             ->line('#define WS_OP_PONG 0xA')
79             ->blank
80             ->comment('Frame flags')
81             ->line('#define WS_FIN 0x80')
82             ->line('#define WS_MASK 0x80')
83             ->blank
84             ->comment('Payload length markers')
85             ->line('#define WS_LEN_16BIT 126')
86             ->line('#define WS_LEN_64BIT 127')
87             ->line('#define WS_MAX_SMALL_PAYLOAD 125')
88             ->line('#define WS_MAX_MEDIUM_PAYLOAD 65535')
89             ->blank
90             ->comment('Close codes (RFC 6455 Section 7.4.1)')
91             ->line('#define WS_CLOSE_NORMAL 1000')
92             ->line('#define WS_CLOSE_GOING_AWAY 1001')
93             ->line('#define WS_CLOSE_PROTOCOL_ERROR 1002')
94             ->line('#define WS_CLOSE_UNSUPPORTED_DATA 1003')
95             ->line('#define WS_CLOSE_INVALID_PAYLOAD 1007')
96             ->line('#define WS_CLOSE_POLICY_VIOLATION 1008')
97             ->line('#define WS_CLOSE_MESSAGE_TOO_BIG 1009')
98             ->line('#define WS_CLOSE_INTERNAL_ERROR 1011')
99             ->blank
100             ->comment('Parsed frame structure')
101             ->line('typedef struct {')
102             ->line(' uint8_t fin;')
103             ->line(' uint8_t rsv;')
104             ->line(' uint8_t opcode;')
105             ->line(' uint8_t masked;')
106             ->line(' uint64_t payload_length;')
107             ->line(' uint8_t mask_key[4];')
108             ->line(' uint8_t* payload;')
109             ->line(' size_t header_size;')
110             ->line(' size_t total_size;')
111             ->line('} WSFrame;')
112             ->blank;
113            
114 3         8 return $builder;
115             }
116              
117             # Generate frame decoder
118             sub gen_frame_decoder {
119 3     3 0 9233 my ($class, $builder) = @_;
120 3         20 my $inline = Hypersonic::JIT::Util->inline_keyword;
121              
122 3         1512 $builder->comment('Decode WebSocket frame from buffer')
123             ->comment('Returns: bytes consumed (>0), 0 if need more data, -1 on error')
124             ->line('static int ws_decode_frame(const uint8_t* buf, size_t len, WSFrame* frame) {');
125            
126 3         103 $builder->if('len < 2')
127             ->line('return 0;')
128             ->endif
129             ->blank
130             ->comment(' First byte: FIN + RSV + opcode')
131             ->line(' frame->fin = (buf[0] & 0x80) != 0;')
132             ->line(' frame->rsv = (buf[0] >> 4) & 0x07;')
133             ->line(' frame->opcode = buf[0] & 0x0F;')
134             ->blank
135             ->comment(' RSV bits must be 0 unless extension negotiated');
136            
137 3         165 $builder->if('frame->rsv != 0')
138             ->line('return -1;')
139             ->endif
140             ->blank
141             ->comment(' Second byte: MASK + payload length')
142             ->line(' frame->masked = (buf[1] & 0x80) != 0;')
143             ->line(' uint8_t len7 = buf[1] & 0x7F;')
144             ->blank
145             ->line(' size_t header_size = 2;')
146             ->line(' uint64_t payload_len;')
147             ->blank;
148            
149 3         43 $builder->if('len7 <= 125')
150             ->line('payload_len = len7;')
151             ->elsif('len7 == 126')
152             ->comment('16-bit extended length');
153            
154 3         75 $builder->if('len < 4')
155             ->line('return 0;')
156             ->endif
157             ->line('payload_len = ((uint64_t)buf[2] << 8) | buf[3];')
158             ->line('header_size = 4;')
159             ->else
160             ->comment('64-bit extended length');
161            
162 3         119 $builder->if('len < 10')
163             ->line('return 0;')
164             ->endif
165             ->line('payload_len = 0;')
166             ->line('{ int i;')
167             ->for('i = 0', 'i < 8', 'i++')
168             ->line('payload_len = (payload_len << 8) | buf[2 + i];')
169             ->endfor
170             ->line('}')
171             ->line('header_size = 10;')
172             ->blank
173             ->comment('MSB must be 0 per RFC');
174            
175 3         30 $builder->if('payload_len & ((uint64_t)1 << 63)')
176             ->line('return -1;')
177             ->endif
178             ->endif
179             ->blank
180             ->comment(' Masking key (client->server frames must be masked)');
181            
182 3         41 $builder->if('frame->masked')
183             ->if('len < header_size + 4')
184             ->line('return 0;')
185             ->endif
186             ->line('memcpy(frame->mask_key, buf + header_size, 4);')
187             ->line('header_size += 4;')
188             ->endif
189             ->blank
190             ->comment(' Check we have full payload');
191            
192 3         47 $builder->if('len < header_size + payload_len')
193             ->line('return 0;')
194             ->endif
195             ->blank
196             ->line(' frame->payload_length = payload_len;')
197             ->line(' frame->header_size = header_size;')
198             ->line(' frame->total_size = header_size + payload_len;')
199             ->line(' frame->payload = (uint8_t*)(buf + header_size);')
200             ->blank
201             ->comment(' Unmask payload in place (XOR with rotating key)');
202            
203 3         44 $builder->if('frame->masked && payload_len > 0')
204             ->line('{ size_t i;')
205             ->for('i = 0', 'i < payload_len', 'i++')
206             ->line('frame->payload[i] ^= frame->mask_key[i & 3];')
207             ->endfor
208             ->line('}')
209             ->endif
210             ->blank
211             ->line(' return (int)frame->total_size;')
212             ->line('}')
213             ->blank;
214            
215             # Helper functions
216 3         61 $builder->comment('Check if opcode is a control frame')
217             ->line("static $inline int ws_is_control(uint8_t opcode) {")
218             ->line(' return (opcode & 0x08) != 0;')
219             ->line('}')
220             ->blank
221             ->comment('Check if opcode is a data frame')
222             ->line("static $inline int ws_is_data(uint8_t opcode) {")
223             ->line(' return opcode == WS_OP_TEXT || opcode == WS_OP_BINARY || opcode == WS_OP_CONTINUATION;')
224             ->line('}')
225             ->blank;
226            
227 3         23 return $builder;
228             }
229              
230             # Generate frame encoder
231             sub gen_frame_encoder {
232 3     3 0 8262 my ($class, $builder) = @_;
233            
234             # Main encoder function
235 3         72 $builder->comment('Encode WebSocket frame (server->client, no masking required)')
236             ->comment('Returns: total frame size, 0 on error')
237             ->line('static size_t ws_encode_frame(uint8_t* buf, size_t buf_size,')
238             ->line(' uint8_t opcode, int fin,')
239             ->line(' const uint8_t* payload, size_t payload_len) {')
240             ->line(' size_t header_size;')
241             ->line(' size_t total_size;')
242             ->blank
243             ->comment(' Calculate header size');
244            
245 3         60 $builder->if('payload_len <= 125')
246             ->line('header_size = 2;')
247             ->elsif('payload_len <= 65535')
248             ->line('header_size = 4;')
249             ->else
250             ->line('header_size = 10;')
251             ->endif
252             ->blank
253             ->line(' total_size = header_size + payload_len;');
254            
255 3         70 $builder->if('total_size > buf_size')
256             ->line('return 0;')
257             ->endif
258             ->blank
259             ->comment(' First byte: FIN + opcode')
260             ->line(' buf[0] = (fin ? WS_FIN : 0) | (opcode & 0x0F);')
261             ->blank
262             ->comment(' Second byte: payload length (no mask bit for server)');
263            
264 3         59 $builder->if('payload_len <= 125')
265             ->line('buf[1] = (uint8_t)payload_len;')
266             ->elsif('payload_len <= 65535')
267             ->line('buf[1] = 126;')
268             ->line('buf[2] = (payload_len >> 8) & 0xFF;')
269             ->line('buf[3] = payload_len & 0xFF;')
270             ->else
271             ->line('buf[1] = 127;')
272             ->line('{ int i;')
273             ->for('i = 0', 'i < 8', 'i++')
274             ->line('buf[2 + i] = (payload_len >> (56 - 8*i)) & 0xFF;')
275             ->endfor
276             ->line('}')
277             ->endif
278             ->blank
279             ->comment(' Copy payload');
280            
281 3         23 $builder->if('payload_len > 0 && payload')
282             ->line('memcpy(buf + header_size, payload, payload_len);')
283             ->endif
284             ->blank
285             ->line(' return total_size;')
286             ->line('}')
287             ->blank;
288            
289             # Encode text
290 3         23 $builder->comment('Encode text message frame')
291             ->line('static size_t ws_encode_text(uint8_t* buf, size_t buf_size,')
292             ->line(' const char* text, size_t text_len) {')
293             ->line(' return ws_encode_frame(buf, buf_size, WS_OP_TEXT, 1,')
294             ->line(' (const uint8_t*)text, text_len);')
295             ->line('}')
296             ->blank;
297            
298             # Encode binary
299 3         30 $builder->comment('Encode binary message frame')
300             ->line('static size_t ws_encode_binary(uint8_t* buf, size_t buf_size,')
301             ->line(' const uint8_t* data, size_t data_len) {')
302             ->line(' return ws_encode_frame(buf, buf_size, WS_OP_BINARY, 1, data, data_len);')
303             ->line('}')
304             ->blank;
305            
306             # Encode close
307 3         21 $builder->comment('Encode close frame with code and optional reason')
308             ->line('static size_t ws_encode_close(uint8_t* buf, size_t buf_size,')
309             ->line(' uint16_t code, const char* reason) {')
310             ->line(' uint8_t payload[128];')
311             ->line(' size_t payload_len = 0;')
312             ->blank;
313            
314 3         53 $builder->if('code')
315             ->line('payload[0] = (code >> 8) & 0xFF;')
316             ->line('payload[1] = code & 0xFF;')
317             ->line('payload_len = 2;')
318             ->blank
319             ->if('reason && reason[0]')
320             ->line('size_t reason_len = strlen(reason);')
321             ->if('reason_len > 123')
322             ->line('reason_len = 123;')
323             ->endif
324             ->line('memcpy(payload + 2, reason, reason_len);')
325             ->line('payload_len += reason_len;')
326             ->endif
327             ->endif
328             ->blank
329             ->line(' return ws_encode_frame(buf, buf_size, WS_OP_CLOSE, 1,')
330             ->line(' payload, payload_len);')
331             ->line('}')
332             ->blank;
333            
334             # Encode ping
335 3         10 $builder->comment('Encode ping frame')
336             ->line('static size_t ws_encode_ping(uint8_t* buf, size_t buf_size,')
337             ->line(' const uint8_t* data, size_t data_len) {');
338            
339 3         18 $builder->if('data_len > 125')
340             ->line('data_len = 125;')
341             ->endif
342             ->line(' return ws_encode_frame(buf, buf_size, WS_OP_PING, 1, data, data_len);')
343             ->line('}')
344             ->blank;
345            
346             # Encode pong
347 3         17 $builder->comment('Encode pong frame (must echo ping payload)')
348             ->line('static size_t ws_encode_pong(uint8_t* buf, size_t buf_size,')
349             ->line(' const uint8_t* data, size_t data_len) {');
350            
351 3         18 $builder->if('data_len > 125')
352             ->line('data_len = 125;')
353             ->endif
354             ->line(' return ws_encode_frame(buf, buf_size, WS_OP_PONG, 1, data, data_len);')
355             ->line('}')
356             ->blank;
357            
358 3         5 return $builder;
359             }
360              
361             # Generate control frame handler
362             sub gen_control_handler {
363 3     3 0 6425 my ($class, $builder) = @_;
364            
365 3         32 $builder->comment('Handle control frames (ping/pong/close)')
366             ->comment('Returns: 1 = handled, 0 = not control, -1 = close connection')
367             ->line('static int ws_handle_control(int fd, WSFrame* frame,')
368             ->line(' uint16_t* close_code, char* close_reason) {')
369             ->line(' uint8_t buf[256];')
370             ->blank;
371            
372             # Handle PING
373 3         46 $builder->if('frame->opcode == WS_OP_PING')
374             ->comment('Must respond with pong containing same payload')
375             ->line('size_t len = ws_encode_pong(buf, sizeof(buf),')
376             ->line(' frame->payload,')
377             ->line(' frame->payload_length);')
378             ->if('len > 0')
379             ->line('send(fd, buf, len, 0);')
380             ->endif
381             ->line('return 1;')
382             ->endif
383             ->blank;
384            
385             # Handle PONG
386 3         21 $builder->if('frame->opcode == WS_OP_PONG')
387             ->comment('Acknowledge only, no response needed')
388             ->line('return 1;')
389             ->endif
390             ->blank;
391            
392             # Handle CLOSE
393 3         155 $builder->if('frame->opcode == WS_OP_CLOSE')
394             ->comment('Parse close code and reason')
395             ->line('uint16_t code = WS_CLOSE_NORMAL;')
396             ->blank
397             ->if('frame->payload_length >= 2')
398             ->line('code = ((uint16_t)frame->payload[0] << 8) | frame->payload[1];')
399             ->if('frame->payload_length > 2')
400             ->comment('Reason is UTF-8, null-terminate')
401             ->line('size_t reason_len = frame->payload_length - 2;')
402             ->if('reason_len > 123')
403             ->line('reason_len = 123;')
404             ->endif
405             ->if('close_reason')
406             ->line('memcpy(close_reason, frame->payload + 2, reason_len);')
407             ->line('close_reason[reason_len] = \'\\0\';')
408             ->endif
409             ->endif
410             ->endif
411             ->blank
412             ->if('close_code')
413             ->line('*close_code = code;')
414             ->endif
415             ->blank
416             ->comment('Echo close frame back')
417             ->line('size_t len = ws_encode_close(buf, sizeof(buf), code, NULL);')
418             ->if('len > 0')
419             ->line('send(fd, buf, len, 0);')
420             ->endif
421             ->line('return -1;')
422             ->endif
423             ->blank
424             ->comment('Not a control frame')
425             ->line(' return 0;')
426             ->line('}')
427             ->blank;
428            
429 3         5 return $builder;
430             }
431              
432             # Generate fragmentation support
433             sub gen_fragment_handler {
434 3     3 0 5802 my ($class, $builder, $max_connections) = @_;
435 3   50     29 $max_connections //= 10000;
436            
437 3         172 $builder->comment('Per-connection fragment accumulator')
438             ->line('typedef struct {')
439             ->line(' uint8_t* buffer;')
440             ->line(' size_t length;')
441             ->line(' size_t capacity;')
442             ->line(' uint8_t opcode;')
443             ->line(' int in_progress;')
444             ->line('} WSFragmentBuffer;')
445             ->blank
446             ->line("static WSFragmentBuffer ws_fragments[$max_connections];")
447             ->blank;
448            
449             # fragment_init
450 3         19 $builder->comment('Initialize fragment buffer for new message')
451             ->line('static int ws_fragment_init(int fd, uint8_t opcode, size_t initial_cap) {');
452            
453 3         54 $builder->if("fd < 0 || fd >= $max_connections")
454             ->line('return -1;')
455             ->endif
456             ->blank
457             ->line(' WSFragmentBuffer* frag = &ws_fragments[fd];')
458             ->blank;
459            
460 3         68 $builder->if('!frag->buffer')
461             ->line('frag->capacity = initial_cap > 0 ? initial_cap : 16384;')
462             ->line('frag->buffer = (uint8_t*)malloc(frag->capacity);')
463             ->if('!frag->buffer')
464             ->line('return -1;')
465             ->endif
466             ->endif
467             ->blank
468             ->line(' frag->length = 0;')
469             ->line(' frag->opcode = opcode;')
470             ->line(' frag->in_progress = 1;')
471             ->blank
472             ->line(' return 0;')
473             ->line('}')
474             ->blank;
475            
476             # fragment_append
477 3         10 $builder->comment('Append data to fragment buffer')
478             ->line('static int ws_fragment_append(int fd, const uint8_t* data, size_t len) {');
479            
480 3         46 $builder->if("fd < 0 || fd >= $max_connections")
481             ->line('return -1;')
482             ->endif
483             ->blank
484             ->line(' WSFragmentBuffer* frag = &ws_fragments[fd];')
485             ->blank
486             ->comment(' Grow buffer if needed (double capacity)');
487            
488 3         51 $builder->while('frag->length + len > frag->capacity')
489             ->line('size_t new_cap = frag->capacity * 2;')
490             ->line('uint8_t* new_buf = (uint8_t*)realloc(frag->buffer, new_cap);')
491             ->if('!new_buf')
492             ->line('return -1;')
493             ->endif
494             ->line('frag->buffer = new_buf;')
495             ->line('frag->capacity = new_cap;')
496             ->endloop
497             ->blank
498             ->line(' memcpy(frag->buffer + frag->length, data, len);')
499             ->line(' frag->length += len;')
500             ->blank
501             ->line(' return 0;')
502             ->line('}')
503             ->blank;
504            
505             # fragment_get
506 3         10 $builder->comment('Get completed fragment buffer')
507             ->line('static WSFragmentBuffer* ws_fragment_get(int fd) {');
508            
509 3         38 $builder->if("fd < 0 || fd >= $max_connections")
510             ->line('return NULL;')
511             ->endif
512             ->line(' return &ws_fragments[fd];')
513             ->line('}')
514             ->blank;
515            
516             # fragment_reset
517 3         14 $builder->comment('Reset fragment buffer after message complete')
518             ->line('static void ws_fragment_reset(int fd) {');
519            
520 3         73 $builder->if("fd < 0 || fd >= $max_connections")
521             ->line('return;')
522             ->endif
523             ->line(' ws_fragments[fd].length = 0;')
524             ->line(' ws_fragments[fd].in_progress = 0;')
525             ->line('}')
526             ->blank;
527            
528             # fragment_free
529 3         10 $builder->comment('Free fragment buffer on connection close')
530             ->line('static void ws_fragment_free(int fd) {');
531            
532 3         25 $builder->if("fd < 0 || fd >= $max_connections")
533             ->line('return;')
534             ->endif
535             ->blank;
536            
537 3         27 $builder->if('ws_fragments[fd].buffer')
538             ->line('free(ws_fragments[fd].buffer);')
539             ->line('ws_fragments[fd].buffer = NULL;')
540             ->endif
541             ->line(' ws_fragments[fd].length = 0;')
542             ->line(' ws_fragments[fd].capacity = 0;')
543             ->line(' ws_fragments[fd].in_progress = 0;')
544             ->line('}')
545             ->blank;
546            
547 3         6 return $builder;
548             }
549              
550             # Generate main frame processor
551             sub gen_frame_processor {
552 3     3 0 6704 my ($class, $builder) = @_;
553            
554             # Message callback typedef
555 3         31 $builder->comment('Message callback type')
556             ->line('typedef void (*WSMessageCallback)(int fd, uint8_t opcode,')
557             ->line(' uint8_t* data, size_t len,')
558             ->line(' void* userdata);')
559             ->blank;
560            
561             # Main processor function
562 3         32 $builder->comment('Process incoming WebSocket data')
563             ->comment('Returns: bytes consumed, 0 = need more, -1 = error, -2 = close')
564             ->line('static int ws_process_data(int fd, uint8_t* buf, size_t len,')
565             ->line(' WSMessageCallback on_message, void* userdata) {')
566             ->line(' size_t pos = 0;')
567             ->line(' uint16_t close_code = 0;')
568             ->line(' char close_reason[128] = {0};')
569             ->blank;
570            
571 3         408 $builder->while('pos < len')
572             ->line('WSFrame frame;')
573             ->line('int consumed = ws_decode_frame(buf + pos, len - pos, &frame);')
574             ->blank
575             ->if('consumed == 0')
576             ->comment('Need more data')
577             ->line('break;')
578             ->endif
579             ->if('consumed < 0')
580             ->line('return -1;')
581             ->endif
582             ->blank
583             ->comment('Handle control frames (can interleave with data)')
584             ->if('ws_is_control(frame.opcode)')
585             ->line('int ctrl = ws_handle_control(fd, &frame, &close_code, close_reason);')
586             ->if('ctrl == -1')
587             ->line('return -2;')
588             ->endif
589             ->line('pos += consumed;')
590             ->line('continue;')
591             ->endif
592             ->blank
593             ->comment('Data frame processing')
594             ->if('frame.opcode != WS_OP_CONTINUATION')
595             ->comment('Start of new message')
596             ->if('!frame.fin')
597             ->comment('First fragment of multi-frame message')
598             ->if('ws_fragment_init(fd, frame.opcode, frame.payload_length * 2) < 0')
599             ->line('return -1;')
600             ->endif
601             ->line('ws_fragment_append(fd, frame.payload, frame.payload_length);')
602             ->else
603             ->comment('Complete message in single frame')
604             ->if('on_message')
605             ->line('on_message(fd, frame.opcode, frame.payload,')
606             ->line(' frame.payload_length, userdata);')
607             ->endif
608             ->endif
609             ->else
610             ->comment('Continuation frame')
611             ->line('WSFragmentBuffer* frag = ws_fragment_get(fd);')
612             ->if('!frag || !frag->in_progress')
613             ->line('return -1;')
614             ->endif
615             ->blank
616             ->line('ws_fragment_append(fd, frame.payload, frame.payload_length);')
617             ->blank
618             ->if('frame.fin')
619             ->comment('Message complete')
620             ->if('on_message')
621             ->line('on_message(fd, frag->opcode, frag->buffer,')
622             ->line(' frag->length, userdata);')
623             ->endif
624             ->line('ws_fragment_reset(fd);')
625             ->endif
626             ->endif
627             ->blank
628             ->line('pos += consumed;')
629             ->endloop
630             ->blank
631             ->line(' return (int)pos;')
632             ->line('}')
633             ->blank;
634            
635 3         8 return $builder;
636             }
637              
638             # Generate all frame handling C code
639             sub generate_c_code {
640 2     2 0 5257 my ($class, $builder, $opts) = @_;
641 2   50     13 $opts //= {};
642            
643 2         9 $class->gen_frame_constants($builder);
644 2         7 $class->gen_frame_decoder($builder);
645 2         76 $class->gen_frame_encoder($builder);
646 2         11 $class->gen_control_handler($builder);
647 2         24 $class->gen_fragment_handler($builder, $opts->{max_connections});
648 2         11 $class->gen_frame_processor($builder);
649            
650 2         11 return $builder;
651             }
652              
653             #
654             # Perl-side helpers for testing and fallback
655             #
656              
657             # Encode a frame in pure Perl
658             sub encode_frame {
659 18     18 0 56758 my ($class, %args) = @_;
660            
661 18   50     68 my $opcode = $args{opcode} // OP_TEXT;
662 18   50     45 my $fin = $args{fin} // 1;
663 18   50     50 my $data = $args{data} // '';
664 18         28 my $mask = $args{mask}; # Optional 4-byte mask key
665            
666 18 100       6637 my $payload = ref($data) ? $data : [unpack('C*', $data)];
667 18         890 my $len = scalar(@$payload);
668            
669 18         27 my @frame;
670            
671             # First byte: FIN + opcode
672 18 100       69 push @frame, ($fin ? 0x80 : 0x00) | ($opcode & 0x0F);
673            
674             # Second byte: MASK + length
675 18 100       38 my $mask_bit = $mask ? 0x80 : 0x00;
676            
677 18 100       36 if ($len <= 125) {
    100          
678 16         40 push @frame, $mask_bit | $len;
679             } elsif ($len <= 65535) {
680 1         2 push @frame, $mask_bit | 126;
681 1         2 push @frame, ($len >> 8) & 0xFF;
682 1         2 push @frame, $len & 0xFF;
683             } else {
684 1         2 push @frame, $mask_bit | 127;
685 1         5 for my $i (0..7) {
686 8         9 push @frame, ($len >> (56 - 8*$i)) & 0xFF;
687             }
688             }
689            
690             # Mask key
691 18 100       47 if ($mask) {
692 8         15 push @frame, @$mask;
693             # XOR payload with mask
694 8         26 for my $i (0..$#$payload) {
695 65         231 $payload->[$i] ^= $mask->[$i % 4];
696             }
697             }
698            
699             # Payload
700 18         2481 push @frame, @$payload;
701            
702 18         2473 return pack('C*', @frame);
703             }
704              
705             # Decode a frame in pure Perl
706             sub decode_frame {
707 20     20 0 47001 my ($class, $data) = @_;
708            
709 20         3287 my @bytes = unpack('C*', $data);
710 20 100       374 return undef if @bytes < 2;
711            
712 18         53 my %frame;
713            
714             # First byte
715 18 100       74 $frame{fin} = ($bytes[0] & 0x80) ? 1 : 0;
716 18         51 $frame{rsv} = ($bytes[0] >> 4) & 0x07;
717 18         36 $frame{opcode} = $bytes[0] & 0x0F;
718            
719             # Second byte
720 18 100       47 $frame{masked} = ($bytes[1] & 0x80) ? 1 : 0;
721 18         30 my $len = $bytes[1] & 0x7F;
722            
723 18         32 my $pos = 2;
724            
725 18 100       54 if ($len == 126) {
    100          
726 1 50       6 return undef if @bytes < 4;
727 1         5 $len = ($bytes[2] << 8) | $bytes[3];
728 1         10 $pos = 4;
729             } elsif ($len == 127) {
730 1 50       5 return undef if @bytes < 10;
731 1         2 $len = 0;
732 1         5 for my $i (0..7) {
733 8         15 $len = ($len << 8) | $bytes[2 + $i];
734             }
735 1         3 $pos = 10;
736             }
737            
738 18         60 $frame{payload_length} = $len;
739            
740             # Mask key
741 18 100       51 if ($frame{masked}) {
742 8 50       23 return undef if @bytes < $pos + 4;
743 8         36 $frame{mask_key} = [@bytes[$pos..$pos+3]];
744 8         14 $pos += 4;
745             }
746            
747             # Payload
748 18 100       50 return undef if @bytes < $pos + $len;
749            
750 17         7401 my @payload = @bytes[$pos..$pos+$len-1];
751            
752             # Unmask
753 17 100       1635 if ($frame{masked}) {
754 8         24 for my $i (0..$#payload) {
755 65         97 $payload[$i] ^= $frame{mask_key}[$i % 4];
756             }
757             }
758            
759 17         1663 $frame{payload} = pack('C*', @payload);
760 17         34 $frame{header_size} = $pos;
761 17         35 $frame{total_size} = $pos + $len;
762            
763 17         3263 return \%frame;
764             }
765              
766             # Encode close frame with code
767             sub encode_close {
768 1     1 0 5459 my ($class, $code, $reason) = @_;
769 1   50     7 $code //= CLOSE_NORMAL;
770 1   50     4 $reason //= '';
771            
772 1         2 my @payload;
773 1         3 push @payload, ($code >> 8) & 0xFF;
774 1         3 push @payload, $code & 0xFF;
775 1         7 push @payload, unpack('C*', substr($reason, 0, 123));
776            
777 1         6 return $class->encode_frame(
778             opcode => OP_CLOSE,
779             fin => 1,
780             data => \@payload,
781             );
782             }
783              
784             # Parse close frame payload
785             sub parse_close {
786 3     3 0 9601 my ($class, $payload) = @_;
787            
788 3         13 my @bytes = unpack('C*', $payload);
789            
790 3         6 my $code = CLOSE_NO_STATUS;
791 3         10 my $reason = '';
792            
793 3 50       12 if (@bytes >= 2) {
794 3         8 $code = ($bytes[0] << 8) | $bytes[1];
795 3 100       18 if (@bytes > 2) {
796 2         10 $reason = pack('C*', @bytes[2..$#bytes]);
797             }
798             }
799            
800 3         21 return ($code, $reason);
801             }
802              
803             1;
804              
805             __END__