#!/usr/bin/perl -s

$pwdcmd = '/bin/pwd';
$user   = 'www'; $group = 'other';

@dflts  = ('/l/www');
@avoid  = ('/l/picons/db', '/l/aicons', '/l/doc/java/tutorial');

$SymLinksIfOwnerMatch = 1;

# web/bin/ftw - file tree walker for FollowSymLinks
# Steve Kinzler, steve@kinzler.com, May 96
# some code borrowed and adapted from /usr/local/lib/perl/find.pl
# https://kinzler.com/me/home.html#webadm

$usage = "usage: $0 [ -l ] [ -s ]
       [ -a | -v ] [ -d ] [ -n ] [ -p ] [ directory ... ]
	-l	list directory tree as it is walked
	-s	walk directories in sorted order
	-a	don't prune avoided directories
	-v	report directories avoided
	-d	report directories skipped as done under another name
	-n	report \"no such\" warnings
	-p	report permission warnings
This should be run as root, though access testing is for user $user and
group $group.  The walk method is " .
(($SymLinksIfOwnerMatch) ? 'SymLinksIfOwnerMatch' : 'FollowSymLinks') . ".
Default directories are " . join(', ', @dflts) . "
Avoided directories are " . join(', ', @avoid) . "\n";
die $usage if $h;

warn "$0: WARNING, cannot walk entire tree (not running as root)\n"
	unless $> == 0;

(@ent = getpwnam($user))  || die "$0: cannot get pwent for $user ($!)\n";
$auid = $ent[2];
(@ent = getgrnam($group)) || die "$0: cannot get grent for $group ($!)\n";
$agid = $ent[2];

chop($cwd = `$pwdcmd 2> /dev/null`);
die "$0: cannot determine cwd ($!)\n" if $? || $cwd eq '';
&auid();

unless ($a) {
	foreach (@avoid) {
		warn("$0: cannot chdir $_ to avoid it ($!)\n"), next
			unless chdir $_;
		&ruid();
		chop($pwd = `$pwdcmd 2> /dev/null`);
		warn("$0: not avoiding $dir, cannot run `$pwdcmd` ($!)\n"),
			&auid(), next if $? || $pwd eq '';
		$avoid{$pwd} = $_;
		chdir $cwd || die "$0: cannot chdir $cwd again ($!)\n";
		&auid();
	}
}

&find((@ARGV) ? @ARGV : @dflts);

###############################################################################

# optimization: set $stat = 1 if _ contains results of stat($_)
sub wanted {
	local(@lstat, $luid, @stat, $uid);

	return unless $SymLinksIfOwnerMatch;

	warn("$0: cannot lstat $name ($!)\n"), $prune = 1, return
		unless @lstat = lstat($_);
	if (-l _) {
		$luid = $lstat[4];
		&ewarn("$!", "cannot stat $name"), $prune = 1, return
			unless @stat = stat($_);
		($stat, $uid) = (1, $stat[4]);
		return if $luid == $uid;
		($luid, $uid) = (&uname($luid), &uname($uid));
		warn "$0: misown $name ($luid -> $uid ", readlink $_, ")\n";
		$prune = 1;
	}
}

sub uname {
	local($uid) = @_;
	local($uname, @ent);

	return $unames{$uid} if defined $unames{$uid};
	$uname = (@ent = getpwuid($uid)) ? $ent[0] : $uid;
	$unames{$uid} = $uname;
	return $uname;
}

###############################################################################

sub find {
	foreach $topdir (@_) {
		$topdir =~ s,/+,/,g; $topdir =~ s,/$,,;
		warn("$0: cannot stat $topdir ($!)\n"), next
			unless stat($topdir);
		if (-d _) {
			unless (chdir $topdir) {
				warn "$0: cannot chdir $topdir ($!)\n";
			} else {
				($dir, $_) = ($topdir, '.');
				$name = $topdir;
				&wanted();
				&finddir($topdir, 0);
			}
		} else {
			($dir, $_) = ('.', $topdir)
				unless ($dir, $_) = $topdir =~ m,^(.*/)(.*)$,;
			$name = $topdir;
			(chdir $dir) ? &wanted()
				     : warn "$0: cannot chdir $dir ($!)\n";
		}
		&ruid();
		chdir $cwd || die "$0: cannot chdir $cwd again ($!)\n";
		&auid();
	}
}

sub finddir {
	local($dir, $lev) = @_;
	local($pwd, $name);

	&ruid();
	chop($pwd = `$pwdcmd 2> /dev/null`);
	warn("$0: skipping $dir, cannot run `$pwdcmd` ($!)\n"),
		&auid(), return if $? || $pwd eq '';
	$v && warn("$0: avoid $dir (same as $diddir{$pwd} == $pwd)\n"),
		&auid(), return if ! $a && defined $avoid{$pwd};
	$d && warn("$0: skip $dir (did as $diddir{$pwd} == $pwd)\n"),
		&auid(), return if defined $diddir{$pwd};
	$diddir{$pwd} = $dir;

	warn("$0: cannot open $dir ($!)\n"), &auid(), return
		unless opendir(DIR, '.');
	local(@filenames) = readdir(DIR);
	warn("$0: cannot read $dir ($!)\n"), closedir DIR, &auid(), return
		unless @filenames;
	closedir DIR;
	&auid();
	@filenames = sort @filenames if $s;

	print '  ' x $lev, "$dir (", $#filenames - 1, ")\n" if $l;

	for (@filenames) {
		next if $_ eq '.' || $_ eq '..';
		$stat = $prune = 0;
		$name = "$dir/$_";
		&wanted();

		next if $prune;
		&ewarn("$!", "cannot stat $name"), next
			unless $stat || stat($_);
		next unless -d _;
		&ewarn("$!", "cannot chdir $name"), next unless chdir $_;

		&finddir($name, $lev + 1);
		chdir(($dir =~ m,^/,) ? $dir : "$cwd/$dir") ||
			die "$0: cannot chdir $dir again ($!)\n";
	}
}

###############################################################################

sub ruid { $> = 0;     $) = 0;     }
sub auid { $) = $agid; $> = $auid; }

sub ewarn {
	local($bang) = shift;
	return if ! $p && $bang =~ /permission/i;
	return if ! $n && $bang =~ /\bno\s+such/i;
	warn "$0: @_ ($bang)\n";
}
