#!/usr/bin/perl
$VER = "\"1.73\"";
#
# This program is based on the 'mactime' program by Dan Farmer and
# and the 'mac_daddy' program by Rob Lee.
#
# It takes as input data from either 'ils -m' or 'fls -m' (from The Sleuth
# Kit) or 'mac-robber'.
# Based on the dates as arguments given, the data is sorted by and
# printed.
#
#
# The Sleuth Kit
# Brian Carrier [carrier@sleuthkit.org]
# Copyright (c) 2003 Brian Carrier.  All rights reserved
#
# TASK
# Copyright (c) 2002 Brian Carrier, @stake Inc.  All rights reserved
#
# 
# The modifications to the original mactime are free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# The modifications to the original mactime are distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License
# along with mactime; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#
#
# Copyright 1999 by Dan Farmer.  All rights reserved.  Some individual
# files may be covered by other copyrights (this will be noted in the
# file itself.)
#
# Redistribution and use in source and binary forms are permitted
# provided that this entire copyright notice is duplicated in all such
# copies.
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR ANY PARTICULAR PURPOSE.
#
# IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, LOSS OF USE, DATA, OR PROFITS OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#


$debug = 0; 

use Date::Manip;

# %month_to_digit = ("Jan", 1, "Feb", 2, "Mar", 3, "Apr", 4, "May", 5, "Jun", 6,
#	"Jul", 7, "Aug", 8, "Sep", 9, "Oct", 10, "Nov", 11, "Dec", 12);
%digit_to_month = ("01", "Jan", "02", "Feb", "03", "Mar", "04", "Apr", "05", 
	"May", "06", "Jun", "07", "Jul", "08", "Aug", "09", "Sep", "10", 
	"Oct", "11", "Nov", "12", "Dec");
%digit_to_day = ("0", "Sun", "1", "Mon", "2", "Tue", "3", "Wed",
  "4", "Thu", "5", "Fri", "6", "Sat");

sub usage {
print <<EOF;
mactime [-b body_file] [-p password_file] [-g group_file] [-i day|hour idx_file] [-d] [-h] [-V] [-y] [-z TIME_ZONE] [DATE]
	-b: Specifies the body file location, else STDIN is used
	-d: Output timeline and index file in comma delimited format
	-h: Display a header with session information
	-i [day | hour] file: Specifies the index file with a summary of results
	-g: Specifies the group file location, else GIDs are used
	-p: Specifies the password file location, else UIDs are used
	-V: Prints the version to STDOUT
	-y: Dates have year first (yyyy/mm/dd) instead of (mm/dd/yyyy)
	-z: Specify the timezone the data came from (in the local system format)
	[DATE]: starting date (01/01/2002) or range (01/01/2001-02/01/2002)
EOF
	exit(1);
};


sub version {
	print "The Sleuth Kit ver $VER\n"; 
}


$BODY = "";
$GROUP = "";
$PASSWD = "";
$TIME = "";
$INDEX = "";		# File name of index
$INDEX_TYPE = $INDEX_DAY;		# Saved to type of index
$INDEX_DAY = 1;		# Daily index (for $INDEX_TYPE)
$INDEX_HOUR = 2;
$COMMA = 0;			# Comma delimited output

$year_first = 0;
$header = 0;

usage() if (scalar (@ARGV) == 0);

