aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMattias Andrée <m@maandree.se>2026-05-28 18:46:27 +0200
committerMattias Andrée <m@maandree.se>2026-05-28 18:46:27 +0200
commit55e9f58fa7c551aebfd6084b778cd942fa8b20ce (patch)
tree7177e49a9c3a3c2e9cd4f88e8507cb19c9f6c1b0
parentm fix (diff)
downloadrelease-scripts-55e9f58fa7c551aebfd6084b778cd942fa8b20ce.tar.gz
release-scripts-55e9f58fa7c551aebfd6084b778cd942fa8b20ce.tar.bz2
release-scripts-55e9f58fa7c551aebfd6084b778cd942fa8b20ce.tar.xz
Update (fixes support for a few edge cases, and leaves nothing laying around)
Signed-off-by: Mattias Andrée <m@maandree.se>
Diffstat (limited to '')
-rw-r--r--LICENSE24
-rw-r--r--README2
-rwxr-xr-xgen-checksums106
-rw-r--r--util/.gitignore18
-rw-r--r--util/.makeenv2
-rw-r--r--util/Makefile36
-rw-r--r--util/README80
-rw-r--r--util/config.mk10
-rwxr-xr-xutil/gen-checksums23
-rwxr-xr-xutil/get-and-check17
-rwxr-xr-xutil/get-checksums42
-rwxr-xr-xutil/order-checksums16
-rw-r--r--util/repodiff.c316
-rw-r--r--util/tmpmount.c44
-rwxr-xr-xutil/validate-tarball27
15 files changed, 663 insertions, 100 deletions
diff --git a/LICENSE b/LICENSE
index d2ba2dc..ece4d44 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,20 @@
-Copyright © 2024, 2025, 2026 Mattias Andrée (m@maandree.se)
+ISC License
-Copying and distribution of these scripts, with or without modification,
-are permitted in any medium without royalty provided the copyright
-notice and this notice are preserved. These scripts is offered as-is,
-without any warranty.
+© 2024, 2025, 2026 Mattias Andrée <m@maandree.se>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+----------
+
+./maandree-dl and ./validate-checksum are under more lax license,
+their license text is available near the top of those files.
diff --git a/README b/README
index d7a9c99..a3f685a 100644
--- a/README
+++ b/README
@@ -8,7 +8,7 @@ a git tag, I know that my local git repositry is clean. This is used
as the reference for truth. I also create a tarball for a static release.
I then use ./gen-checksums which checks the tarball, along with my
non-static releases (created by pushing the git tags), against my local
-git repositry. Once all releases have been validated, ./gen-checksums
+git repositry. Once a release have been validated, ./gen-checksums
outputs the checksums for each tarball, using a number of hash functions.
The checksums are not specifically tied to the tarballs, but rather listed
as known good checksums.
diff --git a/gen-checksums b/gen-checksums
index 785aff6..f894a38 100755
--- a/gen-checksums
+++ b/gen-checksums
@@ -1,104 +1,22 @@
#!/bin/sh
-# Copyright © 2024, 2025 Mattias Andrée (m@maandree.se)
-#
-# Copying and distribution of this script, with or without modification,
-# are permitted in any medium without royalty provided the copyright
-# notice and this notice are preserved. This script is offered as-is,
-# without any warranty.
-
-
set -e
-if ! test $# = 2 || test -t 0 || test -t 1; then
- printf 'usage: %s tarball git-dir < version-info-file > checksum-listing\n' "$0" >&2
+if test ! $# = 3; then
+ printf 'usage: %s git-dir project version\n' "$0" >&2
exit 1
fi
-tarball="$1"
-gitdir="$2"
-
-urls="$(grep '^Tarball: ' | cut -d ' ' -f 2 | grep -v '^https://maandree.se/static/')"
-
-workdir="$$.tmpdir"
-rm -rf -- "${workdir}"
-
-refdir="${workdir}/reference"
-mkdir -p -- "${refdir}"
-gunzip < "${tarball}" | (cd "${refdir}" && tar -x)
-if test "$(printf '%s\n' "${refdir}"/* | wc -l)" = 1; then
- refdir="$(printf '%s\n' "${refdir}"/*)"
-fi
-
-ln -s -- "$(cd -- "${gitdir}" && pwd)" "${workdir}/gitdir"
-
-add_dirs () {
- while read f; do
- printf '%s\n' "$f"
- while :; do
- d="$(printf '%s\n' "$f" | sed 's|/[^/]*$||')"
- if test "$d" = "$f"; then
- break
- fi
- printf '%s\n' "$d"
- f="$d"
- done
- done
-}
-
-gitfiles="$( (cd -- "${gitdir}" && git ls-files) | add_dirs | sort -u | tee -- "${workdir}/gitfiles")"
-test -n "${gitfiles}"
-tarfiles="$( (cd -- "${refdir}" && find) | sed 's/^\.\///' | sed '/^\.$/d' | add_dirs | sort -u | tee -- "${workdir}/tarfiles")"
-test -n "${tarfiles}"
-diff -u -- "${workdir}/gitfiles" "${workdir}/tarfiles" >&2
-# the following line makes the safe assumption that $workdir (and $refdir) does not contain {}
-xargs -I {} diff -u "${refdir}"/{} "${workdir}/gitdir"/{} < "${workdir}/gitfiles" >&2
-
-getandcheck () {
- url="$1"
- number="$2"
- curl -sL -- "${url}" > "${workdir}/downloaded-${number}.tar.gz"
- checkdir="${workdir}/downloaded"
- rm -rf -- "${checkdir}"
- mkdir -p -- "${checkdir}"
- gunzip < "${workdir}/downloaded-${number}.tar.gz" | (cd "${checkdir}" && tar -x)
- if test "$(printf '%s\n' "${checkdir}"/* | wc -l)" = 1; then
- checkdir="$(printf '%s\n' "${checkdir}"/*)"
- fi
- diff -ur -- "${refdir}" "${checkdir}" >&2
- rm -rf -- "${checkdir}"
-}
-
-nr=1
-for url in $urls; do
- getandcheck "${url}" $(( nr++ ))
-done
-
-genfor () {
- tool="$1"
- name="$2"
- sum="$(${tool} < "${tarball}" | cut -d ' ' -f 1)"
- sums="$(printf '%s checksum: %s\n' "${name}" "${sum}")"
- number=1
- for url in $urls; do
- sum="$($tool < "${workdir}/downloaded-${number}.tar.gz" | cut -d ' ' -f 1)"
- test -n "$sum"
- sums="$(printf '%s\n%s checksum: %s\n' "${sums}" "${name}" "${sum}")"
- : $(( number++ ))
- done
- printf '%s\n' "${sums}" | sort -u
+die () {
+ printf '%s\n' "$@" >&2
+ exit 1
}
-genfor sha224sum SHA224
-genfor sha256sum SHA256
-genfor sha384sum SHA384
-genfor sha512sum SHA512
-genfor sha512-224sum SHA512/224
-genfor sha512-256sum SHA512/256
-genfor sha3-224sum SHA3-224
-genfor sha3-256sum SHA3-256
-genfor sha3-384sum SHA3-384
-genfor sha3-512sum SHA3-512
-genfor b2sum BLAKE2b
+gitdir="$1"
+tarball=".static/$2-$3.tar.gz"
+infofile="rel/$2/$3.info"
+sumfile="rel/$2/$3.checksums"
+test -e "${tarball}" || die "${tarball} does not exist"
+test -e "${infofile}" || die "${infofile} does not exist"
-rm -rf -- "${workdir}"
+"$(dirname -- "$0")"/util/gen-checksums "${tarball}" "${gitdir}" < "${infofile}" > "${sumfile}"
diff --git a/util/.gitignore b/util/.gitignore
new file mode 100644
index 0000000..88f9aca
--- /dev/null
+++ b/util/.gitignore
@@ -0,0 +1,18 @@
+*\#*
+*~
+*.o
+*.a
+*.t
+*.lo
+*.to
+*.su
+*.so
+*.so.*
+*.dll
+*.dylib
+*.gch
+*.gcov
+*.gcno
+*.gcda
+/tmpmount
+/repodiff
diff --git a/util/.makeenv b/util/.makeenv
new file mode 100644
index 0000000..f345ae5
--- /dev/null
+++ b/util/.makeenv
@@ -0,0 +1,2 @@
+MAKEENV_OPTS_OPT_ATTACHED_ARG = j
+CC = gcc-extreme -std=c99
diff --git a/util/Makefile b/util/Makefile
new file mode 100644
index 0000000..d5e3a6c
--- /dev/null
+++ b/util/Makefile
@@ -0,0 +1,36 @@
+.POSIX:
+
+CONFIGFILE = config.mk
+include $(CONFIGFILE)
+
+BIN =\
+ tmpmount\
+ repodiff
+
+OBJ =\
+ tmpmount.o\
+ repodiff.o
+
+HDR =
+
+all: $(BIN)
+$(OBJ): $(HDR)
+
+.c.o:
+ $(CC) -c -o $@ $< $(CFLAGS) $(CPPFLAGS)
+
+tmpmount: tmpmount.o
+ $(CC) -o $@ tmpmount.o $(LDFLAGS)
+ $(ASROOT) sh -c "chown -- '0:0' $@ && chmod -- 4755 $@"
+
+repodiff: repodiff.o
+ $(CC) -o $@ repodiff.o $(LDFLAGS)
+
+clean:
+ -rm -f -- *.o *.a *.lo *.su *.so *.so.* *.gch *.gcov *.gcno *.gcda
+ -rm -f -- $(BIN)
+
+.SUFFIXES:
+.SUFFIXES: .o .c
+
+.PHONY: all clean
diff --git a/util/README b/util/README
new file mode 100644
index 0000000..b4ce9b4
--- /dev/null
+++ b/util/README
@@ -0,0 +1,80 @@
+tmpmount mountpoint command [argument] ...
+
+ Mounts a tmpfs at `mountpoint` and execs into
+ `command [argument] ...`. The tmpfs is private
+ to the process and it's children.
+
+
+repodiff directory-1 file-list-1 directory-2 file-list-2
+
+ Checks for differences betweens the files listed in
+ file-list-1, which are relative to directory-1, and
+ files listed in file-list-2, which are relative to
+ directory-2. Will only check for things supported
+ by git: file listing, file names, file content,
+ symlink target, and executable by owner. (Files
+ that are not regular files, symbolic links or
+ directories will cause failure.)
+
+ file-list-1 and file-list-2 use <nul> termination
+ rather then <newline> termination, meaning that
+ if the file listed is created with file(1),
+ `-print0` should have been used, or if with
+ git-ls-files(1), `-z` should have been used.
+
+ If a difference is found 1 is returned, 0 otherwise.
+ Exit value 2 is used to signal runtime error.
+
+
+validate-tarball tarball git-dir work-dir
+
+ Validates the contents a tarball againts a known
+ good directory (git-dir). An empty directory
+ (work-dir) shall be provided as a space for
+ temporary files.
+
+
+order-checksums
+
+ Sorts standard input, removed duplicates checksums
+ and output the checksums lines primarily ordered
+ by the hash algorithms in a particular order and
+ secondarily by the checksums alphabetically sorted.
+
+
+get-checksums tarball
+
+ Calculates the checksums of a tarball.
+
+ A special argument can be provided for printing
+ the order the hash algorithms are output in
+
+
+get-and-check tarball-url reference-dir work-dir
+
+ Downloads tarball-url and uses ./validate-tarball
+ to validate its contents against a known good
+ directory (reference-dir). An empty directory
+ (work-dir) shall be provided as a space for
+ temporary files. If the content of the tarball
+ matches git-tracked files in reference-dir,
+ ./get-checksums is used to output the tarball's
+ checksums.
+
+
+gen-checksums tarball git-dir < version-info-file > checksum-listing
+
+ This brings all of the above together. The tarball,
+ which is assumed to be the tarball for the static release
+ is validates against a known good directory (git-dir),
+ for which the tarball is supposted to be generated from,
+ but this check validates that git itself is not manipulating
+ the tarballs. Then it reads stdin for URLs in lines that
+ begin with "Tarball: " (however, the URL for the static
+ release is discarded as it is assumed to not exist yet
+ but be the same file as input as the argument). These
+ URLs are used to download release tarballs from all
+ mirrors; they are also validates against the known good
+ directory. The checksums for all tarballs, both the
+ one provided in the command line and those downloaded,
+ are output to stdout.
diff --git a/util/config.mk b/util/config.mk
new file mode 100644
index 0000000..d78941c
--- /dev/null
+++ b/util/config.mk
@@ -0,0 +1,10 @@
+PREFIX = /usr
+MANPREFIX = $(PREFIX)/share/man
+
+CC = c99
+
+CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_GNU_SOURCE
+CFLAGS =
+LDFLAGS = -lsimple
+
+ASROOT = asroot
diff --git a/util/gen-checksums b/util/gen-checksums
new file mode 100755
index 0000000..bb1f981
--- /dev/null
+++ b/util/gen-checksums
@@ -0,0 +1,23 @@
+#!/bin/sh
+# See LICENSE file for copyright and license details.
+
+set -e
+
+if test ! $# = 2 || test -t 0 || test -t 1; then
+ printf 'usage: %s tarball git-dir < version-info-file > checksum-listing\n' "$0" >&2
+fi
+
+tarball="$1"
+gitdir="$2"
+
+utildir="$(dirname -- "$0")"
+(cd -- "${utildir}" && (make >/dev/null 2>/dev/null || make))
+
+"${utildir}"/tmpmount /var/empty "${utildir}"/validate-tarball "${tarball}" "${gitdir}" /var/empty
+
+grep '^Tarball: ' | cut -d ' ' -f 2 | grep -v '^https://maandree.se/static/' | (
+ "${utildir}"/get-checksums "${tarball}"
+ while read -r url; do
+ "${utildir}"/tmpmount /var/empty "${utildir}"/get-and-check "${url}" "${gitdir}" /var/empty
+ done
+) | "${utildir}"/order-checksums
diff --git a/util/get-and-check b/util/get-and-check
new file mode 100755
index 0000000..da9f1aa
--- /dev/null
+++ b/util/get-and-check
@@ -0,0 +1,17 @@
+#!/bin/sh
+# See LICENSE file for copyright and license details.
+
+set -e
+
+if ! test $# = 3; then
+ printf 'usage: %s tarball-url reference-dir work-dir\n' "$0" >&2
+ exit 1
+fi
+
+utildir="$(dirname -- "$0")"
+tarball="$3/download-$$.tar.gz"
+
+curl -sL -- "$1" > "${tarball}"
+"${utildir}"/validate-tarball "${tarball}" "$2" "$3"
+"${utildir}"/get-checksums "${tarball}"
+rm -f -- "${tarball}"
diff --git a/util/get-checksums b/util/get-checksums
new file mode 100755
index 0000000..9d1b86a
--- /dev/null
+++ b/util/get-checksums
@@ -0,0 +1,42 @@
+#!/bin/sh
+# See LICENSE file for copyright and license details.
+
+set -e
+
+gensums () {
+ gensum sha224sum SHA224
+ gensum sha256sum SHA256
+ gensum sha384sum SHA384
+ gensum sha512sum SHA512
+ gensum sha512-224sum SHA512/224
+ gensum sha512-256sum SHA512/256
+ gensum sha3-224sum SHA3-224
+ gensum sha3-256sum SHA3-256
+ gensum sha3-384sum SHA3-384
+ gensum sha3-512sum SHA3-512
+ gensum b2sum BLAKE2b
+}
+
+if test $# = 1 && test "$1" = '-- output checksum order --'; then
+ gensum () { printf '%s\n' "$2"; }
+ gensums
+ exit 0
+fi
+
+if ! test $# = 1; then
+ printf 'usage: %s tarball\n' "$0" >&2
+ exit 1
+fi
+
+tarball="$1"
+
+gensum () {
+ tool="$1"
+ name="$2"
+
+ sum="$(${tool} < "${tarball}" | cut -d ' ' -f 1)"
+ test -n "${sum}"
+ printf '%s checksum: %s\n' "${name}" "${sum}"
+}
+
+gensums
diff --git a/util/order-checksums b/util/order-checksums
new file mode 100755
index 0000000..00abca9
--- /dev/null
+++ b/util/order-checksums
@@ -0,0 +1,16 @@
+#!/bin/sh
+# See LICENSE file for copyright and license details.
+
+set -e
+
+if ! test $# = 0; then
+ printf 'usage: %s < unordered-checksum-table > ordered-checksum-table \n' "$0" >&2
+ exit 1
+fi
+
+utildir="$(dirname -- "$0")"
+
+text="$(sort -u)"
+"${utildir}"/get-checksums '-- output checksum order --' | while read sum; do
+ printf '%s\n' "${text}" | grep "^${sum} checksum:"
+done
diff --git a/util/repodiff.c b/util/repodiff.c
new file mode 100644
index 0000000..b27aef6
--- /dev/null
+++ b/util/repodiff.c
@@ -0,0 +1,316 @@
+/* See LICENSE file for copyright and license details. */
+#include <libsimple.h>
+#include <libsimple-arg.h>
+
+NUSAGE(2, "directory-1 file-list-1 directory-2 file-list-2");
+
+
+LIBSIMPLE_PURE
+static int
+parse_fd(const char *s, int dash_fd)
+{
+ int fd, d;
+
+ if (streq(s, "/dev/stdin"))
+ return STDIN_FILENO;
+ if (streq(s, "/dev/stdout"))
+ return STDOUT_FILENO;
+ if (streq(s, "/dev/stderr"))
+ return STDERR_FILENO;
+ if (streq(s, "-"))
+ return dash_fd;
+
+ if (strstarts(s, "/proc/self/fd/"))
+ s = &s[sizeof("/proc/self/fd/") - 1u];
+ else if (strstarts(s, "/dev/fd/"))
+ s = &s[sizeof("/dev/fd/") - 1u];
+ else
+ return -1;
+
+ if ('0' > *s || *s > '9')
+ return -1;
+ fd = (int)(*s++ - '0');
+ while ('0' <= *s && *s <= '9') {
+ d = (int)(*s - '0');
+ if (fd > (INT_MAX - d) / 10)
+ return -1;
+ fd = fd * 10 + d;
+ }
+
+ return *s ? -1 : fd;
+}
+
+
+static char **
+get_file_list(const char *path, size_t *n_out)
+{
+ char *buf = NULL;
+ size_t bufsize = 0u;
+ size_t len = 0u;
+ size_t off, i, j, d;
+ int fd, do_close;
+ ssize_t r;
+ char **list = NULL;
+ size_t listsize = 0u;
+
+ *n_out = 0u;
+
+ do_close = 0;
+ fd = parse_fd(path, STDIN_FILENO);
+ if (fd < 0) {
+ do_close = 1;
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ eprintf("open %s O_RDONLY:", path);
+ }
+
+ for (;;) {
+ if (len == bufsize)
+ buf = erealloc(buf, bufsize += (size_t)8 << 10);
+ r = read(fd, &buf[len], bufsize - len);
+ if (r <= 0) {
+ if (!r)
+ break;
+ if (errno == EINTR)
+ continue;
+ eprintf("read %s:", path);
+ }
+ len += (size_t)r;
+ }
+
+ if (do_close)
+ close(fd);
+
+ buf = erealloc(buf, len + 1u);
+ buf[len++] = '\0';
+
+ off = 0u;
+ for (i = 0u; i < len; i++) {
+ if (buf[i])
+ continue;
+ if (i == off)
+ goto next;
+ while (buf[off] == '/')
+ off += 1u;
+ if (buf[off + 0u] == '.' && buf[off + 1u] == '/')
+ off += 2u;
+ if (!buf[off])
+ goto next;
+ if (buf[off + 0u] == '.' && !buf[off + 1u])
+ goto next;
+
+ d = buf[i - 1u] == '/' ? 1u : 0u;
+ buf[i - d] = '\0';
+ if (!buf[off])
+ goto next;
+
+ if (*n_out == listsize)
+ list = ereallocarray(list, listsize += 128u, sizeof(*list));
+ list[(*n_out)++] = estrdup(&buf[off]);
+ for (j = i - 1u - d; j > off; j--) {
+ if (buf[j] != '/')
+ continue;
+ buf[j] = '\0';
+ if (*n_out == listsize)
+ list = ereallocarray(list, listsize += 128u, sizeof(*list));
+ list[(*n_out)++] = estrdup(&buf[off]);
+ }
+
+ next:
+ off = i + 1u;
+ }
+
+ free(buf);
+ return list;
+}
+
+
+static size_t
+uniq(char **list, size_t n)
+{
+ size_t r, w = 1u;
+ if (!n)
+ return 0u;
+ for (r = 1u; r < n; r++) {
+ if (strcmp(list[r], list[w - 1u]))
+ list[w++] = list[r];
+ else
+ free(list[r]);
+ }
+ return w;
+}
+
+
+static int
+files_equal(int fd1, const char *dir1, int fd2, const char *dir2, const char *path)
+{
+ static char buf1[8192];
+ static char buf2[sizeof(buf1)];
+ size_t len1, len2;
+ ssize_t r;
+
+ do {
+ len1 = 0u;
+ while (len1 < sizeof(buf1)) {
+ r = read(fd1, &buf1[len1], sizeof(buf1) - len1);
+ if (r <= 0) {
+ if (!r)
+ break;
+ if (errno == EINTR)
+ continue;
+ eprintf("read %s/%s:", dir1, path);
+ }
+ len1 += (size_t)r;
+ }
+
+ len2 = 0u;
+ while (len2 < sizeof(buf2)) {
+ r = read(fd2, &buf2[len2], sizeof(buf2));
+ if (r <= 0) {
+ if (!r)
+ break;
+ if (errno == EINTR)
+ continue;
+ eprintf("read %s/%s:", dir2, path);
+ }
+ len2 += (size_t)r;
+ }
+
+ if (len1 != len2 || memcmp(buf1, buf2, len1))
+ return 0;
+ } while (len1 && len2);
+
+ return !len1 && !len2;
+}
+
+
+int
+main(int argc, char *argv[])
+{
+ struct stat st1, st2;
+ const char *dir1, *dir2;
+ char **files1, **files2;
+ size_t nfiles1, nfiles2, i, j;
+ char *target1, *target2;
+ int dirfd1, dirfd2, cmp;
+ int fd1, fd2;
+ int ret = 0;
+
+ libsimple_default_failure_exit = 2;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND;
+
+ if (argc != 4)
+ usage();
+
+ dir1 = argv[0];
+ dir2 = argv[2];
+ dirfd1 = open(dir1, O_PATH);
+ if (dirfd1 < 0)
+ eprintf("open %s O_PATH", dir1);
+ dirfd2 = open(dir2, O_PATH);
+ if (dirfd2 < 0)
+ eprintf("open %s O_PATH", dir2);
+
+ files1 = get_file_list(argv[1], &nfiles1);
+ files2 = get_file_list(argv[3], &nfiles2);
+ libsimple_qsort_str((void *)files1, nfiles1);
+ libsimple_qsort_str((void *)files2, nfiles2);
+ nfiles1 = uniq(files1, nfiles1);
+ nfiles2 = uniq(files2, nfiles2);
+
+ i = j = 0u;
+ while (i < nfiles1 || j < nfiles2) {
+ if (i == nfiles1) {
+ exclusive_to_dir2:
+ weprintf("%s exists only in %s\n", files2[j], dir2);
+ j++;
+ ret = 1;
+ continue;
+ }
+ if (j == nfiles2) {
+ exclusive_to_dir1:
+ weprintf("%s exists only in %s\n", files1[i], dir1);
+ i++;
+ ret = 1;
+ continue;
+ }
+
+ cmp = strcmp(files1[i], files2[j]);
+ if (cmp < 0)
+ goto exclusive_to_dir1;
+ if (cmp > 1)
+ goto exclusive_to_dir2;
+
+ if (fstatat(dirfd1, files1[i], &st1, AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT))
+ eprintf("fstatat %s %s AT_SYMLINK_NOFOLLOW|AT_NO_AUTOMOUNT:", dir1, files1[i]);
+ if (fstatat(dirfd2, files2[j], &st2, AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT))
+ eprintf("fstatat %s %s AT_SYMLINK_NOFOLLOW|AT_NO_AUTOMOUNT:", dir2, files2[j]);
+
+ if ((st1.st_mode & S_IFMT) != (st2.st_mode & S_IFMT)) {
+ weprintf("%s has different file types in %s and %s\n", files1[i], dir1, dir2);
+ ret = 1;
+ goto next;
+ }
+
+ switch (st1.st_mode & S_IFMT) {
+ case S_IFREG:
+ if ((st1.st_mode ^ st2.st_mode) & S_IXUSR) {
+ weprintf("%s is executable in one of but in not both of %s and %s\n", files1[i], dir1, dir2);
+ ret = 1;
+ }
+ if (st1.st_size != st2.st_size) {
+ weprintf("%s has different sizes in %s and %s\n", files1[i], dir1, dir2);
+ ret = 1;
+ goto next;
+ }
+ fd1 = openat(dirfd1, files1[i], O_RDONLY | O_NOFOLLOW);
+ if (fd1 < 0)
+ eprintf("openat %s %s O_RDONLY|O_NOFOLLOW:", dir1, files1[i]);
+ fd2 = openat(dirfd2, files2[j], O_RDONLY | O_NOFOLLOW);
+ if (fd2 < 0)
+ eprintf("openat %s %s O_RDONLY|O_NOFOLLOW:", dir2, files2[j]);
+ if (!files_equal(fd1, dir1, fd2, dir2, files1[i])) {
+ weprintf("%s has different contents in %s and %s\n", files1[i], dir1, dir2);
+ ret = 1;
+ }
+ close(fd1);
+ close(fd2);
+ break;
+
+ case S_IFLNK:
+ target1 = libsimple_ereadlinkat(dirfd1, files1[i]);
+ target2 = libsimple_ereadlinkat(dirfd2, files2[j]);
+ if (strcmp(target1, target2)) {
+ weprintf("%s has different targets in %s and %s\n", files1[i], dir1, dir2);
+ ret = 1;
+ }
+ free(target1);
+ free(target2);
+ break;
+
+ case S_IFDIR:
+ break;
+
+ default:
+ eprintf("%s has unsupported file type\n", files1[i]);
+ }
+
+ next:
+ i++;
+ j++;
+ }
+
+ close(dirfd1);
+ close(dirfd2);
+ while (nfiles1--)
+ free(files1[nfiles1]);
+ while (nfiles2--)
+ free(files2[nfiles2]);
+ free(files1);
+ free(files2);
+ return ret;
+}
diff --git a/util/tmpmount.c b/util/tmpmount.c
new file mode 100644
index 0000000..75e225d
--- /dev/null
+++ b/util/tmpmount.c
@@ -0,0 +1,44 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/mount.h>
+#include <sched.h>
+#include <libsimple.h>
+#include <libsimple-arg.h>
+
+NUSAGE(125, "mountpoint utility [argument] ...");
+
+
+int
+main(int argc, char *argv[])
+{
+ const char *mountpoint;
+
+ libsimple_default_failure_exit = 125;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND;
+
+ if (argc < 2)
+ usage();
+
+ mountpoint = *argv++;
+ argc--;
+
+ if (unshare(CLONE_NEWNS))
+ eprintf("unshare CLONE_NEWNS:");
+ if (mount("none", "/", NULL, MS_REC | MS_SLAVE, NULL))
+ eprintf("mount none / NULL MS_REC|MS_SLAVE NULL:");
+
+ if (mount("tmpfs", mountpoint, "tmpfs", 0, NULL))
+ eprintf("mount tmpfs %s tmpfs 0 NULL:", mountpoint);
+
+ if (setegid(getgid()))
+ eprintf("setegid <real group>:");
+ if (seteuid(getuid()))
+ eprintf("seteuid <real user>:");
+
+ execvp(argv[0], argv);
+ enprintf(errno == ENOENT ? 127 : 126, "execvp %s:", argv[0]);
+ return 0;
+}
diff --git a/util/validate-tarball b/util/validate-tarball
new file mode 100755
index 0000000..14aff6d
--- /dev/null
+++ b/util/validate-tarball
@@ -0,0 +1,27 @@
+#!/bin/sh
+# See LICENSE file for copyright and license details.
+
+set -e
+
+if ! test $# = 3; then
+ printf 'usage: %s tarball git-dir work-dir\n' "$0" >&2
+ exit 1
+fi
+
+utildir="$(dirname -- "$0")"
+
+dir="$3/$$.tmpdir"
+rm -rf -- "${dir}"
+mkdir -- "${dir}"
+
+gunzip < "$1" | (cd -- "${dir}" && tar -x)
+
+(cd -- "$2" && git ls-files -z) > "$3/listing-1"
+(cd -- "${dir}"/*/ && find -print0) > "$3/listing-2"
+
+set +e
+"${utildir}"/repodiff "$2" "$3/listing-1" "${dir}"/* "$3/listing-2"
+ret=$?
+
+rm -rf -- "${dir}"
+exit $ret