#!/usr/bin/perl
#
#    $Id: h2n,v 8.3 2001/05/03 07:26:44 vixie Exp $ (Michael Milligan)
#    Extended to v 2.38 2000-12-26 09:37:14 by Andris Kalnozols, HP Labs
#
# NAME
#
#    h2n - Translate host table to name server file format
#
# SYNOPSIS
#
#    h2n -d DOMAIN -n NET -u CONTACT [options]
#

use FileHandle;		# for calling 'autoflush'
use Symbol;		# for calling 'qualify'
use Sys::Hostname;	# for calling 'hostname'
use Text::Tabs;		# for calling 'expand'

# Various defaults
#
my $Version = "\$Revision: 8.3 $pad";
   $Version =~ s/^\D*([.\d]+).*$/$1/;
my $Host = `hostname`;
   chop($Host);
my $doaliases = 1;
my $domx = 1;
my $dowks = 0;
my $dotxt = 0;
my $dorp = 0;
my $Bootfile = "./named.boot";
my $Conffile = "./named.conf";
my $Hostfile = "/etc/hosts";
my $Commentfile = "";
my $Pwd = `pwd`;
   chop($Pwd);
my $Bwd = "";
my $RespHost = "";
my $RespUser = "";
my $DefSerial = 1;
my $DefRefresh = "3h";
my $DefRetry = "1h";
my $DefExpire = "1w";
my $DefNegCache = "10m";
my $DefTtl = "1d";
my $rfc2308 = 0;
my $BIND_version = "\?";
my $DiG_version = "\?";
my $need_numeric_ttl = 0;
my $DefMxWeight = 10;
my $Defsubnetmask = "255.255.255.0";
my $ForceSerial = -1;
my $UseDateInSerial = 0;
my $DateSerial = 0;
my $lastNorD = "";
my $Printcount = 0;
my $Openfiles = 0;
my $Filelimit = 2048;
my $verbose = 1;
my $linenum = 0;
my $printed_linenum = 0;
my $gen_count = 0;
my $CustomLogging = 0;
my $CustomOptions = 0;
my $NeedHints = 1;
my $multi_homed_mode = "";
my $rfc1123 = "warn";	# NOTE: Set to quoted "0" to disable default checking.
my $audit = 1;		# DNS auditing is independent of RFC name checking.
my $action = ($rfc1123 =~ /warn/) ? "Warning" : "Skipping";
my $verify_mode = 0;
my $recursive_verify = 0;
my $verify_delegations = 1;
my $show_chained_cnames = 0;
my $debug = 0;
my $query_rpt_threshold = 50;
my $valid_SOA_timers = 1;
my $localhost = "127.0.0.1";
my $MakeLoopbackSOA = 1;
my $nameset = "";

# The following two arrays are used in verify mode to create a sorted list
# of nameservers from which a zone transfer is requested.  This helps to
# avoid unnecessary delays by trying nameservers on local networks first.
#
# * The "@local_networks" array should be initialized to the list of
#   network(s) (in CIDR format) to which the localhost is connected,
#   e.g., a dual-homed localhost should have two entries in this array.
# * The "@local_subnetmasks" array should be initialized to the subnet
#   mask(s) which correspond to the network(s) in "@local_networks".
#
my @local_networks = ("15/8");
my @local_subnetmasks = ("255.255.248.0");

# To increase the processing speed of the READ_RRs subroutine, arrange the
# set of recognized RR types in order of decreasing probability, i.e.,
# the most common types of RRs should appear first.  There can be up to
# a 7% speed difference between best-case and worst-case ordering.
#
my $RRtypes  = "MX|A|CNAME|PTR|HINFO|RP|SRV|TXT|WKS|NS|SOA|AFSDB|ISDN|MINFO|";
   $RRtypes .= "M[BDFGR]|PX|RT|X25|AAAA|CERT|GPOS|LOC|NAPTR|NSAP|NSAP-PTR|";
   $RRtypes .= "NXT|SIG|KEY|KX|A6|NULL|EID|NIMLOC|ATMA|DNAME";

my ($Bootsecaddr, $Bootsecsaveaddr, $Confsecaddr, $Confsecsaveaddr);
my ($DiG_buffer_size, $Domain, $Domainfile, $Domainpattern);
my ($Expire, $MasterTtl, $NS, $Refresh, $Retry, $SOA_count, $Serial);
my ($Specialfile, $Ttl, $UseDefaultDomain, $User, $additional, $addr);
my ($addrpattern, $alias, $answer, $answer_section, $attempt);
my ($authority, $bootdb, $bootdom, $bootzone, $canonical, $cmode);
my ($comment, $common_alias, $data, $default_PTR, $domain, $error);
my ($file, $flags, $fname, $found_spclRP, $fqdn, $i);
my ($ip, $key, $line, $make_rr, $match, $message, $n, $names, $netbits);
my ($netfile, $netmask, $netpat, $network, $newline_printed, $ns);
my ($origin, $our_host, $owner, $pDomain, $prog_output, $ptrpat);
my ($query_options, $query_pattern, $revaddr, $separator);
my ($soa_warned, $status, $strip, $tmp, $tmp3, $ttl, $version_buffer);
my ($warning_status, $zone_data);
my (%Aliases, %AliasesPTR, %CommentRRs, %Comments, %Fname, %Hosts, %HostsPTR);
my (%LRUcount, %MXdomains, %MasterZoneOptions, %NSdata, %NSdomains, %NSlist);
my (%Netfiles, %PartialServers, %RRowners, %SlaveZoneOptions, %Ttl);
my (%Wildcards, %cAliases, %cModeSpec, %cpatrel, %deferredPTR, %extNS);
my (%pModeSpec, %pPTR, %pendingPTR, %ptrpatrel, %spclCNAME, %spclPTR);
my (%spclRP, %subzones);
my (@BootOptions, @ConfLogging, @ConfOptions, @DNSrrset);
my (@FullServers, @GlobalMasterZoneOptions, @GlobalSlaveZoneOptions, @Mx);
my (@NSlocal, @NSnet, @NSother, @NSsubnet, @Nameservers);
my (@Netpatterns, @Networks, @Vdomains, @addrs, @aliases, @bootmsgs, @cpats);
my (@elimpats, @makesoa, @our_addrs, @our_netbits, @our_nets, @our_subnetmasks);
my (@our_subnets, @ptrpats);

&PARSEARGS(@ARGV);
&FIXUP;

if ($DiG_version == 900 && ($verify_mode || $audit)) {
    ($message = <<EOT) =~ s/^\s+\|//gm;
    |
    |The DiG utility found by 'h2n' is version 9.0 and is unsupported
    |due to its inability to echo batch file input.  Please install and/or
    |place another version of DiG into this host's search path.
EOT
    if (!$verify_mode) {
	$message .= "The auditing option has been disabled for this run.\n";
	$audit = 0;
    }
    print STDERR "$message\n";
    exit(2) if $verify_mode;
}
# NOTE: All versions of DiG 9.0.1, while usable by 'h2n', have a small
#       defect whereby the command line is not echoed whenever any kind of
#       nameserver connection failure is encountered.  The AUDIT_RRs subroutine
#       treats these detected cases as a general synchronization failure
#       when parsing the output from DiG and will generate a result of
#       "[ DiG error! ]" for the queried domain name.
#

if (!$rfc2308 || $BIND_version !~ /^\d+$/ || $BIND_version < 821) {
    #
    # When in doubt, convert time intervals that may be in symbolic
    # notation to the equivalent number of seconds.  This is the
    # universally understood format.
    #
    $need_numeric_ttl = 1;
    $DefRefresh = &SECONDS($DefRefresh);
    $DefRetry   = &SECONDS($DefRetry);
    $DefExpire  = &SECONDS($DefExpire);
    $DefTtl     = &SECONDS($DefTtl);
    $Refresh    = &SECONDS($Refresh) if $Refresh;
    $Retry      = &SECONDS($Retry) if $Retry;
    $Expire     = &SECONDS($Expire) if $Expire;
    $Ttl        = &SECONDS($Ttl) if $Ttl;
    $MasterTtl  = &SECONDS($MasterTtl) if $MasterTtl;
}

if ($verify_mode) {
    #
    # Verify the zone data for each domain in the @Vdomains array.
    #
    autoflush STDOUT 1;		# Allows single-character output w/o newline.
    autoflush STDERR 1;		# Keeps STDERR output synchronized with STDOUT.

    # It only makes sense to do all this work if the "$verbose" and "$audit"
    # flags are enabled.  However, the user is free to set the level of
    # name checking by choosing the appropriate -I option which will then
    # set the "$rfc1123" flag accordingly.  "Strict" name-checking will be
    # disabled, however, since the RFC-952 check for single-character
    # hostnames and/or aliases is only valid when processing a host table.
    #
    $verbose = 1;
    $audit = 1;
    $rfc1123 = "warn" if $rfc1123 =~ /strict/;

    # In order to verify a domain, a zone transfer must be obtained from
    # one of the domain's listed nameservers.  This program will use DiG
    # to get every IP address of every listed nameserver so that they can
    # all be tried in a zone transfer request before having to give up.
    # Some IP addresses, however, may belong to inaccessible interfaces
    # of multi-homed bastion hosts.  Requesting a zone transfer from such
    # IP addresses will cause cause this program to hang until the connection
    # request times out.  We'll try to avoid these delays by sorting the
    # IP addresses using the information in the pre-initialized arrays
    # "@local_networks" and "@local_subnetmasks".
    #
    $our_host = hostname();
    ($tmp, $tmp, $tmp, $tmp, @our_addrs) = gethostbyname($our_host);

    # The "@our_addrs" array returned by the 'gethostbyname' function
    # contains the IP address(es) of the local host.  Each IP address
    # is a binary structure consisting of four unsigned character values.
    # We'll use the 'pack' and 'unpack' function with a template of 'C4'
    # to do the necessary data manipulations.
    #
    @our_nets = @our_netbits = @our_subnetmasks = @our_subnets = ();
    for ($i = 0; $i < @local_networks; $i++) {
	#
	# Create the corresponding network-related packed data structures.
	#
	($network, $netbits) = split('/', $local_networks[$i]);
	$network .= ".0.0.0";			# ensure a 4-octet format
	$our_nets[$i] = pack('C4', split('\.', $network));
	$netmask = ((2 ** $netbits) - 1) << (32 - $netbits);

	# Convert the above 32-bit "$netmask" variable to a packed 'C4'
	# data structure so that it will be compatible for bitwise AND
	# operations with the other IP-based data structures.
	#
	$our_netbits[$i] = pack('C4', ($netmask & 0xff000000) >> 24,
				      ($netmask & 0x00ff0000) >> 16,
				      ($netmask & 0x0000ff00) >> 8,
				       $netmask & 0x000000ff);

	# Each network specified in "@local_networks" should have a
	# corresponding subnet mask in "@local_subnetmasks".  Extra
	# entries will be ignored while missing subnet masks will
	# default to the value of the pre-initialized "$Defsubnetmask"
	# variable.
	#
	$netmask = $local_subnetmasks[$i];
	$netmask = $Defsubnetmask if !$netmask;
	$our_subnetmasks[$i] = pack('C4', split('\.', $netmask));
    }
    foreach $addr (@our_addrs) {
	#
	# Create an array of subnets to which the local host is connected.
	#
	for ($i = 0; $i < @our_netbits; $i++) {
	    $network = $addr & $our_netbits[$i];
	    if ($network eq $our_nets[$i]) {
		$our_subnets[$i] = $addr & $our_subnetmasks[$i];
		last;
	    }
	}
    }
    $query_options = "+noques +noauthor +nostats";
    $query_pattern = $query_options;
    $query_pattern =~ s/[+]/\\+/g;
    $separator = "";
    while ($domain = pop(@Vdomains)) {
	print STDOUT "$separator";
	print STDOUT "\nVerifying zone data for domain '$domain'.\n";
	print STDOUT "Getting NS RRset...\n";
	#
	# Occasionally, a query is made for a domain's NS RRs and the
	# Additional Section of the response is incomplete.  One or more
	# subsequent queries, however, do return the IP address(es) that
	# that correspond with the NS RR(s) in the Answer Section.
	# The most likely scenario is an NS RRset where one or more NS RRs
	# point to nameservers that are in a different Top-Level Domain
	# (TLD) than the zone itself.  Since the Internet root nameservers
	# do not perform recursion and do not fetch glue, Address RRs that
	# are not on the same TLD server as the NS RRs will not appear in
	# the Additional Section of the response.  Once the local nameserver
	# has the NS RRset cached, successive recursive queries will cause
	# the desired Address RR(s) to appear in the Additional Section (but
	# *only* if the option 'fetch-glue' is set to 'yes' [the default]).
	# In anticipation of this possibility, we'll make up to five attempts
	# to get the Additional Section information that we expect to find.
	#
	@NSlocal = @NSsubnet = @NSnet = @NSother = ();
	$match = $error = 0;
	$attempt = 1;
	until ($match || $error || $attempt > 5) {
	    sleep 2 if $attempt > 1;
	    $prog_output = -1;
	    open(DIGOUT, "dig $query_options $domain NS 2>&1 |");
	    while (<DIGOUT>) {		# Almost always true at least once.
		$prog_output++;		# If > 0, then 'dig' was able to be run.
		next if /^$/;
		chop;
		#
		# Whenever 'h2n' calls the DiG utility, the pattern-matching
		# statements for processing the output are structured to be
		# compatible with the different formats generated by versions
		# 2.X, 8.X, and 9.X of DiG.
		#
		if (/^;.+$query_pattern/o) {
		    $status = "";
		    $answer = $answer_section = 0;
		    @DNSrrset = ();
		} elsif (/^;.+(connection |no route|unreachable)/i) {
		    s/[^:]+:/ /;
		    $message = "DiG reported the following error:\n$_";
		    $error = 1;
		} elsif (/^;.+HEADER.+opcode: QUERY, status: ([^,]+)/) {
		    $status = $1;
		    if ($status ne 'NOERROR') {
			$message = "DiG reported the following status: $status";
			$error = 1;
			$status = "";
		    }
		} elsif ($status && /^;.+flags: (.*); QUE.*, ANS[^:]*: (\d+), AUTH[^:]*: (\d+), ADDIT[^:]*: (\d+)/i) {
		    $flags = $1;
		    $answer = $2;
		    $authority = $3;
		    $additional = $4;
		    if ($answer == 0) {
			if ($flags !~ /aa/ && $authority == 0 && $additional == 0) {
			    #
			    # We've probably queried a nameserver that is
			    # lame due to either bad delegation or it has
			    # invalidated a zone due to bad data (although
			    # SERVFAIL would be the expected response for
			    # the latter case).
			    #
			    $message = "DiG reported a failed query.  Perhaps a lame delegation was encountered.";
			} else {
			    #
			    # We've encountered an undelegated subdomain.
			    #
			    $message = "DiG reported that no NS records exist.";
			}
			$error = 1;
			$status = "";
		    } elsif (($additional < $answer) && ($attempt < 5)) {
			#
			# Quit parsing this query and initiate another
			# recursive one in an attempt to get at least
			# as many nameserver IP addresses in the
			# Additional section as there are nameservers
			# in the Answer section.
			#
			last;
		    } else {
			#
			# We either have at least as many IP addresses as
			# there are nameservers in the Answer section or
			# we are on our last attempted query.  Continue
			# parsing the current response.
			#
			next;
		    }
		}
		next if !$status;
		if ($answer > 0) {
		    if (/^;; ANSWER/) {
			$answer_section = 1;
			next;
		    } elsif (/^;; ADDITIONAL/) {
			$answer_section = 0;
			next;
		    } elsif ($answer_section) {
			#
			# Store the answers so that the NS RRset of the
			# response can be compared to the NS RRset that
			# is actually contained in the domain's zone data.
			#
			s/.+\s+//;
			push(@DNSrrset, lc($_));
			next;
		    }
		    # The Additional Section of the response has been reached.
		    # Assign the IP address to the proper network category.
		    #
		    s/\.\s.*\sA\s+/ /;
		    $tmp = $_;
		    $tmp =~ s/.+ //;
		    $addr = pack('C4', split('\.', $tmp));
		    $match = 0;
		    foreach $tmp (@our_addrs) {
			if ($addr eq $tmp) {
			    #
			    # The localhost is authoritative for the domain.
			    # Naturally, we'll try this IP address first.
			    #
			    push(@NSlocal, $_);
			    $match = 1;
			    last;
			}
		    }
		    next if $match;
		    for ($i = 0; $i < @our_netbits; $i++) {
			if (($addr & $our_netbits[$i]) eq $our_nets[$i]) {
			    #
			    # The IP address is on a local network.
			    # Now see if it's on a local subnet.
			    #
			    if (($addr & $our_subnetmasks[$i]) eq $our_subnets[$i]) {
				push(@NSsubnet, $_);
			    } else {
				push(@NSnet, $_);
			    }
			    $match = 1;
			    last;
			}
		    }
		    next if $match;
		    push(@NSother, $_);
		    $match = 1;
		}
	    }
	    close(DIGOUT);
	    if ($prog_output <= 0) {
		print STDERR "Can't find or run the 'DiG' program - unable to continue.\n";
		print STDERR "I give up ... sorry.\n";
		exit(2);
	    }
	    $attempt++;
	}
	if ($error || !$match) {
	    $message = "Failed to obtain any nameserver IP addresses." if !$error;
	    $answer = 0;
	    $error = 1;
	}
	$zone_data = "/tmp/h2n-zone.data_$domain";
	if ($debug && -s $zone_data) {
	    if ($error) {
		#
		# Even though we'll still proceed with the (re)verification
		# of the zone data left over from a previous run with the
		# '-debug' option, report the error that was encountered
		# when we tried to do a DNS query for the domain's NS RRset.
		# This will explain the subsequent warning message about
		# inconsistent NS RRsets which will surely come later.
		#
		print STDERR "$message\n";
		print STDERR "(proceeding anyway using zone data in '$zone_data')\n";
		$error = 0;
	    } else {
		print STDOUT "(using existing zone data in '$zone_data')\n";
	    }
	    $answer = 1;
	    $BIND_version = "";
	} elsif ($answer) {
	    print STDOUT "Transferring zone..";
	    $version_buffer = "";
	    #
	    # Proactively create a generic error message
	    # just in case something unexpected happens.
	    #
	    $message = "All zone transfer attempts failed.";
	    $answer = 0;
	    @Nameservers = ();
	    push(@Nameservers, @NSlocal) if @NSlocal;
	    push(@Nameservers, @NSsubnet) if @NSsubnet;
	    push(@Nameservers, @NSnet) if @NSnet;
	    push(@Nameservers, @NSother) if @NSother;
	    foreach $NS (@Nameservers) {
		($ns, $ip) = split(' ', $NS);
		print STDOUT ".";
		$version_buffer .= " ";
		$status = 0xffff & system("dig $domain axfr \@$ip > $zone_data 2>&1");
		#
		# If an error occurs, the message will be stored in a
		# variable and the next nameserver will be tried.
		# The loop is exited when a successful zone transfer is
		# made or there are no more nameservers to try.  If the
		# transfer is unsuccessful, the last error message to be
		# stored will be the one that gets displayed.
		#
		if ($status == 0xff00) {
		    $message = "DiG command failed: $!";
		} elsif ($status > 0x80) {
		    $status >>= 8;
		    $message = "DiG command returned non-zero exit status: $status";
		} elsif ($status != 0) {
		    $message = "DiG command exited with ";
		    if ($status &   0x80) {
			$status &= ~0x80;
			$message .= "coredump from ";
		    }
		    $message .= "signal: $status";
		}
		#
		# Regardless of the system() function's exit status,
		# try to examine the last few lines of DiG's output.
		# We'll either get initial confirmation of a successful
		# zone transfer or, possibly, a more detailed message
		# explaining why the attempt(s) failed.
		# The definitive test of a successful zone transfer
		# will be the detection of the trailing SOA record
		# by the READ_RRs subroutine.
		#
		open(ZONE_DATA, "tail -7 $zone_data |");
		while (<ZONE_DATA>) {
		    if (/^;; Received (\d+) answers? \(\d+ records?\)/ ||
			/^;; XFR size:\s+(\d+) names?,\s+\d+ rrs?/i) {
			#
			# These lines are output by pre-9.X and 9.X versions
			# of DiG, respectively, and usually indicate a
			# successful zone transfer.
			#
			$answer = $1;
			if ($answer == 0) {
			    #
			    # Pre-9.X versions of DiG return an answer count
			    # of zero to indicate a disallowed zone transfer.
			    #
			    $message = "Transfer of zone data is disallowed or unavailable.";
			}
			last;
		    } elsif (/^; Transfer failed/) {
			#
			# This line is returned by 9.X versions of DiG when a
			# zone transfer is either disallowed or unavailable.
			#
			$message = "Transfer of zone data is disallowed or unavailable.";
			last;
		    } elsif (/^;; [Cc]onnect(ion.+failed)?: /) {
			#
			# This pattern matches connection failures by all
			# versions of DiG.
			#
			s/[^:]+: //;
			chop;
			$message = ucfirst($_);
			$message .= "." if $message !~ /\.$/;
			last;
		    }
		}
		close(ZONE_DATA);
		last if $answer > 0;
	    }
	    if ($answer > 0) {
		#
		# Report the nameserver from which the zone transfer was
		# obtained and make an inquiry about the version of BIND
		# it is running.
		#
		print STDOUT " (from '$ns' [$ip])\n";
		$BIND_version = &GET_BIND_VERSION($ip);
	    }
	}
	if ($answer == 0) {
	    $n = ($error) ? "" : "\n";
	    print STDERR "${n}$message\n";
	    print STDERR "Unable to verify this domain.\n\n";
	    $separator = "";
	} else {
	    #
	    # Initialize the appropriate global variables that
	    # might be holding data from a previous pass.
	    #
	    if ($domain eq '.') {
		$Domain = "";
	    } else {
		$Domain = $domain;
	    }
	    $origin = "$Domain.";
	    $Domainpattern = ".$Domain";
	    $Domainpattern =~ s/\./\\./g;
	    $SOA_count = 0;
	    $RespHost = $RespUser = "";
	    $Serial = $Refresh = $Retry = $Expire = $Ttl = "";
	    %RRowners = ();
	    %MXdomains = ();
	    %NSdomains = ();
	    %NSdata = ();
	    %NSlist = ();
	    %extNS = ();
	    %subzones = ();
	    %spclCNAME = ();
	    %spclRP = ();
	    %spclPTR = ();
	    %Wildcards = ();
	    $newline_printed = 0;
	    if ($BIND_version) {
		$version_buffer .="(NS BIND version: $BIND_version)";
	    } else {
		$version_buffer = "";
	    }
	    print STDOUT "Parsing zone data...$version_buffer\n";
	    $warning_status = $newline_printed = &READ_RRs($zone_data, $origin, $origin, 0);
	    print STDERR "\n" while $newline_printed--;
	    if ($SOA_count < 2) {
		print STDERR "Incomplete zone transfer detected - suppressing further action.\nUnable to verify this domain.\n\n";
		$warning_status = 1;
	    } else {
		print STDOUT "Performing in-zone and external lookups...\n";
		$warning_status = &VERIFY_ZONE($warning_status);
	    }
	    if ($warning_status) {
		$separator = "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n";
	    } else {
		print STDOUT "Domain verified with no detected improprieties!\n\n";
		$separator = "";
	    }
	}
	unlink($zone_data) if !$debug;
    }
    exit;
}

print STDOUT "Initializing new database files...\n" if $verbose;
&INITDBs;

if (-r $Specialfile) {
    print STDOUT "Reading special file '$Specialfile'...\n" if $verbose;
    #
    # Make sure to disable checking of single-character hostnames and
    # aliases since this RFC-952 restriction is limited to host files.
    #
    $rfc1123 =~ s/strict/STRICT/;

    # The "$newline_printed" variable controls the printing of cosmetic
    # newlines surrounding a block of warning messages that the
    # READ_RRs subroutine may generate.  "$newline_printed" is global
    # in scope to prevent unwanted extra newlines in case READ_RRs is
    # called recursively.  If necessary, the initial newline is output
    # within READ_RRs while the one or two terminating newline characters
    # are printed after the subroutine returns to its original caller.
    #
    $newline_printed = 0;
    $newline_printed = &READ_RRs($Specialfile, "$Domain.", "$Domain.", 0);
    print STDERR "\n" while $newline_printed-- && $verbose;
    $rfc1123 =~ s/STRICT/strict/;

    # If auditing is in effect and RP records were found, set the flag
    # which controls the registration of any TXT RRs that may get
    # generated later on in the script.
    #
    $found_spclRP = keys(%spclRP);
} else {
    $found_spclRP = 0;
}

print STDOUT "Reading host file '$Hostfile'...\n" if $verbose;
&OPEN(*HOSTS, $Hostfile) or die "Couldn't open host file: $!\nCheck your -H option argument.\n";

