#!/usr/bin/perl -w
use strict;
#
# srcdiff is used to compare current user level code with the current
# kernel code and advise of any differences between files which are
# sharing some or all of their content.
#
# There are two classes of sharing which we will check - header files
# in the include directory, which must be exactly the same (use diff)
# and source files which contain routines which must be exactly the
# same (but the userland file is always a subset of the kernel file,
# and hence a more flexible mechanism to "diff" is required).
#
# NB: to cross check that srcdiff is finding all the functions in the
#     user source file, providing you have "mkproto" installed, you
#     can "cd xfsprogs/libxfs" and cut&paste this in a bourne shell:
#     $ for file in xfs_*.c; do
#     > mkproto -nps < $file | perl -ne '
#     > END { print "    $count\t- " }
#     > s/^.* (xfs\w+|\*xfs\w+|xlog\w+|\*xlog\w+) \(.*/\1/ && { $count++ }'
#     > echo $file
#     > done
# (compare this to "srcdiff | fgrep Total:")
#

die "WORKAREA not set" unless defined $ENV{'WORKAREA'};
die "KWORKAREA not set" unless defined $ENV{'KWORKAREA'};
die "DMWORKAREA not set" unless defined $ENV{'DMWORKAREA'};
chdir $ENV{'WORKAREA'};
my $dmworkarea = $ENV{'DMWORKAREA'};
my $kworkarea = $ENV{'KWORKAREA'};
my $xdiff = $ENV{'XDIFF'};
my $quiet = 0;
my $usage = 0;

if ( ! -f "$kworkarea/xfs_fs.h" ) {
	$kworkarea .= '/fs/xfs';
	die "Cannot find XFS in KWORKAREA" unless -f "$kworkarea/xfs_fs.h";
}
die "Cannot find DMAPI in DMWORKAREA" unless -f "$dmworkarea/dmapi.h";

foreach (@ARGV) {
	if (/^-q$/) {
		$quiet++;
	} else {
		print STDERR "Illegal option $_\n";
		$usage++;
	}
}

if ($usage) {
    print STDERR "Usage: $0 [-q]\n";
    exit 1;
}

my @pkglist = qw( xfstests attr acl dmapi xfsdump xfsprogs );
my @difflist = qw(
	xfs_ag.h  xfs_alloc.h  xfs_alloc_btree.h xfs_arch.h
	xfs_attr_leaf.h  xfs_attr_sf.h  xfs_bit.h  xfs_bmap.h
	xfs_bmap_btree.h  xfs_btree.h  xfs_buf_item.h
	xfs_da_btree.h  xfs_dfrag.h  xfs_dinode.h
	xfs_dir2.h  xfs_dir2_block.h  xfs_dir2_data.h
	xfs_dir2_leaf.h  xfs_dir2_node.h  xfs_dir2_sf.h
	xfs_extfree_item.h  xfs_ialloc.h
	xfs_imap.h  xfs_ialloc_btree.h  xfs_inode.h  xfs_inode_item.h
	xfs_inum.h  xfs_log.h  xfs_log_priv.h  xfs_log_recover.h
	xfs_mount.h  xfs_quota.h  xfs_rtalloc.h
	xfs_sb.h  xfs_trans.h  xfs_trans_space.h  xfs_types.h  xfs_fs.h
);

sub straightdiff {
	my ( $file, $prefix1, $prefix2 ) = @_;

	`diff $prefix1/$file $prefix2/$file >/dev/null 2>&1`;
	if (!$quiet) {
		print sprintf("\t%-35s ... ", $file);
		if ($? != 0)	{ printf("FAILED\n(%s/%s differs to %s/%s)\n",
					 $prefix1, $file, $prefix2, $file); }
		else		{ print "ok\n"; }
	} elsif ($? != 0) {
	        printf("\t%-35s ... FAILED\n(%s/%s differs to %s/%s)\n",
			$file, $prefix1, $file, $prefix2, $file);
		if (defined($xdiff)) {
			`$xdiff $prefix1/$file $prefix2/$file`;
		}
	}
}

#
# xfstests directory m4 directory is a repository of all of the
# custom m4 macros used in the packages we look after.
#
sub m4macrodiff {
	my ( $package ) = @_;

	foreach (`ls $package/m4/*.m4`) {
		my $m4 = `basename $_`;
		chomp($m4);
		straightdiff $m4, "$package/m4", "xfstests/m4";
	}
}

my $first = shift @pkglist;
foreach (@pkglist) {
	print "\n=== Checking $_ package ===\n";
	m4macrodiff $_;
	straightdiff 'buildrules', "$first/include", "$_/include";
	straightdiff 'buildmacros', "$first/include", "$_/include";
	straightdiff 'Makefile', "$first/build", "$_/build";
	straightdiff 'Makefile', "$first/build/rpm", "$_/build/rpm";
	straightdiff 'Makefile', "$first/build/tar", "$_/build/tar";
}
print "\n=== Checking headers ===\n";
foreach (@difflist) {
	straightdiff $_, 'xfsprogs/include', "$kworkarea";
}
straightdiff 'dmapi_kern.h', 'dmapi/include', "$dmworkarea";
straightdiff 'dmapi.h', 'dmapi/include', "$dmworkarea";

#
# setstate
# Implements a tri-state FSA, see comments for state transitions
#  (knows about the way the XFS kernel code is written, & makes
#   some assumptions so as to not need to parse generic C code).
# Accepts one line at a time from a source file, picking out the
#   function bodies so they can be subsequently compared.
#

