File Coverage

lib/Remote/Perl/Protocol.pm
Criterion Covered Total %
statement 95 95 100.0
branch 6 6 100.0
condition n/a
subroutine 17 17 100.0
pod 0 9 0.0
total 118 127 92.9


line stmt bran cond sub pod time code
1 134     134   129144 use v5.36;
  134         513  
2             package Remote::Perl::Protocol;
3             our $VERSION = '0.004';
4              
5 134     134   921 use Exporter 'import';
  134         202  
  134         20486  
6              
7             our @EXPORT_OK = qw(
8             HEADER_LEN PROTOCOL_VERSION
9             MSG_HELLO MSG_READY MSG_RUN MSG_DATA MSG_EOF
10             MSG_CREDIT MSG_MOD_REQ MSG_MOD_MISSING MSG_RETURN
11             MSG_SIGNAL MSG_SIGNAL_ACK
12             MSG_ERROR MSG_BYE
13             STREAM_CONTROL STREAM_STDIN STREAM_STDOUT STREAM_STDERR
14             TMPFILE_MASK TMPFILE_NONE TMPFILE_AUTO TMPFILE_LINUX TMPFILE_PERL TMPFILE_NAMED
15             FLAGS_WARNINGS
16             encode_message
17             encode_hello decode_hello
18             encode_credit decode_credit
19             encode_return decode_return
20             encode_run decode_run
21             );
22              
23             use constant {
24 134         173285 PROTOCOL_VERSION => 2,
25             HEADER_LEN => 6, # type(1) + stream(1) + length(4)
26              
27             # Flags byte layout: bits 0-2 = tmpfile strategy (0-4), bit 3+ = flags
28             TMPFILE_MASK => 0x07, # bits 0-2
29              
30             # Tmpfile strategies (carried in bits 0-2 of the flags byte)
31             TMPFILE_NONE => 0, # default: eval $source directly
32             TMPFILE_AUTO => 1, # try linux, fall back to perl
33             TMPFILE_LINUX => 2, # O_TMPFILE (anonymous inode, no directory entry)
34             TMPFILE_PERL => 3, # open('+>', undef) -- anon fd, unlinked on creation
35             TMPFILE_NAMED => 4, # File::Temp -- named file, kept until executor exits
36              
37             # Flag bits (bit 3+)
38             FLAGS_WARNINGS => 0x08, # bit 3
39              
40             # Message types
41             MSG_HELLO => 0x00,
42             MSG_READY => 0x01,
43             MSG_RUN => 0x10,
44             MSG_DATA => 0x20,
45             MSG_EOF => 0x21,
46             MSG_CREDIT => 0x30,
47             MSG_MOD_REQ => 0x40,
48             MSG_MOD_MISSING => 0x41,
49             MSG_RETURN => 0x50,
50             MSG_SIGNAL => 0x60,
51             MSG_SIGNAL_ACK => 0x61,
52             MSG_ERROR => 0xE0,
53             MSG_BYE => 0xF0,
54              
55             # Predefined stream IDs
56             STREAM_CONTROL => 0,
57             STREAM_STDIN => 1,
58             STREAM_STDOUT => 2,
59             STREAM_STDERR => 3,
60 134     134   976 };
  134         243  
