#!/usr/bin/perl -w

# $Id: IPv4.pm,v 0.92 2001/04/05 04:14:20 sweth Exp sweth $
package Net::Address::IPv4;

my @REVISION = qw($Revision: 0.92 $);
my $VERSION = $REVISION[1];

### set up the environment
use strict; 
use English;
use Carp;

use vars qw ( @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS );

use Exporter;
@ISA = qw ( Exporter );

@EXPORT = qw( 
   IsValidIP IsValidMask
);

$EXPORT_TAGS{'ValidIP'} = [ qw(
   IsValidIP IsValidIPBin IsValidIPHex IsValidIPDottedQuad IsValidIPDec
) ];
$EXPORT_TAGS{'ValidMask'} = [ qw(
   IsValidMask IsValidMaskBin IsValidMaskDec IsValidMaskHex IsValidMaskCIDR 
   IsValidMaskDottedQuad
) ];
$EXPORT_TAGS{'ValidX'} = [
   @{ $EXPORT_TAGS{'ValidIP'} },
   @{ $EXPORT_TAGS{'ValidMask'} }
];

$EXPORT_TAGS{'Bin2X'} = [ qw(
   Bin2Dec Bin2Hex Bin2CIDR Bin2DottedQuad
) ];
$EXPORT_TAGS{'Dec2X'} = [ qw(
   Dec2Bin Dec2Hex Dec2CIDR Dec2DottedQuad 
) ];
$EXPORT_TAGS{'Hex2X'} = [ qw(
   Hex2Bin Hex2Dec Hex2CIDR Hex2DottedQuad
) ];
$EXPORT_TAGS{'CIDR2X'} = [ qw(
   CIDR2Bin CIDR2Dec CIDR2Hex CIDR2DottedQuad 
) ];
$EXPORT_TAGS{'DottedQuad2X'} = [ qw(
   DottedQuad2Bin DottedQuad2Dec DottedQuad2Hex DottedQuad2CIDR
) ];
$EXPORT_TAGS{'X2Y'} = [
   @{ $EXPORT_TAGS{'Bin2X'} },
   @{ $EXPORT_TAGS{'Dec2X'} },
   @{ $EXPORT_TAGS{'Hex2X'} },
   @{ $EXPORT_TAGS{'CIDR2X'} },
   @{ $EXPORT_TAGS{'DottedQuad2X'} }
];

$EXPORT_TAGS{'ALL'} = [
   @{ $EXPORT_TAGS{'ValidX'} },
   @{ $EXPORT_TAGS{'X2Y'} }
];

# I could use Exporter::export_ok_tags, but I 
# didn't know about it when I wrote this.
#@EXPORT_OK = (
#   @{ $EXPORT_TAGS{'ValidX'} },
#   @{ $EXPORT_TAGS{'X2Y'} }
#);
@EXPORT_OK = (
   @{ $EXPORT_TAGS{'ALL'} }
);

use subs qw( 
   @EXPORT_OK
   wcarp wcroak
);

### Debugging... don't do any of this if module is being imported.
#print "EXPORT\n", 
#   map ({ "   " . $_ . "\n" } sort (@EXPORT)),
#   "\n";
#print "EXPORT_OK\n",
#   map ({ "   " . $_ . "\n" } sort (@EXPORT_OK)),
#   "\n";
#print "EXPORT_TAGS\n", 
#   map ({ "   :" . $_ . "\n" . 
#      join ("", map { "      " . $_ . "\n" } sort (@{ $EXPORT_TAGS{$_} }))
#   } sort (keys %EXPORT_TAGS)),
#   "\n";

### here be subroutines

# wcarp and wcroak tests $WARNING: if true, they determines the caller 
# dynamically and carps/croaks the message passed as an argument.  Either 
# way, they also return 0, unless exiting, in which case they exit with 
# status 1.
sub wcarp {
   if ($WARNING) {
      (my $funcname = (caller(1))[3]) =~ s/([^:]*::)*//;
      if (defined (@_)) {
         carp ("$funcname raised non-fatal error '" . join (" ", @_) . "'");
      } else {
         carp ("$funcname raised unspecified non-fatal error");
      };
   };
   return 0;
};
   
sub wcroak {
   if ($WARNING) {
      (my $funcname = (caller(1))[3]) =~ s/([^:]*::)*//;
      $ERRNO = 1;
      if (defined (@_)) {
         croak ("$funcname raised fatal error '" . join (" ", @_) . "'");
      } else {
         croak ("$funcname raised unspecified fatal error");
      };
   } else {
      return 0;
   };
};
   