LINE: while (<HOSTS>) {
    $linenum++;
    next if /^#/ || /^$/;			# Skip comments and empty lines.
    chop;					# Remove the trailing newline.
    ($data, $comment) = split('#', $_, 2);	# Separate comments from the
    $comment = "" if !defined($comment);	# interesting bits.
    ($addr, $names) = split(' ', $data, 2);	# Isolate IP addr. from name(s).
    if ($addr !~ /^(\d+[.]){3}\d+$/ || $names =~ /^\s*$/) {
	if ($verbose) {
	    print STDERR "Line $linenum:  Skipping, incorrectly formatted data.\n";
	    print STDERR "> $_\n";
	}
	next LINE;
    }
    $names = lc($names);			# convert name(s) to lower case 

    # Match -e args
    foreach $netpat (@elimpats) {
	next LINE if /[.\s]$netpat/;
    }

    if ($comment =~ /\[\s*ttl\s*=\s*(\d+|(\d+[wdhms])+)\s*\]/i) {
	$ttl = $1;					# set TTL if explicit
	$ttl = &SECONDS($ttl) if $need_numeric_ttl;	# convert if necessary
    } else {						#	 or
	$ttl = "";					# set to default if not
    }
    ($canonical, @aliases) = split(' ', $names);	# separate out aliases
    if ($rfc1123 && &CHECKNAME($canonical, 'A-table')) {
	if ($verbose) {
	    print STDERR "Line $linenum:  $action - '$canonical' not a valid canonical hostname.\n";
	    print STDERR "> $_\n";
	}
	next LINE if $rfc1123 !~ /warn/;
    }

    # Process -c arguments
    #
    # If a 'spcl' file exists for the '-d' domain, it has already been read.
    # The %RRowners hash now contains the discovered 'spcl' domain names and
    # will be used to prevent the creation of conflicting CNAMEs.
    # Checks are also made to prevent the accidental creation of cross-domain
    # wildcard CNAMEs in case "*" characters are lurking in the host table.
    #
    foreach $netpat (@cpats) {
	if (/\.$netpat/) {
	    next LINE if $addr eq $localhost;
	    $canonical =~ s/\.$netpat//; 
	    $cmode = $cModeSpec{$netpat};
	    if ($cmode !~ /D/) {		# create CNAMEs now, not later
		if (!exists($RRowners{$canonical}) && $canonical !~ /^\*($|\.)/) {
		    &PRINTF(*DOMAIN, "%s%s\tCNAME\t%s.%s.\n", &TAB($canonical, 16), $ttl, $canonical, $cpatrel{$netpat});
		    $RRowners{$canonical} = " CNAME ";
		}
		if ($cmode =~ /A/) {		# also create CNAMEs for alias(es)
		    foreach $alias (@aliases) {
			$alias =~ s/\.$netpat//;
			$make_rr = 1 if $alias !~ /^\*($|\.)/;
			if ($rfc1123 && &CHECKNAME($alias, 'CNAME-table')) {
			    if ($verbose) {
				print STDERR "Line $linenum:  $action - '$alias' not a valid hostname alias.\n";
				if ($linenum != $printed_linenum) {
				    print STDERR "> $_\n";
				    $printed_linenum = $linenum;
				}
			    }
			    $make_rr = 0 if $rfc1123 !~ /warn/;
			}
			if ($make_rr && !exists($RRowners{$alias})) {
			    &PRINTF(*DOMAIN, "%s%s\tCNAME\t%s.%s.\n", &TAB($alias, 16), $ttl, $canonical, $cpatrel{$netpat});
			    $RRowners{$alias} = " CNAME ";
			}
		    }
		}
		    
	    } else {				# Defer creation of CNAMEs
		$cAliases{$canonical} = "$canonical $netpat $ttl" if !exists($RRowners{$canonical}) && $canonical !~ /^\*($|\.)/;
		if ($cmode =~ /A/) {
		    foreach $alias (@aliases) {
			$alias =~ s/\.$netpat//;
			$make_rr = 1 if $alias !~ /^\*($|\.)/;
			if ($rfc1123 && &CHECKNAME($alias, 'CNAME-table')) {
			    if ($verbose) {
				print STDERR "Line $linenum:  $action - '$alias' not a valid hostname alias.\n";
				if ($linenum != $printed_linenum) {
				    print STDERR "> $_\n";
				    $printed_linenum = $linenum;
				}
			    }
			    $make_rr = 0 if $rfc1123 !~ /warn/;
			}
			$cAliases{$alias} = "$canonical $netpat $ttl" if $make_rr && !exists($RRowners{$alias});
		    }
		}
	    }
	    # Check if there is a matching -p arg.
	    $match = 'none';
	    foreach $ptrpat (@ptrpats) {
		$match = $ptrpat, last if /\.$ptrpat/;
	    }
	    if ($match eq 'none') {
		next LINE;
	    } else {
		#
		# Reinitialize '$canonical' and '@aliases' prior to
		# processing the -p argument since these variables might
		# have been modified while processing the -c argument.
		#
		($canonical, @aliases) = split(' ', $names);
	    }
	}
    }

    # Check that the address is in the network list.
    $match = 'none';
    foreach $netpat (@Netpatterns) {
	$match = $netpat, last if $addr =~ /^$netpat\./;
    }
    if ($match eq 'none') {
	if ($verbose) {
	    print STDERR "Line $linenum:  Skipping, IP not within range specified by -n/-a options.\n";
	    print STDERR "> $_\n" if $linenum != $printed_linenum;
	}
	next LINE;
    }

    $file = $Netfiles{$match};			# Get filename from -n options.
    if ($file) {
	$strip = "." . &REVERSE($Fname{$file});	# Construct PTR strip string for
	$strip =~ s/\.db$//;			# printing relative owner names
	$strip =~ s/\./\\./g;			# in reverse-mapping DB file.
    }

    # Process -p args
    #
    # Only the canonical name will be considered when trying to match
    # a domain specified in a -p option.  An allowance will be made
    # for host files that have the following format:
    # 
    #   IP-address  unqualified-name  FQDN  [aliases] [# comments]
    #
    # In this case, a match will occur if the unqualified canonical name
    # plus the -p argument is identical to the fully-qualified domain
    # name that appears as the first item in the '@aliases' list, e.g.,
    #
    #   192.1.2.3    diehard  diehard.movie.edu  # matches '-p movie.edu'
    #
    if (@aliases) {
	$fqdn = $data = $aliases[0];
    } else {
	$fqdn = $data = "";
    }
    foreach $netpat (@ptrpats) {
	$data =~ s/\.$netpat//;
	if ($canonical =~ /\.$netpat/ || $canonical eq $data) {
	    next LINE if !$file || $addr eq $localhost;
	    $revaddr = &REVERSE($addr);
	    $revaddr =~ s/$strip$//;
	    $canonical = $fqdn if $canonical !~ /\.$netpat/;
	    $canonical .= "." if $canonical !~ /\.$/;
	    #
	    # This program has a feature for choosing how PTR records
	    # get generated for multi-homed hosts.  They can either
	    # point to the multi-address canonical name (the default)
	    # or, alternatively, to the first unique single-address
	    # interface name.  The default method allows us to create
	    # the PTR record immediately while the alternate method
	    # requires us to defer the PTR creation since it may not
	    # yet be known whether the current host has more than one
	    # address.
	    #
	    # We'll first determine how to set the $default_PTR flag
	    # based on the relevant conditions and exceptions that
	    # may be present with this host.  Creation or deferral
	    # of the PTR record will then follow.
	    #
	    if ($pModeSpec{$netpat} eq "A" || !@aliases ||
		(@aliases == 1 && $canonical eq "$data.$ptrpatrel{$netpat}.")) {
		#
		# Either the "mode=A" argument was present in the -p option
		# for this domain or there is no other choice but to use the
		# canonical name as the RDATA field of the PTR record.
		# NOTE: The "mode=A" argument overrides the '+m P' and '+m CP'
		#       options as well as the "[mh=p]" and "[mh=cp]" flags in
		#       the comment field of this host.
		#
		$default_PTR = 1;
	    } else {
		#
		# Test the status of any +m option that was specified
		# and whether or not it is being overridden by a "[mh=??]"
		# flag in the comment field.
		#
		if ($multi_homed_mode !~ /P/) {
		    #
		    # Use the default PTR method unless overridden.
		    #
		    $default_PTR = ($comment =~ /\[\s*mh\s*=\s*(p|cp|pc)\s*\]/i) ? 0 : 1;
		} else {
		    #
		    # Use the alternate PTR method unless overridden.
		    # NOTE: The absence of the "p" specification in the
		    #       comment flag signifies an override condition.
		    #
		    $default_PTR = ($comment =~ /\[\s*mh\s*=\s*[cd]\s*\]/i) ? 1 : 0;
		}
	    }
	    if ($default_PTR) {
		#
		# The PTR record must point to the canonical name
		# and thus can be created immediately.
		#
		if (!exists($pPTR{$addr})) {
		    &PRINTF($file, "%s%s\tPTR\t%s\n", &TAB($revaddr, 8), $ttl, $canonical);
		    $pPTR{$addr} = 1;
		}
	    } else {
		#
		# Defer the creation of the PTR record until it can be
		# determined whether or not the canonical name is that
		# of a multi-homed host.  If so, the first non-common
		# alias will have an Address record in the forward-mapping
		# file and the PTR record will created to point to it
		# instead of the canonical name.
		#
		if (!exists($HostsPTR{$canonical})) {
		    $tmp = $netpat;
		    $tmp =~ s/\\././g;
		    $HostsPTR{$canonical} = "$tmp ";
		}
		$addrpattern = $addr;
		$addrpattern =~ s/\./\\./g;
		if ($HostsPTR{$canonical} !~ /\b$addrpattern /) {
		    #
		    # Add the new IP address to the hash of canonical names
		    # for PTR records and store the deferred PTR data.
		    #
		    $HostsPTR{$canonical} .= "$addr ";
		    $deferredPTR{"$canonical-$addr"} = "$file $revaddr $ttl";
		}
		# Make sure that all aliases get indexed.
		#
		$AliasesPTR{"$canonical-$addr"} .= "@aliases ";
	    }
	    next LINE;
	}
    }

    if ($canonical !~ /.$Domainpattern$/ && $fqdn !~ /.$Domainpattern$/ &&
	!$UseDefaultDomain) {
	if ($verbose) {
	    $fqdn = $canonical if length($canonical) > length($fqdn);
	    print STDERR "Line $linenum:  Ignoring '$fqdn'; FQDN not in '$Domain' domain.\n";
	    print STDERR "> $_\n";
	}
	next LINE;
    } else {
	$canonical =~ s/$Domainpattern$//;		# strip off domain if present
	if (exists($Hosts{$canonical})) {
	    $addrpattern = $addr;
	    $addrpattern =~ s/\./\\./g;
	    if ($Hosts{$canonical} !~ /\b$addrpattern /) {
		#
		# The above check prevents the creation of duplicate
		# Address and PTR records.  Now go ahead and index the
		# address by canonical name.
		#
		$Hosts{$canonical} .= "$addr ";
		$addrpattern = "";
	    }
	} else {
	    #
	    # This is the first IP address for this host.
	    #
	    $Hosts{$canonical} = "$addr ";
	    $addrpattern = "";
	}
	$Aliases{"$canonical-$addr"} .= "@aliases ";	# index aliases by name and address
	$Comments{"$canonical-$addr"} .= $comment;
	if ($ttl) {
	    if (exists($Ttl{$canonical})) {
		$Ttl{$canonical} = $ttl if &SECONDS($ttl) < &SECONDS($Ttl{$canonical});
		if ($verbose) {
		    print STDERR "Line $linenum:  Hmm, another TTL spec for '$canonical', using lowest value ($Ttl{$canonical}).\n";
		    print STDERR "> $_\n";
		}
	    } else {
		$Ttl{$canonical} = $ttl;
	    }
	}
	if ($file && $addrpattern eq "" && $addr ne $localhost) {
	    $revaddr = &REVERSE($addr);			# Print PTR records
	    $revaddr =~ s/$strip$//;
	    if (!$doaliases || !@aliases ||
		(@aliases == 1 && "$canonical.$Domain" eq $aliases[0])) {
		#
		# Either the -A option is in effect or there is no other
		# choice but to use the canonical name as the RDATA field
		# of the PTR record.
		# NOTE: The -A option overrides the '+m P' and '+m CP'
		#       options as well as the "[mh=p]" and "[mh=cp]"
		#       flags in the comment field of this host.
		#
		$default_PTR = 1;
	    } else {
		if ($multi_homed_mode !~ /P/) {
		    $default_PTR = ($comment =~ /\[\s*mh\s*=\s*(p|cp|pc)\s*\]/i) ? 0 : 1;
		} else {
		    $default_PTR = ($comment =~ /\[\s*mh\s*=\s*[cd]\s*\]/i) ? 1 : 0;
		}
	    }
	    if ($default_PTR) {
		&PRINTF($file, "%s%s\tPTR\t%s.%s.\n", &TAB($revaddr, 8), $ttl, $canonical, $Domain);
	    } else {
		$deferredPTR{"$canonical-$addr"} = "$file $revaddr $ttl";
	    }
	}
    }
}
&CLOSE(*HOSTS);

if ($Commentfile) {
    print STDOUT "Reading comments file '$Commentfile'...\n" if $verbose;
    &OPEN(*F, $Commentfile) or die "Unable to open comments file': $!\nCheck your -C option argument.\n";
    while (<F>) {
	chop;
	($key, $comment) = split(':', $_, 2);
	$CommentRRs{$key} = $comment;
    }
    &CLOSE(*F);
}
    
print STDOUT "Writing database files...\n" if $verbose;

# Go through the list of canonical names.
# If there is more than one address associated with a name, it is a
# multi-homed host.  Special checks are made for the generation of
# A and/or CNAME RRs for multi-homed hosts in the &CNAME subroutine.
#
# Since the %Hosts hash may be quite large, do not call the 'keys'
# function in a list context.  To do so would incur unnecessary
# overhead in both time and memory space when every hash key is
# slurped into the list at once.  Instead, the 'each' function will
# be used to access the hash keys and elements one by one.
# It's imperative, however, to first call 'keys' in a scalar context
# in order to reset the internal hash iterator.  Otherwise, data might
# be missed if the 'each' function doesn't start accessing the hash
# from the beginning.
# 
keys(%Hosts);
while (($canonical, $data) = each %Hosts) {
    @addrs = split(' ', $data);

    $ttl = (defined($Ttl{$canonical})) ? $Ttl{$canonical} : "";
    foreach $addr (@addrs) {
	#
	# Print address record(s) for the canonical name.
	#
	if (exists($RRowners{$canonical}) && $RRowners{$canonical} =~ / CNAME /) {
	    print STDERR "Can't create A record for '$canonical' because CNAME exists for name.\n" if $verbose;
	} elsif ($addr ne $localhost) {
	    &PRINTF(*DOMAIN, "%s%s\tA\t%s\n", ($canonical eq $nameset ? "\t\t" : &TAB($canonical, 16)), $ttl, $addr);
	    $nameset = $canonical;
	}
    }
    if ($domx) {
	&MX($canonical, @addrs);
    }
    if ($dotxt) {
	&TXT($canonical, @addrs);
    }
    if ($Commentfile) {
	&DO_COMMENTS($canonical, @addrs);
    }
    if ($dorp) {
	&RP($canonical, @addrs);
    }
    if ($doaliases) {
	&CNAME($canonical, @addrs);
    }
    if (exists($cAliases{$canonical})) {
	#
	# RRs for the default domain take precedence over identically-named
	# CNAMEs requested by a -c option with "mode=D".
	# Prevent the generation of an illegal duplicate DNS record by
	# removing it from the deferred list.
	#
	($tmp, $netpat) = split(' ', $cAliases{$canonical});
	$cmode = $cModeSpec{$netpat};
	print STDERR "Can't create CNAME for '$canonical.$cpatrel{$netpat}'; another RR exists for name.\n" if $verbose && $cmode !~ /Q/;
	delete($cAliases{$canonical});
    }
}

if (keys(%cAliases)) {
    #
    # The deferred set of non-conflicting CNAMES can finally be created.
    #
    while (($alias, $data) = each %cAliases) {
	if (!exists($RRowners{$alias})) {
	    ($canonical, $netpat, $ttl) = split(' ', $data);
	    &PRINTF(*DOMAIN, "%s%s\tCNAME\t%s.%s.\n", &TAB($alias, 16), $ttl, $canonical, $cpatrel{$netpat});
	    $RRowners{$alias} = " CNAME ";
	}
    }
}
if (keys(%deferredPTR) || keys(%pendingPTR)) {
    #
    # Make sure that deferred PTR records are output.
    #
    if (keys(%HostsPTR)) {
	#
	# Look for domain names that matched a -p option which also
	# point to multi-homed hosts.  The methodology is basically
	# the same as in the CNAME subroutine.
	#
	while (($canonical, $data) = each %HostsPTR) {
	    ($pDomain, @addrs) = split(' ', $data);
	    if (@addrs > 1) {
		#
		# Found a multi-homed host.
		#
		foreach $addr (@addrs) {
		    @aliases = split(' ', $AliasesPTR{"$canonical-$addr"});
		    foreach $alias (@aliases) {
			#
			# For every IP address, check each alias for the
			# following:
			#
			#   1. Skip aliases that are identical to the FQDN.
			#   2. Skip aliases that are common to all addresses
			#      since they have CNAMEs assigned to them.
			#   3. Make the necessary fix-ups so that the PTR record
			#      will point to the first non-common alias since it
			#      is this alias that will have an Address RR in the
			#      forward-mapping data file.
			#   4. Do nothing if we run out of aliases.  The default
			#      PTR record which points to the canonical name
			#      will be generated in the subsequent block.
			#
			next if $canonical eq "$alias.$pDomain." || $canonical eq "$alias.";
			$common_alias = 1;
			foreach $tmp (@addrs) {
			    unless ($AliasesPTR{"$canonical-$tmp"} =~ /(^|\s)$alias\s/) {
				$common_alias = 0;
				last;
			    }
			    last unless $common_alias;
			}
			if ($common_alias) {
			    #
			    # Remove the alias from this as well as the other
			    # addresses of this host so that it won't be
			    # encountered again.
			    #
			    foreach $tmp (@addrs) {
				$AliasesPTR{"$canonical-$tmp"} =~ s/(^|\s)$alias\s/$1/;
			    }
			} elsif (exists($deferredPTR{"$canonical-$addr"})) {
			    #
			    # Make the necessary updates so that reverse-mapping
			    # queries are answered with the unique interface
			    # name instead of the multi-address canonical name.
			    #
			    ($file, $revaddr, $ttl) = split(' ', $deferredPTR{"$canonical-$addr"});
			    $tmp = $alias;
			    $tmp .= ".$pDomain." if $alias !~ /\.$/;
			    $pendingPTR{$file}{$revaddr} = "$tmp $ttl";
			    delete($deferredPTR{"$canonical-$addr"});
			    last;	# finished with this IP address
			}
		    }
		}
	    }
	}
	keys(%deferredPTR);	# Reset the internal hash iterator before we go.
    }
    while (($canonical, $data) = each %deferredPTR) {
	#
	# Anything left over in the deferred PTR hash gets the default
	# treatment - a PTR record that points to the canonical name.
	#
	$canonical =~ s/-(\d+[.]){3}\d+$//;
	($file, $revaddr, $ttl) = split(' ', $data);
	$pendingPTR{$file}{$revaddr} = "$canonical $ttl";
    }
    keys(%pendingPTR);
    while (($file, $addr) = each %pendingPTR) {
	foreach $revaddr (sort { $a <=> $b } keys %{ $pendingPTR{$file} }) {
	    ($canonical, $ttl) = split(' ', $pendingPTR{$file}{$revaddr});
	    $canonical .= ".$Domain." if $canonical !~ /\.$/;
	    &PRINTF($file, "%s%s\tPTR\t%s\n", &TAB($revaddr, 8), $ttl, $canonical);
	}
    }
}
	
# Deal with spcl's
if (-r $Specialfile) {
    &PRINTF(*DOMAIN, "\n\$INCLUDE $Pwd/%s\n", $Specialfile);
    print STDOUT "File '$Specialfile' included.\n" if $verbose;
}
# Only consider spcl's for networks specified with -n.
keys(%Netfiles);
while (($n, $file) = each %Netfiles) {
    $n =~ s/\\//g;
    if (-r "spcl.$n") {
	&PRINTF($file, "\n\$INCLUDE $Pwd/spcl.%s\n", $n);
	if ($verbose) {
	    print STDOUT "File 'spcl.$n' found and included.\n";
	    if ($audit) {
		$data = &REVERSE($n) . ".in-addr.arpa.";
		#
		# Make sure to disable checking of single-character hostnames
		# and aliases since this RFC-952 restriction is limited to
		# entries in the host table.
		#
		$rfc1123 =~ s/strict/STRICT/;
		$newline_printed = 0;

		# Since all forward-mapping data has now been accounted for,
		# the "READ_RRs" subroutine will be able to issue a warning
		# if it detects a PTR record which points to an in-zone CNAME.
		#
		$newline_printed = &READ_RRs("spcl.$n", $data, $data, 0);
		print STDERR "\n" while $newline_printed--;
		$rfc1123 =~ s/STRICT/strict/;
	    }
	}
    }
}

if ($audit && $verbose) {
    #
    # Take the opportunity to undefine some data structures that are
    # no longer needed.  The memory space can then be recycled by the
    # AUDIT_RRs subroutine which itself needs to create still more
    # data structures.
    #
    undef %Aliases;
    undef %Comments;
    undef %Ttl;
    undef %deferredPTR;
    undef %pendingPTR;
    undef %pPTR;
    undef %HostsPTR;
    undef %AliasesPTR;
    undef %cAliases;
    undef %CommentRRs if $Commentfile;
    print STDOUT "Checking NS, MX, and CNAME RRs for various improprieties...\n";
    &AUDIT_RRs(0);
}

# generate boot.* files

print STDOUT "Generating boot and conf files...\n" if $verbose;

&GEN_BOOT;

print STDOUT "Done.\n" if $verbose;

exit;


#
# Subroutine to check for bad names
#
# RFC-1123 changes RFC-952 by allowing domain names to begin with a number
# as well as a letter.  It also extends the maximum length of a hostname
# to 255 characters.  This subroutine makes the following checks:
#
#   1)	The character set of hostnames and domain names is limited to digits,
#	the minus sign (-), the period (.), and any mix of upper/lowercase
#	letters.  Hostname aliases (CNAMEs) are not held to this restriction
#       nor are PTR, SRV, TXT, RP, and HINFO resource records.
#   2)	Hostnames and domain label names must not begin or end with a
#	minus sign or period.  Hostname aliases must not begin or end with
#	an unescaped period nor contain adjacent unescaped periods.
#   3)	If the checking level is set to "strict", hostnames and aliases
#	that are read from the host table must have a minimum length of
#	two characters (RFC-952).
#
# No check is made for maximum length of hostnames and/or domain labels.
#
# Return values:
#   0 = valid
#   1 = invalid
#
sub CHECKNAME {
    my ($name, $rrtype) = @_;
    my $bad_chars;

    if ($rrtype =~ /^(CNAME|CNAME-table|PTR|SRV|TXT|RP|HINFO)$/) {
	#
	# Remove all escape characters that are themselves escaped
	# so that only true escape characters remain for testing.
	#
	$name =~ s/\\\\//g;
	return 1 if $name =~ /^\.|[^\\]\.$|[^\\]\.\./;
    } else {
	$bad_chars = $name;
	$bad_chars =~ tr/A-Za-z0-9.-//d;
	return 1 if $bad_chars || $name =~ /^[.-]|[.-]$|\.\.|\.-|-\./;
    }
    return 1 if $rfc1123 =~ /strict/ && $rrtype =~ /-table$/ && $name =~ /^.$/;
    return 0;
}


#
# Subroutine to fit a name into a tab-delimited space
#
sub TAB {
    my ($name, $field) = @_;
    my $tabs;

    $tabs = $field - length($name);
    unless ($tabs > 0) {
	return "$name ";
    }
    while ($tabs > 0) {
	$name .= "\t";
	$tabs -= 8;
    }
    return $name;
}


#
# Subroutine to print to a file, getting around the open file limit problem.
# 
# If a file is not open (or has not seen before), (re)open it.
# If this fails, close the Least Recently Used file and try again.
# LRU policy will work well assuming the host table is in some
# reasonable numerical IP address order.
#
sub PRINTF {
    my (@args, $openit);
    local *FILE;
    (*FILE, @args) = @_;

    unless ($LRUcount{*FILE} > 0) {
	#
	# File has been closed or never opened.
	#
	if ($LRUcount{*FILE} == 0) {
	    # Oops, the file was closed.  Want to re-open and append to end.
	    #
	    $openit = ">> $Fname{*FILE}";
	} else {
	    # Never been opened before, give it a try.
	    #
	    $openit = "> $Fname{*FILE}";
	}
	&OPEN(*FILE, $openit) or die "Sorry, couldn't open file '$openit': $!\n";
    }
    printf FILE @args;
    $LRUcount{*FILE} = ++$Printcount;
}


#
# Subroutine to open a file.  If necessary, a currently-open
# file will be closed if all available filehandles are in use.
#
sub OPEN {
    my (@sortedFH, $arg, $FH, $ok);
    local *FILE_HANDLE;
    (*FILE_HANDLE, $arg) = @_;

    if ($Openfiles < $Filelimit) {
	$ok = open(*FILE_HANDLE, $arg);
    }
    if (!$ok || !($Openfiles < $Filelimit)) {
	@sortedFH = sort { $LRUcount{$a} cmp $LRUcount{$b}; } keys %LRUcount;
	foreach $FH (@sortedFH) {
	    if ($LRUcount{$FH} > 0) {
		&CLOSE($FH);
		last;
	    }
	}
	$ok = open(*FILE_HANDLE, $arg);
    }
    if ($ok) {
	$Openfiles++;
	return $ok;
    } else {
	return;		# return the undefined value just like the real open()
    }
}


#
# Subroutine to close a file and maintain the
# data structures for managing still-open files.
#
sub CLOSE {
    my ($ok);
    local *FHANDLE;
    (*FHANDLE) = @_;

    $LRUcount{*FHANDLE} = 0;
    $ok = close(*FHANDLE);
    $Openfiles-- if ($ok && $Openfiles > 0);
}


#
# Generate resource record data for strings from the comment
# field that are found in the comment file (-C).
#
sub DO_COMMENTS {
    my ($canonical, @addrs) = @_;
    my (@c, $c, $addr, $comments, $class, $rrtype, $tmp);

    foreach $addr (@addrs) {
	$comments .= " " . $Comments{"$canonical-$addr"};
    }

    @c = split(' ', $comments);
    foreach $c (@c) {
	if (exists($CommentRRs{$c})) {
	    ($class, $rrtype, $tmp) = split(' ', $CommentRRs{$c}, 3);
	    if ($class !~ /^(IN|CH|HS)$/) {
		#
		# Assume the optional CLASS field was omitted (defaults
		# to IN) and that "$class" has the parsed RR type.
		#
		$rrtype = $class
	    }
	    $rrtype = uc($rrtype);
	    if (exists($RRowners{$canonical}) && $RRowners{$canonical} =~ / CNAME /) {
		print STDERR "Can't create $rrtype record for '$canonical' because CNAME exists for name.\n";
	    } else {
		&PRINTF(*DOMAIN, "%s%s\t%s\n", ($canonical eq $nameset ? "\t\t" : &TAB($canonical, 16)), $ttl, $CommentRRs{$c});
		$nameset = $canonical;
		if ($rrtype eq 'TXT' && $found_spclRP) {
		    if (exists($RRowners{$canonical})) {
			$RRowners{$canonical} .= "TXT " if $RRowners{$canonical} !~ / TXT /;
		    } else {
			$RRowners{$canonical} = " TXT ";
		    }
		}
	    }
	}
    }
}


