File Coverage

blib/lib/Net/OSCAR/XML/Template.pm
Criterion Covered Total %
statement 25 267 9.3
branch 0 184 0.0
condition 0 131 0.0
subroutine 9 19 47.3
pod 0 6 0.0
total 34 607 5.6


line stmt bran cond sub pod time code
1             # These objects, initialized with an "OSCAR protocol template" from Net::OSCAR::XML::protoparse,
2             # pack and unpack data according to the specification of that template.
3              
4             package Net::OSCAR::XML::Template;
5             BEGIN {
6 5     5   120 $Net::OSCAR::XML::Template::VERSION = '1.928';
7             }
8              
9 5     5   31 use strict;
  5         11  
  5         165  
10 5     5   27 use warnings;
  5         10  
  5         228  
11              
12 5     5   27 use Net::OSCAR::XML;
  5         12  
  5         154  
13 5     5   464 use Net::OSCAR::Common qw(:loglevels);
  5         10  
  5         1114  
14 5     5   451 use Net::OSCAR::Utility qw(hexdump);
  5         13  
  5         361  
15 5     5   27 use Net::OSCAR::TLV;
  5         10  
  5         227  
16 5     5   29 use Data::Dumper;
  5         16  
  5         219  
17 5     5   28 use Carp;
  5         10  
  5         18826  
