Michal Marek c56654
#!/usr/bin/perl
Michal Marek c56654
Michal Marek c56654
#############################################################################
Michal Marek c56654
# Copyright (c) 2012 Novell, Inc.
Michal Marek c56654
# All Rights Reserved.
Michal Marek c56654
#
Michal Marek c56654
# This program is free software; you can redistribute it and/or
Michal Marek c56654
# modify it under the terms of version 2 of the GNU General Public License as
Michal Marek c56654
# published by the Free Software Foundation.
Michal Marek c56654
#
Michal Marek c56654
# This program is distributed in the hope that it will be useful,
Michal Marek c56654
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Michal Marek c56654
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.   See the
Michal Marek c56654
# GNU General Public License for more details.
Michal Marek c56654
#
Michal Marek c56654
# You should have received a copy of the GNU General Public License
Michal Marek c56654
# along with this program; if not, contact Novell, Inc.
Michal Marek c56654
#
Michal Marek c56654
# To contact Novell about this file by physical or electronic mail,
Michal Marek c56654
# you may find current contact information at www.novell.com
Michal Marek c56654
#############################################################################
Michal Marek c56654
Michal Marek c56654
# A very limited tar writer. Creates archives in the ustar format and
Michal Marek c56654
# makes sure that identical content results in identical archives.
Michal Marek c56654
Michal Marek c56654
use strict;
Michal Marek c56654
use warnings;
Michal Marek c56654
Michal Marek c56654
my $USAGE = "Usage: $0 --mtime=<decimal timestamp> [--exclude=<pattern] [-C directory] file|directory ... >archive.tar\n";
Michal Marek c56654
Michal Marek c56654
use Getopt::Long;
Michal Marek c56654
use File::Find;
Michal Marek c56654
use File::Copy;
Michal Marek c56654
use Fcntl ':mode';
Michal Marek c56654
Michal Marek f53c5d
my $mtime;
Michal Marek df842b
my $disable_paxheaders;
Michal Marek c56654
my $chdir;
Michal Marek 8cb5b4
my $files_from;
Michal Marek c56654
my @exclude = ();
Michal Marek c56654
GetOptions(
Michal Marek c56654
	"t|mtime=i" => \$mtime,
Michal Marek c56654
	"exclude=s" => \@exclude,
Michal Marek df842b
	"no-paxheaders" => \$disable_paxheaders,
Michal Marek c56654
	"C=s" => \$chdir,
Michal Marek 8cb5b4
	"T|files-from=s" => \$files_from,
Michal Marek c56654
) or die($USAGE);
Michal Marek c56654
Michal Marek f53c5d
if (!defined($mtime)) {
Michal Marek f53c5d
	warn "$0: --mtime not specified, using 2000-01-01\n";
Michal Marek f53c5d
	$mtime = 946681200;
Michal Marek f53c5d
}
Michal Marek 8cb5b4
my @args;
Michal Marek 8cb5b4
if ($files_from) {
Michal Marek 8cb5b4
	if ($files_from eq '-') {
Michal Marek 8cb5b4
		@args = <>;
Michal Marek 8cb5b4
	} else {
Michal Marek 8cb5b4
		open(my $fh, '<', $files_from) or die "$files_from: $!\n";
Michal Marek 8cb5b4
		@args = <$fh>;
Michal Marek 8cb5b4
		close($fh);
Michal Marek 8cb5b4
	}
Michal Marek 8cb5b4
	chomp(@args);
Michal Marek 8cb5b4
} else {
Michal Marek 8cb5b4
	@args = @ARGV;
Michal Marek 8cb5b4
}
Michal Marek 8cb5b4
if (!@args) {
Michal Marek 8cb5b4
	print STDERR "No arguments given\n";
Michal Marek 8cb5b4
	die($USAGE);
Michal Marek 8cb5b4
}
Michal Marek c56654
Michal Marek c56654
chdir($chdir) if $chdir;
Michal Marek c56654
my @files;
Michal Marek c56654
Michal Marek a5ef8b
s/\./\\./g for @exclude;
Michal Marek c56654
s/\*/.*/g for @exclude;
Michal Marek c56654
s/\?/./g for @exclude;
Michal Marek a5ef8b
s/.*/^$&\$/ for @exclude;
Michal Marek c56654
sub wanted {
Michal Marek c56654
	for my $pattern (@exclude) {
Michal Marek c56654
		return if $_ =~ $pattern;
Michal Marek c56654
	}
Michal Marek c56654
	push(@files, $File::Find::name);
Michal Marek c56654
}
Michal Marek c56654
Michal Marek 8cb5b4
for my $file (@args) {
Michal Marek c56654
	if (-d $file) {
Michal Marek c56654
		find(\&wanted, $file);
Michal Marek c56654
	} else {
Michal Marek c56654
		push(@files, $file);
Michal Marek c56654
	}
Michal Marek c56654
}
Michal Marek c56654
$| = 1;
Michal Marek c56654
my $total_size = 0;
Michal Marek 7901ad
Michal Marek 7901ad
# Tar header format description
Michal Marek 7901ad
# filed => <width><type>
Michal Marek 7901ad
# where type is either _s_string or _o_ctal
Michal Marek 7901ad
my @header_layout = (
Michal Marek 7901ad
	name => "100s",
Michal Marek 7901ad
	mode => "8o",
Michal Marek 7901ad
	uid => "8o",
Michal Marek 7901ad
	gid => "8o",
Michal Marek 7901ad
	size => "12o",
Michal Marek 7901ad
	mtime => "12o",
Michal Marek 7901ad
	csum => "8s",
Michal Marek 7901ad
	typeflag => "1s",
Michal Marek 7901ad
	linktarget => "100s",
Michal Marek 7901ad
	magic => "6s",
Michal Marek 7901ad
	version => "2s",
Michal Marek 7901ad
	user => "32s",
Michal Marek 7901ad
	group => "32s",
Michal Marek 7901ad
	devmajor => "8o",
Michal Marek 7901ad
	devminor => "8o",
Michal Marek 7901ad
	prefix => "155s"
Michal Marek 7901ad
);
Michal Marek 7901ad
Michal Marek c56654
for my $file (sort(@files)) {
Michal Marek 7901ad
	my %header = ();
Michal Marek c56654
Michal Marek 7901ad
	$header{name} = $file;
Michal Marek 392bcc
	my $need_paxheader = 0;
Michal Marek c56654
	if (length($file) > 100) {
Michal Marek 7901ad
		($header{prefix} = $file) =~ s:/[^/]*$::;
Michal Marek 7901ad
		($header{name} = $file) =~ s:^.*/::;
Michal Marek 7901ad
		if (length($header{name}) > 100 ||
Michal Marek 7901ad
					length($header{prefix}) > 155) {
Michal Marek df842b
			if ($disable_paxheaders) {
Michal Marek df842b
				die "Too long filenames are impossible with --no-paxheaders: $file\n";
Michal Marek df842b
			}
Michal Marek 392bcc
			$header{name} = substr($header{name}, 0, 100);
Michal Marek 392bcc
			$header{prefix} = substr($header{prefix}, 0, 155);
Michal Marek 392bcc
			$need_paxheader = 1;
Michal Marek c56654
		}
Michal Marek c56654
	}
Michal Marek c56654
	my @stat = lstat($file) or die "$file: $!\n";
Michal Marek c56654
	my $mode = $stat[2];
Michal Marek 7901ad
	$header{mode} = ($mode & 0111) ? 0755 : 0644;
Michal Marek 392bcc
	# 65534:65534 is commonly used for nobody:nobody
Michal Marek 392bcc
	$header{uid} = 65534;
Michal Marek 392bcc
	$header{gid} = 65533;
Michal Marek 392bcc
	$header{user} = "nobody";
Michal Marek 392bcc
	$header{group} = "nobody";
Michal Marek 392bcc
Michal Marek 392bcc
	$header{mtime} = $mtime;
Michal Marek 392bcc
Michal Marek 392bcc
	if ($need_paxheader) {
Michal Marek 392bcc
		my $record = "path=$file\n";
Michal Marek 392bcc
		# length means length of the whole record, including the
Michal Marek 392bcc
		# length number
Michal Marek 392bcc
		my $length = length($record) + 2;
Michal Marek 392bcc
		while ($length < length(sprintf("%d %s", $length, $record))) {
Michal Marek 392bcc
			$length++;
Michal Marek 392bcc
		}
Michal Marek 392bcc
		$record = sprintf("%d %s", $length, $record);
Michal Marek 392bcc
		$header{typeflag} = "x";
Michal Marek 392bcc
		$header{size} = length($record);
Michal Marek 392bcc
		print gen_header(\%header);
Michal Marek 392bcc
		$total_size += 512;
Michal Marek 392bcc
		print $record;
Michal Marek 392bcc
		# padding to 512 byte boundary
Michal Marek 392bcc
		my $pad = pad_tail($header{size}, 512);
Michal Marek 392bcc
		print $pad;
Michal Marek 392bcc
		$total_size += length($pad);
Michal Marek 392bcc
	}
Michal Marek c56654
	if (S_ISREG($mode)) {
Michal Marek 7901ad
		$header{size} = $stat[7];
Michal Marek 7901ad
		$header{typeflag} = "0";
Michal Marek c56654
	} elsif (S_ISLNK($mode)) {
Michal Marek 392bcc
		$header{size} = 0;
Michal Marek 7901ad
		$header{linktarget} = readlink($file);
Michal Marek 7901ad
		$header{typeflag} = "2";
Michal Marek c56654
	} elsif (S_ISDIR($mode)) {
Michal Marek 392bcc
		$header{size} = 0;
Michal Marek 7901ad
		$header{typeflag} = "5";
Michal Marek c56654
	} else {
Michal Marek c56654
		die "Only regular files, symlinks and directories supported: $file\n";
Michal Marek c56654
	}
Michal Marek 7901ad
	print gen_header(\%header);
Michal Marek c56654
	$total_size += 512;
Michal Marek c56654
	next unless S_ISREG($mode);
Michal Marek c56654
Michal Marek c56654
	# PAYLOAD
Michal Marek c56654
	copy($file, \*STDOUT);
Michal Marek 7901ad
	$total_size += $header{size};
Michal Marek c56654
	# padding to 512 byte boundary
Michal Marek 392bcc
	my $pad = pad_tail($header{size}, 512);
Michal Marek 392bcc
	print $pad;
Michal Marek 392bcc
	$total_size += length($pad);
Michal Marek c56654
}
Michal Marek c56654
# end of archive marker
Michal Marek c56654
print pad("", 1024);
Michal Marek c56654
$total_size += 1024;
Michal Marek c56654
# pad to 10240 boundary
Michal Marek 392bcc
print pad_tail($total_size, 10240);
Michal Marek c56654
exit;
Michal Marek c56654
Michal Marek 7901ad
sub gen_header {
Michal Marek 7901ad
	my $header = shift;
Michal Marek 7901ad
Michal Marek 7901ad
	$header->{magic} = "ustar";
Michal Marek 7901ad
	$header->{version} = "00";
Michal Marek 7901ad
Michal Marek 7901ad
	my $res = "";
Michal Marek 7901ad
	my $csum_pos = 0;
Michal Marek 7901ad
	for (my $i = 0; $i < scalar(@header_layout); $i += 2) {
Michal Marek 7901ad
		my $field = $header_layout[$i];
Michal Marek 7901ad
		my $fmt = $header_layout[$i + 1];
Michal Marek 7901ad
		(my $length = $fmt) =~ s/.$//;
Michal Marek 7901ad
		(my $type = $fmt) =~ s/^\d*//;
Michal Marek 7901ad
		my $value = $header->{$field};
Michal Marek 7901ad
Michal Marek 7901ad
		# special case
Michal Marek 7901ad
		if ($field eq "csum") {
Michal Marek 7901ad
			$csum_pos = length($res);
Michal Marek 7901ad
			$res .= " " x 8;
Michal Marek 7901ad
			next;
Michal Marek 7901ad
		}
Michal Marek 7901ad
		if ($type eq "s") {
Michal Marek 7901ad
			$value = "" unless defined($value);
Michal Marek 7901ad
			$res .= pad($value, $length);
Michal Marek 7901ad
		} elsif ($type eq "o") {
Michal Marek 7901ad
			$value = 0 unless defined($value);
Michal Marek 7901ad
			$res .= pad_octal($value, $length);
Michal Marek 7901ad
		} else {
Michal Marek 7901ad
			die "Invalid format for $field: $fmt";
Michal Marek 7901ad
		}
Michal Marek 7901ad
	}
Michal Marek 7901ad
	# add the checksum, using the "%06o\0 " format like GNU tar
Michal Marek 7901ad
	my $csum = header_checksum($res);
Michal Marek 7901ad
	substr($res, $csum_pos, 7) = pad_octal($csum, 7);
Michal Marek 7901ad
Michal Marek 7901ad
	# padding to 512 byte boundary
Michal Marek 7901ad
	$res .= pad("", 12);
Michal Marek 7901ad
	die "error: header is not 512 bytes long" unless length($res) == 512;
Michal Marek 7901ad
Michal Marek 7901ad
	return $res;
Michal Marek 7901ad
}
Michal Marek 7901ad
Michal Marek c56654
sub pad {
Michal Marek c56654
	my ($string, $length) = @_;
Michal Marek c56654
	
Michal Marek c56654
	my $pad = $length - length($string);
Michal Marek c56654
	if ($pad < 0) {
Michal Marek c56654
		die "Field over $length bytes: $string\n";
Michal Marek c56654
	}
Michal Marek c56654
	return $string . "\0" x $pad;
Michal Marek c56654
}
Michal Marek c56654
Michal Marek c56654
sub pad_octal {
Michal Marek c56654
	my ($num, $length) = @_;
Michal Marek c56654
Michal Marek c56654
	$length--;
Michal Marek c56654
	return sprintf("%0${length}o\0", $num);
Michal Marek c56654
}
Michal Marek c56654
Michal Marek 392bcc
# after $size bytes written, padd to the neares $boundary
Michal Marek 392bcc
sub pad_tail {
Michal Marek 392bcc
	my ($size, $boundary) = @_;
Michal Marek 392bcc
Michal Marek 392bcc
	return "" unless $size % $boundary;
Michal Marek 392bcc
	my $padding = $boundary - $size % $boundary;
Michal Marek 392bcc
	return pad("", $padding);
Michal Marek 392bcc
}
Michal Marek 392bcc
Michal Marek c56654
sub header_checksum {
Michal Marek c56654
	my $header = shift;
Michal Marek c56654
Michal Marek c56654
	my $res = 0;
Michal Marek c56654
	for (my $i = 0; $i < length($header); $i++) {
Michal Marek c56654
		$res += ord(substr($header, $i, 1))
Michal Marek c56654
	}
Michal Marek c56654
	return $res;
Michal Marek c56654
}
Michal Marek c56654