#
# Generate MX record data
#
sub MX {
    my ($canonical, @addrs) = @_;
    my ($global, $self, $rafcp, $addr, $comments, $rp, $rptxt);
    my ($LocalHost, $rdata);

    $LocalHost = 0;
    foreach $addr (@addrs) {
	$comments .= " " . $Comments{"$canonical-$addr"};
	$LocalHost = 1 if $addr eq $localhost;
    }

    $self = $global = 1; $rafcp = 0;
    if ($comments =~ /\[\s*no\s+mx\s*\]/i) { $self = $global = 0; }
    if ($comments =~ /\[\s*no\s+smtp\s*\]/i) { $self = 0; }
    if ($comments =~ /\[\s*smtp\s*\]/i) { $self = 1; }
    if ($comments =~ /\[\s*rafcp\s*\]/i) { $rafcp = 1; $self = $global = 0; }

    if (exists($RRowners{$canonical}) && $RRowners{$canonical} =~ / CNAME /) {
	print STDERR "Can't create WKS record for '$canonical' because CNAME exists for name.\n" if ($rafcp || $dowks) && $verbose;
	print STDERR "Can't create MX record for '$canonical' because CNAME exists for name.\n" if ($self || $global) && $verbose;
	return;
    }
    # If '[rafcp]' is specified in the comment section, add in a WKS record,
    # and do not add any MX records.
    #
    if ($rafcp) {
	foreach $addr (@addrs) {
	    &PRINTF(*DOMAIN, "%s%s\tWKS\t%s rafcp\n", ($canonical eq $nameset ? "\t\t" : &TAB($canonical, 16)), $ttl, $addr);
	    $nameset = $canonical;
	}
    } elsif (!$LocalHost) {
	if ($self) {
	    # Add WKS if requested
	    if ($dowks) {
		foreach $addr (@addrs) {
		    &PRINTF(*DOMAIN, "%s%s\tWKS\t%s tcp smtp\n", ($canonical eq $nameset ? "\t\t" : &TAB($canonical, 16)), $ttl, $addr);
		    $nameset = $canonical;
		}
	    }
	    &PRINTF(*DOMAIN, "%s%s\tMX\t%s %s\n", ($canonical eq $nameset ? "\t\t" : &TAB($canonical, 16)), $ttl, $DefMxWeight, $canonical);
	    $nameset = $canonical;
	}
	if (@Mx > 0 && $global) {
	    foreach $rdata (@Mx) {
		&PRINTF(*DOMAIN, "%s%s\tMX\t%s\n", ($canonical eq $nameset ? "\t\t" : &TAB($canonical, 16)), $ttl, $rdata);
		$nameset = $canonical;
	    }
	}
    }
}