18              
19             sub new($@) {
20 0     0 0   my $class = shift;
21 0   0       my $package = ref($class) || $class || "Net::OSCAR::XML::Template";
22 0           my $self = {template => $_[0]};
23 0 0 0       $self->{oscar} = $class->{oscar} if ref($class) and $class->{oscar};
24 0           bless $self, $package;
25 0           return $self;
26             }
27              
28             # Net::OSCAR::XML caches Template objects that don't have an associated OSCAR,
29             # so that the same Template can be reused with multiple OSCAR objects.
30             # Before returning a Template to the user, it calls set_oscar, so here we clone
31             # ourself with the new OSCAR.
32             #
33             sub set_oscar($$) {
34 0     0 0   my($self, $oscar) = @_;
35 0           my $clone = $self->new($self->{template});
36 0           $clone->{oscar} = $oscar;
37 0           return $clone;
38             }
39              
40              
41             # If given a scalar ref instead of a scalar as the second argument,
42             # we will modify the packet in-place.
43             sub unpack($$) {
44 0     0 0   my ($self, $x_packet) = @_;
45 0           my $oscar = $self->{oscar};
46 0           my $template = $self->{template};
47 0 0         my $packet = ref($x_packet) ? $$x_packet : $x_packet;
48              
49 0           my %data = ();
50              
51 0     0     $oscar->log_print_cond(OSCAR_DBG_XML, sub { "Decoding:\n", hexdump($packet), "\n according to: ", Data::Dumper::Dumper($template) });
  0            
52              
53 0           assert(ref($template) eq "ARRAY");
54 0           foreach my $datum (@$template) {
55             # In TLV chains, count refers to number of TLVs, not number of repetitions of the datum, so it defaults to infinite.
56 0   0       my $count = $datum->{count} || ($datum->{type} eq "tlvchain" ? -1 : 1);
57 0           my @results;
58              
59              
60             ## Figure out how much input data this datum is dealing with
61              
62 0 0 0       if($datum->{prefix} and $datum->{prefix} eq "count") {
63 0   0       ($count) = unpack($datum->{prefix_packlet}, substr($packet, 0, $datum->{prefix_len}, "")) || 0;
64             }
65              
66 0           my $size = undef;
67 0 0         if($datum->{type} eq "num") {
68 0 0         if($count != -1) {
69 0           $size = $datum->{len} * $count;
70             } else {
71 0           $size = length($packet);
72             }
73             } else {
74 0 0 0       if($datum->{prefix} and $datum->{prefix} eq "length") {
    0          
75 0           ($size) = unpack($datum->{prefix_packlet}, substr($packet, 0, $datum->{prefix_len}, ""));
76             } elsif(exists($datum->{len})) {
77             # In TLV chains, count is the number of TLVs, not a repeat
78             # count for the datum.
79 0 0         if($datum->{type} eq "tlvchain") {
80 0           $size = $datum->{len};
81             } else {
82 0 0         if($count == -1) {
83 0           $size = length($packet);
84             } else {
85 0           $size = $datum->{len} * $count;
86             }
87             }
88             }
89             }
90              
91 0           my $input;
92 0 0         if(defined($size)) {
93 0           $input = substr($packet, 0, $size, "");
94             } else {
95 0           $input = $packet;
96             }
97              
98              
99             ## Okay, we have our input data -- act on it
100              
101 0 0 0       if($datum->{type} eq "num") {
    0          
    0          
102 0   0       for(my $i = 0; ($input ne "") and ($count == -1 or $i < $count); $i++) {
      0        
103 0           push @results, unpack($datum->{packlet}, substr($input, 0, $datum->{len}, ""));
104              
105 0 0 0       if(exists($datum->{enum_byval}) and exists($datum->{enum_byval}->{$results[-1]})) {
106 0           $results[-1] = $datum->{enum_byval}->{$results[-1]};
107             }
108             }
109             } elsif($datum->{type} eq "data" or $datum->{type} eq "ref") {
110             # If we just have simple, no preset length, no subitems, raw data, it can't have a repeat count, since the first repetition will gobble up everything
111 0   0       assert($datum->{type} ne "data" or ($datum->{items} and @{$datum->{items}}) or defined($size) or $count == 1 or $datum->{null_terminated});
112              
113             # We want:
114             #
115             # to be empty string, not undefined, when length==0.
116 0 0 0       if(!$input and $count == 1 and defined($size)) {
      0        
117 0           push @results, "";
118             }
119              
120 0   0       for(my $i = 0; $input and ($count == -1 or $i < $count); $i++) {
      0        
121             # So, consider the structure:
122             #
123             #
124             #
125             #
126             # We don't know the size of 'foo' in advance.
127             # Thus, we pass a reference to the actual packet into protopack.
128             # subpacket will be modified to be the packet minus the bits that the contents of the data consumed.
129              
130 0           my %tmp;
131 0 0         if($datum->{type} eq "data") {
    0          
132 0           my $subinput;
133 0 0         if($datum->{len}) {
    0          
134 0           $subinput = substr($input, 0, $datum->{len}, "");
135             } elsif($datum->{null_terminated}) {
136 0           $input =~ s/^(.*?)\0//;
137 0           $subinput = $1;
138             } else {
139 0           $subinput = $input;
140 0           $input = "";
141             }
142              
143 0 0         if(exists($datum->{pad})) {
144 0           my $pad = chr($datum->{pad});
145 0           $subinput =~ s/$pad*$//;
146             }
147              
148 0 0 0       if($datum->{items} and @{$datum->{items}}) {
  0            
149 0           assert(!$datum->{null_terminated});
150 0           (%tmp) = $self->new($datum->{items})->unpack(\$subinput);
151 0 0         $input = $subinput unless $datum->{len};
152             } else {
153 0 0         $subinput =~ s/\0$// if $datum->{null_terminated};
154              
155             # The simple case -- raw
156 0 0         push @results, $subinput if $datum->{name};
157             }
158             } elsif($datum->{type} eq "ref") {
159 0           (%tmp) = protoparse($oscar, $datum->{name})->unpack(\$input);
160             }
161              
162 0 0         push @results, \%tmp if %tmp;
163             }
164             } elsif($datum->{type} eq "tlvchain") {
165 0           my @unknown;
166              
167             ## First set up a hash to store the data for each TLV, grouped by (sub)type
168             ##
169 0           my $tlvmap = tlv();
170 0 0         if($datum->{subtyped}) {
171 0           foreach (@{$datum->{items}}) {
  0            
172 0   0       $tlvmap->{$_->{num}} ||= tlv();
173 0   0       $tlvmap->{$_->{num}}->{$_->{subtype} || -1} = {%$_};
174             }
175             } else {
176 0           $tlvmap->{$_->{num}} = {%$_} foreach (@{$datum->{items}});
  0            
177             }
178              
179             ## Now, go through the chain and split the data into TLVs.
180             ##
181 0   0       for(my $i = 0; $input and ($count == -1 or $i < $count); $i++) {
      0        
182 0           my %tlv;
183 0 0         if($datum->{subtyped}) {
184 0           (%tlv) = protoparse($oscar, "subtyped_TLV")->unpack(\$input);
185             } else {
186 0           (%tlv) = protoparse($oscar, "TLV")->unpack(\$input);
187             }
188              
189 0           my $unknown = 0;
190 0 0         if(!exists($tlvmap->{$tlv{type}})) {
191 0 0         $tlvmap->{$tlv{type}} = $datum->{subtyped} ? tlv() : {};
192 0           $unknown = 1;
193             }
194              
195 0 0         assert(!exists($tlv{name})) if exists($tlv{count});
196 0 0         if($datum->{subtyped}) {
197 0           assert(exists($tlv{subtype}));
198              
199 0 0         if(!exists($tlvmap->{$tlv{type}}->{$tlv{subtype}})) {
200 0 0         if(exists($tlvmap->{$tlv{type}}->{-1})) {
201 0           $tlv{subtype} = -1;
202             } else {
203 0           $tlvmap->{$tlv{type}}->{$tlv{subtype}} = {};
204 0           $unknown = 1;
205             }
206             }
207              
208 0 0         if(!$unknown) {
209 0           my $type = $tlv{type};
210 0           my $subtype = $tlv{subtype};
211 0   0       $tlvmap->{$type}->{$subtype}->{data} ||= [];
212 0   0       $tlvmap->{$type}->{$subtype}->{outdata} ||= [];
213              
214 0 0         $tlv{data} = "" if !defined($tlv{data});
215 0           push @{$tlvmap->{$type}->{$subtype}->{data}}, $tlv{data};
  0            
216             } else {
217 0           push @unknown, {
218             type => $tlv{type},
219             subtype => $tlv{subtype},
220             data => $tlv{data}
221             };
222             }
223             } else {
224 0 0         if(!$unknown) {
225 0           my $type = $tlv{type};
226 0   0       $tlvmap->{$type}->{data} ||= [];
227 0   0       $tlvmap->{$type}->{outdata} ||= [];
228              
229 0 0         $tlv{data} = "" if !defined($tlv{data});
230 0           push @{$tlvmap->{$tlv{type}}->{data}}, $tlv{data};
  0            
231             } else {
232 0           push @unknown, {
233             type => $tlv{type},
234             data => $tlv{data}
235             };
236             }
237             }
238             }
239              
240             ## Almost done! Go back through the hash we made earlier, which now has the
241             ## data in it, and figure out which TLVs we want to emit.
242             ##
243 0           my @outvals;
244 0           while(my($num, $val) = each %$tlvmap) {
245 0 0         if($datum->{subtyped}) {
246 0           while(my($subtype, $subval) = each %$val) {
247 0 0         push @outvals, $subval if exists($subval->{data});
248             }
249             } else {
250 0 0         push @outvals, $val if exists($val->{data});
251             }
252             }
253              
254              
255             ## Okay, now take the TLVs to emit, and structure the output correctly
256             ## for each thing-to-emit. We'll need to do one last phase of postprocessing
257             ## so that we can group counted TLVs correctly.
258             ##
259 0           foreach my $val (@outvals) {
260 0           foreach (@{$val->{data}}) {
  0            
261 0 0         next unless exists($val->{items});
262 0           my(%tmp) = $self->new($val->{items})->unpack($_);
263             # We want:
264             #
265             # to give x => "" when TLV 1 is present but empty,
266             # not x => undef.
267 0 0 0       if(@{$val->{items}} == 1 and $val->{items}->[0]->{name}) {
  0            
268 0           my $name = $val->{items}->[0]->{name};
269 0 0         $tmp{$name} = "" if !defined($tmp{$name});
270             }
271              
272 0 0         if(@{$val->{items}}) {
  0            
273 0           push @{$val->{outdata}}, \%tmp;
  0            
274             } else {
275 0           push @{$val->{outdata}}, "";
  0            
276             }
277             }
278             }
279              
280              
281             ## Okay, we've stashed the output (formatted data structures) for each TLV.
282             ## Now we need to merge these into results.
283             ## This is normally just pushing everything out to results, as a hashref
284             ## under the TLVs name for named TLVs, but counted TLVs also need to
285             ## be layered into an array.
286             ##
287 0           foreach my $val (@outvals) {
288 0 0         if(exists($val->{count})) {
289 0 0         if(exists($val->{name})) {
290 0           push @results, {
291             $val->{name} => $val->{outdata}
292             };
293             } else {
294 0           push @results, $val->{outdata}->[0];
295             }
296             } else {
297 0 0         if(exists($val->{name})) {
298 0           push @results, {
299             $val->{name} => $val->{outdata}->[0]
300             };
301             } else {
302 0           push @results, $val->{outdata}->[0];
303             }
304             }
305             }
306              
307 0 0         push @results, {__UNKNOWN => [@unknown]} if @unknown;
308             }
309              
310              
311             # If we didn't know the length of the datum in advance,
312             # we've been modifying the entire packet in-place.
313 0 0         $packet = $input if !defined($size);
314              
315              
316             ## Okay, we have the results from this datum, store them away.
317              
318 0 0         if($datum->{name}) {
    0          
319 0 0 0       if($datum->{count} or ($datum->{prefix} and $datum->{prefix} eq "count")) {
  0 0 0        
      0        
      0        
320 0           $data{$datum->{name}} = \@results;
321             } elsif(
322             $datum->{type} eq "ref" or
323             (ref($datum->{items}) and @{$datum->{items}})
324             ) {
325 0           $data{$_} = $results[0]->{$_} foreach keys %{$results[0]};
  0            
326             } else {
327 0           $data{$datum->{name}} = $results[0];
328             }
329             } elsif(@results) {
330 0           foreach my $result(@results) {
331 0 0         next unless ref($result);
332 0           $data{$_} = $result->{$_} foreach keys %$result;
333             }
334             }
335             }
336              
337 0 0   0     $oscar->log_print_cond(OSCAR_DBG_XML, sub { "Decoded:\n", join("\n", map { "\t$_ => ".(defined($data{$_}) ? hexdump($data{$_}) : 'undef') } keys %data) });
  0            
  0            
338              
339             # Remember, passing in a ref to packet in place of actual packet data == in-place editing...
340 0 0         $$x_packet = $packet if ref($x_packet);
341              
342 0           return %data;
343             }
344              
345              
346             sub pack($%) {
347 0     0 0   my($self, %data) = @_;
348 0           my $packet = "";
349 0           my $oscar = $self->{oscar};
350 0           my $template = $self->{template};
351              
352 0 0   0     $oscar->log_print_cond(OSCAR_DBG_XML, sub { "Encoding:\n", join("\n", map { "\t$_ => ".(defined($data{$_}) ? hexdump($data{$_}) : 'undef') } keys %data), "\n according to: ", Data::Dumper::Dumper($template) });
  0            
  0            
353              
354 0           assert(ref($template) eq "ARRAY");
355 0           foreach my $datum (@$template) {
356 0           my $output = undef;
357              
358             ## Figure out what we're packing
359 0           my $value = undef;
360 0 0         $value = $data{$datum->{name}} if $datum->{name};
361 0 0         $value = $datum->{value} if !defined($value);
362 0 0         my @valarray = ref($value) eq "ARRAY" ? @$value : ($value); # Don't modify $value in-place!
363              
364 0 0 0       $datum->{count} = @valarray if $datum->{prefix} and $datum->{prefix} eq "count";
365 0 0         my $max_count = exists($datum->{count}) ? $datum->{count} : 1;
366 0           my $count = 0;
367              
368 0   0       assert($max_count == -1 or @valarray <= $max_count);
369              
370              
371             ## Pack it
372 0 0 0       if($datum->{type} eq "num") {
    0          
    0          
373 0 0         next unless defined($value);
374              
375 0   0       for($count = 0; ($max_count == -1 or $count < $max_count) and @valarray; $count++) {
      0        
376 0           my $val = shift @valarray;
377 0 0 0       if(exists($datum->{enum_byname}) and exists($datum->{enum_byname}->{$val})) {
378 0           $val = $datum->{enum_byname}->{$val};
379             }
380              
381 0           $output .= pack($datum->{packlet}, $val);
382             }
383             } elsif($datum->{type} eq "data" or $datum->{type} eq "ref") {
384 0   0       for($count = 0; ($max_count == -1 or $count < $max_count) and @valarray; $count++) {
      0        
385 0           my $val = shift @valarray;
386              
387 0 0 0       if($datum->{items} and @{$datum->{items}}) {
  0 0          
388 0 0         $output .= $self->new($datum->{items})->pack(ref($val) ? %$val : %data);
389             } elsif($datum->{type} eq "ref") {
390 0   0       assert($max_count == 1 or (ref($val) and ref($val) eq "HASH"));
391 0 0         $output .= protoparse($oscar, $datum->{name})->pack(ref($val) ? %$val : %data);
392             } else {
393 0 0         $output .= $val if defined($val);
394             }
395              
396 0 0         $output .= chr(0) if $datum->{null_terminated};
397 0 0         if(exists($datum->{pad})) {
398 0   0       assert(exists($datum->{len}) and exists($datum->{pad}));
399              
400 0 0         my $outlen = defined($output) ? length($output) : 0;
401 0           my $pad_needed = $datum->{len} - $outlen;
402 0 0         $output .= chr($datum->{pad}) x $pad_needed if $pad_needed;
403             }
404             }
405             } elsif($datum->{type} eq "tlvchain") {
406 0           foreach my $tlv (@{$datum->{items}}) {
  0            
407 0           my $tlvdata = undef;
408              
409 0 0         if(exists($tlv->{name})) {
410 0 0 0       if(exists($data{$tlv->{name}})) {
  0 0          
411 0 0         if(@{$tlv->{items}}) {
  0            
412 0   0       assert(ref($data{$tlv->{name}}) eq "HASH" or ref($data{$tlv->{name}}) eq "ARRAY");
413 0 0         if(ref($data{$tlv->{name}}) eq "ARRAY") {
414 0           $tlvdata = [];
415 0           push @$tlvdata, $self->new($tlv->{items})->pack(%$_) foreach @{$data{$tlv->{name}}};
  0            
416             } else {
417 0           $tlvdata = [$self->new($tlv->{items})->pack(%{$data{$tlv->{name}}})];
  0            
418             }
419             } else {
420 0 0         $tlvdata = [""] if defined($data{$tlv->{name}});
421             }
422             } elsif(exists($tlv->{value}) and !@{$tlv->{items}}) {
423 0           $tlvdata = [$tlv->{value}];
424             }
425             } else {
426 0           my $tmp = $self->new($tlv->{items})->pack(%data);
427              
428             # If TLV has no name and only one element, do special handling for "present but empty" value.
429 0 0 0       if($tmp ne "") {
  0 0 0        
    0 0        
430 0           $tlvdata = [$tmp];
431 0           } elsif(@{$tlv->{items}} == 1 and $tlv->{items}->[0]->{name} and exists($data{$tlv->{items}->[0]->{name}})) {
432 0           $tlvdata = [""];
433             } elsif(!@{$tlv->{items}} and exists($tlv->{value})) {
434 0           $tlvdata = [$tlv->{value}];
435             }
436             }
437            
438 0           assert($tlv->{num});
439 0 0         next unless defined($tlvdata);
440              
441 0           $count++;
442 0 0         if($datum->{subtyped}) {
443 0           my $subtype = 0;
444 0           assert(exists($tlv->{subtype}));
445 0 0         $subtype = $tlv->{subtype} if $tlv->{subtype} != -1;
446              
447             $output .= protoparse($oscar, "subtyped_TLV")->pack(
448             type => $tlv->{num},
449             subtype => $subtype,
450             data => $_
451 0           ) foreach @$tlvdata;
452             } else {
453             $output .= protoparse($oscar, "TLV")->pack(
454             type => $tlv->{num},
455             data => $_
456 0           ) foreach @$tlvdata;
457             }
458             }
459             }
460              
461              
462             ## Handle any prefixes
463 0 0 0       if($datum->{prefix} and defined($output)) {
464 0 0         if($datum->{prefix} eq "count") {
465 0           $packet .= pack($datum->{prefix_packlet}, $count);
466             } else {
467 0           $packet .= pack($datum->{prefix_packlet}, length($output));
468             }
469             }
470              
471 0 0         $packet .= $output if defined($output);
472             }
473              
474 0     0     $oscar->log_print_cond(OSCAR_DBG_XML, sub { "Encoded:\n", hexdump($packet) });
  0            
475 0           return $packet;
476             }
477              
478              
479             sub assert($) {
480 0     0 0   my $test = shift;
481 0 0         return if $test;
482 0           confess("Net::OSCAR internal error");
483             }
484              
485             # Why isn't this imported properly??
486 0     0 0   sub protoparse { Net::OSCAR::XML::protoparse(@_); }
487              
488             1;