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   61789 use Mojo::Base '-base';
  5         165255  
  5         30  
3              
4 5     5   810 use Exporter 'import';
  5         11  
  5         273  
5             our @EXPORT_OK = qw(encode_sentence);
6              
7 5     5   2162 use API::MikroTik::Query 'build_query';
  5         13  
  5         3963  
8              
9             has words => sub { [] };
10              
11             sub encode_sentence {
12 72 50   72 1 502876 shift if ref $_[0];
13 72   50     339 my ($command, $attr, $query, $tag)
      100        
14             = (shift // '', shift // {}, shift, shift);
15              
16 72         179 my $sentence = _encode_word($command);
17              
18 72   100     387 $sentence .= _encode_word("=$_=" . ($attr->{$_} // '')) for keys %$attr;
19              
20 72 100       180 if ($query) {
21 1         2 $sentence .= _encode_word($_) for @{build_query($query)};
  1         6  
22             }
23              
24 72 100       219 $sentence .= _encode_word(".tag=$tag") if $tag;
25              
26             # Closing empty word.
27 72         184 $sentence .= "\x00";
28              
29 72         234 return $sentence;
30             }
31              
32             sub fetch {
33 75     75 1 1549 my ($self, $buff) = @_;
34 75         121 my $words;
35              
36 75 100       210 if (defined(my $old_buff = delete $self->{_buff})) {
37 3         5 $words = $self->{words};
38 3         20 $$buff = $old_buff . $$buff;
39             }
40 72         182 else { $words = $self->{words} = [] }
41              
42 75         199 while (my $w = $self->_fetch_word($buff)) { push @$words, $w }
  221         605  
43 75         180 return $words;
44             }
45              
46             sub is_incomplete {
47 77     77 1 3689 return exists $_[0]->{_buff};
48             }
49              
50             sub reset {
51 2     2 1 236 delete @{$_[0]}{qw(words _buff)};
  2         6  
52 2         7 return $_[0];
53             }
54              
55             sub _encode_length {
56 227     227   2542 my $len = shift;
57              
58 227         332 my $packed;
59              
60             # Screw you, mikrotik engineers, just pack length as 4 bytes. >_<
61 227 100       433 if ($len < 0x80) {
    100          
    100          
    100          
62 216         474 $packed = pack 'C', $len;
63             }
64             elsif ($len < 0x4000) {
65 8         20 $packed = pack 'n', ($len | 0x8000) & 0xffff;
66             }
67             elsif ($len < 0x200000) {
68 1         9 $len |= 0xc00000;
69 1         7 $packed = pack 'Cn', (($len >> 16) & 0xff), ($len & 0xffff);
70             }
71             elsif ($len < 0x10000000) {
72 1         4 $packed = pack 'N', ($len | 0xe0000000);
73             }
74             else {
75 1         5 $packed = pack 'CN', 0xf0, $len;
76             }
77              
78 227         732 return $packed;
79             }
80              
81             sub _encode_word {
82 218     218   847 return _encode_length(length($_[0])) . $_[0];
83             }
84              
85             sub _fetch_word {
86 298     298   992 my ($self, $buff) = @_;
87              
88 298 100       621 return $self->{_buff} = '' unless my $buff_bytes = length $$buff;
89 297 100 100     734 return do { $self->{_buff} = $$buff; $$buff = ''; }
  1         3  
  1         3  
90             if $buff_bytes < 5 && $$buff ne "\x00";
91              
92 296         566 my $len = _strip_length($buff);
93 296         639 my $word = substr $$buff, 0, $len, '';
94              
95 296 100       645 return do { $self->{_buff} = _encode_length($len) . $word; ''; }
  4         12  
  4         16  
96             if (length $word) < $len;
97              
98 292         744 return $word;
99             }
100              
101             sub _strip_length {
102 301     301   457 my $buff = shift;
103              
104 301         766 my $len = unpack 'C', substr $$buff, 0, 1, '';
105              
106 301 100       663 if (($len & 0x80) == 0x00) {
    100          
    100          
    100          
    50          
107 278         560 return $len;
108             }
109             elsif (($len & 0xc0) == 0x80) {
110 20         22 $len &= ~0x80;
111 20         24 $len <<= 8;
112 20         35 $len += unpack 'C', substr $$buff, 0, 1, '';
113             }
114             elsif (($len & 0xe0) == 0xc0) {
115 1         2 $len &= ~0xc0;
116 1         2 $len <<= 16;
117 1         5 $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         42 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