#
# Generate RP record data
#
sub RP {
    my ($canonical, @addrs) = @_;
    my ($addr, $comments, $rp, $rptxt, $user_part, $domain_part);

    if (exists($RRowners{$canonical}) && $RRowners{$canonical} =~ / CNAME /) {
	print STDERR "Can't create RP record for '$canonical' because CNAME exists for name.\n" if $verbose;
	return;
    }

    foreach $addr (@addrs) {
	$comments .= " " . $Comments{"$canonical-$addr"};
    }

    # Be liberal in what we accept, e.g.,
    #
    #   [rp=first.last@host"text"]  [ rp = first.last@host "text" ]
    #   [rp = first.last@host random "text" string ]
    #
    #   all result in RP  MAILBOX  = first\.last.host
    #                 RP  TXTDNAME = current canonical domain name
    #                 TXT RDATA    = "text"
    #
    #   [rp=first.last@host]  [rp= first.last@host "" ]
    #   [rp = first.last@host random "" string ]
    #   [rp = first.last@host random  string ]
    #
    #   all result in RP  MAILBOX  = first\.last.host
    #                 RP  TXTDNAME = . (root zone placeholder)
    #                 no TXT record
    #
    #   [rp="text"]  [ rp = "text" ]  [rp = "text" random string ]
    #
    #   all result in RP  MAILBOX  = . (root zone placeholder)
    #                 RP  TXTDNAME = current canonical domain name
    #                 TXT RDATA    = "text"
    #
    if ($comments =~ /\[\s*rp\s*=\s*([^\s"]+)?[^"]*("[^"]*")?[^\]]*\]/i) {
	$rp = ($1) ? $1 : ".";
	$rptxt = ($2) ? $2 : "";
	$rptxt =~ s/"//g;
	if ($rp =~ /\@/) {
	    ($user_part, $domain_part) = split('\@', $rp);
	    $user_part =~ s/\./\\./g;		# escape "." in username
	    $user_part =~ s/\\\\//g;		# remove redundancies
	    if ($domain_part =~ /\./) {		# multiple domain labels
		$domain_part .= ".";		# append root domain
	    }					# relative domain fmt. otherwise
	    $rp = "$user_part.$domain_part";	# rejoin w/ unescaped "."
	} elsif ($rp !~ /\.$/) {		# procede if no trailing "."
	    $rp =~ s/\./\\./g;			# treat as username & escape "."
	    $rp =~ s/\\\\//g;			# remove redundancies
	}					# leave username in relative fmt
	$rp =~ s/\.\././g;			# remove redundant "." chars.
	&PRINTF(*DOMAIN, "%s%s\tRP\t%s %s\n", ($canonical eq $nameset ? "\t\t" : &TAB($canonical, 16)), $ttl, $rp, ($rptxt eq "" ? "." : "$canonical"));
	unless ($rptxt eq "") {
	    &PRINTF(*DOMAIN, "%s%s\tTXT\t\"%s\"\n", "\t\t", $ttl, $rptxt);
	    if ($found_spclRP) {
		if (exists($RRowners{$canonical})) {
		    $RRowners{$canonical} .= "TXT " if $RRowners{$canonical} !~ / TXT /;
		} else {
		    $RRowners{$canonical} = " TXT ";
		}
	    }
	}
	$nameset = $canonical;
    }
}
    

#
# Generate TXT record data
#
sub TXT {
    my ($canonical, @addrs) = @_;
    my ($addr, $comments);

    foreach $addr (@addrs) {
	$comments .= " " . $Comments{"$canonical-$addr"};
    }

    $comments =~ s/\[\s*no\s+smtp\s*\]//gi;
    $comments =~ s/\[\s*smtp\s*\]//gi;
    $comments =~ s/\[\s*no\s+mx\s*\]//gi;
    $comments =~ s/\[\s*rafcp\s*\]//gi;
    $comments =~ s/\[\s*ttl\s*=\s*(\d+|(\d+[wdhms])+)\s*\]//gi;
    $comments =~ s/\[\s*rp\s*=\s*([^\s"]+)?[^"]*("[^"]*")?[^\]]*\]//gi;
    $comments =~ s/\[\s*mh\s*=\s*(d|c|p|cp|pc)\s*\]//gi;
    $comments =~ s/^\s+//;
    $comments =~ s/\s+$//;
    
    if ($comments) {
	if (exists($RRowners{$canonical}) && $RRowners{$canonical} =~ / CNAME /) {
	    print STDERR "Can't create TXT record for '$canonical' because CNAME exists for name.\n" if $verbose;
	} else {
	    &PRINTF(*DOMAIN, "%s%s\tTXT\t\"%s\"\n", ($canonical eq $nameset ? "\t\t" : &TAB($canonical, 16)), $ttl, $comments);
	    $nameset = $canonical;
	    if ($found_spclRP) {
		if (exists($RRowners{$canonical})) {
		    $RRowners{$canonical} .= "TXT " if $RRowners{$canonical} !~ / TXT /;
		} else {
		    $RRowners{$canonical} = " TXT ";
		}
	    }
	}
    }
}


#
# Generate resource records (CNAME or A) for the aliases of a
# canonical name.  This subroutine is called after the generation
# of all of the canonical name's other RRs (A, MX, TXT, etc.).
#
sub CNAME {
    my ($canonical, @addrs) = @_;
    my ($addr, $alias, $common_alias, $default_method, $file, $interface_alias);
    my ($make_rr, $numaddrs, $revaddr, $rr_written, $tmp, @aliases);

    $rr_written = 0;
    $numaddrs = @addrs;
    foreach $addr (@addrs) {
	#
	# If this is a single-address host, print a CNAME record
	# for each alias.
	# 
	# If this is a multi-homed host, perform the following tasks
	# for each alias of each IP address:
	#
	#   1. Identify aliases that are common to all addresses.
	#      If possible, a CNAME pointing to the canonical name
	#      will be created.
	#
	#   2. The first non-common alias will be assigned an A record
	#      and, if enabled, the appropriate MX RRset.
	#
	#   3. If the default method of handling multi-homed hosts is
	#      in effect, then do the following:
	#
	#      * Subsequent non-common aliases are assigned the same
	#        RRset(s) as the first alias in step #2.
	#
	#      Otherwise, generate the rest of the forward-mapping RRs
	#      for the multi-homed host using the following alternative:
	#
	#      * Subsequent non-common aliases will be assigned a CNAME
	#        that points to the A record created in step #2.
	#
	if ($numaddrs > 1) {
	    #
	    # Each address of a multi-homed host may specify how the
	    # forward- and reverse-mapping RRsets get generated via
	    # the "[mh=??]" flag together with the +m option.
	    # Determine the forward-mapping method that's now in effect.
	    #
	    if ($multi_homed_mode !~ /C/) {
		#
		# Use the default method unless overridden.
		#
		$default_method = ($Comments{"$canonical-$addr"} =~ /\[\s*mh\s*=\s*(c|cp|pc)\s*\]/i) ? 0 : 1;
	    } else {
		#
		# Use the alternate method unless overridden.
		# NOTE: The absence of the "c" specification in the
		#       comment flag signifies an override condition.
		#
		$default_method = ($Comments{"$canonical-$addr"} =~ /\[\s*mh\s*=\s*[dp]\s*\]/i) ? 1 : 0;
	    }
	}
	@aliases = split(' ', $Aliases{"$canonical-$addr"});
	$interface_alias = "";
	foreach $alias (@aliases) {
	    #
	    # Skip over the alias if it and the canonical name differ
	    # only in that one of them has the domain appended to it.
	    #
	    $alias =~ s/$Domainpattern$//;
	    #
	    # If "$UseDefaultDomain" is in effect (-d domain mode=D),
	    # the following typo is not caught when the host file is
	    # read by the main section of the program:
	    #
	    #   host.domain   (correct)
	    #   host .domain  (typo)
	    #
	    # The ".domain" fragment gets interpreted as an alias and will
	    # be rendered to the null string by the previous statement.
	    # Make sure that null aliases are also skipped over.
	    # Otherwise, havoc will ensue later in this subroutine.
	    #
	    next if $alias eq $canonical || !$alias;

	    $common_alias = 0;
	    if ($numaddrs > 1) {
		#
		# If the alias exists for *all* addresses of this host,
		# we can use a CNAME instead of an Address record.
		#
		$common_alias = 1;
		foreach $tmp (@addrs) {
		    unless ($Aliases{"$canonical-$tmp"} =~ /(^|\s)$alias\s/) {
			$common_alias = 0;
			last;
		    }
		    last unless $common_alias;
		}
	    }

	    if ($numaddrs > 1 && !$common_alias && !$interface_alias) {
		if (!$default_method) {
		    #
		    # Only the current alias will be assigned an A record.
		    # Subsequent non-common aliases will be assigned CNAME
		    # records that point back to this one.
		    # Initialize the $interface_alias variable with the
		    # domain name to which the CNAME(s) will reference.
		    # The variable assignment will also prevent subsequent
		    # aliases of the current address from be processed by
		    # this A-record block.
		    #
		    $interface_alias = $alias;
		}
		if (exists($RRowners{$alias}) && $RRowners{$alias} =~ / CNAME /) {
		    print STDERR "Can't create A record for '$alias' because CNAME exists for name.\n" if $verbose;
		    $make_rr = 0;
		} else {
		    $make_rr = 1;
		    if ($rfc1123 && &CHECKNAME($alias, 'A-table')) {
			$make_rr = 0 if $rfc1123 !~ /warn/;
			if ($verbose) {
			    if ($make_rr) {
				print STDERR "Warning: non-RFC-compliant Address record ('$alias') being generated.\n";
			    } else {
				print STDERR "Cannot generate Address record for '$alias' (invalid hostname).\n";
			    }
			    print STDERR "It is an alias for '$canonical', but CNAME not possible (multi-homed).\n";
			}
		    }
		}
		if ($make_rr) {
		    &PRINTF(*DOMAIN, "%s%s\tA\t%s\n", ($alias eq $nameset ? "\t\t" : &TAB($alias, 16)), $ttl, $addr);
		    $nameset = $alias;
		    $rr_written = 1;
		    #
		    # Keep track of domain names that now have Address RRs
		    # assigned to them (we can't make these registrations
		    # in the %Hosts hash because we are in a loop that is
		    # serially reading that data structure with the 'each'
		    # function).
		    # This data will be used to prevent the creation
		    # of conflicting CNAMEs and, if auditing is enabled,
		    # to make sure that in-domain NS and MX RRs point to
		    # domain names that have at least one Address record.
		    #
		    if (exists($RRowners{$alias})) {
			$RRowners{$alias} .= "A ";
		    } else {
			$RRowners{$alias} = " A ";
		    }

		    if ($domx) {
			#
			# Ensure that every Address RR has the accompanying
			# MX RRset.  First, however, the comment flags that
			# are tied to the canonical name of this particular
			# address must be copied to a key based on the current
			# alias.
			#
			$Comments{"$alias-$addr"} = $Comments{"$canonical-$addr"};
			&MX($alias, ($addr));
		    }

		    if (exists($deferredPTR{"$canonical-$addr"})) {
			#
			# Update the deferred PTR hash so that reverse-mapping
			# queries are answered with the unique interface name
			# instead of the multi-address canonical name.
			#
			($file, $revaddr, $tmp) = split(' ', $deferredPTR{"$canonical-$addr"});
			$pendingPTR{$file}{$revaddr} = "$alias $tmp";
			delete($deferredPTR{"$canonical-$addr"});
		    }
		}
	    } else {
		#
		# Make sure that the alias does not already have an
		# assigned domain name before creating a CNAME record.
		#
		if (exists($RRowners{$alias}) || exists($Hosts{$alias})) {
		    print STDERR "Resource record exists for '$alias' already; alias ignored.\n" if $verbose;
		    $make_rr = 0;
		} else {
		    $make_rr = 1;
		    if ($rfc1123 && &CHECKNAME($alias, 'CNAME-table')) {
			$make_rr = 0 if $rfc1123 !~ /warn/;
			if ($verbose) {
			    if ($make_rr) {
				print STDERR "Warning: creating non-RFC-compliant CNAME record for alias '$alias'.\n";
			    } else {
				print STDERR "Cannot create CNAME record for '$alias' (invalid alias).\n";
			    }
			}
		    }
		    if ($make_rr) {
			if ($numaddrs == 1 || $common_alias) {
			    $tmp = $canonical;
			} else {
			    $tmp = $interface_alias;
			}
			&PRINTF(*DOMAIN, "%s%s\tCNAME\t%s\n", ($alias eq $nameset ? "\t\t" : &TAB($alias, 16)), $ttl, $tmp);
			$nameset = $alias;
			$RRowners{$alias} = " CNAME ";
			$rr_written = 1;
			if ($audit && $verbose && $alias =~ /^\*($|\.)/) {
			    #
			    # Register the wildcard CNAME in the hash
			    # that is reserved for this purpose.
			    #
			    $tmp = $alias;
			    $tmp .= ".$Domain." if $tmp !~ /$Domainpattern\.$/;
			    $tmp =~ s/^\*\.//;
			    if (exists($Wildcards{$tmp})) {
				$Wildcards{$tmp} .= "CNAME " if $Wildcards{$tmp} !~ / CNAME /;
			    } else {
				$Wildcards{$tmp} = " CNAME ";
			    }
			}
		    }
		}
	    }

	    if ($common_alias) {
		#
		# Since a CNAME record was either created or otherwise
		# accounted for, remove this name from the alias list so
		# it's not encountered again for the next address of this host.
		#
		foreach $tmp (@addrs) {
		    $Aliases{"$canonical-$tmp"} =~ s/(^|\s)$alias\s/$1/;
		}
	    }
	    if ($rr_written && exists($cAliases{$alias})) {
		#
		# RRs for the default domain take precedence over identically-
		# named CNAMEs requested by a -c option with "mode=D".
		# Prevent the generation of an illegal duplicate DNS record
		# by removing the pending domain name from the deferred list.
		#
		($tmp, $netpat) = split(' ', $cAliases{$alias});
		$cmode = $cModeSpec{$netpat};
		print STDERR "Can't create CNAME for '$alias.$cpatrel{$netpat}'; another RR exists for name.\n" if $verbose && $cmode !~ /Q/;
		delete($cAliases{$alias});
	    }
	    $rr_written = 0;
	}
    }
}


#
# Create the SOA record at the beginning of the db file.
# If necessary, a $TTL directive will also be created.
#
sub MAKE_SOA {
    my ($fname, $s, $tmp, $TTLdirective, $SOAmname, $SOArname);
    my ($SOArefresh, $SOAretry, $SOAexpire, $SOAminimum, @SOA_fields);
    local *FILEH;
    ($fname, *FILEH) = @_;

    if (-s $fname) {
	&OPEN(*FILEH, $fname) or die "Unable to open '$fname': $!\n";
	$_ = <FILEH>;
	#
	# Some sites may want to copy zone files from a slave nameserver.
	# Deal with this possibility by skipping over the comment lines
	# and $ORIGIN directive that precede the SOA record.
	#
	while (/^;/ || /^\$ORIGIN\s+/) { $_ = <FILEH>; }
	chop;
	if (/^\$TTL\s+/) {
	    ($tmp, $TTLdirective) = split(' ', $_);
	    if (!$rfc2308) {
		#
		# We are here because our RFC-2308 status was not set by
		# the -o or +t option and the FIXUP subroutine could not
		# determine the version of BIND on the master nameserver.
		# Now that this status is known, follow the same course
		# of action as documented by the comments in the relevant
		# section of FIXUP.
		#
		$rfc2308 = 1;
		$MasterTtl = $Ttl if $Ttl;
		$Ttl = $DefNegCache;
	    }
	    $_ = <FILEH>;
	    chop;
	}
	if (/\s\(\s*$/) {
	    if (!$soa_warned) {
		print STDOUT "Converting SOA format to new style.\n" if $verbose;
		$soa_warned = 1;
	    }
	    # The SOA record is split across more than one line.
	    # Although any combination is theoretically possible,
	    # only two variations occur in real life.  Either the
	    # SOA serial and timer fields are all on the next line
	    # or these fields appear individually on the next five
	    # lines.
	    #
	    $SOArefresh = "";
	    $_ = <FILEH>;
	    if (/\s\)\s*$/) {
		#
		# The rest of the SOA RR has just been read.
		#
		($Serial, $SOArefresh, $SOAretry, $SOAexpire, $SOAminimum) = split(' ');
	    } else {
		#
		# All we have is the serial number so far.
		#
		($Serial) = split(' ');
	    }
	    if ($ForceSerial > 0) {
		$Serial = $ForceSerial;
	    } else {
		$Serial++;
		$Serial = $DateSerial if $UseDateInSerial && ($DateSerial > $Serial);
	    }
	    if (!$SOArefresh && (!$Refresh || !$Retry || !$Expire || !$Ttl)) {
		#
		# The rest of the SOA fields have not yet been obtained
		# and we need to preserve one or more SOA timer values.
		#
		($SOArefresh) = split(' ', <FILEH>);
		($SOAretry) = split(' ', <FILEH>);
		($SOAexpire) = split(' ', <FILEH>);
		($SOAminimum) = split(' ', <FILEH>);
	    }
	    # Preserve existing SOA timer values in the absence
	    # of a replacement value passed via the -o/+t options.
	    #
	    $SOArefresh = $Refresh if $Refresh;
	    $SOAretry = $Retry if $Retry;
	    $SOAexpire = $Expire if $Expire;
	    $SOAminimum = $Ttl if $Ttl;
	} else {
	    @SOA_fields = split(' ');
	    if ($#SOA_fields == 11) {
		if ($ForceSerial > 0) {
		    $Serial = $ForceSerial;
		} else {
		    $Serial = ++$SOA_fields[6];
		    $Serial = $DateSerial if $UseDateInSerial && ($DateSerial > $Serial);
		}
		$SOArefresh = ($Refresh) ? $Refresh : $SOA_fields[7];
		$SOAretry = ($Retry) ? $Retry : $SOA_fields[8];
		$SOAexpire = ($Expire) ? $Expire : $SOA_fields[9];
		$SOAminimum = ($Ttl) ? $Ttl : $SOA_fields[10];
	    } else {
		print STDERR "Improper format SOA in $fname.\n";
		print STDERR "I give up ... sorry.\n";
		exit(2);
	    }
	}
	&CLOSE(*FILEH);
    } else {
	if ($ForceSerial > 0) {
	    $Serial = $ForceSerial;
	} else {
	    $Serial = $DefSerial;
	    $Serial = $DateSerial if $UseDateInSerial && ($DateSerial > $Serial);
	}
	$SOArefresh = ($Refresh) ? $Refresh : $DefRefresh;
	$SOAretry = ($Retry) ? $Retry : $DefRetry;
	$SOAexpire = ($Expire) ? $Expire : $DefExpire;
	if ($rfc2308) {
	    $SOAminimum = ($Ttl) ? $Ttl : $DefNegCache;
	} else {
	    $SOAminimum = ($Ttl) ? $Ttl : $DefTtl;
	}
    }

    &OPEN(*FILEH, "> $fname") or die "Unable to open '$fname': $!\n";

    $SOAmname = $RespHost;
    $SOArname = $RespUser;
    if ($fname eq $Domainfile) {
	#
	# Make a cosmetic indulgence by keeping in-domain names
	# relative to the origin in the forward-mapping file.
	#
	$SOAmname =~ s/$Domainpattern\.$//;
	$SOArname =~ s/$Domainpattern\.$//;
    }
    if ($rfc2308) {
	$TTLdirective = $MasterTtl if $MasterTtl;
	$TTLdirective = $DefTtl if !$TTLdirective;
	print FILEH "\$TTL $TTLdirective\n";
    }
    print FILEH "\@ IN SOA  $SOAmname $SOArname";
    print FILEH " ( $Serial $SOArefresh $SOAretry $SOAexpire $SOAminimum )\n";
    foreach $s (@FullServers) {
	$tmp = $s;
	$tmp =~ s/$Domainpattern\.$// if $fname eq $Domainfile;
	print FILEH "  IN NS   $tmp\n";
    }
    if (exists($PartialServers{$fname})) {
	foreach $s (split(' ', $PartialServers{$fname})) {
	    $tmp = $s;
	    $tmp =~ s/$Domainpattern\.$// if $fname eq $Domainfile;
	    print FILEH "  IN NS   $tmp\n";
	}
    } elsif (!@FullServers) {
	# Add nameserver in MNAME field of SOA record if missing -s/-S
	print FILEH "  IN NS   $SOAmname\n";
    }
    print FILEH "\n";
    $Fname{*FILEH} = $fname;
    &CLOSE(*FILEH);
}


#
# Reverse the octets of an IP address.
#
sub REVERSE {
    my ($address_token) = @_;

    return join('.', reverse(split('\.', $address_token)));
}


#
# Convert a time period in symbolic notation to the equivalent
# number of seconds.  Repeated time periods are added together
# consistent with the behavior of BIND, e.g.,
#   "1w2d3h2h1d1w"  is calculated identically to "2w3d5h"
#
sub SECONDS {
    my ($ttl) = @_;
    my @weeks   = ("dhms", "w", 604800);
    my @days    = ("whms", "d", 86400);
    my @hours   = ("wdms", "h", 3600);
    my @minutes = ("wdhs", "m", 60);
    my @seconds = ("wdhm", "s", 1);
    my @time_interval = (@weeks, @days, @hours, @minutes, @seconds);
    my ($other_units, $this_unit, $multiplier, $time, $total_seconds);

    return $ttl if $ttl =~ /^\d+$/;
    $total_seconds = 0;
    while (($other_units, $this_unit, $multiplier) = @time_interval) {
	$time = $ttl;
	$time =~ s/(\d+[$other_units])//gi;
	$time =~ s/$this_unit/ /gi;
	$time =~ s/ (\d+)/+$1/g;
	$total_seconds += ($multiplier * eval($time)) if $time;
	shift(@time_interval); shift(@time_interval); shift(@time_interval);
    }
    return $total_seconds;
}


#
# Initialize database files with new or updated SOA records.
#
sub INITDBs {
    my ($i);

    foreach $i (@makesoa) {
	&MAKE_SOA(split(' ', $i));
    }
    # Always make the RR for localhost appear first.
    #
    &PRINTF(*DOMAIN, "%s\tA\t127.0.0.1\n", "localhost\t");
    $RRowners{"localhost"} = " A ";
    
    if ($MakeLoopbackSOA) {
	$file = "DB.127.0.0";
	&MAKE_SOA("db.127.0.0", $file);
	&PRINTF($file, "1\t\tPTR\tlocalhost.\n");
    }
}


#
# Generate a domain name based on a template and iteration parameter
# passed via the $GENERATE directive.  This subroutine is called by
# the READ_RRs subroutine.
#
# Return values:
#    "domain_name" = valid domain name generated
#   " domain_name" = invalid domain name generated
#              " " = invalid template formatting specification encountered
#
sub GEN_NAME {
    my ($template, $i, $rrtype, $side) = @_;
    my ($domain_name, $prefix, $suffix, $offset, $width, $radix);

    $domain_name = "";
    while ($template) {
	if ($template !~ /([^\$]+)?\$(.*)/) {
	    #
	    # We are finished - there are no more "$" characters.
	    # Append what's left of $template to $domain_name and
	    # set $template to null so that the loop exits.
	    #
	    $domain_name .= $template;
	    $template = "";
	} else {
	    #
	    # A "$" character exists.  Append whatever appears in front
	    # of it to $domain_name.  $template is re-assigned with
	    # whatever appears after the "$" character.
	    # 
	    $prefix = (defined($1)) ? $1 : "";
	    $suffix = (defined($2)) ? $2 : "";
	    $domain_name .= $prefix;
	    $template = $suffix;
	    if ($domain_name =~ /\\$/) {
		#
		# It turns out that an escape character preceded the "$"
		# that was just found thus rendering it as just another
		# character.
		# Restore the "$" that was removed from the end of the
		# $domain_name variable and proceed to the next loop iteration.
		#
		$domain_name .= "\$";
	    } else {
		#
		# The "$" character is indeed an iterator replacement symbol.
		# Now see if it is followed by a format specification.
		#
		if ($template !~ /^{/) {
		    #
		    # No format specification appears after the "$" character.
		    # Append the value of the passed iteration variable to
		    # $domain_name.  Nothing more needs to be done until the
		    # next loop iteration.
		    #
		    $domain_name .= $i;
		} elsif ($template =~ /^{(\d+),*([^,}]+)?,*([^,}]+)?}(.*)/) {
		    #
		    # A valid format specification appears after the "$"
		    # substitution symbol, e.g.,  host${0,3,d}
		    # Note: As of version 8.2.2-P5, BIND doesn't complain if
		    #       the 'width' and 'radix' fields do not consist of
		    #       digits and the pattern [doxX], respectively, i.e.,
		    #       host${9,foo,zoo}  defaults to  host${9,0,d}.
		    #
		    $offset = $1;
		    $width = "0$2";		# prepend "0" for zero-padding
		    $radix = $3;
		    $template = $4;
		    $width = 0 if $width !~ /\d+/;
		    $radix = "d" if $radix !~ /[doxX]/;
		    $domain_name .= sprintf "%${width}${radix}", ($i + $offset);
		} else {
		    #
		    # The format specification is invalid.
		    # Exit with the appropriate return value.
		    #
		    return " ";
		}
	    }
	}
    }
    if ($rfc1123 && $rfc1123 !~ /warn/) {
	#
	# The validity of the generated name must now be checked.
	# An invalid name or IP address will stop further processing
	# of the $GENERATE directive and thus prevent its data from
	# being registered in this script's global data structures.
	#
	if ($rrtype eq 'A' and $side eq 'RHS') {
	    if ($domain_name =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
		#
		# Check the octets in the order of likely failure.
		#
		if ($4 > 255 || $3 > 255 || $2 > 255 || $1 > 255) {
		    $domain_name = " ";
		}
	    } else {
		$domain_name = " ";
	    }
	} else {
	    $template = $domain_name;		# Remove a trailing period
	    $template =~ s/([^\\])\.$/$1/;	# before calling CHECKNAME.
	    $domain_name = " $domain_name" if &CHECKNAME($template, $rrtype);
	}
    }
    return $domain_name;
}


#
# Scan 'spcl' files (or zone transfer [AXFR] data if in verify mode)
# so that the following may be done:
#   * Store information about in-domain owner names in order to prevent
#     (detect in verify mode) the creation (existence) of conflicting CNAMEs
#     (forward-mapping files only).
#   * Store information about the RDATA fields of MX, NS, and PTR RRs for
#     later reporting in case these RRs are found to be not pointing to
#     domain names with A records.
#   * Store information about the RDATA field of CNAME RRs so that
#     dangling CNAMEs can be identified.
#   * Keep track of wildcard RRs and child domains so that the
#     auditing of NS, MX, CNAME, and PTR RRs can be done properly.
#   * Report RFC-1123 errors with owner-names and RDATA domain names plus
#     errors with malformed IP addresses.
#
# Return values:
#   0 = no warnings
#   1 = warnings
#   2 = file open error
#
sub READ_RRs {
    my ($spcl_file, $zone, $origin, $warning_status) = @_;
    my ($message, $linenum, $include_file, $new_origin, $zonepattern, $rr);
    my ($owner, $ttl, $rrtype, $rdata, $preference, $default_ttl);
    my ($original_line, $multi, $data, $last_subzone, $gen_file, $tmp, $n);
    my ($range, $start, $stop, $step, $lhs, $gen_owner, $gen_rdata);
    my ($rp_mailbox, $txt_domain, $wildcard_flag, $zone_suffix);
    local (*SPCL, *GEN);

    unless (&OPEN(*SPCL, $spcl_file)) {
	if ($verbose) {
	    print STDERR "\n" if !$newline_printed;
	    print STDERR "Couldn't open '$spcl_file': $!";
	    #
	    # The terminating newline will be output
	    # by this subroutine's caller.
	}
	$warning_status = $newline_printed = 2;
    }
    $owner = $origin;
    $zone_suffix = ($origin eq '.') ? "." : ".$origin";
    $zonepattern = $zone;
    $zonepattern =~ s/\./\\./g;
    $zonepattern = "\\.$zonepattern" if $zone ne '.';
    $message = $original_line = $multi = $ttl = $last_subzone = "";
    $default_ttl = $DefTtl;
    $linenum = 0;
    #
    # Caveat emptor: The following code to deal with split lines is not
    # as flexible as the db_load module in BIND, e.g., it doesn't handle
    # nested parentheses and whitespace must precede each parenthesis
    # character.  It does, however, work just fine for all practical purposes,
    # e.g., the following TXT record is *not* treated as a multi-line RR:
    #
    #  USDA-Machine-Room.net.caltech.edu.  12H IN TXT  "location: USDA (ACSMR)"
    #
    while (<SPCL>) {
	$linenum++;
	next if /^$/ || /^\s*;/;
	if ($multi) {
	    $original_line .= $_;		# Save the original input line
	} else {				# so that it can be echoed in
	    $original_line = $_;		# case there's an error.
	}
	s/([^\\]);.*$/$1/;			# strip comments
	s/\s+$//;				# chop trailing whitespace
	if ($multi || /(.*)\s\(/) {		# deal with multi-line records
	    $n = ($multi) ? " " : $1;		# force issue if already known
	    $n = ($n =~ s/[^\\]\"/$&/g) + 0;	# count unescaped double quotes
	    if (!($n % 2)) {			# even count means unquoted ' ('
		if ($multi) {			# already past the first line
		    s/^\s+//;			# remove leading whitespace
		    $n = " ";			# blank replaces removed newline
		} else {			# still in the first line
		    $n = "";			# leave the first line intact
		}
		$multi .= "$n$_";		# append continuation fragment
		next if $multi !~ /(.*)\s\)/;	# next line if no ' )' found
		$n = $1;			# save ' )' prefix for testing
		$n = ($n =~ s/[^\\]\"/$&/g) + 0;# count unescaped double quotes
		next if ($n % 2);		# ' )' turned out to be quoted
		$_ = $multi;			# else transfer concat'd lines
		$multi = "";			# not doing this is a Bad Thing
	    }
	}
	$rr = 0;
	if (/^([^\$]*?\s)($RRtypes)\s+(.+)/io) {
	    #        ^
	    # Note: Minimal matching must be used in the first group of the
	    #       Regular Expression.  Otherwise, the following SOA record
	    #       will be mistakenly matched as an NS record:
	    #
	    #       @      1D IN SOA       ns hostmaster ( 123 3h 1h 1w 10m )
	    #                              ^^
	    $tmp = lc($1);
	    $rrtype = uc($2);
	    $rdata = lc($3);
	    $rr = 1;
	    $tmp =~ s/\s+in\s+$//;			# strip 'IN' if present
	    if ($tmp =~ /^\S/) {			# $ttl *may* be null
		($owner, $ttl) = split(' ', $tmp);
		#
		# Make sure that all new owner names are fully-qualified.
		#
		$owner = $origin if $owner eq '@';
		$owner .= $zone_suffix unless $owner =~ /(^|[^\\])\.$/;
	    } elsif ($tmp =~ /\S/) {			# $ttl is *not* null
		$ttl = $tmp;
		$ttl =~ s/\s+//g;
	    } else {					# $ttl *is* null
		$ttl = "";
	    }
	    if ($owner =~ /$zonepattern$/ || $owner eq $zone) {
		#
		# Only consider RRs in the current zone.  RRs in child zones
		# will also be processed so that missing glue records can be
		# reported.  A future version of this program should detect
		# and report superfluous child zone RRs as well.
		#
		if ($rrtype =~ /^(SIG|NXT|KEY)$/) {
		    #
		    # These DNSSEC RRs are allowed to share owner names
		    # with CNAMEs per RFC-2065.
		    #
		    $tmp = "";
		} else {
		    $tmp = $owner;
		}
		if ($tmp =~ /$Domainpattern\.$/ || $tmp eq "$Domain.") {
		    #
		    # Normal mode:
		    # ------------
		    # Register owner names that are in the forward-mapping
		    # domain so that conflicting CNAMEs won't be created.
		    # The idea here is that the owner names of already-existing
		    # RRs should have priority over potential CNAMEs that have
		    # yet to be discovered in the host table.
		    #
		    # Verify mode:
		    # ------------
		    # Register owner names as the zone file is read so that
		    # CNAME conflicts can be reported.  In both normal mode
		    # and verify mode, registered CNAMEs will be used to
		    # quickly determine if they are pointed to by any NS
		    # and/or MX resource records during the auditing phase.
		    #
		    $tmp =~ s/$Domainpattern\.$//;
		    if ($rrtype eq 'CNAME' && $owner eq $zone) {
			$message = "Warning: Zone name can not exist as a CNAME";
		    } elsif (exists($RRowners{$tmp})) {
			#
			# See if there's a "CNAME and other data" error.
			#
			if ($RRowners{$tmp} =~ / CNAME /) {
			    $message = "Warning: '$tmp' already exists as a CNAME";
			} elsif ($rrtype eq 'CNAME') {
			    $message = "Warning: '$tmp' already exists as another resource record";
			} elsif ($RRowners{$tmp} !~ / $rrtype /) {
			    #
			    # If necessary, add the RR type to those
			    # already registered to the owner name.
			    #
			    $RRowners{$tmp} .= "$rrtype ";
			}
		    } else {
			#
			# Register the new owner name and its RR type.
			#
			$RRowners{$tmp} = " $rrtype ";
			if ($rfc1123) {
			    #
			    # Make sure to trim root zones and wildcard
			    # characters before submitting the owner field
			    # to the CHECKNAME subroutine.
			    #
			    $tmp =~ s/\.$//;
			    $tmp =~ s/^\*(\.|$)// if $rrtype =~ /^(A|MX|NS)$/;
			    if ($tmp && &CHECKNAME($tmp, $rrtype)) {
				$message .= "Invalid owner name field";
			    }
			}
		    }
		}
		if ($ttl && $ttl !~ /^(\d+|(\d+[wdhms])+)$/) {
		    $n = ($message) ? ".\n" : "";
		    $message .= "${n}Invalid TTL value";
		}
		# Parse and process the RDATA field(s) of the various
		# resource record types that will help in the effort
		# to identify the most common types of configuration
		# errors.  Even if the "$audit" flag is false, the
		# auditing data structures identify new domain names
		# in the RDATA fields and thus help to minimize
		# redundant calls to the CHECKNAME subroutine.
		# For best efficiency, the processing blocks should be
		# arranged so that the most common RR types appear first.
		#
		if ($rrtype eq 'MX') {
		    ($preference, $tmp) = split(' ', $rdata);
		    $rdata = (defined($tmp)) ? $tmp : "";
		    if ($preference !~ /^\d+$/) {
			$n = ($message) ? ".\n" : "";
			$message .= "${n}Invalid MX preference value";
		    }
		    if ($rdata =~ /^\S+$/) {
			$rdata = $origin if $rdata eq '@';
			#
			# Do not append the origin if the RDATA field
			# exists as, or ends with, an unescaped "."
			#
			$rdata .= $zone_suffix unless $rdata =~ /(^|[^\\])\.$/;
			if ($audit || $rfc1123) {
			    if (exists($MXdomains{$rdata})) {
				$MXdomains{$rdata} .= ", '$spcl_file'" if $MXdomains{$rdata} !~ /'$spcl_file'/;
			    } else {
				$MXdomains{$rdata} = "'$spcl_file'";
				if ($rfc1123) {
				    $tmp = $rdata;
				    $tmp =~ s/\.$//;
				    if ($tmp && &CHECKNAME($tmp, "MX")) {
					$n = ($message) ? ".\n" : "";
					$message .= "${n}Invalid RDATA field";
				    }
				}
			    }
			}
		    } elsif ($rdata) {
			$n = ($message) ? ".\n" : "";
			$message .= "${n}Invalid RDATA field";
		    } else {
			$n = ($message) ? ".\n" : "";
			$message .= "${n}Missing RDATA field";
		    }
		} elsif ($rrtype eq 'A') {
		    if ($rdata =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
			if ($4 > 255 || $3 > 255 || $2 > 255 || $1 > 255) {
			    $n = ($message) ? ".\n" : "";
			    $message .= "${n}Invalid IP address";
			}
		    } else {
			$n = ($message) ? ".\n" : "";
			$message .= "${n}Invalid IP address";
		    }
		} elsif ($rrtype eq 'CNAME') {
		    #
		    # Keep track of CNAME references so that dangling
		    # CNAMEs and CNAME loops can be detected.
		    #
		    if ($rdata =~ /^\S+$/) {
			$rdata = $origin if $rdata eq '@';
			$rdata .= $zone_suffix unless $rdata =~ /(^|[^\\])\.$/;
			if ($rrtype eq 'CNAME' && $owner eq $rdata) {
			    $n = ($message) ? ".\n" : "";
			    $tmp = $rdata;
			    $tmp =~ s/$zonepattern$//;
			    $message .= "${n}Warning: '$tmp' points back to itself";
			}
			if ($audit || $rfc1123) {
			    if (exists($spclCNAME{$rdata})) {
				$spclCNAME{$rdata} .= ", '$spcl_file'" if $spclCNAME{$rdata} !~ /'$spcl_file'/;
			    } else {
				$spclCNAME{$rdata} = "'$spcl_file'";
				if ($rfc1123) {
				    $tmp = $rdata;
				    $tmp =~ s/\.$//;
				    if ($tmp && &CHECKNAME($tmp, "CNAME")) {
					$n = ($message) ? ".\n" : "";
					$message .= "${n}Invalid RDATA field";
				    }
				}
			    }
			}
		    } else {
			$n = ($message) ? ".\n" : "";
			$message .= "${n}Invalid RDATA field";
		    }
		} elsif ($rrtype eq 'PTR') {
		    #
		    # Keep track of PTR references to determine whether
		    # or not they properly point to Address RRs.
		    #
		    if ($rdata =~ /^\S+$/) {
			$rdata = $origin if $rdata eq '@';
			$rdata .= $zone_suffix unless $rdata =~ /(^|[^\\])\.$/;
			if ($audit || $rfc1123) {
			    if (exists($spclPTR{$rdata})) {
			    $spclPTR{$rdata} .= ", '$spcl_file'" if $spclPTR{$rdata} !~ /'$spcl_file'/;
			    } else {
				$spclPTR{$rdata} = "'$spcl_file'";
				if ($rfc1123) {
				    $tmp = $rdata;
				    $tmp =~ s/\.$//;
				    if ($tmp && &CHECKNAME($tmp, "PTR")) {
					$n = ($message) ? ".\n" : "";
					$message .= "${n}Invalid RDATA field";
				    }
				}
			    }
			}
		    } else {
			$n = ($message) ? ".\n" : "";
			$message .= "${n}Invalid RDATA field";
		    }
		} elsif ($rrtype eq 'RP') {
		    if ($rdata =~ /^(\S+)\s+(\S+)$/) {
			$rp_mailbox = $1;
			$txt_domain = $2;
			$rp_mailbox = $origin if $rp_mailbox eq '@';
			$txt_domain = $origin if $txt_domain eq '@';
			#
			# Do not append the origin if the MAILBOX/TXTDNAME
			# field:
			#
			#   1. exists as, or ends with, an unescaped "."
			#   2. contains the "@" symbol (MAILBOX only)
			#
			$rp_mailbox .= $zone_suffix unless $rp_mailbox =~ /\@|(^|[^\\])\.$/;
			$txt_domain .= $zone_suffix unless $txt_domain =~ /(^|[^\\])\.$/;
			if ($rp_mailbox =~ /\@|^([^.]+\.)?$/) {
			    #
			    # MAILBOXes containing a "@" character (escaped
			    # or not) and/or consisting of just a single
			    # label are invalid.  However, a single "."
			    # character as a placeholder is OK.
			    #
			    $n = ($message) ? ".\n" : "";
			    $message .= "${n}Invalid MAILBOX field in RP RR";
			}
			if ($txt_domain ne '.' && ($audit || $rfc1123)) {
			    if (exists($spclRP{$txt_domain})) {
				$spclRP{$txt_domain} .= ", '$spcl_file'" if $spclRP{$txt_domain} !~ /'$spcl_file'/;
			    } else {
				$spclRP{$txt_domain} = "'$spcl_file'";
				if ($rfc1123) {
				    $tmp = $txt_domain;
				    $tmp =~ s/\.$//;
				    if (&CHECKNAME($tmp, "TXT")) {
					$n = ($message) ? ".\n" : "";
					$message .= "${n}Invalid TXTDNAME field in RP RR";
				    }
				}
			    }
			}
		    } else {
			$n = ($message) ? ".\n" : "";
			$message .= "${n}Invalid number of RDATA fields in RP RR";
		    }
		} elsif ($rrtype eq 'NS') {
		    if ($rdata =~ /^\S+$/) {
			$rdata = $origin if $rdata eq '@';
			$rdata .= $zone_suffix unless $rdata =~ /(^|[^\\])\.$/;
			if ($audit || $rfc1123) {
			    if (exists($NSdomains{$rdata})) {
				$NSdomains{$rdata} .= ", '$spcl_file'" if $NSdomains{$rdata} !~ /'$spcl_file'/;
			    } else {
				$NSdomains{$rdata} = "'$spcl_file'";
				if ($rfc1123) {
				    $tmp = $rdata;
				    $tmp =~ s/\.$//;
				    if ($tmp && &CHECKNAME($tmp, "NS")) {
					$n = ($message) ? ".\n" : "";
					$message .= "${n}Invalid RDATA field";
				    }
				}
			    }
			}
			if ($audit) {
			    if ($owner =~ /$zonepattern$/ && $owner ne "$last_subzone.") {
				#
				# Keep track of this domain's child zones so
				# that out-of-zone data can be recognized.
				#
				$last_subzone = $tmp = $owner;
				$last_subzone =~ s/\.$//;
				$tmp =~ s/\\\\//g;	# Remove redundant "\".
				if ($tmp =~ /^\*\./) {
				    $tmp =~ s/^\*\.//;
				    $wildcard_flag = 1;
				} else {
				    $wildcard_flag = 0;
				}
				$subzones{$tmp} = $wildcard_flag;
				push(@Vdomains, $last_subzone) if $verify_mode && $recursive_verify && $last_subzone !~ /^\*/;
			    }
			    # Store the owner, TTL, and rdata fields of the NS
			    # resource records.  The NS RRsets for each domain
			    # will be checked for having at least two listed
			    # nameservers (RFC-1034) and consistent TTL values
			    # (RFC-2181).  Checks for necessary glue records
			    # will also be made.
			    # If we are verifying a domain, the NS records will
			    # be supplied as input data to the 'check_del'
			    # program so that proper delegation can be checked.
			    #
			    $tmp = ($ttl) ? $ttl : $default_ttl;
			    $NSdata{$owner} .= " " . &SECONDS($tmp) . " $rdata";
			}
		    } else {
			$n = ($message) ? ".\n" : "";
			$message .= "${n}Invalid RDATA field";
		    }
		} elsif ($rrtype eq 'SOA') {
		    if (!$verify_mode) {
			$n = ($message) ? ".\n" : "";
			$message .= "${n}Warning: SOA RR is invalid in the 'spcl' file context";
		    } else {
			#
			# "$SOA_count" is used to detect the duplicate SOA RR
			# that appears at the end of a successful zone transfer.
			# It's also used to prevent the duplicate record from
			# being reprocessed.
			#
			$SOA_count++;
			if ($SOA_count > 1) {
			    next if $SOA_count == 2 && !$message;
			    if ($SOA_count > 2) {
				$n = ($message) ? ".\n" : "";
				$message .= "${n}Warning: found unexpected SOA record";
			    }
			} else {
			    #
			    # Separate the MNAME and RNAME fields preceding the
			    # "(" character from the subsequent serial number
			    # and timer fields comprising the SOA RDATA field.
			    #
			    $rdata =~ s/\s*[)][^)]*//;	# strip the last ")"
			    ($tmp, $rdata) = split(/\s*[(]\s*/, $rdata);
			    ($RespHost, $RespUser) = split(' ', $tmp);
			    ($Serial, $Refresh, $Retry, $Expire, $Ttl) = split(' ', $rdata);
			    $RespHost = $origin if $RespHost eq '@';
			    $RespUser = $origin if $RespUser eq '@';
			    #
			    # Do not append the origin if the MNAME/RNAME
			    # field:
			    #
			    #   1. exists as, or ends with, the root zone (".").
			    #      RNAMEs may end with an escaped ".", however.
			    #   2. contains the "@" symbol (RNAME only).
			    #
			    $RespHost .= $zone_suffix unless $RespHost =~ /\.$/;
			    $RespUser .= $zone_suffix unless $RespUser =~ /\@|(^|[^\\])\.$/;
			    #
			    # Although a successful zone transfer should return
			    # a valid SOA record, there's a distinct possibility
			    # that it could have bad data (particularly in the
			    # RNAME field) if obtained from a non-BIND server.
			    #
			    if ($owner ne $zone) {
				$n = ($message) ? ".\n" : "";
				$message .= "${n}Warning: owner name of SOA RR must match zone name";
			    } elsif ($owner !~ /\.$/) {
				$n = ($message) ? ".\n" : "";
				$message .= "${n}Warning: owner name of SOA RR must be absolute";
			    }
			    if ($rfc1123) {
				$tmp = $RespHost;	# An MNAME consisting of
				$tmp =~ s/\.$//;	# just a "." is valid.
				if ($tmp && &CHECKNAME($tmp, "NS")) {
				    $n = ($message) ? ".\n" : "";
				    $message .= "${n}Invalid MNAME field in SOA RR";
				}
			    }
			    if ($RespUser =~ /\@|^[^.]+\.$/) {
				#
				# RNAMEs containing a "@" character (escaped or
				# not) and/or consisting of just a single label
				# are invalid.  Just like the MNAME field,
				# however, a single "." as a placeholder is OK.
				#
				$n = ($message) ? ".\n" : "";
				$message .= "${n}Invalid RNAME field in SOA RR";
			    }
			    if ($Serial !~ /^\d+$/) {
				$n = ($message) ? ".\n" : "";
				$message .= "${n}Invalid serial number";
			    }
			    if ($Refresh !~ /^(\d+|(\d+[wdhms])+)$/i) {
				$n = ($message) ? ".\n" : "";
				$message .= "${n}Invalid refresh interval";
				$valid_SOA_timers = 0;
			    }
			    if ($Retry !~ /^(\d+|(\d+[wdhms])+)$/i) {
				$n = ($message) ? ".\n" : "";
				$message .= "${n}Invalid retry interval";
				$valid_SOA_timers = 0;
			    }
			    if ($Expire !~ /^(\d+|(\d+[wdhms])+)$/i) {
				$n = ($message) ? ".\n" : "";
				$message .= "${n}Invalid expiry period";
				$valid_SOA_timers = 0;
			    }
			    if ($Ttl !~ /^(\d+|(\d+[wdhms])+)$/i) {
				$n = ($message) ? ".\n" : "";
				$message .= "${n}Invalid default TTL/negative cache period";
				$valid_SOA_timers = 0;
			    }
			}
		    }
		}
		if ($owner =~ /^\*(\...*)?$zonepattern$/ && $audit) {
		    #
		    # Keep track of wildcarded owner names and their
		    # RR types so that nothing is missed when NS, MX,
		    # PTR, RP, and CNAME RRs are audited.
		    #
		    $tmp = $owner;
		    $tmp =~ s/^\*\.//;
		    if (exists($Wildcards{$tmp})) {
			$Wildcards{$tmp} .= "$rrtype " if $Wildcards{$tmp} !~ / $rrtype /;
		    } else {
			$Wildcards{$tmp} = " $rrtype ";
		    }
		}
	    } else {
		$message = "Warning: data outside zone '$zone' ignored";
	    }
	} elsif (/^\$INCLUDE($|\s+(\S+)?\s*(\S+)?\s*(\S+)?)/) {
	    $include_file = $2;
	    $new_origin = lc($3);
	    $data = $4;
	    if (!$include_file) {
		$message = "Unable to process \$INCLUDE - filename missing";
	    } else {
		if ($new_origin) {
		    if ($new_origin !~ /\.$/) {
			$message = "Terminating period missing from origin argument";
		    }
		    $tmp = $new_origin;
		    $tmp =~ s/\.$//;
		    if ($tmp && $rfc1123 && &CHECKNAME($tmp, 'NS')) {
			$n = ($message) ? ".\n" : "";
			$message .= "${n}Domain name in origin argument is invalid";
		    }
		} else {
		    $new_origin = $origin;
		}
		if ($data) {
		    $n = ($message) ? ".\n" : "";
		    $message .= "${n}Found uncommented text in comment field";
		}
		# Stop what we're doing with the current file and make a
		# recursive call to process the specified $INCLUDE file.
		# If the $INCLUDE directive also specified an origin
		# argument, it will be passed along as well.
		# Upon return, we'll automatically revert back to the
		# origin and default owner name that was in effect before
		# the subroutine call.  This is the current behavior of
		# BIND 8 and differs from that described in RFC-1035.
		#
		$data = $_;		# $_ is global in scope - save it.
		$warning_status = &READ_RRs($include_file, $zone, $new_origin, $warning_status);
		$_ = $data;
		if ($warning_status == 2) {
		    #
		    # The $INCLUDE filename could not be opened.  An error
		    # message has already been output but without the
		    # terminating newline character.
		    #
		    $message = "\n$message";
		    $warning_status = $newline_printed = 1;
		}
	    }
	} elsif (/^\$ORIGIN($|\s+(\S+)?\s*(\S+)?)/) {
	    $new_origin = lc($2);
	    $data = $3;
	    if (!$new_origin) {
		$message = "Missing argument from \$ORIGIN directive";
	    } else {
		if ($new_origin eq '..') {
		    #
		    # As a hack, get rid of the redundant trailing dot.
		    # DiG version 8.2 erroneously generates this $ORIGIN
		    # directive when it transfers the root zone.
		    #
		    $new_origin = ".";
		}
		$origin = $new_origin;
		$zone_suffix = ($origin eq '.') ? "." : ".$origin";
		if ($origin !~ /\.$/) {
		    $message = "Terminating period missing from domain name";
		}
		$tmp = $origin;
		$tmp =~ s/\.$//;
		if ($tmp && $rfc1123 && &CHECKNAME($tmp, 'NS')) {
		    $n = ($message) ? ".\n" : "";
		    $message .= "${n}Domain name is invalid";
		}
		if ($data) {
		    $n = ($message) ? ".\n" : "";
		    $message .= "${n}Found uncommented text in comment field";
		}
	    }
	} elsif (/^\$TTL($|\s+(\S+)?\s*(\S+)?)/) {
	    $tmp = lc($2);
	    $data = $3;
	    if (!$tmp) {
		$message = "Missing argument from \$TTL directive";
	    } else {
		if ($tmp =~ /^(\d+|(\d+[wdhms])+)$/) {
		    $default_ttl = $tmp;
		} else {
		    $message = "Invalid TTL value - ignored";
		}
		if ($data) {
		    $n = ($message) ? ".\n" : "";
		    $message .= "${n}Found uncommented text in comment field";
		}
	    }
	} elsif (/^\$GENERATE($|\s+(\S+)?\s*(\S+)?\s*(\S+)?\s*(\S+)?\s*(\S+)?)/) {
	    #
	    # This directive causes nameserver running BIND version 8.2
	    # or later to generate a set of resource records that differ
	    # from each other by one or more iterator values.
	    #
	    $range = $2;
	    $lhs = $3;
	    $rrtype = uc($4);
	    $rdata = $5;
	    $data = $6;
	    $gen_count++;
	    if (!$range) {
		$message = "Missing argument list from \$GENERATE directive";
	    } else {
		if ($range !~ /(\d+)-(\d+)(.*)/) {
		    $message = "Invalid range argument";
		} else {
		    $start = $1;
		    $stop = $2;
		    $step = $3;
		    if ($stop < $start) {
			$message = "Invalid range (stop < start)";
		    }
		    if ($step) {
			if ($step !~ /^\/(\d+)$/) {
			    $n = ($message) ? ".\n" : "";
			    $message .= "${n}Invalid range argument";
			} else {
			    $step = $1;
			}
		    } else {
			$step = 1;
		    }
		}
		if (!$lhs) {
		    $n = ($message) ? ".\n" : "";
		    $message .= "${n}Missing LHS argument";
		}
		if (!$rrtype) {
		    $n = ($message) ? ".\n" : "";
		    $message .= "${n}Missing RRtype argument";
		} elsif ($rrtype !~ /^(A|AAAA|CNAME|NS|PTR)$/) {
		    $n = ($message) ? ".\n" : "";
		    $message .= "${n}Unsupported RRtype argument";
		}
		if (!$rdata) {
		    $n = ($message) ? ".\n" : "";
		    $message .= "${n}Missing RHS argument";
		}
		if ($data) {
		    $n = ($message) ? ".\n" : "";
		    $message .= "${n}Found uncommented text in comment field";
		}
	    }
	    if (!$message) {
		#
		# Now that the basic syntax of the directive has been
		# checked, it's time to register the RRs that BIND
		# will generate into the appropriate global hash(es).
		# This will be done as follows:
		#
		#   1. Loop through the range specification and write
		#      the resulting RRs to a temporary file.
		#   2. Take the same action as though the temporary
		#      file appeared in an $INCLUDE directive - make
		#      a recursive call to READ_RRs to register the
		#      RRs into the appropriate data structure(s).
		#   3. Remove the temporary file upon returning from
		#      the recursive subroutine call and continue
		#      processing the 'spcl' file.
		#
		$gen_file = "/tmp/GENERATE_#$gen_count";
		unless (&OPEN(*GEN, "> $gen_file")) {
		    $message = "Couldn't open temporary working file: $!";
		} else {
		    for ($i = $start; $i <= $stop; $i += $step) {
			$gen_owner = &GEN_NAME($lhs, $i, $rrtype, "LHS");
			if ($gen_owner =~ /^ /) {
			    $gen_owner = s/^ //;
			    if ($gen_owner) {
				$message = "GEN_NAME: invalid owner name ('$gen_owner')";
			    } else {
				$message = "GEN_NAME: generate LHS failed";
			    }
			    last;
			}
			$gen_rdata = &GEN_NAME($rdata, $i, $rrtype, "RHS");
			if ($gen_rdata =~ /^ /) {
			    $gen_rdata = s/^ //;
			    if ($gen_rdata) {
				$message = "GEN_NAME: invalid RDATA name ('$gen_rdata')";
			    } else {
				$message = "GEN_NAME: generate RHS failed";
			    }
			    last;
			}
			printf(GEN "%s\t%s\t%s\n", &TAB($gen_owner, 24), $rrtype, $gen_rdata);
		    }
		    &CLOSE(*GEN);
		    if (!$message) {
			#
			# Make the recursive call that will register the
			# generated RRs into the relevant data structure(s).
			#
			$data = $_;
			$warning_status = &READ_RRs($gen_file, $zone, $origin, $warning_status);
			$_ = $data;
			if ($warning_status == 2) {
			    #
			    # The temporary working file was not able to
			    # be opened.  An error message has already been
			    # output but without the terminating newline
			    # character.
			    #
			    $message = "\n$message";
			    $warning_status = $newline_printed = 1;
			}
		    }
		    unlink($gen_file) if !$debug;
		}
	    }
	} elsif (/^\$/) {
	    $message = "Unknown directive";
	} else {
	    $message = "Invalid DNS record [unknown RR type and/or missing/extra field(s)] - ignored.\n";
	}
	if ($message && $verbose) {
	    if ($verify_mode) {
		$message .= "." if $message !~ /\n$/;
		$message =~ s/\n$//;		# prevents double-spaced lines
	    } else {
		$n = ($message =~ /\n$/) ? "" : "; ";
		$message .= "${n}file '$spcl_file', line $linenum";
	    }
	    print STDERR "\n" if !$newline_printed;
	    $original_line =~ s/\n$//;		# prevents double-spaced lines
	    if ($rr && $original_line =~ /^(\S+)?(\s+)(.*)/s) {
		#
		# Perform some cosmetic data manipulations to
		# ensure the clarity of the diagnostic output.
		#
		$data = $1;
		$n = $2;
		$tmp = $3;
		$data = "" if !defined($data);
		$n = length(expand("$data$n"));
		$data = ($owner eq $origin) ? "\@" : $owner;
		$data =~ s/$zonepattern$//;
		printf STDERR "%s\n%s%s\n", $message, &TAB("> $data", $n), $tmp;
	    } else {
		print STDERR "$message\n> $original_line\n";
	    }
	    $message = $n = "";
	    $warning_status = $newline_printed = 1;
	}
    }
    &CLOSE(*SPCL);
    return $warning_status;
}


#
# Perform some final error checking that is only possible after
# all of this zone's data has been accounted for.
# Checks are made for the following conditions:
#   * NS, MX, and PTR records that point to CNAMEs or domain names
#     with no Address records or to nonexistent domain names.
#   * CNAME records that point to nonexistent domain names, i.e.,
#     "dangling" CNAMEs.
#   * Zones with only one listed nameserver (violates RFC-1034),
#     NS RRsets with inconsistent TTL values (violates RFC-2181),
#     and NS RRs with missing glue records.
# This subroutine assumes that the 'DiG' program is available for
# resolving domain names that are external to the zone being processed.
#
# Having these checks performed by 'h2n' after it builds the zone
# data files is useful because such misconfigurations are not reported
# in the syslog when the zone is initially loaded by 'named'.  To do so
# would require 'named' to make two passes though the data, an impractical
# task to add to its overhead.  Even though 'named' eventually logs these
# domain names when they are accessed, it's also impractical to expect
# busy hostmasters to be constantly scanning the syslog(s) for such events.
#
# NOTE: Since this subroutine could be called for every run of 'h2n'
#       that processes a host file, careful consideration should be made
#       before adding any more data verification tasks.  Additional data
#       checks which are time-consuming and/or esoteric should be added to
#       the "VERIFY_ZONE" subroutine and called with the -v option instead.
#
# Return values:
#   0 = no warnings
#   1 = warnings
#
sub AUDIT_RRs {
    my ($warning_status) = @_;
    my ($location, $host, $subzone_pattern, $match, $warning, $buffer, $domain);
    my ($DiG_batch, $prog_output, $query_options, $query_pattern, $status, $t);
    my ($tmp, $answer, $first_answer, $wildcard_pattern, $result, $n);
    my ($flags, $authority, $additional, $debug_out, $ns_count, $ttl, $uqdn);
    my ($nextbatch, $nextbatch_is_open, $rrtype, $cname, $cname_n, $cname_loop);
    my ($saved_cname, $chain_length, $max_chain_length, $query_section, $i, $k);
    my ($host_labels, $subzones_exist, $wildcard_OK, $wildcards_exist);
    my ($zone, $last_zone, %MXlist, %extMX, %extPTR, %extCNAME, %extRP);
    my (%baseCNAME, %chainCNAME, %NSrfc1034, %NSrfc2181, @temp);

    $n = ($warning_status) ? "" : "\n";	# controls output of cosmetic newlines
    if ($verify_mode) {			# other cosmetic considerations
	$t = 40;
	$tmp = "(SOA MNAME)";
    } else {
	$t = 32;
	$tmp = "-h option";
    }
    if ($Domain) {
	$domain = $Domain;
    } else {
	$domain = ".";
    }
    # Combine the '-h', '-s', and '-S' NS data structures into one.
    # The %NSlist hash will track each nameserver and where it was found.
    #
    $NSlist{lc($RespHost)} = $tmp if $RespHost !~ /^\.?$/;
    foreach $host (@FullServers) {
	$host = lc($host);
	if (exists($NSlist{$host}) && $NSlist{$host} =~ /-h/) {
	    $NSlist{$host} = "-h,-s options";
	} else {
	    $NSlist{$host} = "-s option";
	}
	# Register the NS RRs of this domain in the same manner
	# as any child domains that were found in a 'spcl' file.
	#
	$NSdata{"$Domain."} .= " " . &SECONDS($DefTtl) . " $host";
    }
    foreach $tmp (keys %PartialServers) {
	@temp = split(' ', $PartialServers{$tmp});
	foreach $host (@temp) {
	    $host = lc($host);
	    if (exists($NSlist{$host}) && $NSlist{$host} !~ /-S/) {
		$NSlist{$host} =~ s/^([^ ]+).*/$1,-S options/;
	    } else {
		$NSlist{$host} = "-S option";
	    }
	    # Register the NS RRs of the forward-mapping domain.
	    #
	    $NSdata{"$Domain."} .= " " . &SECONDS($DefTtl) . " $host" if $tmp eq $Domainfile;
	}
    }
    # Now add the 'spcl' NS data structure to %NSlist.
    #
    if (keys(%NSdomains)) {
	while (($host, $location) = each %NSdomains) {
	    if (exists($NSlist{$host})) {
		$NSlist{$host} .= ", $location";
	    } else {
		$NSlist{$host} = $location;
	    }
	}
    }

    # Combine the '-m' and 'spcl' MX data structures into one.
    # The %MXlist hash will track each mailhost and where it was found.
    #
    foreach $tmp (@Mx) {
	($location, $host) = split(' ', $tmp);
	$host .= ".$Domain." if $host !~ /\.$/;
	$MXlist{lc($host)} = "-m option";
    }
    if (keys(%MXdomains)) {
	while (($host, $location) = each %MXdomains) {
	    if (exists($MXlist{$host})) {
		$MXlist{$host} .= ", $location";
	    } else {
		$MXlist{$host} = $location;
	    }
	}
    }

    # Now that $NSlist and $MXlist have been assembled, separate the
    # in-zone FQDNs from the external ones.
    #
    $subzones_exist = keys(%subzones);
    $wildcards_exist = keys(%Wildcards);
    keys(%NSlist);				# Reset the iterator!
    while (($host, $location) = each %NSlist) {
	#
	# Try to find an explicit match in the local domain.
	#
	if ($host =~ /$Domainpattern\.$/ || $host eq "$Domain.") {
	    $tmp = $host;
	    $tmp =~ s/$Domainpattern\.$//;
	    $match = $warning = 0;
	    if (exists($Hosts{$tmp})) {
		$match = 1;
	    } elsif (exists($RRowners{$tmp})) {
		$rrtype = $RRowners{$tmp};
		if ($rrtype =~ / A /) {
		    $match = 1;
		} elsif ($rrtype =~ / CNAME /) {
		    $match = $warning = 1;
		    $NSlist{$host} = "[CNAME record]|$NSlist{$host}";
		}
	    }
	    if ((!$match && $wildcards_exist) || $subzones_exist) {
		#
		# If no explicit match was found, try matching an existing
		# wildcard.  In any case, however, see if the domain name of
		# this NS host belongs to a delegated subdomain (explicit or
		# wildcard match).  If so, the 'DiG' program will be called
		# upon to make sure that the domain name can be resolved.
		#
		# Note: There are two basic approaches to matching a domain
		#       name with either a child zone or wildcard:
		#
		#       1. Fetch child zones/wildcards from their respective
		#          hashes until a match is found or there are no more
		#          hash indices to compare.  This is a classic example
		#	   of an N**2 search algorithm which becomes very slow
		#          for a zone with many child zones and/or wildcards.
		#
		#       2. [a] See if the current domain name matches that
		#              of a child zone/wildcard.
		#          [b] If no match is found, strip the first label
		#              from the current domain name and retry step [a]
		#              until either a match is found or the remaining
		#              label(s) match the current zone being audited.
		#
		# The cumulative search time for second method has a linear
		# relationship to the number of child zones/wildcards and,
		# thus, is implemented here.
		#
		$host_labels = $host;
		$host_labels =~ s/\\\\//g;	# only true escapes left behind
		$wildcard_OK = 0;
		while ($host_labels ne "$Domain.") {
		    if (exists($subzones{$host_labels}) &&
			($subzones{$host_labels} <= $wildcard_OK)) {
			#
			# Save the subzone NS host for 'DiG' to look up.
			# This may very well cause missing glue records
			# to be fetched from some other nameserver.
			# A specific check for missing glue in the zone
			# data being analyzed will be made later.
			#
			$extNS{$host} = $location;
			$match = 1;
			last;
		    }
		    if (!$match && $wildcard_OK && exists($Wildcards{$host_labels})) {
			$match = 1;
			$tmp = $Wildcards{$host_labels};
			if ($tmp !~ / A /) {
			    if ($tmp =~ / CNAME /) {
				$warning = "(*) CNAME RR";
			    } elsif ($tmp =~ / MX /) {
				$warning = " (*) MX RR  ";
			    } else {
				$warning = "(*) non-A RR";
			    }
			    $NSlist{$host} = "[$warning]|$NSlist{$host}";
			}
			last;
		    }
		    # Find the leading label, i.e., the characters up to
		    # and including the first unescaped ".", and remove
		    # it from the nameserver domain name prior to the
		    # next attempt at matching a child domain/wildcard.
		    #
		    $host_labels =~ s/(\\[.]|[^.])*\.//;;
		    $wildcard_OK = 1;
		}
	    }
	    if ($match) {
		delete($NSlist{$host}) if !$warning;
	    } else {
		$NSlist{$host} = "[no A record ]|$NSlist{$host}";
	    }
	} else {
	    #
	    # Save the external NS host for 'DiG' to look up later.
	    #
	    $extNS{$host} = $location;
	    delete($NSlist{$host});
	}
    }
    keys(%MXlist);
    while (($host, $location) = each %MXlist) {
	if ($host =~ /$Domainpattern\.$/ || $host eq "$Domain.") {
	    $tmp = $host;
	    $tmp =~ s/$Domainpattern\.$//;
	    $match = $warning = 0;
	    if (exists($Hosts{$tmp})) {
		$match = 1;
	    } elsif (exists($RRowners{$tmp})) {
		$rrtype = $RRowners{$tmp};
		if ($rrtype =~ / A /) {
		    $match = 1;
		} elsif ($rrtype =~ / CNAME /) {
		    $match = $warning = 1;
		    $MXlist{$host} = "[CNAME record]|$MXlist{$host}";
		}
	    }
	    if ((!$match && $wildcards_exist) || $subzones_exist) {
		$host_labels = $host;
		$host_labels =~ s/\\\\//g;
		$wildcard_OK = 0;
		while ($host_labels ne "$Domain.") {
		    if (exists($subzones{$host_labels}) &&
			($subzones{$host_labels} <= $wildcard_OK)) {
			$extMX{$host} = $location;
			$match = 1;
			last;
		    }
		    if (!$match && $wildcard_OK && exists($Wildcards{$host_labels})) {
			$match = 1;
			$tmp = $Wildcards{$host_labels};
			if ($tmp !~ / A /) {
			    if ($tmp =~ / CNAME /) {
				$warning = "(*) CNAME RR";
			    } elsif ($tmp =~ / MX /) {
				$warning = " (*) MX RR  ";
			    } else {
				$warning = "(*) non-A RR";
			    }
			    $MXlist{$host} = "[$warning]|$MXlist{$host}";
			}
			last;
		    }
		    $host_labels =~ s/(\\[.]|[^.])*\.//;;
		    $wildcard_OK = 1;
		}
	    }
	    if ($match) {
		delete($MXlist{$host}) if !$warning;
	    } else {
		$MXlist{$host} = "[no A record ]|$MXlist{$host}";
	    }
	} else {
	    $extMX{$host} = $location;
	    delete($MXlist{$host});
	}
    }
    # Attempt to reconcile the RDATA field of any PTR RRs that
    # were found while scanning a 'spcl' or zone data file.
    #
    keys(%spclPTR);
    while (($host, $location) = each %spclPTR) {
	if ($host =~ /$Domainpattern\.$/ || $host eq "$Domain.") {
	    $tmp = $host;
	    $tmp =~ s/$Domainpattern\.$//;
	    $match = $warning = 0;
	    if (exists($Hosts{$tmp})) {
		$match = 1;
	    } elsif (exists($RRowners{$tmp})) {
		$rrtype = $RRowners{$tmp};
		if ($rrtype =~ / A /) {
		    $match = 1;
		} elsif ($rrtype =~ / CNAME /) {
		    $match = $warning = 1;
		    $spclPTR{$host} = "[CNAME record]|$spclPTR{$host}";
		}
	    }
	    if ((!$match && $wildcards_exist) || $subzones_exist) {
		$host_labels = $host;
		$host_labels =~ s/\\\\//g;
		$wildcard_OK = 0;
		while ($host_labels ne "$Domain.") {
		    if (exists($subzones{$host_labels}) &&
			($subzones{$host_labels} <= $wildcard_OK)) {
			$extPTR{$host} = $location;
			$match = 1;
			last;
		    }
		    if (!$match && $wildcard_OK && exists($Wildcards{$host_labels})) {
			$match = 1;
			$tmp = $Wildcards{$host_labels};
			if ($tmp !~ / A /) {
			    if ($tmp =~ / CNAME /) {
				$warning = "(*) CNAME RR";
			    } elsif ($tmp =~ / MX /) {
				$warning = " (*) MX RR  ";
			    } else {
				$warning = "(*) non-A RR";
			    }
			    $spclPTR{$host} = "[$warning]|$spclPTR{$host}";
			}
			last;
		    }
		    $host_labels =~ s/(\\[.]|[^.])*\.//;;
		    $wildcard_OK = 1;
		}
	    }
	    if ($match) {
		delete($spclPTR{$host}) if !$warning;
	    } else {
		$spclPTR{$host} = "[no A record ]|$spclPTR{$host}";
	    }
	} else {
	    $extPTR{$host} = $location;
	    delete($spclPTR{$host});
	}
    }
    # Attempt to reconcile the TXTDNAME field of any RP RRs that
    # were found while scanning a 'spcl' or zone data file.
    #
    keys(%spclRP);
    while (($host, $location) = each %spclRP) {
	if ($host =~ /$Domainpattern\.$/ || $host eq "$Domain.") {
	    $tmp = $host;
	    $tmp =~ s/$Domainpattern\.$//;
	    $match = $warning = 0;
	    if (exists($RRowners{$tmp})) {
		$rrtype = $RRowners{$tmp};
		if ($rrtype =~ / TXT /) {
		    $match = 1;
		} elsif ($rrtype =~ / CNAME /) {
		    $match = $warning = 1;
		    $spclRP{$host} = "[CNAME record]|$spclRP{$host}";
		}
	    }
	    if ((!$match && $wildcards_exist) || $subzones_exist) {
		$host_labels = $host;
		$host_labels =~ s/\\\\//g;
		$wildcard_OK = 0;
		while ($host_labels ne "$Domain.") {
		    if (exists($subzones{$host_labels}) &&
			($subzones{$host_labels} <= $wildcard_OK)) {
			$extRP{$host} = $location;
			$match = 1;
			last;
		    }
		    if (!$match && $wildcard_OK && exists($Wildcards{$host_labels})) {
			$match = 1;
			$tmp = $Wildcards{$host_labels};
			if ($tmp !~ / TXT /) {
			    if ($tmp =~ / CNAME /) {
				$warning = "(*) CNAME RR";
			    } elsif ($tmp =~ / MX /) {
				$warning = " (*) MX RR  ";
			    } else {
				$warning = "(*)nonTXT RR";
			    }
			    $spclRP{$host} = "[$warning]|$spclRP{$host}";
			}
			last;
		    }
		    $host_labels =~ s/(\\[.]|[^.])*\.//;;
		    $wildcard_OK = 1;
		}
	    }
	    if ($match) {
		delete($spclRP{$host}) if !$warning;
	    } else {
		$spclRP{$host} = "[ no TXT RR  ]|$spclRP{$host}";
	    }
	} else {
	    $extRP{$host} = $location;
	    delete($spclRP{$host});
	}
    }
    # Attempt to reconcile the RDATA field of any CNAMEs that
    # were found while scanning a 'spcl' or zone data file.
    #
    keys(%spclCNAME);
    while (($host, $location) = each %spclCNAME) {
	if ($host =~ /$Domainpattern\.$/ || $host eq "$Domain.") {
	    $tmp = $host;
	    $tmp =~ s/$Domainpattern\.$//;
	    $match = 0;
	    if (exists($Hosts{$tmp}) || exists($RRowners{$tmp})) {
		$match = 1;
	    }
	    if ((!$match && $wildcards_exist) || $subzones_exist) {
		$host_labels = $host;
		$host_labels =~ s/\\\\//g;
		$wildcard_OK = 0;
		while ($host_labels ne "$Domain.") {
		    if (exists($subzones{$host_labels}) &&
			($subzones{$host_labels} <= $wildcard_OK)) {
			$extCNAME{$host} = $location;
			$match = 1;
			last;
		    }
		    if (!$match && $wildcard_OK && exists($Wildcards{$host_labels})) {
			$match = 1;
			last;
		    }
		    $host_labels =~ s/(\\[.]|[^.])*\.//;;
		    $wildcard_OK = 1;
		}
	    }
	    if ($match) {
		delete($spclCNAME{$host});
	    } else {
		$spclCNAME{$host} = "[ no such RR ]|$spclCNAME{$host}";
	    }
	} else {
	    $extCNAME{$host} = $location;
	    delete($spclCNAME{$host});
	}
    }

    $answer = keys(%extNS) + keys(%extMX) + keys(%extPTR) + keys(%extRP) +
	      keys(%extCNAME);
    if ($answer) {
	#
	# Use 'DiG' to to find out if the external domain names refer
	# to CNAMEs, lack Address records, or if they even exist.
	#
	$i = "00";
	$chain_length = $max_chain_length = 0;
	$DiG_batch = "/tmp/h2n-dig.bat${i}_$domain";
	unless (&OPEN(*DIGBATCH, "> $DiG_batch")) {
	    print STDERR "Couldn't create batch file for 'DiG': $!\nUnable to check external domain names.\n";
	    %extNS = %extMX = %extPTR = %extRP = %extCNAME = ();
	} else {
	    while (($host, $tmp) = each %extNS) {
		print DIGBATCH "$host A \%NS\n";
	    }
	    while (($host, $tmp) = each %extMX) {
		print DIGBATCH "$host A \%MX\n";
	    }
	    while (($host, $tmp) = each %extPTR) {
		print DIGBATCH "$host A \%PTR\n";
	    }
	    while (($host, $tmp) = each %extRP) {
		print DIGBATCH "$host TXT \%RP\n";
	    }
	    while (($host, $tmp) = each %extCNAME) {
		print DIGBATCH "$host ANY \%CNAME\n";
	    }
	    &CLOSE(*DIGBATCH);

	    print STDOUT "(processing $answer queries for out-of-zone domains)\n" if $answer >= $query_rpt_threshold;
	    if ($debug) {
		$query_options = "";
	    } else {
		$query_options = " +noauthor +noaddit +nostats";
		$debug_out = "";
	    }
	    $query_pattern = $query_options;
	    $query_pattern =~ s/[+]/\\+/g;
	    while (-s $DiG_batch) { 
		#
		# Call DiG to process the original batch file plus any
		# subsequent batch files that get generated as a result
		# of following chained CNAMEs.
		# 
		$debug_out = "tee /tmp/h2n-dig.ans${i}_$domain |" if $debug;
		$prog_output = -1;
		&OPEN(*DIGOUT, "dig $query_options -f $DiG_batch 2>&1 | $debug_out");
		$i++;
		$chain_length++;
		$nextbatch = "/tmp/h2n-dig.bat${i}_$domain";
		unless (&OPEN(*NEXTBATCH, "> $nextbatch")) {
		    $nextbatch_is_open = 0;
		} else {
		    $nextbatch_is_open = 1;
		}
		$rrtype = $saved_cname = "";
		while (<DIGOUT>) {	# Almost always true at least once.
		    $prog_output++;	# If > 0, then 'dig' was able to be run.
		    next if /^$/;
		    chop;
		    if (/^; <<>> DiG \d+\.\d+.* <<>>($query_pattern)? (\S+) (AN?Y?|TXT) %(NS|MX|PTR|RP|CNAME)=?(\S+)?/) {
			$host = $2;
			$rrtype = $4;
			$cname = $5;
			if ($rrtype eq 'CNAME') {
			    #
			    # Deal with the various work-arounds that this
			    # program has to make in order to accommodate a
			    # small internal buffer size in DiG version 8.2
			    # and earlier whereby it overruns the buffer when
			    # reading a line from a batch file that exceeds
			    # 100 characters in length.
			    #
			    if ($chain_length == 1) {
				#
				# When the original batch file is created,
				# the starting CNAME ($cname) is identical
				# to the domain name to be queried ($host).
				# To save buffer space, the redundant field
				# is not written to the batch file.
				#
				$cname = $host;
			    } elsif ($host eq '<NO-OP>') {
				#
				# When subsequent batch files are created to
				# follow chained CNAMEs, it may be necessary
				# to split the batch file entry into two lines.
				# A dummy name is assigned to "$host" followed
				# by the real base CNAME in its expected field.
				#
				$saved_cname = $cname;
			    } elsif ($saved_cname) {
				#
				# This section is reached when the second line
				# of the split-line work-around is reached.
				# The "$host" variable contains the chained
				# CNAME to be queried while the "$cname"
				# variable has just been set to null.
				# Restore the value of the base CNAME from the
				# prior input line to complete the work-around.
				#
				$cname = $saved_cname;
				$saved_cname = "";
			    }
			}
			$status = $result = "";
			$query_section = $first_answer = 0;
			next;
		    } elsif (/^;.+(connection |no route|unreachable)/i) {
			#
			# Check the "$rrtype" variable as a precaution against
			# accidentally overwriting the result of the last
			# successful lookup in case synchronization is lost.
			# This step is necessary in order to accommodate
			# versions 9.0.1 of DiG which fail to echo the command
			# line when encountering any type of connection failure.
			#
			next if !$rrtype;
			s/.+(timed out).*/ $1  /;
			s/.+(refused).*/  $1   /;
			s/.+(no route).+/  $1  /;
			s/.+(unreachable).+/$1 /;
			$result = $_;
			#
			# Instead of getting the next line, we'll fall through
			# to the section where a non-null "$result" variable
			# is processed and throw in the towel for this batch
			# file entry.  It's pointless and likely futile to
			# wait for the sanity check in the Query Section.
			#
		    } elsif (/^;.+HEADER.+opcode: QUERY, status: ([^,]+)/) {
			$status = $1;
			if ($status ne 'NOERROR') {
			    $status = " $status " if length($status) == 6;
			    $status .= " " if length($status) < 8;
			    $result = "  $status  ";
			    $status = "";
			}
			next;		# trust nothing until the Query Section
		    } elsif ($status && /^;.+flags: (.*); QUE.*, ANS[^:]*: (\d+), AUTH[^:]*: (\d+), ADDIT[^:]*: (\d+)/i) {
			$flags = $1;
			$answer = $2;
			$authority = $3;
			$additional = $4;
			if ($answer == 0) {
			    if ($flags !~ /aa/ && $authority == 0 && $additional == 0) {
				#
				# We've probably queried a nameserver that is
				# lame due to either bad delegation or it has
				# invalidated a zone due to bad data (although
				# SERVFAIL would be the expected response for
				# the latter case).
				#
				$result = "failed query";
			    } else {
				if ($rrtype =~ /^(NS|MX|PTR)$/) {
				    $result = "no A record ";
				} elsif ($rrtype eq 'RP') {
				    $result = " no TXT RR  ";
				} else {
				    #
				    # Hmm.  The domain name must exist since
				    # the status of the query is NOERROR and
				    # yet there are no answers for ANY resource
				    # records.  This happens when querying for
				    # an undelegated subdomain, e.g.,
				    #
				    #  a.foo.test.com   exists as a RR in the
				    #                   'test.com' zone
				    #  foo.test.com     does *not* exist as a RR
				    #
				    # We would be here if a query were made for
				    # the domain name 'foo.test.com'.
				    #
				    $result = "no RR match ";
				}
			    }
			    $status = "";
			} else {
			    #
			    # Prepare for the Answer Section once we pass
			    # the sanity check in the Query Section.
			    #
			    $first_answer = 1;
			}
			next;		# trust nothing until the Query Section
		    } elsif (/^;; QUE.+:$/) {
			$query_section = 1;
			next;
		    }
		    next if !$rrtype || $host eq '<NO-OP>';
		    if ($query_section && /^;;?\s*(\S+)\s+/) {
			$tmp = $1;
			$query_section = 0;
			$tmp =~ s/,$/./;	# For DiG versions through 8.X.
			if ($tmp ne $host) {
			    #
			    # The domain name that was passed to DiG in the
			    # batch file and which appears on the echoed
			    # command line is *not* what DiG actually used 
			    # when making its query.  A line in the batch
			    # file that exceeded "$DiG_buffer_size" in length
			    # is the likely source of DiG's buffer overrun.
			    # All we can do now is report what occurred and
			    # then resynchronize on the next line from the
			    # batch input file.
			    #
			    $result = "buffer error";
			    $status = "";
			    $first_answer = 0;
			    if ($chain_length > 1 && exists($baseCNAME{$host})) {
				#
				# Recover the "$cname" field that was most
				# likely trashed due to the buffer overrun.
				#
				$cname = $baseCNAME{$host};
			    }
			}
		    }
		    if ($chain_length > 1) {
			#
			# The backup copy of the base CNAME is no longer needed.
			# Remove the chained CNAME key for possible re-use.
			#
			delete($baseCNAME{$host});
		    }
		    if ($result) {
			if ($rrtype eq 'NS') {
			    $extNS{$host} = "[$result]|$extNS{$host}";
			} elsif ($rrtype eq 'MX') {
			    $extMX{$host} = "[$result]|$extMX{$host}";
			} elsif ($rrtype eq 'PTR') {
			    $extPTR{$host} = "[$result]|$extPTR{$host}";
			} elsif ($rrtype eq 'RP') {
			    $extRP{$host} = "[$result]|$extRP{$host}";
			} else {
			    if ($chain_length == 1) {
				#
				# Despite the name of the control variable,
				# there's no CNAME chain here; a bigger
				# problem has caused us to bail out early.
				#
				$extCNAME{$cname} = "[$result]|$extCNAME{$cname}";
			    } elsif (exists($extCNAME{$cname})) {
				#
				# The reason we tested for the existence of
				# "$cname" as a hash key is that we might
				# not have been able to recover the backup
				# copy after a buffer overrun was detected.
				#
				$extCNAME{$cname} =~ s/\[.+\]/[$result]/;
				$max_chain_length = $chain_length;
			    }
			}
			# Setting "$rrtype" to null after detecting either a
			# buffer overrun or a non-NOERROR query status will
			# effectively ignore DiG's output until the next echoed
			# DiG command in the output stream.  This effectively
			# allows a resynchronization to take place and prevents
			# the accidental overwriting of valid data.
			#
			$rrtype = "" if $result eq "buffer error" || !$status;
			$result = "";
		    }
		    next if !$status;
		    if ($first_answer && /^[^;]/) {
			if (/\sCNAME\s+(\S+)$/) {
			    $cname_n = lc($1);
			    if ($rrtype eq 'NS') {
				$extNS{$host} = "[CNAME record]|$extNS{$host}";
			    } elsif ($rrtype eq 'MX') {
				$extMX{$host} = "[CNAME record]|$extMX{$host}";
			    } elsif ($rrtype eq 'PTR') {
				$extPTR{$host} = "[CNAME record]|$extPTR{$host}";
			    } elsif ($rrtype eq 'RP') {
				$extRP{$host} = "[CNAME record]|$extRP{$host}";
			    } else {
				#
				# Follow the CNAME chain.
				#
				$cname_loop = 0;
				if ($chain_length == 1) {
				    #
				    # Regardless of one's opinion about CNAME
				    # chains being bad practice or a feature,
				    # record this status as well as the current
				    # chain length until final resolution can
				    # be determined.  This program's default
				    # behavior is to treat chained CNAMEs as
				    # no problem as long as a non-CNAME RR is
				    # ultimately resolved.
				    #
				    $extCNAME{$cname} = "[CNAME chain ](1)|$extCNAME{$cname}";
				    $chainCNAME{$cname} = [ $cname ];
				}
				for ($k = 0; $k < $chain_length; $k++) {
				    #
				    # Before rushing off to follow the latest
				    # CNAME, make sure it's not one that we've
				    # already encountered in the current chain.
				    #
				    if ($chainCNAME{$cname}[$k] eq $cname_n) {
					$extCNAME{$cname} =~ s/^[^\|]+/[ CNAME loop ]\($chain_length\)/;
					$cname_loop = 1;
					last;
				    }
				}
				#
				# Regardless of whether or not we've discovered
				# a CNAME loop, maintain accuracy by updating
				# the chain length of the base CNAME and add
				# the latest CNAME to the list of chain members.
				#
				$extCNAME{$cname} =~ s/ \]\(\d+\)/ \]\($chain_length\)/;
				push @{ $chainCNAME{$cname} }, $cname_n;
				if ($nextbatch_is_open && !$cname_loop) {
				    #
				    # Find out where the chained CNAME leads us
				    # by creating the appropriate entry in the
				    # pending DiG batch file.
				    #
				    $buffer = "$cname_n ANY \%CNAME=$cname\n";
				    if (length($buffer) > $DiG_buffer_size) {
					#
					# Avoid creating a buffer overrun in
					# DiG by splitting the necessary data
					# across two lines of batch input.
					#
					print NEXTBATCH "<NO-OP> ANY \%CNAME=$cname\n";
					print NEXTBATCH "$cname_n ANY \%CNAME\n";
				    } else {
					print NEXTBATCH $buffer;
					#
					# If a buffer overrun occurs despite
					# our efforts, the field most likely
					# to be trashed is the one holding the
					# value of "$cname".  As an exercise in
					# paranoid programming, we'll create
					# a reverse-lookup hash as a way to
					# recover the base CNAME in case our
					# fears are realized.
					#
					if (!exists($baseCNAME{$cname_n})) {
					    $baseCNAME{$cname_n} = $cname;
					} else {
					    #
					    # No can do.  Another base CNAME
					    # has already submitted a query to
					    # the current batch file for the
					    # same chained CNAME.  Not only do
					    # we have to give up the current
					    # backup attempt, the original base
					    # CNAME that got here first must
					    # also abandon its backup copy.
					    #
					    $baseCNAME{$cname_n} = "";
					}
				    }
				} else {
				    #
				    # The buck just stopped.
				    # Update the cosmetic control variable.
				    #
				    $max_chain_length = $chain_length;
				}
			    }
			} else {
			    #
			    # All is well - either an Address RR exists for the
			    # object of the NS, MX, or PTR RR or the CNAME RR
			    # points to something other than another CNAME.
			    #
			    if ($rrtype eq 'NS') {
				delete($extNS{$host});
			    } elsif ($rrtype eq 'MX') {
				delete($extMX{$host});
			    } elsif ($rrtype eq 'PTR') {
				delete($extPTR{$host});
			    } elsif ($rrtype eq 'RP') {
				delete($extRP{$host});
			    } elsif ($show_chained_cnames && $extCNAME{$cname} =~ /\]\(\d+\)/) {
				$max_chain_length = $chain_length;
			    } else {
				delete($extCNAME{$cname});
			    }
			}
			$first_answer = 0;	# Ignore remaining output until
			$rrtype = $status = "";	# the next batch entry is read.
		    }
		}
		&CLOSE(*NEXTBATCH) if $nextbatch_is_open;

		# The loop that follows chained CNAMEs will exhaust itself
		# naturally or when the 99th iteration is reached.
		#
		unlink($nextbatch) if -z $nextbatch;
		if ($chain_length == 99) {
		    unlink($nextbatch) if !$debug;
		    $nextbatch = "";
		    $max_chain_length = 99;
		}
		&CLOSE(*DIGOUT);
		if ($prog_output <= 0) {
		    print STDERR "Can't find or run the 'DiG' program - unable to check external domain names.\n";
		    %extNS = %extMX = %extPTR = %extRP = %extCNAME = ();
		    unlink($nextbatch) if !$debug;
		    $nextbatch = "";
		}
		unlink($DiG_batch) if !$debug;
		$DiG_batch = $nextbatch;
	    }
	}
    }

    # Ideally, all of the hashes should be empty.
    # If not, here's where the warnings get reported.
    # Note: The 'if' tests are structured to prevent short-circuiting so
    #       that there are no passed-over calls to the 'keys' function
    #       for initializing the iterator of each hash.
    #
    if ((keys(%NSlist) + keys(%extNS))) {
	print STDERR "${n}Warning: found NS RR(s) pointing to the following problematic domain name(s):\n";
	$n = "";
	while (($host, $tmp) = each %NSlist) {
	    #
	    # If DiG generates output that is unexpected in either content
	    # or sequence and it is detected by the 'h2n' output parser,
	    # the value of the hash index may lack the assignment of the
	    # status field as the parser attempts to resynchronize.  
	    # In such cases, assign a generic status field that indicates
	    # roughly what happened.
	    #
	    $tmp = "[ DiG error! ]|$tmp" if $tmp !~ /\|/;
	    ($status, $location) = split('\|', $tmp);
	    $location = "" if $verify_mode && $location ne "(SOA MNAME)";
	    printf STDERR "%s%s  %s\n", &TAB(" $host", $t), $status, $location;
	}
	while (($host, $tmp) = each %extNS) {
	    $tmp = "[ DiG error! ]|$tmp" if $tmp !~ /\|/;
	    ($status, $location) = split('\|', $tmp);
	    $location = "" if $verify_mode && $location ne "(SOA MNAME)";
	    printf STDERR "%s%s  %s\n", &TAB(" $host", $t), $status, $location;
	}
    }
    if ((keys(%MXlist) + keys(%extMX))) {
	print STDERR "${n}Warning: found MX RR(s) pointing to the following problematic domain name(s):\n";
	$n = "";
	while (($host, $tmp) = each %MXlist) {
	    $tmp = "[ DiG error! ]|$tmp" if $tmp !~ /\|/;
	    ($status, $location) = split('\|', $tmp);
	    $location = "" if $verify_mode;
	    printf STDERR "%s%s  %s\n", &TAB(" $host", $t), $status, $location;
	}
	while (($host, $tmp) = each %extMX) {
	    $tmp = "[ DiG error! ]|$tmp" if $tmp !~ /\|/;
	    ($status, $location) = split('\|', $tmp);
	    $location = "" if $verify_mode;
	    printf STDERR "%s%s  %s\n", &TAB(" $host", $t), $status, $location;
	}
    }
    if ((keys(%spclPTR) + keys(%extPTR))) {
	print STDERR "${n}Warning: found PTR RR(s) pointing to the following problematic domain name(s):\n";
	$n = "";
	while (($host, $tmp) = each %spclPTR) {
	    $tmp = "[ DiG error! ]|$tmp" if $tmp !~ /\|/;
	    ($status, $location) = split('\|', $tmp);
	    $location = "" if $verify_mode;
	    printf STDERR "%s%s  %s\n", &TAB(" $host", $t), $status, $location;
	}
	while (($host, $tmp) = each %extPTR) {
	    $tmp = "[ DiG error! ]|$tmp" if $tmp !~ /\|/;
	    ($status, $location) = split('\|', $tmp);
	    $location = "" if $verify_mode;
	    printf STDERR "%s%s  %s\n", &TAB(" $host", $t), $status, $location;
	}
    }
    if ((keys(%spclRP) + keys(%extRP))) {
	print STDERR "${n}Warning: found RP RR(s) pointing to the following problematic TXT domain(s):\n";
	$n = "";
	while (($host, $tmp) = each %spclRP) {
	    $tmp = "[ DiG error! ]|$tmp" if $tmp !~ /\|/;
	    ($status, $location) = split('\|', $tmp);
	    $location = "" if $verify_mode;
	    printf STDERR "%s%s  %s\n", &TAB(" $host", $t), $status, $location;
	}
	while (($host, $tmp) = each %extRP) {
	    $tmp = "[ DiG error! ]|$tmp" if $tmp !~ /\|/;
	    ($status, $location) = split('\|', $tmp);
	    $location = "" if $verify_mode;
	    printf STDERR "%s%s  %s\n", &TAB(" $host", $t), $status, $location;
	}
    }
    if ((keys(%spclCNAME) + keys(%extCNAME))) {
	print STDERR "${n}Warning: found CNAME(s) pointing to the following problematic domain name(s):\n";
	print STDERR "(numbers within parentheses represent the length of a CNAME chain)\n" if $max_chain_length;
	$n = "";
	if (!$max_chain_length || $verify_mode) {
	    $i = "";
	} else {
	    $i = ($max_chain_length < 10) ? "   " : "    ";
	}
	while (($host, $tmp) = each %spclCNAME) {
	    $tmp = "[ DiG error! ]|$tmp" if $tmp !~ /\|/;
	    ($status, $location) = split('\|', $tmp);
	    $status .= ($status =~ /\)$/) ? "" : $i;
	    $location = "" if $verify_mode;
	    printf STDERR "%s%s  %s\n", &TAB(" $host", $t), $status, $location;
	}
	while (($host, $tmp) = each %extCNAME) {
	    $tmp = "[ DiG error! ]|$tmp" if $tmp !~ /\|/;
	    ($status, $location) = split('\|', $tmp);
	    $status .= ($status =~ /\)$/) ? "" : $i;
	    $location = "" if $verify_mode;
	    printf STDERR "%s%s  %s\n", &TAB(" $host", $t), $status, $location;
	    if ($show_chained_cnames && $status =~ /\]\((\d+)\)/) {
		$chain_length = $1;
		for ($k = 1; $k <= $chain_length; $k++) {
		    print STDERR "  > $chainCNAME{$host}[$k]\n";
		}
	    }

	}
    }

    if (keys(%NSdata)) {
	#
	# Check that each zone has at least two listed nameservers
	# and, if so, that the TTLs of the NS RRset are consistent.
	# While we're at it, nameservers in child zones will trigger
	# a check for necessary glue in this, the parent zone.
	#
	$last_zone = "";
	while (($zone, $buffer) = each %NSdata) {
	    #
	    # Cycle through each zone.
	    #
	    $ns_count = 0;
	    $status = 1;
	    while ($buffer) {
		#
		# For each zone, cycle through each listed nameserver.
		#
		($ttl, $host, $buffer) = split(' ', $buffer, 3);
		$ns_count++;
		if ($ns_count == 1) {
		    $answer = $ttl;
		} elsif ($status) {
		    if ($ttl != $answer) {
			$NSrfc2181{$zone} = 1;
			$status = 0;
		    }
		}
		if ($host =~ /$Domainpattern\.$/) {
		    #
		    # Check for missing glue records.  The "$Domainpattern"
		    # variable is used because it will match on a nameserver
		    # in any child domain (thus requiring glue), not just the
		    # child domain currently being examined.
		    #
		    # Be advised that your mileage may vary when this section
		    # is working in verify mode.  That's because a nameserver
		    # that started out with missing glue may have fetched the
		    # missing A records from some other nameserver during the
		    # course of a recursive query.  Fetched glue is included
		    # in a zone transfer unless requested from a nameserver
		    # that has been configured with the "recursion no" *and*
		    # "fetch-glue no" options.
		    #
		    $host_labels = $host;
		    $host_labels =~ s/\\\\//g;
		    $wildcard_OK = 0;
		    while ($host_labels ne "$Domain.") {
			if (exists($subzones{$host_labels}) &&
			    ($subzones{$host_labels} <= $wildcard_OK)) {
			    #
			    $uqdn = $host;
			    $uqdn =~ s/$Domainpattern\.$//;
			    $match = 0;
			    if (exists($Hosts{$uqdn})) {
				$match = 1;
			    } elsif (exists($RRowners{$uqdn})) {
				$match = 1 if $RRowners{$uqdn} =~ / A /;
			    } elsif ($wildcard_OK && exists($Wildcards{$host_labels})) {
				$match = 1 if $Wildcards{$host_labels} =~ / A /;
			    }
			    if (!$match) {
				if (!$last_zone) {
				    print STDERR "${n}Warning: found NS RR(s) to be missing the requisite glue record(s):\n";
				    $n = "";
				}
				if ($zone ne $last_zone) {
				    $last_zone = $zone;
				    $authority = ($zone eq "$Domain.") ? "\@" : $zone;
				    $authority =~ s/$Domainpattern\.$//;
				    printf STDERR "%s\t%s\tIN NS\t%s\n", &TAB(" $authority", 16), $ttl, $uqdn;
				} else {
				    printf STDERR "%s\t%s\tIN NS\t%s\n", &TAB(" ", 16), $ttl, $uqdn;
				}
			    }
			    last;
			} else {
			    $host_labels =~ s/(\\[.]|[^.])*\.//;;
			    $wildcard_OK = 1;
			}
		    }
		}
	    }
	    $NSrfc1034{$zone} = 1 if $ns_count == 1;
	}

	if (keys(%NSrfc1034)) {
	    print STDERR "${n}Warning: found zone(s) not having at least two listed nameservers (RFC-1034):\n";
	    $n = "";
	    while (($zone, $tmp) = each %NSrfc1034) {
		($ttl, $host) = split(' ', $NSdata{$zone});
		$authority = ($zone eq "$Domain.") ? "\@" : $zone;
		$authority =~ s/$Domainpattern\.$//;
		$uqdn = $host;
		$uqdn =~ s/$Domainpattern\.$//;
		printf STDERR "%s\t%s\tIN NS\t%s\n", &TAB(" $authority", 16), $ttl, $uqdn;
	    }
	}
	if (keys(%NSrfc2181)) {
	    print STDERR "${n}Warning: found NS RRset(s) with inconsistent TTL values (RFC-2181):\n";
	    $n = "";
	    while (($zone, $tmp) = each %NSrfc2181) {
		$buffer = $NSdata{$zone};
		$ns_count = 1;
		while ($buffer) {
		    ($ttl, $host, $buffer) = split(' ', $buffer, 3);
		    $uqdn = $host;
		    $uqdn =~ s/$Domainpattern\.$//;
		    if ($ns_count == 1) {
			$authority = ($zone eq "$Domain.") ? "\@" : $zone;
			$authority =~ s/$Domainpattern\.$//;
			printf STDERR "%s\t%s\tIN NS\t%s\n", &TAB(" $authority", 16), $ttl, $uqdn;
		    } else {
			printf STDERR "%s\t%s\tIN NS\t%s\n", &TAB(" ", 16), $ttl, $uqdn;
		    }
		    $ns_count++;
		}
	    }
	}
    }

    if ($n) {
	return 0;
    } else {
	print STDERR "\n" if !$verify_mode;
	return 1;
    }
}


