blob: 2513b9b72d72eadf15da9e1da6f729d913fafa58 [file] [log] [blame]
#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Long;
use Fcntl ':mode';
my ($directory, $mtime, $nopadding, $norec, $verbose);
GetOptions(
"directory=s" => \$directory,
"mtime=i" => \$mtime,
"nopadding" => \$nopadding,
"no-recursion" => \$norec,
"verbose" => \$verbose,
);
chdir($directory) || die "cannot chdir";
my $num_entries = 0;
sub recurse_dir {
my $path = shift;
my @results = ("$path/");
opendir my $dh, $path or die "cannot open $path";
while (my $entry = readdir $dh) {
next if $entry eq ".";
next if $entry eq "..";
if (-d "$path/$entry") {
push @results, (&recurse_dir("$path/$entry"));
} else {
push @results, "$path/$entry";
}
}
closedir $dh;
return @results;
}
my @entries;
if (!-e $ARGV[0]) {
die "does not exist: $ARGV[0]";
} elsif (-d $ARGV[0] && !$norec) {
@entries = sort (recurse_dir($ARGV[0]));
} else {
@entries = ($ARGV[0]);
}
foreach my $fname (@entries) {
if ($verbose) {
print STDERR "$fname\n";
}
my (
$dev, $ino, $mode, $nlink, $uid, $gid, $rdev,
$size, $atime, $mtime_, $ctime, $blksize, $blocks
) = lstat($fname);
if (!defined $mode) {
die "failed to stat $fname";
}
my $content = "";
my $type;
my $linkname = "";
my $username = $ENV{LOGNAME} || $ENV{USER} || getpwuid($<) || "";
if (S_ISLNK($mode)) {
$type = 2;
$linkname = readlink $fname;
} elsif (S_ISREG($mode)) {
$type = 0;
open(my $fh, '<', $fname);
$content = do { local $/; <$fh> };
close($fh);
} elsif (S_ISDIR($mode)) {
$type = 5;
}
my $entry = pack(
'a100 a8 a8 a8 a12 a12 A8 a1 a100 a6 a2 a32 a32 a8 a8 a155 x12',
$fname,
sprintf('%07o', $mode & 07777),
sprintf('%07o', $<), # uid
sprintf('%07o', $(), # gid
sprintf('%011o', length $content), # size
sprintf('%011o', $mtime),
# mtime
'', # checksum
$type,
$linkname, # linkname
"ustar ", # magic
" ", # version
"$username", # username
"$username", # groupname
'', # dev major
'', # dev minor
'', # prefix
);
# compute and insert checksum
substr($entry, 148, 7)
= sprintf("%06o\0", unpack("%16C*", $entry));
print $entry;
$num_entries += 1;
if (length $content) {
my $num_blocks = int((length $content) / 512);
if ((length $content) % 512 != 0) {
$num_blocks += 1;
}
print $content;
print(("\x00") x ($num_blocks * 512 - (length $content)));
$num_entries += $num_blocks;
}
}
if (!$nopadding) {
# https://www.gnu.org/software/tar/manual/html_node/Standard.html
#
# Physically, an archive consists of a series of file entries terminated
# by an end-of-archive entry, which consists of two 512 blocks of zero
# bytes. At the end of the archive file there are two 512-byte blocks
# filled with binary zeros as an end-of-file marker.
print(pack 'a512', '');
print(pack 'a512', '');
$num_entries += 2;
# https://www.gnu.org/software/tar/manual/html_section/tar_76.html
#
# Some devices requires that all write operations be a multiple of a
# certain size, and so, tar pads the archive out to the next record
# boundary.
#
# The default blocking factor is 20. With a block size of 512 bytes, we
# get a record size of 10240.
my $num_records = int($num_entries * 512 / 10240);
if (($num_entries * 512) % 10240 != 0) {
$num_records += 1;
}
for (my $i = $num_entries ; $i < $num_records * 10240 / 512 ; $i++) {
print(pack 'a512', '');
}
}