#!/usr/pkg/bin/perl
#
#	$NetBSD: check-all,v 1.5 2008/04/30 13:10:52 martin Exp $
#
# Copyright (c) 1999, 2000, 2001, 2002, 2003 The NetBSD Foundation, Inc.
# All rights reserved.
#
# This code is derived from software contributed to The NetBSD Foundation
# by Konrad E. Schroder <perseant@hhhh.org>.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; 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.
#

#
# Use dumplfs to find all locations of the Ifile inode on a given disk.
# Order these by serial number and call fsck_lfs on the raw disk for each.
# If any fsck gives errors (any line of all capital letters, with a few
# exceptions) print an error code with the daddr of the failing Ifile inode
# location.
#

$| = 1;
$rdev = $ARGV[0];
$gfile = $ARGV[1];
$wfile = $ARGV[2];
$sstart = $ARGV[3];
$test_rfw = 1; # $ARGV[4];
$rollid = 0;
open(DUMPLFS, "dumplfs $rdev |");

# Look for "roll_id" so we don't use garbage
while (<DUMPLFS>) {
	if ($ssize == 0 && m/ssize *([0-9]*)/) {
		$ssize = $1;
	}
	if ($fsize == 0 && m/fsize *([0-9]*)/) {
		$fsize = $1;
	}
	if (m/roll_id *([x0-9a-f]*)/) {
		$rollid = $1;
		last;
	}
}

# Now look for inodes and segment summaries.  Build a hash table of these
# based on serial number.  Ignore any with serial numbers lower than $sstart.

%iloc = ();
%snloc = ();
%sumloc = ();
print "Reading segments:";
while (<DUMPLFS>) {
	if (m/roll_id *([0-9a-f]*)/) {
		# print "rollid $1\n";
		if ("0x$1" ne $rollid) {
			# Skip the rest of this segment
			print "{skip bad rollid 0x$1}";
			while(<DUMPLFS>) {
				last if m/SEGMENT/;
			}
			# Fall through
		}
	}
	if (m/roll_id.*serial *([0-9]*)/) {
		$serno = $1;
		$snloc{$serno} = $segnum;
		$sumloc{$serno} = $sumloc;
		print "($serno)";
		if ($serno < $sstart) {
			# Skip the rest of this partial segment
			#print "{skip bad serno $serno}";
			while(<DUMPLFS>) {
				last if m/Segment Summary/ ||
					m/SEGMENT/;
			}
			# Fall through
		}
	}
	if (m/Segment Summary Info at 0x([0-9a-f]*)/) {
		$sumloc = $1;
		next;
	}
	if (m/0x([0-9a-f]*)/) {
		foreach $ss (split "0x", $_) {
			if ($ss =~ m/^([0-9a-f][0-9a-f]*)/) {
				# print "iblk 0x$1\n";
				$daddr = $1;
				if (m/[^0-9]1v1/) {
					# print "** ifblk 0x$daddr\n";
					$iloc{$serno} = $daddr;
					$lastaddr = $daddr;
				}
			}
		}
	}
	if (m/SEGMENT *([0-9]*)/) {
		$segnum = $1;
		print "[$segnum]";
	}
}
print "\n";
close(DUMPLFS);

# Complain about missing partial-segments
for ($i = $sstart; $i < $serno; ++$i) {
	if (hex $sumloc{$i} == 0 && $i > 0) {
		print "Oops, couldn't find pseg $i\n";
	}
}

# If there were no checkpoints, print *something*
if ($#iloc == 0) {
	print "0 $sstart 0\n";
	exit 0;
}

#
# Now fsck each checkpoint in turn, beginning with $sstart.
# Because the log wraps we will have to reconstruct the filesystem image
# as it existed at each checkpoint before running fsck.
#
# Look for lines containing only caps or "!", but ignore known
# false positives.
#
$error = 0;
$lastgood = $sstart - 1;
open(LOG, ">>check-all.log");
print "Available checkpoints:";
print LOG "Available checkpoints:";
foreach $k (sort { $a <=> $b } keys %iloc) {
	$a = $iloc{$k};
	print " $a";
	print LOG " $a";
}
print "\n";
print LOG "\n";

