Blob Blame History Raw
From e38a26f5d6050a5c72f7503c8664ca11623ed319 Mon Sep 17 00:00:00 2001
From: Matteo Croce <teknoraver@meta.com>
Date: Fri, 6 Dec 2024 07:45:17 +0100
Subject: [PATCH] RPM with Copy on Write

This is part of https://fedoraproject.org/wiki/Changes/RPMCoW

The majority of changes are in two new programs:

= rpm2extents

Modeled as a 'stream processor'. It reads a regular .rpm file on stdin,
and produces a modified .rpm file on stdout. The lead, signature and
headers are preserved 1:1 to allow all the normal metadata inspection,
signature verification to work as expected. Only the 'payload' is
modified.

The primary motivation for this tool is to re-organize the payload as a
sequence of raw file extents (hence the name). The files are organized
by their digest identity instead of path/filename. If any digest is
repeated, then the file is skipped/de-duped. Only regular files are
represented. All other entries like directories, symlinks, devices are
fully described in the headers and are omitted.

The files are padded so they start on `sysconf(_SC_PAGESIZE)` boundries
to permit 'reflink' syscalls to work in the `reflink` plugin.

At the end of the file is a footer with 3 sections:

1. List of calculated digests of the input stream. This is used in
   `librepo` because the file *written* is a derivative, and not the
   same as the repo metadata describes. `rpm2extents` takes one or more
   positional arguments that described which digest algorithms are
   desired. This is often just `SHA256`. This program is only measuring
   and recording the digest - it does not express an opinion on whether
   the file is correct. Due to the API on most compression libraries
   directly reading the source file, the whole file digest is measured
   using a subprocess and pipes. I don't love it, but it works.
2. Sorted List of file content digests + offset pairs. This is used in
   the plugin with a trivial binary search to locate the start of file
   content. The size is not needed because it's part of normal headers.
3. (offset of 1., offset of 2., 8 byte MAGIC value) triple

= reflink plugin

Looks for the 8 byte magic value at the end of the rpm file. If present
it alters the `RPMTAG_PAYLOADFORMAT` in memory to `clon`, and reads in
the digest-> offset table.

`rpmPackageFilesInstall()` in `fsm.c` is
modified to alter the enumeration strategy from
`rpmfiNewArchiveReader()` to `rpmfilesIter()` if not `cpio`. This is
needed because there is no cpio to enumerate. In the same function, if
`rpmpluginsCallFsmFilePre()` returns `RPMRC_PLUGIN_CONTENTS` then
`fsmMkfile()` is skipped as it is assumed the plugin did the work.

The majority of the work is in `reflink_fsm_file_pre()` - the per file
hook for RPM plugins. If the file enumerated in
`rpmPackageFilesInstall()` is a regular file, this function will look up
the offset in the digest->offset table and will try to reflink it, then
fall back to a regular copy. If reflinking does work: we will have
reflinked a whole number of pages, so we truncate the file to the
expected size. Therefore installing most files does involve two writes:
the reflink of the full size, then a fork/copy on write for the last
page worth.

If the file passed to `reflink_fsm_file_pre()` is anything other than a
regular file, it return `RPMRC_OK` so the normal mechanics of
`rpmPackageFilesInstall()` are used. That handles directories, symlinks
and other non file types.

= New API for internal use

1. `rpmReadPackageRaw()` is used within `rpm2extents` to read all the
   headers without trying to validate signatures. This eliminates the
   runtime dependency on rpmdb.
2. `rpmteFd()` exposes the Fd behind the rpmte, so plugins can interact
   with the rpm itself.
3. `RPMRC_PLUGIN_CONTENTS` in `rpmRC_e` for use in
   `rpmpluginsCallFsmFilePre()` specifically.
4. `pgpStringVal()` is used to help parse the command line in
   `rpm2extents` - the positional arguments are strings, and this
   converts the values back to the values in the table.

Nothing has been removed, and none of the changes are intended to be
used externally, so I don't think a soname bump is warranted here.

Co-authored-by: Matthew Almond <malmond@meta.com>
---
 build/pack.c                      |   2 +-
 include/rpm/rpmcli.h              |  10 +
 include/rpm/rpmextents_internal.h |  58 +++
 include/rpm/rpmlib.h              |   9 +
 include/rpm/rpmpgp.h              |   9 +
 include/rpm/rpmte.h               |   2 +
 include/rpm/rpmtypes.h            |   3 +-
 lib/CMakeLists.txt                |   1 +
 lib/fsm.c                         |  45 +-
 lib/package.c                     |  36 ++
 lib/rpmchecksig.c                 | 116 ++++-
 lib/rpmextents.c                  | 109 +++++
 lib/rpmlead.c                     |  43 +-
 lib/rpmlead.h                     |  37 +-
 lib/rpmplugin.h                   |   9 +
 lib/rpmplugins.c                  |  94 +++-
 lib/rpmplugins.h                  |  17 +
 lib/rpmte.c                       |   5 +
 lib/transaction.c                 |  29 +-
 macros.in                         |   1 +
 plugins/CMakeLists.txt            |   4 +-
 plugins/reflink.c                 | 401 +++++++++++++++++
 rpmio/rpmpgp.c                    |  28 ++
 rpmio/rpmpgp_internal.c           |  18 -
 scripts/CMakeLists.txt            |   2 +-
 scripts/rpm2extents_dump          |  94 ++++
 tests/CMakeLists.txt              |   1 +
 tests/atlocal.in                  |  21 +
 tests/rpm2extents.at              | 151 +++++++
 tests/rpmtests.at                 |   1 +
 tools/CMakeLists.txt              |   5 +-
 tools/rpm2extents.c               | 707 ++++++++++++++++++++++++++++++
 32 files changed, 1978 insertions(+), 90 deletions(-)
 create mode 100644 include/rpm/rpmextents_internal.h
 create mode 100644 lib/rpmextents.c
 create mode 100644 plugins/reflink.c
 create mode 100755 scripts/rpm2extents_dump
 create mode 100644 tests/rpm2extents.at
 create mode 100644 tools/rpm2extents.c

diff --git a/build/pack.c b/build/pack.c
index f7dac6d..c38b1a9 100644
--- a/build/pack.c
+++ b/build/pack.c
@@ -495,7 +495,7 @@ static rpmRC writeRPM(Package pkg, unsigned char ** pkgidp,
     }
 
     /* Write the lead section into the package. */
-    if (rpmLeadWrite(fd, pkg->header)) {
+    if (rpmLeadWriteFromHeader(fd, pkg->header)) {
 	rpmlog(RPMLOG_ERR, _("Unable to write package: %s\n"), Fstrerror(fd));
 	goto exit;
     }
diff --git a/include/rpm/rpmcli.h b/include/rpm/rpmcli.h
index 3e5900d..09274e3 100644
--- a/include/rpm/rpmcli.h
+++ b/include/rpm/rpmcli.h
@@ -421,6 +421,16 @@ int rpmcliImportPubkeys(rpmts ts, ARGV_const_t argv);
  */
 int rpmcliVerifySignatures(rpmts ts, ARGV_const_t argv);
 
+
+/** \ingroup rpmcli
+ * Verify package signatures.
+ * @param ts		transaction set
+ * @param fd		a file descriptor to verify
+ * @param msg		a string containing textual information about the verification, similar to rpmcliVerifySignatures output.
+ * @return		0 on success
+ */
+int rpmcliVerifySignaturesFD(rpmts ts, FD_t fd, char **msg);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/include/rpm/rpmextents_internal.h b/include/rpm/rpmextents_internal.h
new file mode 100644
index 0000000..0a3318c
--- /dev/null
+++ b/include/rpm/rpmextents_internal.h
@@ -0,0 +1,58 @@
+#ifndef _RPMEXTENTS_INTERNAL_H
+#define _RPMEXTENTS_INTERNAL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdint.h>
+
+/** \ingroup rpmextents
+ * RPM extents library
+ */
+
+/* magic value at end of file (64 bits) that indicates this is a transcoded
+ * rpm.
+ */
+#define EXTENTS_MAGIC 3472329499408095051
+
+typedef uint64_t extents_magic_t;
+
+struct __attribute__ ((__packed__)) extents_footer_offsets_t {
+    off64_t checksig_offset;
+    off64_t table_offset;
+    off64_t csum_offset;
+};
+
+struct __attribute__ ((__packed__)) extents_footer_t {
+    struct extents_footer_offsets_t offsets;
+    extents_magic_t magic;
+};
+
+/** \ingroup rpmextents
+ * Checks the results of the signature verification ran during transcoding.
+ * @param fd	The FD_t of the transcoded RPM
+ * @param print_content Whether or not to print the result from rpmsig
+ * @return	The number of checks that `rpmvsVerify` failed during transcoding.
+ */
+int extentsVerifySigs(FD_t fd, int print_content);
+
+/** \ingroup rpmextents
+ * Read the RPM Extents footer from a file descriptor.
+ * @param fd		The FD_t of the transcoded RPM
+ * @param[out] footer	A pointer to an allocated extents_footer_t with a copy of the footer.
+ * @return		RPMRC_OK on success, RPMRC_NOTFOUND if not a transcoded file, RPMRC_FAIL on any failure.
+ */
+rpmRC extentsFooterFromFD(FD_t fd, struct extents_footer_t *footer);
+
+/** \ingroup rpmextents
+ * Check if a RPM is a transcoded RPM
+ * @param fd	The FD_t of the transcoded RPM
+ * return	RPMRC_OK on success, RPMRC_NOTFOUND if not a transcoded file, RPMRC_FAIL on any failure.
+ */
+rpmRC isTranscodedRpm(FD_t fd);
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* _RPMEXTENTS_INTERNAL_H */
diff --git a/include/rpm/rpmlib.h b/include/rpm/rpmlib.h
index db38397..b11f87f 100644
--- a/include/rpm/rpmlib.h
+++ b/include/rpm/rpmlib.h
@@ -155,6 +155,15 @@ rpmRC rpmReadHeader(rpmts ts, FD_t fd, Header *hdrp, char ** msg);
 rpmRC rpmReadPackageFile(rpmts ts, FD_t fd,
 		const char * fn, Header * hdrp);
 
+/** \ingroup header
+ * Return package signature, header from file handle, no verification.
+ * @param fd		file handle
+ * @param[out] sigp		address of header (or NULL)
+ * @param[out] hdrp		address of header (or NULL)
+ * @return		RPMRC_OK on success
+ */
+rpmRC rpmReadPackageRaw(FD_t fd, Header * sigp, Header * hdrp);
+
 /** \ingroup rpmtrans
  * Install source package.
  * @param ts		transaction set
diff --git a/include/rpm/rpmpgp.h b/include/rpm/rpmpgp.h
index a7eecbe..24e9617 100644
--- a/include/rpm/rpmpgp.h
+++ b/include/rpm/rpmpgp.h
@@ -962,6 +962,15 @@ typedef enum pgpValType_e {
  */
 const char * pgpValString(pgpValType type, uint8_t val);
 
+/** \ingroup rpmpgp
+ * Return  OpenPGP value for a string.
+ * @param type		type of value
+ * @param str		string to lookup
+ * @param[out] val  byte value associated with string
+ * @return		0 on success else -1
+ */
+int pgpStringVal(pgpValType type, const char *str, uint8_t *val);
+
 /** \ingroup rpmpgp
  * Return (native-endian) integer from big-endian representation.
  * @param s		pointer to big-endian integer
diff --git a/include/rpm/rpmte.h b/include/rpm/rpmte.h
index effcdab..499fd4f 100644
--- a/include/rpm/rpmte.h
+++ b/include/rpm/rpmte.h
@@ -209,6 +209,8 @@ const char * rpmteNEVR(rpmte te);
  */
 const char * rpmteNEVRA(rpmte te);
 
+FD_t rpmteFd(rpmte te);
+
 /** \ingroup rpmte
  * Retrieve key from transaction element.
  * @param te		transaction element
diff --git a/include/rpm/rpmtypes.h b/include/rpm/rpmtypes.h
index 7c14553..552ec52 100644
--- a/include/rpm/rpmtypes.h
+++ b/include/rpm/rpmtypes.h
@@ -106,7 +106,8 @@ typedef	enum rpmRC_e {
     RPMRC_NOTFOUND	= 1,	/*!< Generic not found code. */
     RPMRC_FAIL		= 2,	/*!< Generic failure code. */
     RPMRC_NOTTRUSTED	= 3,	/*!< Signature is OK, but key is not trusted. */
-    RPMRC_NOKEY		= 4	/*!< Public key is unavailable. */
+    RPMRC_NOKEY		= 4,	/*!< Public key is unavailable. */
+    RPMRC_PLUGIN_CONTENTS = 5     /*!< fsm_file_pre plugin is handling content */
 } rpmRC;
 
 #ifdef __cplusplus
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
index 2528a64..754a198 100644
--- a/lib/CMakeLists.txt
+++ b/lib/CMakeLists.txt
@@ -41,6 +41,7 @@ target_sources(librpm PRIVATE
 	rpmchroot.c rpmchroot.h
 	rpmplugins.c rpmplugins.h rpmplugin.h rpmug.c rpmug.h
 	rpmtriggers.h rpmtriggers.c rpmvs.c rpmvs.h
+	rpmextents.c
 )
 
 if(ENABLE_SQLITE)
diff --git a/lib/fsm.c b/lib/fsm.c
index 36708ac..2df7db7 100644
--- a/lib/fsm.c
+++ b/lib/fsm.c
@@ -869,6 +869,24 @@ static rpmfi fsmIterFini(rpmfi fi, struct diriter_s *di)
     return rpmfiFree(fi);
 }
 
