#  $HeadURL$ $LastChangedRevision$
#spp comment vim: set filetype=perl: #

BEGIN {
#  This works, but it would be nicer to use a single
#  line with 'use' instead. But that will require that
#  either ade.pl calls the same thing, or that we
#  merge all those things into a single file.
use lib substr `ade-config ade_include_prefix`,0,-1;
require "ade.pl";
ADE->import(qw($ade_app_progname ade_gep_main ade_err_warning ade_err_internal ade_err_debug $ade_tmp_dir ade_tmp_deregisterfile ade_tmp_registerfile ade_err_registerdefderrs));
}

require File::Find;
require URI::Escape;

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

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

my($magic_number) = "FaDFiLe";
my($fad_version) = "3";
my($fad_sort_cmd) = substr `fad-config fad_sort_cmd`,0,-1;

#  File system object type descriptors
my($IT_FILE)    = "f";
my($IT_DIR)     = "d";
my($IT_PIPE)    = "p";
#  This one is not just used in this file, but also in fadfixperms.spp, and that
#  gets included fore this one, so it seems the use before the declaration.
$main::IT_SYMLINK = "l";
my($IT_BLOCK)   = "b";
my($IT_CHAR)    = "c";
my($IT_SOCKET)  = "s";

#  Change types. $main::CT_TYPE refers to a change in the items
#  type field as listed above.
$main::CT_DELETED = "d";
$main::CT_ADDED   = "a";
$main::CT_TYPE    = "t";
$main::CT_OWNER   = "u";
$main::CT_GROUP   = "g";
$main::CT_MODE    = "m";
$main::CT_LINKS   = "l";
my($CT_CONT)      = "c";		# this one isn't global

#  Psuedo-change types which allow calling functions not
#  to have to analyse what exactly a contents change
#  means for a particular object.
$main::CT_CONT_MAJMIN  = "s";  
$main::CT_CONT_CRC     = "C";
$main::CT_CONT_SYMLINK = "S";

#  This character is used as a separator in FAD files. It
#  should not be a character that is likely to appear in a 
#  file name. 
my($fs) = ":";
my($rs) = "\n";
my($header_fs) = " ";
my($header_rs) = "\n";

my($BY_REQUEST_NOT_CHECKED) = -1;	# won't clash with valid file contents
my($NOT_READABLE)           = -2;	# won't clash with valid file contents

#  In making comparions, sometimes we need a key that will
#  always be greater that the other key. This is such a key.
my($VERY_BIG_KEY) = "~~~~~~~~~~~~~~~~~~~~~~~~~~";

my($sort_file, $unames, $newinodenumber);

#  Because File::Find does not support passing *through* arbitrary
#  data (such as into which FAD store I want '&wanted' to write
#  records), the FAD records table must be global. But we can make
#  a slight improvement, by making only references to it global
#  and keeping the FAD store itself private (to some function
#  somewhere). So, actually, since they are global there is
#  no need to put any code here :-)

sub write_fad_header
{
    my($errstack_ref, $out_handle) = @_;
    my ($fs_escaped, $rs_escaped);

    if (($rc=&escape($errstack_ref, $fs, \$fs_escaped)) != $ade_err_ok) {
        return($rc);
    }
    if (($rc=&escape($errstack_ref, $rs, \$rs_escaped)) != $ade_err_ok) {
        return($rc);
    }

    #  Statutory headers
    print $out_handle $magic_number . $header_rs;
    print $out_handle "FAD-Version" . $header_fs . $fad_version . $rs;
    print $out_handle "Field-Separator" . $header_fs . $fs_escaped . $header_rs;
    print $out_handle "Record-Separator" . $header_fs . $rs_escaped . $header_rs;

    #  Version-specific headers
    print $out_handle "Unix-Time" . $header_fs . time . $header_rs;

    #  End of headers marker
    print $out_handle "EOH" . $header_rs;

    return($ade_err_ok);
}

sub fadstream_to_fadstore
{
    my($errstack_ref, $fadstream_handle, $attribs_ref, $names_ref, $madeupdevinodes_ref, $regexp) = @_;

    while (<$fadstream_handle>) {
        chomp;
        #  The regexp provides grep capability
        &insert_fadrecord_in_fadstore($errstack_ref, $_, $attribs_ref, $names_ref, $madeupdevinodes_ref) if (!defined($regexp) || /$regexp/);
    }

    return($ade_err_ok);
}