my $line;	# line number in current source file
my $state;	# current FSA state
my $funcbody;	# current function body (contents)

sub setstate {
	my ( $newline ) = @_;
	$line++;

	# - state 0:
	#	if line looks like start of a function, transition to 1
	#		& squirrel line away as line 1 of current function
	if ($state == 0) {
		if ($newline =~ m/^[xfs|xlog]/) {
			$state = 1;
			$funcbody = $newline;
		}
	}

	# - state 1:
	#	if line looks like start of a function, stay here
	#		& squirrel line away as line 1 of current function
	#	otherwise if line isn't start of function body,
	#		squirrel line away as next line of current function
	#		(args/..., but not sure this is a real function yet)
	#	otherwise (start of function)
	#		squirrel line away as next line of current function
	#		transition to state 2
	elsif ($state == 1) {
		if ($newline =~ m/^[xfs|xlog]/) {
			$funcbody = $newline;
		}
		elsif ($newline =~ m/^\{/) {
			$state = 2;
			$funcbody .= $newline;
		}
	}

	# - state 2:
	#	if line looks like end of function body,
	#		squirrel line away as last line of current function
	#		tell someone we have a complete function ready
	#		transition to state 0
	#	otherwise
	#		squirrel line away as next line of current function
	elsif ($state == 2) {
		$funcbody .= $newline;
		if ($newline =~ m/^\}/) {
			$state = 0;
			return $funcbody;
		}
	}

	else {
		die "unknown state transition";
	}
	return undef;	# i.e. not at end of a function
}

sub listfuncs {
	my ( $file ) = @_;
	my @funcs;

	$funcbody = '';
	$state = $line = 0;

	open(USER, "$file") || die "cannot open $file";
	while (<USER>) {
		my $func = setstate($_);
		push @funcs, $func if (defined($func));	# store function away
	}
	close USER;
	return @funcs;
}

sub hashfuncs {
	my ( $file ) = @_;
	my %funcs;

	$funcbody = '';
	$state = $line = 0;

	open(KERN, "$file") || die "cannot open $file";
	while (<KERN>) {
		my $func = setstate($_);
		if (defined($func)) {
			$func =~ m/^([xfs|xlog]\w+)\s*\(/;
			next unless defined($1);
			my $name = $1;
			if (defined($func)) {
				$funcs{$name} = $func;	# store function away
			}
		}
	}
	close KERN;
	return %funcs;
}

sub diffme {
	my ( $sa, $sb ) = @_;

	return unless defined($xdiff);

	open(FILEA, "> /tmp/diff.user.$$") || die "cannot write to /tmp/diff.user.$$";
	open(FILEB, "> /tmp/diff.kern.$$") || die "cannot write to /tmp/diff.kern.$$";
	print FILEA $sa;
	print FILEB $sb;
	close FILEA;
	close FILEB;
	`$xdiff /tmp/diff.user.$$ /tmp/diff.kern.$$`;
        unlink ("/tmp/diff.user.$$","/tmp/diff.kern.$$");
}

sub functiondiff {
	my ( $file, $prefix1, $prefix2 ) = @_;
	my $plural = '';
	my $count = 0;
	my $name;
        my $found = 0;

	print "\n=== Checking $file routines ===\n" unless ($quiet);

	# iterate over user funcs, match up to kernel funcs
	#
	my @user = listfuncs "$prefix1/$file";
	my %kern = hashfuncs "$prefix2/$file";

	foreach my $userfunc (@user) {

		$userfunc =~ m/^([xfs|xlog]\w+)\s*\(/;
		next unless (defined($1));
		$name = $1;
		$count++;

		if (exists($kern{$name})) {
			if ($userfunc ne $kern{$name}) {
	                        print "\n=== $file routines ===\n"
                                    if (!$found++ && $quiet);

			        printf("\t%-35s ... ", $name);
				print "FAILED\n";
				diffme $userfunc, $kern{$name};
			}
			elsif (!$quiet) {
			        printf("\t%-35s ... ", $name);
				print "ok\n";
			}
		}
		else {
			print "Cannot find kernel function $userfunc";
			print " in file $prefix2/$file\n";
		}
	}
	($count != 1) && ( $plural = 's' );
	print "( Total: $count routine$plural checked in $file )\n" unless ($quiet);
}

# xfsprogs/{libxfs,libxlog}/* fs/xfs/*
my @funclist = qw(
	xfs_alloc.c  xfs_alloc_btree.c  xfs_attr.c  xfs_attr_leaf.c
	xfs_bmap.c  xfs_bmap_btree.c  xfs_btree.c  xfs_da_btree.c
	xfs_dir2.c  xfs_dir2_block.c  xfs_dir2_data.c xfs_dir2_leaf.c
	xfs_dir2_node.c  xfs_dir2_sf.c xfs_ialloc.c  xfs_ialloc_btree.c
	xfs_inode.c  xfs_mount.c  xfs_rtalloc.c xfs_trans.c
);

print "\n=== Checking libxfs code ===\n";
foreach (@funclist) {
	functiondiff $_, 'xfsprogs/libxfs', "$kworkarea";
}
print "\n=== Checking libxlog code ===\n";
functiondiff 'xfs_log_recover.c', 'xfsprogs/libxlog', "$kworkarea";