61              
62             # encode_message($type, $stream, $body) -> bytes
63 7570     7570 0 248587 sub encode_message($type, $stream, $body = '') {
  7570         12230  
  7570         13325  
  7570         14366  
  7570         11235  
64 7570         109678 return pack('CCN', $type, $stream, length($body)) . $body;
65             }
66              
67             # HELLO body: version(u8) + window_size(u32 BE)
68 1428     1428 0 6417 sub encode_hello($version, $window_size) {
  1428         3443  
  1428         5070  
  1428         2332  
69 1428         22794 return pack('CN', $version, $window_size);
70             }
71 1     1 0 734 sub decode_hello($body) {
  1         2  
  1         1  
72 1         5 return unpack('CN', $body);
73             }
74              
75             # CREDIT body: grant(u32 BE)
76 1478     1478 0 3798 sub encode_credit($n) {
  1478         2308  
  1478         3291  
77 1478         12644 return pack('N', $n);
78             }
79 1     1 0 631 sub decode_credit($body) {
  1         1  
  1         2  
80 1         4 return unpack('N', $body);
81             }
82              
83             # RETURN body: exit_code(u8) + optional message bytes
84 2     2 0 1698 sub encode_return($exit_code, $message = '') {
  2         2  
  2         3  
  2         3  
85 2         8 return pack('C', $exit_code) . $message;
86             }
87 1460     1460 0 5511 sub decode_return($body) {
  1460         3569  
  1460         2849  
88 1460         6043 my $exit_code = unpack('C', $body);
89 1460 100       13195 my $message = length($body) > 1 ? substr($body, 1) : '';
90 1460         8876 return ($exit_code, $message);
91             }
92              
93             # RUN body: flags(u8) + argc(u32) + [len(u32) + bytes]... + source(rest)
94 1461     1461 0 5369 sub encode_run($flags, $source, @argv) {
  1461         2767  
  1461         2848  
  1461         2370  
  1461         1923  
95 1461         23657 my $buf = pack('CN', $flags, scalar @argv);
96 1461         8297 for my $arg (@argv) {
97 14         47 $buf .= pack('N', length($arg)) . $arg;
98             }
99 1461         10514 return $buf . $source;
100             }
101 2     2 0 6 sub decode_run($body) {
  2         2  
  2         2  
102 2         2 my $off = 0;
103 2         5 my $flags = unpack('C', substr($body, $off, 1)); $off += 1;
  2         3  
104 2         3 my $argc = unpack('N', substr($body, $off, 4)); $off += 4;
  2         2  
105 2         2 my @argv;
106 2         5 for (1 .. $argc) {
107 2         4 my $len = unpack('N', substr($body, $off, 4)); $off += 4;
  2         2  
108 2         3 push @argv, substr($body, $off, $len); $off += $len;
  2         2  
109             }
110 2         3 my $source = substr($body, $off);
111 2         8 return ($flags, $source, @argv);
112             }
113              
114             # ------------------------------------------------------------------------------
115             # Remote::Perl::Protocol::Parser -- stateful incremental decoder
116             # ------------------------------------------------------------------------------
117             package Remote::Perl::Protocol::Parser;
118              
119 134     134   1236 use constant HEADER_LEN => Remote::Perl::Protocol::HEADER_LEN;
  134         238  
  134         51004  
120              
121 1440     1440   10498 sub new($class) {
  1440         4222  
  1440         2370  
122 1440         32300 return bless { buf => '' }, $class;
123             }
124              
125             # Feed raw bytes; returns list of decoded message hashrefs:
126             # { type => $t, stream => $s, body => $b }
127 8213     8213   548721 sub feed($self, $data) {
  8213         13004  
  8213         18588  
  8213         10660  
128 8213         29806 $self->{buf} .= $data;
129 8213         26673 return $self->_drain;
130             }
131              
132 8213     8213   11709 sub _drain($self) {
  8213         12415  
  8213         13360  
133 8213         14365 my @msgs;
134 8213         29684 while (length($self->{buf}) >= HEADER_LEN) {
135 10457         49056 my ($type, $stream, $len) = unpack('CCN', $self->{buf});
136 10457 100       30598 last if length($self->{buf}) < HEADER_LEN + $len;
137 10456         30589 substr($self->{buf}, 0, HEADER_LEN, '');
138 10456 100       36680 my $body = $len ? substr($self->{buf}, 0, $len, '') : '';
139 10456         106027 push @msgs, { type => $type, stream => $stream, body => $body };
140             }
141 8213         57863 return @msgs;
142             }
143              
144             # How many bytes are buffered but not yet forming a complete message
145 2     2   2630 sub pending_bytes($self) { length($self->{buf}) }
  2         3  
  2         4  
  2         7  
146              
147             1;
148              
149             __END__