# -*- mode: Perl -*-
######################################################################
# 04/09/99 rcc
# Insert 'use lib'
#
### SNMP Request/Response Handling
######################################################################
### The abstract class SNMP_Session defines objects that can be used
### to communicate with SNMP entities.  It has methods to send
### requests to and receive responses from an agent.
###
### Currently it has one subclass, SNMPv1_Session, which implements
### the SNMPv1 protocol.
######################################################################
### Created by:  Simon Leinen  <[email protected]>
###
### Contributions and fixes by:
###
### Matthew Trunnell <[email protected]>
### Tobias Oetiker <[email protected]>
### Heine Peters <[email protected]>
### Daniel L. Needles <[email protected]>
### Mike Mitchell <[email protected]>
### Clinton Wong <[email protected]>
### Alan Nichols <[email protected]>
### Mike McCauley <[email protected]>
######################################################################

package SNMP_Session;

require 5.002;

use strict qw(vars subs);       # cannot use strict subs here
                               # because of the way we use
                               # generated file handles
use Exporter;
use vars qw(@ISA $VERSION @EXPORT $errmsg $suppress_warnings);
use Socket;
use lib qw(. /usr/local/netprint/lib);
use BER;

sub map_table ($$$);
sub map_table_4 ($$$$);
sub map_table_start_end ($$$$$$);
sub index_compare ($$);
sub oid_diff ($$);

$VERSION = '0.70';

@ISA = qw(Exporter);

@EXPORT = qw(errmsg suppress_warnings index_compare oid_diff);

my $default_debug = 0;

### Default initial timeout (in seconds) waiting for a response PDU
### after a request is sent.  Note that when a request is retried, the
### timeout is increased by BACKOFF (see below).
###
my $default_timeout = 2.0;

### Default number of retries for each SNMP request.  If no response
### is received after TIMEOUT seconds, the request is resent and a new
### response awaited with a longer timeout (see the documentation on
### BACKOFF below).
###
my $default_retries = 5;

### Default backoff factor for SNMP_Session objects.  This factor is
### used to increase the TIMEOUT every time an SNMP request is
### retried.
###
my $default_backoff = 1.0;

### Default value for maxRepetitions.  This specifies how many table
### rows are requested in getBulk requests.  Used when walking tables
### using getBulk (only available in SNMPv2(c) and later).  If this is
### too small, then a table walk will need unnecessarily many
### request/response exchanges.  If it is too big, the agent may
### compute many variables after the end of the table.  It is
### recommended to set this explicitly for each table walk by using
### map_table_4().
###
my $default_max_repetitions = 12;

$SNMP_Session::errmsg = '';
$SNMP_Session::suppress_warnings = 0;

sub get_request      { 0 | context_flag };
sub getnext_request  { 1 | context_flag };
sub get_response     { 2 | context_flag };
sub set_request      { 3 | context_flag };
sub trap_request     { 4 | context_flag };
sub getbulk_request  { 5 | context_flag };
sub inform_request   { 6 | context_flag };
sub trap2_request    { 7 | context_flag };

sub standard_udp_port { 161 };

sub open
{
   return SNMPv1_Session::open (@_);
}

sub timeout { $_[0]->{timeout} }
sub retries { $_[0]->{retries} }
sub backoff { $_[0]->{backoff} }
sub set_timeout {
   my ($session, $timeout) = @_;
   die "timeout ($timeout) must be a positive number" unless $timeout > 0.0;
   $session->{'timeout'} = $timeout;
}
sub set_retries {
   my ($session, $retries) = @_;
   die "retries ($retries) must be a non-negative integer"
       unless $retries == int ($retries) && $retries >= 0;
   $session->{'retries'} = $retries;
}
sub set_backoff {
   my ($session, $backoff) = @_;
   die "backoff ($backoff) must be a number >= 1.0"
       unless $backoff == int ($backoff) && $backoff >= 1.0;
   $session->{'backoff'} = $backoff;
}

sub encode_request_3 ($$$@)
{
   my($this, $reqtype, $encoded_oids_or_pairs, $i1, $i2) = @_;
   my($request);
   local($_);

   ++$this->{request_id};
   foreach $_ (@{$encoded_oids_or_pairs}) {
     if (ref ($_) eq 'ARRAY') {
       $_ = &encode_sequence ($_->[0], $_->[1])
         || return $this->ber_error ("encoding pair");
     } else {
       $_ = &encode_sequence ($_, encode_null())
         || return $this->ber_error ("encoding value/null pair");
     }
   }
   $request = encode_tagged_sequence
       ($reqtype,
        encode_int ($this->{request_id}),
        defined $i1 ? encode_int ($i1) : encode_int_0,
        defined $i2 ? encode_int ($i2) : encode_int_0,
        encode_sequence (@{$encoded_oids_or_pairs}))
         || return $this->ber_error ("encoding request PDU");
   return $this->wrap_request ($request);
}