# The validation subroutines all fail to check to see if $_[0]
# is defined.  This is intentional; since the validation checks are
# positive, a null argument passed to a validation sub will return
# false, which I think is better than invoking croak and stopping all
# processing.  If -w is set, the validation subs _do_ use carp to
# provide a warning; -w will also complain because of use of uninitialized
# vars, but this will help track down why.

# the IsValidIP and IsValidMask subs are just wrappers for the other
# validation subroutines.
sub IsValidIP {
   if (not defined ($_[0])) {
      wcarp "No argument provided"
   } else {
      my ($incoming_ip, $incoming_format);
      chomp ($incoming_ip = $_[0]);
      if (defined ($_[1])) {
         chomp ($incoming_format = $_[1]);
      } else {
         $incoming_format = "dq";
      };
   
      if ($incoming_format =~ /^bin$/i) {
         IsValidIPBin ($incoming_ip);
      } elsif ($incoming_format =~ /^dec$/i) {
         IsValidIPDec ($incoming_ip);
      } elsif ($incoming_format =~ /^hex$/i) {
         IsValidIPHex ($incoming_ip);
      } elsif ($incoming_format =~ /^dq$/i) {
         IsValidIPDottedQuad ($incoming_ip);
      } else {
         wcroak "Invalid incoming format \"$incoming_format\"";
      };
   };
};
  
sub IsValidMask {
   if (not defined ($_[0])) {
      wcarp "No argument provided"
   } else {
      my ($incoming_mask, $incoming_format);
      chomp ($incoming_mask = $_[0]);
      if (defined ($_[1])) {
         chomp ($incoming_format = $_[1]);
      } else {
         $incoming_format = "dq";
      };
   
      if ($incoming_format =~ /^bin$/i) {
         IsValidMaskBin ($incoming_mask);
      } elsif ($incoming_format =~ /dec/i) {
         IsValidMaskDec ($incoming_mask);
      } elsif ($incoming_format =~ /hex/i) {
         IsValidMaskHex ($incoming_mask);
      } elsif ($incoming_format =~ /cidr/i) {
         IsValidMaskCIDR ($incoming_mask);
      } elsif ($incoming_format =~ /dq/i) {
         IsValidMaskDottedQuad ($incoming_mask);
      } else {
         wcroak "Invalid incoming format \"$incoming_format\"";
      };
   };
};
  

# The real validation subs.  I use regexes for validation rather than
# numeric comparisons, because some of the formats (e.g. dotted quad)
# are not straight numeric values, and because the regex also takes care of
# catching non-numeric values.  
sub IsValidIPDottedQuad {
   if (not defined ($_[0])) {
      wcarp "No argument provided"
   } else {
      my ($incoming_ip);
      chomp ($incoming_ip = $_[0]);
      # this regex matches any number from 000 to 255, with or without
      # leading 0s for numbers under 100.
      my $dq_regex = "([01]?[0-9][0-9]?|2([0-4][0-9]|5[0-5]))";
      $incoming_ip =~ m/^($dq_regex\.){3}$dq_regex$/;
   };
};

sub IsValidIPBin {
   if (not defined ($_[0])) {
      wcarp "No argument provided"
   } else {
      my ($incoming_ip);
      chomp ($incoming_ip = $_[0]);
      $incoming_ip =~ m/^[01]{1,32}$/;
   };
};

sub IsValidIPHex {
   if (not defined ($_[0])) {
      wcarp "No argument provided"
   } else {
      my ($incoming_ip);
      chomp ($incoming_ip = $_[0]);
      $incoming_ip =~ m/^[0-9A-Fa-f]{1,8}$/;
   };
};

sub IsValidIPDec {
   if (not defined ($_[0])) {
      wcarp "No argument provided"
   } else {
      my ($incoming_ip);
      chomp ($incoming_ip = $_[0]);
      # This returns true for any number from 0 to 4294967296 (2**32), with or
      # without leading 0s for numbers under 1000000000.  Doing it as a pure
      # regex was cumbersome and inefficient, and I'm lazy.
      $incoming_ip =~ m/^[0-9]{1,10}$/ and $incoming_ip < 4294967296;
   };
};

# the nice things about masks is that their true bits must be contiguous,
# so all we have to do to validate them is make sure that they would be valid
# ips, and then convert them to binary and check to see if the resulting
# string matches that description.  (Thanks to EMF, I now know that masks
# don't need to be contiguous, but I'm not going to rewrite these, since he
# and I are the only two people I know of who have ever used non-contiguous
# masks for anything other than intellectual exercises.)
sub IsValidMaskCIDR {
   if (not defined ($_[0])) {
      wcarp "No argument provided"
   } else {
      my ($incoming_mask);
      chomp ($incoming_mask = $_[0]);
      # This regex allows optional leading zeroes for numbers under 10.  I
      # use this rather than the convert-to-binary + check-for-contiguous-1s
      # test because cidr is just so easy to validate directly.
      $incoming_mask =~ m/^([0-2]?[0-9]|3[0-2])$/;
   };
};

