#!/bin/sh
set -eu
export LC_ALL=C.UTF-8

# The python tarfile module is able to read archives with sparse files in them
# but it is unable to write them. Make sure, that after being processed with
# mmtarfilter, the resulting archive contains the correct (expanded) data.

trap "rm -f /tmp/mksparsetar.pl /tmp/orig.tar" EXIT INT TERM

sparsetest() {
  test "$(tar tvf /tmp/orig.tar)" = "-rw-rw-r-- 0/0         8388608 1970-01-01 01:00 sparsefile"
  if [ "$("$MMTARFILTER" </tmp/orig.tar | tar --to-stdout --extract sparsefile | sha1sum)" != "e4d6196039c65beb8ba41b5b45f404549e41303c  -" ]; then
    echo "E: checksum of extracted sparsefile didn't match" >&2
    return 1
  fi
}

# How to create the testfile on a filesystem that supports sparse files:
#
# $ echo start > sparsefile
# $ truncate -s 4M sparsefile
# $ echo middle >> sparsefile
# $ truncate -s 8388604 sparsefile
# $ echo end >> sparsefile
# $ stat -c '%b*%B-%s' -- sparsefile
# 24*512-8388608
# $ sha1sum sparsefile
# e4d6196039c65beb8ba41b5b45f404549e41303c  sparsefile
#
# The number of allocated blocks (24) is much smaller than the size of the file
# (8MiB) when multiplied by the block size (512)