#
# Do some basic sanity checks on the SOA timer values.  These checks
# are the same ones that BIND performs when a zone is loaded.
#
# Return values:
#   0 = no warnings
#   1 = warnings
#
sub CHECK_SOA_TIMERS {
    my ($refresh, $retry, $expire, $ttl, $message);

    # If the READ_RRs subroutine detected a bad or missing SOA timer,
    # there is no point in proceeding.  Exit with a successful return
    # code since the error has already been reported.
    #
    return 0 if !$valid_SOA_timers;

    $refresh = ($Refresh) ? &SECONDS($Refresh) : &SECONDS($DefRefresh);
    $retry   = ($Retry) ? &SECONDS($Retry) : &SECONDS($DefRetry);
    $expire  = ($Expire) ? &SECONDS($Expire) : &SECONDS($DefExpire);
    if ($Ttl) {
	$ttl = &SECONDS($Ttl);
    } else {
	$ttl = ($rfc2308) ? &SECONDS($DefNegCache) : &SECONDS($DefTtl);
    }
    $message = "";

    if ($expire < ($refresh + $retry)) {
	$message = " SOA expire value is less than SOA refresh + retry\n   ($Expire < $Refresh + $Retry)";
    }
    if ($expire < ($refresh + (10 * $retry))) {
	$n = ($message) ? ".\n" : "";
	$message .= "$n SOA expire value is less than SOA refresh + (10 * retry)\n   ($Expire < $Refresh + (10 * $Retry))";
    }
    if ($expire < (7 * 24 * 3600)) {
	$n = ($message) ? ".\n" : "";
	$message .= "$n SOA expire value ($Expire) is less than 7 days";
    }
    if ($expire > (183 * 24 * 3600)) {
	$n = ($message) ? ".\n" : "";
	$message .= "$n SOA expire value ($Expire) is greater than 6 months";
    }
    if ($refresh < (2 * $retry)) {
	$n = ($message) ? ".\n" : "";
	$message .= "$n SOA refresh value is less than SOA retry * 2 ($Refresh < ($Retry * 2))";
    }
    if (!$verify_mode && $rfc2308 && $ttl > 10800) {
	$n = ($message) ? ".\n" : "";
	$message .= "$n SOA negative cache value ($Ttl) exceeds recommended maximum of 3 hours";
    }
    if ($message) {
	if ($verify_mode) {
	    print STDERR "\nWarning: found the following problematic SOA time interval(s):\n";
	} else {
	    print STDERR "Warning: the -o/+t option values generated the following message(s):\n";
	}
	print STDERR "$message.\n";
	return 1;
    } else {
	return 0;
    }
}


