#!/usr/bin/perl
use strict;
#  Allow bare words, so &ade_err_error() calls look nicer.
no strict 'subs';
use lib substr `ade-config ade_include_prefix`,0,-1;
use ADE;
use lib substr `fad-config fad_include_prefix`,0,-1;
use FAD;
use Getopt::Long qw(:config no_ignore_case);
#use File::Find;
use Sys::Hostname;

my($subversion_string) = '# $HeadURL$ $LastChangedRevision$';

#  Without predeclaring variables returned by fad-config, an error will be generated 
#  (probably because of 'use strict' above).  If ade-app-config is modified to generate a 
#  'my' itself then somehow the 'my' narrows the scope of the assignment to within the 
#  backquotes. Try running:
#
#      perl -e 'eval "my(\$x)=4"; print "x=$x\n";'
# 
#  with and without the 'my' to see what I mean. This means that we should not willy
#  nilly just run: 
#
#      eval `fad-config --format=perl`
#
#  Because if a new variable is added to the output then it will break this script. So
#  this script must (1) do 'my' for anything it wants, (2) specify explicitly anything
#  it wants from fad-config as a way of preventing fad-config from showing assignments
#  for more variables than we have 'my'ed.

my($fad_log_prefix, $fad_state_prefix);
eval `fad-config --format=perl fad_log_prefix fad_state_prefix`;

&ade_err_registerdefderrs({
    fadscan_err_misc        => { fmt => "%s" },
});

my($opt_mode, $opt_sttdir, $opt_logdir, $opt_mailto, $opt_nocrcs);
my(@config_hasharray) = (
    { dsc => "opt_mode",    var => \$opt_mode,    dfl => "undefined" },
    { dsc => "opt_logdir",  var => \$opt_logdir,  dfl => "$fad_log_prefix/$ade_app_progname" },
    { dsc => "opt_sttdir",  var => \$opt_sttdir,  dfl => "$fad_state_prefix/$ade_app_progname" },
    { dsc => "opt_mailto",  var => \$opt_mailto,  dfl => "undefined" },
    { dsc => "opt_nocrcs",  var => \$opt_nocrcs,  dfl => 0,          },
);

my(@find_roots, $fileset);
my($all_cnt, $del_cnt, $own_cnt, $grp_cnt, $mde_cnt, $typ_cnt, $lnk_cnt);
my($crc_cnt, $mmn_cnt, $add_cnt, $sym_cnt, $schedflag_file, $hostname); 
#  The names of the days and monthes, used in date conversion
my(@day_names)  = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");

#  Log and state files should not be readable by common users
umask 077;

