File Coverage

blib/lib/API/MikroTik/Sentence.pm
Criterion Covered Total %
statement 67 67 100.0
branch 30 32 93.7
condition 8 9 88.8
subroutine 11 11 100.0
pod 4 4 100.0
total 120 123 97.5


line stmt bran cond sub pod time code
1             package API::MikroTik::Sentence;
2 5     5   113455 use Mojo::Base '-base';
  5         151315  
  5         40  
3              
4 5     5   996 use Exporter 'import';
  5         26  
  5         338  
5             our @EXPORT_OK = qw(encode_sentence);
6              
7 5     5   2451 use API::MikroTik::Query 'build_query';
  5         21  
  5         5287  
8              
9             has words => sub { [] };
10              
11             sub encode_sentence {
12 72 50   72 1 503879 shift if ref $_[0];
13 72   50     358 my ($command, $attr, $query, $tag)
      100        
14             = (shift // '', shift // {}, shift, shift);
15              
16 72         176 my $sentence = _encode_word($command);
17              
18 72   100     374 $sentence .= _encode_word("=$_=" . ($attr->{$_} // '')) for keys %$attr;
19              
20 72 100       169 if ($query) {
21 1         2 $sentence .= _encode_word($_) for @{build_query($query)};
  1         7  
22             }
23              
24 72 100       227 $sentence .= _encode_word(".tag=$tag") if $tag;
25              
26             # Closing empty word.
27 72         137 $sentence .= "\x00";
28              
29 72         236 return $sentence;
30             }
31              
32             sub fetch {
33 75     75 1 2042 my ($self, $buff) = @_;
34 75         127 my $words;
35              
36 75 100       224 if (defined(my $old_buff = delete $self->{_buff})) {
37 3         5 $words = $self->{words};
38 3         22 $$buff = $old_buff . $$buff;
39             }
40 72         187 else { $words = $self->{words} = [] }
41              
42 75         221 while (my $w = $self->_fetch_word($buff)) { push @$words, $w }
  221         628  
43 75         205 return $words;
44             }
45              
46             sub is_incomplete {
47 77     77 1 4625 return exists $_[0]->{_buff};
48             }
49              
50             sub reset {
51 2     2 1 327 delete @{$_[0]}{qw(words _buff)};
  2         8  
52 2         7 return $_[0];
53             }
54              
55             sub _encode_length {
56 227     227   4206 my $len = shift;
57              
58 227         330 my $packed;
59              
60             # Screw you, mikrotik engineers, just pack length as 4 bytes. >_<
61 227 100       429 if ($len < 0x80) {
    100          
    100          
    100          
62 216         467 $packed = pack 'C', $len;
63             }
64             elsif ($len < 0x4000) {
65 8         29 $packed = pack 'n', ($len | 0x8000) & 0xffff;
66             }
67             elsif ($len < 0x200000) {
68 1         12 $len |= 0xc00000;
69 1         8 $packed = pack 'Cn', (($len >> 16) & 0xff), ($len & 0xffff);
70             }
71             elsif ($len < 0x10000000) {
72 1         5 $packed = pack 'N', ($len | 0xe0000000);
73             }
74             else {
75 1         4 $packed = pack 'CN', 0xf0, $len;
76             }
77              
78 227         745 return $packed;
79             }
80              
81             sub _encode_word {
82 218     218   1341 return _encode_length(length($_[0])) . $_[0];
83             }
84              
85             sub _fetch_word {
86 298     298   1490 my ($self, $buff) = @_;
87              
88 298 100       647 return $self->{_buff} = '' unless my $buff_bytes = length $$buff;
89 297 100 100     710 return do { $self->{_buff} = $$buff; $$buff = ''; }
  1         3  
  1         4  
90             if $buff_bytes < 5 && $$buff ne "\x00";
91              
92 296         523 my $len = _strip_length($buff);
93 296         664 my $word = substr $$buff, 0, $len, '';
94              
95 296 100       599 return do { $self->{_buff} = _encode_length($len) . $word; ''; }
  4         12  
  4         16  
96             if (length $word) < $len;
97              
98 292         773 return $word;
99             }
100              
101             sub _strip_length {
102 301     301   453 my $buff = shift;
103              
104 301         775 my $len = unpack 'C', substr $$buff, 0, 1, '';
105              
106 301 100       709 if (($len & 0x80) == 0x00) {
    100          
    100          
    100          
    50          
107 278         556 return $len;
108             }
109             elsif (($len & 0xc0) == 0x80) {
110 20         27 $len &= ~0x80;
111 20         28 $len <<= 8;
112 20         42 $len += unpack 'C', substr $$buff, 0, 1, '';
113             }
114             elsif (($len & 0xe0) == 0xc0) {
115 1         3 $len &= ~0xc0;
116 1         2 $len <<= 16;
117 1         6 $len += unpack 'n', substr $$buff, 0, 2, '';
118             }
119             elsif (($len & 0xf0) == 0xe0) {
120 1         6 $len = unpack 'N', pack('C', ($len & ~0xe0)) . substr($$buff, 0, 3, '');
121             }
122             elsif (($len & 0xf8) == 0xf0) {
123 1         4 $len = unpack 'N', substr $$buff, 0, 4, '';
124             }
125              
126 23         53 return $len;
127             }
128              
129             1;
130              
131             =encoding utf8
132              
133             =head1 NAME
134              
135             API::MikroTik::Sentence - Encode and decode API sentences
136              
137             =head1 SYNOPSIS
138              
139             use API::MikroTik::Sentence qw(encode_sentence);
140              
141             my $command = '/interface/print';
142             my $attr = {'.proplist' => '.id,name,type'};
143             my $query = {type => ['ipip-tunnel', 'gre-tunnel'], running => 'true'};
144             my $tag = 1;
145              
146             my $bytes = encode_sentence($command, $attr, $query, $tag);
147              
148             my $sentence = API::MikroTik::Sentence->new();
149             my $words = $sentence->fetch(\$bytes);
150             say $_ for @$words;
151              
152             =head1 DESCRIPTION
153              
154             Provides subroutines for encoding API sentences and parsing them back into words.
155              
156             =head1 METHODS
157              
158             =head2 encode_sentence
159              
160             my $bytes = encode_sentence($command, $attr, $query, $tag);
161              
162             Encodes sentence. Attributes is a hashref with attribute-value pairs. Query will
163             be parsed with L.
164              
165             Can be also called as an object method.
166              
167             =head2 fetch
168              
169             my $words = $sentence->fetch(\$buff);
170              
171             Fetches a sentence from a buffer and parses it into a list of API words. In a
172             situation when amount of data in the buffer are insufficient to complete the
173             sentence, already processed words and the remaining buffer will be stored in an
174             object. On a next call will prepend a buffer with kept data and merge a result
175             with the one stored from a previous call.
176              
177              
178             =head2 is_incomplete
179              
180             my $done = !$sentence->is_incomplete;
181              
182             Indicates that a processed buffer was incomplete and remaining amount of data was
183             insufficient to complete a sentence.
184              
185             =head2 reset
186              
187             my $sentence->reset;
188              
189             Clears an incomplete status and removes a remaining buffer.
190              
191             =head1 SEE ALSO
192              
193             L
194              
195             =cut
196