From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from lists.gentoo.org (pigeon.gentoo.org [208.92.234.80]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by finch.gentoo.org (Postfix) with ESMTPS id 6B40E138335 for ; Tue, 21 May 2019 14:12:04 +0000 (UTC) Received: from pigeon.gentoo.org (localhost [127.0.0.1]) by pigeon.gentoo.org (Postfix) with SMTP id 5C47DE087E; Tue, 21 May 2019 14:12:03 +0000 (UTC) Received: from smtp.gentoo.org (dev.gentoo.org [IPv6:2001:470:ea4a:1:5054:ff:fec7:86e4]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by pigeon.gentoo.org (Postfix) with ESMTPS id 2B021E087E for ; Tue, 21 May 2019 14:12:02 +0000 (UTC) Received: from oystercatcher.gentoo.org (oystercatcher.gentoo.org [148.251.78.52]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.gentoo.org (Postfix) with ESMTPS id 5FFE5344D2B for ; Tue, 21 May 2019 14:12:01 +0000 (UTC) Received: from localhost.localdomain (localhost [IPv6:::1]) by oystercatcher.gentoo.org (Postfix) with ESMTP id D1D505F2 for ; Tue, 21 May 2019 14:11:59 +0000 (UTC) From: "Fabian Groffen" To: gentoo-commits@lists.gentoo.org Content-Transfer-Encoding: 8bit Content-type: text/plain; charset=UTF-8 Reply-To: gentoo-dev@lists.gentoo.org, "Fabian Groffen" Message-ID: <1558374828.d16f69568285d76dfa8a367f2458bc16f9e049e4.grobian@gentoo> Subject: [gentoo-commits] proj/portage-utils:master commit in: man/, man/include/, libq/, / X-VCS-Repository: proj/portage-utils X-VCS-Files: Makefile.am applets.h configure.ac libq/Makefile.am libq/hash.c libq/hash.h man/include/qmanifest.desc man/qmanifest.1 qmanifest.c X-VCS-Directories: libq/ man/ man/include/ / X-VCS-Committer: grobian X-VCS-Committer-Name: Fabian Groffen X-VCS-Revision: d16f69568285d76dfa8a367f2458bc16f9e049e4 X-VCS-Branch: master Date: Tue, 21 May 2019 14:11:59 +0000 (UTC) Precedence: bulk List-Post: List-Help: List-Unsubscribe: List-Subscribe: List-Id: Gentoo Linux mail X-BeenThere: gentoo-commits@lists.gentoo.org X-Auto-Response-Suppress: DR, RN, NRN, OOF, AutoReply X-Archives-Salt: ee027d24-ef5f-470b-8c26-79da37aae0eb X-Archives-Hash: 519314438aea1bf919fcc549870bf888 commit: d16f69568285d76dfa8a367f2458bc16f9e049e4 Author: Fabian Groffen gentoo org> AuthorDate: Mon May 20 17:53:48 2019 +0000 Commit: Fabian Groffen gentoo org> CommitDate: Mon May 20 17:53:48 2019 +0000 URL: https://gitweb.gentoo.org/proj/portage-utils.git/commit/?id=d16f6956 qmanifest: new applet to verify and generate thick Manifests This incorporates https://github.com/grobian/hashgen into portage-utils as qmanifest. Signed-off-by: Fabian Groffen gentoo.org> Makefile.am | 10 + applets.h | 24 +- configure.ac | 73 ++ libq/Makefile.am | 2 + libq/hash.c | 196 +++++ libq/hash.h | 26 + man/include/qmanifest.desc | 29 + man/qmanifest.1 | 94 +++ qmanifest.c | 1695 ++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 2140 insertions(+), 9 deletions(-) diff --git a/Makefile.am b/Makefile.am index b36173c..f28a073 100644 --- a/Makefile.am +++ b/Makefile.am @@ -17,6 +17,7 @@ APPLETS = \ qkeyword \ qlist \ qlop \ + qmanifest \ qmerge \ qpkg \ qsearch \ @@ -37,6 +38,7 @@ dist_man_MANS = \ man/qkeyword.1 \ man/qlist.1 \ man/qlop.1 \ + man/qmanifest.1 \ man/qmerge.1 \ man/qpkg.1 \ man/qsearch.1 \ @@ -59,6 +61,7 @@ q_SOURCES = \ qkeyword.c \ qlist.c \ qlop.c \ + qmanifest.c \ qmerge.c \ qpkg.c \ qsearch.c \ @@ -69,14 +72,21 @@ q_SOURCES = \ qxpak.c \ $(NULL) q_CPPFLAGS = \ + $(OPENMP_CFLAGS) \ + $(GPGME_CFLAGS) \ -I$(top_srcdir)/libq \ -I$(top_builddir)/autotools/gnulib \ -I$(top_srcdir)/autotools/gnulib \ $(NULL) q_LDADD = \ + $(OPENMP_CFLAGS) \ $(top_builddir)/libq/libq.la \ $(top_builddir)/autotools/gnulib/libgnu.a \ -liniparser \ + $(LIBSSL) \ + $(LIBBL2) \ + $(LIBZ) \ + $(GPGME_LIBS) \ $(LIB_CLOCK_GETTIME) \ $(LIB_EACCESS) \ $(NULL) diff --git a/applets.h b/applets.h index fbb4e8c..902e664 100644 --- a/applets.h +++ b/applets.h @@ -38,23 +38,24 @@ typedef int (*APPLET)(int, char **); #define DECLARE_APPLET(applet) \ extern int applet##_main(int, char **) __attribute__((weak)); DECLARE_APPLET(q) +DECLARE_APPLET(qatom) DECLARE_APPLET(qcheck) DECLARE_APPLET(qdepends) DECLARE_APPLET(qfile) +/*DECLARE_APPLET(qglsa) disable */ +DECLARE_APPLET(qgrep) +DECLARE_APPLET(qkeyword) DECLARE_APPLET(qlist) DECLARE_APPLET(qlop) +DECLARE_APPLET(qmanifest) +DECLARE_APPLET(qmerge) +DECLARE_APPLET(qpkg) DECLARE_APPLET(qsearch) DECLARE_APPLET(qsize) DECLARE_APPLET(qtbz2) +DECLARE_APPLET(qtegrity) DECLARE_APPLET(quse) DECLARE_APPLET(qxpak) -DECLARE_APPLET(qpkg) -DECLARE_APPLET(qgrep) -DECLARE_APPLET(qatom) -DECLARE_APPLET(qmerge) -DECLARE_APPLET(qkeyword) -/*DECLARE_APPLET(qglsa) disable */ -DECLARE_APPLET(qtegrity) #undef DECLARE_APPLET static const struct applet_t { @@ -66,7 +67,6 @@ static const struct applet_t { /* q must always be the first applet */ {"q", q_main, " ", "virtual applet"}, {"qatom", qatom_main, "", "split atom strings"}, - {"qkeyword", qkeyword_main, " ", "list packages based on keywords"}, {"qcheck", qcheck_main, "", "verify integrity of installed packages"}, {"qdepends", qdepends_main, "", "show dependency info"}, {"qfile", qfile_main, "", "list all pkgs owning files"}, @@ -74,16 +74,18 @@ static const struct applet_t { {"qglsa", qglsa_main, " ", "check GLSAs against system"}, */ {"qgrep", qgrep_main, " [pkg ...]", "grep in ebuilds"}, + {"qkeyword", qkeyword_main, " ", "list packages based on keywords"}, {"qlist", qlist_main, "", "list files owned by pkgname"}, {"qlop", qlop_main, "", "emerge log analyzer"}, + {"qmanifest", qmanifest_main, "", "verify or generate thick Manifest files"}, {"qmerge", qmerge_main, "", "fetch and merge binary package"}, {"qpkg", qpkg_main, "", "manipulate Gentoo binpkgs"}, {"qsearch", qsearch_main, "", "search pkgname/desc"}, {"qsize", qsize_main, "", "calculate size usage"}, {"qtbz2", qtbz2_main, "", "manipulate tbz2 packages"}, + {"qtegrity", qtegrity_main, "", "verify files with IMA"}, {"quse", quse_main, "", "find pkgs using useflags"}, {"qxpak", qxpak_main, "", "manipulate xpak archives"}, - {"qtegrity", qtegrity_main, "", "verify files with IMA"}, /* aliases for equery compatibility */ {"belongs", qfile_main, NULL, NULL}, @@ -107,6 +109,10 @@ static const struct applet_t { /* alias for qtegrity */ {"integrity", qtegrity_main, NULL, NULL}, + /* old hashgen */ + {"hashgen", qmanifest_main, NULL, NULL}, + {"hashverify",qmanifest_main, NULL, NULL}, + {NULL, NULL, NULL, NULL} }; diff --git a/configure.ac b/configure.ac index 43a4609..2a39df8 100644 --- a/configure.ac +++ b/configure.ac @@ -8,6 +8,7 @@ AC_CONFIG_MACRO_DIR([autotools/m4]) AC_PROG_CC_C99 AM_PROG_CC_C_O +AC_OPENMP AC_USE_SYSTEM_EXTENSIONS gl_EARLY @@ -31,6 +32,78 @@ AC_DEFINE_UNQUOTED([CONFIG_EPREFIX], ["$with_eprefix"], [Gentoo Prefix offset path]) AC_SUBST([CONFIG_EPREFIX], ["$with_eprefix"]) +AC_ARG_ENABLE([qmanifest], [AS_HELP_STRING([--enable-qmanifest], + [support qmanifest applet])], + [], [enable_qmanifest=auto]) +LIBSSL= +LIBBL2= +LIBZ= +HASGPGME= +AS_IF([test "x$enable_qmanifest" != xno], + [AC_CHECK_HEADERS([openssl/err.h \ + openssl/ssl.h], [], [LIBSSL=_missing_header]) + AC_CHECK_LIB([ssl${LIBSSL}], [SSL_connect], + [LIBSSL="-lssl" + AC_DEFINE([HAVE_SSL], [1], [Define if you have ssl]) + AC_CHECK_LIB([crypto], + [ERR_reason_error_string], + [LIBSSL="${LIBSSL} -lcrypto"], + []) + AC_SUBST([LIBSSL], ["${LIBSSL}"]) + ], + [if test "x$enable_qmanifest" != xauto; then + AC_MSG_FAILURE( + [--enable-qmanifest was given, but test for ssl failed]) + fi + LIBSSL= + ]) + AC_CHECK_HEADERS([blake2.h], [], [LIBBL2=_missing_header]) + AC_CHECK_LIB([b2${LIBBL2}], [blake2b_update], + [LIBBL2="-lb2" + AC_DEFINE([HAVE_BLAKE2B], [1], + [Define if you have blake2b]) + AC_SUBST([LIBBL2], ["${LIBBL2}"]) + ], + [if test "x$enable_qmanifest" != xauto; then + AC_MSG_FAILURE( + [--enable-qmanifest was given, but test for blake2b failed]) + fi + LIBBL2= + ]) + AC_CHECK_HEADERS([zlib.h], [], [LIBZ=_missing_header]) + AC_CHECK_LIB([z${LIBZ}], [gzopen], + [LIBZ="-lz" + AC_DEFINE([HAVE_LIBZ], [1], + [Define if you have zlib]) + AC_SUBST([LIBZ], ["${LIBZ}"]) + ], + [if test "x$enable_qmanifest" != xauto; then + AC_MSG_FAILURE( + [--enable-qmanifest was given, but test for libz failed]) + fi + LIBZ= + ]) + AM_PATH_GPGME([], [HASGPGME=yes]) + AC_MSG_CHECKING([whether to enable qmanifest]) + case "x${LIBSSL}${LIBBL2}${LIBZ}-${HASGPGME}" in + "x-lssl"*"-lb2-lz-yes") + AC_MSG_RESULT([yes]) + ;; + *) + enable_qmanifest=no + AC_MSG_RESULT([no: missing dependencies]) + ;; + esac + if test "x$enable_qmanifest" != xno ; then + AC_DEFINE([ENABLE_QMANIFEST], [1], + [Define if qmanifest should be compiled]) + fi + ], + [ + AC_MSG_CHECKING([whether to enable qmanifest]) + AC_MSG_RESULT([no: disabled by configure argument]) + ]) + AX_CFLAGS_WARN_ALL AC_DEFUN([PT_CHECK_CFLAG],[AX_CHECK_COMPILER_FLAGS([$1],[CFLAGS="$CFLAGS $1"])]) m4_foreach_w([flag], [ diff --git a/libq/Makefile.am b/libq/Makefile.am index 62ffb83..9cb19a2 100644 --- a/libq/Makefile.am +++ b/libq/Makefile.am @@ -8,6 +8,7 @@ QFILES = \ copy_file.c copy_file.h \ dep.c dep.h \ eat_file.c eat_file.h \ + hash.c hash.h \ hash_fd.c hash_fd.h \ human_readable.c human_readable.h \ i18n.h \ @@ -31,5 +32,6 @@ QFILES = \ noinst_LTLIBRARIES = libq.la libq_la_SOURCES = $(QFILES) libq_la_CPPFLAGS = \ + $(OPENMP_CFLAGS) \ -I$(top_builddir)/autotools/gnulib \ -I$(top_srcdir)/autotools/gnulib diff --git a/libq/hash.c b/libq/hash.c new file mode 100644 index 0000000..5b13d49 --- /dev/null +++ b/libq/hash.c @@ -0,0 +1,196 @@ +/* + * Copyright 2018-2019 Gentoo Foundation + * Distributed under the terms of the GNU General Public License v2 + * + * Copyright 2018- Fabian Groffen - + * + * The contents of this file was taken from: + * https://github.com/grobian/hashgen + * which was discontinued at the time the sources were incorporated into + * portage-utils as qmanifest. + */ + +#include "main.h" + +#ifdef HAVE_SSL +#include +#include +#endif +#ifdef HAVE_BLAKE2B +#include +#endif + +#include "hash.h" + +static int hashes = HASH_DEFAULT; + +void +hash_hex(char *out, const unsigned char *buf, const int length) +{ + switch (length) { + /* SHA256_DIGEST_LENGTH */ + case 32: + snprintf(out, 64 + 1, + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x", + buf[ 0], buf[ 1], buf[ 2], buf[ 3], buf[ 4], + buf[ 5], buf[ 6], buf[ 7], buf[ 8], buf[ 9], + buf[10], buf[11], buf[12], buf[13], buf[14], + buf[15], buf[16], buf[17], buf[18], buf[19], + buf[20], buf[21], buf[22], buf[23], buf[24], + buf[25], buf[26], buf[27], buf[28], buf[29], + buf[30], buf[31] + ); + break; + /* SHA512_DIGEST_LENGTH, WHIRLPOOL_DIGEST_LENGTH, BLAKE2B_OUTBYTES */ + case 64: + snprintf(out, 128 + 1, + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x", + buf[ 0], buf[ 1], buf[ 2], buf[ 3], buf[ 4], + buf[ 5], buf[ 6], buf[ 7], buf[ 8], buf[ 9], + buf[10], buf[11], buf[12], buf[13], buf[14], + buf[15], buf[16], buf[17], buf[18], buf[19], + buf[20], buf[21], buf[22], buf[23], buf[24], + buf[25], buf[26], buf[27], buf[28], buf[29], + buf[30], buf[31], buf[32], buf[33], buf[34], + buf[35], buf[36], buf[37], buf[38], buf[39], + buf[40], buf[41], buf[42], buf[43], buf[44], + buf[45], buf[46], buf[47], buf[48], buf[49], + buf[50], buf[51], buf[52], buf[53], buf[54], + buf[55], buf[56], buf[57], buf[58], buf[59], + buf[60], buf[61], buf[62], buf[63] + ); + break; + /* fallback case, should never be necessary */ + default: + { + int i; + for (i = 0; i < length; i++) { + snprintf(&out[i * 2], 3, "%02x", buf[i]); + } + } + break; + } +} + +/** + * Computes the hashes for file fname and writes the hex-representation + * for those hashes into the address space pointed to by the return + * pointers for these hashes. The caller should ensure enough space is + * available. Only those hashes which are in the global hashes variable + * are computed, the address space pointed to for non-used hashes are + * left untouched, e.g. they can be NULL. The number of bytes read from + * the file pointed to by fname is returned in the flen argument. + */ +void +hash_compute_file( + const char *fname, + char *sha256, + char *sha512, + char *whrlpl, + char *blak2b, + size_t *flen) +{ + FILE *f; + char data[8192]; + size_t len; +#ifdef HAVE_SSL + SHA256_CTX s256; + SHA512_CTX s512; + WHIRLPOOL_CTX whrl; +#endif +#ifdef HAVE_BLAKE2B + blake2b_state bl2b; +#endif + + if ((f = fopen(fname, "r")) == NULL) + return; + +#ifdef HAVE_SSL + SHA256_Init(&s256); + SHA512_Init(&s512); + WHIRLPOOL_Init(&whrl); +#endif +#ifdef HAVE_BLAKE2B + blake2b_init(&bl2b, BLAKE2B_OUTBYTES); +#endif + + while ((len = fread(data, 1, sizeof(data), f)) > 0) { + *flen += len; +#pragma omp parallel sections + { +#ifdef HAVE_SSL +#pragma omp section + { + if (hashes & HASH_SHA256) + SHA256_Update(&s256, data, len); + } +#pragma omp section + { + if (hashes & HASH_SHA512) + SHA512_Update(&s512, data, len); + } +#pragma omp section + { + if (hashes & HASH_WHIRLPOOL) + WHIRLPOOL_Update(&whrl, data, len); + } +#endif +#ifdef HAVE_BLAKE2B +#pragma omp section + { + if (hashes & HASH_BLAKE2B) + blake2b_update(&bl2b, (unsigned char *)data, len); + } +#endif + } + } + fclose(f); + +#pragma omp parallel sections + { +#ifdef HAVE_SSL + { + if (hashes & HASH_SHA256) { + unsigned char sha256buf[SHA256_DIGEST_LENGTH]; + SHA256_Final(sha256buf, &s256); + hash_hex(sha256, sha256buf, SHA256_DIGEST_LENGTH); + } + } +#pragma omp section + { + if (hashes & HASH_SHA512) { + unsigned char sha512buf[SHA512_DIGEST_LENGTH]; + SHA512_Final(sha512buf, &s512); + hash_hex(sha512, sha512buf, SHA512_DIGEST_LENGTH); + } + } +#pragma omp section + { + if (hashes & HASH_WHIRLPOOL) { + unsigned char whrlplbuf[WHIRLPOOL_DIGEST_LENGTH]; + WHIRLPOOL_Final(whrlplbuf, &whrl); + hash_hex(whrlpl, whrlplbuf, WHIRLPOOL_DIGEST_LENGTH); + } + } +#endif +#ifdef HAVE_BLAKE2B +#pragma omp section + { + if (hashes & HASH_BLAKE2B) { + unsigned char blak2bbuf[BLAKE2B_OUTBYTES]; + blake2b_final(&bl2b, blak2bbuf, BLAKE2B_OUTBYTES); + hash_hex(blak2b, blak2bbuf, BLAKE2B_OUTBYTES); + } + } +#endif + } +} diff --git a/libq/hash.h b/libq/hash.h new file mode 100644 index 0000000..157c454 --- /dev/null +++ b/libq/hash.h @@ -0,0 +1,26 @@ +/* + * Copyright 2018-2019 Gentoo Foundation + * Distributed under the terms of the GNU General Public License v2 + * + * Copyright 2018- Fabian Groffen - + */ + +#ifndef _HASH_H +#define _HASH_H 1 + +enum hash_impls { + HASH_SHA256 = 1<<0, + HASH_SHA512 = 1<<1, + HASH_WHIRLPOOL = 1<<2, + HASH_BLAKE2B = 1<<3 +}; + +/* default changed from sha256, sha512, whirlpool + * to blake2b, sha512 on 2017-11-21 */ +#define HASH_DEFAULT (HASH_BLAKE2B | HASH_SHA512); + +void hash_hex(char *out, const unsigned char *buf, const int length); +void hash_compute_file(const char *fname, char *sha256, char *sha512, + char *whrlpl, char *blak2b, size_t *flen); + +#endif diff --git a/man/include/qmanifest.desc b/man/include/qmanifest.desc new file mode 100644 index 0000000..b873419 --- /dev/null +++ b/man/include/qmanifest.desc @@ -0,0 +1,29 @@ +\fIqmanifest\fR allows to verify or generate thick signed Manifests in +an ebuild tree. By default, \fIqmanifest\fR will verify the main tree +as specified by Portage's configuration (one can check which using +\fIq\fR \fB-o\fR). In this mode, it will output some information about +the GPG signature of the top-level Manifest file, and further reports on +any problems it encounters. +.P +This applet was originally a standalone program \fIhashgen\fR and its +alias \fIhashverify\fR. Aliases for these names are still available for +historical reasons. With the incorporation of \fIhashgen\fR in +\fBportage-utils\fR, development on the former has stopped in favour of +the latter. +.P +The arguments to \fIqmanifest\fR can be directories or names of +overlays. By default, each argument is attempted to be matched against +all overlay names, and if that fails, treated as directory to presume a +tree is in. This behaviour can be overridden with the \fB-d\fR and +\fB-o\fR flags to force treating the arguments as directories or +overlay names respectively. Note that overlay names are those as +defined in \fIrepos.conf\fR from Portage's configuration. The +\fIrepo_name\fR files from the overlays themselves (if present) are +ignored. +.P +This applet does similar things as \fIapp-portage/gemato\fR. However, +the output and implemented strategies are completely different. When +compiled with \fBUSE=openmp\fR, this applet will exploit parallelism +where possible to traverse a tree. Should you want to limit the number +of parallel threads, export \fBOMP_NUM_THREADS\fR in your environment +with the desired maximum amount of threads in use by \fIqmanifest\fR. diff --git a/man/qmanifest.1 b/man/qmanifest.1 new file mode 100644 index 0000000..e223122 --- /dev/null +++ b/man/qmanifest.1 @@ -0,0 +1,94 @@ +.\" generated by mkman.py, please do NOT edit! +.TH qmanifest "1" "May 2019" "Gentoo Foundation" "qmanifest" +.SH NAME +qmanifest \- verify or generate thick Manifest files +.SH SYNOPSIS +.B qmanifest +\fI[opts] \fR +.SH DESCRIPTION +\fIqmanifest\fR allows to verify or generate thick signed Manifests in +an ebuild tree. By default, \fIqmanifest\fR will verify the main tree +as specified by Portage's configuration (one can check which using +\fIq\fR \fB-o\fR). In this mode, it will output some information about +the GPG signature of the top-level Manifest file, and further reports on +any problems it encounters. +.P +This applet was originally a standalone program \fIhashgen\fR and its +alias \fIhashverify\fR. Aliases for these names are still available for +historical reasons. With the incorporation of \fIhashgen\fR in +\fBportage-utils\fR, development on the former has stopped in favour of +the latter. +.P +The arguments to \fIqmanifest\fR can be directories or names of +overlays. By default, each argument is attempted to be matched against +all overlay names, and if that fails, treated as directory to presume a +tree is in. This behaviour can be overridden with the \fB-d\fR and +\fB-o\fR flags to force treating the arguments as directories or +overlay names respectively. Note that overlay names are those as +defined in \fIrepos.conf\fR from Portage's configuration. The +\fIrepo_name\fR files from the overlays themselves (if present) are +ignored. +.P +This applet does similar things as \fIapp-portage/gemato\fR. However, +the output and implemented strategies are completely different. When +compiled with \fBUSE=openmp\fR, this applet will exploit parallelism +where possible to traverse a tree. Should you want to limit the number +of parallel threads, export \fBOMP_NUM_THREADS\fR in your environment +with the desired maximum amount of threads in use by \fIqmanifest\fR. +.SH OPTIONS +.TP +\fB\-g\fR, \fB\-\-generate\fR +Generate thick Manifests and sign. +.TP +\fB\-d\fR, \fB\-\-dir\fR +Treat arguments as directories. +.TP +\fB\-o\fR, \fB\-\-overlay\fR +Treat arguments as overlay names. +.TP +\fB\-\-root\fR \fI\fR +Set the ROOT env var. +.TP +\fB\-v\fR, \fB\-\-verbose\fR +Make a lot of noise. +.TP +\fB\-q\fR, \fB\-\-quiet\fR +Tighter output; suppress warnings. +.TP +\fB\-C\fR, \fB\-\-nocolor\fR +Don't output color. +.TP +\fB\-h\fR, \fB\-\-help\fR +Print this help and exit. +.TP +\fB\-V\fR, \fB\-\-version\fR +Print version and exit. + +.SH "REPORTING BUGS" +Please report bugs via http://bugs.gentoo.org/ +.br +Product: Portage Development; Component: Tools +.SH AUTHORS +.nf +Ned Ludd +Mike Frysinger +Fabian Groffen +.fi +.SH "SEE ALSO" +.BR q (1), +.BR qatom (1), +.BR qcheck (1), +.BR qdepends (1), +.BR qfile (1), +.BR qgrep (1), +.BR qkeyword (1), +.BR qlist (1), +.BR qlop (1), +.BR qmerge (1), +.BR qpkg (1), +.BR qsearch (1), +.BR qsize (1), +.BR qtbz2 (1), +.BR qtegrity (1), +.BR quse (1), +.BR qxpak (1) diff --git a/qmanifest.c b/qmanifest.c new file mode 100644 index 0000000..09e7881 --- /dev/null +++ b/qmanifest.c @@ -0,0 +1,1695 @@ +/* + * Copyright 2018-2019 Gentoo Foundation + * Distributed under the terms of the GNU General Public License v2 + * + * Copyright 2018- Fabian Groffen - + * + * The contents of this file was taken from: + * https://github.com/grobian/hashgen + * which was discontinued at the time the sources were incorporated into + * portage-utils as qmanifest. + */ + +#include "main.h" + +#ifdef ENABLE_QMANIFEST + +#include "applets.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "eat_file.h" +#include "hash.h" + +#define QMANIFEST_FLAGS "gdo" COMMON_FLAGS +static struct option const qmanifest_long_opts[] = { + {"generate", no_argument, NULL, 'g'}, + {"dir", no_argument, NULL, 'd'}, + {"overlay", no_argument, NULL, 'o'}, + COMMON_LONG_OPTS +}; +static const char * const qmanifest_opts_help[] = { + "Generate thick Manifests and sign", + "Treat arguments as directories", + "Treat arguments as overlay names", + COMMON_OPTS_HELP +}; +#define qmanifest_usage(ret) usage(ret, QMANIFEST_FLAGS, qmanifest_long_opts, qmanifest_opts_help, NULL, lookup_applet_idx("qmanifest")) + +static int hashes = HASH_DEFAULT; + +/* linked list structure to hold verification complaints */ +typedef struct verify_msg { + char *msg; + struct verify_msg *next; +} verify_msg; + +typedef struct _gpg_signature { + char *algo; + char *fingerprint; + char isgood:1; + char *timestamp; + char *signer; + char *pkfingerprint; + char *reason; +} gpg_sig; + +gpg_sig *verify_gpg_sig(const char *path, verify_msg **msgs); +char *verify_timestamp(const char *ts); +char verify_manifest(const char *dir, const char *manifest, verify_msg **msgs); + +/* Generate thick Manifests based on thin Manifests, or verify a tree. */ + +/* In order to build this program, the following packages are required: + * - sys-libs/zlib (for compressing/decompressing Manifest files) + * - app-crypt/gpgme (for signing/verifying the top level manifest) + */ + +static inline void +update_times(struct timeval *tv, struct stat *s) +{ +#ifdef __MACH__ +# define st_mtim st_mtimespec +# define st_atim st_atimespec +#endif + if (tv[1].tv_sec < s->st_mtim.tv_sec || + (tv[1].tv_sec == s->st_mtim.tv_sec && + tv[1].tv_usec < s->st_mtim.tv_nsec / 1000)) + { + tv[0].tv_sec = s->st_atim.tv_sec; + tv[0].tv_usec = s->st_atim.tv_nsec / 1000; + tv[1].tv_sec = s->st_mtim.tv_sec; + tv[1].tv_usec = s->st_mtim.tv_nsec / 1000; + } +} + +#define LISTSZ 64 + +/** + * qsort comparator which runs strcmp. + */ +static int +compare_strings(const void *l, const void *r) +{ + const char **strl = (const char **)l; + const char **strr = (const char **)r; + return strcmp(*strl, *strr); +} + +/** + * Return a sorted list of entries in the given directory. All entries + * starting with a dot are ignored, and not present in the returned + * list. The list and all entries are allocated using xmalloc() and need + * to be freed. + * This function returns 0 when everything is fine, non-zero otherwise. + */ +static char +list_dir(char ***retlist, size_t *retcnt, const char *path) +{ + DIR *d; + struct dirent *e; + size_t rlen = 0; + size_t rsize = 0; + char **rlist = NULL; + + if ((d = opendir(path)) != NULL) { + while ((e = readdir(d)) != NULL) { + /* skip all dotfiles */ + if (e->d_name[0] == '.') + continue; + + if (rlen == rsize) { + rsize += LISTSZ; + rlist = realloc(rlist, + rsize * sizeof(rlist[0])); + if (rlist == NULL) { + fprintf(stderr, "out of memory\n"); + return 1; + } + } + rlist[rlen] = xstrdup(e->d_name); + if (rlist[rlen] == NULL) { + fprintf(stderr, "out of memory\n"); + return 1; + } + rlen++; + } + closedir(d); + + qsort(rlist, rlen, sizeof(rlist[0]), compare_strings); + + *retlist = rlist; + *retcnt = rlen; + return 0; + } else { + return 1; + } +} + +/** + * Write hashes in Manifest format to the file open for writing m, or + * gzipped file open for writing gm. The hashes written are for a file + * in root found by name. The Manifest entry will be using type as + * first component. + */ +static void +write_hashes( + struct timeval *tv, + const char *root, + const char *name, + const char *type, + FILE *m, + gzFile gm) +{ + size_t flen = 0; + char sha256[(SHA256_DIGEST_LENGTH * 2) + 1]; + char sha512[(SHA512_DIGEST_LENGTH * 2) + 1]; + char whrlpl[(WHIRLPOOL_DIGEST_LENGTH * 2) + 1]; + char blak2b[(BLAKE2B_OUTBYTES * 2) + 1]; + char data[8192]; + char fname[8192]; + size_t len; + struct stat s; + + snprintf(fname, sizeof(fname), "%s/%s", root, name); + + if (stat(fname, &s) != 0) + return; + + update_times(tv, &s); + + hash_compute_file(fname, sha256, sha512, whrlpl, blak2b, &flen); + + len = snprintf(data, sizeof(data), "%s %s %zd", type, name, flen); + if (hashes & HASH_BLAKE2B) + len += snprintf(data + len, sizeof(data) - len, + " BLAKE2B %s", blak2b); + if (hashes & HASH_SHA256) + len += snprintf(data + len, sizeof(data) - len, + " SHA256 %s", sha256); + if (hashes & HASH_SHA512) + len += snprintf(data + len, sizeof(data) - len, + " SHA512 %s", sha512); + if (hashes & HASH_WHIRLPOOL) + len += snprintf(data + len, sizeof(data) - len, + " WHIRLPOOL %s", whrlpl); + len += snprintf(data + len, sizeof(data) - len, "\n"); + + if (m != NULL) + fwrite(data, len, 1, m); + if (gm != NULL) + gzwrite(gm, data, len); +} + +/** + * Walk through a directory recursively and write hashes for each file + * found to the gzipped open stream for writing zm. The Manifest + * entries generated will all be of DATA type. + */ +static char +write_hashes_dir( + struct timeval *tv, + const char *root, + const char *name, + gzFile zm) +{ + char path[8192]; + char **dentries; + size_t dentrieslen; + size_t i; + + snprintf(path, sizeof(path), "%s/%s", root, name); + if (list_dir(&dentries, &dentrieslen, path) == 0) { + for (i = 0; i < dentrieslen; i++) { + snprintf(path, sizeof(path), "%s/%s", name, dentries[i]); + free(dentries[i]); + if (write_hashes_dir(tv, root, path, zm) == 0) + continue; + /* regular file */ + write_hashes(tv, root, path, "DATA", NULL, zm); + } + free(dentries); + return 0; + } else { + return 1; + } +} + +/** + * Walk through directory recursively and write hashes for each file + * found to the open stream for writing m. All files will not use the + * "files/" prefix and Manifest entries will be of AUX type. + */ +static char +process_files(struct timeval *tv, const char *dir, const char *off, FILE *m) +{ + char path[8192]; + char **dentries; + size_t dentrieslen; + size_t i; + + snprintf(path, sizeof(path), "%s/%s", dir, off); + if (list_dir(&dentries, &dentrieslen, path) == 0) { + for (i = 0; i < dentrieslen; i++) { + snprintf(path, sizeof(path), "%s%s%s", + off, *off == '\0' ? "" : "/", dentries[i]); + free(dentries[i]); + if (process_files(tv, dir, path, m) == 0) + continue; + /* regular file */ + write_hashes(tv, dir, path, "AUX", m, NULL); + } + free(dentries); + return 0; + } else { + return 1; + } +} + +/** + * Read layout.conf file specified by path and extract the + * manifest-hashes property from this file. The hash set specified for + * this property will be returned. When the property isn't found the + * returned hash set will be the HASH_DEFAULT set. When the file isn't + * found, 0 will be returned (which means /no/ hashes). + */ +static int +parse_layout_conf(const char *path) +{ + FILE *f; + char buf[8192]; + size_t len = 0; + size_t sz; + char *p; + char *q; + char *tok; + char *last_nl; + char *start; + int ret = 0; + + if ((f = fopen(path, "r")) == NULL) + return 0; + + /* read file, examine lines after encountering a newline, that is, + * if the file doesn't end with a newline, the final bit is ignored */ + while ((sz = fread(buf + len, 1, sizeof(buf) - len, f)) > 0) { + len += sz; + start = buf; + last_nl = NULL; + for (p = buf; (size_t)(p - buf) < len; p++) { + if (*p == '\n') { + if (last_nl != NULL) + start = last_nl + 1; + last_nl = p; + do { + sz = strlen("manifest-hashes"); + if (strncmp(start, "manifest-hashes", sz)) + break; + if ((q = strchr(start + sz, '=')) == NULL) + break; + q++; + while (isspace((int)*q)) + q++; + /* parse the tokens, whitespace separated */ + tok = q; + do { + while (!isspace((int)*q)) + q++; + sz = q - tok; + if (strncmp(tok, "SHA256", sz) == 0) { + ret |= HASH_SHA256; + } else if (strncmp(tok, "SHA512", sz) == 0) { + ret |= HASH_SHA512; + } else if (strncmp(tok, "WHIRLPOOL", sz) == 0) { + ret |= HASH_WHIRLPOOL; + } else if (strncmp(tok, "BLAKE2B", sz) == 0) { + ret |= HASH_BLAKE2B; + } else { + fprintf(stderr, "warning: unsupported hash from " + "layout.conf: %.*s\n", (int)sz, tok); + } + while (isspace((int)*q) && *q != '\n') + q++; + tok = q; + } while (*q != '\n'); + /* got it, expect only once, so stop processing */ + fclose(f); + return ret; + } while (0); + } + } + if (last_nl != NULL) { + last_nl++; /* skip \n */ + len = last_nl - buf; + memmove(buf, last_nl, len); + last_nl = buf; + } else { + /* skip too long line */ + len = 0; + } + } + + fclose(f); + /* if we didn't find anything, return the default set */ + return HASH_DEFAULT; +} + +static const char *str_manifest = "Manifest"; +static const char *str_manifest_gz = "Manifest.gz"; +static const char *str_manifest_files_gz = "Manifest.files.gz"; +enum type_manifest { + GLOBAL_MANIFEST, /* Manifest.files.gz + Manifest */ + SUBTREE_MANIFEST, /* Manifest.gz for recursive list of files */ + EBUILD_MANIFEST, /* Manifest thick from thin */ + CATEGORY_MANIFEST /* Manifest.gz with Manifest entries */ +}; +static const char * +generate_dir(const char *dir, enum type_manifest mtype) +{ + FILE *f; + char path[8192]; + struct stat s; + struct timeval tv[2]; + char **dentries; + size_t dentrieslen; + size_t i; + + /* our timestamp strategy is as follows: + * - when a Manifest exists, use its timestamp + * - when a meta-Manifest is written (non-ebuilds) use the timestamp + * of the latest Manifest referenced + * - when a Manifest is written for something like eclasses, use the + * timestamp of the latest file in the dir + * this way we should keep updates limited to where changes are, and + * also get reproducible mtimes. */ + tv[0].tv_sec = 0; + tv[0].tv_usec = 0; + tv[1].tv_sec = 0; + tv[1].tv_usec = 0; + + if (mtype == GLOBAL_MANIFEST) { + const char *mfest; + size_t len; + gzFile mf; + time_t rtime; + + snprintf(path, sizeof(path), "%s/%s", dir, str_manifest_files_gz); + if ((mf = gzopen(path, "wb9")) == NULL) { + fprintf(stderr, "failed to open file '%s' for writing: %s\n", + path, strerror(errno)); + return NULL; + } + + /* These "IGNORE" entries are taken from gx86, there is no + * standardisation on this, on purpose, apparently. */ + len = snprintf(path, sizeof(path), + "IGNORE distfiles\n" + "IGNORE local\n" + "IGNORE lost+found\n" + "IGNORE packages\n" + "IGNORE snapshots\n"); + gzwrite(mf, path, len); + + if (list_dir(&dentries, &dentrieslen, dir) != 0) + return NULL; + + for (i = 0; i < dentrieslen; i++) { + /* ignore existing Manifests */ + if (strcmp(dentries[i], str_manifest_files_gz) == 0 || + strcmp(dentries[i], str_manifest) == 0) + { + free(dentries[i]); + continue; + } + + snprintf(path, sizeof(path), "%s/%s", dir, dentries[i]); + + mfest = NULL; + if (!stat(path, &s)) { + if (s.st_mode & S_IFDIR) { + if ( + strcmp(dentries[i], "eclass") == 0 || + strcmp(dentries[i], "licenses") == 0 || + strcmp(dentries[i], "metadata") == 0 || + strcmp(dentries[i], "profiles") == 0 || + strcmp(dentries[i], "scripts") == 0 + ) + { + mfest = generate_dir(path, SUBTREE_MANIFEST); + } else { + mfest = generate_dir(path, CATEGORY_MANIFEST); + } + + if (mfest == NULL) { + fprintf(stderr, "generating Manifest for %s failed!\n", + path); + gzclose(mf); + return NULL; + } + + snprintf(path, sizeof(path), "%s/%s", + dentries[i], mfest); + write_hashes(tv, dir, path, "MANIFEST", NULL, mf); + } else if (s.st_mode & S_IFREG) { + write_hashes(tv, dir, dentries[i], "DATA", NULL, mf); + } /* ignore other "things" (like symlinks) as they + don't belong in a tree */ + } else { + fprintf(stderr, "stat(%s) failed: %s\n", + path, strerror(errno)); + } + free(dentries[i]); + } + free(dentries); + gzclose(mf); + + if (tv[0].tv_sec != 0) { + snprintf(path, sizeof(path), "%s/%s", dir, str_manifest_files_gz); + utimes(path, tv); + } + + /* create global Manifest */ + snprintf(path, sizeof(path), "%s/%s", dir, str_manifest); + if ((f = fopen(path, "w")) == NULL) { + fprintf(stderr, "failed to open file '%s' for writing: %s\n", + path, strerror(errno)); + return NULL; + } + + write_hashes(tv, dir, str_manifest_files_gz, "MANIFEST", f, NULL); + time(&rtime); + len = strftime(path, sizeof(path), + "TIMESTAMP %Y-%m-%dT%H:%M:%SZ\n", gmtime(&rtime)); + fwrite(path, len, 1, f); + fflush(f); + fclose(f); + + /* because we write a timestamp in Manifest, we don't mess with + * its mtime, else it would obviously lie */ + return str_manifest_files_gz; + } else if (mtype == SUBTREE_MANIFEST) { + const char *ldir; + gzFile mf; + + snprintf(path, sizeof(path), "%s/%s", dir, str_manifest_gz); + if ((mf = gzopen(path, "wb9")) == NULL) { + fprintf(stderr, "failed to open file '%s' for writing: %s\n", + path, strerror(errno)); + return NULL; + } + + ldir = strrchr(dir, '/'); + if (ldir == NULL) + ldir = dir; + if (strcmp(ldir, "metadata") == 0) { + size_t len; + len = snprintf(path, sizeof(path), + "IGNORE timestamp\n" + "IGNORE timestamp.chk\n" + "IGNORE timestamp.commit\n" + "IGNORE timestamp.x\n"); + gzwrite(mf, path, len); + } + + if (list_dir(&dentries, &dentrieslen, dir) != 0) + return NULL; + + for (i = 0; i < dentrieslen; i++) { + /* ignore existing Manifests */ + if (strcmp(dentries[i], str_manifest_gz) == 0) { + free(dentries[i]); + continue; + } + + if (write_hashes_dir(tv, dir, dentries[i], mf) != 0) + write_hashes(tv, dir, dentries[i], "DATA", NULL, mf); + free(dentries[i]); + } + + free(dentries); + gzclose(mf); + + if (tv[0].tv_sec != 0) { + /* set Manifest and dir mtime to most recent file found */ + snprintf(path, sizeof(path), "%s/%s", dir, str_manifest_gz); + utimes(path, tv); + utimes(dir, tv); + } + + return str_manifest_gz; + } else if (mtype == CATEGORY_MANIFEST) { + const char *mfest; + gzFile mf; + + snprintf(path, sizeof(path), "%s/%s", dir, str_manifest_gz); + if ((mf = gzopen(path, "wb9")) == NULL) { + fprintf(stderr, "failed to open file '%s' for writing: %s\n", + path, strerror(errno)); + return NULL; + } + + if (list_dir(&dentries, &dentrieslen, dir) != 0) + return NULL; + + for (i = 0; i < dentrieslen; i++) { + /* ignore existing Manifests */ + if (strcmp(dentries[i], str_manifest_gz) == 0) { + free(dentries[i]); + continue; + } + + snprintf(path, sizeof(path), "%s/%s", dir, dentries[i]); + if (!stat(path, &s)) { + if (s.st_mode & S_IFDIR) { + mfest = generate_dir(path, EBUILD_MANIFEST); + + if (mfest == NULL) { + fprintf(stderr, "generating Manifest for %s failed!\n", + path); + gzclose(mf); + return NULL; + } + + snprintf(path, sizeof(path), "%s/%s", + dentries[i], mfest); + write_hashes(tv, dir, path, "MANIFEST", NULL, mf); + } else if (s.st_mode & S_IFREG) { + write_hashes(tv, dir, dentries[i], "DATA", NULL, mf); + } /* ignore other "things" (like symlinks) as they + don't belong in a tree */ + } else { + fprintf(stderr, "stat(%s) failed: %s\n", + path, strerror(errno)); + } + free(dentries[i]); + } + + free(dentries); + gzclose(mf); + + if (tv[0].tv_sec != 0) { + /* set Manifest and dir mtime to most ebuild dir found */ + snprintf(path, sizeof(path), "%s/%s", dir, str_manifest_gz); + utimes(path, tv); + utimes(dir, tv); + } + + return str_manifest_gz; + } else if (mtype == EBUILD_MANIFEST) { + char newmanifest[8192]; + FILE *m; + + snprintf(newmanifest, sizeof(newmanifest), "%s/.Manifest.new", dir); + if ((m = fopen(newmanifest, "w")) == NULL) { + fprintf(stderr, "failed to open file '%s' for writing: %s\n", + newmanifest, strerror(errno)); + return NULL; + } + + /* we know the Manifest is sorted, and stuff in files/ is + * prefixed with AUX, hence, if it exists, we need to do it + * first */ + snprintf(path, sizeof(path), "%s/files", dir); + process_files(tv, path, "", m); + + /* the Manifest file may be missing in case there are no DIST + * entries to be stored */ + snprintf(path, sizeof(path), "%s/%s", dir, str_manifest); + if (!stat(path, &s)) + update_times(tv, &s); + f = fopen(path, "r"); + if (f != NULL) { + /* copy the DIST entries, we could do it unconditional, but this + * way we can re-run without producing invalid Manifests */ + while (fgets(path, sizeof(path), f) != NULL) { + if (strncmp(path, "DIST ", 5) == 0) + if (fwrite(path, strlen(path), 1, m) != 1) { + fprintf(stderr, "failed to write to " + "%s/.Manifest.new: %s\n", + dir, strerror(errno)); + fclose(f); + fclose(m); + return NULL; + } + } + fclose(f); + } + + if (list_dir(&dentries, &dentrieslen, dir) == 0) { + for (i = 0; i < dentrieslen; i++) { + if (strcmp(dentries[i] + strlen(dentries[i]) - 7, + ".ebuild") != 0) + { + free(dentries[i]); + continue; + } + write_hashes(tv, dir, dentries[i], "EBUILD", m, NULL); + free(dentries[i]); + } + free(dentries); + } + + write_hashes(tv, dir, "ChangeLog", "MISC", m, NULL); + write_hashes(tv, dir, "metadata.xml", "MISC", m, NULL); + + fflush(m); + fclose(m); + + snprintf(path, sizeof(path), "%s/%s", dir, str_manifest); + rename(newmanifest, path); + + if (tv[0].tv_sec != 0) { + /* set Manifest and dir mtime to most recent file we found */ + utimes(path, tv); + utimes(dir, tv); + } + + return str_manifest; + } else { + return NULL; + } +} + +static const char * +process_dir_gen(const char *dir) +{ + char path[8192]; + int newhashes; + int curdirfd; + + snprintf(path, sizeof(path), "%s/metadata/layout.conf", dir); + if ((newhashes = parse_layout_conf(path)) != 0) { + hashes = newhashes; + } else { + return "generation must be done on a full tree"; + } + + if ((curdirfd = open(".", O_RDONLY)) < 0) { + fprintf(stderr, "cannot open current directory?!? %s\n", + strerror(errno)); + } + if (chdir(dir) != 0) { + fprintf(stderr, "cannot chdir() to %s: %s\n", dir, strerror(errno)); + return "not a directory"; + } + + if (generate_dir(".\0", GLOBAL_MANIFEST) == NULL) + return "generation failed"; + + /* return to where we were before we called this function */ + fchdir(curdirfd); + close(curdirfd); + + return NULL; +} + +static void +msgs_add( + verify_msg **msgs, + const char *manifest, + const char *ebuild, + const char *fmt, ...) +{ + char buf[4096]; + int len; + va_list ap; + verify_msg *msg; + + if (msgs == NULL || *msgs == NULL) + return; + + msg = (*msgs)->next = xmalloc(sizeof(verify_msg)); + + len = snprintf(buf, sizeof(buf), "%s:%s:", + manifest ? manifest : "", + ebuild ? ebuild : ""); + + va_start(ap, fmt); + vsnprintf(buf + len, sizeof(buf) - len, fmt, ap); + va_end(ap); + + msg->msg = xstrdup(buf); + msg->next = NULL; + *msgs = msg; +} + +gpg_sig * +verify_gpg_sig(const char *path, verify_msg **msgs) +{ + gpgme_ctx_t g_ctx; + gpgme_data_t manifest; + gpgme_data_t out; + gpgme_verify_result_t vres; + gpgme_signature_t sig; + gpgme_key_t key; + char buf[64]; + FILE *f; + struct tm *ctime; + gpg_sig *ret = NULL; + + if ((f = fopen(path, "r")) == NULL) { + msgs_add(msgs, path, NULL, "failed to open: %s", strerror(errno)); + return NULL; + } + + if (gpgme_new(&g_ctx) != GPG_ERR_NO_ERROR) { + msgs_add(msgs, path, NULL, "failed to create gpgme context"); + return NULL; + } + + if (gpgme_data_new(&out) != GPG_ERR_NO_ERROR) { + msgs_add(msgs, path, NULL, "failed to create gpgme data"); + return NULL; + } + + if (gpgme_data_new_from_stream(&manifest, f) != GPG_ERR_NO_ERROR) { + msgs_add(msgs, path, NULL, + "failed to create new gpgme data from stream"); + return NULL; + } + + if (gpgme_op_verify(g_ctx, manifest, NULL, out) != GPG_ERR_NO_ERROR) { + msgs_add(msgs, path, NULL, "failed to verify signature"); + return NULL; + } + + vres = gpgme_op_verify_result(g_ctx); + fclose(f); + + if (vres == NULL || vres->signatures == NULL) { + msgs_add(msgs, path, NULL, + "verification failed due to a missing gpg keyring"); + return NULL; + } + + /* we only check/return the first signature */ + if ((sig = vres->signatures) != NULL) { + ret = xmalloc(sizeof(gpg_sig)); + + if (sig->status != GPG_ERR_NO_PUBKEY) { + ret->algo = xstrdup(gpgme_pubkey_algo_name(sig->pubkey_algo)); + snprintf(buf, sizeof(buf), + "%.4s %.4s %.4s %.4s %.4s %.4s %.4s %.4s %.4s %.4s", + sig->fpr + 0, sig->fpr + 4, sig->fpr + 8, sig->fpr + 12, + sig->fpr + 16, sig->fpr + 20, sig->fpr + 24, sig->fpr + 28, + sig->fpr + 32, sig->fpr + 36); + ret->fingerprint = xstrdup(buf); + ret->isgood = sig->status == GPG_ERR_NO_ERROR ? 1 : 0; + ctime = gmtime((time_t *)&sig->timestamp); + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S UTC", ctime); + ret->timestamp = xstrdup(buf); + + if (gpgme_get_key(g_ctx, sig->fpr, &key, 0) == GPG_ERR_NO_ERROR) { + if (key->uids != NULL) + ret->signer = xstrdup(key->uids->uid); + if (key->subkeys != NULL) { + snprintf(buf, sizeof(buf), + "%.4s %.4s %.4s %.4s %.4s " + "%.4s %.4s %.4s %.4s %.4s", + key->subkeys->fpr + 0, key->subkeys->fpr + 4, + key->subkeys->fpr + 8, key->subkeys->fpr + 12, + key->subkeys->fpr + 16, key->subkeys->fpr + 20, + key->subkeys->fpr + 24, key->subkeys->fpr + 28, + key->subkeys->fpr + 32, key->subkeys->fpr + 36); + ret->pkfingerprint = xstrdup(buf); + } + gpgme_key_release(key); + } + } + + switch (sig->status) { + case GPG_ERR_NO_ERROR: + /* nothing */ + ret->reason = NULL; + break; + case GPG_ERR_SIG_EXPIRED: + ret->reason = xstrdup("the signature is valid but expired"); + break; + case GPG_ERR_KEY_EXPIRED: + ret->reason = xstrdup("the signature is valid but the key " + "used to verify the signature has expired"); + break; + case GPG_ERR_CERT_REVOKED: + ret->reason = xstrdup("the signature is valid but the key " + "used to verify the signature has been revoked"); + break; + case GPG_ERR_BAD_SIGNATURE: + printf("the signature is invalid\n"); + break; + case GPG_ERR_NO_PUBKEY: + printf("the signature could not be verified due to a " + "missing key\n"); + break; + default: + printf("there was some other error which prevented the " + "signature verification\n"); + break; + } + } + + gpgme_release(g_ctx); + + return ret; +} + +static size_t checked_manifests = 0; +static size_t checked_files = 0; +static size_t failed_files = 0; +static char strict = 0; + +static char +verify_file(const char *dir, char *mfline, const char *mfest, verify_msg **msgs) +{ + char *path; + char *size; + long long int fsize; + char *hashtype; + char *hash; + char *p; + char buf[8192]; + size_t flen = 0; + char sha256[(SHA256_DIGEST_LENGTH * 2) + 1]; + char sha512[(SHA512_DIGEST_LENGTH * 2) + 1]; + char whrlpl[(WHIRLPOOL_DIGEST_LENGTH * 2) + 1]; + char blak2b[(BLAKE2B_OUTBYTES * 2) + 1]; + char ret = 0; + + /* mfline is a Manifest file line with type and leading path + * stripped, something like: + * file + * we parse this, and verify the size and hashes */ + + path = mfline; + p = strchr(path, ' '); + if (p == NULL) { + msgs_add(msgs, mfest, NULL, "corrupt manifest line: %s", path); + return 1; + } + *p++ = '\0'; + + size = p; + p = strchr(size, ' '); + if (p == NULL) { + msgs_add(msgs, mfest, NULL, "corrupt manifest line, need size"); + return 1; + } + *p++ = '\0'; + fsize = strtoll(size, NULL, 10); + if (fsize == 0 && errno == EINVAL) { + msgs_add(msgs, mfest, NULL, "corrupt manifest line, " + "size is not a number: %s", size); + return 1; + } + + sha256[0] = sha512[0] = whrlpl[0] = blak2b[0] = '\0'; + snprintf(buf, sizeof(buf), "%s/%s", dir, path); + hash_compute_file(buf, sha256, sha512, whrlpl, blak2b, &flen); + + if (flen == 0) { + msgs_add(msgs, mfest, path, "cannot open file!"); + return 1; + } + + checked_files++; + + if (flen != (size_t)fsize) { + msgs_add(msgs, mfest, path, + "file size mismatch\n" + " got: %zd\n" + "expected: %lld", + flen, fsize); + failed_files++; + return 1; + } + + /* now we are in free territory, we read TYPE HASH pairs until we + * drained the string, and match them against what we computed */ + while (p != NULL && *p != '\0') { + hashtype = p; + p = strchr(hashtype, ' '); + if (p == NULL) { + msgs_add(msgs, mfest, path, + "corrupt manifest line, missing hash type"); + return 1; + } + *p++ = '\0'; + + hash = p; + p = strchr(hash, ' '); + if (p != NULL) + *p++ = '\0'; + + if (strcmp(hashtype, "SHA256") == 0) { + if (!(hashes & HASH_SHA256)) { + if (strict) + msgs_add(msgs, mfest, path, + "hash SHA256 is not " + "enabled for this repository"); + } else if (strcmp(hash, sha256) != 0) { + msgs_add(msgs, mfest, path, + "SHA256 hash mismatch\n" + "computed: '%s'\n" + "Manifest: '%s'", + sha256, hash); + ret = 1; + } + sha256[0] = '\0'; + } else if (strcmp(hashtype, "SHA512") == 0) { + if (!(hashes & HASH_SHA512)) { + if (strict) + msgs_add(msgs, mfest, path, + "hash SHA512 is not " + "enabled for this repository"); + } else if (strcmp(hash, sha512) != 0) { + msgs_add(msgs, mfest, path, + "SHA512 hash mismatch\n" + "computed: '%s'\n" + "Manifest: '%s'", + sha512, hash); + ret = 1; + } + sha512[0] = '\0'; + } else if (strcmp(hashtype, "WHIRLPOOL") == 0) { + if (!(hashes & HASH_WHIRLPOOL)) { + if (strict) + msgs_add(msgs, mfest, path, + "hash WHIRLPOOL is not " + "enabled for this repository"); + } else if (strcmp(hash, whrlpl) != 0) { + msgs_add(msgs, mfest, path, + "WHIRLPOOL hash mismatch\n" + "computed: '%s'\n" + "Manifest: '%s'", + whrlpl, hash); + ret = 1; + } + whrlpl[0] = '\0'; + } else if (strcmp(hashtype, "BLAKE2B") == 0) { + if (!(hashes & HASH_BLAKE2B)) { + if (strict) + msgs_add(msgs, mfest, path, + "hash BLAKE2B is not " + "enabled for this repository"); + } else if (strcmp(hash, blak2b) != 0) { + msgs_add(msgs, mfest, path, + "BLAKE2B hash mismatch\n" + "computed: '%s'\n" + "Manifest: '%s'", + blak2b, hash); + ret = 1; + } + blak2b[0] = '\0'; + } else { + msgs_add(msgs, mfest, path, "unsupported hash: %s", hashtype); + ret = 1; + } + } + + if (sha256[0] != '\0') { + msgs_add(msgs, mfest, path, "missing hash: SHA256"); + ret = 1; + } + if (sha512[0] != '\0') { + msgs_add(msgs, mfest, path, "missing hash: SHA512"); + ret = 1; + } + if (whrlpl[0] != '\0') { + msgs_add(msgs, mfest, path, "missing hash: WHIRLPOOL"); + ret = 1; + } + if (blak2b[0] != '\0') { + msgs_add(msgs, mfest, path, "missing hash: BLAKE2B"); + ret = 1; + } + + failed_files += ret; + return ret; +} + +static int +compare_elems(const void *l, const void *r) +{ + const char *strl = *((const char **)l) + 2; + const char *strr = *((const char **)r) + 2; + unsigned char cl; + unsigned char cr; + /* compare treating / as end of string */ + while ((cl = *strl++) == (cr = *strr++)) + if (cl == '\0') + return 0; + if (cl == '/') + cl = '\0'; + if (cr == '/') + cr = '\0'; + return cl - cr; +} + +struct subdir_workload { + size_t subdirlen; + size_t elemslen; + char **elems; +}; + +static char +verify_dir( + const char *dir, + char **elems, + size_t elemslen, + size_t skippath, + const char *mfest, + verify_msg **msgs) +{ + char **dentries = NULL; + size_t dentrieslen = 0; + size_t curelem = 0; + size_t curdentry = 0; + char *entry; + char *slash; + char etpe; + char ret = 0; + int cmp; + struct subdir_workload **subdir = NULL; + size_t subdirsize = 0; + size_t subdirlen = 0; + size_t elem; + + /* shortcut a single Manifest entry pointing to the same dir + * (happens at top-level) */ + if (elemslen == 1 && skippath == 0 && + **elems == 'M' && strchr(*elems + 2, '/') == NULL) + { + if ((ret = verify_file(dir, *elems + 2, mfest, msgs)) == 0) { + slash = strchr(*elems + 2, ' '); + if (slash != NULL) + *slash = '\0'; + /* else, verify_manifest will fail, so ret will be handled */ + ret = verify_manifest(dir, *elems + 2, msgs); + } + return ret; + } + + /* + * We have a list of entries from the manifest just read, now we + * need to match these onto the directory layout. From what we got + * - we can ignore TIMESTAMP and DIST entries + * - IGNOREs need to be handled separate (shortcut) + * - MANIFESTs need to be handled on their own, for memory + * consumption reasons, we defer them to until we've verified + * what's left, we treat the path the Manifest refers to as IGNORE + * - DATAs, EBUILDs and MISCs needs verifying + * - AUXs need verifying, but in files/ subdir + * If we sort both lists, we should be able to do a merge-join, to + * easily flag missing entries in either list without hashing or + * anything. + */ + if (list_dir(&dentries, &dentrieslen, dir) == 0) { + while (curdentry < dentrieslen) { + if (strcmp(dentries[curdentry], str_manifest) == 0 || + strcmp(dentries[curdentry], str_manifest_gz) == 0 || + strcmp(dentries[curdentry], str_manifest_files_gz) == 0) + { + curdentry++; + continue; + } + + if (curelem < elemslen) { + entry = elems[curelem] + 2 + skippath; + etpe = *elems[curelem]; + } else { + entry = (char *)""; + etpe = 'I'; + } + + /* handle subdirs first */ + if ((slash = strchr(entry, '/')) != NULL) { + size_t sublen = slash - entry; + int elemstart = curelem; + char **subelems = &elems[curelem]; + + /* collect all entries like this one (same subdir) into + * a sub-list that we can verify */ + curelem++; + while (curelem < elemslen && + strncmp(entry, elems[curelem] + 2 + skippath, + sublen + 1) == 0) + curelem++; + + if (subdirlen == subdirsize) { + subdirsize += LISTSZ; + subdir = realloc(subdir, + subdirsize * sizeof(subdir[0])); + if (subdir == NULL) { + msgs_add(msgs, mfest, NULL, "out of memory allocating " + "sublist for %.*s", (int)sublen, entry); + return 1; + } + } + subdir[subdirlen] = xmalloc(sizeof(struct subdir_workload)); + subdir[subdirlen]->subdirlen = sublen; + subdir[subdirlen]->elemslen = curelem - elemstart; + subdir[subdirlen]->elems = subelems; + subdirlen++; + + curelem--; /* move back, see below */ + + /* modify the last entry to be the subdir, such that we + * can let the code below synchronise with dentries */ + elems[curelem][2 + skippath + sublen] = ' '; + entry = elems[curelem] + 2 + skippath; + etpe = 'S'; /* flag this was a subdir */ + } + + /* does this entry exist in list? */ + if (*entry == '\0') { + /* end of list reached, force dir to catch up */ + cmp = 1; + } else { + slash = strchr(entry, ' '); + if (slash != NULL) + *slash = '\0'; + cmp = strcmp(entry, dentries[curdentry]); + if (slash != NULL) + *slash = ' '; + } + if (cmp == 0) { + /* equal, so yay */ + if (etpe == 'D') { + ret |= verify_file(dir, entry, mfest, msgs); + } + /* else this is I(GNORE) or S(ubdir), which means it is + * ok in any way (M shouldn't happen) */ + curelem++; + curdentry++; + } else if (cmp < 0) { + /* entry is missing from dir */ + if (etpe == 'I') { + /* right, we can ignore this */ + } else { + ret |= 1; + slash = strchr(entry, ' '); + if (slash != NULL) + *slash = '\0'; + msgs_add(msgs, mfest, entry, "%s file listed in Manifest, " + "but not found", etpe == 'M' ? "MANIFEST" : "DATA"); + if (slash != NULL) + *slash = ' '; + failed_files++; + } + curelem++; + } else if (cmp > 0) { + /* dir has extra element */ + ret |= 1; + msgs_add(msgs, mfest, NULL, + "file not listed: %s", dentries[curdentry]); + curdentry++; + failed_files++; + } + } + + while (dentrieslen-- > 0) + free(dentries[dentrieslen]); + free(dentries); + +#pragma omp parallel for shared(ret) private(entry, etpe, slash) + for (elem = 0; elem < subdirlen; elem++) { + char ndir[8192]; + + entry = subdir[elem]->elems[0] + 2 + skippath; + etpe = subdir[elem]->elems[0][0]; + + /* restore original entry format */ + subdir[elem]->elems[subdir[elem]->elemslen - 1] + [2 + skippath + subdir[elem]->subdirlen] = '/'; + + if (etpe == 'M') { + size_t skiplen = strlen(dir) + 1 + subdir[elem]->subdirlen; + /* sub-Manifest, we need to do a proper recurse */ + slash = strrchr(entry, '/'); /* cannot be NULL */ + snprintf(ndir, sizeof(ndir), "%s/%s", dir, entry); + ndir[skiplen] = '\0'; + slash = strchr(ndir + skiplen + 1, ' '); + if (slash != NULL) /* path should fit in ndir ... */ + *slash = '\0'; + if (verify_file(dir, entry, mfest, msgs) != 0 || + verify_manifest(ndir, ndir + skiplen + 1, msgs) != 0) + ret |= 1; + } else { + snprintf(ndir, sizeof(ndir), "%s/%.*s", dir, + (int)subdir[elem]->subdirlen, entry); + ret |= verify_dir(ndir, subdir[elem]->elems, + subdir[elem]->elemslen, + skippath + subdir[elem]->subdirlen + 1, mfest, msgs); + } + + free(subdir[elem]); + } + + if (subdir) + free(subdir); + + return ret; + } else { + return 1; + } +} + +char +verify_manifest( + const char *dir, + const char *manifest, + verify_msg **msgs) +{ + char buf[8192]; + FILE *f; + gzFile mf; + char ret = 0; + + size_t elemssize = 0; + size_t elemslen = 0; + char **elems = NULL; +#define append_list(STR) \ + if (strncmp(STR, "TIMESTAMP ", 10) != 0 || strncmp(STR, "DIST ", 5) != 0) {\ + char *endp = STR + strlen(STR) - 1;\ + while (isspace(*endp))\ + *endp-- = '\0';\ + if (elemslen == elemssize) {\ + elemssize += LISTSZ;\ + elems = xrealloc(elems, elemssize * sizeof(elems[0]));\ + }\ + if (strncmp(STR, "IGNORE ", 7) == 0) {\ + STR[5] = 'I';\ + elems[elemslen] = xstrdup(STR + 5);\ + elemslen++;\ + } else if (strncmp(STR, "MANIFEST ", 9) == 0) {\ + STR[7] = 'M';\ + elems[elemslen] = xstrdup(STR + 7);\ + elemslen++;\ + } else if (strncmp(STR, "DATA ", 5) == 0 ||\ + strncmp(STR, "MISC ", 5) == 0 ||\ + strncmp(STR, "EBUILD ", 7) == 0)\ + {\ + if (*STR == 'E') {\ + STR[5] = 'D';\ + elems[elemslen] = xstrdup(STR + 5);\ + } else {\ + STR[3] = 'D';\ + elems[elemslen] = xstrdup(STR + 3);\ + }\ + elemslen++;\ + } else if (strncmp(STR, "AUX ", 4) == 0) {\ + /* translate directly into what it is: DATA in files/ */\ + size_t slen = strlen(STR + 2) + sizeof("files/");\ + elems[elemslen] = xmalloc(slen);\ + snprintf(elems[elemslen], slen, "D files/%s", STR + 4);\ + elemslen++;\ + }\ + } + + snprintf(buf, sizeof(buf), "%s/%s", dir, manifest); + if (strcmp(manifest, str_manifest) == 0) { + if ((f = fopen(buf, "r")) == NULL) { + msgs_add(msgs, buf, NULL, "failed to open %s: %s\n", + manifest, strerror(errno)); + return 1; + } + while (fgets(buf, sizeof(buf), f) != NULL) { + append_list(buf); + } + fclose(f); + } else if (strcmp(manifest, str_manifest_files_gz) == 0 || + strcmp(manifest, str_manifest_gz) == 0) + { + if ((mf = gzopen(buf, "rb9")) == NULL) { + msgs_add(msgs, buf, NULL, "failed to open %s: %s\n", + manifest, strerror(errno)); + return 1; + } + while (gzgets(mf, buf, sizeof(buf)) != NULL) { + append_list(buf); + } + gzclose(mf); + } + + /* The idea: + * - Manifest without MANIFEST entries, we need to scan the entire + * subtree + * - Manifest with MANIFEST entries, assume they are just one level + * deeper, thus ignore that subdir, further like above + * - Manifest at top-level, needs to be igored as it only points to + * the larger Manifest.files.gz + */ + qsort(elems, elemslen, sizeof(elems[0]), compare_elems); + snprintf(buf, sizeof(buf), "%s/%s", dir, manifest); + ret = verify_dir(dir, elems, elemslen, 0, buf + 2, msgs); + checked_manifests++; + + while (elemslen-- > 0) + free(elems[elemslen]); + free(elems); + + return ret; +} + +char * +verify_timestamp(const char *ts) +{ + char buf[8192]; + FILE *f; + char *ret = NULL; + + if ((f = fopen(ts, "r")) != NULL) { + while (fgets(buf, sizeof(buf), f) != NULL) { + if (strncmp(buf, "TIMESTAMP ", 10) == 0) { + char *endp = buf + strlen(buf) - 1; + while (isspace(*endp)) + *endp-- = '\0'; + ret = xstrdup(buf + 10); + break; + } + } + fclose(f); + } + + return ret; +} + +static void +format_line(const char *pfx, const char *msg, int twidth) +{ + size_t msglen = strlen(pfx) + strlen(msg); + + if (*pfx == '-') { + fprintf(stdout, "%s%s%s%s\n", pfx, RED, msg, NORM); + } else { + if (!verbose && msglen > (size_t)twidth) { + int to_remove = 3 + (msglen - twidth); + int first_half = msglen / 2 - to_remove / 2; + int remainder = msglen / 2 + (to_remove + 1) / 2; + fprintf(stdout, "%s%.*s...%s\n", + pfx, first_half, msg, msg + remainder); + } else { + fprintf(stdout, "%s%s\n", pfx, msg); + } + } +} + +static const char * +process_dir_vrfy(const char *dir) +{ + char buf[8192]; + int newhashes; + const char *ret = NULL; + struct timeval startt; + struct timeval finisht; + double etime; + int curdirfd; + char *timestamp; + verify_msg topmsg; + verify_msg *walk = &topmsg; + gpg_sig *gs; + struct winsize winsz; + int twidth = 80; + + ioctl(0, TIOCGWINSZ, &winsz); + if (winsz.ws_col > 0) + twidth = (int)winsz.ws_col; + + gettimeofday(&startt, NULL); + + fprintf(stdout, "verifying %s...\n", dir); + snprintf(buf, sizeof(buf), "%s/metadata/layout.conf", dir); + if ((newhashes = parse_layout_conf(buf)) != 0) { + hashes = newhashes; + } else { + return "verification must be done on a full tree"; + } + + if ((curdirfd = open(".", O_RDONLY)) < 0) { + fprintf(stderr, "cannot open current directory?!? %s\n", + strerror(errno)); + } + if (chdir(dir) != 0) { + fprintf(stderr, "cannot chdir() to %s: %s\n", dir, strerror(errno)); + return "not a directory"; + } + + if ((gs = verify_gpg_sig(str_manifest, &walk)) == NULL) { + ret = "gpg signature invalid"; + } else { + fprintf(stdout, + "%s%s%s signature made %s by\n" + "%s\n" + "primary key fingerprint %s\n" + "%4s subkey fingerprint %s\n", + gs->isgood ? GREEN : RED, + gs->isgood ? "good": "BAD", + NORM, gs->timestamp, + gs->signer, + gs->pkfingerprint, + gs->algo, gs->fingerprint); + if (!gs->isgood) + fprintf(stdout, "reason: %s%s%s\n", RED, gs->reason, NORM); + free(gs->algo); + free(gs->fingerprint); + free(gs->timestamp); + free(gs->signer); + free(gs->pkfingerprint); + if (!gs->isgood) + free(gs->reason); + free(gs); + } + + if ((timestamp = verify_timestamp(str_manifest)) != NULL) { + fprintf(stdout, "%s timestamp: %s\n", str_manifest, timestamp); + free(timestamp); + } else { + ret = "manifest timestamp entry missing"; + } + + /* verification goes like this: + * - verify the signature of the top-level Manifest file (done + * above) + * - read the contents of the Manifest file, and process the + * entries - verify them, check there are no files which shouldn't + * be there + * - recurse into directories for which Manifest files are defined + */ + walk->next = NULL; + if (verify_manifest(".\0", str_manifest, &walk) != 0) + ret = "manifest verification failed"; + + { + char *mfest; + char *ebuild; + char *msg; + char *lastmfest = (char *)"-"; + char *lastebuild = (char *)"-"; + char *msgline; + const char *pfx; + verify_msg *next; + + for (walk = topmsg.next; walk != NULL; walk = walk->next) { + mfest = walk->msg; + ebuild = strchr(mfest, ':'); + if (ebuild != NULL) { + *ebuild++ = '\0'; + msg = strchr(ebuild, ':'); + if (msg != NULL) + *msg++ = '\0'; + } + if (ebuild != NULL && msg != NULL) { + if (strcmp(mfest, lastmfest) != 0 || + strcmp(ebuild, lastebuild) != 0) + { + char *mycat = mfest; + char *mypkg = NULL; + + if ((mfest = strchr(mycat, '/')) != NULL) { + *mfest++ = '\0'; + mypkg = mfest; + if ((mfest = strchr(mypkg, '/')) != NULL) { + *mfest++ = '\0'; + } else { + mfest = mypkg; + mypkg = NULL; + } + } else { + mfest = mycat; + mycat = NULL; + } + + fprintf(stdout, "%s%s%s" "%s%s%s%s" "%s%s" "%s%s%s%s\n", + mycat == NULL ? "" : BOLD, + mycat == NULL ? "" : mycat, + mycat == NULL ? "" : "/", + mypkg == NULL ? "" : BLUE, + mypkg == NULL ? "" : mypkg, + mypkg == NULL ? "" : NORM, + mypkg == NULL ? "" : "/", + mfest, *ebuild == '\0' ? ":" : "::", + CYAN, ebuild, NORM, *ebuild == '\0' ? "" : ":"); + } + + lastmfest = mfest; + lastebuild = ebuild; + + pfx = "- "; + msgline = msg; + while ((msgline = strchr(msgline, '\n')) != NULL) { + *msgline++ = '\0'; + format_line(pfx, msg, twidth); + pfx = " "; + msg = msgline; + } + format_line(pfx, msg, twidth); + } + } + + walk = topmsg.next; + while (walk != NULL) { + next = walk->next; + free(walk); + walk = next; + } + } + + gettimeofday(&finisht, NULL); + + /* return to where we were before we called this function */ + fchdir(curdirfd); + close(curdirfd); + + etime = ((double)((finisht.tv_sec - startt.tv_sec) * 1000000 + + finisht.tv_usec) - (double)startt.tv_usec) / 1000000.0; + printf("checked %zd Manifests, %zd files, %zd failures in %.02fs\n", + checked_manifests, checked_files, failed_files, etime); + return ret; +} + +int +qmanifest_main(int argc, char **argv) +{ + char *prog; + const char *(*runfunc)(const char *); + int ret = 0; + const char *rsn; + bool isdir = false; + bool isoverlay = false; + char *overlay; + size_t n; + int i; + + if ((prog = strrchr(argv[0], '/')) == NULL) { + prog = argv[0]; + } else { + prog++; + } + if (*prog == 'q') + prog++; + + runfunc = NULL; + if (strcmp(prog, "hashverify") == 0) { + runfunc = process_dir_vrfy; + } else if (strcmp(prog, "hashgen") == 0) { + runfunc = process_dir_gen; + } + + while ((ret = GETOPT_LONG(QMANIFEST, qmanifest, "")) != -1) { + switch (ret) { + COMMON_GETOPTS_CASES(qmanifest) + case 'g': runfunc = process_dir_gen; break; + case 'd': isdir = true; break; + case 'o': isoverlay = true; break; + } + } + + if (isdir && isoverlay) { + warn("cannot specify both directories (-d) and overlays (-o), " + "continuing using overlays only"); + isdir = false; + } + + if (runfunc == NULL) + /* default mode: verify */ + runfunc = process_dir_vrfy; + + gpgme_check_version(NULL); + + if (isoverlay || (!isdir && !isoverlay)) { + char buf[_Q_PATH_MAX]; + char *repo; + size_t repolen; + + array_for_each(overlays, n, overlay) { + repo = xarrayget(overlay_names, n); + if (strcmp(repo, "") == 0) { + snprintf(buf, sizeof(buf), "%s/profiles/repo_name", overlay); + if (eat_file(buf, &repo, &repolen)) { + free(array_get_elem(overlays, n)); + array_get_elem(overlays, n) = repo; + } + } + } + } + + argc -= optind; + argv += optind; + for (i = 0; i < argc; i++) { + array_for_each(overlay_names, n, overlay) { + if (strcmp(overlay, argv[i]) == 0) { + overlay = xarrayget(overlays, n); + break; + } + overlay = NULL; + } + + /* behaviour: + * if isdir is set: treat argument as directory + * if isoverlay is set: treat argument as overlay (look it up) + * if neither is set: treat argument as overlay if it exists, + * else as directory */ + + if (isoverlay && overlay == NULL) { + warn("no such overlay: %s", argv[i]); + continue; + } + if (isdir || (!isoverlay && overlay == NULL)) /* !isdir && !isoverlay */ + overlay = argv[i]; + + rsn = runfunc(overlay); + if (rsn != NULL) { + printf("%s%s%s\n", RED, rsn, NORM); + ret |= 1; + } + } + + if (i == 0) { + rsn = runfunc(main_overlay); + if (rsn != NULL) { + printf("%s%s%s\n", RED, rsn, NORM); + ret |= 1; + } + } + + return ret; +} + +#endif