sub encode_get_request
{
   my($this, @oids) = @_;
   return encode_request_3 ($this, get_request, \@oids);
}

sub encode_getnext_request
{
   my($this, @oids) = @_;
   return encode_request_3 ($this, getnext_request, \@oids);
}

sub encode_getbulk_request
{
   my($this, $non_repeaters, $max_repetitions, @oids) = @_;
   return encode_request_3 ($this, getbulk_request, \@oids,
                            $non_repeaters, $max_repetitions);
}

sub encode_set_request
{
   my($this, @encoded_pairs) = @_;
   return encode_request_3 ($this, set_request, \@encoded_pairs);
}

sub encode_trap_request ($$$$$$@)
{
   my($this, $ent, $agent, $gen, $spec, $dt, @pairs) = @_;
   my($request);
   local($_);

   foreach $_ (@pairs) {
     if (ref ($_) eq 'ARRAY') {
       $_ = &encode_sequence ($_->[0], $_->[1])
         || return $this->ber_error ("encoding pair");
     } else {
       $_ = &encode_sequence ($_, encode_null())
         || return $this->ber_error ("encoding value/null pair");
     }
   }
   $request = encode_tagged_sequence
       (trap_request, $ent, $agent, $gen, $spec, $dt, encode_sequence (@pairs))
         || return $this->ber_error ("encoding trap PDU");
   return $this->wrap_request ($request);
}

sub decode_get_response
{
   my($this, $response) = @_;
   my @rest;
   @{$this->{'unwrapped'}};
}

sub decode_trap_request ($$) {
   my ($this, $trap) = @_;
   my ($snmp_version, $community, $ent, $agent, $gen, $spec, $dt,
       $bindings);
   ($snmp_version, $community, $ent, $agent, $gen, $spec, $dt, $bindings)
       = decode_by_template ($trap, "%{%i%s%*{%O%A%i%i%u%{%@",
                             SNMP_Session::trap_request
                             );
   return undef
       unless $snmp_version == $this->snmp_version ();
   if (!defined $ent) {
       warn "BER error decoding trap:\n  ".$BER::errmsg."\n";
   }
   return ($community, $ent, $agent, $gen, $spec, $dt, $bindings);
}

sub wait_for_response
{
   my($this) = shift;
   my($timeout) = shift || 10.0;
   my($rin,$win,$ein) = ('','','');
   my($rout,$wout,$eout);
   vec($rin,$this->sockfileno,1) = 1;
   select($rout=$rin,$wout=$win,$eout=$ein,$timeout);
}

sub get_request_response ($@)
{
   my($this, @oids) = @_;
   return $this->request_response_5 ($this->encode_get_request (@oids),
                                     get_response, \@oids, 1);
}

sub set_request_response ($@)
{
   my($this, @pairs) = @_;
   return $this->request_response_5 ($this->encode_set_request (@pairs),
                                     get_response, \@pairs, 1);
}

sub getnext_request_response ($@)
{
   my($this,@oids) = @_;
   return $this->request_response_5 ($this->encode_getnext_request (@oids),
                                     get_response, \@oids, 1);
}

sub getbulk_request_response ($$$@)
{
   my($this,$non_repeaters,$max_repetitions,@oids) = @_;
   return $this->request_response_5
       ($this->encode_getbulk_request ($non_repeaters,$max_repetitions,@oids),
        get_response, \@oids, 1);
}

sub trap_request_send ($$$$$$@)
{
   my($this, $ent, $agent, $gen, $spec, $dt, @pairs) = @_;
   my($req);

   $req = $this->encode_trap_request ($ent, $agent, $gen, $spec, $dt, @pairs);
   ## Encoding may have returned an error.
   return undef unless defined $req;
   $this->send_query($req)
       || return $this->error ("send_trap: $!");
   return 1;
}