sub fadscan
{
    my($errstack_ref) = @_;
    my($rc, $confirm, $cmd, $pars, $lc_dayname);
    my($oldfad_stem, $log_file, $in_file);
    my(@find_filters, %headers, $combined_scan_args);
    my($nowsec, $nowmin, $nowhour, $nowmday, $nowmon, $nowyear, $nowwday, $nowyday, $nowisdst, $nowwday, $nowwday);
    my($in_handle);

    #
    #  Load defaults, command line options and stuff from config file
    #

    if (($rc=&load_options_and_config_file($errstack_ref)) != $ade_err_ok) {
        return($rc);
    }
    &ade_err_debug($errstack_ref, 10, "fadscan: config loaded");

    ##########################################################################
    #
    #  Process arguments
    #
    ##########################################################################

    if  ($opt_mode eq "schedule") {
        (!defined($ARGV[0]) || defined($ARGV[1])) && &ade_msg_usage($errstack_ref, \&app_usage, 1);
        $fileset = $ARGV[0];
    } else {
        (!defined($ARGV[1]) || defined($ARGV[2])) && &ade_msg_usage($errstack_ref, \&app_usage, 1);
        $fileset = $ARGV[0];
        $in_file = $ARGV[1];
    }

    ##########################################################################
    #
    #  Sanity checks and derivations
    #
    ##########################################################################

    if (! -d $opt_logdir) {
        &ade_err_error($errstack_ref, fadscan_err_misc, "can't access $opt_logdir");
        return($ade_err_fail);
    }
    if (! -d $opt_sttdir) {
        &ade_err_error($errstack_ref, fadscan_err_misc, "can't access $opt_sttdir");
        return($ade_err_fail);
    }

    #  Set some global variables that we'll want to access everywhere.
    $hostname = &Sys::Hostname::hostname;
    $hostname || &ade_err_error($errstack_ref, fadscan_err_misc, "can't determine hostname");
    ($nowsec, $nowmin, $nowhour, $nowmday, $nowmon, $nowyear, $nowwday, $nowyday, $nowisdst) = localtime;
    $oldfad_stem = "$opt_sttdir/$fileset-basesnap";
    ($lc_dayname = $day_names[$nowwday]) =~ tr/A-Z/a-z/;
    $log_file = "$opt_logdir/$fileset-$lc_dayname.gz";
    $schedflag_file = "$opt_sttdir/$fileset-scheduled";
    &ade_err_debug($errstack_ref, 10, "fadscan: log_file=$log_file, in_file=$in_file, schedflag_file=$schedflag_file, opt_mode=$opt_mode");
    

    ##########################################################################
    #
    #  Guts
    #
    ##########################################################################

    #  And here we make do all the work.
    &ade_tmp_registerfile($errstack_ref, $log_file);
    if (($rc=&ade_spc_rollingstatusmanager($errstack_ref,
                                           $opt_mode, 
                                           $schedflag_file, 
                                           \&fadscan_scan_cb, 
                                           \&fadscan_compare_cb, 
					   undef,
                                           $oldfad_stem, 
                                           ".gz",
                                           $log_file, 
                                           { opt_nocrcs => $opt_nocrcs, in_file => $in_file },
	                                   $ade_tmp_dir,
					   1)) != $ade_err_ok) {
        return($rc);
    }
    #  No need to delete it we get this far.
    &ade_tmp_deregisterfile($errstack_ref, $log_file);

    #  Mail the log file if necessary.
    if (($opt_mode eq "check" || $opt_mode eq "refresh") && $opt_mailto ne "undefined") {
        &ade_err_debug($errstack_ref, 4, "main: opening compressed log file ...");
        if (($rc=&ade_fcm_openreadcompressed($errstack_ref, $log_file, \*LOG_HANDLE)) != $ade_err_ok) {
            &ade_err_error($errstack_ref, fadscan_err_misc, "can't open $log_file");
            return($rc);
        }
        %headers = ();
        &ade_err_debug($errstack_ref, 4, "main: emailing log file ...");
        &ade_mua_addmailheader($errstack_ref, \%headers, "Subject: Fadscan ($hostname $fileset $day_names[$nowwday])");
        if (($rc=&ade_mua_sendmail($errstack_ref, \%headers, \*LOG_HANDLE, [ $opt_mailto ])) != $ade_err_ok) {
            return($rc);
        }
        close LOG_HANDLE;
    }

    #  Ensure sensible return code
    return($ade_err_ok);
}

sub app_version
{
    my($errstack_ref, $version_ref) = @_;
    my($rc);

    if (($rc=&ade_tmf_extractversionfromsvnstring($errstack_ref, $subversion_string, $version_ref)) != $ade_err_ok) {
        &ade_err_error($errstack_ref, ade_err_internal, "app_version: ade_tmf_extractversionfromsvnstring failed");
        return($rc);
    }

    return($ade_err_ok);
}

sub app_listpaths
{
    my($errstack_ref, $pathlist_ref) = @_;
    my($rc);

    %$pathlist_ref = ();
    return($ade_err_ok);
}

sub app_usage
{
    print "Usage:   $ade_app_progname [ <options> ] { -i | -c | -r } <scanid> <filelist-file>\n";
    print "         $ade_app_progname [ <options> ] -s               <scanid>\n";
    print "\n";
    #  standard options
    print "Options: -V | --version               display program version\n";
    print "         -v | --verbose               verbose\n";
    print "         -d | --debug=<level>         set debug level\n";
    print "         -h | --help                  display this text\n";
    print "         -p | --list-paths            list used paths\n";
    #  application-specific option
    print "         -i | --mode-init             operate in 'initialise' mode\n";
    print "         -c | --mode-check            operate in 'check' mode\n";
    print "         -r | --mode-refresh          operate in 'refresh' mode\n";
    print "         -s | --mode-schedule         operate in 'schedule' mode\n";
    print "              --log-dir=<logdir>      use alternate log directory\n";
    print "              --state-dir=<sttdir>    use alternate state info directory\n";
    print "         -m | --mail=<addr>           mail logs to <addr>\n";
    print "              --no-crcs               do not calculate CRCs\n";
    print "\n";

    return($ade_err_ok);
}

