public inbox for gentoo-commits@lists.gentoo.org
 help / color / mirror / Atom feed
* [gentoo-commits] proj/portage-utils:master commit in: man/, man/include/, libq/, /
@ 2019-05-21 14:11 Fabian Groffen
  0 siblings, 0 replies; only message in thread
From: Fabian Groffen @ 2019-05-21 14:11 UTC (permalink / raw
  To: gentoo-commits

commit:     d16f69568285d76dfa8a367f2458bc16f9e049e4
Author:     Fabian Groffen <grobian <AT> gentoo <DOT> org>
AuthorDate: Mon May 20 17:53:48 2019 +0000
Commit:     Fabian Groffen <grobian <AT> gentoo <DOT> 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 <grobian <AT> 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,         "<applet> <args>", "virtual applet"},
 	{"qatom",     qatom_main,     "<pkg>",           "split atom strings"},
-	{"qkeyword",  qkeyword_main,  "<action> <args>", "list packages based on keywords"},
 	{"qcheck",    qcheck_main,    "<pkgname>",       "verify integrity of installed packages"},
 	{"qdepends",  qdepends_main,  "<pkgname>",       "show dependency info"},
 	{"qfile",     qfile_main,     "<filename>",      "list all pkgs owning files"},
@@ -74,16 +74,18 @@ static const struct applet_t {
 	{"qglsa",     qglsa_main,     "<action> <list>", "check GLSAs against system"},
 	*/
 	{"qgrep",     qgrep_main,     "<expr> [pkg ...]", "grep in ebuilds"},
+	{"qkeyword",  qkeyword_main,  "<action> <args>", "list packages based on keywords"},
 	{"qlist",     qlist_main,     "<pkgname>",       "list files owned by pkgname"},
 	{"qlop",      qlop_main,      "<pkgname>",       "emerge log analyzer"},
+	{"qmanifest", qmanifest_main, "<misc args>",     "verify or generate thick Manifest files"},
 	{"qmerge",    qmerge_main,    "<pkgnames>",      "fetch and merge binary package"},
 	{"qpkg",      qpkg_main,      "<misc args>",     "manipulate Gentoo binpkgs"},
 	{"qsearch",   qsearch_main,   "<regex>",         "search pkgname/desc"},
 	{"qsize",     qsize_main,     "<pkgname>",       "calculate size usage"},
 	{"qtbz2",     qtbz2_main,     "<misc args>",     "manipulate tbz2 packages"},
+	{"qtegrity",  qtegrity_main,  "<misc args>",     "verify files with IMA"},
 	{"quse",      quse_main,      "<useflag>",       "find pkgs using useflags"},
 	{"qxpak",     qxpak_main,     "<misc args>",     "manipulate xpak archives"},
-	{"qtegrity",  qtegrity_main,  "<misc args>",     "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  - <grobian@gentoo.org>
+ *
+ * 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 <openssl/sha.h>
+#include <openssl/whrlpool.h>
+#endif
+#ifdef HAVE_BLAKE2B
+#include <blake2.h>
+#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  - <grobian@gentoo.org>
+ */
+
+#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] <misc args>\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<arg>\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 <solar@gentoo.org>
+Mike Frysinger <vapier@gentoo.org>
+Fabian Groffen <grobian@gentoo.org>
+.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  - <grobian@gentoo.org>
+ *
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <fcntl.h>
+#include <ctype.h>
+#include <dirent.h>
+#include <time.h>
+#include <errno.h>
+#include <termios.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/time.h>
+#include <sys/ioctl.h>
+#include <openssl/sha.h>
+#include <openssl/whrlpool.h>
+#include <blake2.h>
+#include <zlib.h>
+#include <gpgme.h>
+
+#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 <SIZE> <HASHTYPE HASH ...>
+	 * 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, "<PORTDIR>") == 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


^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2019-05-21 14:12 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2019-05-21 14:11 [gentoo-commits] proj/portage-utils:master commit in: man/, man/include/, libq/, / Fabian Groffen

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox