#!/usr/bin/perl
#
#  aggis - describe what the given aggregate means.
#
#       usage:  aggis [-d|-D] ip_prefix[/prefix_length]
#          or:  aggis [-d|-D] aggregate #_of_nets
#          or:  aggis [-flags] aggregate - aggregate\n" .
#                    e.g.  aggis 141.212 - 141.214 \n" .
#          or:  aggis [-d|-D] aggregate - <end_last_byte_of_prefix_value>
#                    e.g.  aggis 141.212/18 - 216 
#
#              flag -d: "include dotted decimal masks"
#              flag -D: "include inverted dotted decimal masks"
#              flag -q: always use "dotted.quad" format
#              flag -l: expand only into a list of classful nets.
#              flag -r: always use "host ranges" for expansions.
#              flag -h: print this "help"
#              flag -l: expand into a list of classful ip/lens\n" .
#              flag -L: expand into a list of classful ip's only\n" .
#              flag -T: trim extra blank lines
#
# Dale Johnson, Merit (dsj@merit.edu) 
# & Tony Bates, RIPE  (Tony.Bates@ripe.net)
#
#  This program and the accompanying man page are available from:
#       merit.edu:pub/nsfnet/cidr/aggis and aggis.l .
#
# $Id: aggis,v 2.7 1994/11/18 20:44:11 dsj Exp $
#
require "getopts.pl";

&Getopts('DLTdlqrh') || &usage ;        # Sets $opt_d if "-d" specified, etc.
&usage if ( @ARGV < 1 || @ARGV > 3 );
&usage if ( $opt_h );                   # -h = "help"

$DOTTED_QUAD = $opt_q ;                 # -q = "dotted.quad format"
$AGG_LIST_ONLY = $opt_l ;               # -l = wants a list of classful aggs
$LIST_ONLY = $opt_L || $AGG_LIST_ONLY;  # -L = wants a list of classful nets
$HOST_RANGE = $opt_r   ;                # -r = "net-range format"
$NO_EXTRA_LINES = $opt_T   ;            # -T = "skip extra blank lines"
                                        #       (used by nets.nag)