sub load_options_and_config_file
{
    my($errstack_ref) = @_;
    my($key, $old_sigwarn_handler, $i);

    #
    #  Unset everything.
    #
    #  (This function needs to be re-entrant;it can be called when the
    #  program starts, but it can also be called when SIGHUP is recieved.)
    #

    for ($i=0; $i<=$#config_hasharray; $i++) {
        undef ${$config_hasharray[$i]{var}};
    }

    #
    #  Process command line options (top priority).
    #

    #  Redirect GetOptions's alerts to our alerter.
    $old_sigwarn_handler = $SIG{'__WARN__'};
    $SIG{'__WARN__'} = sub { &ade_msg_usage($errstack_ref, \&app_usage, 1); };
    Getopt::Long::GetOptions(
        #  Standard opts
        "V|version"       => sub { &ade_msg_version($errstack_ref, \&app_version); },
        "v|verbose"       => sub { &ade_err_resetstack(level => 3); },
        "d|debug=i"       => sub { &ade_err_resetstack(level => $_[1]); },
        "h|help"          => sub { &ade_msg_usage($errstack_ref, \&app_usage, 0); },
        "p|list-paths"    => sub { &ade_msg_listpaths($errstack_ref, \&app_listpaths); },
        #  Application specific opts
        "i|mode-init"     => sub { $opt_mode = "init"; },
        "c|mode-check"    => sub { $opt_mode = "check"; },
        "r|mode-refresh"  => sub { $opt_mode = "refresh"; },
        "s|mode-schedule" => sub { $opt_mode = "schedule"; },
        "log-dir=s"       => \$opt_logdir,
        "state-dir=s"     => \$opt_sttdir,
        "m|mail=s"        => \$opt_mailto,
        "no-crcs"         => sub { $opt_nocrcs = 1; }
    );
    $SIG{'__WARN__'} = $old_sigwarn_handler;

    #
    #  Assign defaults (third priority therefore done third)
    #

    for ($i=0; $i<=$#config_hasharray; $i++) {
        #  Skip using default if variable already has value
        next if (defined(${$config_hasharray[$i]{var}}));
        #  Skip using default if there is no default!
        next if (!defined($config_hasharray[$i]{dfl}));
        #  Use default
        ${$config_hasharray[$i]{var}} = $config_hasharray[$i]{dfl};
    }

    #
    #  Derivations
    #
    #  Some values depend on others. Work those out now.
    #

    #
    #  Verify that everything is now set.
    #

    for ($i=0; $i<=$#config_hasharray; $i++) {
        next if (defined(${$config_hasharray[$i]{var}}));
        &ade_err_error($errstack_ref, ade_err_undefined, $config_hasharray[$i]{dsc},  "configuration parameter");
        return($ade_err_fail);
    }

    #
    #  Miscellaneous sanity checks
    #

    #
    #  If we get this far everything is ok.
    #

    return($ade_err_ok);
}