sub request_response_5 ($$$$$)
{
   my ($this, $req, $response_tag, $oids, $errorp) = @_;
   my $retries = $this->retries;
   my $timeout = $this->timeout;
   my ($nfound, $timeleft);

   ## Encoding may have returned an error.
   return undef unless defined $req;

   $timeleft = $timeout;
   $this->send_query ($req)
       || return $this->error ("send_query: $!");
   while ($retries > 0) {
       ($nfound, $timeleft) = $this->wait_for_response($timeleft);
       if ($nfound > 0) {
           my($response_length);

           $response_length
               = $this->receive_response_3 ($response_tag, $oids, $errorp);
           if ($response_length) {
               return $response_length;
           } elsif (defined ($response_length)) {
               # A response has been received, but for a different
               # request ID.
           } else {
               return undef;
           }
       } else {
           ## No response received - retry
           --$retries;
           $timeout *= $this->backoff;
           $timeleft = $timeout;
           $this->send_query ($req)
               || return $this->error ("send_query: $!");
       }
   }
   $this->error ("no response received");
}


sub error_return ($$)
{
   my ($this,$message) = @_;
   $SNMP_Session::errmsg = $message;
   unless ($SNMP_Session::suppress_warnings) {
       $message =~ s/^/  /mg;
       warn ("Error:\n".$message."\n");
   }
   return undef;
}

sub error ($$)
{
   my ($this,$message) = @_;
   my $session = $this->to_string;
   $SNMP_Session::errmsg = $message."\n".$session;
   unless ($SNMP_Session::suppress_warnings) {
       $session =~ s/^/  /mg;
       $message =~ s/^/  /mg;
       warn ("SNMP Error:\n".$SNMP_Session::errmsg."\n");
   }
   return undef;
}

sub ber_error ($$)
{
 my ($this,$type) = @_;
 my ($errmsg) = $BER::errmsg;

 $errmsg =~ s/^/  /mg;
 return $this->error ("$type:\n$errmsg");
}

sub map_table ($$$) {
   my ($session, $columns, $mapfn) = @_;
   return $session->map_table_4 ($columns, $mapfn,
                                 $session->default_max_repetitions ());
}

sub map_table_4 ($$$$) {
   my ($session, $columns, $mapfn, $max_repetitions) = @_;
   return $session->map_table_start_end ($columns, $mapfn,
                                         "", undef,
                                         $max_repetitions);
}

sub map_table_start_end ($$$$$$) {
   my ($session, $columns, $mapfn, $start, $end, $max_repetitions) = @_;

   my @encoded_oids;
   my $call_counter = 0;
   my $base_index = $start;

   do {
       foreach (@encoded_oids = @{$columns}) {
           $_=encode_oid (@{$_},split '\.',$base_index)
               || return $session->ber_error ("encoding OID $base_index");
       }
       if ($session->getnext_request_response (@encoded_oids)) {
           my $response = $session->pdu_buffer;
           my ($bindings) = $session->decode_get_response ($response);
           my $smallest_index = undef;
           my @collected_values = ();

           my @bases = @{$columns};
           while ($bindings ne '') {
               my ($binding, $oid, $value);
               my $base = shift @bases;
               ($binding, $bindings) = decode_sequence ($bindings);
               ($oid, $value) = decode_by_template ($binding, "%O%@");

               my $out_index;

               $out_index = &oid_diff ($base, $oid);
               my $cmp;
               if (!defined $smallest_index
                   || ($cmp = index_compare ($out_index,$smallest_index)) == -1) {
                   $smallest_index = $out_index;
                   grep ($_=undef, @collected_values);
                   push @collected_values, $value;
               } elsif ($cmp == 1) {
                   push @collected_values, undef;
               } else {
                   push @collected_values, $value;
               }
           }
           (++$call_counter,
            &$mapfn ($smallest_index, @collected_values))
               if defined $smallest_index;
           $base_index = $smallest_index;
       } else {
           return undef;
       }
   }
   while (defined $base_index
          && (!defined $end || index_compare ($base_index, $end) < 0));
   $call_counter;
}

sub index_compare ($$) {
 my ($i1, $i2) = @_;
 $i1 = '' unless defined $i1;
 $i2 = '' unless defined $i2;
 if ($i1 eq '') {
     return $i2 eq '' ? 0 : 1;
 } elsif ($i2 eq '') {
     return 1;
 } elsif (!$i1) {
     return $i2 eq '' ? 1 : !$i2 ? 0 : 1;
 } elsif (!$i2) {
     return -1;
 } else {
   my ($f1,$r1) = split('\.',$i1,2);
   my ($f2,$r2) = split('\.',$i2,2);

   if ($f1 < $f2) {
     return -1;
   } elsif ($f1 > $f2) {
     return 1;
   } else {
     return index_compare ($r1,$r2);
   }
 }
}

