#!/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 || 
		     (defined($bytes[$i]) && $bytes[$i] !~ /\d+/) )
			{
			printf "Illegal byte value %s\n", $bytes[$i];
			exit;
			}
	#	$ip = ($ip << 8) + $bytes[$i] ;
		}
	$ip = ($bytes[0] * 16777216) + ($bytes[1] * 65536) +
					  ($bytes[2] * 256) + $bytes[3];


	#
	#  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) & 0x000000ff ;
	$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 ));
		}
	}