#
# Perform various consistency checks on zone data for each domain
# specified with the -v option.  The zone transfer data must first
# be processed by the "READ_RRs" subroutine before this one is called.
# Checks are made for the following conditions:
#   * SOA records containing time specifications with extreme values
#     (via the "CHECK_SOA_TIMERS" subroutine).
#   * NS, MX, and PTR records that point to CNAMEs or domain names
#     with no Address records or to nonexistent domain names (via the
#     "AUDIT_RRs" subroutine).
#   * CNAME records that point to nonexistent domain names, i.e.,
#     "dangling" CNAMEs (via the "AUDIT_RRs" subroutine).
#   * Zones with only one listed nameserver (violates RFC-1034),
#     NS RRsets with inconsistent TTL values (violates RFC-2181),
#     and NS RRs with missing glue (via the "AUDIT_RRs" subroutine).
#   * Lame delegations and nameservers that are not running
#     or are unresponsive.  Accomplished with the 'check_del'
#     program but only if delegation checking is not purposely
#     disabled by specifying the '-no-check-del' option.
#
# NOTE: In order to not adversely affect the amount of time that
#       'h2n' takes in its normal task of generating zone data,
#       future consistency checks should be limited to the -v option by
#       placing them into this subroutine instead of "AUDIT_RRs".
#
# Return values:
#   0 = no warnings
#   1 = warnings
#
sub VERIFY_ZONE {
    my ($warning_status) = @_;
    my ($domain, $del_batch, $location, $host, $buffer, $ttl, $n);
    my ($prog_output, $answer, $first_answer, $debug_out, $i, $t);
    my (%check_del_RRs, @zone_rrset, @sorted_rrset, $ns_data, $mismatch);

    if ($Domain) {
	$domain = $Domain;
    } else {
	$domain = ".";
    }

    # First do some sanity checks on the SOA timer values and then call
    # the "AUDIT_RRs" subroutine so that the %NSlist and %extNS data
    # structures can be referenced.  These hashes are needed to prepare
    # an appropriate list of nameservers for which proper delegation is
    # to be checked.
    # 
    $n = (&AUDIT_RRs(&CHECK_SOA_TIMERS)) ? "" : "\n";

    # Per RFC-1034, the NS RRsets that surround a zone cut are required
    # to be kept consistent.  We will now check for this by comparing
    # the NS RRset from the original DNS query (ostensibly above the zone
    # cut) to the NS RRset obtained from the zone transfer data, i.e.,
    # below the zone cut).  Our mileage may vary, however, because the
    # nameserver(s) that supplied the answer to the recursive DNS query
    # may have discovered and replaced the less credible NS RRset above
    # the zone cut with the authoritative (and thus more credible) NS
    # RRset from the zone itself.
    # 
    #
    $ns_data = $NSdata{"$Domain."};
    $ns_data =~ s/ \d+ / /g;
    @zone_rrset = split(' ', $ns_data);
    @sorted_rrset = sort { $a cmp $b } @zone_rrset;
    @zone_rrset = @sorted_rrset;
    @sorted_rrset = sort { $a cmp $b } @DNSrrset;
    @DNSrrset = @sorted_rrset;
    $mismatch = 0;
    if (scalar(@DNSrrset) != scalar(@zone_rrset)) {
	$mismatch = 1;
    } else {
	for ($i = 0; $i < scalar(@DNSrrset); $i++) {
	    if ($DNSrrset[$i] ne $zone_rrset[$i]) {
		$mismatch = 1;
		last;
	    }
	}
    }
    if ($mismatch) {
	print STDERR "${n}Warning: found inconsistent NS RRsets surrounding the zone boundary (RFC-1034):\n";
	$n = "";
	$t = (length(" $Domain.") <= 20) ? 16 : 24;
	for ($i = 0; $i < @DNSrrset; $i++) {
	    if ($i == 0) {
		printf STDERR "%s\tIN NS\t%s\n", &TAB(" $Domain.", $t), $DNSrrset[$i];
	    } else {
		printf STDERR "%s\tIN NS\t%s\n", &TAB(" ", $t), $DNSrrset[$i];
	    }
	}
	print STDERR " (non-authoritative)\n";
	print STDERR " ---------------------------- zone cut ----------------------------\n";
	print STDERR " (  authoritative  )\n";
	for ($i = 0; $i < @zone_rrset; $i++) {
	    if ($i == 0) {
		printf STDERR "%s\tIN NS\t%s\n", &TAB(" \@", $t), $zone_rrset[$i];
	    } else {
		printf STDERR "%s\tIN NS\t%s\n", &TAB(" ", $t), $zone_rrset[$i];
	    }
	}
    }

    if (keys(%NSdata) && $verify_delegations) {
	#
	# First, create the input file that will
	# be needed by the 'check_del' program.
	#
	$del_batch = "/tmp/h2n-del.bat_$domain";
	unless (open(*DELBATCH, "> $del_batch")) {
	    print STDERR "Couldn't create batch file for 'check_del': $!\nUnable to verify nameserver delegations.\n";
	    $verify_delegations = 0;
	} else {
	    #
	    # Be thorough by also checking the NS RRset
	    # of the parent domain's delegation.
	    #
	    for ($i = 0; $i < @DNSrrset; $i++) {
		print DELBATCH "$Domain.\t\t\tIN NS\t$DNSrrset[$i]\n";
		$check_del_RRs{"$Domain."} .= " $DNSrrset[$i] ";
	    }
	    while (($location, $buffer) = each %NSdata) {
		#
		# Cycle through each zone.
		#
		while ($buffer) {
		    #
		    # For each zone, cycle through each listed nameserver.
		    #
		    ($ttl, $host, $buffer) = split(' ', $buffer, 3);
		    if ((!exists($NSlist{$host}) || $NSlist{$host} =~ /CNAME/) &&
			(!exists($extNS{$host}) || $extNS{$host} =~ /CNAME/)) {
			#
			# Only check nameservers that have a reasonable
			# expectation of being found.
			#
			if (!exists($check_del_RRs{$location}) ||
			    $check_del_RRs{$location} !~ / $host /) {
			    #
			    # Avoid duplicate NS RRs for 'check_del' to process.
			    #
			    print DELBATCH "$location\t\t$ttl\tIN NS\t$host\n";
			    $check_del_RRs{$location} .= " $host ";
			}
		    }
		}
	    }
	    close(*DELBATCH);
	}

	if ($verify_delegations && -s $del_batch) {
	    $prog_output = 0;
	    $first_answer = $answer = 1;
	    if ($debug) {
		$debug_out = "tee /tmp/h2n-del.ans_$domain |";
	    } else {
		$debug_out = "";
	    }
	    #
	    # Use the '-F' (Fast) argument of 'check_del'.
	    # Otherwise, you'll be sorry (and bored) when it
	    # trudges through a large list of unresponsive servers.
	    #
	    open(*DELOUT, "check_del -F -v -f $del_batch 2>&1 | $debug_out");
	    while (<DELOUT>) {			# 99.999% true at least once.
		last if /sh: check_del:/;	# If error, flag stays false.
		$prog_output = 1;
		next if /^$|^Skipping | is authoritative | (moved|put).* list/;
		$answer = 0 if /^\s*\d/;	# Ignore everything past the
						# "proper" & "improper" summary.
		#
		# If this point is reached, then 'check_del' has found
		# something noteworthy to report.
		#
		if ($answer) {
		    if ($first_answer) {
			print STDERR "${n}Warning: verifying the NS delegations generated the following error(s):\n";
			$n = "";
			$first_answer = 0;
		    }
		    print STDERR " $_";
		}
	    }
	    close(*DELOUT);
	    if (!$prog_output) {
		print STDERR "Can't find or run the 'check_del' program - unable to verify NS delegations.\n";
	    }
	    unlink($del_batch) if !$debug;
	}
    }

    $n = "Verification completed.\n" if $n;
    if ($warning_status || !$n) {
	print STDERR "$n\n";
	return 1;
    } else {
	return 0;
    }
}


