#!/usr/athena/bin/perl
#
#   Pipe a tar file through this shell script, before extracting it, and it
# will convert all of the hard links in the file to symbolic (soft) links...
#
#   With the -afs option, it will leave links to files in the same directory
# as the file it's linking to as hard links.  This is ideal for AFS, which
# doesn't allow hard links to files in different directories, but does allow
# them in a single directory.
#
#   Caviat:  This appears to only function if the input tar file was created
# with GNU tar, and the output file is extracted with GNU tar.  I fear this
# is due to different treatment of the chksum field, but as I was using GNU
# documentation to guide me, this is what I have...
#
#                                 - Chris P. Ross (cross@eng.umd.edu)
#                                   21-May-1992

# Variable initialization
$RECORDSIZE = 512;
$DEBUG = 0;

binmode(STDIN);
binmode(STDOUT);
select(STDOUT);

$| = 1;
$record_no = 0;
$record = "";
$records_changed = 0;

$SIG{'INT'} = end;

sub usage {
	print STDERR "usage:\n";
	print STDERR "	$0 [ -afs ]\n";
	exit(1);
}

# This routine calculates the checksum of a string
sub sum {
	local($string) = @_;
	local($sum) = 0;
	local($i);

	for ($i = 0 ; $i < length($string) ; $i++) {
		$sum += ord(substr($string, $i, 1));
	}
	$sum;
}

&usage if ($#ARGV > 0);

if ($#ARGV == 0) {
	&usage if ($ARGV[0] ne "-afs");
	$link_in_same_dir = 1;
} else {
	$link_in_same_dir = 0;
}

while (read(STDIN, $record, $RECORDSIZE)) {
	$record_no++;

	$chksum = oct(substr($record, 148, 8));
	$linkflag = substr($record, 156, 1);

	if ($linkflag eq "1") {
		$name = substr($record, 0, 100);
		$linkname = substr($record, 157, 100);
		$mode = substr($record, 100, 8);

		print STDERR "Found a link.  $name is a link to $linkname\n" if $DEBUG;

		if ($DEBUG > 1) {
			open(OLDREC, "> oldrec") || die "Can't open oldrec";
			binmode(OLDREC);
			print OLDREC $record;
			close(OLDREC);
		}

		$old_sum = &sum($linkname) + &sum($mode);

		$lastpos = $[;
		while (($pos = index($name, "/", $lastpos)) >= $[) {
			if (substr($linkname,0,($pos - $lastpos)) eq substr($name, $lastpos,($pos - $lastpos))) {
				substr($linkname, 0, ($pos - $lastpos + 1)) = "";
				$linkname = $linkname . ("\0" x ($pos - $lastpos + 1));
			} else {
				$newpos = $pos;
				while (($newpos = index($name, "/", $newpos)) >= $[) {
					$linkname = substr($linkname, 0, 97);
					substr($linkname, 0, 0) = "../";
					$newpos++;
				}
			}
			$lastpos = $pos + 1;
		}
#
#   If it's in a mode so as to replace *all* hard links, or if it's already
# decided that it links to another child of a higher directory, or if it's in
# a subdirectory of the current, then go ahead and change the hard link to a
# soft one.  Else, having made no modifications to the actual record, just
# write it out as-is.
#
		if ((! $link_in_same_dir) || (substr($linkname, 0, 2) eq "..") || (index($linkname, "/", 0) >= $[)) {
			if (substr($linkname, 0, 2) ne "..") {
				$linkname = substr($linkname, 0, 98);
				substr($linkname, 0, 0) = "./";
			}
		#	$linkname = $linkname . ("\0" x (100 - length($linkname)));
			if ($DEBUG > 1) {
				open(MIDREC, "> midrec") || die "Can't open midrec file";
				binmode(MIDREC);
				print MIDREC $record;
				print MIDREC "\n".length($linkname)."\n";
				close(MIDREC);
			}
			substr($record, 157, 100) = $linkname;

			# Sym-links are always mode 777, though mode doesn't really
			# matter for sym-links...
			$mode = sprintf("%lo", oct(777));
			$mode = (" " x (6 - length($mode))) . $mode . " \0";
			substr($record, 100, 8) = $mode;

			$new_sum = &sum($linkname) + &sum($mode);

			$chksum = sprintf("%6lo", (($chksum - $old_sum) + $new_sum + 1));
			substr($record, 156, 1) = "2";
			substr($record, 148, 8) = ($chksum . "\0 ");
			$records_changed++;
			print STDERR "Record now set to sym-link to $linkname\n" if $DEBUG;
			if ($DEBUG > 1) {
				open(NEWREC, "> newrec") || die "Can't open newrec file";
				binmode(NEWREC);
				print NEWREC $record;
				close(NEWREC);
				&end(1);
			}
		}
	}

	print $record;

#  Now, skip over any blocks of data, so we can read the next header.

	$size = oct(substr($record, 124, 12));

	$records = $size / $RECORDSIZE;
	$records++ if ($records != int($records));
	$records = int($records);
	read(STDIN, $record, ($records * $RECORDSIZE));
	print $record;
	$record_no += $records;
}

&end(0);

sub end {
	local($exitcode) = @_[0];

	$record_no--;

	print (STDERR "\rProcessed $record_no records, changed $records_changed.\n");

	exit $exitcode;
}