sub oid_diff ($$) {
 my($base, $full) = @_;
 my $base_dotnot = join ('.',@{$base});
 my $full_dotnot = BER::pretty_oid ($full);

 return undef unless substr ($full_dotnot, 0, length $base_dotnot)
   eq $base_dotnot
     && substr ($full_dotnot, length $base_dotnot, 1) eq '.';
 substr ($full_dotnot, length ($base_dotnot)+1);
}

sub pretty_address
{
   my($addr) = shift;
   my($port,$ipaddr) = unpack_sockaddr_in($addr);
   return sprintf ("[%s].%d",inet_ntoa($ipaddr),$port);
}

sub version { $VERSION; }

package SNMPv1_Session;

use strict qw(vars subs);       # see above
use vars qw(@ISA);
use lib qw(. /usr/local/netprint/lib);
use SNMP_Session;
use Socket;
use BER;

@ISA = qw(SNMP_Session);

sub snmp_version { 0 }

sub open
{
   my($this,$remote_hostname,$community,$port,
      $max_pdu_len,$bind_to_port,$max_repetitions) = @_;
   my($name,$aliases,$remote_addr,$socket);

   my $udp_proto = 0;

   $community = 'public' unless defined $community;
   $port = SNMP_Session::standard_udp_port unless defined $port;
   $max_pdu_len = 8000 unless defined $max_pdu_len;
   $max_repetitions = $default_max_repetitions
       unless defined $max_repetitions;
   $remote_addr = inet_aton ($remote_hostname)
       || return $this->error_return ("can't resolve \"$remote_hostname\" to IP address");
   $socket = 'SNMP'.sprintf ("%s:%d", inet_ntoa ($remote_addr), $port);
   (($name,$aliases,$udp_proto) = getprotobyname('udp'))
       unless $udp_proto;
   $udp_proto=17 unless $udp_proto;
   socket ($socket, PF_INET, SOCK_DGRAM, $udp_proto)
       || return $this->error_return ("creating socket: $!");
   if (defined $bind_to_port) {
       my $sockaddr = sockaddr_in ($bind_to_port, INADDR_ANY);
       bind ($socket, $sockaddr)
           || return $this->error_return ("binding to port $bind_to_port: $!");
   }
   $remote_addr = pack_sockaddr_in ($port, $remote_addr);
   bless {
          'sock' => $socket,
          'sockfileno' => fileno ($socket),
          'community' => $community,
          'remote_hostname' => $remote_hostname,
          'remote_addr' => $remote_addr,
          'max_pdu_len' => $max_pdu_len,
          'pdu_buffer' => '\0' x $max_pdu_len,
          'request_id' => int (rand 0x80000000 + rand 0xffff),
          'timeout' => $default_timeout,
          'retries' => $default_retries,
          'backoff' => $default_backoff,
          'debug' => $default_debug,
          'error_status' => 0,
          'error_index' => 0,
          'default_max_repetitions' => $max_repetitions,
         };
}

sub open_trap_session (@) {
   my ($this, $port) = @_;
   $port = 162 unless defined $port;
   return $this->open ("0.0.0.0", "", 161, undef, $port);
}

sub sock { $_[0]->{sock} }
sub sockfileno { $_[0]->{sockfileno} }
sub remote_addr { $_[0]->{remote_addr} }
sub pdu_buffer { $_[0]->{pdu_buffer} }
sub max_pdu_len { $_[0]->{max_pdu_len} }
sub default_max_repetitions { $_[0]->{default_max_repetitions} }

sub close
{
   my($this) = shift;
   close ($this->sock) || $this->error ("close: $!");
}

sub wrap_request
{
   my($this) = shift;
   my($request) = shift;

   encode_sequence (encode_int ($this->snmp_version),
                    encode_string ($this->{community}),
                    $request)
     || return $this->ber_error ("wrapping up request PDU");
}

my @error_status_code = qw(noError tooBig noSuchName badValue readOnly
                          genErr noAccess wrongType wrongLength
                          wrongEncoding wrongValue noCreation
                          inconsistentValue resourceUnavailable
                          commitFailed undoFailed authorizationError
                          notWritable inconsistentName);