while((scalar (@ARGV) > 0) && (($_ = $ARGV[0]) =~ /^-(.)(.*)/)) {
	# Body File
	if (/^-b$/) {
		shift(@ARGV);
		if (defined $ARGV[0]) {
			$BODY = $ARGV[0];
		} else {
			print "-b requires body file argument\n";
		}
	}
	elsif (/^-d$/) {
		$COMMA = 1;
	}
	# Group File
	elsif (/^-g$/) {
		shift(@ARGV);
		if (defined $ARGV[0]) {
			&'load_group_info(0,$ARGV[0]);
			$GROUP = $ARGV[0];
		} else {
			print "-g requires group file argument\n";
			usage();
		}
	}
	# Password File
	elsif (/^-p$/) {
		shift(@ARGV);
		if (defined $ARGV[0]) {
			&'load_passwd_info(0,$ARGV[0]);
			$PASSWD = $ARGV[0];
		} else {
			print "-p requires password file argument\n";
			usage();
		}
	}
	elsif (/^-h$/) {
		$header = 1;
	}
	# Index File
	elsif (/^-i$/) {
		shift(@ARGV);

		if (defined $ARGV[0]) {
			# Find out what type
			if ($ARGV[0] eq "day") {
				$INDEX_TYPE = $INDEX_DAY;
			}
			elsif ($ARGV[0] eq "hour") {
				$INDEX_TYPE = $INDEX_HOUR;
			}
			shift(@ARGV);
			unless (defined $ARGV[0]) {
				print "-i requires index file argument\n";
				usage();
			}
			$INDEX = $ARGV[0];
		} else {
			print "-i requires index file argument and type\n";
			usage();
		}
		open (INDEX, ">$INDEX") or die "Can not open $INDEX";
	}
	elsif (/^-V$/) {
		version();
		exit (0);
	}
	elsif (/^-y$/) {
		$year_first = 1;
	}
	elsif (/^-z$/) {
		shift(@ARGV);
		if (defined $ARGV[0]) {
			$ENV{TZ} = "$ARGV[0]";
		} else {
			print "-z requires the time zone argument\n";
			usage();
		}
	}
	else {
		print "Unknown option: $_\n";
		usage();
	}
	shift(@ARGV);
}

# Was the time given
if (defined $ARGV[0]) {
	$TIME = $ARGV[0];
	if ($ARGV[0] =~ /-/) {
		($t_in, $t_out) = split(/-/, $ARGV[0]);
	} else {
		$t_in = $ARGV[0];
		$t_out = 0;
	}
	$parse = &ParseDate($t_in);
	$in_seconds = &UnixDate($parse, "%s");
	die "Invalid Date: $t_in\n" if ($in_seconds <= 0);

	if ($t_out) {
		$parse = &ParseDate($t_out);
		$out_seconds = &UnixDate($parse, "%s");
		die "Invalid Date: $t_out\n" if ($out_seconds <= 0);
	}
}
else {
	$in_seconds = 0;
}



# Print header info
print_header() if ($header == 1);


# Print the index header
if ($INDEX ne "") {
	my $time_str = "";
	if ($INDEX_TYPE == $INDEX_DAY) {
		$time_str = "Daily";
	} else {
		$time_str = "Hourly";
	}
	if ($BODY ne "") {
		print INDEX "$time_str Summary for Timeline of $BODY\n\n";
	} else {
		print INDEX "$time_str Summary for Timeline of STDIN\n\n";
	}
}


read_body();

print_tl();



################ SUBROUTINES ##################

