aboutsummaryrefslogtreecommitdiff
path: root/tests/sys/fs/tarfs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/sys/fs/tarfs')
-rw-r--r--tests/sys/fs/tarfs/Makefile10
-rw-r--r--tests/sys/fs/tarfs/mktar.c273
-rw-r--r--tests/sys/fs/tarfs/tarfs_test.sh417
-rw-r--r--tests/sys/fs/tarfs/tarsum.c128
4 files changed, 828 insertions, 0 deletions
diff --git a/tests/sys/fs/tarfs/Makefile b/tests/sys/fs/tarfs/Makefile
new file mode 100644
index 000000000000..72355a08a158
--- /dev/null
+++ b/tests/sys/fs/tarfs/Makefile
@@ -0,0 +1,10 @@
+PACKAGE= tests
+
+TESTSDIR= ${TESTSBASE}/sys/fs/tarfs
+BINDIR= ${TESTSDIR}
+
+PROGS+= mktar tarsum
+
+ATF_TESTS_SH+= tarfs_test
+
+.include <bsd.test.mk>
diff --git a/tests/sys/fs/tarfs/mktar.c b/tests/sys/fs/tarfs/mktar.c
new file mode 100644
index 000000000000..b4ec271c73a1
--- /dev/null
+++ b/tests/sys/fs/tarfs/mktar.c
@@ -0,0 +1,273 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2023 Klara, Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <err.h>
+#include <fcntl.h>
+#include <paths.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#define PROGNAME "mktar"
+
+#define SUBDIRNAME "directory"
+#define NORMALFILENAME "file"
+#define SPARSEFILENAME "sparse_file"
+#define HARDLINKNAME "hard_link"
+#define SHORTLINKNAME "short_link"
+#define LONGLINKNAME "long_link"
+
+static bool opt_g;
+static bool opt_v;
+
+static void verbose(const char *fmt, ...)
+{
+ va_list ap;
+
+ if (!opt_v)
+ return;
+ fprintf(stderr, "%s: ", PROGNAME);
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ fprintf(stderr, "\n");
+}
+
+static void
+mknormalfile(const char *filename, mode_t mode)
+{
+ char buf[512];
+ ssize_t res;
+ int fd;
+
+ if ((fd = open(filename, O_RDWR|O_CREAT|O_EXCL, mode)) < 0)
+ err(1, "%s", filename);
+ for (unsigned int i = 0; i < sizeof(buf); i++)
+ buf[i] = 32 + i % 64;
+ res = write(fd, buf, sizeof(buf));
+ if (res < 0)
+ err(1, "%s", filename);
+ if (res != sizeof(buf))
+ errx(1, "%s: short write", filename);
+ close(fd);
+}
+
+static void
+mksparsefile(const char *filename, mode_t mode)
+{
+ char buf[511];
+ ssize_t res;
+ int fd;
+
+ if ((fd = open(filename, O_RDWR|O_CREAT|O_EXCL, mode)) < 0)
+ err(1, "%s", filename);
+ for (unsigned int i = 33; i <= 126; i++) {
+ memset(buf, i, sizeof(buf));
+ if (lseek(fd, 1048576LU * (i - 32), SEEK_SET) < 0)
+ err(1, "%s", filename);
+ res = write(fd, buf, sizeof(buf));
+ if (res < 0)
+ err(1, "%s", filename);
+ if (res != sizeof(buf))
+ errx(1, "%s: short write", filename);
+ }
+ close(fd);
+}
+
+static char *
+mklonglinktarget(const char *dirname, const char *filename)
+{
+ char *piece, *target;
+
+ if (asprintf(&piece, "%1$s/../%1$s/../%1$s/../%1$s/../", dirname) < 0)
+ err(1, "asprintf()");
+ if (asprintf(&target, "%1$s%1$s%1$s%1$s%1$s%1$s%1$s%1$s%2$s", piece, filename) < 0)
+ err(1, "asprintf()");
+ free(piece);
+ return target;
+}
+
+static void
+mktar(void)
+{
+ char *linktarget;
+
+ /* create a subdirectory */
+ verbose("mkdir %s", SUBDIRNAME);
+ if (mkdir(SUBDIRNAME, 0755) != 0)
+ err(1, "%s", SUBDIRNAME);
+
+ /* create a normal file */
+ verbose("creating %s", NORMALFILENAME);
+ mknormalfile(NORMALFILENAME, 0644);
+
+ /* create a sparse file */
+ verbose("creating %s", SPARSEFILENAME);
+ mksparsefile(SPARSEFILENAME, 0644);
+ chflags(SPARSEFILENAME, UF_NODUMP);
+
+ /* create a hard link */
+ verbose("link %s %s", SPARSEFILENAME, HARDLINKNAME);
+ if (link(SPARSEFILENAME, HARDLINKNAME) != 0)
+ err(1, "%s", HARDLINKNAME);
+
+ /* create a symbolic link with a short target */
+ verbose("symlink %s %s", SPARSEFILENAME, SHORTLINKNAME);
+ if (symlink(SPARSEFILENAME, SHORTLINKNAME) != 0)
+ err(1, "%s", SHORTLINKNAME);
+
+ /* create a symbolic link with a long target */
+ linktarget = mklonglinktarget(SUBDIRNAME, SPARSEFILENAME);
+ verbose("symlink %s %s", linktarget, LONGLINKNAME);
+ if (symlink(linktarget, LONGLINKNAME) != 0)
+ err(1, "%s", LONGLINKNAME);
+ free(linktarget);
+}
+
+static void
+usage(void)
+{
+
+ fprintf(stderr, "usage: %s [-gv] tarfile\n", PROGNAME);
+ exit(EXIT_FAILURE);
+}
+
+int
+main(int argc, char *argv[])
+{
+ const char *tarfilename;
+ char *dirname;
+ int opt, wstatus;
+ pid_t pid;
+
+ while ((opt = getopt(argc, argv, "gv")) != -1)
+ switch (opt) {
+ case 'g':
+ opt_g = true;
+ break;
+ case 'v':
+ opt_v = true;
+ break;
+ default:
+ usage();
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (argc != 1)
+ usage();
+ tarfilename = *argv;
+
+ if (asprintf(&dirname, "%s%s.XXXXXXXX", _PATH_TMP, PROGNAME) < 0)
+ err(1, "asprintf()");
+ if (mkdtemp(dirname) == NULL)
+ err(1, "%s", dirname);
+ verbose("mkdir %s", dirname);
+
+ /* fork a child to create the files */
+ if ((pid = fork()) < 0)
+ err(1, "fork()");
+ if (pid == 0) {
+ verbose("cd %s", dirname);
+ if (chdir(dirname) != 0)
+ err(1, "%s", dirname);
+ verbose("umask 022");
+ umask(022);
+ mktar();
+ verbose("cd -");
+ exit(0);
+ }
+ if (waitpid(pid, &wstatus, 0) < 0)
+ err(1, "waitpid()");
+ if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0)
+ errx(1, "child failed");
+
+ /* fork a child to create the tarball */
+ if ((pid = fork()) < 0)
+ err(1, "fork()");
+ if (pid == 0) {
+ verbose("creating tarball");
+ execlp(opt_g ? "gtar" : "tar",
+ "tar",
+ "-c",
+ "-f", tarfilename,
+ "-C", dirname,
+ "--posix",
+ "--zstd",
+#if 0
+ "--options", "zstd:frame-per-file",
+#endif
+ "./" SUBDIRNAME "/../" NORMALFILENAME,
+ "./" SPARSEFILENAME,
+ "./" HARDLINKNAME,
+ "./" SHORTLINKNAME,
+ "./" SUBDIRNAME,
+ "./" LONGLINKNAME,
+ NULL);
+ err(1, "execlp()");
+ }
+ if (waitpid(pid, &wstatus, 0) < 0)
+ err(1, "waitpid()");
+ if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0)
+ errx(1, "child failed");
+
+ /* fork a child to delete everything */
+ if ((pid = fork()) < 0)
+ err(1, "fork()");
+ if (pid == 0) {
+ verbose("cd %s", dirname);
+ if (chdir(dirname) != 0)
+ err(1, "%s", dirname);
+ verbose("rm %s", LONGLINKNAME);
+ (void)unlink(LONGLINKNAME);
+ verbose("rm %s", SHORTLINKNAME);
+ (void)unlink(SHORTLINKNAME);
+ verbose("rm %s", HARDLINKNAME);
+ (void)unlink(HARDLINKNAME);
+ verbose("rm %s", SPARSEFILENAME);
+ (void)unlink(SPARSEFILENAME);
+ verbose("rmdir %s", SUBDIRNAME);
+ (void)rmdir(SUBDIRNAME);
+ verbose("cd -");
+ exit(0);
+ }
+ if (waitpid(pid, &wstatus, 0) < 0)
+ err(1, "waitpid()");
+ if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0)
+ errx(1, "child failed");
+ verbose("rmdir %s", dirname);
+ (void)rmdir(dirname);
+
+ exit(0);
+}
diff --git a/tests/sys/fs/tarfs/tarfs_test.sh b/tests/sys/fs/tarfs/tarfs_test.sh
new file mode 100644
index 000000000000..d4de71271985
--- /dev/null
+++ b/tests/sys/fs/tarfs/tarfs_test.sh
@@ -0,0 +1,417 @@
+#!/bin/sh
+#-
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2023-2024 Klara, Inc.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+
+mnt="$(realpath ${TMPDIR:-/tmp})/mnt"
+
+# expected SHA256 checksum of file contained in test tarball
+sum=4da2143234486307bb44eaa610375301781a577d1172f362b88bb4b1643dee62
+
+tar() {
+ if [ -n "${TARFS_USE_GNU_TAR}" ] ; then
+ gtar --posix --absolute-names "$@"
+ else
+ bsdtar "$@"
+ fi
+}
+
+mktar() {
+ "$(atf_get_srcdir)"/mktar ${TARFS_USE_GNU_TAR+-g} "$@"
+}
+
+tarsum() {
+ "$(atf_get_srcdir)"/tarsum
+}
+
+tarfs_setup() {
+ mkdir "${mnt}"
+}
+
+tarfs_cleanup() {
+ umount -f "${mnt}" 2>/dev/null || true
+}
+
+atf_test_case tarfs_basic cleanup
+tarfs_basic_head() {
+ atf_set "descr" "Basic function test"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+}
+tarfs_basic_body() {
+ tarfs_setup
+ local tarball="${PWD}/tarfs_test.tar.zst"
+ mktar "${tarball}"
+ atf_check mount -rt tarfs "${tarball}" "${mnt}"
+ atf_check -o match:"^${tarball} on ${mnt} \(tarfs," mount
+ atf_check test "${mnt}"/sparse_file -ef "${mnt}"/hard_link
+ atf_check test "${mnt}"/sparse_file -ef "${mnt}"/short_link
+ atf_check test "${mnt}"/sparse_file -ef "${mnt}"/long_link
+ atf_check -o inline:"${sum}\n" sha256 -q "${mnt}"/sparse_file
+ atf_check -o inline:"2,40755\n" stat -f%l,%p "${mnt}"/directory
+ atf_check -o inline:"1,100644\n" stat -f%l,%p "${mnt}"/file
+ atf_check -o inline:"2,100644\n" stat -f%l,%p "${mnt}"/hard_link
+ atf_check -o inline:"1,120755\n" stat -f%l,%p "${mnt}"/long_link
+ atf_check -o inline:"1,120755\n" stat -f%l,%p "${mnt}"/short_link
+ atf_check -o inline:"2,100644\n" stat -f%l,%p "${mnt}"/sparse_file
+ atf_check -o inline:"3,40755\n" stat -f%l,%p "${mnt}"
+}
+tarfs_basic_cleanup() {
+ tarfs_cleanup
+}
+
+atf_test_case tarfs_basic_gnu cleanup
+tarfs_basic_gnu_head() {
+ atf_set "descr" "Basic function test using GNU tar"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+ atf_set "require.progs" "gtar"
+}
+tarfs_basic_gnu_body() {
+ TARFS_USE_GNU_TAR=true
+ tarfs_basic_body
+}
+tarfs_basic_gnu_cleanup() {
+ tarfs_basic_cleanup
+}
+
+atf_test_case tarfs_notdir_device cleanup
+tarfs_notdir_device_head() {
+ atf_set "descr" "Regression test for PR 269519 and 269561"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+}
+tarfs_notdir_device_body() {
+ tarfs_setup
+ atf_check mknod d b 0xdead 0xbeef
+ tar -cf tarfs_notdir.tar d
+ rm d
+ mkdir d
+ echo "boom" >d/f
+ tar -rf tarfs_notdir.tar d/f
+ atf_check -s not-exit:0 -e match:"Invalid" \
+ mount -rt tarfs tarfs_notdir.tar "${mnt}"
+}
+tarfs_notdir_device_cleanup() {
+ tarfs_cleanup
+}
+
+atf_test_case tarfs_notdir_device_gnu cleanup
+tarfs_notdir_device_gnu_head() {
+ atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+ atf_set "require.progs" "gtar"
+}
+tarfs_notdir_device_gnu_body() {
+ TARFS_USE_GNU_TAR=true
+ tarfs_notdir_device_body
+}
+tarfs_notdir_device_gnu_cleanup() {
+ tarfs_notdir_device_cleanup
+}
+
+atf_test_case tarfs_notdir_dot cleanup
+tarfs_notdir_dot_head() {
+ atf_set "descr" "Regression test for PR 269519 and 269561"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+}
+tarfs_notdir_dot_body() {
+ tarfs_setup
+ echo "hello" >d
+ tar -cf tarfs_notdir.tar d
+ rm d
+ mkdir d
+ echo "world" >d/f
+ tar -rf tarfs_notdir.tar d/./f
+ atf_check -s not-exit:0 -e match:"Invalid" \
+ mount -rt tarfs tarfs_notdir.tar "${mnt}"
+}
+tarfs_notdir_dot_cleanup() {
+ tarfs_cleanup
+}
+
+atf_test_case tarfs_notdir_dot_gnu cleanup
+tarfs_notdir_dot_gnu_head() {
+ atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+ atf_set "require.progs" "gtar"
+}
+tarfs_notdir_dot_gnu_body() {
+ TARFS_USE_GNU_TAR=true
+ tarfs_notdir_dot_body
+}
+tarfs_notdir_dot_gnu_cleanup() {
+ tarfs_notdir_dot_cleanup
+}
+
+atf_test_case tarfs_notdir_dotdot cleanup
+tarfs_notdir_dotdot_head() {
+ atf_set "descr" "Regression test for PR 269519 and 269561"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+}
+tarfs_notdir_dotdot_body() {
+ tarfs_setup
+ echo "hello" >d
+ tar -cf tarfs_notdir.tar d
+ rm d
+ mkdir d
+ echo "world" >f
+ tar -rf tarfs_notdir.tar d/../f
+ atf_check -s not-exit:0 -e match:"Invalid" \
+ mount -rt tarfs tarfs_notdir.tar "${mnt}"
+}
+tarfs_notdir_dotdot_cleanup() {
+ tarfs_cleanup
+}
+
+atf_test_case tarfs_notdir_dotdot_gnu cleanup
+tarfs_notdir_dotdot_gnu_head() {
+ atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+ atf_set "require.progs" "gtar"
+}
+tarfs_notdir_dotdot_gnu_body() {
+ TARFS_USE_GNU_TAR=true
+ tarfs_notdir_dotdot_body
+}
+tarfs_notdir_dotdot_gnu_cleanup() {
+ tarfs_notdir_dotdot_cleanup
+}
+
+atf_test_case tarfs_notdir_file cleanup
+tarfs_notdir_file_head() {
+ atf_set "descr" "Regression test for PR 269519 and 269561"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+}
+tarfs_notdir_file_body() {
+ tarfs_setup
+ echo "hello" >d
+ tar -cf tarfs_notdir.tar d
+ rm d
+ mkdir d
+ echo "world" >d/f
+ tar -rf tarfs_notdir.tar d/f
+ atf_check -s not-exit:0 -e match:"Invalid" \
+ mount -rt tarfs tarfs_notdir.tar "${mnt}"
+}
+tarfs_notdir_file_cleanup() {
+ tarfs_cleanup
+}
+
+atf_test_case tarfs_notdir_file_gnu cleanup
+tarfs_notdir_file_gnu_head() {
+ atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+ atf_set "require.progs" "gtar"
+}
+tarfs_notdir_file_gnu_body() {
+ TARFS_USE_GNU_TAR=true
+ tarfs_notdir_file_body
+}
+tarfs_notdir_file_gnu_cleanup() {
+ tarfs_notdir_file_cleanup
+}
+
+atf_test_case tarfs_emptylink cleanup
+tarfs_emptylink_head() {
+ atf_set "descr" "Regression test for PR 277360: empty link target"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+}
+tarfs_emptylink_body() {
+ tarfs_setup
+ touch z
+ ln -f z hard
+ ln -fs z soft
+ tar -cf - z hard soft | dd bs=512 skip=1 | tr z '\0' | \
+ tarsum >> tarfs_emptylink.tar
+ atf_check -s not-exit:0 -e match:"Invalid" \
+ mount -rt tarfs tarfs_emptylink.tar "${mnt}"
+}
+tarfs_emptylink_cleanup() {
+ tarfs_cleanup
+}
+
+atf_test_case tarfs_linktodir cleanup
+tarfs_linktodir_head() {
+ atf_set "descr" "Regression test for PR 277360: link to directory"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+}
+tarfs_linktodir_body() {
+ tarfs_setup
+ mkdir d
+ tar -cf - d | dd bs=512 count=1 > tarfs_linktodir.tar
+ rmdir d
+ touch d
+ ln -f d link
+ tar -cf - d link | dd bs=512 skip=1 >> tarfs_linktodir.tar
+ atf_check -s not-exit:0 -e match:"Invalid" \
+ mount -rt tarfs tarfs_linktodir.tar "${mnt}"
+}
+tarfs_linktodir_cleanup() {
+ tarfs_cleanup
+}
+
+atf_test_case tarfs_linktononexistent cleanup
+tarfs_linktononexistent_head() {
+ atf_set "descr" "Regression test for PR 277360: link to nonexistent target"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+}
+tarfs_linktononexistent_body() {
+ tarfs_setup
+ touch f
+ ln -f f link
+ tar -cf - f link | dd bs=512 skip=1 >> tarfs_linktononexistent.tar
+ atf_check -s not-exit:0 -e match:"Invalid" \
+ mount -rt tarfs tarfs_linktononexistent.tar "${mnt}"
+}
+tarfs_linktononexistent_cleanup() {
+ tarfs_cleanup
+}
+
+atf_test_case tarfs_checksum cleanup
+tarfs_checksum_head() {
+ atf_set "descr" "Verify that the checksum covers header padding"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+}
+tarfs_checksum_body() {
+ tarfs_setup
+ touch f
+ tar -cf tarfs_checksum.tar f
+ truncate -s 500 tarfs_checksum.tar
+ printf "\1\1\1\1\1\1\1\1\1\1\1\1" >> tarfs_checksum.tar
+ dd if=/dev/zero bs=512 count=2 >> tarfs_checksum.tar
+ hexdump -C tarfs_checksum.tar
+ atf_check -s not-exit:0 -e match:"Invalid" \
+ mount -rt tarfs tarfs_checksum.tar "${mnt}"
+}
+tarfs_checksum_cleanup() {
+ tarfs_cleanup
+}
+
+atf_test_case tarfs_long_names cleanup
+tarfs_long_names_head() {
+ atf_set "descr" "Verify that tarfs supports long file names"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+}
+tarfs_long_names_body() {
+ tarfs_setup
+ local a b c d e
+ a="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ b="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+ c="cccccccccccccccccccccccccccccccccccccccc"
+ d="dddddddddddddddddddddddddddddddddddddddd"
+ e="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
+ mkdir -p "${a}"
+ touch "${a}/${b}_${c}_${d}_${e}_foo"
+ ln "${a}/${b}_${c}_${d}_${e}_foo" "${a}/${b}_${c}_${d}_${e}_bar"
+ ln -s "${b}_${c}_${d}_${e}_bar" "${a}/${b}_${c}_${d}_${e}_baz"
+ tar -cf tarfs_long_names.tar "${a}"
+ atf_check mount -rt tarfs tarfs_long_names.tar "${mnt}"
+}
+tarfs_long_names_cleanup() {
+ tarfs_cleanup
+}
+
+atf_test_case tarfs_long_paths cleanup
+tarfs_long_paths_head() {
+ atf_set "descr" "Verify that tarfs supports long paths"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+}
+tarfs_long_paths_body() {
+ tarfs_setup
+ local a b c d e
+ a="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ b="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+ c="cccccccccccccccccccccccccccccccccccccccc"
+ d="dddddddddddddddddddddddddddddddddddddddd"
+ e="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
+ mkdir -p "${a}/${b}/${c}/${d}/${e}"
+ touch "${a}/${b}/${c}/${d}/${e}/foo"
+ ln "${a}/${b}/${c}/${d}/${e}/foo" "${a}/${b}/${c}/${d}/${e}/bar"
+ ln -s "${b}/${c}/${d}/${e}/bar" "${a}/baz"
+ tar -cf tarfs_long_paths.tar "${a}"
+ atf_check mount -rt tarfs tarfs_long_paths.tar "${mnt}"
+}
+tarfs_long_paths_cleanup() {
+ tarfs_cleanup
+}
+
+atf_test_case tarfs_git_archive cleanup
+tarfs_git_archive_head() {
+ atf_set "descr" "Verify that tarfs supports archives created by git"
+ atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
+ atf_set "require.progs" "git"
+}
+tarfs_git_archive_body() {
+ tarfs_setup
+ mkdir foo
+ echo "Hello, world!" >foo/bar
+ git -C foo init --initial-branch=tarfs
+ git -C foo config user.name "File System"
+ git -C foo config user.email fs@freebsd.org
+ git -C foo add bar
+ git -C foo commit -m bar
+ git -C foo archive --output=../tarfs_git_archive.tar HEAD
+ atf_check mount -rt tarfs tarfs_git_archive.tar "${mnt}"
+ atf_check -o file:foo/bar cat "${mnt}"/bar
+}
+tarfs_git_archive_cleanup() {
+ tarfs_cleanup
+}
+
+atf_init_test_cases() {
+ atf_add_test_case tarfs_basic
+ atf_add_test_case tarfs_basic_gnu
+ atf_add_test_case tarfs_notdir_device
+ atf_add_test_case tarfs_notdir_device_gnu
+ atf_add_test_case tarfs_notdir_dot
+ atf_add_test_case tarfs_notdir_dot_gnu
+ atf_add_test_case tarfs_notdir_dotdot
+ atf_add_test_case tarfs_notdir_dotdot_gnu
+ atf_add_test_case tarfs_notdir_file
+ atf_add_test_case tarfs_notdir_file_gnu
+ atf_add_test_case tarfs_emptylink
+ atf_add_test_case tarfs_linktodir
+ atf_add_test_case tarfs_linktononexistent
+ atf_add_test_case tarfs_checksum
+ atf_add_test_case tarfs_long_names
+ atf_add_test_case tarfs_long_paths
+ atf_add_test_case tarfs_git_archive
+}
diff --git a/tests/sys/fs/tarfs/tarsum.c b/tests/sys/fs/tarfs/tarsum.c
new file mode 100644
index 000000000000..73ead2230a5e
--- /dev/null
+++ b/tests/sys/fs/tarfs/tarsum.c
@@ -0,0 +1,128 @@
+/*-
+ * Copyright (c) 2024 Klara, Inc.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * This program reads a tarball from stdin, recalculates the checksums of
+ * all ustar records within it, and writes the result to stdout.
+ */
+
+#include <err.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+static bool opt_v;
+
+static int
+verbose(const char *fmt, ...)
+{
+ va_list ap;
+ int ret;
+
+ if (!opt_v)
+ return (0);
+ va_start(ap, fmt);
+ ret = vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ return (ret);
+}
+
+static void
+tarsum(FILE *in, const char *ifn, FILE *out, const char *ofn)
+{
+ union {
+ uint8_t bytes[512];
+ struct {
+ uint8_t prelude[148];
+ char checksum[8];
+ uint8_t interlude[101];
+ char magic[6];
+ char version[2];
+ char postlude[];
+ };
+ } ustar;
+ unsigned long sum;
+ off_t offset = 0;
+ ssize_t ret;
+ size_t i;
+
+ for (;;) {
+ if ((ret = fread(&ustar, 1, sizeof(ustar), in)) < 0)
+ err(1, "%s", ifn);
+ else if (ret == 0)
+ break;
+ else if ((size_t)ret < sizeof(ustar))
+ errx(1, "%s: Short read", ifn);
+ if (strcmp(ustar.magic, "ustar") == 0 &&
+ ustar.version[0] == '0' && ustar.version[1] == '0') {
+ verbose("header found at offset %#lx\n", offset);
+ verbose("current checksum %.*s\n",
+ (int)sizeof(ustar.checksum), ustar.checksum);
+ memset(ustar.checksum, ' ', sizeof(ustar.checksum));
+ for (sum = i = 0; i < sizeof(ustar); i++)
+ sum += ustar.bytes[i];
+ verbose("calculated checksum %#lo\n", sum);
+ sprintf(ustar.checksum, "%#lo", sum);
+ }
+ if ((ret = fwrite(&ustar, 1, sizeof(ustar), out)) < 0)
+ err(1, "%s", ofn);
+ else if ((size_t)ret < sizeof(ustar))
+ errx(1, "%s: Short write", ofn);
+ offset += sizeof(ustar);
+ }
+ verbose("%lu bytes processed\n", offset);
+}
+
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: tarsum [-v] [-o output] [input]\n");
+ exit(1);
+}
+
+int
+main(int argc, char *argv[])
+{
+ const char *ifn, *ofn = NULL;
+ FILE *in, *out;
+ int opt;
+
+ while ((opt = getopt(argc, argv, "o:v")) != -1) {
+ switch (opt) {
+ case 'o':
+ ofn = optarg;
+ break;
+ case 'v':
+ opt_v = true;
+ break;
+ default:
+ usage();
+ }
+ }
+ argc -= optind;
+ argv += optind;
+ if (argc == 0 || strcmp(*argv, "-") == 0) {
+ ifn = "stdin";
+ in = stdin;
+ } else if (argc == 1) {
+ ifn = *argv;
+ if ((in = fopen(ifn, "rb")) == NULL)
+ err(1, "%s", ifn);
+ } else {
+ usage();
+ }
+ if (ofn == NULL || strcmp(ofn, "-") == 0) {
+ ofn = "stdout";
+ out = stdout;
+ } else {
+ if ((out = fopen(ofn, "wb")) == NULL)
+ err(1, "%s", ofn);
+ }
+ tarsum(in, ifn, out, ofn);
+ return (0);
+}