sub unwrap_response_5b
{
   my ($this,$response,$tag,$oids,$errorp) = @_;
   my ($community,$request_id,@rest,$snmpver);

   ($snmpver,$community,$request_id,
    $this->{error_status},
    $this->{error_index},
    @rest)
       = decode_by_template ($response, "%{%i%s%*{%i%i%i%{%@",
                             $tag);
   return $this->ber_error ("Error decoding response PDU")
     unless defined $snmpver;
   return $this->error ("Received SNMP response with unknown snmp-version field $snmpver")
       unless $snmpver == $this->snmp_version;
   if ($this->{error_status} != 0) {
     if ($errorp) {
       my ($oid, $errmsg);
       $errmsg = $error_status_code[$this->{error_status}] || $this->{error_status};
       $oid = $oids->[$this->{error_index}-1]
         if $this->{error_index} > 0 && $this->{error_index}-1 <= $#{$oids};
       $oid = $oid->[0]
         if ref($oid) eq 'ARRAY';
       return ($community, $request_id,
               $this->error ("Received SNMP response with error code\n"
                             ."  error status: $errmsg\n"
                             ."  index ".$this->{error_index}
                             .(defined $oid
                               ? " (OID: ".&BER::pretty_oid($oid).")"
                               : "")));
     } else {
       if ($this->{error_index} == 1) {
         @rest[$this->{error_index}-1..$this->{error_index}] = ();
       }
     }
   }
   ($community, $request_id, @rest);
}

sub send_query ($$)
{
   my ($this,$query) = @_;
   send ($this->sock,$query,0,$this->remote_addr);
}

sub receive_response_3
{
   my ($this, $response_tag, $oids, $errorp) = @_;
   my ($remote_addr);
   $remote_addr = recv ($this->sock,$this->{'pdu_buffer'},$this->max_pdu_len,0);
   return $this->error ("receiving response PDU: $!")
       unless defined $remote_addr;
   return $this->error ("short (".length $this->{'pdu_buffer'}
                        ." bytes) response PDU")
       unless length $this->{'pdu_buffer'} > 2;
   my $response = $this->{'pdu_buffer'};
   ##
   ## Check whether the response came from the address we've sent the
   ## request to.  If this is not the case, we should probably ignore
   ## it, as it may relate to another request.
   ##
   if ($this->{'debug'} && $remote_addr ne $this->{'remote_addr'}) {
       warn "Response came from ".&SNMP_Session::pretty_address($remote_addr)
           .", not ".&SNMP_Session::pretty_address($this->{'remote_addr'})
               unless $SNMP_Session::suppress_warnings;
   }
   $this->{'last_sender_addr'} = $remote_addr;
   my ($response_community, $response_id, @unwrapped)
       = $this->unwrap_response_5b ($response, $response_tag,
                                    $oids, $errorp);
   if ($response_community ne $this->{community}
       || $response_id ne $this->{request_id}) {
       if ($this->{'debug'}) {
           warn "$response_community != $this->{community}"
               unless $SNMP_Session::suppress_warnings
                   || $response_community eq $this->{community};
           warn "$response_id != $this->{request_id}"
               unless $SNMP_Session::suppress_warnings
                   || $response_id == $this->{request_id};
       }
       return 0;
   }
   if (!defined $unwrapped[0]) {
       $this->{'unwrapped'} = undef;
       return undef;
   }
   $this->{'unwrapped'} = \@unwrapped;
   return length $this->pdu_buffer;
}

sub receive_trap
{
   my ($this) = @_;
   my ($remote_addr, $iaddr, $port, $trap);
   $remote_addr = recv ($this->sock,$this->{'pdu_buffer'},$this->max_pdu_len,0);
   return undef unless $remote_addr;
   ($port, $iaddr) = sockaddr_in($remote_addr);
   $trap = $this->{'pdu_buffer'};
   return ($trap, $iaddr, $port);
}

sub describe
{
   my($this) = shift;
   print $this->to_string (),"\n";
}

sub to_string
{
   my($this) = shift;
   my ($class,$prefix);

   $class = ref($this);
   $prefix = ' ' x (length ($class) + 2);
   ($class." (remote host: \"".$this->{remote_hostname}
    ."\" ".&SNMP_Session::pretty_address ($this->remote_addr)."\n"
    .$prefix."  community: \"".$this->{'community'}."\"\n"
    .$prefix." request ID: ".$this->{'request_id'}."\n"
    .$prefix."PDU bufsize: ".$this->{'max_pdu_len'}." bytes\n"
    .$prefix."    timeout: ".$this->{timeout}."s\n"
    .$prefix."    retries: ".$this->{retries}."\n"
    .$prefix."    backoff: ".$this->{backoff}.")");
##    sprintf ("SNMP_Session: %s (size %d timeout %g)",
##    &SNMP_Session::pretty_address ($this->remote_addr),$this->max_pdu_len,
##             $this->timeout);
}