sub fadscan_compare_cb
{
    my ($errstack_ref, $oldfad_file, $newfad_file, $rollno) = @_;
    my ($rc, $fmtdatestart, $fmtdateend);
    my (%old_fad_headers, %headers);
    my ($diff_rc, $version);
    my (%old_store, %new_store);

    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: oldfad_file=$oldfad_file, newfad_file=$newfad_file, rollno=$rollno");

    #  Get the time of the report.
    $fmtdatestart = localtime;

    #  Open the two input files 
    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: opening the two fad files for comparison ...");
    if (($rc=&ade_fcm_openreadcompressed($errstack_ref, $oldfad_file, \*OLDFAD_HANDLE)) != $ade_err_ok) {
        &ade_err_error($errstack_ref, fadscan_err_misc, "can't open $oldfad_file");
        return($rc);
    }
    if (($rc=&ade_fcm_openreadcompressed($errstack_ref, $newfad_file, \*NEWFAD_HANDLE)) != $ade_err_ok) {
        &ade_err_error($errstack_ref, fadscan_err_misc, "can't open $newfad_file");
        return($rc);
    }
    
    #  Load fad files.
    &ade_err_debug($errstack_ref, 5, "fadscan_compare_cb: loading first FAD file ...");
    return($rc) if (($rc=&fad_load($errstack_ref, \*OLDFAD_HANDLE, \%old_store, undef)) != $ade_err_ok);
    &ade_err_debug($errstack_ref, 5, "fadscan_compare_cb: loading second FAD file ...");
    return($rc) if (($rc=&fad_load($errstack_ref, \*NEWFAD_HANDLE, \%new_store, undef)) != $ade_err_ok);


    #  Write report header.
    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: writing a report header ...");
    print "SECURITY VALIDATION PROCESS\n";
    print "===========================\n";
    print "\n";
    print "HOST:                                      $hostname\n";
    print "FILESET:                                   $fileset\n";
    print "START DATE:                                $fmtdatestart\n";
    print "LAST REFRESH DATE:                         " . 
            localtime($old_fad_headers{'Unix-Time'}) . "\n";
    if (($rc=&app_version($errstack_ref, \$version)) != $ade_err_ok) {
        return($rc);
    }
    print "FADSCAN:                                   $version\n";
    print "\n";

    #  Write the title of the differences report
    print "FILE BY FILE\n";
    print "------------\n";
    print "\n"; 
    
    #  Initialise some counters that are used to generate a report summary
    $all_cnt = 0;
    $del_cnt = 0; 
    $own_cnt = 0; 
    $grp_cnt = 0; 
    $mde_cnt = 0;  
    $typ_cnt = 0;  
    $lnk_cnt = 0;
    $crc_cnt = 0; 
    $mmn_cnt = 0; 
    $add_cnt = 0;   
    $sym_cnt = 0;

    #  Actually work out the differences processing them via a callback.
    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: calling fadstreams_to_diffstream() to diff the opened fad files ...");
    if (($rc=&fad_diff($errstack_ref, \%old_store, \%new_store, \&fadscan_processdiff_cb, { out_handle => \*STDOUT, del_cnt_ref => \$del_cnt, own_cnt_ref => \$own_cnt, grp_cnt_ref => \$grp_cnt, mde_cnt_ref => \$mde_cnt, typ_cnt_ref => \$typ_cnt, lnk_cnt_ref => \$lnk_cnt, crc_cnt_ref => \$crc_cnt, mmn_cnt_ref => \$mmn_cnt, add_cnt_ref => \$add_cnt, sym_cnt_ref => \$sym_cnt, all_cnt_ref => \$all_cnt })) != $ade_err_ok) {
        close NEWFAD_HANDLE;
        close OLDFAD_HANDLE;
        &ade_err_error($errstack_ref, fadscan_err_misc, "failed to work out differences");
    }

    #  We've finished reading the fad files now.
    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: comparison dome, closing fad files ...");
    close NEWFAD_HANDLE;
    close OLDFAD_HANDLE;
    print "\n"; 

    #  Write report footer.
    &ade_err_debug($errstack_ref, 10, "fadscan_compare_cb: writing a report footer ...");
    print "SUMMARY\n";
    print "-------\n";
    print "\n";
    print "Total files changed:                       $all_cnt\n";
    print "Added:                                     $add_cnt\n";
    print "Deleted:                                   $del_cnt\n";
    print "Owner changed:                             $own_cnt\n";
    print "Group changed:                             $grp_cnt\n";
    print "Mode changed:                              $mde_cnt\n";
    print "Links changed:                             $lnk_cnt\n";
    print "Checksum changed:                          $crc_cnt\n"; 
    print "Type changed:                              $typ_cnt\n"; 
    print "Symlink dest changed:                      $sym_cnt\n"; 
    print "Major minor no. changed:                   $mmn_cnt\n"; 
    print "\n";
    $fmtdateend = localtime;
    print "END DATE:                                  $fmtdateend\n";
    #  We've finished writing to the report

    return($ade_err_ok);
}

sub fadscan_scan_cb
{
    my ($errstack_ref, $param) = @_;
    my ($rc, $findroots_ref, $findfilters_ref);
    my (%store1, $collected_info_ref, $opt_nocrcs, $in_file, $in_handle);

    #  Unpack the two parameters from the one the callback interface allowed to pass.
    $opt_nocrcs  = ${$param}{opt_nocrcs};
    $in_file = ${$param}{in_file};
    
    &ade_err_debug($errstack_ref, 10, "fadscan: opening $in_file ...");
    if ($in_file eq "-") {
        $in_handle = \*STDIN;
    } elsif (!open(IN_HANDLE, $in_file)) {
        &ade_err_error($errstack_ref, fadscan_err_misc, "$in_file: can't open");
        return($rc);
    } else {
        $in_handle = \*IN_HANDLE;
    }

    &ade_err_debug($errstack_ref, 10, "fadscan_scan_cb: reading file list ...");
    while (<$in_handle>) {
        chomp;
        if (($rc=&fad_collect_info($errstack_ref, $_, $opt_nocrcs, \$collected_info_ref)) != $ade_err_ok) {
            close IN_HANDLE if ($in_file ne "-");
            return($rc);
        }
        if (($rc=&fad_insert_info($errstack_ref, \%store1, $collected_info_ref)) != $ade_err_ok) {
            close IN_HANDLE if ($in_file ne "-");
            return($rc);
        }
    }

    close IN_HANDLE if ($in_file ne "-");


    #  Dump store to stdout, which is redirected by ade_spc_rollingstatusmanager().
    &ade_err_debug($errstack_ref, 10, "fadscan_scan_cb: dumping store to stdout (which is redirected) ...");
    if (($rc=&fad_dump($errstack_ref, \*STDOUT, \%store1)) != $ade_err_ok) {
        return($rc);
    }

    return($ade_err_ok);
}