sub PARSEARGS {
    my @args = @_;
    my ($i, $net, $subnetmask, $option, $tmp1, $tmp2, $AltDb, $AltSpcl);
    my ($file, @targs, $ExpectDomain, @sorted, @unsorted, %duplicate);
    my ($tmpTtl, $tmpMasterTtl, @ctime);

    $i = 0;
    while ($i <= $#args) {
	$option = $args[$i];
	if ($option eq "-d") {
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		if (!defined($Domain)) {
		    $Domain = lc($args[$i]);
		    $Domain =~ s/\.+$//;
		    $Domainpattern = ".$Domain";
		    $Domainpattern =~ s/\./\\./g;	# for stripping off domain
	
		    $Domainfile = $Domain;
		    $Domainfile =~ s/\..*//;
		    $Specialfile = "spcl.$Domainfile";
		    $Domainfile  = "db.$Domainfile";
		} elsif (!defined($AltDb) && $args[$i] =~ /^db=/i) {
		    $Domainfile = $args[$i];
		    $Domainfile =~ s/^db=//i;
		    if ($Domainfile =~ /\// && $Domainfile !~ /^\.\/[^\/]+/) {
			print STDERR "No pathname allowed in '-d db=' argument.\n";
			print STDERR "I give up ... sorry.\n";
			exit(2);
		    }
		    $AltDb = 1;
		} elsif (!defined($AltSpcl) && $args[$i] =~ /^spcl=/i) {
		    $Specialfile = $args[$i];
		    $Specialfile =~ s/^spcl=//i;
		    $AltSpcl = 1;
		} elsif (!defined($UseDefaultDomain) && $args[$i] =~ /^mode=d/i) {
		    $UseDefaultDomain = 1;
		} else {
		    print STDERR "Extra -d option '$args[$i]' ignored, only one instance allowed.\n" if $verbose;
		}
	    }
	    $i--;
	    # Add entry to the boot file.
	    $lastNorD = "-d $Domainfile";
	    push(@makesoa, "$Domainfile DOMAIN");
	    push(@bootmsgs, "$Domain $Domainfile");

	} elsif ($option eq "-f") {
	    $file = $args[++$i];
	    &OPEN(*OPT, $file) or die "Unable to open options file '$file': $!\n";
	    while (<OPT>) {
		next if /^$/ || /^\s*[#;]/;
		if (/^\s*-f/) {
		    print STDERR "Already reading from file '$file'; -f option ignored.\n" if $verbose;
		    next;
		}
		chop; s/\s+[#;].*//;
		@targs = (@targs, split(' '));
	    }
	    &CLOSE(*OPT);
	    #
	    # Preserve the order in which the arguments were presented to
	    # this program by appending what's left of the original argument
	    # list to those that were just read in from the options file.
	    # The next argument to be processed will be the first one from
	    # the just-read file.
	    #
	    push(@targs, @args[++$i..$#args]);
	    @args = @targs;
	    @targs = ();
	    $i = -1;

	} elsif ($option eq "-z") {
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		if ($args[$i] !~ /^(\d+[.]){3}\d+$/) {
		    print STDERR "Improper IP address ($args[$i]) in -z option.\n";
		    print STDERR "I give up ... sorry.\n";
		    exit(2);
		}
		if (!defined($Bootsecsaveaddr)) {
		    $Bootsecsaveaddr = $args[$i];
		} else {
		    $Bootsecsaveaddr .= " " . $args[$i];
		}
		if (!defined($Confsecsaveaddr)) {
		    $Confsecsaveaddr = $args[$i] . ";";
		} else {
		    $Confsecsaveaddr .= " " . $args[$i] . ";";
		}
	    }
	    $i--;

	} elsif ($option eq "-Z") {
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		if ($args[$i] !~ /^(\d+[.]){3}\d+$/) {
		    print STDERR "Improper IP address ($args[$i]) in -Z option.\n";
		    print STDERR "I give up ... sorry.\n";
		    exit(2);
		}
		if (!defined($Bootsecaddr)) {
		    $Bootsecaddr = $args[$i];
		} else {
		    $Bootsecaddr .= " " . $args[$i];
		}
		if (!defined($Confsecaddr)) {
		    $Confsecaddr = $args[$i] . ";";
		} else {
		    $Confsecaddr .= " " . $args[$i] . ";";
		}
	    }
	    $i--;

	} elsif ($option eq "-b") {
	    $Bootfile = $args[++$i];

	} elsif ($option eq "+c") {
	    $Conffile = $args[++$i];

	} elsif ($option eq "-A") {
	    $doaliases = 0;

	} elsif ($option eq "-M") {
	    $domx = 0;

	} elsif ($option eq "-w") {
	    $dowks = 1;

	} elsif ($option eq "-D") {
	    $delegate = 1;
	    if (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		$Delfile = $args[$i];
	    } else {
		$i--; 
	    }

	} elsif ($option eq "-t") {
	    $dotxt = 1;

	} elsif ($option eq "-r") {
	    $dorp = 1;

	} elsif ($option eq "-u") {
	    $User = $args[++$i];

	} elsif ($option eq "-s") {
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		push(@FullServers, $args[$i]);
	    }
	    $i--;

	} elsif ($option eq "-S") {
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		if ($lastNorD =~ /^-d /) {
		    $PartialServers{"$Domainfile"} .= " $args[$i]";
		} elsif ($lastNorD =~ /^-n /) {
		    $tmp1 = $lastNorD;
		    $tmp1 =~ s/^-n //;
		    foreach $tmp2 (split(' ', $tmp1)) {
			$PartialServers{"db.$tmp2"} .= " $args[$i]";
		    }
		}
		else {
		    print STDERR "Uh, the -S option is position dependent (applies to preceding -d or -n).\n";
		    print STDERR "I give up ... sorry.\n";
		    exit(2);
		}
	    }
	    $i--;

	} elsif ($option eq "-m") {
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		if ($args[$i] !~ /:/) {
		    print STDERR "Improper -m option '$args[$i]' ignored.\n" if $verbose;
		}
		else { push(@Mx, $args[$i]); }
	    }
	    $i--;

	} elsif ($option eq "+m") {
	    $tmp1 = "";
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		$tmp1 .= $args[$i];
	    }
	    if ($tmp1 !~ /^(D|C|P|CP|PC)$/i) {
		print STDERR "Improper '$tmp1' argument in +m option.\nValid argument must be one of: D, C, P, or CP.\n";
		print STDERR "I give up ... sorry.\n";
		exit(2);
	    } else {
		$tmp1 = uc($tmp1);
		if ($multi_homed_mode) {
		    print STDERR "Hmm, using '+m $tmp1' to override previous +m option.\n" if $verbose;
		}
		$multi_homed_mode = $tmp1;
	    }
	    $i--;

	} elsif ($option eq "-c") {
	    $ExpectDomain = 1;
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		if ($ExpectDomain && $args[$i] =~ /^mode=/i) {
		    print STDERR "Improper -c option; a domain name must precede '$args[$i]' argument.\n";
		    print STDERR "I give up ... sorry.\n";
		    exit(2);
		}
		if ($args[$i] !~ /^mode=/i) {
		    $tmp1 = lc($args[$i]);
		    $tmp1 =~ s/\.*$//;
		    if ($tmp1 !~ /\./) { $tmp1 .= ".$Domain"; }
		    $tmp2 = $tmp1;
		    $tmp2 =~ s/\./\\./g; 
		    $cpatrel{$tmp2} = $tmp1;
		    push(@cpats, $tmp2);
		    $cModeSpec{$tmp2} = "";	# 'mode=' arg may update this
		    $ExpectDomain = 0;
		} else {
		    $tmp1 = uc($args[$i]);
		    $tmp1 =~ s/^MODE=//;
		    @unsorted = unpack('C*', $tmp1);
		    @sorted = sort { $a <=> $b } @unsorted;
		    $tmp1 = pack('C*', @sorted);
		    if ($tmp1 !~ /^(A|D|AD|ADQ|DQ)$/) {
			print STDERR "Improper '$args[$i]' argument in -c option.\nValid 'mode=' value must be one of: A, D, AD, ADQ, or DQ.\n";
			print STDERR "I give up ... sorry.\n";
			exit(2);
		    }
		    $cModeSpec{$tmp2} = $tmp1;	# update with actual flag(s)
		    $ExpectDomain = 1;
		}
	    }
	    $i--;

	} elsif ($option eq "-e") {
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		$tmp1 = lc($args[$i]);
		$tmp1 =~ s/\.*$//;
		$tmp1 .= ".$Domain" if $tmp1 !~ /\./;
		$tmp1 =~ s/\./\\./g; 
		push(@elimpats, $tmp1);
	    }
	    $i--;

	} elsif ($option eq "-h") {
	    $Host = $args[++$i];

	} elsif ($option eq "-o") {
	    if ("$args[++$i]:" !~ /^((\d+|(\d+[wdhms])+)?:){1,5}$/i ||
		$args[$i] =~ /^([+-].*|:+)?$/) {
		print STDERR "Improper -o option '$args[$i]'.\n";
		print STDERR "I give up ... sorry.\n";
		exit(2);
	    }
	    # Maintain backward compatibility by not assuming RFC-2308
	    # status unless the -o option contains the delimiter for
	    # the fifth argument ("$MasterTtl"), e.g.,
	    #
	    #   -o :::8h	Sets non-RFC-2308 TTL interval in the
	    #			SOA Minimum field for all DB files.
	    #   -o ::::8h	Sets RFC-2308 $TTL directive for all DB files.
	    #			SOA Minimum fields either retain existing values
	    #			or get the default negative caching interval.
	    #   -o :::30m:	Sets RFC-2308 negative caching interval in the
	    #			SOA Minimum field for all DB files.
	    #			$TTL directives either retain existing values
	    #			or are created with the default TTL value.
	    #   -o :::30m:8h	Sets RFC-2308 $TTL directive and SOA Minimum
	    #			field for all DB files.
	    #
	    # RFC-2308 status can also be set with the +t option.
	    #
	    $tmpTtl = $tmpMasterTtl = "";
	    if ($Refresh || $Retry || $Expire || $Ttl || $MasterTtl) {
		print STDERR "Hmm, using '-o $args[$i]' to override previous -o/+t option.\n" if $verbose;
		if ($rfc2308 && $args[$i] !~ /^([^:]*:){4}[^:]*$/) {
		    print STDERR "(Retaining previous \$TTL/Negative cache value(s), however.)\n" if $verbose;
		    $tmpTtl = $Ttl;
		    $tmpMasterTtl = $MasterTtl;
		}
	    }
	    $rfc2308 = 1 if $args[$i] =~ /^([^:]*:){4}[^:]*$/;
	    ($Refresh, $Retry, $Expire, $Ttl, $MasterTtl) = split(':', $args[$i]);
	    $Ttl = $tmpTtl if $tmpTtl;
	    $MasterTtl = $tmpMasterTtl if $tmpMasterTtl;
	    &CHECK_SOA_TIMERS if $verbose;

	} elsif ($option eq "+t") {
	    if ($args[++$i] !~ /^(\d+|(\d+[wdhms])+)$/i) {
		print STDERR "Improper +t option '$args[$i]' for DEFAULT-TTL.\n";
		print STDERR "I give up ... sorry.\n";
		exit(2);
	    }
	    $MasterTtl = $args[$i];
	    if ($args[++$i] !~ /^([+-].*)?$/) {
		if ($args[$i] !~ /^(\d+|(\d+[wdhms])+)$/i) {
		    print STDERR "Improper +t option '$args[$i]' for MINIMUM-TTL.\n";
		    print STDERR "I give up ... sorry.\n";
		    exit(2);
		}
		$Ttl = $args[$i];
	    } else {
		$Ttl = $DefNegCache;
		$i--;
	    }
	    if ($rfc2308) {
		print STDERR "Hmm, using '+t $MasterTtl $Ttl' to override previous +t/-o option.\n" if $verbose;
	    } else {
		$rfc2308 = 1;
	    }
	    &CHECK_SOA_TIMERS if $verbose;

	} elsif ($option eq "-i") {
	    $ForceSerial = $args[++$i];

	} elsif ($option eq "-H") {
	    $Hostfile = $args[++$i];
	    if (! -r $Hostfile || -z $Hostfile) {
		print STDERR "Invalid file '$Hostfile' specified for -H option.\n";
		print STDERR "I give up ... sorry.\n";
		exit(2);
	    }

	} elsif ($option eq "-C") {
	    $Commentfile = $args[++$i];
	    if (! -r $Commentfile || -z $Commentfile) {
		print STDERR "Invalid file '$Commentfile' specified for -C option.\n";
		print STDERR "I give up ... sorry.\n";
		exit(2);
	    }

	} elsif ($option eq "-N") {
	    $Defsubnetmask = $args[++$i];
	    if ($Defsubnetmask !~ /^(\d+[.]){3}\d+$/) {
		print STDERR "Improper subnet mask ($Defsubnetmask).\n";
		print STDERR "I give up ... sorry.\n";
		exit(2);
	    }
	    if ($#Networks >= 0) {
		print STDERR "Hmm, -N option should probably be specified before any -n options.\n" if $verbose;
	    }

	} elsif ($option eq "-n" || $option eq "-a") {
	    $lastNorD = "-n" if $option eq "-n";
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		($net, $subnetmask) = split(':', $args[$i]);
		$subnetmask = $Defsubnetmask if !$subnetmask;
		if ($subnetmask !~ /^(\d+[.]){3}\d+$/) {
		    print STDERR "Improper subnet mask ($subnetmask).\n";
		    print STDERR "I give up ... sorry.\n";
		    exit(2);
		}
		foreach $tmp1 (&SUBNETS($net, $subnetmask)) {
		    foreach $line (@Networks) {
			if ($line eq $tmp1) {
			    print STDERR "Duplicate zone skipped ($tmp1), check your -n/-a and masks for overlap.\n" if $verbose;
			    $tmp1 = "";
			    last;
			}
		    }
		    next if !$tmp1;
		    push(@Networks, $tmp1);
		    $netpat = $tmp1;
		    $netpat =~ s/\./\\./g;
		    push(@Netpatterns, $netpat);

		    #
		    # Create db files for PTR records. (if -n)
		    # Save the file names in an array for future use.
		    #
		    if ($option eq "-n") {
			$netfile = "DB.$tmp1";
			#
			# Prepend our package name to the value that is stored
			# in the %Netfiles hash.  This hash value is used as a
			# globally-scoped file descriptor typeglob for printing
			# RRs to the DB files and as a key in the %Fnames hash.
			#
			$Netfiles{$netpat} = "\*" . qualify($netfile);
			push(@makesoa, "db.$tmp1 $netfile");

			# Add entry to the boot file.
			#
			$revaddr = &REVERSE($tmp1);
			push(@bootmsgs, "$revaddr.in-addr.arpa db.$tmp1");
			$lastNorD .= " $tmp1";
		    } elsif ($tmp1 eq '127.0.0') {
			#
			# Skip the reverse-map if option is '-a 127.0.0'.
			#
			$MakeLoopbackSOA = 0;
		    }
		}
	    }
	    $i--;

	} elsif ($option eq "-p") {
	    $ExpectDomain = 1;
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		if ($ExpectDomain && $args[$i] =~ /^mode=/i) {
		    print STDERR "Improper -p option; a domain name must precede '$args[$i]' argument.\n";
		    print STDERR "I give up ... sorry.\n";
		    exit(2);
		}
		if ($args[$i] !~ /^mode=/i) {
		    $tmp1 = lc($args[$i]);
		    $tmp1 =~ s/\.*$//;
		    if ($tmp1 !~ /\./) { $tmp1 .= ".$Domain"; }
		    $tmp2 = $tmp1;
		    $tmp2 =~ s/\./\\./g; 
		    $ptrpatrel{$tmp2} = $tmp1;
		    push(@ptrpats, $tmp2);
		    $pModeSpec{$tmp2} = "";	# 'mode=' arg may update this
		    $ExpectDomain = 0;
		} else {
		    $tmp1 = uc($args[$i]);
		    $tmp1 =~ s/^MODE=//;
		    #
		    # Accept "D" as a valid flag for consistency with the +m
		    # option and the "[mh= ]" comment flag in the host file.
		    # Silently ignore it, however.
		    #
		    if ($tmp1 !~ /^(A|D|AD|DA|P)$/) {
			print STDERR "Improper '$args[$i]' argument in -p option.\nValid 'mode=' value must be one of 'A' or 'P'.\n";
			print STDERR "I give up ... sorry.\n";
			exit(2);
		    }
		    $tmp1 =~ s/D//;
		    $pModeSpec{$tmp2} = $tmp1;	# update with actual flag
		    $ExpectDomain = 1;
		}
	    }
	    $i--;

	} elsif ($option eq "-1") {
	    print STDERR "Option -1 is obsolete ... ignored.\n" if $verbose;

	} elsif ($option eq "-F") {
	    print STDERR "Option -F is now the default (and only) way ... ignored.\n" if $verbose;

	} elsif ($option eq "-L") {
	    if ($args[++$i] !~ /^\d*$/) {
	       print STDERR "Improper numerical argument for -L option ignored.\n" if $verbose;
	    }
	    elsif ($args[$i] < 10) {
	       print STDERR "Using minimum value of 10 for -L option.\n" if $verbose;
	       $Filelimit = 10;
	    }
	    else { $Filelimit = $args[$i]; }
	
	} elsif ($option eq "-q") {
	    $verbose = 0;

	} elsif ($option eq "-B") {
	    $Bwd = $args[++$i] . "/";
	    $Bwd =~ s/\/+$/\//;
	    if ($Bwd !~ /^\//) {
	       print STDERR "Must use an absolute pathname for -B option.\n";
	       print STDERR "I give up ... sorry.\n";
	       exit(2);
	    }

	} elsif ($option eq "-W") {
	    $Pwd = $args[++$i];
	    $Pwd =~ s/\/+$//;
	    if ($Pwd !~ /^\//) {
	       print STDERR "Must use an absolute pathname for -W option.\n";
	       print STDERR "I give up ... sorry.\n";
	       exit(2);
	    }

	} elsif ($option eq "-O") {
	    $tmp1 = "";
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		$tmp1 .= "$args[$i] ";
	    }
	    $i--; 
	    $tmp1 =~ s/ $//;
	    push(@BootOptions, $tmp1);

	} elsif ($option eq "+O") {
	    $tmp1 = "";
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		$tmp1 .= "$args[$i] ";
	    }
	    $i--; 
	    $CustomOptions = 1;
	    if ($tmp1) {
		$tmp1 =~ s/ *$//;
		$tmp1 .= ";" unless $tmp1 =~ /[;{]$/;
		push(@ConfOptions, $tmp1);
	    } else {
		$NeedHints = 0;
	    }

	} elsif ($option eq "+om") {
	    $tmp1 = "";
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		$tmp1 .= "$args[$i] ";
	    }
	    $i--; 
	    $tmp1 =~ s/ *$//;
	    $tmp1 .= ";" unless $tmp1 =~ /[;{]$/;
	    if ($lastNorD =~ /^-d (.*)/) {
		$MasterZoneOptions{"db.$1"} .= "$tmp1\n";
	    } elsif ($lastNorD =~ /^-n /) {
		$tmp2 = $lastNorD;
		$tmp2 =~ s/^-n //;
	 	foreach $tmp3 (split(' ', $tmp2)) {
		    $MasterZoneOptions{"db.$tmp3"} .= "$tmp1\n";
		}
	    }
	    else {
		push(@GlobalMasterZoneOptions, $tmp1);
	    }

	} elsif ($option eq "+os") {
	    $tmp1 = "";
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		$tmp1 .= "$args[$i] ";
	    }
	    $i--; 
	    $tmp1 =~ s/ *$//;
	    $tmp1 .= ";" unless $tmp1 =~ /[;{]$/;
	    if ($lastNorD =~ /^-d (.*)/) {
		$SlaveZoneOptions{"db.$1"} .= "$tmp1\n";
	    } elsif ($lastNorD =~ /^-n /) {
		$tmp2 = $lastNorD;
		$tmp2 =~ s/^-n //;
	 	foreach $tmp3 (split(' ', $tmp2)) {
		    $SlaveZoneOptions{"db.$tmp3"} .= "$tmp1\n";
		}
	    }
	    else {
		push(@GlobalSlaveZoneOptions, $tmp1);
	    }

	} elsif ($option eq "+L") {
	    $CustomLogging = 1;
	    $tmp1 = "";
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		$tmp1 .= "$args[$i] ";
	    }
	    $i--; 
	    if ($tmp1) {
		$tmp1 =~ s/ *$//;
		$tmp1 .= ";" unless $tmp1 =~ /;$/;
		push(@ConfLogging, $tmp1);
	    }

	} elsif ($option eq "-I") {
	    #
	    # Maintain backward-compatibility in case there is no argument.
	    #
	    $rfc1123 = "fail";
	    $audit = 1;
	    $action = "Skipping";
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		if ($args[$i] =~ /^(no|false|off|none|disable|ignore)$/i) {
		    $rfc1123 = "0";
		    $audit = 0;
		} elsif ($args[$i] =~ /^warn$/i) {
		    $rfc1123 = "warn";
		    $audit = 0;
		    $action = "Warning";
		} elsif ($args[$i] =~ /^audit$/i) {
		    $rfc1123 = "warn";
		    $audit = 1;
		    $action = "Warning";
		} elsif ($args[$i] =~ /^warn\-strict$/i) {
		    $rfc1123 = "warn-strict";
		    $audit = 1;
		    $action = "Warning";
		} elsif ($args[$i] =~ /^(yes|true|on|check|enable|fail)$/i) {
		    $rfc1123 = "fail";
		    $audit = 1;
		    $action = "Skipping";
		} elsif ($args[$i] =~ /^strict$/i) {
		    $rfc1123 = "strict";
		    $audit = 1;
		    $action = "Skipping";
		} else {
		    $tmp1 = ($rfc1123) ? $rfc1123 : "ignore";
		    $tmp1 = "audit" if $audit && $rfc1123 eq 'warn';
		    print STDERR "Unknown '-I' argument ('$args[$i]'); name checking remains '$tmp1'.\n" if $verbose;
		}
	    }
	    $i--;

	} elsif ($option eq "-v") {
	    while (++$i <= $#args && $args[$i] !~ /^[+-]/) {
		$tmp1 = lc($args[$i]);
		#
		# Remove trailing dot(s) but leave one
		# if it's the root domain.
		#
		$tmp1 =~ s/(\S)\.+$/$1/;
		#
		# Stack the domains with the 'unshift' function so that they
		# can be popped in the same order.  This allows child domains
		# to be pushed onto the stack during processing and thus grouped
		# with the parent domain if recursive verification is enabled.
		#
		unshift(@Vdomains, $tmp1) if !exists($duplicate{$tmp1});
		$duplicate{$tmp1} = 1;
	    }
	    $verify_mode = 1;
	    $i--;

	} elsif ($option =~ /^-recurs(e|ive|ion)$/i) {
	    #
	    # Causes each child domain to be verified immediately
	    # after completing verification of the parent domain.
	    #
	    $recursive_verify = 1;

	} elsif ($option =~ /^-no-?(check|verify)[_-]?del$/i) {
	    #
	    # Bypasses the checking of proper delegations with the
	    # 'check_del' program when in verify mode.  'check_del'
	    # can be quite time-consuming when encountering a large
	    # number of unresponsive nameservers.
	    #
	    $verify_delegations = 0;

	} elsif ($option =~ /^-show-?(cname|chained)-?(chain|cname)s?$/i) {
	    #
	    # The length of out-of-zone CNAME chains are normally
	    # displayed only if the CNAME ultimately fails to resolve
	    # to a non-CNAME domain name.  This option overrides the
	    # default behavior by identifying all instances of
	    # out-of-zone CNAME chains.
	    #
	    $show_chained_cnames = 1;

	} elsif ($option eq "-debug") {
	    #
	    # Prevents the removal of all temporary files that get
	    # created during the course of normal processing including
	    # zone transfer files:
	    #  * If zone auditing is in effect, the DiG batch input file
	    #    is saved as well as a copy of the answers to the queries.
	    #  * If delegations are being verified, the complete input and
	    #    output of the 'check_del' program is also saved.
	    #  * If a domain is being verified and the zone transfer file
	    #    still exists from a previous run with -debug, the existing
	    #    zone transfer data will be used instead of requesting a
	    #    new copy from an authoritative nameserver.
	    #
	    $debug = 1;

	} elsif ($option eq "-y") {
	    @ctime = localtime(time);
	    $DateSerial = ($ctime[3] * 100) + (($ctime[4] + 1) * 10000) +
			  (($ctime[5] + 1900) * 1000000);
	    $UseDateInSerial = 1;

	} else {
	    if ($option =~ /^[+-].+/) {
		print STDERR "Unknown option '$option'; ignored.\n" if $verbose;
	    }
	}
	$i++;
    }
    
    if ($verify_mode) {
	if (!@Vdomains) {
	    print STDERR "The -v option requires at least one domain name as an argument.\n";
	    print STDERR "I give up ... sorry.\n";
	    exit(2);
	}
	if (@Networks || defined($Domain) || defined($User)) {
	    print STDERR "The -d, -n, and/or -u options are incompatible with -v.\n";
	    print STDERR "I give up ... sorry.\n";
	    exit(2);
	}
    } elsif (!@Networks || !defined($Domain) || !defined($User)) {
	print STDERR "Must specify at least -d, one -n and -u.\n";
	print STDERR "I give up ... sorry.\n";
	exit(2);
    } elsif (!$doaliases && $multi_homed_mode =~ /[CP]/) {
	print STDERR "'+m $multi_homed_mode' option incompatible with -A option.\n";
	print STDERR "I give up ... sorry.\n";
	exit(2);
    }
}


