File Coverage

blib/lib/DOCSIS/ConfigFile/Decode.pm
Criterion Covered Total %
statement 137 151 90.7
branch 46 78 58.9
condition 3 7 42.8
subroutine 27 27 100.0
pod 16 16 100.0
total 229 279 82.0


line stmt bran cond sub pod time code
1             package DOCSIS::ConfigFile::Decode;
2 17     17   128 use strict;
  17         36  
  17         888  
3 17     17   88 use warnings;
  17         36  
  17         423  
4 17     17   10256 use bytes;
  17         252  
  17         89  
5              
6 17     17   581 use Carp qw(confess);
  17         37  
  17         877  
7 17     17   19349 use Math::BigInt;
  17         481852  
  17         89  
8 17     17   401929 use Socket;
  17         69956  
  17         41494  
9              
10             our %SNMP_TYPE = (
11             0x02 => ['INTEGER', \&int],
12             0x04 => ['STRING', \&string],
13             0x05 => ['NULLOBJ', sub { }],
14             0x40 => ['IPADDRESS', \&ip],
15             0x41 => ['COUNTER', \&uint],
16             0x42 => ['UNSIGNED', \&uint],
17             0x43 => ['TIMETICKS', \&uint],
18             0x44 => ['OPAQUE', \&uint],
19             0x46 => ['COUNTER64', \&bigint],
20             );
21              
22             sub bigint {
23 2     2 1 17 my @bytes = unpack 'C*', _test_length(int => $_[0]);
24 2         6 my $negative = $bytes[0] & 0x80;
25 2         13 my $int64 = Math::BigInt->new(0);
26              
27             # setup int64
28 2         299 for my $chunk (@bytes) {
29 11 100       4247 $chunk ^= 0xff if ($negative);
30 11         34 $int64 = ($int64 << 8) | $chunk;
31             }
32              
33 2 100       982 if ($negative) {
34 1         6 $int64 *= -1;
35 1         147 $int64 -= 1;
36             }
37              
38 2         189 return $int64;
39             }
40              
41             sub ether {
42 2     2 1 32 my $length = length $_[0];
43 2 50 33     30 return join '', unpack 'H2' x $length, $_[0] if $length == 6 or $length == 12;
44 0         0 confess "Invalid ether input. Invalid length ($length)";
45             }
46              
47 52     52 1 301 sub hexstr { '0x' . join '', unpack 'H*', $_[0] }
48              
49             sub int {
50 12     12 1 68 my @bytes = unpack 'C*', _test_length(int => $_[0], 'int');
51 12         29 my $negative = $bytes[0] & 0x80;
52 12         18 my $int = 0;
53              
54 12         46 for my $chunk (@bytes) {
55 30 100       59 $chunk ^= 0xff if ($negative);
56 30         56 $int = ($int << 8) | $chunk;
57             }
58              
59 12 100       26 if ($negative) {
60 4         5 $int *= -1;
61 4         8 $int -= 1;
62             }
63              
64 12         40 return $int;
65             }
66              
67 6 50   6 1 50 sub ip { inet_ntoa($_[0]) || confess 'inet_ntoa(...) failed to unpack binary string' }
68 35     35 1 82 sub mic {&hexstr}
69 1     1 1 5 sub no_value {''}
70              
71             sub snmp_object {
72 12     12 1 145 my $bin = $_[0];
73 12         26 my ($byte, $length, $oid, $type, $value);
74              
75             # message
76 12         33 $type = _truncate_and_unpack(\$bin, 'C1'); # 0x30
77 12         47 $length = _snmp_length(\$bin);
78              
79             # oid
80 12         38 $type = _truncate_and_unpack(\$bin, 'C1'); # 0x06
81 12         40 $length = _snmp_length(\$bin);
82 12         42 $oid = _snmp_oid(\$bin, $length);
83              
84             # value
85 12         36 $type = $SNMP_TYPE{_truncate_and_unpack(\$bin, 'C1')};
86 12         41 $length = _snmp_length(\$bin);
87 12         49 $value = $type->[1]->($bin);
88              
89 12         80 return {oid => $oid, type => $type->[0], value => $value};
90             }
91              
92             sub string {
93              
94             # not sure why this is able to join - may be removed later
95 23 50   23 1 107 my $bin = @_ > 1 ? join '', map { chr $_ } @_ : $_[0];
  0         0  
96              
97 23 100       105 return hexstr($bin) if $bin =~ /^[^\t\n\r\x20-\xEF]/;
98 22         79 $bin =~ s/([^\x20-\x24\x26-\x7e])/{ sprintf "%%%02x", ord $1 }/ge;
  27         45  
  27         102  
99 22         67 return $bin;
100             }
101              
102 4     4 1 14 sub stringz { my $str = string(@_); $str =~ s/%00$//; $str; }
  4         16  
  4         11  
103 56     56 1 144 sub uchar { unpack 'C', _test_length(uchar => $_[0], 'char') }
104              
105             sub uint {
106 27     27 1 61 my @bytes = unpack 'C*', _test_length(uint => $_[0], 'int');
107 27         47 my $value = 0;
108              
109 27         81 $value = ($value << 8) | $_ for (@bytes);
110 27         79 return $value;
111             }
112              
113 48     48 1 95 sub ushort { unpack 'n', _test_length(ushort => $_[0], 'short int') }
114              
115             sub ushort_list {
116 2     2 1 21 [map { ushort($_) } $_[0] =~ /(..)/g]
  20         36  
117             }
118              
119             sub vendorspec {
120 2   50 2 1 8 my $bin = $_[0] || '';
121 2         4 my ($vendor, @ret, $length);
122              
123             # extract length (not sure what the first byte is...)
124 2 50       10 $bin =~ s/^.(.)//s or confess 'Invalid vendorspec input. Could not extract length';
125 2         8 $length = unpack 'C', $1;
126              
127             # extract vendor
128 2 50       24 $bin =~ s/^(.{$length})//s or confess 'Invalid vendorspec input. Could not extract vendor';
129 2         16 $vendor = sprintf '0x' . ('%02x' x $length), unpack 'C*', $1;
130              
131             # extract TLV
132 2         12 while ($bin =~ s/^(.)(.)//s) {
133 2         5 my $type = unpack 'C*', $1;
134 2         4 my $length = unpack 'C*', $2;
135              
136 2 50       28 if ($bin =~ s/^(.{$length})//s) {
137 2         10 push @ret, {type => $type, length => $length, value => hexstr($1)};
138             }
139             }
140              
141 2 50       7 confess "vendorspec('...') is left with ($length) bytes after decoding" if $length = length $bin;
142 2         19 return $vendor, \@ret;
143             }
144              
145             sub vendor {
146 3   50 3 1 15 my $bin = shift || '';
147 3 50       23 my $length = $bin =~ s/^.(.)//s ? unpack 'C', $1 : 0;
148 3         7 my ($id, @options);
149              
150 3 50       37 if ($bin =~ s/^(.{$length})//s) {
151 3         7 $id = sprintf "0x@{['%02x' x $length]}", unpack 'C*', $1;
  3         30  
152             }
153              
154 3         18 while ($bin =~ s/^(.)(.)//s) {
155 7         19 my $type = unpack 'C*', $1;
156 7         13 my $length = unpack 'C*', $2;
157              
158 7 50       50 $bin =~ s/^(.{$length})//s or next;
159 7         34 push @options, $type, hexstr($1);
160             }
161              
162 3 50       10 confess 'Bytes left in vendorspec' if length $bin;
163 3 50       8 confess 'Invalid vendorspec' unless defined $id;
164 3         17 return {id => $id, options => \@options};
165             }
166              
167             sub _byte_size {
168 143 100   143   339 return 2 if lc $_[0] eq 'short int';
169 95 100       222 return 4 if lc $_[0] eq 'int';
170 56 50       112 return 4 if lc $_[0] eq 'long int';
171 56 50       152 return 1 if lc $_[0] eq 'char';
172 0 0       0 return 4 if lc $_[0] eq 'float';
173 0 0       0 return 8 if lc $_[0] eq 'double';
174 0 0       0 return 12 if lc $_[0] eq 'long double';
175 0 0       0 return 16 if lc $_[0] eq 'md5digest';
176             }
177              
178             sub _snmp_length {
179 36     36   74 my $length = _truncate_and_unpack($_[0], 'C1'); # length?
180              
181 36 100       96 if ($length <= 0x80) {
    50          
    50          
182 32         75 return $length;
183             }
184             elsif ($length == 0x81) {
185 0         0 return _truncate_and_unpack($_[0], 'C1');
186             }
187             elsif ($length == 0x82) {
188 4         9 $length = 0;
189              
190 4         21 for my $byte (_truncate_and_unpack($_[0], 'C2')) {
191 8         21 $length = $length << 8 | $byte;
192             }
193              
194 4         10 return $length;
195             }
196              
197 0         0 confess "Too long SNMP length: ($length)";
198             }
199              
200             sub _snmp_oid {
201 12     12   49 my @bytes = _truncate_and_unpack($_[0], 'C' . $_[1]);
202 12         37 my @oid = (0);
203 12         19 my $subid = 0;
204              
205 12         27 for my $id (@bytes) {
206 130 50       252 if ($subid & 0xfe000000) {
207 0         0 confess "_snmp_oid(@bytes): Sub-identifier too large: ($subid)";
208             }
209              
210 130         187 $subid = ($subid << 7) | ($id & 0x7f);
211              
212 130 100       221 unless ($id & 0x80) {
213 126 50       242 confess "_snmp_oid(@bytes): Exceeded max length" if (128 <= @oid);
214 126         180 push @oid, $subid;
215 126         178 $subid = 0;
216             }
217             }
218              
219             # the first two sub-id are in the first id
220 12 100       36 if ($oid[1] == 0x2b) { # Handle the most common case
    50          
    50          
221 10         17 $oid[0] = 1;
222 10         16 $oid[1] = 3;
223             }
224             elsif ($oid[1] < 40) {
225 0         0 $oid[0] = 0;
226             }
227             elsif ($oid[1] < 80) {
228 2         3 $oid[0] = 1;
229 2         4 $oid[1] -= 40;
230             }
231             else {
232 0         0 $oid[0] = 2;
233 0         0 $oid[1] -= 80;
234             }
235              
236 12         23 return SNMP::translateObj(join '.', @oid) || join '.', @oid
237             if DOCSIS::ConfigFile::CAN_TRANSLATE_OID;
238 12         82 return join '.', @oid;
239             }
240              
241             sub _test_length {
242 145     145   219 my $name = $_[0];
243 145         276 my $length = length $_[1];
244              
245 145 50       276 if (!$length) {
246 0         0 confess "$name(...) bytestring length is zero";
247             }
248 145 100       271 if ($_[2]) {
249 143         260 my $max = _byte_size($_[2]);
250 143 50       311 confess "$name(...) bytestring length is invalid: $max < $length" if ($max < $length);
251             }
252              
253 145         499 return $_[1];
254             }
255              
256             sub _truncate_and_unpack {
257 88     88   171 my ($bin_ref, $type) = @_;
258 88 50       404 my $n = ($type =~ /C/ ? 1 : 2) * ($type =~ /(\d+)/)[0];
259              
260 88 50       930 return unpack $type, $1 if $$bin_ref =~ s/^(.{$n})//s;
261 0           confess "_truncate_and_unpack('...', $type) failed to truncate binary string";
262             }
263              
264             1;
265              
266             =encoding utf8
267              
268             =head1 NAME
269              
270             DOCSIS::ConfigFile::Decode - Decode functions for a DOCSIS config-file
271              
272             =head1 DESCRIPTION
273              
274             L has functions which is used to decode binary data
275             into either plain strings or complex data structures, dependent on the function
276             called.
277              
278             =head1 FUNCTIONS
279              
280             =head2 bigint
281              
282             Returns a C object.
283              
284             =head2 ether
285              
286             Will unpack the input string and return a MAC address in this format:
287             "00112233" or "00112233445566".
288              
289             =head2 hexstr
290              
291             Will unpack the input string and a string with leading "0x", followed
292             by hexidesimal characters.
293              
294             =head2 int
295              
296             Will unpack the input string and return an integer, from -2147483648
297             to 2147483647.
298              
299             =head2 ip
300              
301             Will unpack the input string and return a human readable IPv4 address.
302              
303             =head2 string
304              
305             Returns human-readable string, where special characters are "uri encoded".
306             Example: "%" = "%25" and " " = "%20". It can also return the value from
307             L if it starts with a weird character, such as C<\x00>.
308              
309             =head2 stringz
310              
311             Same as string above. However this string is zero-terminated in encoded
312             form, but this function remove the last "\0" seen in the string.
313              
314             =head2 mic
315              
316             Returns a value, printed as hex.
317              
318             =head2 no_value
319              
320             This method will return an empty string. It is used by DOCSIS types, which
321             has zero length.
322              
323             =head2 snmp_object
324              
325             Will take a binary string and decode it into a complex
326             datastructure, with "oid", "type" and "value".
327              
328             =head2 uchar
329              
330             Will unpack the input string and return a short integer, from 0 to 255.
331              
332             =head2 uint
333              
334             Will unpack the input string and return an integer, from 0 to 4294967295.
335              
336             =head2 ushort
337              
338             Will unpack the input string and return a short integer, from 0 to 65535.
339              
340             =head2 ushort_list
341              
342             Same as L, but will return an array ref with the integers.
343              
344             =head2 ushort_list
345              
346             =head2 vendor
347              
348             Will byte-encode a complex vendorspec datastructure.
349              
350             =head2 vendorspec
351              
352             Will unpack the input string and return a complex datastructure,
353             representing the vendor specific data.
354              
355             =head1 SEE ALSO
356              
357             L
358              
359             =cut