sub fadstore_to_fadstream
{
    my($errstack_ref, $out_handle, $attribs_ref, $names_ref, $madeupdevinodes_ref) = @_;
    my($key,$dev,$inode,$i,$j, $old_lc_all, $outputline);
    my($rc, @othernames);
    my($temporary_file_handle);
    my($suppressed_dev, $suppressed_inode);

    &ade_err_debug($errstack_ref, 40, "fadstore_to_fadstream: sof");
    $rc = 0;

    #  Write a magic number at the top of the FAD file
    &ade_err_debug($errstack_ref, 40, "fadstore_to_fadstream: writing FAD header to stream ...");
    if (($rc=&write_fad_header($errstack_ref, $out_handle)) != $ade_err_ok) {
        return($rc);
    }
    &ade_err_debug($errstack_ref, 40, "fadstore_to_fadstream: after writing header");

    #  Dump the FAD store - unsorted - to a temporary file
    &ade_err_debug($errstack_ref, 40, "fadstore_to_fadstream: writing unsorted FAD store onto $sort_file ...");
    &ade_tmp_registerfile($errstack_ref, $sort_file);
    if (!open(TMP1_HANDLE, ">$sort_file")) {
        &ade_err_error($errstack_ref, fad_err_misc, "can't open $sort_file");
        return($ade_err_fail);
    }
    foreach $key (sort {${$names_ref}{$a} cmp ${$names_ref}{$b}} (keys %{$names_ref})) {
        ($dev, $inode, $i) = split($;, $key);
        #  output all interesting information except list of other links.
        #  (In order to make the internally passed around fadrecord the same as the external
        #  one, that requires that the external one has dev and inode info too. But this
        #  is irrelevent in the outside world, so we just put blank ones in; this is clearer
        #  that putting the numbers there and then not using them, I think.)
        $suppressed_dev = "";
        $suppressed_inode = "";
        #printf TMP1_HANDLE join($fs, qw( %s %s %s %s %d %d %o %d %s )), ${$names_ref}{$key}, $suppressed_dev, $suppressed_inode, @{${$attribs_ref}{$dev, $inode}}{ qw( type uid gid mode nlink contents )};
        $outputline = sprintf(join($fs, qw( %s %s %s %s %d %d %o %d %s )), ${$names_ref}{$key}, $suppressed_dev, $suppressed_inode, @{${$attribs_ref}{$dev, $inode}}{ qw( type uid gid mode nlink contents )});
        #  If there are other links then also list them all except the
        #  one by which we're listing it of course!
        if (${$attribs_ref}{$dev,$inode}->{namecnt} != 1) {
            undef @othernames;
            for ($j=0; $j<${$attribs_ref}{$dev,$inode}->{namecnt}; $j++) {
                #  Don't print the name of the item itself in the list of other names!
                #  Actually, don't print it here at all, just collect the names we
		#  do want to print, and then we'll outpuit the list, but sorted, in
		#  a moment.
                push(@othernames, ${$names_ref}{$dev, $inode, $j}) if ($j != $i);
            }
            #print TMP1_HANDLE $fs . join($fs, sort(@othernames));
            $outputline .= $fs . join($fs, sort(@othernames));
        }
        $outputline .= $rs;
        #print TMP1_HANDLE $rs;
        print TMP1_HANDLE $outputline;
        &ade_err_debug($errstack_ref, 40, "fadstore_to_fadstream: wrote line [$outputline]");
    }
    close TMP1_HANDLE;
    &ade_err_debug($errstack_ref, 40, "fadstore_to_fadstream: done writing unsorted FAD store to $sort_file");

    #  now get unix's sort command to sort that temporary file and write the 
    #  sorted version to the passed handle.

    #  Sun Feb 20 17:40:09 MET 2000 - Alexis
    #  GNU sort is fussy (as should all sorts I guess) about locale.
    #  Without setting it to 'C', 'b' comes between '/a' and '/c'!
    $old_lc_all = $ENV{'LC_ALL'};
    $ENV{'LC_ALL'} = "C";
    &ade_err_debug($errstack_ref, 40, "fadstore_to_fadstream: slurping from pipe '$fad_sort_cmd < $sort_file |' ...");
    if (!open(FROMSORT_HANDLE, "$fad_sort_cmd < $sort_file |")) {
        &ade_err_error($errstack_ref, fad_err_misc, "can't call sort to sort $sort_file");
        return($ade_err_fail);
    }
    $ENV{'LC_ALL'} = $old_lc_all;
    print $out_handle <FROMSORT_HANDLE>;
    close FROMSORT_HANDLE;
    unlink $sort_file;
    &ade_tmp_deregisterfile($errstack_ref, $sort_file);
    &ade_err_debug($errstack_ref, 40, "fadstore_to_fadstream: done");

    #  sensible return code
    return($ade_err_ok);
}