# Read the body file from the BODY variable
sub read_body {

	# Read the body file from STDIN or the -b specified body file
	if ($BODY ne "") {
		open(BODY, "<$BODY") or die "Can't open $BODY";
	} else {
		open(BODY, "<&STDIN") or die "Can't dup STDIN";
	}

	while (<BODY>) {
		my $tmp;

		($tmp,$file,$tmp,$st_ino,$tmp,$st_ls,$tmp,$st_uid,$st_gid,
		$tmp,$st_size,$st_atime,$st_mtime,$st_ctime,$tmp,$tmp)
			= &tm_split($_);

		# Sanity check so that we ignore the header entries
		next unless ((defined $st_mtime) && ($st_mtime =~ /\d+/));
		next unless ((defined $st_atime) && ($st_atime =~ /\d+/));
		next unless ((defined $st_ctime) && ($st_ctime =~ /\d+/));
		next unless ((defined $st_ino) && ($st_ino =~ /[\d-]+/));

		# we need *some* value in mactimes!
		next if (!$st_atime && !$st_mtime && !$st_ctime);

		# Skip of these are all too early
		next if (($st_mtime < $in_seconds) && ($st_atime < $in_seconds) &&
		  ($st_ctime < $in_seconds));


		#
		#  First, put all the times in one big array...
		#

		#
		# If the date on the file is too old, don't put it in the array
		#
		my $post = ",$st_ino,$file";

		if ($out_seconds) {
			$time2macstr{"$st_mtime$post"} .= "m" if 
			  (($st_mtime >= $in_seconds) && ($st_mtime < $out_seconds) &&
			  ((!(exists $time2macstr{"$st_mtime$post"})) || 
			  ($time2macstr{"$st_mtime$post"} !~ /m/)));

			$time2macstr{"$st_atime$post"} .= "a" if 
			  (($st_atime >= $in_seconds) && ($st_atime < $out_seconds) &&
			  ((!(exists $time2macstr{"$st_atime$post"})) || 
			  ($time2macstr{"$st_atime$post"} !~ /a/)));

			$time2macstr{"$st_ctime$post"} .= "c" if 
			  (($st_ctime >= $in_seconds) && ($st_ctime < $out_seconds) &&
			  ((!(exists $time2macstr{"$st_ctime$post"})) || 
			  ($time2macstr{"$st_ctime$post"} !~ /c/)));

		} else {
			$time2macstr{"$st_mtime$post"} .= "m" if 
			  (($st_mtime >= $in_seconds) && 
			  ((!(exists $time2macstr{"$st_mtime$post"})) || 
			  ($time2macstr{"$st_mtime$post"} !~ /m/)));

			$time2macstr{"$st_atime,$st_ino,$file"} .= "a" if 
			  (($st_atime >= $in_seconds) &&
			  ((!(exists $time2macstr{"$st_atime$post"})) || 
			  ($time2macstr{"$st_atime$post"} !~ /a/)));

			$time2macstr{"$st_ctime,$st_ino,$file"} .= "c" if 
			  (($st_ctime >= $in_seconds) &&
			  ((!(exists $time2macstr{"$st_ctime$post"})) || 
			  ($time2macstr{"$st_ctime$post"} !~ /c/)));
		}


		# if the UID or GID is not in the array then add it.  
		# these are filled if the -p or -g options are given
		$uid2names{$st_uid} = $st_uid
		  unless (defined $uid2names{$st_uid}); 
		$gid2names{$st_gid} = $st_gid
		  unless (defined $gid2names{$st_gid});

		#
		# put /'s between multiple UID/GIDs
		#
		$uid2names{$st_uid} =~ s@\s@/@g;
		$gid2names{$st_gid} =~ s@\s@/@g;

		$file2other{$file} = 
		  "$st_ls:$uid2names{$st_uid}:$gid2names{$st_gid}:$st_size";
	}

	close BODY;
} # end of read_body


sub print_header {
	return if ($header == 0);

	print "The Sleuth Kit mactime Timeline\n";

	print "Input Source: ";
	if ($BODY eq "") {
		print "STDIN\n";
	} else {
		print "$BODY\n";
	}

	print "Time: $TIME\t\t" if ($TIME ne "");

	if ($ENV{TZ} eq "") {
		print "\n";
	} else {
		print "Timezone: $ENV{TZ}\n";
	}

	print "passwd File: $PASSWD" if ($PASSWD ne "");
	if ($GROUP ne "") {
		print "\t" if ($PASSWD ne "");
		print "group File: $GROUP";
	}
	print "\n" if (($PASSWD ne "") || ($GROUP ne ""));

	print "\n";
}