sub IsValidMaskBin {
   if (not defined ($_[0])) {
      wcarp "No argument provided"
   } else {
      my ($incoming_mask);
      chomp ($incoming_mask = $_[0]);
      if (IsValidIP ($incoming_mask, 'bin')) {
         $incoming_mask =~ m/^1*0*$/;
      } else {
         return 0;
      };
   };
};

sub IsValidMaskDottedQuad {
   if (not defined ($_[0])) {
      wcarp "No argument provided"
   } else {
      my ($incoming_mask);
      chomp ($incoming_mask = $_[0]);
      if (IsValidIP ($incoming_mask, 'dq')) {
         (DottedQuad2Bin ($incoming_mask)) =~ m/^1*0*$/
      } else {
         return 0;
      };
   };
};

sub IsValidMaskHex {
   if (not defined ($_[0])) {
      wcarp "No argument provided"
   } else {
      my ($incoming_mask);
      chomp ($incoming_mask = $_[0]);
      if (IsValidIP ($incoming_mask, 'hex')) {
         (Hex2Bin ($incoming_mask)) =~ m/^1*0*$/
      } else {
         return 0;
      };
   };
};

sub IsValidMaskDec {
   if (not defined ($_[0])) {
      wcarp "No argument provided"
   } else {
      my ($incoming_mask);
      chomp ($incoming_mask = $_[0]);
      if (IsValidIP ($incoming_mask, 'dec')) {
         (Dec2Bin (incoming_mask)) =~ m/^1*0*$/;
      } else {
         return 0;
      };
   };
};

# the conversion subroutines.  most use pack and unpack to convert to
# and from various formats; the dotted quad ones also use split and join
# to break the numbers into dotted quads (obviously).
sub DottedQuad2Dec {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidIP ($incoming_value, 'dq')) {
         return unpack ('N', pack ('C4', split (/\./, $incoming_value)));
      } else {
         wcroak "Invalid dotted-quad IPv4 value";
      };
   };
};

sub Dec2DottedQuad {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidIP ($incoming_value, 'dec')) {
         return join ('.', unpack ('C4', pack ('N', $incoming_value)));
      } else {
         wcroak "Invalid decimal IPv4 value";
      };
   };
};

sub DottedQuad2Bin {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidIP ($incoming_value, 'dq')) {
         return unpack ('B32', pack ('C4', split (/\./, $incoming_value)));
      } else {
         wcroak "Invalid dotted-quad IPv4 value";
      };
   };
};

sub Bin2DottedQuad {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidIP ($incoming_value, 'bin')) {
         return join ('.', unpack ('C4',
            pack ('B32', substr ("0" x 32 . $incoming_value, -32))));
      } else {
         wcroak "Invalid binary IPv4 value";
      };
   };
};

sub DottedQuad2Hex {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidIP ($incoming_value, 'dq')) {
         return unpack ('H8', pack ('C4', split (/\./, $incoming_value)));
      } else {
         wcroak "Invalid dotted-quad IPv4 value";
      };
   };
};

sub Hex2DottedQuad {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidIP ($incoming_value, 'hex')) {
         return join ('.', unpack ('C4',
            pack ('H8', substr ("0" x 8 . $incoming_value, -8))));
      } else {
         wcroak "Invalid hexadecimal IPv4 value";
      };
   };
};

sub Hex2Bin {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidIP ($incoming_value, 'hex')) {
         return unpack ('B32',
            pack ('H8', substr ("0" x 8 . $incoming_value, -8)));
      } else {
         wcroak "Invalid hexadecimal IPv4 value";
      };
   };
};

sub Bin2Hex {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidIP ($incoming_value, 'bin')) {
         return unpack ('H8',
            pack ('B32', substr ("0" x 32 . $incoming_value, -32)));
      } else {
         wcroak "Invalid binary IPv4 value";
      };
   };
};

sub Dec2Bin {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidIP ($incoming_value, 'dec')) {
         return unpack ('B32', pack ('N', $incoming_value));
      } else {
         wcroak "Invalid decimal IPv4 value";
      };
   };
};

sub Bin2Dec {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidIP ($incoming_value, 'bin')) {
         return unpack ('N', 
            pack ('B32', substr ("0" x 32 . $incoming_value, -32)));
      } else {
         wcroak "Invalid binary IPv4 value";
      };
   };
};

sub Dec2Hex {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidIP ($incoming_value, 'dec')) {
         return unpack ('H8', pack ('N', $incoming_value));
      } else {
         wcroak "Invalid decimal IPv4 value";
      };
   };
};