sub fadscan_processdiff_cb
{
    my($errstack_ref, $param, $change_ref) = @_;
    my($oldstate, $newstate, $changetype, $file);
    my($i);
    my($out_handle, $del_cnt_ref, $own_cnt_ref, $grp_cnt_ref, $mde_cnt_ref, $typ_cnt_ref, $lnk_cnt_ref, $crc_cnt_ref, $mmn_cnt_ref, $add_cnt_ref, $sym_cnt_ref, $all_cnt_ref);
    my($txt_typ, $txt_own, $txt_grp, $txt_mde, $txt_add, $txt_del, $txt_mmn, $txt_crc, $txt_lnk, $txt_sym);

    #  Unpack the many parameters from the one the callback interface allowed to pass.
    $out_handle  = ${$param}{out_handle};
    $del_cnt_ref = ${$param}{del_cnt_ref};
    $own_cnt_ref = ${$param}{own_cnt_ref};
    $grp_cnt_ref = ${$param}{grp_cnt_ref};
    $mde_cnt_ref = ${$param}{mde_cnt_ref};
    $typ_cnt_ref = ${$param}{typ_cnt_ref};
    $lnk_cnt_ref = ${$param}{lnk_cnt_ref};
    $crc_cnt_ref = ${$param}{crc_cnt_ref};
    $mmn_cnt_ref = ${$param}{mmn_cnt_ref};
    $add_cnt_ref = ${$param}{add_cnt_ref};
    $sym_cnt_ref = ${$param}{sym_cnt_ref};
    $all_cnt_ref = ${$param}{all_cnt_ref};

    #  Each output line will consist of all of these fields but with possibly
    #  a couple of them not blank.
    $txt_typ = "   ";
    $txt_own = "   ";
    $txt_grp = "   ";
    $txt_mde = "   ";
    $txt_add = "   ";
    $txt_del = "   ";
    $txt_mmn = "   ";
    $txt_crc = "   ";
    $txt_lnk = "   ";
    $txt_sym = "   ";

    #  For each sort of difference detected for the one file for which this callback was called ...
    for ($i=0; $i < $#{@{$change_ref}{changes}}+1; $i++) {
  
        #  Create some aliases
        $changetype = ${$change_ref}{changes}[$i]{type};
        $oldstate   = ${$change_ref}{changes}[$i]{old};
        $newstate   = ${$change_ref}{changes}[$i]{new};
        $file       = ${$change_ref}{file};
        &ade_err_debug($errstack_ref, 4, "fadscan_processdiff_cb:: changetype=$changetype, oldstate=$oldstate, newstate=$newstate, file=$file");

	#  Now turn on the appropriate bits of text
        if ($changetype eq $CT_OWNER) {
            ${$own_cnt_ref}++;
            $txt_own = "own";
        } elsif ($changetype eq $CT_GROUP) {
            ${$grp_cnt_ref}++;
            $txt_grp = "grp";
        } elsif ($changetype eq $CT_MODE) {
            ${$mde_cnt_ref}++;
            $txt_mde = "mde";
        } elsif ($changetype eq $CT_LINKS) {
            ${$lnk_cnt_ref}++;
            $txt_lnk = "lnk";
        } elsif ($changetype eq $CT_DELETED) {
            ${$del_cnt_ref}++;
            $txt_del = "del";
        } elsif ($changetype eq $CT_TYPE) {
            ${$typ_cnt_ref}++;
            $txt_typ = "typ";
        } elsif ($changetype eq $CT_ADDED) {
            ${$add_cnt_ref}++;
            $txt_add = "add";
        } elsif ($changetype eq $CT_CONT_CRC) {
            ${$crc_cnt_ref}++;
            $txt_crc = "crc";
        } elsif ($changetype eq $CT_CONT_SYMLINK) {
            ${$sym_cnt_ref}++;
            $txt_sym = "sym";
        } elsif ($changetype eq $CT_CONT_MAJMIN) {
            ${$mmn_cnt_ref}++;
            $txt_mmn = "mmn";
        } else {
            &ade_err_error($errstack_ref, ade_err_internal, "fadscan_processdiff_cb: unexpected CT: $changetype");
            return($ade_err_fail);
        }
    }
    ${$all_cnt_ref}++;

    #  and write the record with the file name tagged on the end.
    print $out_handle "$txt_add $txt_del $txt_typ $txt_own $txt_grp $txt_mde $txt_lnk $txt_crc $txt_sym $txt_mmn    $file\n";

    return($ade_err_ok);
}

&ade_gep_main(\&fadscan);