#
# Calculate all the subnets from a network number and mask.
#
#
sub SUBNETS {
    my ($network, $mask) = @_;
    my (@ans, @net, @mask, $i, $pos, $start, $end, $partial, $temp, $flag);

    @net = split('\.', $network);
    @mask = split('\.', $mask );
    while ($#net < $#mask) {
	push(@net, 0);
    }
    $start = join('.', @net);
    $flag = $pos = 0;
    for ($i = 0; $i < $#mask; $i++) {
	$mask[$i] &= 0x00FF;
	$temp = $net[$i] & $mask[$i];
	$flag++ if $net[$i] != $temp;
	$net[$i] = $temp;
    }
    if ($flag) {
	$end = join('.', @net);
	print STDERR "Oops, $start is not lower bound of super/subnet range, using $end\n" if $verbose;
    }

    #
    # For DNS purposes, interested in the 1st, 2nd, or 3rd octet of
    # the subnet mask (@mask).  A contiguous subnet mask is assumed!
    #
    foreach $mask (@mask) {
	if ($mask == 0x00FF) {
	    $pos++;
	} else {
	    $pos-- if $mask == 0;
	    last;
	}
    }
    if ($pos < $#mask) {
	$start = $net[$pos];
	$end = $net[$pos] | ($mask[$pos] ^ 0x00FF);
	$partial = "";
	for ($i = 0; $i < $pos; $i++) {
	    $partial .= "$net[$i].";
	}
	while ($start <= $end) {
	    push(@ans, $partial.$start++);
	}
    } else {
	push(@ans, "$net[0].$net[1].$net[2]");
    }

    return @ans;
}


#
# Establish what we will be using for SOA records and
# the global MX RRset.  Single-label domain names will
# be qualified with the current domain (-d option).
# Set the default mode for handling multi-homed hosts.
#
sub FIXUP {
    my ($buffer, $s, $t, $preference, $mxhost, $error, $user_part);
    my ($domain_part, $major_version, $minor_version, $patch_version, @temp);

    if (!$verify_mode) {
	$error = 0;
	$Domain =~ s/\.$//;
	if ($rfc1123 && &CHECKNAME($Domain, 'NS')) {
	    print STDERR "Domain name '$Domain' (-d) is invalid.\n";
	    $error = 1;
	}

	$Host =~ s/\.$//;
	if ($rfc1123 && &CHECKNAME($Host, 'NS')) {
	    print STDERR "SOA host name '$Host' (-h) is invalid.\n";
	    $error = 1;
	} else {
	    if ($Host =~ /\./) {
		$RespHost = "$Host.";
	    } else {
		$RespHost = "$Host.$Domain.";
	    }
	    $RespHost =~ s/\.\././g;			# remove redundant "."
	}

	if ($User =~ /\\\@/) {
	    #
	    # Flag the mistaken attempt to escape the ampersand character.
	    #
	    print STDERR "SOA RNAME field '$User' (-u) is invalid.\n";
	    $error = 1;
	} else {
	    if ($User =~ /\@/) {
		($user_part, $domain_part) = split('\@', $User);
		$user_part =~ s/\./\\./g;		# escape "." in username
		$user_part =~ s/\\\\//g;		# remove redundancies
		if ($domain_part =~ /\./) {		# multiple domain labels
		    $domain_part .= ".";		# append root domain
		} else {
		    $domain_part .= ".$Domain.";	# append our domain name
		}
		$RespUser = "$user_part.$domain_part";	# join w/ unescaped "."
	    } elsif ($User !~ /\.$/) {			# already RNAME if no
		$user_part = $User;			# "@" and trailing "."
		$user_part =~ s/\./\\./g;		# escape "." in username
		$user_part =~ s/\\\\//g;		# remove redundancies
		$RespUser = "$user_part.$Domain.";	# join w/ unescaped "."
	    } else {
		$RespUser = $User;
	    }
	    $RespUser =~ s/\.\././g;
	}

	# Clean up nameservers
	#
	foreach $s (@FullServers) {
	    $s =~ s/\.$//;
	    if ($rfc1123 && &CHECKNAME($s, 'NS')) {
		print STDERR "Nameserver name '$s' (-s) is invalid.\n";
		$error = 1;
	    } else {
		$s .= ".$Domain" if $s !~ /\./;
		$s .= ".";
	    }
	}
	foreach $t (keys %PartialServers) {
	    @temp = split(' ', $PartialServers{$t});
	    foreach $s (@temp) {
		$s =~ s/\.$//;
		if ($rfc1123 && &CHECKNAME($s, 'NS')) {
		    print STDERR "Nameserver name '$s' (-S) is invalid.\n";
		    $error = 1;
		} else {
		    $s .= ".$Domain" if $s !~ /\./;
		    $s .= ".";
		}
	    }
	    $PartialServers{$t} = join(' ', @temp);
	}

	# Clean up MX hosts
	#
	foreach $s (@Mx) {
	    ($preference, $mxhost) = split(':', $s);
	    if ($preference !~ /^\d+$/) {
		print STDERR "MX preference value '$preference' (-m) is invalid.\n";
		$error = 1;
		#
		# Treat this as a fatal error.  Before exiting, however,
		# we'll also enable the MX hostname(s) to be checked.
		#
		$rfc1123 = "strict";
	    }
	    $mxhost =~ s/\.$//;
	    if ($rfc1123 && &CHECKNAME($mxhost, 'MX')) {
		print STDERR "MX hostname '$mxhost' (-m) is invalid.\n";
		$error = 1;
	    } else {
		if ($mxhost =~ /$Domainpattern$/) {
		    #
		    # Prevent unnecessary verbosity by keeping in-domain
		    # names in origin-relative format since the MX records
		    # will appear only in the forward-mapping file.
		    #
		    $mxhost =~ s/$Domainpattern$//;
		} else {
		    $mxhost .= "." if $mxhost =~ /\./;
		}
		$s = "$preference $mxhost";
	    }
	}
	if ($error && $rfc1123 !~ /warn/) {
	    print STDERR "I give up ... sorry.\n";
	    exit(2);
	}

	# If no +m option was specified, forward and reverse RRsets of
	# multi-homed hosts will be generated in the default manner.
	#
	$multi_homed_mode = "D" if !$multi_homed_mode;
    }

    # h2n will try to call the DiG utility to provide various
    # items of useful DNS information.  In preparation, maximize
    # our chances of finding DiG by setting the PATH environment
    # variable accordingly.
    #
    if ($ENV{PATH} !~ /\/usr\/local\/bin/) {
	#
	# We may be running from cron(1M).  Add the contents of
	# the '/etc/PATH' file to our environment.
	#
	$buffer = "";
	&OPEN(*SYSPATH, "/etc/PATH");
	read(SYSPATH, $buffer, 16384);
	&CLOSE(*SYSPATH);
	$buffer .= ":" if $buffer;
	$ENV{PATH} = "$buffer$ENV{PATH}";
	if ($ENV{PATH} !~ /\/usr\/local\/bin/) {
	    #
	    # Round up the usual suspects as a last resort...
	    #
	    $ENV{PATH} = "/usr/local/bin:/usr/contrib/bin:/usr/bin:/usr/sbin:/sbin:/usr/lib:/etc:$ENV{PATH}";
	}
    }
    # Get the version of DiG that is installed on this system.
    #
    &OPEN(*DIGOUT, "dig . 2>&1 |");
    while (<DIGOUT>) {
	if (/^; <<>> DiG (\d+)(\.(\d+))?(\.(\d+).*)? <<>>/) {
	    $major_version = (defined $1) ? $1 : 0;
	    $minor_version = (defined $3) ? $3 : 0;
	    $patch_version = (defined $5) ? $5 : 0;
	    $DiG_version = (100 * $major_version) + (10 * $minor_version) + $patch_version;
	    last;
	}
    }
    &CLOSE(*DIGOUT);
    if ($DiG_version < 830) {
	#
	# Set the threshold at which long command lines to DiG
	# must be split across two lines when following chained
	# CNAMEs in the AUDIT_RRs subroutine.
	#
	$DiG_buffer_size = 98;
    } elsif ($DiG_version < 900) {
	#
	# The buffer size was increased in version 8.3.  Assume
	# that the same value exists for subsequent 8.X versions.
	#
	$DiG_buffer_size = 382;
    } else {
	#
	# Version 9 of DiG is a rewrite of this utility with a
	# significant increase in the buffer sizes.
	#
	$DiG_buffer_size = 986;
    }

    if (!$verify_mode) {
	#
	# RFC-2308 is implemented in BIND nameservers starting with
	# version 8.2.  This specifies that the SOA Minimum Field
	# is defined to be the negative caching interval and the
	# default time-to-live value is now defined with a new
	# master zone file directive, $TTL.
	# BIND 8.2 and subsequent versions will issue a warning
	# when the $TTL directive is missing from a master zone
	# that is being loaded.
	# In order to suppress these warnings, we'll try to use
	# DiG to issue a special query to the master nameserver
	# to find out which version of BIND it is running.
	# Even if our RFC-2308 status is already known via the
	# -o/+t options, we still want to know what version of
	# BIND we're running.  That's because symbolic TTL values
	# were not supported until version 8.2.1.  If our version
	# of BIND is an earlier one or can not be determined, all
	# TTL values will be converted into their equivalent number
	# of seconds.
	#
	$BIND_version = &GET_BIND_VERSION($RespHost);
	if ($BIND_version =~ /^(\d+)\.(\d+)(\.(\d+))?/) {
	    $major_version = (defined $1) ? $1 : 0;
	    $minor_version = (defined $2) ? $2 : 0;
	    $patch_version = (defined $4) ? $4 : 0;
	    $BIND_version = (100 * $major_version) + (10 * $minor_version) + $patch_version;
	    if ($BIND_version >= 820) {
		if (!$rfc2308) {
		    #
		    # Enable the generation of $TTL directives in the MAKE_SOA
		    # subroutine by toggling the "$rfc2308" flag as though the
		    # enabling positional argument had been specified in the
		    # -o/+t option.
		    #
		    $rfc2308 = 1;

		    # NOTE: If a value for "$Ttl" was specified with -o/+t,
		    #       transfer the value to its proper context in
		    #       "$MasterTtl".  Otherwise, existing $TTL
		    #       directives will retain their values or be
		    #       created with the value of "$DefTtl".
		    # 
		    $MasterTtl = $Ttl if $Ttl;

		    # The MAKE_SOA subroutine will use the SOA time intervals
		    # from an already-existing DB file in the absence of a
		    # replacement value via the -o/+t option.  Since we had to
		    # discover our RFC-2308 status from a DNS query instead
		    # of having it explicitly configured in the -o/+t option,
		    # we must assume that the SOA RRs of the DB files still
		    # have SOA Minimum fields in their old context of holding
		    # a positive TTL value.
		    # In order to make sure that existing SOA Minimum fields
		    # assume their new context, we'll explicitly set "$Ttl"
		    # as though a value had been passed via the -o/+t option.
		    # The SOA RRs will thus be initialized with the default
		    # recommended value for the negative cache interval.
		    #
		    $Ttl = $DefNegCache;
		}
	    }
	}
    }
}


sub GET_BIND_VERSION {
    my ($nameserver) = @_;
    my ($continuation_line, $version, $status);
    local *DIGOUT;

    $continuation_line = "";
    $version = "unavailable";
    &OPEN(*DIGOUT, "dig +nostats \@$nameserver version.bind txt chaos 2>&1 |");
    while (<DIGOUT>) {
	next if /^$/;
	chop;
	if (/^;.+HEADER.+opcode: QUERY, status: ([^,]+)/) {
	    $status = $1;
	    if ($status ne 'NOERROR') {
		if ($status eq 'NOTIMP') {
		    #
		    # We are probably dealing with a non-BIND nameserver.
		    #
		    $version = "*Non-BIND NS*";
		} else {
		    $version = $status;
		}
		last;
	    }
	} elsif (/^VERSION\.BIND\.\s+.*TXT\s+\"([^\"]*)/i) {
	    $version = $1;
	    if ($version =~ /\\$/) {
		$version =~ s/(\s+)\\$/$1/;	# remove pre-spaced escape char.
		$version =~ s/\\$/ /;		# escape becomes space char.
		$continuation_line = "1";
		next;
	    } else {
		$version = "unavailable" if $version =~ /^\s*$/;
		last;
	    }
	} elsif ($continuation_line && /([^\"]*)/) {
	    $continuation_line = $1;
	    $continuation_line =~ s/^\s+//;
	    $version .= $continuation_line;
	    if ($version =~ /\\$/) {
		$version =~ s/(\s+)\\$/$1/;
		$version =~ s/\\$/ /;
		next;
	    } else {
		$version = "unavailable" if $version =~ /^\s*$/;
		last;
	    }
	}
    }
    &CLOSE(*DIGOUT);
    return "$version";
}

    
sub GEN_BOOT {
    my($revaddr, $n, $one, $bcname, $sname);

    if (! -e "boot.cacheonly") {
	$bcname = "${Bwd}boot.cacheonly";
	&OPEN(*F, "> $bcname") or die "Unable to write '$bcname': $!\n";
	print F "\ndirectory  $Pwd\n\n";
	print F "cache      .\t\t\t\tdb.cache\n";
	print F "primary    0.0.127.in-addr.arpa\t\tdb.127.0.0\n";
	&CLOSE(*F);
    }

    if (! -e "conf.cacheonly") {
	$bcname = "${Bwd}conf.cacheonly";
	&OPEN(*F, "> $bcname") or die "Unable to write '$bcname': $!\n";
	print F "\noptions {\n";
	print F "\tdirectory \"$Pwd\";\n};\n\n";
	if ($NeedHints) {
	    print F "zone \".\"\t\t\t{ type hint;\tfile \"db.cache\"; };\n";
	}
	print F "zone \"0.0.127.in-addr.arpa\"\t{ type master;\tfile \"db.127.0.0\"; };\n";
	&CLOSE(*F);
    }
    
    if ($Bwd && $Bootfile !~ /^\//) {
	$bcname = "$Bwd$Bootfile";
    } else {
	$bcname = "$Bootfile";
    }
    &OPEN(*F, "> $bcname") or die "Unable to write '$bcname': $!\n";
    print F "\n";
    foreach $line (@BootOptions) {
	printf F "%s\n", $line;
    }
    print F "\ndirectory  $Pwd\n\n";
    print F "cache      .\t\t\t\tdb.cache\n";
    print F "primary    0.0.127.in-addr.arpa\t\tdb.127.0.0\n";
    foreach $line (@bootmsgs) {
	($bootdom, $bootdb) = split(' ', $line);
	printf F "primary    %s%s\n", &TAB($bootdom, 29), $bootdb;
    }
    $sname = "spcl-boot";
    if (-r $sname) {
	printf F "\n";
	&OPEN(*ADD, $sname) or die "Unable to read '$sname': $!\n";
	while (<ADD>) { print F; }
	&CLOSE(*ADD);
	print STDOUT "File '$sname' found and appended to '$bcname'.\n" if $verbose;
    }
    &CLOSE(*F);

    if ($Bwd && $Conffile !~ /^\//) {
	$bcname = "$Bwd$Conffile";
    } else {
	$bcname = "$Conffile";
    }
    &OPEN(*F, "> $bcname") or die "Unable to write '$bcname': $!\n";
    if ($CustomOptions) {
	if (@ConfOptions) {
	    print F "\noptions {\n";
	    print F "\tdirectory \"$Pwd\";\n";
	    foreach $line (@ConfOptions) {
		printf F "\t%s\n", $line;
	    }
	    print F "};\n\n";
	}
    } else {
	print F "\noptions {\n";
	print F "\tdirectory \"$Pwd\";\n";
	print F "};\n\n";
    }
    if ($CustomLogging) {
	if (@ConfLogging) {
	    print F "logging {\n";
	    foreach $line (@ConfLogging) {
		printf F "\t%s\n", $line;
	    }
	    print F "};\n\n";
	} else {
	    print F "logging {\n";
	    print F "\tcategory lame-servers { null; };\n";
	    print F "\tcategory cname { null; };\n";
	    print F "\tcategory security { default_syslog; };\n";
	    print F "};\n\n";
	}
    }
    if ($NeedHints) {
	print F "zone \".\"\t\t\t{ type hint;\tfile \"db.cache\"; };\n";
    }
    print F "zone \"0.0.127.in-addr.arpa\"\t{ type master;\tfile \"db.127.0.0\"; };\n";
    foreach $line (@bootmsgs) {
	($bootdom, $bootdb) = split(' ', $line);
	printf F "zone %s{ type master;\tfile \"%s\";", &TAB("\"$bootdom\"", 27), $bootdb;
	foreach $one (@GlobalMasterZoneOptions) {
	    printf F "\n                                  %s", $one;
	}
	if (exists($MasterZoneOptions{"$bootdb"})) {
	    foreach $one (split('\n', $MasterZoneOptions{"$bootdb"})) {
		printf F "\n                                  %s", $one;
	    }
	}
	printf F " };\n";
    }
    $sname = "spcl-conf";
    if (-r $sname) {
	printf F "\n";
	&OPEN(*ADD, $sname) or die "Unable to read '$sname': $!\n";
	while (<ADD>) { print F; }
	&CLOSE(*ADD);
	print STDOUT "File '$sname' found and appended to '$bcname'.\n" if $verbose;
    }
    &CLOSE(*F);

    if (defined($Bootsecaddr)) {
	$bcname = "${Bwd}boot.sec";
	&OPEN(*F, "> $bcname") or die "Unable to write '$bcname': $!\n";
	print  F "\n";
	foreach $line (@BootOptions) {
	    printf F "%s\n", $line;
	}
	print  F "\ndirectory  $Pwd\n\n";
	print  F "cache      .\t\t\t\tdb.cache\n";
	print  F "primary    0.0.127.in-addr.arpa\t\tdb.127.0.0\n";
	foreach $line (@bootmsgs) {
	    ($bootdom, $bootdb) = split(' ', $line);
	    printf F "secondary  %s%s\n", &TAB($bootdom, 29), $Bootsecaddr;
	}
	$sname = "spcl-boot.sec";
	if (-r $sname) {
	    printf F "\n";
	    &OPEN(*ADD, $sname) or die "Unable to read '$sname': $!\n";
	    while (<ADD>) { print F; }
	    &CLOSE(*ADD);
	    print STDOUT "File '$sname' found and appended to '$bcname'.\n" if $verbose;
	}
	&CLOSE(*F);
    }

    if (defined($Confsecaddr)) {
	$bcname = "${Bwd}conf.sec";
	&OPEN(*F, "> $bcname") or die "Unable to write '$bcname': $!\n";
	if ($CustomOptions) {
	    if (@ConfOptions) {
		print F "\noptions {\n";
		print F "\tdirectory \"$Pwd\";\n";
		foreach $line (@ConfOptions) {
		    printf F "\t%s\n", $line;
		}
		print F "};\n\n";
	    }
	} else {
	    print F "\noptions {\n";
	    print F "\tdirectory \"$Pwd\";\n";
	    print F "};\n\n";
	}
	if ($CustomLogging) {
	    if (@ConfLogging) {
		print  F "logging {\n";
		foreach $line (@ConfLogging) {
		    printf F "\t%s\n", $line;
		}
		print  F "};\n\n";
	    } else {
		print  F "logging {\n";
		print  F "\tcategory lame-servers { null; };\n";
		print  F "\tcategory cname { null; };\n";
		print  F "\tcategory security { default_syslog; };\n";
		print  F "};\n\n";
	    }
	}
	if ($NeedHints) {
	    print  F "zone \".\"\t\t\t{ type hint;\tfile \"db.cache\"; };\n";
	}
	print  F "zone \"0.0.127.in-addr.arpa\"\t{ type master;\tfile \"db.127.0.0\"; };\n";
	foreach $line (@bootmsgs) {
	    ($bootdom, $bootzone) = split(' ', $line);
	    printf F "zone %s{ type slave;\tmasters { %s };", &TAB("\"$bootdom\"", 27), $Confsecaddr;
	    foreach $one (@GlobalSlaveZoneOptions) {
		printf F "\n                                  %s", $one;
	    }
	    if (exists($SlaveZoneOptions{"$bootzone"})) {
		foreach $one (split('\n', $SlaveZoneOptions{"$bootzone"})) {
		    printf F "\n                                  %s", $one;
		}
	    }
	    printf F " };\n";
	}
	$sname = "spcl-conf.sec";
	if (-r $sname) {
	    printf F "\n";
	    &OPEN(*ADD, $sname) or die "Unable to read '$sname': $!\n";
	    while (<ADD>) { print F; }
	    &CLOSE(*ADD);
	    print STDOUT "File '$sname' found and appended to '$bcname'.\n" if $verbose;
	}
	&CLOSE(*F);
    }

    if (defined($Bootsecsaveaddr)) {
	$bcname = "${Bwd}boot.sec.save";
	&OPEN(*F, "> $bcname") or die "Unable to write '$bcname': $!\n";
	print  F "\n";
	foreach $line (@BootOptions) {
	    printf F "%s\n", $line;
	}
	print  F "\ndirectory  $Pwd\n\n";
	print  F "cache      .\t\t\t\tdb.cache\n";
	print  F "primary    0.0.127.in-addr.arpa\t\tdb.127.0.0\n";
	foreach $line (@bootmsgs) {
	    ($bootdom, $bootdb) = split(' ', $line);
	    printf F "secondary  %s%s%s\n", &TAB($bootdom, 29), &TAB($Bootsecsaveaddr, 16), $bootdb;
	}
	$sname = "spcl-boot.sec.save";
	if (-r $sname) {
	    printf F "\n";
	    &OPEN(*ADD, $sname) or die "Unable to read '$sname': $!\n";
	    while (<ADD>) { print F; }
	    &CLOSE(*ADD);
	    print STDOUT "File '$sname' found and appended to '$bcname'.\n" if $verbose;
	}
	&CLOSE(*F);
    }

    if (defined($Confsecsaveaddr)) {
	$bcname = "${Bwd}conf.sec.save";
	&OPEN(*F, "> $bcname") or die "Unable to write '$bcname': $!\n";
	if ($CustomOptions) {
	    if (@ConfOptions) {
		print F "\noptions {\n";
		print F "\tdirectory \"$Pwd\";\n";
		foreach $line (@ConfOptions) {
		    printf F "\t%s\n", $line;
		}
		print F "};\n\n";
	    }
	} else {
	    print F "\noptions {\n";
	    print F "\tdirectory \"$Pwd\";\n";
	    print F "};\n\n";
	}
	if ($CustomLogging) {
	    if (@ConfLogging) {
		print  F "logging {\n";
		foreach $line (@ConfLogging) {
		    printf F "\t%s\n", $line;
		}
		print  F "};\n\n";
	    } else {
		print  F "logging {\n";
		print  F "\tcategory lame-servers { null; };\n";
		print  F "\tcategory cname { null; };\n";
		print  F "\tcategory security { default_syslog; };\n";
		print  F "};\n\n";
	    }
	}
	if ($NeedHints) {
	    print  F "zone \".\"\t\t\t{ type hint;\tfile \"db.cache\"; };\n";
	}
	print  F "zone \"0.0.127.in-addr.arpa\"\t{ type master;\tfile \"db.127.0.0\"; };\n";
	foreach $line (@bootmsgs) {
	    ($bootdom, $bootdb) = split(' ', $line);
	    $bootzone = $bootdb; $bootdb =~ s/^db/bak/i;
	    printf F "zone %s{ type slave;\tfile %smasters { %s };", &TAB("\"$bootdom\"", 27), &TAB("\"$bootdb\";", 19), $Confsecsaveaddr;
	    foreach $one (@GlobalSlaveZoneOptions) {
		printf F "\n                                  %s", $one;
	    }
	    if (exists($SlaveZoneOptions{"$bootzone"})) {
		foreach $one (split('\n', $SlaveZoneOptions{"$bootzone"})) {
		    printf F "\n                                  %s", $one;
		}
	    }
	    printf F " };\n";
	}
	$sname = "spcl-conf.sec.save";
	if (-r $sname) {
	    printf F "\n";
	    &OPEN(*ADD, $sname) or die "Unable to read '$sname': $!\n";
	    while (<ADD>) { print F; }
	    &CLOSE(*ADD);
	    print STDOUT "File '$sname' found and appended to '$bcname'.\n" if $verbose;
	}
	&CLOSE(*F);
    }
}