#
# Copy the partial segments $_[0]--$_[1] from the raw device onto
# the working file.  Return the next partial-segment serial number
# after the last one we copied (usually $_[1] + 1, except in case of
# an error).
#
sub copypseg
{
	my ($blstart, $blstop, $segstop, $cmd);
	my ($totalstart, $totalstop);

	$totalstart = 0;
	$totalstop = 0;
	for ($i = $_[0]; $i <= $_[1]; ++$i) {
		$blstart = hex $sumloc{$i};
		last if $blstart <= 0;
		$totalstart = $blstart if $totalstart == 0;
		$blstop = hex $sumloc{$i + 1};
		$segstop = ((int ($blstart / $fps)) + 1) * $fps;
		if ($segstop < $blstop || $blstop < $blstart) {
			#print "Adjusting $blstop -> $segstop\n";
			$blstop = $segstop;
		}
		$totalstop = $blstop;

		print "pseg $i: write blocks ", hex $blstart, "-", hex ($blstop - 1), "\n";
		$blstart = $blstop;
	}
	$cmd = "dd if=$rdev of=$wfile bs=$fsize seek=$totalstart " .
		"skip=$totalstart conv=notrunc count=" .
		($totalstop - $totalstart);
#	print "$cmd\n";
	system("$cmd >/dev/null 2>&1");

	return $i;
}

print "Recreating filesystem image as of $sstart:\n";
if ($sstart == 0) {
	$cmd = "dd if=$rdev of=$wfile bs=1m conv=swab,oldebcdic"; # garbage
} else {
	$cmd = "dd if=$gfile of=$wfile bs=1m";
}
print "$cmd\n";
system("$cmd >/dev/null 2>&1");

print "Copying over first superblock\n";
system("dd if=$rdev of=$wfile bs=8k count=2 conv=notrunc >/dev/null 2>&1");

sub test_fsck
{
	my $a = $_[0];
	my $flags = $_[1];
	my $printit = $_[2];
	my $output = "";

	$flags = "-n -f -i 0x$a $wfile" unless $flags;

	$cmd = "fsck_lfs $flags";
	print "$cmd\n";
	print LOG "$cmd\n";
	open(FSCK, "$cmd 2>&1 |");
	while(<FSCK>) {
		print LOG;
		$rline = "$_";
		chomp;

		# Known false positives (mismatch between sb and ifile,
		# which should be expected given we're using an arbitrarily
		# old version of the ifile)
		if (m/AVAIL GIVEN/ ||
		    m/BFREE GIVEN/ ||
		    m/DMETA GIVEN/ ||
		    m/NCLEAN GIVEN/ ||
		    m/FREE BUT NOT ON FREE LIST/ ||	# UNWRITTEN inodes OK
		    m/FILE SYSTEM WAS MODIFIED/ ||
		    m/FREE LIST HEAD IN SUPERBLOCK/ ) {
			next;
		}

		# Fsck reports errors in ALL CAPS
		# But don't count hex numbers as "lowercase".
		$oline = "$_";
		s/0x[0-9a-f]*//g;
		if (m/[A-Z]/ && ! m/[a-z]/) {
			$error = 1;
			$errsn = $k;
			$errstr = "1 $k 0x$a $oline";
			# last;
		}

		# Log everything we get, except for some things we
		# will see every single time.
		if (m/checkpoint invalid/ ||
		    m/skipping free list check/ ||
		    m/expect discrepancies/) {
			next;
		}
		$output .= $rline;
	}
	close(FSCK);

	if ($? != 0) {
		$error = 1;
		$errsn = $k;
		$errstr = "1 $k 0x$a <" . (hex $?) . ">";
	}

	if ($error || $printit) {
		print $output;
	}
}