+static int fiIterator(rpmPlugins plugins, FD_t payload, rpmfiles files, rpmfi *fi)
+{
+    rpmRC plugin_rc = rpmpluginsCallFsmFileArchiveReader(plugins, payload, files, fi);
+    switch (plugin_rc) {
+	case RPMRC_PLUGIN_CONTENTS:
+	    if (*fi == NULL)
+                return RPMERR_BAD_MAGIC;
+            return RPMRC_OK;
+	case RPMRC_OK:
+	    *fi = rpmfiNewArchiveReader(payload, files, RPMFI_ITER_READ_ARCHIVE);
+	    if (*fi == NULL)
+                return RPMERR_BAD_MAGIC;
+            return RPMRC_OK;
+	default:
+            return RPMRC_FAIL;
+    }
+}
+
 int rpmPackageFilesInstall(rpmts ts, rpmte te, rpmfiles files,
               rpmpsm psm, char ** failedFile)
 {
@@ -920,8 +938,9 @@ int rpmPackageFilesInstall(rpmts ts, rpmte te, rpmfiles files,
     if (rc)
 	goto exit;
 
-    fi = fsmIter(payload, files,
-		 payload ? RPMFI_ITER_READ_ARCHIVE : RPMFI_ITER_FWD, &di);
+    rc = fiIterator(plugins, payload, files, &fi);
+    if (rc)
+        goto exit;
 
     if (fi == NULL) {
         rc = RPMERR_BAD_MAGIC;
@@ -944,6 +963,9 @@ int rpmPackageFilesInstall(rpmts ts, rpmte te, rpmfiles files,
         if (!fp->skip) {
 	    int mayopen = 0;
 	    int fd = -1;
+
+	    if (di.dirfd >= 0)
+		fsmClose(&di.dirfd);
 	    rc = ensureDir(plugins, rpmfiDN(fi), 0,
 			    (fp->action == FA_CREATE), 0, &di.dirfd);
 
@@ -953,9 +975,10 @@ int rpmPackageFilesInstall(rpmts ts, rpmte te, rpmfiles files,
 	    }
 
 	    /* Run fsm file pre hook for all plugins */
-	    if (!rc)
+	    if (!rc) {
 		rc = rpmpluginsCallFsmFilePre(plugins, fi, fp->fpath,
 					      fp->sb.st_mode, fp->action);
+	    }
 	    if (rc)
 		goto setmeta; /* for error notification */
 
@@ -983,11 +1006,18 @@ int rpmPackageFilesInstall(rpmts ts, rpmte te, rpmfiles files,
 	    if (fp->action == FA_TOUCH)
 		goto setmeta;
 
-            if (S_ISREG(fp->sb.st_mode)) {
+	    rpmRC plugin_rc = rpmpluginsCallFsmFileInstall(plugins, fi, fp->fpath, fp->sb.st_mode, fp->action);
+	    if (!(plugin_rc == RPMRC_PLUGIN_CONTENTS || plugin_rc == RPMRC_OK)){
+		rc = plugin_rc;
+	    } else if(plugin_rc == RPMRC_PLUGIN_CONTENTS){
+		rc = RPMRC_OK;
+		/* The reflink plugins handles hardlink differently, metadata has to be set. */
+		fp->setmeta = 1;
+	    } else if (S_ISREG(fp->sb.st_mode)) {
 		if (rc == RPMERR_ENOENT) {
 		    rc = fsmMkfile(di.dirfd, fi, fp, files, psm, nodigest,
-				   &firstlink, &firstlinkfile, &di.firstdir,
-				   &fd);
+				   &firstlink, &firstlinkfile,
+				   &di.firstdir, &fd);
 		}
             } else if (S_ISDIR(fp->sb.st_mode)) {
                 if (rc == RPMERR_ENOENT) {
@@ -1056,10 +1086,13 @@ setmeta:
 
     /* If all went well, commit files to final destination */
     fi = fsmIter(NULL, files, RPMFI_ITER_FWD, &di);
+
     while (!rc && (fx = rpmfiNext(fi)) >= 0) {
 	struct filedata_s *fp = &fdata[fx];
 
 	if (!fp->skip) {
+	    if (di.dirfd >= 0)
+		fsmClose(&di.dirfd);
 	    if (!rc)
 		rc = ensureDir(NULL, rpmfiDN(fi), 0, 0, 0, &di.dirfd);
 
diff --git a/lib/package.c b/lib/package.c
index 8eee368..d4256a7 100644
--- a/lib/package.c
+++ b/lib/package.c
@@ -394,5 +394,41 @@ exit:
     return rc;
 }
 
+rpmRC rpmReadPackageRaw(FD_t fd, Header * sigp, Header * hdrp)
+{
+    char *msg = NULL;
+    hdrblob sigblob = hdrblobCreate();
+    hdrblob blob = hdrblobCreate();
+    Header h = NULL;
+    Header sigh = NULL;
+
+    rpmRC rc = hdrblobRead(fd, 1, 0, RPMTAG_HEADERSIGNATURES, sigblob, &msg);
+    if (rc != RPMRC_OK)
+	goto exit;
+
+    rc = hdrblobRead(fd, 1, 1, RPMTAG_HEADERIMMUTABLE, blob, &msg);
+    if (rc != RPMRC_OK)
+	goto exit;
+
+    rc = hdrblobImport(sigblob, 0, &sigh, &msg);
+    if (rc)
+	goto exit;
+
+    rc = hdrblobImport(blob, 0, &h, &msg);
+    if (rc)
+	goto exit;
+
+    *sigp = headerLink(sigh);
+    *hdrp = headerLink(h);
 
+exit:
+    if (rc != RPMRC_OK && msg)
+	rpmlog(RPMLOG_ERR, "%s: %s\n", Fdescr(fd), msg);
+    hdrblobFree(sigblob);
+    hdrblobFree(blob);
+    headerFree(sigh);
+    headerFree(h);
+    free(msg);
 
+    return rc;
+}
diff --git a/lib/rpmchecksig.c b/lib/rpmchecksig.c
index 3a3a4bd..8257dd9 100644
--- a/lib/rpmchecksig.c
+++ b/lib/rpmchecksig.c
@@ -16,6 +16,7 @@
 #include <rpm/rpmlog.h>
 #include <rpm/rpmstring.h>
 #include <rpm/rpmkeyring.h>
+#include <rpm/rpmextents_internal.h>
 
 #include "rpmio_internal.h" 	/* fdSetBundle() */
 #include "rpmlead.h"
@@ -208,36 +209,24 @@ exit:
 }
 
 static int rpmpkgVerifySigs(rpmKeyring keyring, int vfylevel, rpmVSFlags flags,
-			   FD_t fd, const char *fn)
+			   FD_t fd, rpmsinfoCb cb, void *cbdata)
 {
     char *msg = NULL;
-    struct vfydata_s vd = { .seen = 0,
-			    .bad = 0,
-			    .verbose = rpmIsVerbose(),
-    };
     int rc;
-    struct rpmvs_s *vs = rpmvsCreate(vfylevel, flags, keyring);
 
-    rpmlog(RPMLOG_NOTICE, "%s:%s", fn, vd.verbose ? "\n" : "");
+
+    if(isTranscodedRpm(fd) == RPMRC_OK){
+	return extentsVerifySigs(fd, 1);
+    }
+
+    struct rpmvs_s *vs = rpmvsCreate(vfylevel, flags, keyring);
 
     rc = rpmpkgRead(vs, fd, NULL, NULL, &msg);
 
     if (rc)
 	goto exit;
 
-    rc = rpmvsVerify(vs, RPMSIG_VERIFIABLE_TYPE, vfyCb, &vd);
-
-    if (!vd.verbose) {
-	if (vd.seen & RPMSIG_DIGEST_TYPE) {
-	    rpmlog(RPMLOG_NOTICE, " %s", (vd.bad & RPMSIG_DIGEST_TYPE) ?
-					_("DIGESTS") : _("digests"));
-	}
-	if (vd.seen & RPMSIG_SIGNATURE_TYPE) {
-	    rpmlog(RPMLOG_NOTICE, " %s", (vd.bad & RPMSIG_SIGNATURE_TYPE) ?
-					_("SIGNATURES") : _("signatures"));
-	}
-	rpmlog(RPMLOG_NOTICE, " %s\n", rc ? _("NOT OK") : _("OK"));
-    }
+    rc = rpmvsVerify(vs, RPMSIG_VERIFIABLE_TYPE, cb, cbdata);
 
 exit:
     if (rc && msg)
@@ -247,15 +236,39 @@ exit:
     return rc;
 }
 
+static void rpmkgVerifySigsPreLogging(struct vfydata_s *vd, const char *fn){
+    rpmlog(RPMLOG_NOTICE, "%s:%s", fn, vd->verbose ? "\n" : "");
+}
+
+static void rpmkgVerifySigsPostLogging(struct vfydata_s *vd, int rc){
+    if (!vd->verbose) {
+	if (vd->seen & RPMSIG_DIGEST_TYPE) {
+	    rpmlog(RPMLOG_NOTICE, " %s", (vd->bad & RPMSIG_DIGEST_TYPE) ?
+					_("DIGESTS") : _("digests"));
+	}
+	if (vd->seen & RPMSIG_SIGNATURE_TYPE) {
+	    rpmlog(RPMLOG_NOTICE, " %s", (vd->bad & RPMSIG_SIGNATURE_TYPE) ?
+					_("SIGNATURES") : _("signatures"));
+	}
+	rpmlog(RPMLOG_NOTICE, " %s\n", rc ? _("NOT OK") : _("OK"));
+    }
+}
+
 /* Wrapper around rpmkVerifySigs to preserve API */
 int rpmVerifySignatures(QVA_t qva, rpmts ts, FD_t fd, const char * fn)
 {
     int rc = 1; /* assume failure */
+    struct vfydata_s vd = { .seen = 0,
+			    .bad = 0,
+			    .verbose = rpmIsVerbose(),
+    };
     if (ts && qva && fd && fn) {
 	rpmKeyring keyring = rpmtsGetKeyring(ts, 1);
 	rpmVSFlags vsflags = rpmtsVfyFlags(ts);
 	int vfylevel = rpmtsVfyLevel(ts);
-	rc = rpmpkgVerifySigs(keyring, vfylevel, vsflags, fd, fn);
+	rpmkgVerifySigsPreLogging(&vd, fn);
+	rc = rpmpkgVerifySigs(keyring, vfylevel, vsflags, fd, vfyCb, &vd);
+	rpmkgVerifySigsPostLogging(&vd, rc);
     	rpmKeyringFree(keyring);
     }
     return rc;
@@ -277,12 +290,22 @@ int rpmcliVerifySignatures(rpmts ts, ARGV_const_t argv)
 
     while ((arg = *argv++) != NULL) {
 	FD_t fd = Fopen(arg, "r.ufdio");
+	struct vfydata_s vd = { .seen = 0,
+				.bad = 0,
+				.verbose = rpmIsVerbose(),
+	};
 	if (fd == NULL || Ferror(fd)) {
 	    rpmlog(RPMLOG_ERR, _("%s: open failed: %s\n"), 
 		     arg, Fstrerror(fd));
 	    res++;
-	} else if (rpmpkgVerifySigs(keyring, vfylevel, vsflags, fd, arg)) {
+	} else {
+	    rpmkgVerifySigsPreLogging(&vd, arg);
+	    int rc = rpmpkgVerifySigs(keyring, vfylevel, vsflags, fd,
+				      vfyCb, &vd);
+	    rpmkgVerifySigsPostLogging(&vd, rc);
+	    if (rc) {
 	    res++;
+	    }
 	}
 
 	Fclose(fd);
@@ -290,3 +313,52 @@ int rpmcliVerifySignatures(rpmts ts, ARGV_const_t argv)
     rpmKeyringFree(keyring);
     return res;
 }
+
+struct vfydatafd_s {
+    size_t len;
+    char msg[BUFSIZ];
+};
+
+
+static int vfyFDCb(struct rpmsinfo_s *sinfo, void *cbdata)
+{
+    struct vfydatafd_s *vd = cbdata;
+    char *vmsg, *msg;
+    size_t n;
+    size_t remainder = BUFSIZ - vd->len >= 0 ? BUFSIZ - vd->len : 0;
+
+    vmsg = rpmsinfoMsg(sinfo);
+    rasprintf(&msg, "    %s\n", vmsg);
+    n = rstrlcpy(vd->msg + vd->len, msg, remainder);
+    free(vmsg);
+    free(msg);
+    if(n <= remainder){
+	vd->len += n;
+    }
+    return 1;
+}
+
+
+int rpmcliVerifySignaturesFD(rpmts ts, FD_t fdi, char **msg)
+{
+    rpmRC rc = RPMRC_FAIL;
+    rpmKeyring keyring = rpmtsGetKeyring(ts, 1);
+    rpmVSFlags vsflags = rpmtsVfyFlags(ts);
+    int vfylevel = rpmtsVfyLevel(ts);
+    struct vfydatafd_s vd = {.len = 0};
+
+    vsflags |= rpmcliVSFlags;
+    if (rpmcliVfyLevelMask) {
+	vfylevel &= ~rpmcliVfyLevelMask;
+	rpmtsSetVfyLevel(ts, vfylevel);
+    }
+
+    if (!rpmpkgVerifySigs(keyring, vfylevel, vsflags, fdi, vfyFDCb, &vd)) {
+	rc = RPMRC_OK;
+    }
+    *msg = strdup(vd.msg);
+
+    rpmKeyringFree(keyring);
+    return rc;
+}
+
diff --git a/lib/rpmextents.c b/lib/rpmextents.c
new file mode 100644
index 0000000..ef687d6
--- /dev/null
+++ b/lib/rpmextents.c
@@ -0,0 +1,109 @@
+
+#include "system.h"
+
+#include <rpm/rpmlog.h>
+#include <rpm/rpmio.h>
+#include <rpm/rpmextents_internal.h>
+#include <string.h>
+#include <errno.h>
+
+
+
+int extentsVerifySigs(FD_t fd, int print_content){
+    rpm_loff_t current;
+    int32_t rc;
+    size_t len;
+    uint64_t content_len;
+    char *content = NULL;
+    struct extents_footer_t footer;
+
+    current = Ftell(fd);
+
+    if(extentsFooterFromFD(fd, &footer) != RPMRC_OK) {
+	rc = -1;
+	goto exit;
+    }
+    if(Fseek(fd, footer.offsets.checksig_offset, SEEK_SET) < 0) {
+	rpmlog(RPMLOG_ERR, _("extentsVerifySigs: Failed to seek signature verification offset\n"));
+	rc = -1;
+	goto exit;
+    }
+    len = sizeof(rc);
+    if (Fread(&rc, len, 1, fd) != len) {
+	rpmlog(RPMLOG_ERR, _("extentsVerifySigs: Failed to read Signature Verification RC\n"));
+	rc = -1;
+	goto exit;
+    }
+
+    if(print_content) {
+	len = sizeof(content_len);
+	if (Fread(&content_len, len, 1, fd) != len) {
+	    rpmlog(RPMLOG_ERR, _("extentsVerifySigs: Failed to read signature content length\n"));
+	    goto exit;
+	}
+
+	content = rmalloc(content_len + 1);
+	if(content == NULL) {
+	    rpmlog(RPMLOG_ERR, _("extentsVerifySigs: Failed to allocate memory to read signature content\n"));
+	    goto exit;
+	}
+	content[content_len] = 0;
+	if (Fread(content, content_len, 1, fd) != content_len) {
+	    rpmlog(RPMLOG_ERR, _("extentsVerifySigs: Failed to read signature content\n"));
+	    goto exit;
+	}
+
+	rpmlog(RPMLOG_NOTICE, "%s", content);
+    }
+exit:
+    if(content){
+	rfree(content);
+    }
+    if (Fseek(fd, current, SEEK_SET) < 0) {
+	rpmlog(RPMLOG_ERR, _("extentsVerifySigs: unable to seek back to original location\n"));
+    }
+    return rc;
+
+}
+
+rpmRC extentsFooterFromFD(FD_t fd, struct extents_footer_t *footer) {
+
+    rpmRC rc = RPMRC_NOTFOUND;
+    rpm_loff_t current;
+    size_t len;
+
+    // If the file is not seekable, we cannot detect whether or not it is transcoded.
+    if(Fseek(fd, 0, SEEK_CUR) < 0) {
+        return RPMRC_FAIL;
+    }
+    current = Ftell(fd);
+
+    len = sizeof(struct extents_footer_t);
+    if(Fseek(fd, -len, SEEK_END) < 0) {
+	rc = RPMRC_FAIL;
+	goto exit;
+    }
+    if (Fread(footer, len, 1, fd) != len) {
+	rpmlog(RPMLOG_ERR, _("isTranscodedRpm: unable to read footer\n"));
+	rc = RPMRC_FAIL;
+	goto exit;
+    }
+    if (footer->magic != EXTENTS_MAGIC) {
+	rc = RPMRC_NOTFOUND;
+	goto exit;
+    }
+    rc = RPMRC_OK;
+exit:
+    if (Fseek(fd, current, SEEK_SET) < 0) {
+	rpmlog(RPMLOG_ERR, _("isTranscodedRpm: unable to seek back to original location\n"));
+	rc = RPMRC_FAIL;
+    }
+    return rc;
+}
+
+rpmRC isTranscodedRpm(FD_t fd) {
+    struct extents_footer_t footer;
+    return extentsFooterFromFD(fd, &footer);
+}
+
+
diff --git a/lib/rpmlead.c b/lib/rpmlead.c
index d0db60a..e4504d2 100644
--- a/lib/rpmlead.c
+++ b/lib/rpmlead.c
@@ -24,24 +24,6 @@ static unsigned char const lead_magic[] = {
     RPMLEAD_MAGIC0, RPMLEAD_MAGIC1, RPMLEAD_MAGIC2, RPMLEAD_MAGIC3
 };
 
-/** \ingroup lead
- * The lead data structure.
- * The lead needs to be 8 byte aligned.
- * @deprecated The lead (except for signature_type) is legacy.
- * @todo Don't use any information from lead.
- */
-struct rpmlead_s {
-    unsigned char magic[4];
-    unsigned char major;
-    unsigned char minor;
-    short type;
-    short archnum;
-    char name[66];
-    short osnum;
-    short signature_type;       /*!< Signature header type (RPMSIG_HEADERSIG) */
-    char reserved[16];      /*!< Pad to 96 bytes -- 8 byte aligned! */
-};
-
 static int rpmLeadFromHeader(Header h, struct rpmlead_s *l)
 {
     if (h != NULL) {
@@ -70,13 +52,23 @@ static int rpmLeadFromHeader(Header h, struct rpmlead_s *l)
 }
 
 /* The lead needs to be 8 byte aligned */
-rpmRC rpmLeadWrite(FD_t fd, Header h)
+rpmRC rpmLeadWriteFromHeader(FD_t fd, Header h)
 {
     rpmRC rc = RPMRC_FAIL;
     struct rpmlead_s l;
 
-    if (rpmLeadFromHeader(h, &l)) {
-	
+    if (rpmLeadFromHeader(h, &l)) {	
+	rc = rpmLeadWrite(fd, l);
+    }
+
+    return rc;
+}
+
+/* The lead needs to be 8 byte aligned */
+rpmRC rpmLeadWrite(FD_t fd, struct rpmlead_s l)
+{
+    rpmRC rc = RPMRC_FAIL;
+
 	l.type = htons(l.type);
 	l.archnum = htons(l.archnum);
 	l.osnum = htons(l.osnum);
@@ -84,7 +76,6 @@ rpmRC rpmLeadWrite(FD_t fd, Header h)
 	    
 	if (Fwrite(&l, 1, sizeof(l), fd) == sizeof(l))
 	    rc = RPMRC_OK;
-    }
 
     return rc;
 }
@@ -99,6 +90,11 @@ static rpmRC rpmLeadCheck(struct rpmlead_s *lead, char **msg)
 }
 
 rpmRC rpmLeadRead(FD_t fd, char **emsg)
+{
+    return rpmLeadReadAndReturn(fd, emsg, NULL);
+}
+
+rpmRC rpmLeadReadAndReturn(FD_t fd, char **emsg, struct rpmlead_s * ret)
 {
     rpmRC rc = RPMRC_OK;
     struct rpmlead_s l;
@@ -128,5 +124,8 @@ rpmRC rpmLeadRead(FD_t fd, char **emsg)
 	    free(err);
     }
 
+	if (ret)
+		*ret = l;
+
     return rc;
 }
diff --git a/lib/rpmlead.h b/lib/rpmlead.h
index 80db44e..4ba2ba9 100644
--- a/lib/rpmlead.h
+++ b/lib/rpmlead.h
@@ -19,13 +19,39 @@ extern "C" {
 
 #define RPMLEAD_SIZE 96         /*!< Don't rely on sizeof(struct) */
 
+/** \ingroup lead
+ * The lead data structure.
+ * The lead needs to be 8 byte aligned.
+ * @deprecated The lead (except for signature_type) is legacy.
+ * @todo Don't use any information from lead.
+ */
+struct rpmlead_s {
+    unsigned char magic[4];
+    unsigned char major;
+    unsigned char minor;
+    short type;
+    short archnum;
+    char name[66];
+    short osnum;
+    short signature_type;       /*!< Signature header type (RPMSIG_HEADERSIG) */
+    char reserved[16];      /*!< Pad to 96 bytes -- 8 byte aligned! */
+};
+
 /** \ingroup lead
  * Write lead to file handle.
  * @param fd		file handle
  * @param h		package header
  * @return		RPMRC_OK on success, RPMRC_FAIL on error
  */
-rpmRC rpmLeadWrite(FD_t fd, Header h);
+rpmRC rpmLeadWriteFromHeader(FD_t fd, Header h);
+
+/** \ingroup lead
+ * Write lead to file handle.
+ * @param fd		file handle
+ * @param l		lead
+ * @return		RPMRC_OK on success, RPMRC_FAIL on error
+ */
+rpmRC rpmLeadWrite(FD_t fd, struct rpmlead_s l);
 
 /** \ingroup lead
  * Read lead from file handle.
@@ -35,6 +61,15 @@ rpmRC rpmLeadWrite(FD_t fd, Header h);
  */
 rpmRC rpmLeadRead(FD_t fd, char **emsg);
 
+/** \ingroup lead
+ * Read lead from file handle and return it.
+ * @param fd		file handle
+ * @param[out] emsg		failure message on error (malloced)
+ * @param[out] ret		address of lead
+ * @return		RPMRC_OK on success, RPMRC_FAIL/RPMRC_NOTFOUND on error
+ */
+rpmRC rpmLeadReadAndReturn(FD_t fd, char **emsg, struct rpmlead_s * ret);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/lib/rpmplugin.h b/lib/rpmplugin.h
index fab4b3e..c82d6be 100644
--- a/lib/rpmplugin.h
+++ b/lib/rpmplugin.h
@@ -60,6 +60,13 @@ typedef rpmRC (*plugin_fsm_file_prepare_func)(rpmPlugin plugin, rpmfi fi,
 					      int fd, const char* path,
 					      const char *dest,
 					      mode_t file_mode, rpmFsmOp op);
+typedef rpmRC (*plugin_fsm_file_install_func)(rpmPlugin plugin, rpmfi fi,
+					      const char* path,
+					      mode_t file_mode, rpmFsmOp op);
+typedef rpmRC (*plugin_fsm_file_archive_reader_func)(rpmPlugin plugin,
+						     FD_t payload,
+						     rpmfiles files, rpmfi *fi);
+
 
 typedef struct rpmPluginHooks_s * rpmPluginHooks;
 struct rpmPluginHooks_s {
@@ -80,6 +87,8 @@ struct rpmPluginHooks_s {
     plugin_fsm_file_pre_func		fsm_file_pre;
     plugin_fsm_file_post_func		fsm_file_post;
     plugin_fsm_file_prepare_func	fsm_file_prepare;
+    plugin_fsm_file_install_func	fsm_file_install;
+    plugin_fsm_file_archive_reader_func	fsm_file_archive_reader;
 };
 
 #ifdef __cplusplus
diff --git a/lib/rpmplugins.c b/lib/rpmplugins.c
index 62e806b..24abe96 100644
--- a/lib/rpmplugins.c
+++ b/lib/rpmplugins.c
@@ -364,14 +364,29 @@ rpmRC rpmpluginsCallFsmFilePre(rpmPlugins plugins, rpmfi fi, const char *path,
     plugin_fsm_file_pre_func hookFunc;
     int i;
     rpmRC rc = RPMRC_OK;
+    rpmRC hook_rc;
     char *apath = abspath(fi, path);
 
     for (i = 0; i < plugins->count; i++) {
 	rpmPlugin plugin = plugins->plugins[i];
 	RPMPLUGINS_SET_HOOK_FUNC(fsm_file_pre);
-	if (hookFunc && hookFunc(plugin, fi, apath, file_mode, op) == RPMRC_FAIL) {
-	    rpmlog(RPMLOG_ERR, "Plugin %s: hook fsm_file_pre failed\n", plugin->name);
-	    rc = RPMRC_FAIL;
+	if (hookFunc) {
+	    hook_rc = hookFunc(plugin, fi, apath, file_mode, op);
+	    if (hook_rc == RPMRC_FAIL) {
+		rpmlog(RPMLOG_ERR, "Plugin %s: hook fsm_file_pre failed\n", plugin->name);
+		rc = RPMRC_FAIL;
+	    } else if (hook_rc == RPMRC_PLUGIN_CONTENTS && rc != RPMRC_FAIL) {
+		if (rc == RPMRC_PLUGIN_CONTENTS) {
+		    /* Another plugin already said it'd handle contents. It's
+		     * undefined how these would combine, so treat this as a
+		     * failure condition.
+		    */
+		    rc = RPMRC_FAIL;
+		} else {
+		    /* Plugin will handle content */
+		    rc = RPMRC_PLUGIN_CONTENTS;
+		}
+	    }
 	}
     }
     free(apath);
@@ -420,3 +435,76 @@ rpmRC rpmpluginsCallFsmFilePrepare(rpmPlugins plugins, rpmfi fi,
 
     return rc;
 }
+
+rpmRC rpmpluginsCallFsmFileInstall(rpmPlugins plugins, rpmfi fi,
+				   const char *path, mode_t file_mode,
+				   rpmFsmOp op)
+{
+    plugin_fsm_file_install_func hookFunc;
+    int i;
+    rpmRC rc = RPMRC_OK;
+    rpmRC hook_rc;
+    char *apath = abspath(fi, path);
+
+    for (i = 0; i < plugins->count; i++) {
+	rpmPlugin plugin = plugins->plugins[i];
+	RPMPLUGINS_SET_HOOK_FUNC(fsm_file_install);
+	if (hookFunc) {
+	    hook_rc = hookFunc(plugin, fi, apath, file_mode, op);
+	    if (hook_rc == RPMRC_FAIL) {
+		rpmlog(RPMLOG_ERR, "Plugin %s: hook fsm_file_install failed\n", plugin->name);
+		rc = RPMRC_FAIL;
+	    } else if (hook_rc == RPMRC_PLUGIN_CONTENTS && rc != RPMRC_FAIL) {
+		if (rc == RPMRC_PLUGIN_CONTENTS) {
+		    /* Another plugin already said it'd handle contents. It's
+		     * undefined how these would combine, so treat this as a
+		     * failure condition.
+		    */
+		    rc = RPMRC_FAIL;
+		} else {
+		    /* Plugin will handle content */
+		    rc = RPMRC_PLUGIN_CONTENTS;
+		}
+	    }
+	}
+    }
+    free(apath);
+
+    return rc;
+}
+
+rpmRC rpmpluginsCallFsmFileArchiveReader(rpmPlugins plugins, FD_t payload,
+				   rpmfiles files, rpmfi *fi)
+{
+    plugin_fsm_file_archive_reader_func hookFunc;
+    int i;
+    rpmRC rc = RPMRC_OK;
+    rpmRC hook_rc;
+
+    for (i = 0; i < plugins->count; i++) {
+	rpmPlugin plugin = plugins->plugins[i];
+	RPMPLUGINS_SET_HOOK_FUNC(fsm_file_archive_reader);
+	if (hookFunc) {
+	    hook_rc = hookFunc(plugin, payload, files, fi);
+	    if (hook_rc == RPMRC_FAIL) {
+		rpmlog(RPMLOG_ERR, "Plugin %s: hook fsm_file_archive_reader failed\n", plugin->name);
+		rc = RPMRC_FAIL;
+	    } else if (hook_rc == RPMRC_PLUGIN_CONTENTS && rc != RPMRC_FAIL) {
+		if (rc == RPMRC_PLUGIN_CONTENTS) {
+		    /* Another plugin already said it'd handle contents. It's
+		     * undefined how these would combine, so treat this as a
+		     * failure condition.
+		    */
+		    rc = RPMRC_FAIL;
+		} else {
+		    /* Plugin will handle content */
+		    rc = RPMRC_PLUGIN_CONTENTS;
+		}
+	    }
+	}
+    }
+
+    return rc;
+}
+
+
diff --git a/lib/rpmplugins.h b/lib/rpmplugins.h
index 287a302..a00f156 100644
--- a/lib/rpmplugins.h
+++ b/lib/rpmplugins.h
@@ -168,6 +168,23 @@ rpmRC rpmpluginsCallFsmFilePrepare(rpmPlugins plugins, rpmfi fi,
                                    int fd, const char *path, const char *dest,
                                    mode_t mode, rpmFsmOp op);
 
+/** \ingroup rpmplugins
+ * Call the fsm file install plugin hook
+ * @param plugins	plugins structure
+ * @param fi		file info iterator (or NULL)
+ * @param path		file object path
+ * @param file_mode	file object mode
+ * @param op		file operation + associated flags
+ * @return		RPMRC_OK on success, RPMRC_FAIL otherwise
+ */
+RPM_GNUC_INTERNAL
+rpmRC rpmpluginsCallFsmFileInstall(rpmPlugins plugins, rpmfi fi,
+				   const char* path, mode_t file_mode,
+				   rpmFsmOp op);
+
+RPM_GNUC_INTERNAL
+rpmRC rpmpluginsCallFsmFileArchiveReader(rpmPlugins plugins, FD_t payload,
+					 rpmfiles files, rpmfi *fi);
 #ifdef __cplusplus
 }
 #endif
diff --git a/lib/rpmte.c b/lib/rpmte.c
index d31152a..642e809 100644
--- a/lib/rpmte.c
+++ b/lib/rpmte.c
@@ -437,6 +437,11 @@ FD_t rpmteSetFd(rpmte te, FD_t fd)
     return NULL;
 }
 
+FD_t rpmteFd(rpmte te)
+{
+    return (te != NULL ? te->fd : NULL);
+}
+
 fnpyKey rpmteKey(rpmte te)
 {
     return (te != NULL ? te->key : NULL);
diff --git a/lib/transaction.c b/lib/transaction.c
index 70d2587..1e98f11 100644
--- a/lib/transaction.c
+++ b/lib/transaction.c
@@ -28,6 +28,7 @@
 #include <rpm/rpmstring.h>
 #include <rpm/rpmsq.h>
 #include <rpm/rpmkeyring.h>
+#include <rpm/rpmextents_internal.h>
 
 #include "fprint.h"
 #include "misc.h"
@@ -1292,19 +1293,25 @@ static int verifyPackageFiles(rpmts ts, rpm_loff_t total)
 
 	rpmtsNotify(ts, p, RPMCALLBACK_VERIFY_PROGRESS, oc++, total);
 	FD_t fd = rpmtsNotify(ts, p, RPMCALLBACK_INST_OPEN_FILE, 0, 0);
-	if (fd != NULL) {
-	    prc = rpmpkgRead(vs, fd, NULL, NULL, &vd.msg);
-	    rpmtsNotify(ts, p, RPMCALLBACK_INST_CLOSE_FILE, 0, 0);
+	if(fd != NULL && isTranscodedRpm(fd) == RPMRC_OK) {
+	    /* Transcoded RPMs are validated at transcoding time */
+	    prc = RPMRC_OK;
+	    verified = 1;
+	} else {
+	    if (fd != NULL) {
+		prc = rpmpkgRead(vs, fd, NULL, NULL, &vd.msg);
+		rpmtsNotify(ts, p, RPMCALLBACK_INST_CLOSE_FILE, 0, 0);
+	    }
+	    if (prc == RPMRC_OK)
+		prc = rpmvsVerify(vs, RPMSIG_VERIFIABLE_TYPE, vfyCb, &vd);
+
+	    /* Record verify result */
+	    if (vd.type[RPMSIG_SIGNATURE_TYPE] == RPMRC_OK)
+		verified |= RPMSIG_SIGNATURE_TYPE;
+	    if (vd.type[RPMSIG_DIGEST_TYPE] == RPMRC_OK)
+		verified |= RPMSIG_DIGEST_TYPE;
 	}
 
-	if (prc == RPMRC_OK)
-	    prc = rpmvsVerify(vs, RPMSIG_VERIFIABLE_TYPE, vfyCb, &vd);
-
-	/* Record verify result */
-	if (vd.type[RPMSIG_SIGNATURE_TYPE] == RPMRC_OK)
-	    verified |= RPMSIG_SIGNATURE_TYPE;
-	if (vd.type[RPMSIG_DIGEST_TYPE] == RPMRC_OK)
-	    verified |= RPMSIG_DIGEST_TYPE;
 	rpmteSetVerified(p, verified);
 
 	if (prc)
diff --git a/macros.in b/macros.in
index 7eb3d2b..df7defe 100644
--- a/macros.in
+++ b/macros.in
@@ -1188,6 +1188,7 @@ Supplements:   (%{name} = %{version}-%{release} and langpacks-%{1})\
 
 # Transaction plugin macros
 %__plugindir		%{_libdir}/rpm-plugins
+%__transaction_reflink		%{__plugindir}/reflink.so
 %__transaction_systemd_inhibit	%{__plugindir}/systemd_inhibit.so
 %__transaction_selinux		%{__plugindir}/selinux.so
 %__transaction_syslog		%{__plugindir}/syslog.so
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index c2c112b..ed63a43 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -1,5 +1,6 @@
 add_library(prioreset MODULE prioreset.c)
 add_library(syslog MODULE syslog.c)
+add_library(reflink MODULE reflink.c)
 
 if(WITH_SELINUX)
 	add_library(selinux MODULE selinux.c)
@@ -44,7 +45,6 @@ get_property(plugins DIRECTORY PROPERTY BUILDSYSTEM_TARGETS)
 foreach(plugin ${plugins})
 	target_link_libraries(${plugin} PRIVATE librpmio librpm ${Intl_LIBRARIES})
 	# XX this is wrong, but required while rpmplugin.h is there
-	target_include_directories(${plugin} PRIVATE ${CMAKE_SOURCE_DIR}/lib ${Intl_INCLUDE_DIRS})
+	target_include_directories(${plugin} PRIVATE ${CMAKE_SOURCE_DIR}/lib ${CMAKE_SOURCE_DIR}/rpmio ${Intl_INCLUDE_DIRS})
 	install(TARGETS ${plugin} DESTINATION ${plugindir})
 endforeach()
-
diff --git a/plugins/reflink.c b/plugins/reflink.c
new file mode 100644
index 0000000..7ce3a9f
--- /dev/null
+++ b/plugins/reflink.c
@@ -0,0 +1,401 @@
+#include "system.h"
+
+#include <errno.h>
+#include <sys/resource.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#if defined(__linux__)
+#include <linux/fs.h>        /* For FICLONE */
+#endif
+
+#include <rpm/rpmlog.h>
+#include <rpm/rpmlib.h>
+#include <rpm/rpmextents_internal.h>
+#include <rpm/rpmfileutil.h>
+#include "rpmplugin.h"
+#include "rpmte_internal.h"
+#include "rpmio_internal.h"
+
+
+#include "debug.h"
+
+#include <sys/ioctl.h>
+
+/* use hash table to remember inode -> ix (for rpmfilesFN(ix)) lookups */
+#undef HASHTYPE
+#undef HTKEYTYPE
+#undef HTDATATYPE
+#define HASHTYPE inodeIndexHash
+#define HTKEYTYPE rpm_ino_t
+#define HTDATATYPE const char *
+#include "rpmhash.H"
+#include "rpmhash.C"
+
+/* We use this in find to indicate a key wasn't found. This is an
+ * unrecoverable error, but we can at least show a decent error. 0 is never a
+ * valid offset because it's the offset of the start of the file.
+ */
+#define NOT_FOUND 0
+
+#define BUFFER_SIZE (1024 * 128)
+
+struct reflink_state_s {
+    /* Stuff that's used across rpms */
+    long fundamental_block_size;
+    char *buffer;
+
+    /* stuff that's used/updated per psm */
+    uint32_t keys, keysize;
+
+    /* table for current rpm, keys * (keysize + sizeof(rpm_loff_t)) */
+    unsigned char *table;
+    FD_t fd;
+    rpmfiles files;
+    inodeIndexHash inodeIndexes;
+    int transcoded;
+};
+
+typedef struct reflink_state_s * reflink_state;
+
+/*
+ * bsearch_r: implements a re-entrant version of stdlib's bsearch.
+ * code taken and adapted from /usr/include/bits/stdlib-bsearch.h
+ */
+static inline void *
+bsearch_r (const void *__key, const void *__base, size_t __nmemb, size_t __size,
+	 __compar_d_fn_t __compar, void *__arg)
+{
+  size_t __l, __u, __idx;
+  const void *__p;
+  int __comparison;
+
+  __l = 0;
+  __u = __nmemb;
+  while (__l < __u)
+    {
+      __idx = (__l + __u) / 2;
+      __p = (const void *) (((const char *) __base) + (__idx * __size));
+      __comparison = (*__compar) (__key, __p, __arg);
+      if (__comparison < 0)
+	__u = __idx;
+      else if (__comparison > 0)
+	__l = __idx + 1;
+      else
+	{
+#if __GNUC_PREREQ(4, 6)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wcast-qual"
+#endif
+	  return (void *) __p;
+#if __GNUC_PREREQ(4, 6)
+# pragma GCC diagnostic pop
+#endif
+	}
+    }
+
+  return NULL;
+}
+
+static int cmpdigest(const void *k1, const void *k2, void *data) {
+    rpmlog(RPMLOG_DEBUG, _("reflink: cmpdigest k1=%p k2=%p\n"), k1, k2);
+    return memcmp(k1, k2, *(int *)data);
+}
+
+static int inodeCmp(rpm_ino_t a, rpm_ino_t b)
+{
+    return (a != b);
+}
+
+static unsigned int inodeId(rpm_ino_t a)
+{
+    /* rpm_ino_t is uint32_t so maps safely to unsigned int */
+    return (unsigned int)a;
+}
+
+static rpmRC reflink_init(rpmPlugin plugin, rpmts ts) {
+    reflink_state state = rcalloc(1, sizeof(struct reflink_state_s));
+
+    /* IOCTL-FICLONERANGE(2): ...Disk filesystems generally require the offset
+     * and length arguments to be aligned to the fundamental block size.
+     *
+     * The value of "fundamental block size" is directly related to the
+     * system's page size, so we should use that.
+     */
+    state->fundamental_block_size = sysconf(_SC_PAGESIZE);
+    state->buffer = rcalloc(1, BUFFER_SIZE);
+    rpmPluginSetData(plugin, state);
+
+    return RPMRC_OK;
+}
+
+static void reflink_cleanup(rpmPlugin plugin) {
+    reflink_state state = rpmPluginGetData(plugin);
+    free(state->buffer);
+    free(state);
+}
+
+static rpmRC reflink_psm_pre(rpmPlugin plugin, rpmte te) {
+    rpmRC rc;
+    size_t len;
+
+    reflink_state state = rpmPluginGetData(plugin);
+    state->fd = rpmteFd(te);
+    if (state->fd == 0) {
+	rpmlog(RPMLOG_DEBUG, _("reflink: fd = 0, no install\n"));
+	return RPMRC_OK;
+    }
+
+    rpm_loff_t current = Ftell(state->fd);
+    rc = isTranscodedRpm(state->fd);
+
+    switch(rc){
+	// Fail to parse the file, fail the plugin.
+	case RPMRC_FAIL:
+	    return RPMRC_FAIL;
+	// This is not a transcoded file, do nothing.
+	case RPMRC_NOTFOUND:
+	    return RPMRC_OK;
+	default:
+	    break;
+    }
+    rpmlog(RPMLOG_DEBUG, _("reflink: *is* transcoded\n"));
+    state->transcoded = 1;
+
+    state->files = rpmteFiles(te);
+    /* tail of file contains offset_table, offset_checksums then magic */
+    if (Fseek(state->fd, -(sizeof(rpm_loff_t) * 2 + sizeof(extents_magic_t)), SEEK_END) < 0) {
+	rpmlog(RPMLOG_ERR, _("reflink: failed to seek for tail %p\n"),
+	       state->fd);
+	return RPMRC_FAIL;
+    }
+    rpm_loff_t table_start;
+    len = sizeof(table_start);
+    if (Fread(&table_start, len, 1, state->fd) != len) {
+	rpmlog(RPMLOG_ERR, _("reflink: unable to read table_start\n"));
+	return RPMRC_FAIL;
+    }
+    if (Fseek(state->fd, table_start, SEEK_SET) < 0) {
+	rpmlog(RPMLOG_ERR, _("reflink: unable to seek to table_start\n"));
+	return RPMRC_FAIL;
+    }
+    len = sizeof(state->keys);
+    if (Fread(&state->keys, len, 1, state->fd) != len) {
+	rpmlog(RPMLOG_ERR, _("reflink: unable to read number of keys\n"));
+	return RPMRC_FAIL;
+    }
+    len = sizeof(state->keysize);
+    if (Fread(&state->keysize, len, 1, state->fd) != len) {
+	rpmlog(RPMLOG_ERR, _("reflink: unable to read keysize\n"));
+	return RPMRC_FAIL;
+    }
+    rpmlog(
+	RPMLOG_DEBUG,
+	_("reflink: table_start=0x%lx, keys=%d, keysize=%d\n"),
+	table_start, state->keys, state->keysize
+    );
+    /* now get digest table if there is a reason to have one. */
+    if (state->keys == 0 || state->keysize == 0) {
+	/* no files (or no digests(!)) */
+	state->table = NULL;
+    } else {
+	int table_size = state->keys * (state->keysize + sizeof(rpm_loff_t));
+	state->table = rcalloc(1, table_size);
+	if (Fread(state->table, table_size, 1, state->fd) != table_size) {
+	    rpmlog(RPMLOG_ERR, _("reflink: unable to read table\n"));
+	    return RPMRC_FAIL;
+	}
+	state->inodeIndexes = inodeIndexHashCreate(
+	    state->keys, inodeId, inodeCmp, NULL, (inodeIndexHashFreeData)free
+	);
+    }
+
+    /* Seek back to original location.
+     * Might not be needed if we seek to offset immediately
+     */
+    if (Fseek(state->fd, current, SEEK_SET) < 0) {
+	rpmlog(RPMLOG_ERR,
+	       _("reflink: unable to seek back to original location\n"));
+	return RPMRC_FAIL;
+    }
+    return RPMRC_OK;
+}
+
+static rpmRC reflink_psm_post(rpmPlugin plugin, rpmte te, int res)
+{
+    reflink_state state = rpmPluginGetData(plugin);
+    state->files = rpmfilesFree(state->files);
+    if (state->table) {
+	free(state->table);
+	state->table = NULL;
+    }
+    if (state->inodeIndexes) {
+	inodeIndexHashFree(state->inodeIndexes);
+	state->inodeIndexes = NULL;
+    }
+    state->transcoded = 0;
+    return RPMRC_OK;
+}
+
+
+/* have a prototype, warnings system */
+rpm_loff_t find(const unsigned char *digest, reflink_state state);
+
+rpm_loff_t find(const unsigned char *digest, reflink_state state) {
+    rpmlog(RPMLOG_DEBUG,
+	   _("reflink: bsearch_r(key=%p, base=%p, nmemb=%d, size=%lu)\n"),
+	   digest, state->table, state->keys,
+	   state->keysize + sizeof(rpm_loff_t));
+    char *entry = bsearch_r(digest, state->table, state->keys,
+			    state->keysize + sizeof(rpm_loff_t), cmpdigest,
+			    &state->keysize);
+    if (entry == NULL) {
+	return NOT_FOUND;
+    }
+    rpm_loff_t offset = *(rpm_loff_t *)(entry + state->keysize);
+    return offset;
+}
+
+static rpmRC reflink_fsm_file_install(rpmPlugin plugin, rpmfi fi, const char* path,
+                                  mode_t file_mode, rpmFsmOp op)
+{
+    struct file_clone_range fcr;
+    rpm_loff_t size;
+    int dst, rc;
+    const char **hl_target = NULL;
+
+    reflink_state state = rpmPluginGetData(plugin);
+    if (state->table == NULL) {
+	/* no table means rpm is not in reflink format, so leave. Now. */
+	return RPMRC_OK;
+    }
+    if (op == FA_TOUCH) {
+	/* we're not overwriting an existing file. */
+	return RPMRC_OK;
+    }
+    fcr.dest_offset = 0;
+    if (S_ISREG(file_mode) && !(rpmfiFFlags(fi) & RPMFILE_GHOST)) {
+	rpm_ino_t inode = rpmfiFInode(fi);
+	/* check for hard link entry in table. GetEntry overwrites hlix with
+	 * the address of the first match.
+	 */
+	if (inodeIndexHashGetEntry(state->inodeIndexes, inode, &hl_target,
+				   NULL, NULL)) {
+	    /* entry is in table, use hard link */
+	    if (link(hl_target[0], path) != 0) {
+		rpmlog(RPMLOG_ERR,
+		       _("reflink: Unable to hard link %s -> %s due to %s\n"),
+		       hl_target[0], path, strerror(errno));
+		return RPMRC_FAIL;
+	    }
+	    return RPMRC_PLUGIN_CONTENTS;
+	}
+	/* if we didn't hard link, then we'll track this inode as being
+	 * created soon
+	 */
+	if (rpmfiFNlink(fi) > 1) {
+	    /* minor optimization: only store files with more than one link */
+	    inodeIndexHashAddEntry(state->inodeIndexes, inode, rstrdup(path));
+	}
+	/* derived from wfd_open in fsm.c */
+	mode_t old_umask = umask(0577);
+	dst = open(path, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR);
+	umask(old_umask);
+	if (dst == -1) {
+	    rpmlog(RPMLOG_ERR,
+		   _("reflink: Unable to open %s for writing due to %s, flags = %x\n"),
+		   path, strerror(errno), rpmfiFFlags(fi));
+	    return RPMRC_FAIL;
+	}
+	size = rpmfiFSize(fi);
+	if (size > 0) {
+	    /* round src_length down to fundamental_block_size multiple */
+	    fcr.src_length = size / state->fundamental_block_size * state->fundamental_block_size;
+	    if ((size % state->fundamental_block_size) > 0) {
+		/* round up to next fundamental_block_size. We expect the data
+		 * in the rpm to be similarly padded.
+		 */
+		fcr.src_length += state->fundamental_block_size;
+	    }
+	    fcr.src_fd = Fileno(state->fd);
+	    if (fcr.src_fd == -1) {
+		close(dst);
+		rpmlog(RPMLOG_ERR, _("reflink: src fd lookup failed\n"));
+		return RPMRC_FAIL;
+	    }
+	    fcr.src_offset = find(rpmfiFDigest(fi, NULL, NULL), state);
+	    if (fcr.src_offset == NOT_FOUND) {
+		close(dst);
+		rpmlog(RPMLOG_ERR, _("reflink: digest not found\n"));
+		return RPMRC_FAIL;
+	    }
+	    rpmlog(RPMLOG_DEBUG,
+	           _("reflink: Reflinking %llu bytes at %llu to %s orig size=%ld, file=%lld\n"),
+		   fcr.src_length, fcr.src_offset, path, size, fcr.src_fd);
+	    rc = ioctl(dst, FICLONERANGE, &fcr);
+	    if (rc) {
+		rpmlog(RPMLOG_WARNING,
+		       _("reflink: falling back to copying bits for %s due to %d, %d = %s\n"),
+		       path, rc, errno, strerror(errno));
+		if (Fseek(state->fd, fcr.src_offset, SEEK_SET) < 0) {
+		    close(dst);
+		    rpmlog(RPMLOG_ERR,
+			   _("reflink: unable to seek on copying bits\n"));
+		    return RPMRC_FAIL;
+		}
+		rpm_loff_t left = size;
+		size_t len, read, written;
+		while (left) {
+		    len = (left > BUFFER_SIZE ? BUFFER_SIZE : left);
+		    read = Fread(state->buffer, len, 1, state->fd);
+		    if (read != len) {
+			close(dst);
+			rpmlog(RPMLOG_ERR,
+			       _("reflink: short read on copying bits\n"));
+			return RPMRC_FAIL;
+		    }
+		    written = write(dst, state->buffer, len);
+		    if (read != written) {
+			close(dst);
+			rpmlog(RPMLOG_ERR,
+			       _("reflink: short write on copying bits\n"));
+			return RPMRC_FAIL;
+		    }
+		    left -= len;
+		}
+	    } else {
+		/* reflink worked, so truncate */
+		rc = ftruncate(dst, size);
+		if (rc) {
+		    rpmlog(RPMLOG_ERR,
+			   _("reflink: Unable to truncate %s to %ld due to %s\n"),
+			   path, size, strerror(errno));
+		     return RPMRC_FAIL;
+		}
+	    }
+	}
+	close(dst);
+	return RPMRC_PLUGIN_CONTENTS;
+    }
+    return RPMRC_OK;
+}
+
+static rpmRC reflink_fsm_file_archive_reader(rpmPlugin plugin, FD_t payload,
+					     rpmfiles files, rpmfi *fi) {
+    reflink_state state = rpmPluginGetData(plugin);
+    if(state->transcoded) {
+	*fi = rpmfilesIter(files, RPMFI_ITER_FWD);
+	return RPMRC_PLUGIN_CONTENTS;
+    }
+    return RPMRC_OK;
+}
+
+struct rpmPluginHooks_s reflink_hooks = {
+    .init = reflink_init,
+    .cleanup = reflink_cleanup,
+    .psm_pre = reflink_psm_pre,
+    .psm_post = reflink_psm_post,
+    .fsm_file_install = reflink_fsm_file_install,
+    .fsm_file_archive_reader = reflink_fsm_file_archive_reader,
+};
diff --git a/rpmio/rpmpgp.c b/rpmio/rpmpgp.c
index f545213..5b2ad78 100644
--- a/rpmio/rpmpgp.c
+++ b/rpmio/rpmpgp.c
@@ -81,3 +81,31 @@ pgpArmor pgpReadPkts(const char * fn, uint8_t ** pkt, size_t * pktlen)
     free(b);
     return ec;
 }
+
+/** \ingroup rpmpgp
+ * Return value of an OpenPGP string.
+ * @param vs		table of (string,value) pairs
+ * @param s		string token to lookup
+ * @param se		end-of-string address
+ * @return		byte value
+ */
+static inline
+int pgpValTok(pgpValTbl vs, const char * s, const char * se)
+{
+    do {
+	size_t vlen = strlen(vs->str);
+	if (vlen <= (se-s) && rstreqn(s, vs->str, vlen))
+	    break;
+    } while ((++vs)->val != -1);
+    return vs->val;
+}
+
+int pgpStringVal(pgpValType type, const char *str, uint8_t *val)
+{
+    pgpValTbl tbl = pgpValTable(type);
+    if (tbl == NULL) return -1;
+    int v = pgpValTok(tbl, str, str + strlen(str));
+    if (v == -1) return -1;
+    *val = (uint8_t)v;
+    return 0;
+}
diff --git a/rpmio/rpmpgp_internal.c b/rpmio/rpmpgp_internal.c
index 0e38946..0cfe9b9 100644
--- a/rpmio/rpmpgp_internal.c
+++ b/rpmio/rpmpgp_internal.c
@@ -88,24 +88,6 @@ static void pgpPrtTime(const char * pre, const uint8_t *p, size_t plen)
     }
 }
 
-/** \ingroup rpmpgp
- * Return value of an OpenPGP string.
- * @param vs		table of (string,value) pairs
- * @param s		string token to lookup
- * @param se		end-of-string address
- * @return		byte value
- */
-static inline
-int pgpValTok(pgpValTbl vs, const char * s, const char * se)
-{
-    do {
-	size_t vlen = strlen(vs->str);
-	if (vlen <= (se-s) && rstreqn(s, vs->str, vlen))
-	    break;
-    } while ((++vs)->val != -1);
-    return vs->val;
-}
-
 /** \ingroup rpmpgp
  * Decode length from 1, 2, or 5 octet body length encoding, used in
  * new format packet headers and V4 signature subpackets.
diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt
index 07749bc..8f8ae64 100644
--- a/scripts/CMakeLists.txt
+++ b/scripts/CMakeLists.txt
@@ -12,7 +12,7 @@ install(PROGRAMS
 	rpm_macros_provides.sh
 	rpmdb_dump rpmdb_load
 	rpm2cpio.sh tgpg
-	sysusers.sh
+	sysusers.sh rpm2extents_dump
 	DESTINATION ${RPM_CONFIGDIR}
 )
 install(FILES
diff --git a/scripts/rpm2extents_dump b/scripts/rpm2extents_dump
new file mode 100755
index 0000000..596a59a
--- /dev/null
+++ b/scripts/rpm2extents_dump
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+
+import argparse
+import binascii
+import os
+import struct
+import sys
+
+MAGIC_SIZE = 8
+MAGIC_STR = b'KWTSH100'
+
+POS_SIZE = 8
+
+def keep_position(func):
+    def wrapper(*args, **kwargs):
+        curr = args[0].tell()
+        res = func(*args, **kwargs)
+        f.seek(curr, os.SEEK_SET)
+        return res
+    return wrapper
+
+def read_validation_digest(f, validation_offset):
+	digests = []
+    # validation
+	f.seek(validation_offset, os.SEEK_SET)
+	val_content_len, val_digests_num = struct.unpack('=QI', f.read(8+4))
+	for i in range(val_digests_num):
+		algo_name_len, digest_len = struct.unpack('=II', f.read(8))
+		algo_name, digest = struct.unpack(f'{algo_name_len}s{digest_len}s', f.read(algo_name_len+digest_len))
+		digests.append((algo_name, binascii.hexlify(digest)))
+	return digests
+
+
+def read_digests_table(f, digest_offset):
+	digests = []
+    # validation
+	f.seek(digest_offset, os.SEEK_SET)
+	table_len, digest_len = struct.unpack('=II', f.read(8))
+
+	for i in range(table_len):
+		digest, pos = struct.unpack(f'{digest_len}sQ', f.read(digest_len + 8))
+		digests.append((pos, binascii.hexlify(digest)))
+	return digests
+
+def read_signature_output(f, signature_offset):
+    f.seek(signature_offset, os.SEEK_SET)
+    signature_rc, signature_output_len = struct.unpack('=IQ', f.read(12))
+    return signature_rc, f.read(signature_output_len)
+
+@keep_position
+def parse_file(f):
+	digests = []
+	pos_table_offset = f.seek(-8 - 3*POS_SIZE, os.SEEK_END)
+	signature_offset, digest_offset, validation_offset = struct.unpack('=QQQ', f.read(3*POS_SIZE))
+
+	validation_digests = read_validation_digest(f, validation_offset)
+	digests_table = read_digests_table(f, digest_offset)
+	signature_ouput = read_signature_output(f, signature_offset)
+
+	return validation_digests, digests_table, signature_ouput
+
+@keep_position
+def is_transcoded(f):
+    f.seek(-MAGIC_SIZE, os.SEEK_END)
+    magic = f.read(MAGIC_SIZE)
+    return magic == MAGIC_STR
+
+def arg_parse():
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--dump-signature', action='store_true')
+    parser.add_argument('--dump-file-digest-table', action='store_true')
+    parser.add_argument('--dump-digests', action='store_true')
+    parser.add_argument('file')
+
+    return parser.parse_args()
+
+if __name__ == '__main__':
+    args = arg_parse()
+    f = open(args.file, 'rb')
+    if not is_transcoded(f):
+        sys.exit(1)
+
+    validation_digests, digests_table, signature_output = parse_file(f)
+    if(args.dump_file_digest_table):
+        for digest in digests_table:
+            print(f"FileDigest {hex(digest[0])}: {digest[1]}")
+
+    if(args.dump_digests):
+        for validation_digest in validation_digests:
+            print(f"HeaderDigest {validation_digest[0]} {validation_digest[1]}")
+
+    if(args.dump_signature):
+        print(f"RPMSignOutput RC {signature_output[0]}\nRPMSignOutput Content {signature_output[1].decode()}")
+
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index a6afbc0..bee3b64 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -51,6 +51,7 @@ set(TESTSUITE_AT
 	rpmvfylevel.at
 	rpmpgp.at
 	rpmdevel.at
+	rpm2extents.at
 )
 
 set(TESTPROGS rpmpgpcheck rpmpgppubkeyfingerprint)
diff --git a/tests/atlocal.in b/tests/atlocal.in
index a5faeed..9930cb4 100644
--- a/tests/atlocal.in
+++ b/tests/atlocal.in
@@ -100,6 +100,19 @@ snapshot()
     esac
 }
 
+FSTYPE=$(stat -f -c %T /)
+REFLINKABLE_FS=("xfs" "brtfs")
+
+REFLINK_DISABLED=true;
+for item in "${REFLINKABLE_FS[@]}"
+do
+    if test "${FSTYPE}" = "${item}"
+    then
+	REFLINK_DISABLED=false;
+	break
+    fi
+done
+
 setup_env()
 {
     if [ -d tree ]; then
@@ -138,6 +151,14 @@ runroot()
              --nouserns
 }
 
+function runroot_plugins()
+{
+    setup_env
+    (unset RPM_CONFIGDIR RPM_POPTEXEC_PATH; cd ${RPMTEST} && \
+     MAGIC="/magic/magic" FAKECHROOT_BASE="${RPMTEST}" fakechroot "$@" --define "_buildhost testhost" --define "_topdir /build" --nouserns
+    )
+}
+
 runroot_other()
 {
     setup_env
diff --git a/tests/rpm2extents.at b/tests/rpm2extents.at
new file mode 100644
index 0000000..c9c79c5
--- /dev/null
+++ b/tests/rpm2extents.at
@@ -0,0 +1,151 @@
+#    rpm2extents.at: Some very basic checks
+#
+#    Copyright (C) 2022  Manu Bretelle <chantr4@gmail.com>
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+AT_BANNER([rpm2extents tests])
+
+# ------------------------------
+
+# check that transcoder write magic at the end
+AT_SETUP([rpm2extents magic])
+AT_KEYWORDS([rpm2extents])
+AT_CHECK([runroot_other cat /data/RPMS/hello-2.0-1.x86_64.rpm | runroot_other rpm2extents SHA256 | tail -c8],
+[0],
+[KWTSH100],
+[ignore])
+AT_CLEANUP
+
+# Check that transcoder writes checksig return code and content.
+#
+AT_SETUP([rpm2extents signature])
+AT_KEYWORDS([rpm2extents])
+AT_CHECK([
+RPMDB_INIT
+
+runroot_other cat /data/RPMS/hello-2.0-1.x86_64-signed.rpm | runroot_other rpm2extents SHA256 > /tmp/hello-2.0-1.x86_64-signed.rpm 2> /dev/null
+rpm2extents_dump --dump-signature /tmp/hello-2.0-1.x86_64-signed.rpm
+runroot rpmkeys --import /data/keys/rpm.org-rsa-2048-test.pub
+runroot_other cat /data/RPMS/hello-2.0-1.x86_64-signed.rpm | runroot_other rpm2extents SHA256 > /tmp/hello-2.0-1.x86_64-signed.rpm
+rpm2extents_dump --dump-signature /tmp/hello-2.0-1.x86_64-signed.rpm
+],
+[0],
+[RPMSignOutput RC 2
+RPMSignOutput Content     Header V4 RSA/SHA256 Signature, key ID 1964c5fc: NOKEY
+    Header SHA256 digest: OK
+    Header SHA1 digest: OK
+    Payload SHA256 digest: OK
+    V4 RSA/SHA256 Signature, key ID 1964c5fc: NOKEY
+    MD5 digest: OK
+
+RPMSignOutput RC 0
+RPMSignOutput Content     Header V4 RSA/SHA256 Signature, key ID 1964c5fc: OK
+    Header SHA256 digest: OK
+    Header SHA1 digest: OK
+    Payload SHA256 digest: OK
+    V4 RSA/SHA256 Signature, key ID 1964c5fc: OK
+    MD5 digest: OK
+
+],
+[])
+AT_CLEANUP
+
+AT_SETUP([rpm2extents signature verification])
+AT_KEYWORDS([rpm2extents])
+AT_CHECK([
+RPMDB_INIT
+
+runroot_other cat /data/RPMS/hello-2.0-1.x86_64-signed.rpm | runroot_other rpm2extents SHA256 > ${RPMTEST}/tmp/hello-2.0-1.x86_64-signed.rpm 2> /dev/null
+runroot rpmkeys -Kv /tmp/hello-2.0-1.x86_64-signed.rpm; echo $?
+runroot rpmkeys --import /data/keys/rpm.org-rsa-2048-test.pub
+runroot_other cat /data/RPMS/hello-2.0-1.x86_64-signed.rpm | runroot_other rpm2extents SHA256 > ${RPMTEST}/tmp/hello-2.0-1.x86_64-signed.rpm
+runroot rpmkeys -Kv /tmp/hello-2.0-1.x86_64-signed.rpm; echo $?
+],
+[0],
+[/tmp/hello-2.0-1.x86_64-signed.rpm:
+    Header V4 RSA/SHA256 Signature, key ID 1964c5fc: NOKEY
+    Header SHA256 digest: OK
+    Header SHA1 digest: OK
+    Payload SHA256 digest: OK
+    V4 RSA/SHA256 Signature, key ID 1964c5fc: NOKEY
+    MD5 digest: OK
+1
+/tmp/hello-2.0-1.x86_64-signed.rpm:
+    Header V4 RSA/SHA256 Signature, key ID 1964c5fc: OK
+    Header SHA256 digest: OK
+    Header SHA1 digest: OK
+    Payload SHA256 digest: OK
+    V4 RSA/SHA256 Signature, key ID 1964c5fc: OK
+    MD5 digest: OK
+0
+],
+[])
+AT_CLEANUP
+
+# check that package in denylist is not transcoded
+AT_SETUP([rpm2extents denylist])
+AT_KEYWORDS([rpm2extents])
+AT_CHECK([
+export LIBREPO_TRANSCODE_RPMS_DENYLIST="vim,hello,cowsay"
+runroot_other cat /data/RPMS/hello-2.0-1.x86_64.rpm | runroot_other rpm2extents SHA256 | runroot_other cmp /data/RPMS/hello-2.0-1.x86_64.rpm -],
+[0],
+[],
+[ignore])
+AT_CLEANUP
+
+AT_SETUP([rpm2extents install package])
+AT_KEYWORDS([rpm2extents reflink])
+AT_SKIP_IF([$REFLINK_DISABLED])
+AT_CHECK([
+RPMDB_INIT
+
+runroot_other cat /data/RPMS/hello-2.0-1.x86_64.rpm | runroot_other rpm2extents SHA256 > ${RPMTEST}/tmp/hello-2.0-1.x86_64.rpm 2> /dev/null
+runroot_plugins rpm -i --nodeps --undefine=%__transaction_dbus_announce /tmp/hello-2.0-1.x86_64.rpm
+test -f ${RPMTEST}/usr/bin/hello
+],
+[0],
+[],
+[])
+AT_CLEANUP
+
+AT_SETUP([reflink ignores non-transcoded package])
+AT_KEYWORDS([reflink])
+AT_CHECK([
+RPMDB_INIT
+
+runroot_plugins rpm -i --nodeps --undefine=%__transaction_dbus_announce /data/RPMS/hello-2.0-1.x86_64.rpm && exit $?
+# Check that the file is properly installed in chroot
+test -f ${RPMTEST}/usr/bin/hello
+],
+[0],
+[],
+[])
+AT_CLEANUP
+
+AT_SETUP([reflink hardlink package])
+AT_KEYWORDS([reflink hardlink])
+AT_SKIP_IF([$REFLINK_DISABLED])
+AT_CHECK([
+RPMDB_INIT
+
+PKG=hlinktest-1.0-1.noarch.rpm
+runroot_other cat /data/RPMS/${PKG} | runroot_other rpm2extents SHA256 > ${RPMTEST}/tmp/${PKG} 2> /dev/null
+runroot_plugins rpm -i --nodeps --undefine=%__transaction_dbus_announce /tmp/${PKG}
+],
+[0],
+[],
+[])
+AT_CLEANUP
diff --git a/tests/rpmtests.at b/tests/rpmtests.at
index d675452..3e6d528 100644
--- a/tests/rpmtests.at
+++ b/tests/rpmtests.at
@@ -24,3 +24,4 @@ m4_include([rpmreplace.at])
 m4_include([rpmconfig.at])
 m4_include([rpmconfig2.at])
 m4_include([rpmconfig3.at])
+m4_include([rpm2extents.at])
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index f4fbefe..d369d20 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -12,6 +12,7 @@ add_executable(rpmdeps rpmdeps.c)
 add_executable(rpmgraph rpmgraph.c)
 add_executable(rpmlua rpmlua.c)
 add_executable(rpmuncompress rpmuncompress.c)
+add_executable(rpm2extents rpm2extents.c)
 
 target_link_libraries(rpmsign PRIVATE librpmsign)
 target_link_libraries(rpmlua PRIVATE LUA::LUA)
@@ -32,6 +33,7 @@ endif()
 
 target_include_directories(rpmlua PRIVATE ${CMAKE_SOURCE_DIR}/rpmio)
 target_include_directories(rpmgraph PRIVATE ${CMAKE_SOURCE_DIR}/lib)
+target_include_directories(rpm2extents PRIVATE ${CMAKE_SOURCE_DIR}/lib ${CMAKE_SOURCE_DIR}/rpmio)
 
 if (READLINE_FOUND)
 	target_link_libraries(rpmspec PRIVATE PkgConfig::READLINE)
@@ -62,5 +64,4 @@ install(TARGETS
 	rpm rpmdb rpmkeys rpm2cpio rpmsign rpmbuild rpmspec
 	rpmlua rpmgraph
 )
-install(TARGETS rpmdeps rpmuncompress DESTINATION ${RPM_CONFIGDIR})
-
+install(TARGETS rpmdeps rpmuncompress rpm2extents DESTINATION ${RPM_CONFIGDIR})
diff --git a/tools/rpm2extents.c b/tools/rpm2extents.c
new file mode 100644
index 0000000..98124c2
--- /dev/null
+++ b/tools/rpm2extents.c
@@ -0,0 +1,707 @@
+/* rpm2extents: convert payload to inline extents */
+
+#include "system.h"
+
+#include <rpm/rpmcli.h>
+#include <rpm/rpmlib.h>		/* rpmReadPackageFile .. */
+#include <rpm/rpmlog.h>
+#include <rpm/rpmfi.h>
+#include <rpm/rpmtag.h>
+#include <rpm/rpmio.h>
+#include <rpm/rpmpgp.h>
+#include <rpm/rpmts.h>
+#include <rpm/rpmextents_internal.h>
+
+#include "rpmlead.h"
+#include "signature.h"
+#include "header_internal.h"
+#include "rpmio_internal.h"
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <signal.h>
+#include <errno.h>
+#include <string.h>
+
+#include "debug.h"
+
+/* hash of void * (pointers) to file digests to offsets within output.
+ * The length of the key depends on what the FILEDIGESTALGO is.
+ */
+#undef HASHTYPE
+#undef HTKEYTYPE
+#undef HTDATATYPE
+#define HASHTYPE digestSet
+#define HTKEYTYPE const unsigned char *
+#include "rpmhash.H"
+#include "rpmhash.C"
+
+struct digestoffset {
+    const unsigned char * digest;
+    rpm_loff_t pos;
+};
+
+rpm_loff_t pad_to(rpm_loff_t pos, rpm_loff_t unit);
+
+rpm_loff_t pad_to(rpm_loff_t pos, rpm_loff_t unit)
+{
+    return (unit - (pos % unit)) % unit;
+}
+
+static struct poptOption optionsTable[] = {
+    { NULL, '\0', POPT_ARG_INCLUDE_TABLE, rpmcliAllPoptTable, 0,
+    N_("Common options for all rpm modes and executables:"), NULL },
+
+    POPT_AUTOALIAS
+    POPT_AUTOHELP
+    POPT_TABLEEND
+};
+
+
+static void FDDigestInit(FD_t fdi, uint8_t algos[], uint32_t algos_len){
+    int algo;
+
+    for (algo = 0; algo < algos_len; algo++) {
+	fdInitDigest(fdi, algos[algo], 0);
+    }
+}
+
+static int FDWriteDigests(
+    FD_t fdi,
+    FD_t fdo,
+    uint8_t algos[],
+    uint32_t algos_len)
+{
+    const char *filedigest, *algo_name;
+    size_t filedigest_len, len;
+    uint32_t algo_name_len, algo_digest_len;
+    int algo;
+    rpmRC rc = RPMRC_FAIL;
+
+    ssize_t fdilength = fdOp(fdi, FDSTAT_READ)->bytes;
+
+    len = sizeof(fdilength);
+    if (Fwrite(&fdilength, len, 1, fdo) != len) {
+	rpmlog(RPMLOG_ERR, _("Unable to write input length %zd: %d, %s\n"),
+	       fdilength, errno, strerror(errno));
+	goto exit;
+    }
+    len = sizeof(algos_len);
+    if (Fwrite(&algos_len, len, 1, fdo) != len) {
+	algo_digest_len = (uint32_t)filedigest_len;
+	rpmlog(RPMLOG_ERR, _("Unable to write number of digests: %d, %s\n"),
+	       errno, strerror(errno));
+	goto exit;
+    }
+    for (algo = 0; algo < algos_len; algo++) {
+	fdFiniDigest(fdi, algos[algo], (void **)&filedigest, &filedigest_len, 0);
+
+	algo_name = pgpValString(PGPVAL_HASHALGO, algos[algo]);
+	algo_name_len = (uint32_t)strlen(algo_name);
+	algo_digest_len = (uint32_t)filedigest_len;
+
+	len = sizeof(algo_name_len);
+	if (Fwrite(&algo_name_len, len, 1, fdo) != len) {
+	    rpmlog(RPMLOG_ERR,
+		   _("Unable to write digest algo name length: %d, %s\n"),
+		   errno, strerror(errno));
+	    goto exit;
+	}
+	len = sizeof(algo_digest_len);
+	if (Fwrite(&algo_digest_len, len, 1, fdo) != len) {
+	    rpmlog(RPMLOG_ERR,
+		   _("Unable to write number of bytes for digest: %d, %s\n"),
+		   errno, strerror(errno));
+	     goto exit;
+	}
+	if (Fwrite(algo_name, algo_name_len, 1, fdo) != algo_name_len) {
+	    rpmlog(RPMLOG_ERR, _("Unable to write digest algo name: %d, %s\n"),
+		   errno, strerror(errno));
+	    goto exit;
+	}
+	if (Fwrite(filedigest, algo_digest_len, 1, fdo ) != algo_digest_len) {
+	    rpmlog(RPMLOG_ERR,
+		   _("Unable to write digest value %u, %zu: %d, %s\n"),
+		   algo_digest_len, filedigest_len,
+		   errno, strerror(errno));
+	    goto exit;
+	}
+    }
+    rc = RPMRC_OK;
+exit:
+    return rc;
+}
+
+/**
+ * Check if package is in deny list.
+ * @param package_name	package name
+ * @return 		true if package is in deny list
+ */
+static inline int isInDenyList(char *package_name)
+{
+    int is_in_deny_list = 0;
+    if (package_name) {
+	char *e_denylist = getenv("LIBREPO_TRANSCODE_RPMS_DENYLIST");
+	char *denytlist_item = strtok(e_denylist, ",");
+	while (denytlist_item) {
+	    if (strstr(package_name, denytlist_item)) {
+		is_in_deny_list = 1;
+		break;
+	    }
+	    denytlist_item = strtok(NULL, ",");
+	}
+    }
+    return is_in_deny_list;
+}
+
+static rpmRC FDWriteSignaturesValidation(FD_t fdo, int rpmvsrc, char *msg) {
+    size_t len;
+    rpmRC rc = RPMRC_FAIL;
+
+    if(rpmvsrc){
+	rpmlog(RPMLOG_WARNING,
+	       _("Error verifying package signatures:\n%s\n"), msg);
+    }
+
+    len = sizeof(rpmvsrc);
+    if (Fwrite(&rpmvsrc, len, 1, fdo) != len) {
+	rpmlog(RPMLOG_ERR,
+	       _("Unable to write signature verification RC code %d: %d, %s\n"),
+	       rpmvsrc, errno, strerror(errno));
+	goto exit;
+    }
+    size_t content_len = msg ? strlen(msg) : 0;
+    len = sizeof(content_len);
+    if (Fwrite(&content_len, len, 1, fdo) != len) {
+	rpmlog(RPMLOG_ERR,
+	       _("Unable to write signature verification output length %zd: %d, %s\n"),
+	       content_len, errno, strerror(errno));
+	goto exit;
+    }
+    if (Fwrite(msg, content_len, 1, fdo) != content_len) {
+	rpmlog(RPMLOG_ERR,
+	       _("Unable to write signature verification output %s: %d, %s\n"),
+	       msg, errno, strerror(errno));
+	goto exit;
+    }
+
+    rc = RPMRC_OK;
+exit:
+
+    return rc;
+}
+
+static rpmRC validator(FD_t fdi, FD_t digesto, FD_t sigo,
+	uint8_t algos[],
+	uint32_t algos_len){
+    int rpmvsrc;
+    rpmRC rc = RPMRC_FAIL;
+    char *msg = NULL;
+    rpmts ts = rpmtsCreate();
+
+    rpmtsSetRootDir(ts, rpmcliRootDir);
+
+    FDDigestInit(fdi, algos, algos_len);
+
+    rpmvsrc = rpmcliVerifySignaturesFD(ts, fdi, &msg);
+
+    // Write result of digest computation
+    if(FDWriteDigests(fdi, digesto, algos, algos_len) != RPMRC_OK) {
+	rpmlog(RPMLOG_ERR, _("Failed to write digests: %d, %s\n"),
+	       errno, strerror(errno));
+	goto exit;
+    }
+
+    // Write result of signature validation.
+    if(FDWriteSignaturesValidation(sigo, rpmvsrc, msg)) {
+	rpmlog(RPMLOG_ERR,
+	       _("Failed to write signature verification result: %d, %s\n"),
+	       errno, strerror(errno));
+	goto exit;
+    }
+    rc = RPMRC_OK;
+exit:
+    if(msg) {
+	free(msg);
+    }
+    rpmtsFree(ts);
+    return rc;
+}
+
+static void sanitizeSignatureHeader(Header * sigh)
+{
+    struct rpmtd_s td;
+
+    /* This is inspired by the code in unloadImmutableRegion. See https://github.com/rpm-software-management/rpm/pull/1330 */
+    if (!headerGet(*sigh, RPMTAG_HEADERSIGNATURES, &td, HEADERGET_DEFAULT)) {
+	/* Signature header corrupt/missing */
+	rpmlog(RPMLOG_WARNING, _("Error verifying signature header\n"));
+	rpmtdFreeData(&td);
+	Header nh = headerCopy(*sigh);
+	headerFree(*sigh);
+	*sigh = headerLink(nh);
+	headerFree(nh);
+    }
+    rpmtdFreeData(&td);
+}
+
+static rpmRC process_package(FD_t fdi, FD_t digestori, FD_t validationi)
+{
+    uint32_t diglen;
+    /* GNU C extension: can use diglen from outer context */
+    int digestSetCmp(const unsigned char * a, const unsigned char * b) {
+	return memcmp(a, b, diglen);
+    }
+
+    unsigned int digestSetHash(const unsigned char * digest) {
+        /* assumes sizeof(unsigned int) < diglen */
+        return *(unsigned int *)digest;
+    }
+
+    int digestoffsetCmp(const void * a, const void * b) {
+	return digestSetCmp(
+	    ((struct digestoffset *)a)->digest,
+	    ((struct digestoffset *)b)->digest
+	);
+    }
+
+    FD_t fdo;
+    FD_t gzdi;
+    Header h, sigh;
+    long fundamental_block_size = sysconf(_SC_PAGESIZE);
+    rpmRC rc = RPMRC_OK;
+    rpm_mode_t mode;
+    char *rpmio_flags = NULL, *zeros;
+    const unsigned char *digest;
+    rpm_loff_t pos, size, pad, digest_pos, validation_pos, digest_table_pos;
+    uint32_t offset_ix = 0;
+    size_t len;
+    int next = 0;
+    struct rpmlead_s l;
+    rpmfiles files = NULL;
+    rpmfi fi = NULL;
+    char *msg = NULL;
+    struct digestoffset *offsets = NULL;
+    digestSet ds = NULL;
+
+    fdo = fdDup(STDOUT_FILENO);
+
+    rc = rpmLeadReadAndReturn(fdi, &msg, &l);
+    if (rc != RPMRC_OK)
+	goto exit;
+
+    /* Skip conversion if package is in deny list */
+    if (isInDenyList(l.name)) {
+	rpmlog(RPMLOG_WARNING, _("package %s is in deny list: conversion skipped\n"), l.name);
+	if (rpmLeadWrite(fdo, l)) {
+	    rpmlog(RPMLOG_ERR, _("Unable to write package lead: %s\n"),
+		    Fstrerror(fdo));
+	    rc = RPMRC_FAIL;
+	    goto exit;
+	}
+
+	ssize_t fdilength = ufdCopy(fdi, fdo);
+	if (fdilength == -1) {
+	    rpmlog(RPMLOG_ERR, _("process_package cat failed\n"));
+	    rc = RPMRC_FAIL;
+	    goto exit;
+	}
+
+	goto exit;
+    } else {
+	if (rpmReadPackageRaw(fdi, &sigh, &h)) {
+	    rpmlog(RPMLOG_ERR, _("Error reading package\n"));
+	    exit(EXIT_FAILURE);
+	}
+
+	sanitizeSignatureHeader(&sigh);
+
+	if (rpmLeadWriteFromHeader(fdo, h)) {
+	    rpmlog(RPMLOG_ERR, _("Unable to write package lead: %s\n"),
+		   Fstrerror(fdo));
+	    exit(EXIT_FAILURE);
+	}
+
+	if (rpmWriteSignature(fdo, sigh)) {
+	    rpmlog(RPMLOG_ERR, _("Unable to write signature: %s\n"),
+		   Fstrerror(fdo));
+	    exit(EXIT_FAILURE);
+	}
+
+	if (headerWrite(fdo, h, HEADER_MAGIC_YES)) {
+	    rpmlog(RPMLOG_ERR, _("Unable to write headers: %s\n"),
+		   Fstrerror(fdo));
+	    exit(EXIT_FAILURE);
+	}
+
+	/* Retrieve payload size and compression type. */
+	{
+	    const char *compr =
+		headerGetString(h, RPMTAG_PAYLOADCOMPRESSOR);
+	    rpmio_flags =
+		rstrscat(NULL, "r.", compr ? compr : "gzip", NULL);
+	}
+
+	gzdi = Fdopen(fdi, rpmio_flags);	/* XXX gzdi == fdi */
+	free(rpmio_flags);
+
+	if (gzdi == NULL) {
+	    rpmlog(RPMLOG_ERR, _("cannot re-open payload: %s\n"),
+		   Fstrerror(gzdi));
+	    exit(EXIT_FAILURE);
+	}
+
+	files = rpmfilesNew(NULL, h, 0, RPMFI_KEEPHEADER);
+	fi = rpmfiNewArchiveReader(gzdi, files,
+				   RPMFI_ITER_READ_ARCHIVE_CONTENT_FIRST);
+
+	/* this is encoded in the file format, so needs to be fixed size (for
+	 * now?)
+	 */
+	diglen = (uint32_t) rpmDigestLength(rpmfiDigestAlgo(fi));
+	ds = digestSetCreate(rpmfiFC(fi), digestSetHash, digestSetCmp, NULL);
+	offsets = xcalloc(rpmfiFC(fi), sizeof(*offsets));
+	pos = RPMLEAD_SIZE + headerSizeof(sigh, HEADER_MAGIC_YES);
+
+	/* main headers are aligned to 8 byte boundry */
+	pos += pad_to(pos, 8);
+	pos += headerSizeof(h, HEADER_MAGIC_YES);
+
+	zeros = xcalloc(fundamental_block_size, 1);
+
+	while (next >= 0) {
+	    next = rpmfiNext(fi);
+	    if (next == RPMERR_ITER_END) {
+		rc = RPMRC_OK;
+		break;
+	    }
+	    mode = rpmfiFMode(fi);
+	    if (!S_ISREG(mode) || !rpmfiArchiveHasContent(fi)) {
+		/* not a regular file, or the archive doesn't contain any content
+		 * for this entry.
+		 */
+		continue;
+	    }
+	    digest = rpmfiFDigest(fi, NULL, NULL);
+	    if (digestSetGetEntry(ds, digest, NULL)) {
+		/* This specific digest has already been included, so skip it. */
+		continue;
+	    }
+	    pad = pad_to(pos, fundamental_block_size);
+	    if (Fwrite(zeros, sizeof(char), pad, fdo) != pad) {
+		rpmlog(RPMLOG_ERR, _("Unable to write padding\n"));
+		rc = RPMRC_FAIL;
+		goto exit;
+	    }
+	    /* round up to next fundamental_block_size */
+	    pos += pad;
+	    digestSetAddEntry(ds, digest);
+	    offsets[offset_ix].digest = digest;
+	    offsets[offset_ix].pos = pos;
+	    offset_ix++;
+	    size = rpmfiFSize(fi);
+	    rc = rpmfiArchiveReadToFile(fi, fdo, 0);
+	    if (rc != RPMRC_OK) {
+		char *errstr = rpmfileStrerror(rc);
+		rpmlog(RPMLOG_ERR,
+		       _("rpmfiArchiveReadToFile failed while extracting "
+			 "\"%s\" with RC %d: %s\n"),
+		       rpmfiFN(fi), rc, errstr);
+		free(errstr);
+		goto exit;
+	    }
+	    pos += size;
+	}
+	Fclose(gzdi);		/* XXX gzdi == fdi */
+
+	qsort(offsets, (size_t) offset_ix, sizeof(struct digestoffset),
+	      digestoffsetCmp);
+
+	validation_pos = pos;
+	ssize_t validation_len = ufdCopy(validationi, fdo);
+	if (validation_len == -1) {
+	    rpmlog(RPMLOG_ERR, _("validation output ufdCopy failed\n"));
+	    rc = RPMRC_FAIL;
+	    goto exit;
+	}
+
+	digest_table_pos = validation_pos + validation_len;
+
+	len = sizeof(offset_ix);
+	if (Fwrite(&offset_ix, len, 1, fdo) != len) {
+	    rpmlog(RPMLOG_ERR, _("Unable to write length of table\n"));
+	    rc = RPMRC_FAIL;
+	    goto exit;
+	}
+	len = sizeof(diglen);
+	if (Fwrite(&diglen, len, 1, fdo) != len) {
+	    rpmlog(RPMLOG_ERR, _("Unable to write length of digest\n"));
+	    rc = RPMRC_FAIL;
+	    goto exit;
+	}
+	len = sizeof(rpm_loff_t);
+	for (int x = 0; x < offset_ix; x++) {
+	    if (Fwrite(offsets[x].digest, diglen, 1, fdo) != diglen) {
+		rpmlog(RPMLOG_ERR, _("Unable to write digest\n"));
+		rc = RPMRC_FAIL;
+		goto exit;
+	    }
+	    if (Fwrite(&offsets[x].pos, len, 1, fdo) != len) {
+		rpmlog(RPMLOG_ERR, _("Unable to write offset\n"));
+		rc = RPMRC_FAIL;
+		goto exit;
+	    }
+	}
+	digest_pos =
+	    (digest_table_pos + sizeof(offset_ix) + sizeof(diglen) +
+	     offset_ix * (diglen + sizeof(rpm_loff_t))
+	    );
+
+	ssize_t digest_len = ufdCopy(digestori, fdo);
+	if (digest_len == -1) {
+	    rpmlog(RPMLOG_ERR, _("digest table ufdCopy failed\n"));
+	    rc = RPMRC_FAIL;
+	    goto exit;
+	}
+
+	/* add more padding so the last file can be cloned. It doesn't matter that
+	 * the table and validation etc are in this space. In fact, it's pretty
+	 * efficient if it is.
+	 */
+
+	pad =
+	    pad_to((validation_pos + validation_len +
+		    2 * sizeof(rpm_loff_t) + sizeof(uint64_t)),
+		   fundamental_block_size);
+	if (Fwrite(zeros, sizeof(char), pad, fdo) != pad) {
+	    rpmlog(RPMLOG_ERR, _("Unable to write final padding\n"));
+	    rc = RPMRC_FAIL;
+	    goto exit;
+	}
+	zeros = _free(zeros);
+	struct extents_footer_t footer = {.offsets =
+		{ validation_pos, digest_table_pos, digest_pos },.magic =
+	    EXTENTS_MAGIC };
+	len = sizeof(footer);
+	if (Fwrite(&footer, len, 1, fdo) != len) {
+	    rpmlog(RPMLOG_ERR, _("Unable to write footer\n"));
+	    rc = RPMRC_FAIL;
+	    goto exit;
+	}
+    }
+
+  exit:
+    rpmfilesFree(files);
+    rpmfiFree(fi);
+    headerFree(h);
+    headerFree(sigh);
+    free(offsets);
+    Fclose(fdo);
+    digestSetFree(ds);
+    return rc;
+}
+
+static off_t ufdTee(FD_t sfd, FD_t *fds, int len)
+{
+    char buf[BUFSIZ];
+    ssize_t rdbytes, wrbytes;
+    off_t total = 0;
+
+    while (1) {
+	rdbytes = Fread(buf, sizeof(buf[0]), sizeof(buf), sfd);
+
+	if (rdbytes > 0) {
+	    for(int i=0; i < len; i++) {
+		wrbytes = Fwrite(buf, sizeof(buf[0]), rdbytes, fds[i]);
+		if (wrbytes != rdbytes) {
+		    rpmlog(RPMLOG_ERR,
+			   _("Error wriing to FD %d: %s\n"),
+			   i, Fstrerror(fds[i]));
+		    total = -1;
+		    break;
+		}
+	    }
+	    if(total == -1){
+		break;
+	    }
+	    total += wrbytes;
+	} else {
+	    if (rdbytes < 0)
+		total = -1;
+	    break;
+	}
+    }
+
+    return total;
+}
+
+static rpmRC teeRpm(FD_t fdi, uint8_t algos[], uint32_t algos_len) {
+    rpmRC rc = RPMRC_FAIL;
+    off_t offt = -1;
+    // tee-ed stdin
+    int processorpipefd[2];
+    int validatorpipefd[2];
+    // metadata
+    int meta_digestpipefd[2];
+    int meta_rpmsignpipefd[2];
+
+    pid_t cpids[2], w;
+    int wstatus;
+    FD_t fds[2];
+
+     if (pipe(processorpipefd) == -1) {
+	rpmlog(RPMLOG_ERR, _("Processor pipe failure\n"));
+	return RPMRC_FAIL;
+    }
+
+    if (pipe(validatorpipefd) == -1) {
+	rpmlog(RPMLOG_ERR, _("Validator pipe failure\n"));
+	return RPMRC_FAIL;
+    }
+
+    if (pipe(meta_digestpipefd) == -1) {
+	rpmlog(RPMLOG_ERR, _("Meta digest pipe failure\n"));
+	return RPMRC_FAIL;
+    }
+
+    if (pipe(meta_rpmsignpipefd) == -1) {
+	rpmlog(RPMLOG_ERR, _("Meta rpm signature pipe failure\n"));
+	return RPMRC_FAIL;
+    }
+
+    cpids[0] = fork();
+    if (cpids[0] == 0) {
+	/* child: validator */
+	close(processorpipefd[0]);
+	close(processorpipefd[1]);
+	close(validatorpipefd[1]);
+	close(meta_digestpipefd[0]);
+	close(meta_rpmsignpipefd[0]);
+	FD_t fdi = fdDup(validatorpipefd[0]);
+	FD_t digesto = fdDup(meta_digestpipefd[1]);
+	FD_t sigo = fdDup(meta_rpmsignpipefd[1]);
+	close(meta_digestpipefd[1]);
+	close(meta_rpmsignpipefd[1]);
+	rc = validator(fdi, digesto, sigo, algos, algos_len);
+	if(rc != RPMRC_OK) {
+	    rpmlog(RPMLOG_ERR, _("Validator failed with RC %d\n"), rc);
+	}
+	Fclose(fdi);
+	Fclose(digesto);
+	Fclose(sigo);
+	if (rc != RPMRC_OK) {
+	    exit(EXIT_FAILURE);
+	}
+	exit(EXIT_SUCCESS);
+    } else {
+	/* parent: main program */
+	cpids[1] = fork();
+	if (cpids[1] == 0) {
+	    /* child: process_package */
+	    close(validatorpipefd[0]);
+	    close(validatorpipefd[1]);
+	    close(processorpipefd[1]);
+	    close(meta_digestpipefd[1]);
+	    close(meta_rpmsignpipefd[1]);
+	    FD_t fdi = fdDup(processorpipefd[0]);
+	    close(processorpipefd[0]);
+	    FD_t sigi = fdDup(meta_rpmsignpipefd[0]);
+	    close(meta_rpmsignpipefd[0]);
+	    FD_t digestori = fdDup(meta_digestpipefd[0]);
+	    close(meta_digestpipefd[0]);
+
+	    rc = process_package(fdi, digestori, sigi);
+	    if(rc != RPMRC_OK) {
+		rpmlog(RPMLOG_ERR, _("Package processor failed: %d\n"), rc);
+	    }
+	    Fclose(digestori);
+	    Fclose(sigi);
+	    /* fdi is normally closed through the stacked file gzdi in the
+	     * function
+	     */
+
+	    if (rc != RPMRC_OK) {
+		exit(EXIT_FAILURE);
+	    }
+	    exit(EXIT_SUCCESS);
+
+
+	} else {
+	    /* Actual parent. Read from fdi and write to both processes */
+	    close(processorpipefd[0]);
+	    close(validatorpipefd[0]);
+	    fds[0] = fdDup(processorpipefd[1]);
+	    fds[1] = fdDup(validatorpipefd[1]);
+	    close(validatorpipefd[1]);
+	    close(processorpipefd[1]);
+	    close(meta_digestpipefd[0]);
+	    close(meta_digestpipefd[1]);
+	    close(meta_rpmsignpipefd[0]);
+	    close(meta_rpmsignpipefd[1]);
+
+	    rc = RPMRC_OK;
+	    offt = ufdTee(fdi, fds, 2);
+	    if(offt == -1){
+		rpmlog(RPMLOG_ERR, _("Failed to tee RPM\n"));
+		rc = RPMRC_FAIL;
+	    }
+	    Fclose(fds[0]);
+	    Fclose(fds[1]);
+	    w = waitpid(cpids[0], &wstatus, 0);
+	    if (w == -1) {
+		rpmlog(RPMLOG_ERR, _("waitpid cpids[0] failed\n"));
+		rc = RPMRC_FAIL;
+	    }
+	    w = waitpid(cpids[1], &wstatus, 0);
+	    if (w == -1) {
+		rpmlog(RPMLOG_ERR, _("waitpid cpids[1] failed\n"));
+		rc = RPMRC_FAIL;
+	    }
+	}
+    }
+
+    return rc;
+}
+
+int main(int argc, char *argv[]) {
+    rpmRC rc;
+    poptContext optCon = NULL;
+    const char **args = NULL;
+    int nb_algos = 0;
+
+    xsetprogname(argv[0]);	/* Portability call -- see system.h */
+    rpmReadConfigFiles(NULL, NULL);
+    optCon = rpmcliInit(argc, argv, optionsTable);
+    poptSetOtherOptionHelp(optCon, "[OPTIONS]* <DIGESTALGO>");
+
+    if (poptPeekArg(optCon) == NULL) {
+	rpmlog(RPMLOG_ERR,
+	       _("Need at least one DIGESTALGO parameter, e.g. 'SHA256'\n"));
+	poptPrintUsage(optCon, stderr, 0);
+	exit(EXIT_FAILURE);
+    }
+
+    args = poptGetArgs(optCon);
+
+    for (nb_algos=0; args[nb_algos]; nb_algos++);
+    uint8_t algos[nb_algos];
+    for (int x = 0; x < nb_algos; x++) {
+	if (pgpStringVal(PGPVAL_HASHALGO, args[x], &algos[x]) != 0)
+	{
+	    rpmlog(RPMLOG_ERR,
+		   _("Unable to resolve '%s' as a digest algorithm, exiting\n"),
+		   args[x]);
+	    exit(EXIT_FAILURE);
+	}
+    }
+
+    FD_t fdi = fdDup(STDIN_FILENO);
+    rc = teeRpm(fdi, algos, nb_algos);
+    Fclose(fdi);
+    if (rc != RPMRC_OK) {
+	/* translate rpmRC into generic failure return code. */
+	return EXIT_FAILURE;
+    }
+    return EXIT_SUCCESS;
+}
-- 
2.47.1