sub collect_info
{
    my($errstack_ref, $name, $suppress_crcs, $collected_info_array_ref_ref) = @_;

    my($linktext, $type, $dev, $inode, $nlink, $uid, $gid, $rdev);
    my($size, $atime, $mtime, $ctime, $blksize, $blocks, $oldslash);
    my ($i, $j, $buf, $contents, $mode, $fad_record, $smode, $pname, $pcontents);

    &ade_err_debug($errstack_ref, 40, "collect_info: processing $_ ...");

    #  For all fs entities we identify their type. How do you spot a 
    #  symbolic link? Assume it is one and try seeing where it points.
    if ($linktext = readlink($name)) {
        $type = $main::IT_SYMLINK;
        ($dev,$inode,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = lstat($name);

    #  If we can't see where it goes then I guess it's not a symbolic link.
    } elsif (!stat($name)) {
        return($ade_err_fail);
    } else {
        ($dev,$inode,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat(_);
        $type = $IT_FILE   if -f _;
        $type = $IT_DIR    if -d _;
        $type = $IT_PIPE   if -p _;
        $type = $IT_SOCKET if -S _;
        $type = $IT_BLOCK  if -b _;
        $type = $IT_CHAR   if -c _;
    }

    #  Now based on what it was, certain bits of information
    #  are ignored, and certain extra information is ascertained. For
    #  a symlink, we note the destination it points to.
    if ($type eq $main::IT_SYMLINK) {
        #  Can symbolic links by hard linked ? 
        $nlink = 1;
        $contents = $linktext;
        $uid = 0;
        $gid = 0;

    #  For a directory there is nothing else to note.
    } elsif ($type eq $IT_DIR) {
        #  Can directories by hard linked ?
        $nlink = 1;
        $contents = 0;

    #  For a file we get the CRC, if we can, unless instructed not to.
    } elsif ($type eq $IT_FILE && $suppress_crcs) {
        $contents = $BY_REQUEST_NOT_CHECKED;
   
    } elsif ($type eq $IT_FILE) {
        #  \0 to protect against trailing spaces (or leading '>') which open() will 
 	#  ignore (see perlfaq5(1) man page).
        if (open(CRCFP, "$name\0")) {
            #  $oldslash = $/;
            #  undef $/;
            $contents = 0;
            while (read(CRCFP, $buf, 262144)) {
                $contents += unpack("%32C*", $buf);
                $contents = $contents % 32767;
            }
            #  $/ = $oldslash;
            close(CRCFP);
        } else {
            &ade_err_warning($errstack_ref, ade_err_access, $name, "open", $!);
            $contents = $NOT_READABLE;
        }

    #  For device files we save the major and minode numbers
    } elsif ($type eq $IT_BLOCK) {
        $contents = $rdev;

    } elsif ($type eq $IT_CHAR) {
        $contents = $rdev;

    
    #  For a socket there's nothing.
    } elsif ($type eq $IT_SOCKET) {
        $contents = 0;

    #  For a pipe there's nothing.
    } elsif ($type eq $IT_PIPE) {
        $contents = 0;
    }

    #  Additionally the mode contains some junk at the high order
    #  bits so we mask them off.
    $mode &= 07777;

    ${$collected_info_array_ref_ref} = [$name, $dev, $inode, $type, $uid, $gid, $mode, $nlink, $contents];

    return($ade_err_ok);
}

############################################################################### 
# 
# 
#      function:    &fadstreams_to_diffstream 
#
#      parameters:  file handle for first fad file
#                   file handle for second fad file
#                   callback function to report a difference
#                   file handle of output file (this is passed to the callback 
#                       function)
#
#      uses: 
#
#      calls:
#
#      sets:
#
#      returns:     nothing
#
#      description:
#
#
###############################################################################

sub fadstreams_to_diffstream
{
    my($errstack_ref, $inhandle1, $inhandle2, $callbackfnc, $outhandle, $diff_rc_ref) = @_;
    my($must_read_fadfile1, $must_read_fadfile2, $fadkey1, $fadkey2);
    my($fname1, $type1, $uid1, $gid1, $mode1, $nlink1, $contents1, $hardlinks1);
    my($fname2, $type2, $uid2, $gid2, $mode2, $nlink2, $contents2, $hardlinks2);
    my(@changes, $changed_file, $change, $fadline1, $fadline2, $remainder);
    my($unused_dev1, $unused_dev2, $unused_inode1, $unused_inode2, $rc);

    $must_read_fadfile1 = 1;
    $must_read_fadfile2 = 1;
    #  tell the caller than no differences have been found yet
    ${$diff_rc_ref} = 0;    
    while (!(eof($inhandle1) && eof($inhandle2))) {
        &ade_err_debug($errstack_ref, 40, "fadstreams_to_diffstream: start of read loop [$must_read_fadfile1, $must_read_fadfile2]");

        if ($must_read_fadfile1) {
            $must_read_fadfile1 = 0;
            &ade_err_debug($errstack_ref, 40, "fadstreams_to_diffstream: reading from file 1");
            if (!($fadline1 = <$inhandle1>)) {
                $fadkey1 = $VERY_BIG_KEY;
                $must_read_fadfile2 = 1;
                &ade_err_debug($errstack_ref, 40, "can't read from file 1");
            } else {
                chop($fadline1);
                &ade_err_debug($errstack_ref, 40, "fadstreams_to_diffstream: read from file 1 [$fadline1]");
                ($fadkey1, $remainder) = split(/$fs/, $fadline1);
            }
        }
    
        if ($must_read_fadfile2) {
            $must_read_fadfile2 = 0;
            &ade_err_debug($errstack_ref, 40, "fadstreams_to_diffstream: reading from file 2");
            if (!($fadline2 = <$inhandle2>)) {
                $fadkey2 = $VERY_BIG_KEY;
                $must_read_fadfile1 = 1;
                &ade_err_debug($errstack_ref, 40, "can't read from file 2");
            } else {
                chop($fadline2);
                &ade_err_debug($errstack_ref, 40, "fadstreams_to_diffstream: read from file 2 [$fadline2]");
                ($fadkey2, $remainder) = split(/$fs/, $fadline2);
            }
        }
    
        #  no changes for the current listings-pair detected yet ...
        undef(@changes);
        undef($changed_file);

        #  if file is in one but not in other then we have a deleted file
        if ($fadkey1 lt $fadkey2) {
            &ade_err_debug($errstack_ref, 30, "fadstreams_to_diffstream: $fadkey1: deleted");
            push(@changes, {
                               change_type => $main::CT_DELETED,
                           });
            if (($rc=&unescape($errstack_ref, $fadkey1, \$changed_file)) != $ade_err_ok) {
                return($rc);
            }
            $must_read_fadfile1 = 1;

        #  if file is in the other but not in the first then we have an added file
        } elsif ($fadkey2 lt $fadkey1) {
            &ade_err_debug($errstack_ref, 30, "fadstreams_to_diffstream: $fadkey2: added");
            push(@changes, {
                               change_type => $main::CT_ADDED,
                           });
            if (($rc=&unescape($errstack_ref, $fadkey2, \$changed_file)) != $ade_err_ok) {
                return($rc);
            }
            $must_read_fadfile2 = 1;

        #  otherwise we have entries in each listing for the current file, but
        #  those listings may be different so here check
        } elsif ($fadline1 ne $fadline2) {
            ($fname1, $unused_dev1, $unused_inode1, $type1, $uid1, $gid1, $mode1, $nlink1, $contents1, $hardlinks1) = split(/$fs/, $fadline1, 10);
            ($fname2, $unused_dev2, $unused_inode2, $type2, $uid2, $gid2, $mode2, $nlink2, $contents2, $hardlinks2) = split(/$fs/, $fadline2, 10);
            &ade_err_debug($errstack_ref, 30, "fadstreams_to_diffstream: $fadkey1: some sort of change");
            if ($type1 ne $type2) {
                push(@changes, {
                                   change_type => $main::CT_TYPE,
                                   old_value   => $type1,
                                   new_value   => $type2,
                               });
            } else {
                ($uid1 ne $uid2) && push(@changes, {
                                   change_type => $main::CT_OWNER,
                                   old_value   => $uid1,
                                   new_value   => $uid2,
                               });
                ($gid1 ne $gid2) && push(@changes, {
                                   change_type => $main::CT_GROUP,
                                   old_value   => $gid1,
                                   new_value   => $gid2,
                               });
                ($mode1 ne $mode2) && push(@changes, {
                                   change_type => $main::CT_MODE,
                                   old_value   => $mode1,
                                   new_value   => $mode2,
                               });
                (($nlink1 ne $nlink2) || ($hardlinks1 ne $hardlinks2)) && push(@changes, {
                                   change_type => $main::CT_LINKS,
                                   #  IMPORTANT: hard links need splitting, unescaping and 
				   #  passing as an array
                                   old_value   => $hardlinks1,
                                   new_value   => $hardlinks2,
                               });
                #  File contents checking is not as simple as 'if contents not equal
		#  record difference' because file contents can be one of three possible
		#  values: (1) a normal checksum, (2) $BY_REQUEST_NOT_CHECKED,
		#  (3) $NOT_READABLE. That gives nine possible content-type-pairs (five 
		#  non-symmetric).
		if ($type1 eq $IT_FILE) {
                    #  if either file was unreadable then error (5 of 9 cases)
                    #  (note that if the whole record from 1st file is identical to
		    #  the second and they (both) contain $NOT_READABLE then we don't
		    #  get this far because identical records are skipped higher up.
                    if ($contents1 == $NOT_READABLE || $contents2 == $NOT_READABLE) {
            		&ade_err_error($errstack_ref, fad_err_misc, "$fname1 or $fname2 was unreadable when read");
                        return($ade_err_fail);

                    #  second case is that both files are marked $BY_REQUEST_NOT_CHECKED
		    #  which is fine, no action necessary.
                    } elsif ($contents1 == $BY_REQUEST_NOT_CHECKED && $contents2 ==
			    $BY_REQUEST_NOT_CHECKED) {
                        &ade_err_debug($errstack_ref, 40, "fadstreams_to_diffstream: two not crc'd files");

                    #  third case is one has $BY_REQUEST_NOT_CHECKED and the other
		    #  doesn't. This is an error.
                    } elsif ($contents1 == $BY_REQUEST_NOT_CHECKED || $contents2 ==
			    $BY_REQUEST_NOT_CHECKED) {
                        &ade_err_error($errstack_ref, fad_err_misc, "only one of $fname1 and $fname2 was not CRC'ed");
                        return($ade_err_fail);
			
                    #  fourth case (only remaining possibility) is that both files
		    #  contain valid CRCs. So we record the difference.
                    } elsif ($contents1 != $contents2) {
                        push(@changes, {
                            change_type => $main::CT_CONT_CRC,
                            old_value   => $contents1,
                            new_value   => $contents2,
                        });
                    } 
                }
                if ($type1 eq $main::IT_SYMLINK && $contents1 ne $contents2) {
                     if (($rc=&unescape($errstack_ref, $contents1, \$contents1_escaped)) != $ade_err_ok) {
                         return($rc);
                     }
                     if (($rc=&unescape($errstack_ref, $contents2, \$contents2_escaped)) != $ade_err_ok) {
                         return($rc);
                     }
                     push(@changes, {
                                   change_type => $main::CT_CONT_SYMLINK,
                                   old_value   => $contents1_escaped,
                                   new_value   => $contents2_escaped,
                               });
                } elsif ($type1 eq $IT_BLOCK && $contents1 ne $contents2) {
                     push(@changes, {
                                   change_type => $main::CT_CONT_MAJMIN,
                                   old_value   => $contents1,
                                   new_value   => $contents2,
                               });
                } elsif ($type1 eq $IT_CHAR && $contents1 ne $contents2) {
                     push(@changes, {
                                   change_type => $main::CT_CONT_MAJMIN,
                                   old_value   => $contents1,
                                   new_value   => $contents2,
                               });
                }
            }
            if (($rc=&unescape($errstack_ref, $fadkey1, \$changed_file)) != $ade_err_ok) {
                return($rc);
            }
            $must_read_fadfile1 = 1;
            $must_read_fadfile2 = 1;

        #  Otherwise we have identical entries for the same file in both listings.
        } else {
            $must_read_fadfile1 = 1;
            $must_read_fadfile2 = 1;
        }

        #  report any changes detected
        if (defined($changed_file)) {
            if (($rc=&$callbackfnc($errstack_ref, $outhandle, $changed_file, @changes)) != 0) {
                &ade_err_debug($errstack_ref, 40, "fadstreams_to_diffstream: callbackfnc() failed, returning failure ...");
                return($rc);
            }
            #  tell the caller that differences were found
	    ${$diff_rc_ref} = 1;    
        }
    }

    #  If we get this far then success
    &ade_err_debug($errstack_ref, 40, "fadstreams_to_diffstream: returning success ...");
    return($ade_err_ok);
}

sub read_fad_header
{
    my($errstack_ref, $fad_handle, $read_header_ref) = @_;
    my($key, $val, %read_header);
    my($read_header_magic_number);
    my($read_header_line);

    &ade_err_debug($errstack_ref, 40, "read_fad_header: reading magic number");
    #  This already assumes that the record separator in the header ($rs_header)
    #  is a newline :-(
    if (!($read_header_magic_number = <$fad_handle>)) {
        return($ade_err_fail);
    }
    chomp($read_header_magic_number);
    
    #  Read in remaining header lines - up to 'EOH', and validate
    while (!eof($fad_handle)) {
        &ade_err_debug($errstack_ref, 40, "read_fad_header: about to read a line");
        $read_header_line = <$fad_handle>;
        chomp($read_header_line);
        ($key, $val) = split($header_fs, $read_header_line, 2);
        (defined($key) && !defined($val)) && ($val = 0);
        $read_header{$key} = $val;
        &ade_err_debug($errstack_ref, 40, "read_fad_header: read header (key=$key, val=$val)");
        ($key eq "EOH") && last;
    }

    #  Validate headers
    &ade_err_debug($errstack_ref, 40, "read_fad_header: validating magic number");
    if ($read_header_magic_number ne $magic_number) {
        return($ade_err_fail);
    }
    #  currently we don't have any users to support multiple FAD versions 
    #  for! :-)
    &ade_err_debug($errstack_ref, 40, "read_fad_header: validating fad version (file contains $read_header{'FAD-Version'})");
    if (($read_header{'FAD-Version'} ne "2.02" && $read_header{'FAD-Version'} ne "3")) {
        return($ade_err_fail);
    }
    #  record separator *MUST* be new line currently
    &ade_err_debug($errstack_ref, 40, "read_fad_header: validating record separator");
    if (($rc=&unescape($errstack_ref, $read_header{'Record-Separator'}, \$rs_unescaped)) != $ade_err_ok) {
        return($rc);
    }
    if ($rs_unescaped ne $rs) {
        return($ade_err_fail);
    }
    #  currently we don't act upon alternative field separators
    &ade_err_debug($errstack_ref, 40, "read_fad_header: validating field separator");
    if (($rc=&unescape($errstack_ref, $read_header{'Field-Separator'}, \$fs_unescaped)) != $ade_err_ok) {
        return($rc);
    }
    if ($fs_unescaped ne $fs) {
        return($ade_err_fail);
    }
    &ade_err_debug($errstack_ref, 40, "read_fad_header: validating end of header marker");
    if (!defined($read_header{'EOH'})) {
        return($ade_err_fail);
    }

    #  the remaining headers are simply ignored
    &ade_err_debug($errstack_ref, 40, "read_fad_header: validating free fields");

    #  delete the headers which are not the concern of other programs
    delete $read_header{'FAD-Version'};
    delete $read_header{'Record-Separator'};
    delete $read_header{'Field-Separator'};
    delete $read_header{'EOH'};

    #  The rest are for things higher up (e.g. fadscan uses Unix-Time in
    #  the report headers.
    if (defined($read_header_ref)) {
        %{$read_header_ref} = %read_header;
    }

    return($ade_err_ok);
}

sub modify_fadrecord_from_fadstore_by_name
{
    my($errstack_ref, $name, $attribs_ref, $names_ref, $madeupdevinodes_ref, $fad_record) = @_;
    my($rc);

    if (($rc = &delete_fadrecord_from_fadstore_by_name($errstack_ref, $name, $attribs_ref, $names_ref, $madeupdevinodes_ref)) != $ade_err_ok) {
        return($rc);
    } elsif (($rc = &insert_fadrecord_in_fadstore($errstack_ref, $fad_record, $attribs_ref, $names_ref, $madeupdevinodes_ref)) != $ade_err_ok) {
        return($rc);
    }

    return($ade_err_ok);
}

sub delete_fadrecord_from_fadstore_by_name
{
    my($errstack_ref, $name, $attribs_ref, $names_ref, $madeupdevinodes_ref) = @_;
    my($dev, $inode, $deleted_index, $i, $key, $names_dev, $names_inode);

    #  Get the dev and inode (which uniquely identifies the filesystem item itself)
    if (($rc=&escape($errstack_ref, $name, \$escaped_name)) != $ade_err_ok) {
        return($rc);
    }
    ($dev, $inode) = @{${$madeupdevinodes_ref}{$escaped_name}}{ qw( dev inode ) };
    &ade_err_debug($errstack_ref, 40, sprintf("delete_fadrecord_from_fadstore_by_name: dev/inode=%d/%d (name=%s), count=%d", $dev, $inode, $name, ${$attribs_ref}{$dev, $inode}->{namecnt}));

    #  What happens next depends whether there are multiple names for this item or
    #  not. This we can determine by looking at the 'namecnt' field in %attribs.
    if (${$attribs_ref}{$dev,$inode}->{namecnt} == 1) {

        #  If there is only one name, then we can delete all the records from
        #  the three passed hashes which refer to the passed name.
        delete ${$attribs_ref}{$dev,$inode};
        delete ${$madeupdevinodes_ref}{$escaped_name};
	#  The '0' is the index of the first - and in this case *only* - name for
	#  the filesystem item specified by the (dev,inode) pair.
        delete ${$names_ref}{$dev,$inode,0};

    } elsif (${$attribs_ref}{$dev,$inode}->{namecnt} > 1) {

        #  If there is more than one name then things are a little more complicated.
        #  First, we can simply delete the 'name to (dev,inode) lookup table.'
        delete ${$madeupdevinodes_ref}{$escaped_name};
        
        #  Second, and this is the complicated one, *somewhere* in the names hash,
        #  is stored the record which says that the nth name of the filesystem
        #  item identified by the (dev,inode) pair. Unfortunately, we don't know
        #  where it it. So we have to scan the table. We have something like 
        #  this:
        #
        #  $names(123,456,0) == "/some/file/or/other"
        #  $names(789,111,0) == "/bin/ed"
        #  $names(789,111,1) == "/bin/edit"
        #  $names(789,111,2) == "/bin/ex"
        #  $names(987,333,0) == "/some/other/stuff"
        #
        #  So when we delete the record (e.g. for "/bin/edit" in the example) we
        #  may be left with a missing name index (i.e. 0,2 in the example above),
        #  so we need to shuffle everything up, by modifying the indexes (i.e.
        #  to 0,1). Actually shuffling up is a bit difficult, 'cos either we
        #  need some temporary storage to move (not copy) the records we want to
        #  keep into, before we move those which we moved out (and didn't just
        #  delete) back in ... Hmm .... simple is to scan all the names for the
        #  fs item, delete the one we want to delete, but not its index, and
        #  then move the last item into that noted index's slot. (We undef 
        #  the index of the noted deleted item first, so that after we can 
        #  make a sanity test.)
        undef $deleted_index;
        foreach $key (keys %{$names_ref}) {
            ($names_dev, $names_inode, $i) = split($;, $key);
            next if ($names_dev != $dev || $names_inode != $inode);
            next if ${$names_ref}{$dev,$inode,$i} ne $escaped_name;
            $deleted_index = $i;
            #  We can make an optimisation here: if we have just deleted the
            #  name, then there is no point continuing the scan to the end of
            #  the hash, since we've already found what we were looking for.
            last;
        }
      	#  Ok, so now we move the last name for this item into the 
      	#  freed slot (assuming it wasn't the *last* name that we actually
      	#  deleted!) First we just make a sanity check that we didn't
      	#  try to delete an undefined item - but this should never be the
      	#  case.
      	&ade_err_internal($errstack_ref,"tried to delete unknown name ($name)!") if (!defined($deleted_index));
        if ($deleted_index != ${$attribs_ref}{$dev,$inode}->{namecnt} - 1) {
            ${$names_ref}{$dev,$inode,$deleted_index} = ${$names_ref}{$dev,$inode,${$attribs_ref}{$dev,$inode}->{namecnt}-1};
        }
        
        #  And finally, we decrement the name count in the attribs hash. (i.e.
        #  we say that there is one less name referring to this fs item).
        ${$attribs_ref}{$dev,$inode}->{namecnt}--;

    } else {
        &ade_err_internal($errstack_ref, sprintf("invalid name count %d for item '%s'", ${$attribs_ref}{$dev,$inode}->{namecnt}, $name));
    }

    return($ade_err_ok);
}

#
#  Remember: FAD records contain *already* escaped names.
#

sub insert_fadrecord_in_fadstore
{
    my($errstack_ref, $fad_record, $attribs_ref, $names_ref, $madeupdevinodes_ref) = @_;
    my($name, $type, $uid, $gid, $mode, $nlink, $contents, @othernames, $smode, $dev, $inode);
    my($i, $j, $escaped_contents, $unescaped_name);

    #&ade_err_debug($errstack_ref, 40, "insert_fadrecord_in_fadstore: sof: FAD store contains " . scalar(keys(%{$attribs_ref})) . " record(s)");

    #  The 'store' is actually three associate arrays. 
    #
    #  The first is %attribs which is indexed by device and inode, 
    #  and contains the type, uid, gid, mode, number of hard links, contents.
    #  Actually it also contains the dev and inode, but these are only used internally;
    #  as soon as a fadrecord is written to file these are stripped since they
    #  are not transportable (from host to host or point in time to point in time).
    #
    #  The second is %names which is indexed by device, inode number *and*
    #  name index number. It contains the name of the file.
    #
    #  The third is %madeupdevinodes. This maps file names to dev/inode pairs.
    #  It is used at printout time, to put the *other* names by which a file
    #  is known.
    #
    #  Certain calls to insert_fadrecord_in_fadstore()
    #  are immediately after calls to filename_to_fadrecord() - in which case the
    #  device and inode number can be passed. But in other situations a
    #  record is passed without actually examining the file explicitly and
    #  so the device and inode number are unknown.
    #
    #  To get round this we simply *make up* device and inode references,
    #  but use the information about hard linked items to re-serve previously
    #  made up device/inode pairs where appropriate. So for this function's
    #  reference only, we have another array which maps names to inode/device
    #  pairs.
   
    #  Although the record has winged its way around as a packed (join'ed) scalar,
    #  the FAD store stores the attributes in a more detailed and accessible manner
    #  though this is not meant for direct access by applications. So we start
    #  by chopping the record back up ...

    #  IMPORTANT NOTE: *EITHER* the record we split here contains some othernames
    #  (if the record was read from a file) *OR* the dev and inode are set because
    #  we're passing around data about a recently (i.e. in the same process) scanned
    #  file. *BOTH IS IMPOSSIBLE*.

    &ade_err_debug($errstack_ref, 40, "insert_fadrecord_in_fadstore: fad_record=$fad_record");

    if (($rc=&unpack_fadrec($errstack_ref, $fad_record, \$unpacked_fadrec_array_ref)) != $ade_err_ok) {
        return($rc);
    }
    ($name, $dev, $inode, $type, $uid, $gid, $smode, $nlink, $contents, @othernames) = @{$unpacked_fadrec_array_ref};
    &ade_err_debug($errstack_ref, 40, "insert_fadrecord_in_fadstore: smode=$smode");

    #  For historic reasons we turn the mode from an '0...'-style string back into a
    #  number here, but really if we do that then we ought also to de-uriescape the
    #  file name (and contents if is symlink). Perhaps this should be moved inside
    #  &unpack_fadrec().
    #$mode = oct($smode);
    $mode = $smode;
    &ade_err_debug($errstack_ref, 40, "insert_fadrecord_in_fadstore: mode=$mode");

    #  determine dev and inode number for name - and record hardlinks if new
    if (defined($dev) && $dev && defined($inode) && $inode) {
        &ade_err_debug($errstack_ref, 40, "insert_fadrecord_in_fadstore: name=$name, using passed dev and inode (dev=$dev, inode=$inode) ...");
    } elsif (defined(${$madeupdevinodes_ref}{$name})) {
        &ade_err_debug($errstack_ref, 40, "insert_fadrecord_in_fadstore: using previously gen'd dev and inode");
        $dev = ${$madeupdevinodes_ref}{$escaped_name}->{dev};
        $inode = ${$madeupdevinodes_ref}{$escaped_name}->{inode};
    } else {
        &ade_err_debug($errstack_ref, 40, "insert_fadrecord_in_fadstore: gen'ing dev and inode ...");
        $dev = 123;
        $inode = ++$newinodenumber;
    }

    #  Previously we only recorded the dev and inode of the name if the dev and inode
    #  were made up (e.g. by fadcat). But delete_fadrecord_from_fadstore() uses the
    #  madeupdevinodes hash as its first port of call, so we must now unconditionally
    #  insert it. (Ok, this is redundant if the entry already exists, but it does no
    #  harm and it simplifies the code.
    ${$madeupdevinodes_ref}{$name} = { dev => $dev, inode => $inode };
        
    #  Even in the low memory version we push the info into the structures
    #  But in the low memory version, we immediately print it and delete it
    #  if we have encounted all hardlinks (or there were no other hardlinks).
    if (!defined(${$attribs_ref}{$dev, $inode}->{namecnt})) {
        ${$attribs_ref}{$dev, $inode}->{type}     = $type;
        ${$attribs_ref}{$dev, $inode}->{uid}      = $uid;
        ${$attribs_ref}{$dev, $inode}->{gid}      = $gid;
        ${$attribs_ref}{$dev, $inode}->{mode}     = $mode;
        ${$attribs_ref}{$dev, $inode}->{nlink}    = $nlink;
        ${$attribs_ref}{$dev, $inode}->{contents} = $contents;
        #  we must initialise this to zero since it gets used as a key in
        #  a hash, which is scanned *numerically*. (Failure to set it to 
        #  zero results in looking for an entry with key '0' which doesn't
        #  exist.
        ${$attribs_ref}{$dev, $inode}->{namecnt}  = 0;
    } 
    ${$names_ref}{$dev, $inode, ${$attribs_ref}{$dev, $inode}->{namecnt}} = $name;
    
    ${$attribs_ref}{$dev, $inode}->{namecnt}++;
    &ade_err_debug($errstack_ref, 40, sprintf("insert_fadrecord_in_fadstore: dev/inode=%d/%d (name=%s), count=%d", $dev, $inode, $name, ${$attribs_ref}{$dev, $inode}->{namecnt}));
    
    #  This little bit of code is used by 'fadcat' only. If othernames 
    #  were in the record then note that these names are references to 
    #  the same disk item for future reference.
    foreach (@othernames) {
        &ade_err_debug($errstack_ref, 40, "insert_fadrecord_in_fadstore: noting $_ has same dev/inode as $name");
        ${$madeupdevinodes_ref}{$_} = ${$madeupdevinodes_ref}{$name};
    }

    &ade_err_debug($errstack_ref, 40, sprintf("insert_fadrecord_in_fadstore: name=%s: after insertion: attribs-count=%d, names-count=%d, madeupdevinodes-count=%d", $name, scalar(keys(%{$attribs_ref})), scalar(keys(%{$names_ref})), scalar(keys(%{$madeupdevinodes_ref}))));
}

sub load_config_file
{
    my($errstack_ref, $config_file, $find_filters_ref, $find_roots_ref, $other_tuples_ref) = @_;
    my($lineno);

    if (!open(CONFIG_HANDLE, $config_file)) {
        &ade_err_error($errstack_ref, fad_err_misc, "can't open $config_file");
        return($ade_err_fail);
    }
    while (<CONFIG_HANDLE>) {
        chomp;
        $lineno++;
  
        #  Ignore blank lines and hash-led comments
        (/^\s*$/ || /^\s*\#.*$/) && next;

        #  If the line starts with a "r" then its a root definition line
        if (/^r\s+(\S+)\s*$/) {
            if ($1 =~ /^[^\/]/) {
                &ade_err_error($errstack_ref, fad_err_misc, "$config_file:$lineno: non-absolute root");
                return($ade_err_fail);
            }
            if (-e $1) {
                push(@{$find_roots_ref}, $1);
            } else {
                &ade_err_warning($errstack_ref, "$config_file:$lineno: $1: $!");
            }

        #  If the line starts with a "i" or "e" then its a filter
        } elsif (/([ie])\s+(.*?)\s*$/) {
            if (!&ade_smf_isvalidregexp($2)) {
                &ade_err_error($errstack_ref, fad_err_misc, "$config_file:$lineno: bad regexp");
                return($ade_err_fail);
            }
            push(@{$find_filters_ref}, {
                 type    => $1,
                 pattern => $2,
            });
        } else {
            #  If no array for other config pars supplied then error
            &ade_err_internal($errstack_ref,"$config_file:$lineno: entry") if ($other_tuples_ref == undef);
            #  'split' without params splits $_ on whitespace - exactly what we want!
            push(@{$other_tuples_ref}, [ split ]);
        }
    }
    close CONFIG_HANDLE;

    return($ade_err_ok);
}

sub findhier_to_fadstore
{
    my($errstack_ref, $find_filters_ref, $find_roots_ref, $attribs_ref, $names_ref, $madeupdevinodes_ref, $suppress_crcs) = @_;

    &ade_err_debug($errstack_ref, 50, "findhier_to_fadstore: sof");
    #  File::Find will call &wanted and that will need to list of filters
    #  and the FAD store. Since we can't pass arbitrary variables through
    #  File::Find's interface, we put refs to the stuff into globals.
    $main::attribs_ref = $attribs_ref;
    $main::names_ref = $names_ref;
    $main::madeupdevinodes_ref = $madeupdevinodes_ref;
    $main::find_filters_ref = $find_filters_ref;
    $main::suppress_crcs = $suppress_crcs;
    &File::Find::find(\&wanted, @{$find_roots_ref});

    return($ade_err_ok);
}

#  This function needs improving - 54% of execution time is spent in this
#  function (not calls).

sub wanted
{
    my($errstack_ref, $passed, $i, $j, $dev);
    local($_);
    my($find_filters_ref, $suppress_crcs, $attribs_ref, $names_ref, $madeupdevinodes_ref) = ($main::find_filters_ref, $main::suppress_crcs, $main::attribs_ref, $main::names_ref, $main::madeupdevinodes_ref);

    #  Check all the filters for something that matches $name (the name of
    #  the file passed by &find.
    for ($i=0, $passed=1; $i <= $#{$find_filters_ref}; $i++) {

        #  If we have a matching file, set $passed accordingly.
        if ($File::Find::name =~ /^${$find_filters_ref}[$i]{pattern}$/) {
            $passed = (${$find_filters_ref}[$i]{type} eq "i") ? 1 : 0;
        }

        #  The above is actually enough! But we can make a substatantial
        #  optimization: if the pattern is for excluding $name/.* then we
        #  can prune this branch, *providing* there are no subsequent 
	#  inclusions which would mean that this exclused directory could
	#  contain something we need to include.

        #  So if the *exclusion* itself (not the thing excluded) is of
	#  the form '.../.*' and the current name matches the bit without
	#  the '/.*' and the thing is a directory, (and the pattern is
	#  an exclusion, not an inclusion) ...
        if (${$find_filters_ref}[$i]{pattern} =~ m!^(.*)/\.\*$! && $File::Find::name =~ /^$1$/ && -d $File::Find::name && ${$find_filters_ref}[$i]{type} eq "e") {
            #  ... search all remaining patterns ...
            for ($j=$i+1; $j<=$#{$find_filters_ref}; $j++) {
                #  ... and if there is an inclusion of the form $name/something then
                #  $name cannot be pruned
                (${$find_filters_ref}[$j]{type} eq "i" && ${$find_filters_ref}[$j]{pattern} =~ /^\Q$File::Find::name\E\/.+$/) && last;
                #  If there is an inclusion of the form .*something then 
                #  $name cannot be pruned
                (${$find_filters_ref}[$j]{type} eq "i" && ${$find_filters_ref}[$j]{pattern} =~ /^\.\*/) && last;
            }
            #  If we got to the end of that search loop (without 'last'ing out
	    #  on an inclusion) then we may prune
            $File::Find::prune |= 1 if ($j > $#{$find_filters_ref});
        }
    }

    #  If after all the filtering, $name is not to be included then return before
    #  sticking it in store.
    return ($ade_err_ok) if (!$passed);

    #  Stick it in the store.
    if (($rc=&collect_info($errstack_ref, $File::Find::name, $suppress_crcs, \$collected_info_array_ref)) != $ade_err_ok) {
        return($rc);
    }
    if (($rc=&pack_fadrec($errstack_ref, $collected_info_array_ref, \$packed_fadrec)) != $ade_err_ok) {
        return($rc);
    }
    if (($rc=&insert_fadrecord_in_fadstore($errstack_ref, $packed_fadrec, $attribs_ref, $names_ref, $madeupdevinodes_ref)) != $ade_err_ok) {
        return($rc);
    }

    #  Return code not used by caller, which is a perl module.
}

sub select_name_from_fadstore
{
    #  Of the three variables which identify the FAD store, only $names_ref
    #  is actually used here. The remaining two are supplied simply in order to 
    #  have a consistent interface.
    my($errstack_ref, $attribs_ref, $names_ref, $madeupdevinodes_ref, $namearray_ref) = @_;

    @{$namearray_ref} = (map { &unescape($_) } values %{$names_ref});

    return($ade_err_ok);
}

sub escape
{
    my ($errstack_ref, $what, $what_escaped_ref) = @_;
    ${$what_escaped_ref} = &URI::Escape::uri_escape($what, "%$fs$rs");

    return($ade_err_ok);
}

sub unescape
{
    my ($errstack_ref, $what, $what_unescaped_ref) = @_;
    ${$what_unescaped_ref} = &URI::Escape::uri_unescape($what);

    return($ade_err_ok);
}

sub pack_fadrec
{
    #my ($errstack_ref, $name, $dev, $inode, $type, $uid, $gid, $mode, $nlink, $contents) = @_;
    my ($errstack_ref, $collected_info_array_ref, $packed_fadrec_ref) = @_;
    my ($escaped_name);

    ($name, $dev, $inode, $type, $uid, $gid, $mode, $nlink, $contents) = @{$collected_info_array_ref};
    &ade_err_debug($errstack_ref, 40, "pack_fadrec: name=$name, dev=$dev, inode=$inode ...");

    if (($rc=&escape($errstack_ref, $name, \$escaped_name)) != $ade_err_ok) {
        return($rc);
    }

    ${$packed_fadrec_ref} = join($fs, 
        #  The item name can contain dangerous characters, so escape them
        $escaped_name,
        $dev, 
        $inode, 
        $type, 
        $uid, 
        $gid, 
        #  In the fadstore and in files, the mode is in octal
        sprintf("%o", $mode),
        $nlink, 
        #  Symlinks destinations can contain dangerous characters, so escape them, note 
    	#  that '%' must also be escaped otherwise filenames which include URI escape
        #  *already* will be unURI'd when unpacked, and that will result in mismatching
	#  filenames.
        ($type eq $main::IT_SYMLINK) ? &escape($contents) : $contents);

    return($ade_err_ok);
}

sub select_fadrecord_from_fadstore_by_name
{
    #  Of the three variables which identify the FAD store, only $names_ref and 
    #  $attribs_ref are actually used here. The remaining one is supplied simply in
    #  order to have a consistent interface.
    my($errstack_ref, $name, $attribs_ref, $names_ref, $madeupdevinodes_ref, $packed_fadrec_ref) = @_;
    #&ade_err_debug($errstack_ref, 40, sprintf("select_fadrecord_from_fadstore_by_name: name=%s, attribs-count=%d, names-count=%d, madeupdevinodes-count=%d", $name, scalar(keys(%{$attribs_ref})), scalar(keys(%{$names_ref})), scalar(keys(%{$madeupdevinodes_ref})))); 
    my($dev, $inode, $type, $uid, $gid, $mode, $nlink, $contents);

    #  From the name, get the dev and inode.
    ($dev, $inode) = @{${$madeupdevinodes_ref}{&escape($name)}}{ qw( dev inode ) };

    #&ade_err_debug($errstack_ref, 40, "select_fadrecord_from_fadstore_by_name: for $name, retrieved dev=$dev,inode=$inode");

    #  Why do I bother reassembling the whole record? Why just pass the record?
    $type = ${$attribs_ref}{$dev, $inode}{type};
    $uid = ${$attribs_ref}{$dev, $inode}{uid};
    $gid = ${$attribs_ref}{$dev, $inode}{gid};
    $mode = ${$attribs_ref}{$dev, $inode}{mode};
    $nlink = ${$attribs_ref}{$dev, $inode}{nlink};
    $contents = ${$attribs_ref}{$dev, $inode}{contents};
    $contents = ($type eq $main::IT_SYMLINK) ? &unescape($contents) : $contents;
    if (($rc=&pack_fadrec($errstack_ref, [$name, $dev, $inode, $type, $uid, $gid, $mode, $nlink, $contents], $packed_fadrec_ref)) != $ade_err_ok) {
        return($rc);
    }

    return($ade_err_ok);
}

sub unpack_fadrec
{
    my ($errstack_ref, $fadrecord, $unpacked_fadrec_array_ref_ref) = @_;
    my ($name, $dev, $inode, $type, $uid, $gid, $mode, $nlink, $contents);
    my (@escaped_othernames, $escaped_name, $escaped_contents, @othernames);

    ($escaped_name, $dev, $inode, $type, $uid, $gid, $mode, $nlink, $escaped_contents, @escaped_othernames) = split($fs, $fadrecord);

    if ($type eq $main::IT_SYMLINK) {
        if (($rc=&unescape($errstack, $escaped_contents, \$contents)) != $ade_err_ok) {
            return($rc);
        }
    } else {
        $contents = $escaped_contents;
    }

    if (($rc=&unescape($errstack, $escaped_name, \$name)) != $ade_err_ok) {
        return($rc);
    }

    @othernames = map { 
        if (($rc=&unescape($errstack_ref, $_, \$unescaped_underscore_ref)) != $ade_err_ok) {
            return($rc);
        }
        $unescaped_underscore_ref;
    } @escaped_othernames;

    ${$unpacked_fadrec_array_ref_ref} = [$name, $dev, $inode, $type, $uid, $gid, oct($mode), $nlink, $contents, @othernames];

    return($ade_err_ok);
}

sub version_fad
{
    my ($errstack_ref) = @_; shift;

    printf "FAD format version: $fad_version\n";
    printf "FAD sort method:    %s\n", $fad_sort_cmd ? "external" : "internal"; 
    printf "FAD record sorter:  $fad_sort_cmd\n" if ($fad_sort_cmd);
}

sub _fad_loadtimechecks
{
    my ($errstack_ref) = @_; shift;

    chop($unames = `uname -s`);
    if ($ENV{'FAD_SORT_CMD'}) {
        $fad_sort_cmd = $ENV{'FAD_SORT_CMD'};
    } elsif ($unames eq 'HP-UX') {
        $fad_sort_cmd = "/usr/bin/sort -t%s -k1,1";
    } elsif ($unames eq 'SunOS') {
        $fad_sort_cmd = "/usr/bin/sort -t%s +0 -1";
    } elsif ($unames ne 'Linux') {
        &ade_err_error($errstack_ref, fad_err_misc, "don't know how to sort under $unames; set FAD_SORT_CMD");
        return($ade_err_fail);
    } elsif (-f "/etc/debian_version") {
        $fad_sort_cmd = "/usr/bin/sort -t%s -k 1,1";
    } elsif (-f "/etc/redhat-release") {
        $fad_sort_cmd = "/bin/sort -t%s +0 -1";
    } else {
        &ade_err_error($errstack_ref, fad_err_misc, "don't know how to sort under this distribution of Linux; set FAD_SORT_CMD");
        return($ade_err_fail);
    }
    
    #  check sort command defined
    if (!$fad_sort_cmd) {
        &ade_err_error($errstack_ref, fad_err_misc, "sorting command not defined!");
	return($ade_err_fail);
    }
    
    #  check it's absolute
    if ($fad_sort_cmd !~ /^\//) {
        &ade_err_internal($errstack_ref, "FAD_SORT_CMD ($fad_sort_cmd) not absolute");
    }
    
    #  check it's executable
    if (! -x ($fad_sort_cmd =~ /^([^\s]+)/)[0]) {
        &ade_err_error($errstack_ref, fad_err_misc, "can't execute $fad_sort_cmd");
	return($ade_err_fail);
    }
    #  Change the field separator in the sort command if it's defined
    $fad_sort_cmd = sprintf($fad_sort_cmd, $fs);
    
    #  Name of a temporary scratch file created when &mkfad is called in
    #  low memory mode.
    $sort_file = "$ade_tmp_dir/$ade_app_progname.$$.sort";
}

&_fad_loadtimechecks(undef);