$blstart = 0;
$fps = $ssize / $fsize;
$oind = ($sstart ? $sstart : 1);
BIGLOOP: foreach $k (sort { $a <=> $b } keys %iloc) {
	$a = $iloc{$k};

	if (hex($a) > hex($lastaddr)) {
		print "Skipping out-of-place checkpoint $k at $a\n";
		next;
	}

	if ($test_rfw && $iloc{$oind - 1}) {
		for ($tk = $oind; $tk < $k; $tk++) {
			print "Test roll-forward agent at non-checkpoint pseg $tk\n";
			print LOG "Test roll-forward agent at non-checkpoint pseg $tk\n";
			&copypseg($oind, $tk);
			# Add -d flag here for verbose debugging info
			$flags = "-p -f -i 0x" . $iloc{$oind - 1} . " $wfile";
			&test_fsck($iloc{$oind - 1}, $flags, 1);
			last BIGLOOP if $error;

			# note lack of -i flag, since the roll-forward
			# will have rewritten the superblocks.
			&test_fsck($iloc{$oind - 1}, "-n -f $wfile", 0);
			last BIGLOOP if $error;
		}
	}

	print "Recreate fs state at checkpoint pseg $k (from " . ($oind - 1) .
	      ")\n";
	$oind = &copypseg($oind, $k);

	&test_fsck($a, "", 0);

	last if $error;
	$lastgood = $k;	# record last good serial number
}

if ($errstr) {
	print "$errstr\n";
	exit 0;
}

if (!$errstr) {
	print "Bring filesystem state up to log wrap\n";
	$lastgood = &copypseg($oind, 100000000000) - 1;

	print "Copying this good image to $gfile\n";
	system("dd bs=1m if=$rdev of=$gfile >/dev/null 2>&1");
	print "0 $lastgood 0x$a\n";
	exit 0;
}

#
# Ifile write-checking paranoia.
#
# If we found an error, try to find which blocks of the Ifile inode changed
# between the last good checkpoint and this checkpoint; and which blocks
# *should* have changed.  This means (1) which segments were written; and
# (2) which inodes were written.  The 0 block of the Ifile should always
# have changed since lfs_avail is always in flux.
#

$cmd = "dumplfs";
$oseg = -1;
%iblk = ();
%iblk_done = ();
%why = ();
$iblk{0} = 1;
for ($i = $lastgood + 1; $i <= $errsn; $i++) {
	if ($oseg != $snloc{$i}) {
		$oseg = 0 + $snloc{$i};
		$cmd .= " -s$oseg";
	}
}
$cmd .= " $rdev";

open(DUMPLFS, "$cmd |");
while(<DUMPLFS>) {
	if (m/ifpb *([0-9]*)/) {
		$ifpb = $1;
	}
	if (m/sepb *([0-9]*)/) {
		$sepb = $1;
	}
	if (m/cleansz *([0-9]*)/) {
		$cleansz = $1;
	}
	if (m/segtabsz *([0-9]*)/) {
		$segtabsz = $1;
	}
	last if m/SEGMENT/;
}
while(<DUMPLFS>) {
	chomp;

	# Skip over partial segments outside our range of interest
	if (m/roll_id.*serial *([0-9]*)/) {
		$serno = $1;
		if ($serno <= $lastgood || $serno > $errsn) {
			# Skip the rest of this partial segment
			while(<DUMPLFS>) {
				last if m/Segment Summary/ || m/SEGMENT/;
			}
			next;
		}
	}

	# Look for inodes
	if (m/Inode addresses/) {
		s/^[^{]*{/ /o;
		s/}[^{]*$/ /o;
		s/}[^{]*{/,/og;
		s/v[0-9]*//og;
		@ilist = split(',');
		foreach $i (@ilist) {
			$i =~ s/ *//og;
			next if $i == 1;
			$iaddr = $cleansz + $segtabsz + int ($i / $ifpb);
			$iblk{$iaddr} = 1;
			$why{$iaddr} .= " $i";
		}
	}

	# Look for Ifile blocks actually written
	if (m/FINFO for inode: ([0-9]*) version/) {
		$i = $1;
		$inoblkmode = ($i == 1);
	}
	if ($inoblkmode && m/^[-\t 0-9]*$/) {
		s/\t/ /og;
		s/^ *//o;
		s/ *$//o;
		@bn = split(' ');
		foreach $b (@bn) {
			$iblk_done{$b} = 1;
		}
	}
}
close(DUMPLFS);

# Report found and missing Ifile blocks
print "Ifile blocks found:";
foreach $b (sort { $a <=> $b } keys %iblk) {
	if ($iblk_done{$b} == 1) {
		print " $b";
	}
}
print "\n";
	
print "Ifile blocks missing:";
foreach $b (sort { $a <=> $b } keys %iblk) {
	if ($iblk_done{$b} == 0) {
		$why{$b} =~ s/^ *//o;
		print " $b ($why{$b})";
	}
}
print "\n";

print "$errstr\n";
exit 0;