sub Hex2Dec {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidIP ($incoming_value, 'hex')) {
         return unpack ('N', 
            pack ('H8', substr ("0" x 8 . $incoming_value, -8)));
      } else {
         wcroak "Invalid hexadecimal IPv4 value";
      };
   };
};

sub CIDR2DottedQuad {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidMask ($incoming_value, 'cidr')) {
         return Bin2DottedQuad (2 ** $incoming_value);
      } else {
         wcroak "Invalid CIDR IPv4 mask value";
      };
   };
};

sub DottedQuad2CIDR {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidMask ($incoming_value, 'dq')) {
         # since we know this is a valid mask, we can just count the 1s,
         # by getting the return value of a tr with no substitution.  The
         # temp value is so that tr is working on an assignment to a variable
         # rather than on the subroutine itself.
         return ((my $temp = DottedQuad2Bin ($incoming_value)) =~ tr/1//);
      } else {
         wcroak "Invalid dotted-quad IPv4 mask value";
      };
   };
};

sub CIDR2Bin {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidMask ($incoming_value, 'cidr')) {
         return (2 ** $incoming_value);
      } else {
         wcroak "Invalid CIDR IPv4 mask value";
      };
   };
};

sub Bin2CIDR {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidMask ($incoming_value, 'bin')) {
         return ($incoming_value =~ tr/1//);
      } else {
         wcroak "Invalid binary IPv4 mask value";
      };
   };
};

sub CIDR2Hex {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidMask ($incoming_value, 'cidr')) {
         return Bin2Hex (2 ** $incoming_value);
      } else {
         wcroak "Invalid CIDR IPv4 mask value";
      };
   };
};

sub Hex2CIDR {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidMask ($incoming_value, 'hex')) {
         # since we know this is a valid mask, we can just count the 1s,
         # by getting the return value of a tr with no substitution.  The
         # temp value is so that tr is working on an assignment to a variable
         # rather than on the subroutine itself.
         return ((my $temp = Hex2Bin ($incoming_value)) =~ tr/1//);
      } else {
         wcroak "Invalid hexadecimal IPv4 mask value";
      };
   };
};

sub CIDR2Dec {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidMask ($incoming_value, 'cidr')) {
         return Bin2Dec (2 ** $incoming_value);
      } else {
         wcroak "Invalid CIDR IPv4 mask value";
      };
   };
};

sub Dec2CIDR {
   if (not defined ($_[0])) {
      wcarp "No argument provided";
   } else {
      my ($incoming_value);
      chomp ($incoming_value = $_[0]);
      if (IsValidMask ($incoming_value, 'dec')) {
         # since we know this is a valid mask, we can just count the 1s,
         # by getting the return value of a tr with no substitution.  The
         # temp value is so that tr is working on an assignment to a variable
         # rather than on the subroutine itself.
         return ((my $temp = Dec2Bin ($incoming_value)) =~ tr/1//);
      } else {
         wcroak "Invalid decimal IPv4 mask value";
      };
   };
};

# the fun subroutines.

# EnumerateNetAddrs (Network/Mask, [NetFormat/MaskFormat]) will return
# a list of all valid IP addresses in the specified netblock.
# EnumerateNetNodes does the same after stripping off the network and
# broadcast addresses.
sub EnumerateNetAddrs {
   my ($net_format, $net_value, $mask_format, $mask_value, @net_addrs);
   if (defined ($_[1])) {
      ($net_format, $mask_format) = split (/\//, $_[1])
   } else {
      ($net_format, $mask_format) = ('dq', 'cidr');
   };
   if (defined ($_[0])) {
      ($net_value, $mask_value) = split (/\//, $_[0])
   } else {
      (my $funcname = (caller(0))[3]) =~ s/([^:]*::)*//;
      croak "No network specified for ${funcname} to enumerate!" ;
   };
   if (IsValidIP ($net_value, $net_format)) {
      if (IsValidMask ($mask_value, $mask_format)) {
      
      } else {
         return;
      };
   } else {
      return;
   };
};

sub EnumerateNetNodes {
};

# IsValidSubnetIP will check to see if a given IP address is a valid member
# of a given subnet.  IsValidSubnetNode will do the same, but will also
# return false if the IP address would conflict with the network or
# broadcast addresses for the subnet.  I'm not implementing these yet, as
# I think I need to use a more object-oriented approach given the number of
# parameters that these routines should allow.  I should then go back and do
# the same for the EnumerateNet* routines.

# SummarizeNetAddrs will turn an array of addresses into a set of
# network numbers with netmasks.  By default, it will create the "most 
# concise" description of that block of addresses; I may give it some 
# options, though.
sub SummarizeNetAddrs {                                     
};