#
# Print the time line
#
sub print_tl {

	my $prev_day = "";   # has the format of 'day day_week mon year'
	my $prev_hour = "";  # has just the hour and is used for hourly index
	my $prev_cnt = 0;

	my $delim = ":";
	if ($COMMA != 0) {
		print "Date,Size,Type,Mode,UID,GID,Meta,File Name\n";
		$delim = ",";
	}

	for $key (sort {$a <=> $b} keys %time2macstr) {
		if ($key =~ /^(\d+),([\d-]+),(.*)$/) {
			$time = $1;
			$inode = $2;
			$file = $3;
		} else {
			next;
		}

		($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = 
		  localtime($time);

		# the month here is 0-11, not 1-12, like what we want
		$mon++;

		print "\t($sec,$min,$hour,MDay: $mday,M: $mon,$year,$wday,$yday,$isdst) = ($time)\n" if $debug;

		#
		# cosmetic change to make it look like unix dates
		#
		$mon   = "0$mon"   if $mon   < 10;
		$mday  = "0$mday"  if $mday  < 10;
		$hour  = "0$hour"  if $hour  < 10;
		$min   = "0$min"   if $min   < 10;
		$sec   = "0$sec"   if $sec   < 10;

		$yeart = $year + 1900;
	    #  How do we print the date? 
   		#
		if ($year_first) {
			$date_string = 
			  "$yeart $digit_to_month{$mon} $mday $digit_to_day{$wday} $hour:$min:$sec";
		}
    	else {
			$date_string = 
			  "$digit_to_day{$wday} $digit_to_month{$mon} $mday $yeart $hour:$min:$sec";
		}

		#
		# However, we only print the date if it's different from the one 
		# above.  We need to fill the empty space with blanks, though.
		#
		if ($old_date_string eq $date_string) {
			$date_string = " ";
			$prev_cnt++
			  if ($INDEX ne "");
		} else { 
			$old_date_string = $date_string; 

			# Indexing code
			if ($INDEX ne "") {
				# First time it is run
				if ($prev_day eq "") {
					$prev_day = "$mday $wday $mon $yeart";
					$prev_hour = $hour;
					$prev_cnt = 0;
				}
				# A new day, so print the results
				elsif ($prev_day ne "$mday $wday $mon $yeart") {
					my  @prev_vals = split(/ /, $prev_day);

					my $date_str = 
					  "$digit_to_day{$prev_vals[1]} ".
					  "$digit_to_month{$prev_vals[2]} ".
					  "$prev_vals[0] ${prev_vals[3]}";

					$date_str .= " $prev_hour:00:00"
					  if ($INDEX_TYPE == $INDEX_HOUR);

					print INDEX "${date_str}${delim} $prev_cnt\n";

					# Reset
					$prev_cnt = 0;
					$prev_day = "$mday $wday $mon $yeart";
					$prev_hour = $hour;

				}
				# Same day, but new hour
				elsif (($INDEX_TYPE == $INDEX_HOUR) && ($prev_hour != $hour)) {
					my  @prev_vals = split(/ /, $prev_day);

					print INDEX 
					  "$digit_to_day{$prev_vals[1]} ".
					  "$digit_to_month{$prev_vals[2]} ".
					  "$prev_vals[0] ${prev_vals[3]} ".
					  "$prev_hour:00:00${delim} $prev_cnt\n";

					# Reset
					$prev_cnt = 0;
					$prev_hour = $hour;
				}
				$prev_cnt++;
			}
		}
	
		#
		#  Muck around with the [mac]times string to make it pretty.
		# If it has all three times, leave it alone.
		#
		$mactime = $time2macstr{$key};
		if (length($mactime) == 2) {
			$one = substr($mactime, 0, 1);
			$two = substr($mactime, 1, 1);
			if ($one eq "a")    { $mactime = ".$mactime"; }
			elsif ($one eq "m") { 
				if ($two eq "a") { $mactime = "$mactime."; }
				else             { $mactime = "$one.$two"; }
			}
		}
		elsif (length($mactime) == 1) {
			$one = substr($mactime, 0, 1);
			if    ($one eq "m") { $mactime = "$mactime.."; }
			elsif ($one eq "a") { $mactime = ".$mactime."; }
			elsif ($one eq "c") { $mactime = "..$mactime"; }
		}

		($ls,$uids,$groups,$size) = split(/:/, $file2other{$file});

		print "FILE: $file MODES: $ls U: $uids G: $groups S: $size\n" 
		  if $debug;

		if ($COMMA == 0) {
			printf("%24s %8s %3s %s %-8s %-8s %-8s %s\n", 
				$date_string, $size, $mactime, $ls, $uids, $groups, 
				$inode, $file);
		} else {
			printf("%s,%s,%s,%s,%s,%s,%s,%s\n", 
				$old_date_string, $size, $mactime, $ls, $uids, $groups, 
				$inode, $file);
		}
	}

	# Finish the index page for the last entry 
	if (($INDEX ne "") && ($prev_cnt > 0)) {
		my  @prev_vals = split(/ /, $prev_day);

		my $date_str =
		  "$digit_to_day{$prev_vals[1]} ".
		  "$digit_to_month{$prev_vals[2]} ".
		  "$prev_vals[0] ${prev_vals[3]}"; 

		$date_str .= " $prev_hour:00:00"
		  if ($INDEX_TYPE == $INDEX_HOUR);

		print INDEX "${date_str}${delim} $prev_cnt\n";

		close INDEX;
	}
}




#
#   Routines for reading and caching user and group information.  These
# are used in multiple programs... it caches the info once, then hopefully
# won't be used again.
#
#  Steve Romig, May 1991.
#
# Provides a bunch of routines and a bunch of arrays.  Routines 
# (and their usage):
#
#    load_passwd_info($use_getent, $file_name)
#
#	loads user information into the %uname* and %uid* arrays 
#	(see below).  
#
#	If $use_getent is non-zero:
#	    get the info via repeated 'getpwent' calls.  This can be
#	    *slow* on some hosts, especially if they are running as a
#	    YP (NIS) client.
#	If $use_getent is 0:
#	    if $file_name is "", then get the info from reading the 
#	    results of "ypcat passwd" and from /etc/passwd.  Otherwise, 
#	    read the named file.  The file should be in passwd(5) 
#	    format.
#
#    load_group_info($use_gentent, $file_name)
#
#	is similar to load_passwd_info.
#
# Information is stored in several convenient associative arrays:
#
#   %uname2shell	Assoc array, indexed by user name, value is 
#			shell for that user name.
#
#   %uname2dir		Assoc array, indexed by user name, value is
#			home directory for that user name.
#
#   %uname2uid		Assoc array, indexed by name, value is uid for 
#			that uid.
#			
#   %uname2passwd	Assoc array, indexed by name, value is password
#			for that user name.
#
#   %uid2names		Assoc array, indexed by uid, value is list of
#			user names with that uid, in form "name name
#			name...". 
#
#   %gid2members	Assoc array, indexed by gid, value is list of
#			group members in form "name name name..."
#
#   %gname2gid		Assoc array, indexed by group name, value is
#			matching gid.
#
#   %gid2names		Assoc array, indexed by gid, value is the
#			list of group names with that gid in form 
#			"name name name...".
#
# You can also use routines named the same as the arrays - pass the index 
# as the arg, get back the value.  If you use this, get{gr|pw}{uid|gid|nam} 
# will be used to lookup entries that aren't found in the cache.
#
# To be done:
#    probably ought to add routines to deal with full names.
#    maybe there ought to be some anal-retentive checking of password 
#	and group entries.
#    probably ought to cache get{pw|gr}{nam|uid|gid} lookups also.
#    probably ought to avoid overwriting existing entries (eg, duplicate 
#       names in password file would collide in the tables that are 
#	indexed by name).
#
# Disclaimer:
#    If you use YP and you use netgroup entries such as 
#	+@servers::::::
#	+:*:::::/usr/local/utils/messages
#    then loading the password file in with &load_passwd_info(0) will get 
#    you mostly correct YP stuff *except* that it won't do the password and 
#    shell substitutions as you'd expect.  You might want to use 
#    &load_passwd_info(1) instead to use getpwent calls to do the lookups, 
#    which would be more correct.
#
#
#  minor changes to make it fit with the TCT program, 9/25/99, - dan
#

package main;

%uname2shell = ();
%uname2dir = ();
%uname2uid = ();
%uname2passwd = ();
%uid2names = ();
%gid2members = ();
%gname2gid = ();
%gid2names = ();

$DOMAINNAME = "/bin/domainname" unless defined $DOMAINNAME;
$YPCAT = "/bin/ypcat" unless defined $YPCAT;

$yptmp = "./yptmp.$$";

$passwd_loaded = 0;		# flags to use to avoid reloading everything
$group_loaded = 0;		# unnecessarily...

#
# We provide routines for getting values from the data structures as well.
# These are named after the data structures they cache their data in.  Note 
# that they will all generate password and group file lookups via getpw* 
# and getgr* if they can't find info in the cache, so they will work
# "right" even if load_passwd_info and load_group_info aren't called to 
# preload the caches.
#
# I should point out, however, that if you don't call load_*_info to preload
# the cache, uid2names, gid2names and gid2members *will not* be complete, since 
# you must read the entire password and group files to get a complete picture.
# This might be acceptable in some cases, so you can skip the load_*_info
# calls if you know what you are doing...
#
sub uname2shell {
    local($key) = @_;

    if (! defined($uname2shell{$key})) {
	&add_pw_info(getpwnam($key));
    }

    return($uname2shell{$key});
}

sub uname2dir {
    local($key) = @_;
    local(@pw_info);

    if (! defined($uname2dir{$key})) {
	&add_pw_info(getpwnam($key));
    }

    return($uname2dir{$key});
}

sub uname2uid {
    local($key) = @_;
    local(@pw_info);

    if (! defined($uname2uid{$key})) {
	&add_pw_info(getpwnam($key));
    }

    return($uname2uid{$key});
}

sub uname2passwd {
    local($key) = @_;
    local(@pw_info);

    if (! defined($uname2passwd{$key})) {
	&add_pw_info(getpwnam($key));
    }

    return($uname2passwd{$key});
}

sub uid2names {
    local($key) = @_;
    local(@pw_info);

    if (! defined($uid2names{$key})) {
	&add_pw_info(getpwuid($key));
    }

    return($uid2names{$key});
}

sub gid2members {
    local($key) = @_;
    local(@gr_info);

    if (! defined($gid2members{$key})) {
	&add_gr_info(getgrgid($key));
    }

    return($gid2members{$key});
}

sub gname2gid {
    local($key) = @_;
    local(@gr_info);

    if (! defined($gname2gid{$key})) {
	&add_gr_info(getgrnam($key));
    }

    return($gname2gid{$key});
}

sub gid2names {
    local($key) = @_;
    local(@gr_info);

    if (! defined($gid2names{$key})) {
	&add_gr_info(getgrgid($key));
    }

    return($gid2names{$key});
}

#
# Update user information for the user named $name.  We cache the password, 
# uid, login group, home directory and shell.
#

sub add_pw_info {
    local($name, $passwd, $uid, $gid) = @_;
    local($dir, $shell);

#
# Ugh!  argh...yech...sigh.  If we use getpwent, we get back 9 elts, 
# if we parse /etc/passwd directly we get 7.  Pick off the last 2 and 
# assume that they are the $directory and $shell.  
#
    $num = ( $#_ >= 7 ? 8 : 6 );
    $dir = $_[$num - 1];
    $shell = $_[$num] || '/bin/sh';


    if ($name ne "") {
	$uname2shell{$name} = $shell;
	$uname2dir{$name} = $dir;
	$uname2uid{$name} = $uid;
	$uname2passwd{$name} = $passwd;

	if ($gid ne "") {
	    # fixme: should probably check for duplicates...sigh

	    if (defined($gid2members{$gid})) {
		$gid2members{$gid} .= " $name";
	    } else {
		$gid2members{$gid} = $name;
	    }
	}

	if ($uid ne "") {
	    if (defined($uid2names{$uid})) {
		$uid2names{$uid} .= " $name";
	    } else {
		$uid2names{$uid} = $name;
	    }
	}
    }
}

#
# Update group information for the group named $name.  We cache the gid 
# and the list of group members.
#

sub add_gr_info {
    local($name, $passwd, $gid, $members) = @_;

    if ($name ne "") {
	$gname2gid{$name} = $gid;

	if ($gid ne "") {
	    if (defined($gid2names{$gid})) {
		$gid2names{$gid} .= " $name";
	    } else {
		$gid2names{$gid} = $name;
	    }

	    # fixme: should probably check for duplicates

	    $members = join(' ', split(/[, \t]+/, $members));

	    if (defined($gid2members{$gid})) {
		$gid2members{$gid} .= " " . $members;
	    } else {
		$gid2members{$gid} = $members;
	    }
	}
    }
}

#
# We need to suck in the entire group and password files so that we can 
# make the %uid2names, %gid2members and %gid2names lists complete.  Otherwise,
# we would just read the entries as needed with getpw* and cache the results.
# Sigh.
#
# There are several ways that we might find the info.  If $use_getent is 1, 
# then we just use getpwent and getgrent calls to read the info in.
#
# That isn't real efficient if you are using YP (especially on a YP client), so
# if $use_getent is 0, we can use ypcat to get a copy of the passwd and
# group maps in a fairly efficient manner.  If we do this we have to also read
# the local /etc/{passwd,group} files to complete our information.  If we aren't 
# using YP, we just read the local password and group files.
#
sub load_passwd_info {
    local($use_getent, $file_name) = @_;
    local(@pw_info);

    if ($passwd_loaded) {
	return;
    }

    $passwd_loaded = 1;

    if ($'GET_PASSWD) {
	# open(GFILE, "$'GET_PASSWD|") || die "can't $'GET_PASSWD";
	&pipe_command(GFILE, "$'GET_PASSWD", "-");
	while (<GFILE>) {
		chop;
		&add_pw_info(split(/:/));
		}
	close(GFILE);
	}
    else {

    if ($use_getent) {
	#
	# Use getpwent to get the info from the system, and add_pw_info to 
	# cache it.
	#
	while (@pw_info = getpwent) {
	    &add_pw_info(@pw_info);
	}

	endpwent;

	return;
    } elsif ($file_name eq "") {
	# chop($has_yp = `$DOMAINNAME`);
	chop($has_yp = &command_to_string($DOMAINNAME));

	if ($has_yp) {
	    #
	    # If we have YP (NIS), then use ypcat to get the stuff from the 
	    # map.@
	    #
	    # system("$YPCAT passwd > $yptmp 2> /dev/null");
	    &redirect_command($YPCAT, "passwd", ">$yptmp");
	    if (-s $yptmp) {
		&pipe_command(FILE, $YPCAT, "passwd", "-|");
	    	while (<FILE>) {
			chop;
			&add_pw_info(split(/:/));
	    		}
	    	}
	    close(FILE);
	}

	#
	# We have to read /etc/passwd no matter what...
	#
	$file_name = "/etc/passwd";
    }

    open(FILE, $file_name) ||
      die "can't open $file_name";

    while (<FILE>) {
	chop;
	    
	if ($_ !~ /^\+/) {
	    &add_pw_info(split(/:/));
	}

	# fixme: if the name matches +@name, then this is a weird 
	# netgroup thing, and we aren't dealing with it right.  might want
	# to warn the poor user...suggest that he use the use_getent 
	# method instead.
    }
    }

    close(FILE);
}

sub load_group_info {
    local($use_getent, $file_name) = @_;
    local(@gr_info);

    if ($group_loaded) {
	return;
    }

    $group_loaded = 1;

    if ($use_getent) {
	#
	# Use getgrent to get the info from the system, and add_gr_info to 
	# cache it.
	#
	while ((@gr_info = getgrent()) != 0) {
	    &add_gr_info(@gr_info);
	}

	endgrent();

	return();
    } elsif ($file_name eq "") {
	# chop($has_yp = `$DOMAINNAME`);
	chop($has_yp = &command_to_string($DOMAINNAME));

	if ($has_yp) {
	    #
	    # If we have YP (NIS), then use ypcat to get the stuff from the 
	    # map.
	    #
	    # system("$YPCAT passwd > $yptmp 2> /dev/null");
	    &redirect_command($YPCAT, "passwd", ">$yptmp");
	    if (-s $yptmp) {
		&pipe_command(FILE, $YPCAT, "group", "-|");
	    	while (<FILE>) {
			chop;
			&add_gr_info(split(/:/));
	    		}
	    	close(FILE);
		}
	}

	#
	# We have to read /etc/group no matter what...
	#
	$file_name = "/etc/group";
    }

    open(FILE, $file_name) ||
      die "can't open $file_name";

    while (<FILE>) {
	chop;
	if ($_ !~ /^\+/) {
	    &add_gr_info(split(/:/));
	}

	# fixme: if the name matches +@name, then this is a weird 
	# netgroup thing, and we aren't dealing with it right.  might want
	# to warn the poor user...suggest that he use the use_getent 
	# method instead.
    }

    close(FILE);
}

unlink $yptmp;

1;

#
# Split a time machine record.
#
sub tm_split {
        local($line) = @_;
        local(@fields);

        for (@fields = split(/\|/, $line)) {
                s/%([A-F0-9][A-F0-9])/pack("C", hex($1))/egis;
        }
        return @fields;
}
1;