$arg1 = @ARGV[0];
#
#  If a 2nd parameter is supplied, its # of nets
#  If a 3nd parameter is supplied, the syntax is "IP - IP" 
#                                             or "IP - <new_last_byte>"
#
if ( @ARGV >= 2 )
        {
        if ( @ARGV == 2 )
                {
                #
                #  Two parameters:  "IP <# of nets>"
                #
                $n_nets = $ARGV[1] ;
                &usage("2nd parameter not a legal number of nets.") 
                        if ( ($n_nets !~ m/^\d+$/) || (int( $n_nets ) <= 0 ));
                ($ip, $len ) = &parse_arg( $arg1 );
                $one_net = ( 1 << 32-$len );

                print "\n" unless ($NO_EXTRA_LINES);
                printf "  %s starting at %s can be represented as:\n\n", 
                        &class_nets( $ip, $len, $n_nets),
                        &printip( $ip, $len );
                &reagg( $ip, $ip + ($one_net*$n_nets) - 1 );
                exit(0);
                }
        else    # @ARGV == 3 
                {
                &usage if $ARGV[1] ne "-";
                $arg3 = $ARGV[2];

                if ( $arg3 =~ /^\d+$/ )
                        {
                        #
                        #  Three parameters:  "IP - <new_last_byte>"
                        #
                        ( $START_ip, $len1 ) = &parse_arg( $arg1 );
                        $end_arg = $arg1 ;
                        if ( $end_arg =~ m'/' )
                                { $end_arg =~ s"\d+\.*/"$arg3/" ; }
                        else     
                                { $end_arg =~ s"\d+\.*$"$arg3" ; }
                        ( $END_ip, $len ) = &parse_arg( $end_arg );

                        }
                else
                        {
                        #
                        #  Three parameters:  "IP - IP"
                        #
                        ( $START_ip, $len1) = &parse_arg( $arg1 );
                        ( $END_ip,   $len ) = &parse_arg( $arg3 );
                        }

                &usage("Error: the value \"$end_arg\" is lower " .
                         "than \"$arg1\".") if( $END_ip < $START_ip );

                print "\n" unless ($NO_EXTRA_LINES);

                printf "The range of nets from %s to %s can be" .
                       " represented by:\n\n",
                        &printip( $START_ip, $len1 ),
                        &printagg( $END_ip, $len );
                $END_ip += ( 1 << 32-$len )-1;
                &reagg( $START_ip, $END_ip );
                exit(0);
                }
        }


#
#  Ok;  this is a simple one-argument query:  "Translate this thing in
#                                  classless notation to classful notation"
#
($ip, $len) = &parse_arg( $arg1 );
$this_agg = &printagg( $ip, $len );


$mask = &mask_of_len($len) ;

if ( $LIST_ONLY )
        {
        # Nice feature suggested by Peter Merdian:
        #
        # List out the classful nets one per line (for use in scripts)
        #
        $n_nets = ( $std_masklen < $len ) ? 0 : 
                                           ( 1 << ( $std_masklen - $len ));
        while ( $n_nets-- )
                {
                printf "%s\n", 
                        (($AGG_LIST_ONLY) ? &printagg( $ip, $std_masklen ):
                                            &printip( $ip, $std_masklen ));
                $ip += $one_net ;
                }

        exit(0);
        }


if ( $len == 0 )
        {
        printf "\n  0/0 specifies \"default\".\n\n";
        }
elsif ( $len == 32 )
        {
        printf "\n  %s is the address of one host.\n\n", $this_agg;
        }
elsif ( $len == $std_masklen ) 
        {
        printf "\n  %s is equivalent to Class $CLASS network %s\n\n", 
                        $this_agg, &printip( $ip, $std_masklen );
        }
elsif ( $len > $std_masklen )
        {
        #
        #  Subnet of Class X.   (format for 1 or two lines).
        #
        $fraction = ( 1 << ( $len - $std_masklen ));
        $head_of_line = sprintf("\n  %s is a subnet representing 1/%d of " . 
                                            "Class ${CLASS} network",
                $this_agg, $fraction);
        $ip_string = &printip( ($ip & &mask_of_len($std_masklen)), 
                                                             $std_masklen );
        if ((length($head_of_line) + length($ip_string )) < 78 )
                {
                $line = sprintf("%s %s", $head_of_line, $ip_string );
                print $line,"\n\n";
                $prev_linelen = length($line);
                }
        else
                {
                printf("%s\n%s%s\n\n", $head_of_line, 
                        " " x ( 76 - length( $ip_string )),
                        $ip_string);
                $prev_linelen = 77 ;
                }

        $line = sprintf("Address Range %s - %s", 
                        &printip( $ip, 32 ),
                        &printip( $ip + ( 1 << 32-$len )-1 , 32 ));
        print " " x ($prev_linelen - length($line) - 1), $line, "\n";
        }
else
        {
        #
        #  Aggregate equivalent to N Class X's.  (2 aligned lines).
        #
        $n_nets = ( 1 << ( $std_masklen - $len ));
        $end_ip  = $ip + ( ($n_nets-1) * $one_net );
        $head_of_line = sprintf(
          "  %s is an aggregate equivalent to %d Class ${CLASS}s:",
                &printagg( $ip, $len ), $n_nets);
        printf "\n%s  %s\n%sto  %s\n",
                $head_of_line,
                &printip( $ip, $std_masklen ),
                " " x ( length( $head_of_line) -2 ),
                &printip( $end_ip, $std_masklen );
        }

exit(0);
                        
#------------------------------- subroutines ----------------------------

sub usage
        {
        local( $msg ) = @_ ;
        $Rev = (split(' ', '$Revision: 2.7 $'))[1];     # grab RCSID string
        printf STDERR "\n%s\n\n", $msg if ($msg);
        printf STDERR 
              "usage:  aggis [-flags] ip_prefix[/prefix_length]" .
                                                "\t(Version $Rev)\n" .
              "   or:  aggis [-flags] aggregate #_of_nets\n" .
              "   or:  aggis [-flags] aggregate - aggregate\n" .
              "             e.g.  aggis 141.212 - 141.214 \n" .
              "   or:  aggis [-flags] aggregate - <end_last_byte_value>\n" .
              "             e.g.  aggis 141.212/18 - 216 \n" .
              "\n" .
              "           flag -d: \"include dotted decimal masks\"\n" .
              "           flag -D: \"include inverted dotted decimal masks\"\n".
              "           flag -r: always use \"host ranges\" for expansions.\n".
              "           flag -q: always use \"dotted.quad\" format\n" .
              "           flag -l: expand into a list of classful ip/lens\n" .
              "           flag -L: expand into a list of classful ip's only\n" .
              "           flag -T: trim extra blank lines on expansions\n" .
              "           flag -h: print this \"help\".\n" ;
        exit (1);
        }

#
#  $mask = &mask_of_len( $len );
#
#  This is in a subroutine only because the algorithm is so obscure:
#
#  For $len = 8,
#      32 - $len = 24
#      1 << $len = 0x01000000
#      negative of that = 0xff000000
#
sub mask_of_len
        {
        local( $len ) = @_ ;
        return(-( 1 << (32 - $len )));
        }

sub parse_arg
        {
        #
        #  Parse the Prefix:
        #
        local( $arg )= @_ ;
        local( $ip, $len, @butes, @F );

        @bytes = split( '\.', $arg );
        $lowest_explicit_byte = $#bytes ;

        for ($i=0; $i<4 ; $i++ )
                {
                if ( ($bytes[$i] < 0 ) || $bytes[$i] > 255 )
                        {
                        printf "Illegal byte value %s\n", $bytes[$i];
                        exit;
                        }
                $ip = ($ip << 8) + $bytes[$i] ;
                }
        #
        #  Set global parameters:  $CLASS, $std_masklen, $one_net
        #       based on the high byte of the IP address.
        #
        &determine_class( $bytes[0] );

        #
        #  Parse the 1st parameter for a "/length".
        #  Supply a classful default length ONLY if no length was specified.
        #
        @F = split( '/', $arg ) ;
        $length_given = ( @F == 2 );

        if ( $length_given )                     # Was an /pfxlen given?
                {
                $len = $F[1];
                }
        else
                {       
                #  Old version:  user the classful default lengths.
                #
                $len = ( $bytes[0] < 128 ) ? 8  : # No explicit length given.
                       ( $bytes[0] < 192 ) ? 16 : # Default to classful defn.
                                             24 ;
                #
                #  *IF* this default causes a CIDR-alignment problem,
                #    then that's probably not what they wanted.  Try
                #    8, 16, 24, or 32 based on the number of octets 
                #    that they supplied when they typed the address.  
                #    E.g.,:
                #    35   ->  35/8
                #    35.1 -> 35/16
                #    193  -> 193/8
                #    193.128.2.15 -> 193.128.2.15/24
                #
                #  I know this is ugly, but it comes from having different
                #  ways of using IP notation.

                $mask = &mask_of_len( $len );
                $len =  (8,16,24,32)[$lowest_explicit_byte] 
                                                if ( ($mask & $ip) != $ip );
                #
                }
        #
        #  Handle illegal specified lengths:
        #
        if ( ( $len > 32 ) || ( $len < 0 ))
                {
                print "-" x 78, "\n" ;
                printf 
                qq|Error:  "/length" must be between 0 and 32 (not %d).\n|, 
                        $len ;

                print "-" x 78, "\n" ;
                exit (1) ;
                }

        $mask = &mask_of_len( $len );
        #
        #  Handle non-CIDR-aligned blocks:
        #
        $adjusted_ip = $ip & $mask ;
        if ( $adjusted_ip != $ip )
                {
                print "-" x 78, "\n" ;

                printf  "Error:  The aggregate \"%s\" is invalid.  " .
                                                "(There are bits in \"%s\"\n" .
                        "        outside of mask \"%s\" ).\n",
                        &printagg( $ip, $len ),
                        &printip( $ip, $len ),
                        &printip( $mask, 32 );

                for ( $count=0, $bits=$ip ; $bits ; $bits <<= 1 )
                        { $count++; }
                
                printf 
                "\n        The largest aggregate (smallest prefix length) " .
                                   "using the\n" . 
                        "        prefix \"%s\" is:\n\n" ,
                                &printip( $ip, $len ),
                                &printagg( $ip, $count ) ;

                &expand_1_agg( $ip, $count );

                printf "\n        The \"*/%d\" aggregate containing \"%s\" " .
                                                        "is:\n\n",
                        $len,
                        &printip( $ip, $len ),
                        &printagg( $adjusted_ip, $len ) ;

                &expand_1_agg( $adjusted_ip, $len );

                print "-" x 78, "\n" ;
                exit(1);
                }
        return( ( $ip, $len ) );
        }

sub determine_class
        {
        local($highbyte) = @_ ;

        if ( $highbyte < 128 )
                {
                $CLASS = 'A' ;
                $std_masklen = 8;
                $one_net = 0x01000000 ;
                }
        elsif ( $highbyte < 192 )
                {
                $CLASS = 'B' ;
                $std_masklen = 16;
                $one_net = 0x00010000 ;
                }
        elsif ( $highbyte < 224 )
                {
                $CLASS = 'C' ;
                $std_masklen = 24;
                $one_net = 0x00000100 ;
                }
        else
                {
                printf "\n  %s specifies a multicast address.  Reject it.\n\n",
                        $arg ;
                exit(1);
                }
        }

sub printagg
        {
        local($ip, $len) = @_ ;

        return( 
            ( $opt_d ) ? sprintf("%s/%d(%s)", 
                        &printip($ip,$len),$len,&printip(&mask_of_len($len))) :
            ( $opt_D ) ? sprintf("%s/%d(%s)", 
                        &printip($ip,$len),$len,&printip(~ &mask_of_len($len))):
                         sprintf("%s/%d", &printip($ip,$len), $len ) 
                ) ;
        }

sub printip
        {
        local($ip, $len) = @_ ;
        local( $s );

        $byte0 = $ip >> 24 ;
        $byte1 = ($ip & 0x00ff0000) >> 16 ;
        $byte2 = ($ip & 0x0000ff00) >>  8 ;
        $byte3 = ($ip & 0x000000ff) ;

        if ( ($byte3) || $len > 24 || $DOTTED_QUAD ) 
                { $s=sprintf ("%d.%d.%d.%d", $byte0, $byte1, $byte2, $byte3 );}
        elsif ( ($byte2) || $len > 16 ) 
                { $s=sprintf ("%d.%d.%d", $byte0, $byte1, $byte2 );}
        elsif ( ($byte1) || $len > 8 ) 
                { $s=sprintf ("%d.%d", $byte0, $byte1 );}
        else
                { $s=sprintf ("%d", $byte0 );}
        
        return( $s );
        }

#
#  Tell what aggregates would be necessary to represent this.
#
sub reagg
        {
        local( $start_ip, $end_ip ) = @_;

        do { $start_ip = &print_part( $start_ip, $end_ip ) }
                                        while ( $start_ip <= $end_ip );

        print "\n" unless ($NO_EXTRA_LINES);
        }

#
#  (Used only by reagg):  determine & print one of the reaggregation aggregates.
#
sub print_part
        {
        local( $start, $end ) = @_ ;
        local( $pwr );
        exit(0) if ( ! ~ $start );       # 0xffffffff;  255.255.255.255

        #
        # Try adding blocks of 1, 2, 4... to the start address.
        # Stop checking (& use the highest previous value) when the 
        #   end address that gives is beyond the target end address,
        #   OR when the resulting aggregate no longer starts at the
        #   start address (e.g. it has bits beyond the masklen).
        #
        for ( $pwr=1 ; $pwr <= 32 ; $pwr++ )
                {
                $pwr2 = 1 << $pwr ;
                $this_end = $start + $pwr2-1 ;
                $pwr2mask = -$pwr2 ;            # 0x00010000 -> 0xffff0000
                #
                #
                if ( ( $this_end > $end ) ||
                   ($start != ( $start & $pwr2mask ))) 
                        {
                        &expand_1_agg( $start, 33-$pwr );
                        return( $start + $pwr2/2 );
                        }
                }

        printf "***** PANIC:  Can't calculate this!! *****\n" ;
        exit(1);
        }

#
#  Print a single aggregate value line, like:
#
#                35/8  (  1 net:   35 )
#        or:     36/7  (  2 nets:  36 - 37 )
#
sub expand_1_agg
        {
        local( $ip, $len ) = @_ ;
        local( $n_nets );

        if ( $len >= 32 && ! $HOST_RANGE)
                {
                #
                #  35.1.1.1/32 is the address of one host.
                #  ---------------------------------------
                printf "     %16s  (  1 host:  %s )\n",
                        &printagg( $ip, $len ),
                        &printip( $ip, 32 );
                }
        elsif ( ( $len > $std_masklen ) || $HOST_RANGE )
                {
                #
                #   35.1.4/23  ( hosts:  35.1.4.0 - 35.1.5.255 )
                #   --------------------------------------------
                printf "     %16s  (%3d hosts: %s - %s )\n",
                        &printagg( $ip, $len ),
                        1 << (32 - $len),
                        &printip( $ip, 32 ),
                        &printip( $ip + ( 1 << 32-$len )-1 , 32 );
                }
        elsif ( $len == $std_masklen )
                {
                #
                #       35/8  (  1 net:   35 )
                #       ----------------------
                printf "     %16s  (  1 net:   %s )\n",
                        &printagg( $ip, $len ),
                        &printip( $ip, $std_masklen );
                }
        else
                {
                #
                #       36/6  (  4 nets:  36 - 39 )
                #       ---------------------------
                $n_nets = 2 ** ( $std_masklen - $len );
                printf "     %16s  (%3d nets:  %s - %s )\n",
                        &printagg( $ip, $len ),
                        $n_nets,
                        &printip( $ip, $std_masklen ), 
                        &printip( $ip + 
                                ($one_net * ($n_nets-1)),
                                $std_masklen );
                }
        }

sub class_nets
        {
        local( $ip, $len, $nbr ) = @_;
        local( $nets_str, $class );

        $nets_str = ( $nbr > 1 ) ? "nets" : "net" ;

        if ( $ip > 0xc0000000 )
                {
                $class = ( $len == 24 ) ? "Class C" : "\"\*/$len\"" ;
                return( sprintf("%d $class $nets_str", $nbr ));
                }
        elsif ( $ip > 0x80000000 )
                {
                $class = ( $len == 16 ) ? "Class B" : "\"\*/$len\"" ;
                return( sprintf("%d $class $nets_str", $nbr ));
                }
        else
                {
                $class = ( $len == 8 ) ? "Class A" : "\"\*/$len\"" ;
                return( sprintf("%d $class $nets_str", $nbr ));
                }
        }