# We don't create it using GNU tar or bsdtar to create the test case input and
# craft the tarballs with Perl instead, because we cannot know whether
# this test is run on a filesystem that supports sparse files, so maybe we
# are unable to create a file that GNU tar or bsdtar can then pack up as
# sparse.
mksparsetar() {
  bsdtar=0
  if [ $# -gt 0 ] && [ "$1" = "bsdtar" ]; then
    bsdtar=1
  fi
  cat <<'END'
#!/usr/bin/env perl

use strict;
use warnings;
use POSIX qw(ceil);

my @entries = (
END

  # print everything from stdin
  cat

  cat <<'END'
);

my $num_entries = 0;

foreach my $file (@entries) {
    my ($fname, $mode, $type, $content) = @{$file};
    my $entry = pack(
        'a100 a8 a8 a8 a12 a12 A8 a1 a100 a6 a2 a32 a32 a8 a8 a155 x12',
        $fname,
END
  if [ $bsdtar -eq 0 ]; then
    cat <<'END'
        sprintf('%07o',  $mode),
        sprintf('%07o',  0),                  # uid
        sprintf('%07o',  0),                  # gid
        sprintf('%011o', length $content),    # size
        sprintf('%011o', 0),                  # mtime
END
  else
    cat <<'END'
        sprintf('%06o ',  $mode),
        sprintf('%06o ',  0),                  # uid
        sprintf('%06o ',  0),                  # gid
        sprintf('%011o ', length $content),    # size
        sprintf('%011o ', 0),                  # mtime
END
  fi
  cat <<'END'
        '',                                   # checksum
        $type,
        '',                                   # linkname
        "ustar",                              # magic
        "00",                                 # version
        '',                                   # username
        '',                                   # groupname
END
  if [ $bsdtar -eq 0 ]; then
    cat <<'END'
        '',                                   # dev major
        '',                                   # dev minor
END
  else
    cat <<'END'
        '000000 ',                                   # dev major
        '000000 ',                                   # dev minor
END
  fi
  cat <<'END'
        '',                                   # prefix
    );

    # compute and insert checksum
    substr($entry, 148, 7)
      = sprintf("%06o\0", unpack("%16C*", $entry));
    print $entry;
    $num_entries += 1;

    if (length $content) {
        my $blocks = ceil ((length $content)/512);
        print (pack (('a'.($blocks*512)), $content));
        $num_entries += $blocks;
    }
}

# 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.
END
  if [ $bsdtar -eq 0 ]; then
    cat <<'END'
print "\x00"x((20*ceil(($num_entries*512)/10240)-$num_entries)*512);
END
  else
    # bsdtar/libarchive seem to be using a different blocking factor
    # FIXME: the manual of bsdtar mentions 10240 as the record size
    cat <<'END'
print "\x00"x((10*ceil(($num_entries*512)/5120)-$num_entries)*512);
END
  fi
}

MMTARFILTER=
[ -x /usr/bin/mmtarfilter ] && MMTARFILTER=/usr/bin/mmtarfilter
[ -x ./tarfilter ] && MMTARFILTER=./tarfilter

# The tarball below uses PAX format 1.0 used by tar versions 1.15.92 and later
# and can also be created with GNU tar like this:
#
# $ unshare --map-auto --map-root-user --fork --pid -- tar --clamp-mtime --mtime=@0 --pax-option='delete=atime,delete=ctime' --numeric-owner --owner=0 --group=0 --format=pax --sparse -cf - sparsefile > sparse-archive.tar
# $ stat -c %s sparse-archive.tar
# 20480
# $ sha1sum sparse-archive.tar
# 20f5e1c72a3bf40ba11bd9b693692f3f11b82f99  sparse-archive.tar
#
# You need the tar package version 1.35+dfsg-7 or later for the above to work
# because earlier versions embedded the process ID into the name field of the
# ustar header in the following format:
# %d/GNUSparseFile.%p/%f
# The %p part is the PID of the tar process itself.
# https://www.gnu.org/software/tar/manual/html_node/PAX-1.html
#
# To create a reproducible tarball with earlier tar versions you can prefix
# above command with:
#
# $ unshare --map-auto --map-root-user --fork --pid -- tar ...
#
# But running that will create a slightly different tarball because the PID
# of tar in the new namespace will be 1 and tar 1.35+dfsg-7 uses a 0 as the
# static PID like libarchive does it.
#
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1152143
#
cat <<'END' | mksparsetar >/tmp/mksparsetar.pl
    # filename            mode      type content
    ['./PaxHeaders/sparsefile', oct(644), 'x', "22 GNU.sparse.major=1\x0a22 GNU.sparse.minor=0\x0a30 GNU.sparse.name=sparsefile\x0a31 GNU.sparse.realsize=8388608\x0a"],
    ['./GNUSparseFile.0/sparsefile', oct(664), 0, ((pack 'a512a4096a4096', "4\n0\n4096\n4194304\n4096\n8384512\n4096\n8388608\n0\n", "start\n","middle\n").("\x00"x4092)."end\n")],
END

perl /tmp/mksparsetar.pl >/tmp/orig.tar
test "$(stat -c %s /tmp/orig.tar)" -eq 20480
echo "20f5e1c72a3bf40ba11bd9b693692f3f11b82f99  /tmp/orig.tar" | sha1sum --status --check
if ! sparsetest; then
  echo "E: failed with PAX 1.0" >&2
  exit 1
fi

# The tarball below uses PAX format 0.0 used by tar versions 1.14–1.15.1 and
# can also be created with GNU tar like this:
#
# $ tar --sparse-version 0.1 --pax-option delete=GNU.sparse.map --clamp-mtime --mtime=@0 --pax-option='delete=atime,delete=ctime' --numeric-owner --owner=0 --group=0 --format=pax --sparse -cf sparse-archive.tar sparsefile
# $ stat -c %s sparse-archive.tar
# 20480
# $ sha1sum sparse-archive.tar
# a1d3cbf5059fc9e767852712bfe40b59f02af1db  sparse-archive.tar
#
# https://www.gnu.org/software/tar/manual/html_node/PAX-0.html

cat <<'END' | mksparsetar >/tmp/mksparsetar.pl
    # filename            mode      type content
    ['./PaxHeaders/sparsefile', oct(644), 'x', "27 GNU.sparse.size=8388608\x0a26 GNU.sparse.numblocks=4\x0a23 GNU.sparse.offset=0\x0a28 GNU.sparse.numbytes=4096\x0a29 GNU.sparse.offset=4194304\x0a28 GNU.sparse.numbytes=4096\x0a29 GNU.sparse.offset=8384512\x0a28 GNU.sparse.numbytes=4096\x0a29 GNU.sparse.offset=8388608\x0a25 GNU.sparse.numbytes=0\x0a"],
    ['sparsefile', oct(664), 0, ((pack 'a4096a4096', "start\n","middle\n").("\x00"x4092)."end\n")],
END

perl /tmp/mksparsetar.pl >/tmp/orig.tar
test "$(stat -c %s /tmp/orig.tar)" -eq 20480
echo "a1d3cbf5059fc9e767852712bfe40b59f02af1db  /tmp/orig.tar" | sha1sum --status --check
if ! sparsetest; then
  echo "E: failed with PAX 0.0" >&2
  exit 1
fi

# The tarball below uses PAX format 0.1 used by tar versions 1.15.2-1.15.91 and
# can also be created with GNU tar like this:
#
# $ tar --sparse-version 0.1 --clamp-mtime --mtime=@0 --pax-option='delete=atime,delete=ctime' --numeric-owner --owner=0 --group=0 --format=pax --sparse -cf sparse-archive.tar sparsefile
# $ stat -c %s sparse-archive.tar
# 20480
# $ sha1sum sparse-archive.tar
# e00ad1a1b4b2bbe04d0412048c7f29aa574e5380  sparse-archive.tar
#
# You need the tar package version 1.35+dfsg-7 or later for the above to work
# because of reasons explained above.

cat <<'END' | mksparsetar >/tmp/mksparsetar.pl
    # filename            mode      type content
    ['./PaxHeaders/sparsefile', oct(644), 'x', "27 GNU.sparse.size=8388608\x0a26 GNU.sparse.numblocks=4\x0a30 GNU.sparse.name=sparsefile\x0a61 GNU.sparse.map=0,4096,4194304,4096,8384512,4096,8388608,0\x0a"],
    ['./GNUSparseFile.0/sparsefile', oct(664), 0, ((pack 'a4096a4096', "start\n","middle\n").("\x00"x4092)."end\n")],
END

perl /tmp/mksparsetar.pl >/tmp/orig.tar
test "$(stat -c %s /tmp/orig.tar)" -eq 20480
echo "e00ad1a1b4b2bbe04d0412048c7f29aa574e5380  /tmp/orig.tar" | sha1sum --status --check
if ! sparsetest; then
  echo "E: failed with PAX 0.1" >&2
  exit 1
fi

# The tarball below uses PAX format 1.0 but uses bsdtar to produce it which
# avoids the unreproducible PID embedded in each tarball because libarchive
# sets the PID to a constant value of zero.
#
# $ bsdtar --clamp-mtime --mtime=@0 --numeric-owner --uid 0 --gid 0 --read-sparse -cf sparse-archive.tar sparsefile
# $ stat -c %s sparse-archive.tar
# 15360
# $ sha1sum sparse-archive.tar
# c8d1cbe4308380ed91d3134003230af916da6a8f  sparse-archive.tar
#

cat <<'END' | mksparsetar bsdtar >/tmp/mksparsetar.pl
    # filename            mode      type content
    ['PaxHeader/sparsefile', oct(664), 'x', "22 GNU.sparse.major=1\x0a22 GNU.sparse.minor=0\x0a30 GNU.sparse.name=sparsefile\x0a31 GNU.sparse.realsize=8388608\x0a"],
    ['GNUSparseFile.0/sparsefile', oct(664), 0, ((pack 'a512a4096a4096', "3\n0\n4096\n4194304\n4096\n8384512\n4096\n", "start\n","middle\n").("\x00"x4092)."end\n")],
END

perl /tmp/mksparsetar.pl >/tmp/orig.tar
test "$(stat -c %s /tmp/orig.tar)" -eq 15360
echo "c8d1cbe4308380ed91d3134003230af916da6a8f  /tmp/orig.tar" | sha1sum --status --check
if ! sparsetest; then
  echo "E: failed with PAX 1.0 (bsdtar)" >&2
  exit 1
fi
