package ZoneFile;

use Conf;
use ZoneEntry;

return 1;

sub get_zone_file {
    my $ze = shift;

    my $zone;

    eval {
	$zone = '$TTL' . "\t$Conf::ttl\n"
	    . '$ORIGIN' . "\t" . $ze->zone() . ".\n"
		. get_soa($ze) . get_verified_zone_data($ze->srcfile());
    } ;

    if ($@) {
	die('Error generating zone file for ' . $ze->zone() . "\n$@");
    }

    return $zone;
}

sub get_soa {
    my $ze = shift;

    my $rp_name = $ze->maintainer();
    $rp_name =~ s/@/./;

    # Construct a serial number: we can't get month number with 
    #   leading zeroes from Date::Format, so jump through hoops.
    # NOTE that this assumes the script will be run no more often 
    #   than every 15 minutes or so...probably a safe assumption for 
    #   now, but...
    my @ltime = localtime(time);
    my $year = $ltime[5] + 1900;
    my $monthno = $ltime[4] + 1;
    $monthno = "0$monthno" if ($monthno < 10);
    my $mday = $ltime[3];
    $mday = "0$mday" if ($mday < 10);
    my $qtrhr = ($ltime[2] * 4) + int($ltime[1] / 15);
    $qtrhr = "0$qtrhr" if ($qtrhr < 10);
    my $serial = "$year$monthno$mday$qtrhr";

    my @record = ('@', 'SOA', $Conf::ns_name . '.', $rp_name . '.',
		  '(', $serial,
		  $Conf::refresh, $Conf::retry, $Conf::expire, 
		  $Conf::negttl, ')');
    return join("\t", @record) . "\n";
}

sub get_verified_zone_data {
    my $file = shift;

    open(SRC, $file) or die("Could not open source file $file: $!\n");

    my $data = "";
    
    while (<SRC>) {
	if (/^\s+$/) {
	    next;
	}

	my @record = split(/\s+/); 

        # Special case, the TXT record can have many spaces in its
	# quoted string that should not be converted to tabs.
	if ($record[1] eq "TXT") {
	    @record = split(/\s+/,$_,3);
	}

	eval {
	    verify_record([@record]);
	} ;
	if ($@) {
	    die("$@Malformed record $_\n");
	}
	$data .= join("\t", @record) . "\n";
    }

    close(SRC);

    return "$data\n";
}			     

sub verify_record {
    my $recref = shift;
    
    my @entries = @{$recref};

    if ($entries[1] !~ /^[[:alnum:]]{1,6}$/) {
	die("Malformed record class $entries[1]\n");
    }

    # This is safe to do with user input data because we insist that
    #   $entries[1] is 1-6 alphanumeric characters
    my $verify_func = "verify_$entries[1]_record";

    if (defined(&$verify_func)) {
	&$verify_func([@entries]);
    } else {
	die("Unknown record class $entries[1]\n");
    }
}

sub verify_number {
    my $number = shift();
    my $min = shift();
    my $max = shift();

    $number == 0 and !($number =~ /0+/)
	and die("Malformed number $number not a whole number\n");

    $number < $min and die("Number $number less than $min\n");
    $number > $max and die("Number $number greater than $max\n");
}

sub verify_ipaddr {
    my $ipaddr = shift() || die('No IP address');

    my @quad = split(/\./, $ipaddr);
    scalar(@quad) == 4 or die("IP address not a dotted quad: $ipaddr\n");
    map { verify_number($_, 0, 255) } @quad;
}

sub verify_ip6addr {
    my $ip6addr = shift() || die('No IPv6 address');

    $ip6addr !~ /^[[:xdigit:]:]+$/
	and die("Malformed IPv6 address $ip6addr\n");
}

sub verify_localname {
    my $lname = shift() || die('No localname');

    $lname ne '@' and $lname ne '*' and $lname !~ /^(\*\.)*[[:alnum:]-]+$/
	and die("Malformed local name $lname\n");
}

sub verify_fqdn {
    my $fqdn = shift() || die('No FQDN');
    
    $fqdn !~ /^[[:alnum:]\.-]+$/ and die("Malformed fqdn $fqdn\n");
}

sub verify_servicename {
    my $srvname = shift() || die('No service name');

    my ($service, $proto) = ($srvname =~ /_(\S+)\._(\S+)/)
	or die("Malformed service name $srvname\n");

    verify_localname($service);
    verify_localname($proto);
}

sub verify_A_record {
    my $a = shift() || die('No A record');

    scalar(@$a) == 3 or die("An A record needs 3 fields\n");

    eval {
	verify_localname($a->[0]);
	verify_ipaddr($a->[2]);
    } ;
    $@ and die("$@Bad A record\n");
}

sub verify_CNAME_record {
    my $cname = shift() || die('No CNAME record');

    scalar(@$cname) == 3 or die("A CNAME record needs 3 fields\n");

    eval {
	verify_localname($cname->[0]);
	verify_fqdn($cname->[2]);
    } ;
    $@ and die("$@Bad CNAME record\n");
}

sub verify_NS_record {
    my $ns = shift() || die('No NS record');

    scalar(@$ns) == 3 or die("A NS record needs 3 field\n");

    eval {
	verify_localname($ns->[0]);
	verify_fqdn($ns->[2]);
    } ;
    @$ and die("$@Bad NS record\n");
} 

sub verify_MX_record {
    my $mx = shift() || die('No MX record');

    scalar(@$mx) == 4 or die("A MX record needs 4 fields\n");

    eval {
	verify_localname($mx->[0]);
	verify_number($mx->[2], 0, 32767);
	verify_fqdn($mx->[3]);
    } ;
    $@ and die("$@Bad MX record\n");
}

sub verify_AAAA_record {
    my $aaaa = shift() || die('No AAAA record');

    scalar(@$aaaa) == 3 or die("An AAAA record needs 3 fields\n");

    eval {
	verify_localname($aaaa->[0]);
	verify_ip6addr($aaaa->[2]);
    } ;
    $@ and die("$@Bad AAAA record\n");
}

sub verify_AFSDB_record {
    my $afsdb = shift() || die('No AFSDB record');

    scalar(@$afsdb) == 4 or die("AFSDB record needs 4 fields\n");

    eval {
	verify_localname($afsdb->[0]);
	verify_number($afsdb->[2], 1, 2);
	verify_fqdn($afsdb->[3]);
    } ;
    $@ and die("$@Bad AFSDB record\n");
}

sub verify_TXT_record {
    my $txt = shift() || die('No SRV record');

    eval {
	verify_localname($txt->[0]);
        $txt->[2]  =~ /^"[^"\\]*/ or die("Missing begin quote\n");
        grep { /["\\]/ } @{$txt}[3..$#$txt-1] and die("Unexpected double quote\n");
        $txt->[-1] =~ /[^"\\]*"$/ or die("Missing end quote\n");
    } ;
    $@ and die("$@Bad TXT record\n");
}

sub verify_SRV_record {
    my $srv = shift() || die('No SRV record');

    scalar(@$srv) == 6 or die("SRV record needs 6 fields\n");
    eval {
	verify_servicename($srv->[0]);
	verify_number($srv->[2], 0, 65535);
	verify_number($srv->[3], 0, 65535);
	verify_number($srv->[4], 0, 65535);
	verify_fqdn($srv->[5]);
    } ;
    $@ and die("$@Bad SRV record\n");
}

