# These objects, initialized with an "OSCAR protocol template" from Net::OSCAR::XML::protoparse, # pack and unpack data according to the specification of that template. package Net::OSCAR::XML::Template; use strict; use warnings; use Net::OSCAR::XML; use Net::OSCAR::Common qw(:loglevels); use Net::OSCAR::Utility qw(hexdump); use Net::OSCAR::TLV; use Data::Dumper; use Carp; sub new($@) { my $class = shift; my $package = ref($class) || $class || "Net::OSCAR::XML::Template"; my $self = {template => $_[0]}; $self->{oscar} = $class->{oscar} if ref($class) and $class->{oscar}; bless $self, $package; return $self; } # Net::OSCAR::XML caches Template objects that don't have an associated OSCAR, # so that the same Template can be reused with multiple OSCAR objects. # Before returning a Template to the user, it calls set_oscar, so here we clone # ourself with the new OSCAR. # sub set_oscar($$) { my($self, $oscar) = @_; my $clone = $self->new($self->{template}); $clone->{oscar} = $oscar; return $clone; } # If given a scalar ref instead of a scalar as the second argument, # we will modify the packet in-place. sub unpack($$) { my ($self, $x_packet) = @_; my $oscar = $self->{oscar}; my $template = $self->{template}; my $packet = ref($x_packet) ? $$x_packet : $x_packet; my %data = (); $oscar->log_print_cond(OSCAR_DBG_XML, sub { "Decoding:\n", hexdump($packet), "\n according to: ", Data::Dumper::Dumper($template) }); assert(ref($template) eq "ARRAY"); foreach my $datum (@$template) { # In TLV chains, count refers to number of TLVs, not number of repetitions of the datum, so it defaults to infinite. my $count = $datum->{count} || ($datum->{type} eq "tlvchain" ? -1 : 1); my @results; ## Figure out how much input data this datum is dealing with if($datum->{prefix} and $datum->{prefix} eq "count") { ($count) = unpack($datum->{prefix_packlet}, substr($packet, 0, $datum->{prefix_len}, "")) || 0; } my $size = undef; if($datum->{type} eq "num") { if($count != -1) { $size = $datum->{len} * $count; } else { $size = length($packet); } } else { if($datum->{prefix} and $datum->{prefix} eq "length") { ($size) = unpack($datum->{prefix_packlet}, substr($packet, 0, $datum->{prefix_len}, "")); } elsif(exists($datum->{len})) { # In TLV chains, count is the number of TLVs, not a repeat # count for the datum. if($datum->{type} eq "tlvchain") { $size = $datum->{len}; } else { if($count == -1) { $size = length($packet); } else { $size = $datum->{len} * $count; } } } } my $input; if(defined($size)) { $input = substr($packet, 0, $size, ""); } else { $input = $packet; } ## Okay, we have our input data -- act on it if($datum->{type} eq "num") { for(my $i = 0; ($input ne "") and ($count == -1 or $i < $count); $i++) { push @results, unpack($datum->{packlet}, substr($input, 0, $datum->{len}, "")); if(exists($datum->{enum_byval}) and exists($datum->{enum_byval}->{$results[-1]})) { $results[-1] = $datum->{enum_byval}->{$results[-1]}; } } } elsif($datum->{type} eq "data" or $datum->{type} eq "ref") { # 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 assert($datum->{type} ne "data" or ($datum->{items} and @{$datum->{items}}) or defined($size) or $count == 1 or $datum->{null_terminated}); # We want: # # to be empty string, not undefined, when length==0. if(!$input and $count == 1 and defined($size)) { push @results, ""; } for(my $i = 0; $input and ($count == -1 or $i < $count); $i++) { # So, consider the structure: # # # # # We don't know the size of 'foo' in advance. # Thus, we pass a reference to the actual packet into protopack. # subpacket will be modified to be the packet minus the bits that the contents of the data consumed. my %tmp; if($datum->{type} eq "data") { my $subinput; if($datum->{len}) { $subinput = substr($input, 0, $datum->{len}, ""); } elsif($datum->{null_terminated}) { $input =~ s/^(.*?)\0//; $subinput = $1; } else { $subinput = $input; $input = ""; } if(exists($datum->{pad})) { my $pad = chr($datum->{pad}); $subinput =~ s/$pad*$//; } if($datum->{items} and @{$datum->{items}}) { assert(!$datum->{null_terminated}); (%tmp) = $self->new($datum->{items})->unpack(\$subinput); $input = $subinput unless $datum->{len}; } else { $subinput =~ s/\0$// if $datum->{null_terminated}; # The simple case -- raw push @results, $subinput if $datum->{name}; } } elsif($datum->{type} eq "ref") { (%tmp) = protoparse($oscar, $datum->{name})->unpack(\$input); } push @results, \%tmp if %tmp; } } elsif($datum->{type} eq "tlvchain") { my @unknown; ## First set up a hash to store the data for each TLV, grouped by (sub)type ## my $tlvmap = tlv(); if($datum->{subtyped}) { foreach (@{$datum->{items}}) { $tlvmap->{$_->{num}} ||= tlv(); $tlvmap->{$_->{num}}->{$_->{subtype} || -1} = {%$_}; } } else { $tlvmap->{$_->{num}} = {%$_} foreach (@{$datum->{items}}); } ## Now, go through the chain and split the data into TLVs. ## for(my $i = 0; $input and ($count == -1 or $i < $count); $i++) { my %tlv; if($datum->{subtyped}) { (%tlv) = protoparse($oscar, "subtyped_TLV")->unpack(\$input); } else { (%tlv) = protoparse($oscar, "TLV")->unpack(\$input); } my $unknown = 0; if(!exists($tlvmap->{$tlv{type}})) { $tlvmap->{$tlv{type}} = $datum->{subtyped} ? tlv() : {}; $unknown = 1; } assert(!exists($tlv{name})) if exists($tlv{count}); if($datum->{subtyped}) { assert(exists($tlv{subtype})); if(!exists($tlvmap->{$tlv{type}}->{$tlv{subtype}})) { if(exists($tlvmap->{$tlv{type}}->{-1})) { $tlv{subtype} = -1; } else { $tlvmap->{$tlv{type}}->{$tlv{subtype}} = {}; $unknown = 1; } } if(!$unknown) { my $type = $tlv{type}; my $subtype = $tlv{subtype}; $tlvmap->{$type}->{$subtype}->{data} ||= []; $tlvmap->{$type}->{$subtype}->{outdata} ||= []; $tlv{data} = "" if !defined($tlv{data}); push @{$tlvmap->{$type}->{$subtype}->{data}}, $tlv{data}; } else { push @unknown, { type => $tlv{type}, subtype => $tlv{subtype}, data => $tlv{data} }; } } else { if(!$unknown) { my $type = $tlv{type}; $tlvmap->{$type}->{data} ||= []; $tlvmap->{$type}->{outdata} ||= []; $tlv{data} = "" if !defined($tlv{data}); push @{$tlvmap->{$tlv{type}}->{data}}, $tlv{data}; } else { push @unknown, { type => $tlv{type}, data => $tlv{data} }; } } } ## Almost done! Go back through the hash we made earlier, which now has the ## data in it, and figure out which TLVs we want to emit. ## my @outvals; while(my($num, $val) = each %$tlvmap) { if($datum->{subtyped}) { while(my($subtype, $subval) = each %$val) { push @outvals, $subval if exists($subval->{data}); } } else { push @outvals, $val if exists($val->{data}); } } ## Okay, now take the TLVs to emit, and structure the output correctly ## for each thing-to-emit. We'll need to do one last phase of postprocessing ## so that we can group counted TLVs correctly. ## foreach my $val (@outvals) { foreach (@{$val->{data}}) { next unless exists($val->{items}); my(%tmp) = $self->new($val->{items})->unpack($_); # We want: # # to give x => "" when TLV 1 is present but empty, # not x => undef. if(@{$val->{items}} == 1 and $val->{items}->[0]->{name}) { my $name = $val->{items}->[0]->{name}; $tmp{$name} = "" if !defined($tmp{$name}); } if(@{$val->{items}}) { push @{$val->{outdata}}, \%tmp; } else { push @{$val->{outdata}}, ""; } } } ## Okay, we've stashed the output (formatted data structures) for each TLV. ## Now we need to merge these into results. ## This is normally just pushing everything out to results, as a hashref ## under the TLVs name for named TLVs, but counted TLVs also need to ## be layered into an array. ## foreach my $val (@outvals) { if(exists($val->{count})) { if(exists($val->{name})) { push @results, { $val->{name} => $val->{outdata} }; } else { push @results, $val->{outdata}->[0]; } } else { if(exists($val->{name})) { push @results, { $val->{name} => $val->{outdata}->[0] }; } else { push @results, $val->{outdata}->[0]; } } } push @results, {__UNKNOWN => [@unknown]} if @unknown; } # If we didn't know the length of the datum in advance, # we've been modifying the entire packet in-place. $packet = $input if !defined($size); ## Okay, we have the results from this datum, store them away. if($datum->{name}) { if($datum->{count} or ($datum->{prefix} and $datum->{prefix} eq "count")) { $data{$datum->{name}} = \@results; } elsif( $datum->{type} eq "ref" or (ref($datum->{items}) and @{$datum->{items}}) ) { $data{$_} = $results[0]->{$_} foreach keys %{$results[0]}; } else { $data{$datum->{name}} = $results[0]; } } elsif(@results) { foreach my $result(@results) { next unless ref($result); $data{$_} = $result->{$_} foreach keys %$result; } } } $oscar->log_print_cond(OSCAR_DBG_XML, sub { "Decoded:\n", join("\n", map { "\t$_ => ".(defined($data{$_}) ? hexdump($data{$_}) : 'undef') } keys %data) }); # Remember, passing in a ref to packet in place of actual packet data == in-place editing... $$x_packet = $packet if ref($x_packet); return %data; } sub pack($%) { my($self, %data) = @_; my $packet = ""; my $oscar = $self->{oscar}; my $template = $self->{template}; $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) }); assert(ref($template) eq "ARRAY"); foreach my $datum (@$template) { my $output = undef; ## Figure out what we're packing my $value = undef; $value = $data{$datum->{name}} if $datum->{name}; $value = $datum->{value} if !defined($value); my @valarray = ref($value) eq "ARRAY" ? @$value : ($value); # Don't modify $value in-place! $datum->{count} = @valarray if $datum->{prefix} and $datum->{prefix} eq "count"; my $max_count = exists($datum->{count}) ? $datum->{count} : 1; my $count = 0; assert($max_count == -1 or @valarray <= $max_count); ## Pack it if($datum->{type} eq "num") { next unless defined($value); for($count = 0; ($max_count == -1 or $count < $max_count) and @valarray; $count++) { my $val = shift @valarray; if(exists($datum->{enum_byname}) and exists($datum->{enum_byname}->{$val})) { $val = $datum->{enum_byname}->{$val}; } $output .= pack($datum->{packlet}, $val); } } elsif($datum->{type} eq "data" or $datum->{type} eq "ref") { for($count = 0; ($max_count == -1 or $count < $max_count) and @valarray; $count++) { my $val = shift @valarray; if($datum->{items} and @{$datum->{items}}) { $output .= $self->new($datum->{items})->pack(ref($val) ? %$val : %data); } elsif($datum->{type} eq "ref") { assert($max_count == 1 or (ref($val) and ref($val) eq "HASH")); $output .= protoparse($oscar, $datum->{name})->pack(ref($val) ? %$val : %data); } else { $output .= $val if defined($val); } $output .= chr(0) if $datum->{null_terminated}; if(exists($datum->{pad})) { assert(exists($datum->{len}) and exists($datum->{pad})); my $outlen = defined($output) ? length($output) : 0; my $pad_needed = $datum->{len} - $outlen; $output .= chr($datum->{pad}) x $pad_needed if $pad_needed; } } } elsif($datum->{type} eq "tlvchain") { foreach my $tlv (@{$datum->{items}}) { my $tlvdata = undef; if(exists($tlv->{name})) { if(exists($data{$tlv->{name}})) { if(@{$tlv->{items}}) { assert(ref($data{$tlv->{name}}) eq "HASH" or ref($data{$tlv->{name}}) eq "ARRAY"); if(ref($data{$tlv->{name}}) eq "ARRAY") { $tlvdata = []; push @$tlvdata, $self->new($tlv->{items})->pack(%$_) foreach @{$data{$tlv->{name}}}; } else { $tlvdata = [$self->new($tlv->{items})->pack(%{$data{$tlv->{name}}})]; } } else { $tlvdata = [""] if defined($data{$tlv->{name}}); } } elsif(exists($tlv->{value}) and !@{$tlv->{items}}) { $tlvdata = [$tlv->{value}]; } } else { my $tmp = $self->new($tlv->{items})->pack(%data); # If TLV has no name and only one element, do special handling for "present but empty" value. if($tmp ne "") { $tlvdata = [$tmp]; } elsif(@{$tlv->{items}} == 1 and $tlv->{items}->[0]->{name} and exists($data{$tlv->{items}->[0]->{name}})) { $tlvdata = [""]; } elsif(!@{$tlv->{items}} and exists($tlv->{value})) { $tlvdata = [$tlv->{value}]; } } assert($tlv->{num}); next unless defined($tlvdata); $count++; if($datum->{subtyped}) { my $subtype = 0; assert(exists($tlv->{subtype})); $subtype = $tlv->{subtype} if $tlv->{subtype} != -1; $output .= protoparse($oscar, "subtyped_TLV")->pack( type => $tlv->{num}, subtype => $subtype, data => $_ ) foreach @$tlvdata; } else { $output .= protoparse($oscar, "TLV")->pack( type => $tlv->{num}, data => $_ ) foreach @$tlvdata; } } } ## Handle any prefixes if($datum->{prefix} and defined($output)) { if($datum->{prefix} eq "count") { $packet .= pack($datum->{prefix_packlet}, $count); } else { $packet .= pack($datum->{prefix_packlet}, length($output)); } } $packet .= $output if defined($output); } $oscar->log_print_cond(OSCAR_DBG_XML, sub { "Encoded:\n", hexdump($packet) }); return $packet; } sub assert($) { my $test = shift; return if $test; confess("Net::OSCAR internal error"); } # Why isn't this imported properly?? sub protoparse { Net::OSCAR::XML::protoparse(@_); } 1;