### SNMP Agent support
### contributed by Mike McCauley <[email protected]>
###
sub receive_request
{
   my ($this) = @_;
   my ($remote_addr, $iaddr, $port, $request);

   $remote_addr = recv($this->sock, $this->{'pdu_buffer'},
                       $this->{'max_pdu_len'}, 0);
   return undef unless $remote_addr;
   ($port, $iaddr) = sockaddr_in($remote_addr);
   $request = $this->{'pdu_buffer'};
   return ($request, $iaddr, $port);
}

sub decode_request
{
   my ($this, $request) = @_;
   my ($snmp_version, $community, $requestid, $errorstatus, $errorindex, $bindings);

   ($snmp_version, $community, $requestid, $errorstatus, $errorindex, $bindings)
       = decode_by_template ($request, "%{%i%s%*{%i%i%i%@", SNMP_Session::get_request);
   if (defined $snmp_version)
   {
       # Its a valid get_request
       return(SNMP_Session::get_request, $requestid, $bindings, $community);
   }

   ($snmp_version, $community, $requestid, $errorstatus, $errorindex, $bindings)
       = decode_by_template ($request, "%{%i%s%*{%i%i%i%@", SNMP_Session::getnext_request);
   if (defined $snmp_version)
   {
       # Its a valid getnext_request
       return(SNMP_Session::getnext_request, $requestid, $bindings, $community);
   }

   ($snmp_version, $community, $requestid, $errorstatus, $errorindex, $bindings)
       = decode_by_template ($request, "%{%i%s%*{%i%i%i%@", SNMP_Session::set_request);
   if (defined $snmp_version)
   {
       # Its a valid set_request
       return(SNMP_Session::set_request, $requestid, $bindings, $community);
   }

   # Something wrong with this packet
   # Decode failed
   return undef;
}

package SNMPv2c_Session;
use strict qw(vars subs);       # see above
use vars qw(@ISA);
use SNMP_Session;
use BER;

@ISA = qw(SNMPv1_Session);

sub snmp_version { 1 }

sub open
{
   my $session = SNMPv1_Session::open (@_);
   return bless $session;
}

sub map_table_start_end ($$$$$$) {
   my ($session, $columns, $mapfn, $start, $end, $max_repetitions) = @_;

   my @encoded_oids;
   my $call_counter = 0;
   my $base_index = $start;
   $max_repetitions = $session->default_max_repetitions
       unless defined $max_repetitions;

   do {
       foreach (@encoded_oids = @{$columns}) {
           $_=encode_oid (@{$_},split '\.',$base_index)
               || return $session->ber_error ("encoding OID $base_index");
       }
       if ($session->getbulk_request_response (0, $max_repetitions,
                                               @encoded_oids)) {
           my $response = $session->pdu_buffer;
           my ($bindings) = $session->decode_get_response ($response);
           my $smallest_index = undef;
           my @collected_values = ();

           my @bases = @{$columns};
           my $n_bindings = 0;
           while ($bindings ne '') {
               my ($binding, $oid, $value);
               unless (defined $bases[0]) {
                   @bases = @{$columns};
                   (++$call_counter,
                    &$mapfn ($smallest_index, @collected_values))
                       if defined $smallest_index;
                   $base_index = $smallest_index;
                   $smallest_index = undef;
                   @collected_values = ();
               }
               my $base = shift @bases;

               ($binding, $bindings) = decode_sequence ($bindings);
               ($oid, $value) = decode_by_template ($binding, "%O%@");

               my $out_index;

               ++$n_bindings;
               $out_index = SNMP_Session::oid_diff ($base, $oid);
               my $cmp;
               if (!defined $smallest_index
                   || ($cmp = SNMP_Session::index_compare
                       ($out_index,$smallest_index)) == -1) {
                   $smallest_index = $out_index;
                   grep ($_=undef, @collected_values);
                   push @collected_values, $value;
               } elsif ($cmp == 1) {
                   push @collected_values, undef;
               } else {
                   push @collected_values, $value;
               }
           }
           @bases = @{$columns};
           (++$call_counter,
            &$mapfn ($smallest_index, @collected_values))
               if defined $smallest_index;
           $base_index = $smallest_index;
           $smallest_index = undef;
           @collected_values = ();
       } else {
           return undef;
       }
   }
   while (defined $base_index
          && (!defined $end || index_compare ($base_index, $end) < 0));
   $call_counter;
}

1;