public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/python3-rpm] epel10: Add support for %autosetup -C
@ 2026-06-25  7:43 Michal Domonkos
  0 siblings, 0 replies; only message in thread
From: Michal Domonkos @ 2026-06-25  7:43 UTC (permalink / raw)
  To: git-commits

            A new commit has been pushed.

            Repo   : rpms/python3-rpm
            Branch : epel10
            Commit : 6755696e77a37edde42725c0decab56f93e796d7
            Author : Michal Domonkos <mdomonko@redhat.com>
            Date   : 2026-06-18T09:49:28+02:00
            Stats  : +1170/-1 in 2 file(s)
            URL    : https://src.fedoraproject.org/rpms/python3-rpm/c/6755696e77a37edde42725c0decab56f93e796d7?branch=epel10

            Log:
            Add support for %autosetup -C

Resolves: RHEL-141269

---
diff --git a/rpm-4.19.x-add-autosetup-C.patch b/rpm-4.19.x-add-autosetup-C.patch
new file mode 100644
index 0000000..e07211d
--- /dev/null
+++ b/rpm-4.19.x-add-autosetup-C.patch
@@ -0,0 +1,1164 @@
+From 93dceb0dc1e05a6e71e966c27814364eb77346d2 Mon Sep 17 00:00:00 2001
+From: Matteo Croce <teknoraver@meta.com>
+Date: Tue, 16 Jan 2024 17:26:16 -0500
+Subject: [PATCH 01/14] add build directory auto path to %autosetup
+
+Add a `-C` flag to %autosetup and %setup which ensures that the sources
+will be extracted in the root of the build directory.
+It works by inspecting the archive and stripping the first path entry
+if the archive has a top level directory alone in the root.
+
+The archive inspection and path stripping is implemented in rpmuncompress
+which now has a new `-C` flag:
+
+    $ ~/rpm/usr/lib/rpm/rpmuncompress -x -v -C source-1.0.0 source-singleroot.tar.gz
+    mkdir 'source-1.0.0' ;  /usr/bin/gzip -dc  'source-singleroot.tar.gz' | /usr/bin/tar -xvvof -  -C 'source-1.0.0' --strip-components=1
+    -rw-r--r-- root/root        32 2024-01-16 19:13 source-xxxxxxxxxx/file1
+    -rw-r--r-- root/root     33886 2024-01-16 19:13 source-xxxxxxxxxx/file2
+    drwxr-xr-x root/root         0 2024-01-16 19:13 source-xxxxxxxxxx/dir1/
+    -r--r--r-- root/root       210 2024-01-16 19:13 source-xxxxxxxxxx/dir1/file3
+
+    $ find source-1.0.0 -ls
+        92341      0 drwxr-xr-x   1 teknoraver teknoraver       28 Jan 16 19:16 source-1.0.0
+        92342      4 -rw-r--r--   1 teknoraver teknoraver       32 Jan 16 19:13 source-1.0.0/file1
+        92343     36 -rw-r--r--   1 teknoraver teknoraver    33886 Jan 16 19:13 source-1.0.0/file2
+        92344      0 drwxr-xr-x   1 teknoraver teknoraver       10 Jan 16 19:13 source-1.0.0/dir1
+        92345      4 -r--r--r--   1 teknoraver teknoraver      210 Jan 16 19:13 source-1.0.0/dir1/file3
+
+    $ ~/rpm/usr/lib/rpm/rpmuncompress -x -v -C source-2.0.0 source-noroot.tar.gz
+    mkdir 'source-2.0.0' ;  /usr/bin/gzip -dc  'source-noroot.tar.gz' | /usr/bin/tar -xvvof -  -C 'source-2.0.0'
+    drwxr-xr-x root/root         0 2024-01-16 19:13 dir1/
+    -r--r--r-- root/root       210 2024-01-16 19:13 dir1/file3
+    -rw-r--r-- root/root        32 2024-01-16 19:13 file1
+    -rw-r--r-- root/root     33886 2024-01-16 19:13 file2
+
+    $ find source-2.0.0 -ls
+        92346      0 drwxr-xr-x   1 teknoraver teknoraver       28 Jan 16 19:17 source-2.0.0
+        92347      0 drwxr-xr-x   1 teknoraver teknoraver       10 Jan 16 19:13 source-2.0.0/dir1
+        92348      4 -r--r--r--   1 teknoraver teknoraver      210 Jan 16 19:13 source-2.0.0/dir1/file3
+        92349      4 -rw-r--r--   1 teknoraver teknoraver       32 Jan 16 19:13 source-2.0.0/file1
+        92350     36 -rw-r--r--   1 teknoraver teknoraver    33886 Jan 16 19:13 source-2.0.0/file2
+
+And it's exposed to %autosetup
+
+    $ rpmbuild -bp test.spec
+    Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.NL55sI
+    + umask 022
+    + cd /home/teknoraver/rpmbuild/BUILD
+    + cd /home/teknoraver/rpmbuild/BUILD
+    + rm -rf netperf-2.7.1
+    + /usr/lib/rpm/rpmuncompress -x -C netperf-2.7.1 /home/teknoraver/rpmbuild/SOURCES/netperf-3bc455b.tar.gz
+    + STATUS=0
+    + '[' 0 -ne 0 ']'
+    + cd netperf-2.7.1
+    + rm -rf /home/teknoraver/rpmbuild/BUILD/netperf-2.7.1-SPECPARTS
+    + /usr/bin/mkdir -p /home/teknoraver/rpmbuild/BUILD/netperf-2.7.1-SPECPARTS
+    + /usr/bin/chmod -Rf a+rX,u+w,g-w,o-w .
+    + RPM_EC=0
+    ++ jobs -p
+    + exit 0
+
+(backported from commit 33853c73cf6af5ff3108fe5c59a0a7a1614bed99)
+---
+ CMakeLists.txt        |  5 +--
+ build/parsePrep.c     | 13 +++++---
+ docs/manual/spec.md   |  3 ++
+ macros.in             |  4 +--
+ tools/CMakeLists.txt  |  1 +
+ tools/rpmuncompress.c | 74 +++++++++++++++++++++++++++++++++++++++++--
+ 6 files changed, 87 insertions(+), 13 deletions(-)
+
+diff --git a/CMakeLists.txt b/CMakeLists.txt
+index 6dbf179f3..7b9fbf56f 100644
+--- a/CMakeLists.txt
++++ b/CMakeLists.txt
+@@ -205,6 +205,7 @@ pkg_check_modules(ZSTD IMPORTED_TARGET libzstd>=1.3.8)
+ pkg_check_modules(LIBELF IMPORTED_TARGET libelf)
+ pkg_check_modules(LIBDW IMPORTED_TARGET libdw)
+ pkg_check_modules(LIBLZMA IMPORTED_TARGET liblzma>=5.2.0)
++pkg_check_modules(LIBARCHIVE REQUIRED IMPORTED_TARGET libarchive)
+ 
+ # Lua module does not ship an IMPORTED target, define our own
+ add_library(LUA::LUA INTERFACE IMPORTED)
+@@ -265,10 +266,6 @@ if (WITH_SELINUX)
+ 	pkg_check_modules(SELINUX REQUIRED IMPORTED_TARGET libselinux)
+ endif()
+ 
+-if (WITH_ARCHIVE)
+-	pkg_check_modules(LIBARCHIVE REQUIRED IMPORTED_TARGET libarchive)
+-endif()
+-
+ if (WITH_FSVERITY)
+ 	pkg_check_modules(FSVERITY REQUIRED IMPORTED_TARGET libfsverity)
+ endif()
+diff --git a/build/parsePrep.c b/build/parsePrep.c
+index 07d9a6923..022a5be7d 100644
+--- a/build/parsePrep.c
++++ b/build/parsePrep.c
+@@ -124,7 +124,7 @@ exit:
+  * @param quietly	should -vv be omitted from tar?
+  * @return		expanded %setup macro (NULL on error)
+  */
+-static char *doUntar(rpmSpec spec, uint32_t c, int quietly)
++static char *doUntar(rpmSpec spec, uint32_t c, int quietly, int autoPath)
+ {
+     char *buf = NULL;
+     struct Source *sp;
+@@ -135,7 +135,9 @@ static char *doUntar(rpmSpec spec, uint32_t c, int quietly)
+     }
+ 
+     buf = rpmExpand("%{__rpmuncompress} -x ",
+-		    quietly ? "" : "-v ","%{shescape:", sp->path, "}", NULL);
++		    quietly ? "" : "-v ",
++		    autoPath ? "-C %{buildsubdir} " : "",
++		    "%{shescape:", sp->path, "}", NULL);
+     rstrcat(&buf,
+ 	"\nSTATUS=$?\n"
+ 	"if [ $STATUS -ne 0 ]; then\n"
+@@ -165,12 +167,13 @@ static int doSetupMacro(rpmSpec spec, const char *line)
+     rpmRC rc = RPMRC_FAIL;
+     uint32_t num;
+     int leaveDirs = 0, skipDefaultAction = 0;
+-    int createDir = 0, quietly = 0;
++    int createDir = 0, quietly = 0, autoPath = 0;
+     char * dirName = NULL;
+     struct poptOption optionsTable[] = {
+ 	    { NULL, 'a', POPT_ARG_STRING, NULL, 'a',	NULL, NULL},
+ 	    { NULL, 'b', POPT_ARG_STRING, NULL, 'b',	NULL, NULL},
+ 	    { NULL, 'c', 0, &createDir, 0,		NULL, NULL},
++	    { NULL, 'C', 0, &autoPath, 0,		NULL, NULL},
+ 	    { NULL, 'D', 0, &leaveDirs, 0,		NULL, NULL},
+ 	    { NULL, 'n', POPT_ARG_STRING, &dirName, 0,	NULL, NULL},
+ 	    { NULL, 'T', 0, &skipDefaultAction, 0,	NULL, NULL},
+@@ -202,7 +205,7 @@ static int doSetupMacro(rpmSpec spec, const char *line)
+ 	    goto exit;
+ 	}
+ 
+-	{   char *chptr = doUntar(spec, num, quietly);
++	{   char *chptr = doUntar(spec, num, quietly, 0);
+ 	    if (chptr == NULL)
+ 		goto exit;
+ 
+@@ -259,7 +262,7 @@ static int doSetupMacro(rpmSpec spec, const char *line)
+ 
+     /* do the default action */
+    if (!skipDefaultAction) {
+-	char *chptr = doUntar(spec, 0, quietly);
++	char *chptr = doUntar(spec, 0, quietly, autoPath);
+ 	if (!chptr)
+ 	    goto exit;
+ 	appendBuf(spec, chptr, 1);
+diff --git a/docs/manual/spec.md b/docs/manual/spec.md
+index 098099875..3ace79e56 100644
+--- a/docs/manual/spec.md
++++ b/docs/manual/spec.md
+@@ -489,6 +489,9 @@ can just create the directory. It accepts a number of options:
+ -a N        unpack source N after changing to the build directory
+ -b N        unpack source N before changing to the build directory
+ -c          create the build directory (and change to it) before unpacking
++-C          Create the build directory and ensure the archive contents
++            are unpacked there, stripping the top level directory in the archive
++            if it exists
+ -D          do not delete the build directory prior to unpacking (used
+             when more than one source is to be unpacked with `-a` or `-b`)
+ -n DIR      set the name of build directory (default is `%{name}-%{version}`)
+diff --git a/macros.in b/macros.in
+index ef413a358..19be79909 100644
+--- a/macros.in
++++ b/macros.in
+@@ -1338,8 +1338,8 @@ end
+ #           	usage of git repository and per-patch commits.
+ # -N		Disable automatic patch application
+ # -p<num>	Use -p<num> for patch application	
+-%autosetup(a:b:cDn:TvNS:p:)\
+-%setup %{-a} %{-b} %{-c} %{-D} %{-n} %{-T} %{!-v:-q}\
++%autosetup(a:b:cCDn:TvNS:p:)\
++%setup %{-a} %{-b} %{-c} %{-C} %{-D} %{-n} %{-T} %{!-v:-q}\
+ %{-S:%global __scm %{-S*}}\
+ %{expand:%__scm_setup_%{__scm} %{!-v:-q}}\
+ %{!-N:%autopatch %{-v} %{-p:-p%{-p*}}}
+diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
+index 5be4cf2d0..ea452e738 100644
+--- a/tools/CMakeLists.txt
++++ b/tools/CMakeLists.txt
+@@ -19,6 +19,7 @@ target_link_libraries(rpmlua PRIVATE LUA::LUA)
+ target_link_libraries(rpmbuild PRIVATE librpmbuild)
+ target_link_libraries(rpmspec PRIVATE librpmbuild)
+ target_link_libraries(rpmdeps PRIVATE librpmbuild)
++target_link_libraries(rpmuncompress PRIVATE PkgConfig::LIBARCHIVE)
+ 
+ if (HAVE_STRCHRNUL)
+ 	add_executable(rpmsort rpmsort.c)
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index e13cc6a66..c4832332c 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -6,6 +6,9 @@
+ #include <stdio.h>
+ #include <string.h>
+ 
++#include <archive.h>
++#include <archive_entry.h>
++
+ #include <rpm/rpmcli.h>
+ #include <rpm/rpmstring.h>
+ 
+@@ -14,6 +17,7 @@
+ static int verbose = 0;
+ static int extract = 0;
+ static int dryrun = 0;
++static char *dstpath = NULL;
+ 
+ static struct poptOption optionsTable[] = {
+     { "extract", 'x', POPT_ARG_VAL, &extract, 1,
+@@ -22,6 +26,8 @@ static struct poptOption optionsTable[] = {
+ 	N_("provide more detailed output"), NULL },
+     { "dry-run", 'n', POPT_ARG_VAL, &dryrun, 1,
+ 	N_("only print what would be done"), NULL },
++    { "path", 'C', POPT_ARG_STRING, &dstpath, 0,
++	N_("extract into a specific path"), NULL },
+ 
+     POPT_AUTOALIAS
+     POPT_AUTOHELP
+@@ -78,16 +84,78 @@ static char *doUncompress(const char *fn)
+     return cmd;
+ }
+ 
++/**
++ * Detect if an archive has a single top level entry, and it's a directory.
++ *
++ * @param path	path of the archive
++ * @return	1 if archive as only a directory as top level entry,
++ * 		0 if it contains multiple top level entries or a single file
++ * 		-1 on archive error
++ */
++static int singleRoot(const char *path)
++{
++	struct archive *a;
++	struct archive_entry *entry;
++	int r, ret = -1, rootLen;
++	char *rootName = NULL;
++
++	a = archive_read_new();
++	archive_read_support_filter_all(a);
++	archive_read_support_format_all(a);
++	r = archive_read_open_filename(a, path, 10240);
++	if (r != ARCHIVE_OK) {
++	    goto afree;
++	}
++	if (archive_read_next_header(a, &entry) != ARCHIVE_OK) {
++	    goto afree;
++	}
++	rootName = xstrdup(archive_entry_pathname(entry));
++	rootLen = strlen(rootName);
++	if (archive_entry_filetype(entry) != AE_IFDIR) {
++	    /* Root entry is not a directory */
++	    ret = 0;
++	    goto afree;
++	}
++	while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
++	    if (strncmp(rootName, archive_entry_pathname(entry), rootLen)) {
++		/* multiple top level entries */
++		ret = 0;
++		goto afree;
++	    }
++	}
++	ret = 1;
++
++afree:
++	free(rootName);
++	r = archive_read_free(a);
++	if (r != ARCHIVE_OK)
++	    ret = -1;
++
++	return ret;
++}
++
+ static char *doUntar(const char *fn)
+ {
+     const struct archiveType_s *at = NULL;
+     char *buf = NULL;
+     char *tar = NULL;
+     const char *taropts = verbose ? "-xvvof" : "-xof";
++    char *mkdir = NULL;
++    char *stripcd = NULL;
+ 
+     if ((at = getArchiver(fn)) == NULL)
+ 	goto exit;
+ 
++    if (dstpath) {
++	    int sr = singleRoot(fn);
++
++	    /* the trick is simple, if the archive has multiple entries,
++	     * just extract it into the specified destination path, otherwise
++	     * strip the first path entry and extract in the destination path
++	     */
++	    rasprintf(&mkdir, "mkdir '%s' ; ", dstpath);
++	    rasprintf(&stripcd, " -C '%s' %s", dstpath, sr ? "--strip-components=1" : "");
++    }
+     tar = rpmGetPath("%{__tar}", NULL);
+     if (at->compressed != COMPRESSED_NOT) {
+ 	char *zipper = NULL;
+@@ -96,7 +164,7 @@ static char *doUntar(const char *fn)
+ 	zipper = rpmExpand(at->cmd, " ", at->unpack, " ",
+ 			   verbose ? "" : at->quiet, NULL);
+ 	if (needtar) {
+-	    rasprintf(&buf, "%s '%s' | %s %s -", zipper, fn, tar, taropts);
++	    rasprintf(&buf, "%s %s '%s' | %s %s - %s", mkdir ?: "", zipper, fn, tar, taropts, stripcd ?: "");
+ 	} else if (at->compressed == COMPRESSED_GEM) {
+ 	    char *tmp = xstrdup(fn);
+ 	    const char *bn = basename(tmp);
+@@ -119,11 +187,13 @@ static char *doUntar(const char *fn)
+ 	}
+ 	free(zipper);
+     } else {
+-	rasprintf(&buf, "%s %s '%s'", tar, taropts, fn);
++	rasprintf(&buf, "%s %s %s '%s' %s", mkdir ?: "", tar, taropts, fn, stripcd ?: "");
+     }
+ 
+ exit:
+     free(tar);
++    free(mkdir);
++    free(stripcd);
+     return buf;
+ }
+ 
+-- 
+2.54.0
+
+
+From 8ee83a3174f4956baad3feb164d68d1b48e629b6 Mon Sep 17 00:00:00 2001
+From: Florian Festi <ffesti@redhat.com>
+Date: Mon, 17 Jun 2024 15:03:28 +0200
+Subject: [PATCH 02/14] Pass TZ=UTC to zip in rpmuncompress
+
+The ZIP format has no notion of time zone, so timestamps are only
+meaningful if it is known what time zone they were created in. Pass UTC
+to prevent time stamps to depend on local time zone setting and make
+builds from sources in zip files (more) reproducible.
+
+The other archive formats (7zip using UTC, Ruby gems using tar and tar
+itself) don't have this issue. Everything else are stream compressors
+that don't deal with file meta data.
+
+Tested manually with Fedora's xz-java-1.9.zip as mentioned in the issue.
+
+Resolves: #2955
+(cherry picked from commit b847608cc22236ae19af51dc3faef16398df4110)
+---
+ tools/rpmuncompress.c | 31 +++++++++++++++++--------------
+ 1 file changed, 17 insertions(+), 14 deletions(-)
+
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index c4832332c..4c9e46aad 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -40,19 +40,20 @@ struct archiveType_s {
+     const char *cmd;
+     const char *unpack;
+     const char *quiet;
++    int setTZ;
+ } archiveTypes[] = {
+-    { COMPRESSED_NOT,	0,	"%{__cat}" ,	"",		"" },
+-    { COMPRESSED_OTHER,	0,	"%{__gzip}",	"-dc",		""  },
+-    { COMPRESSED_BZIP2,	0,	"%{__bzip2}",	"-dc",		"" },
+-    { COMPRESSED_ZIP,	1,	"%{__unzip}",	"",		"-qq" },
+-    { COMPRESSED_LZMA,	0,	"%{__xz}",	"-dc",		"" },
+-    { COMPRESSED_XZ,	0,	"%{__xz}",	"-dc",		"" },
+-    { COMPRESSED_LZIP,	0,	"%{__lzip}",	"-dc",		"" },
+-    { COMPRESSED_LRZIP,	0,	"%{__lrzip}",	"-dqo-",	"" },
+-    { COMPRESSED_7ZIP,	1,	"%{__7zip}",	"x",		"" },
+-    { COMPRESSED_ZSTD,	0,	"%{__zstd}",	"-dc",		"" },
+-    { COMPRESSED_GEM,	1,	"%{__gem}",	"unpack",	"" },
+-    { -1,		0,	NULL,		NULL,		NULL },
++    { COMPRESSED_NOT,	0,	"%{__cat}" ,	"",		"", 0 },
++    { COMPRESSED_OTHER,	0,	"%{__gzip}",	"-dc",		"", 0 },
++    { COMPRESSED_BZIP2,	0,	"%{__bzip2}",	"-dc",		"", 0 },
++    { COMPRESSED_ZIP,	1,	"%{__unzip}",	"",		"-qq", 1 },
++    { COMPRESSED_LZMA,	0,	"%{__xz}",	"-dc",		"", 0 },
++    { COMPRESSED_XZ,	0,	"%{__xz}",	"-dc",		"", 0 },
++    { COMPRESSED_LZIP,	0,	"%{__lzip}",	"-dc",		"", 0 },
++    { COMPRESSED_LRZIP,	0,	"%{__lrzip}",	"-dqo-",	"", 0 },
++    { COMPRESSED_7ZIP,	1,	"%{__7zip}",	"x",		"", 0 },
++    { COMPRESSED_ZSTD,	0,	"%{__zstd}",	"-dc",		"", 0 },
++    { COMPRESSED_GEM,	1,	"%{__gem}",	"unpack",	"", 0 },
++    { -1,		0,	NULL,		NULL,		NULL, 0 },
+ };
+ 
+ static const struct archiveType_s *getArchiver(const char *fn)
+@@ -77,7 +78,8 @@ static char *doUncompress(const char *fn)
+     char *cmd = NULL;
+     const struct archiveType_s *at = getArchiver(fn);
+     if (at) {
+-	cmd = rpmExpand(at->cmd, " ", at->unpack, NULL);
++	cmd = rpmExpand(at->setTZ ? "TZ=UTC " : "",
++			at->cmd, " ", at->unpack, NULL);
+ 	/* path must not be expanded */
+ 	cmd = rstrscat(&cmd, " ", fn, NULL);
+     }
+@@ -161,7 +163,8 @@ static char *doUntar(const char *fn)
+ 	char *zipper = NULL;
+ 	int needtar = (at->extractable == 0);
+ 
+-	zipper = rpmExpand(at->cmd, " ", at->unpack, " ",
++	zipper = rpmExpand(at->setTZ ? "TZ=UTC " : "",
++			   at->cmd, " ", at->unpack, " ",
+ 			   verbose ? "" : at->quiet, NULL);
+ 	if (needtar) {
+ 	    rasprintf(&buf, "%s %s '%s' | %s %s - %s", mkdir ?: "", zipper, fn, tar, taropts, stripcd ?: "");
+-- 
+2.54.0
+
+
+From c4f56c0898b470811034f62099f70be733d58b36 Mon Sep 17 00:00:00 2001
+From: Florian Festi <ffesti@redhat.com>
+Date: Mon, 17 Jun 2024 18:19:45 +0200
+Subject: [PATCH 03/14] Free cmd for --dry-run too
+
+Prevent memory leak and the memory sanatizer failing.
+
+(cherry picked from commit e8a252dca9f0731dc5f24d91447a6906363ff9ec)
+---
+ tools/rpmuncompress.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index 4c9e46aad..e8232acbc 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -234,10 +234,10 @@ int main(int argc, char *argv[])
+ 	    if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
+ 		ec = EXIT_SUCCESS;
+ 	}
+-	free(cmd);
+     }
+ 
+ exit:
++    free(cmd);
+     rpmcliFini(optCon);
+     return ec;
+ }
+-- 
+2.54.0
+
+
+From b94d7b57aaa80d7bb171da846215f306b3d78c3d Mon Sep 17 00:00:00 2001
+From: Panu Matilainen <pmatilai@redhat.com>
+Date: Tue, 27 Aug 2024 13:27:20 +0300
+Subject: [PATCH 04/14] Work around unowned directories in rpmuncompress -C
+
+Some archives have a directory prefix but are missing the leading
+directory entry, work around it by just comparing the leading directory
+components (if any) across the archive.
+
+Add tests for unowned directories with rpmuncompress -C
+
+GNU tar seems to always place the root directory node in there, but
+clearly not all implementations do. One of them being Python tarfile
+as of Python 3.12.5, so the test-tarballs created from the pre-existing
+source-singleroot.tar.gz content with:
+
+	import tarfile
+
+	tar = tarfile.open("source-singleroot-unowned1.tar.gz", "w:gz")
+	for name in ["source-strip/file1", "source-strip/file2"]:
+	    tar.add(name)
+	tar.close()
+
+	tar = tarfile.open("source-singleroot-unowned2.tar.gz", "w:gz")
+	for name in ["source-strip/dir1", "source-strip/file1",
+			"source-strip/file2"]:
+	    tar.add(name)
+	tar.close()
+
+Fixes: #3250
+(cherry picked from commit 671fc8e5d6633c14c9621903a23c0040acb43f65)
+---
+ tools/rpmuncompress.c | 13 ++++++++-----
+ 1 file changed, 8 insertions(+), 5 deletions(-)
+
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index e8232acbc..2cf988dac 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -112,15 +112,18 @@ static int singleRoot(const char *path)
+ 	    goto afree;
+ 	}
+ 	rootName = xstrdup(archive_entry_pathname(entry));
+-	rootLen = strlen(rootName);
+-	if (archive_entry_filetype(entry) != AE_IFDIR) {
+-	    /* Root entry is not a directory */
++	char *sep = strchr(rootName, '/');
++	if (sep == NULL) {
++	    /* No directories in the pathname */
+ 	    ret = 0;
+ 	    goto afree;
+ 	}
++
++	/* Do all entries in the archive start with the same lead directory? */
++	rootLen = sep - rootName + 1;
+ 	while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
+-	    if (strncmp(rootName, archive_entry_pathname(entry), rootLen)) {
+-		/* multiple top level entries */
++	    const char *p = archive_entry_pathname(entry);
++	    if (strncmp(rootName, p, rootLen)) {
+ 		ret = 0;
+ 		goto afree;
+ 	    }
+-- 
+2.54.0
+
+
+From b754b7baa2ab07cb4b987b6c4d160a12197e320e Mon Sep 17 00:00:00 2001
+From: Florian Festi <ffesti@redhat.com>
+Date: Thu, 29 Aug 2024 12:29:05 +0200
+Subject: [PATCH 05/14] Replace gcc only ? : operator usage
+
+(cherry picked from commit c90733d96e93f3544853821787190fbb01472b15)
+---
+ tools/rpmuncompress.c | 23 +++++++++++++----------
+ 1 file changed, 13 insertions(+), 10 deletions(-)
+
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index 2cf988dac..a3b7464d2 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -152,14 +152,17 @@ static char *doUntar(const char *fn)
+ 	goto exit;
+ 
+     if (dstpath) {
+-	    int sr = singleRoot(fn);
+-
+-	    /* the trick is simple, if the archive has multiple entries,
+-	     * just extract it into the specified destination path, otherwise
+-	     * strip the first path entry and extract in the destination path
+-	     */
+-	    rasprintf(&mkdir, "mkdir '%s' ; ", dstpath);
+-	    rasprintf(&stripcd, " -C '%s' %s", dstpath, sr ? "--strip-components=1" : "");
++	int sr = singleRoot(fn);
++
++	/* the trick is simple, if the archive has multiple entries,
++	 * just extract it into the specified destination path, otherwise
++	 * strip the first path entry and extract in the destination path
++	 */
++	rasprintf(&mkdir, "mkdir '%s' ; ", dstpath);
++	rasprintf(&stripcd, " -C '%s' %s", dstpath, sr ? "--strip-components=1" : "");
++    } else {
++	mkdir = xstrdup("");
++	stripcd = xstrdup("");
+     }
+     tar = rpmGetPath("%{__tar}", NULL);
+     if (at->compressed != COMPRESSED_NOT) {
+@@ -170,7 +173,7 @@ static char *doUntar(const char *fn)
+ 			   at->cmd, " ", at->unpack, " ",
+ 			   verbose ? "" : at->quiet, NULL);
+ 	if (needtar) {
+-	    rasprintf(&buf, "%s %s '%s' | %s %s - %s", mkdir ?: "", zipper, fn, tar, taropts, stripcd ?: "");
++	    rasprintf(&buf, "%s %s '%s' | %s %s - %s", mkdir, zipper, fn, tar, taropts, stripcd);
+ 	} else if (at->compressed == COMPRESSED_GEM) {
+ 	    char *tmp = xstrdup(fn);
+ 	    const char *bn = basename(tmp);
+@@ -193,7 +196,7 @@ static char *doUntar(const char *fn)
+ 	}
+ 	free(zipper);
+     } else {
+-	rasprintf(&buf, "%s %s %s '%s' %s", mkdir ?: "", tar, taropts, fn, stripcd ?: "");
++	rasprintf(&buf, "%s %s %s '%s' %s", mkdir, tar, taropts, fn, stripcd);
+     }
+ 
+ exit:
+-- 
+2.54.0
+
+
+From dc2a2d06006b3f55dfd4e80bcd8f8e801ff0c0c9 Mon Sep 17 00:00:00 2001
+From: Florian Festi <ffesti@redhat.com>
+Date: Thu, 29 Aug 2024 15:40:18 +0200
+Subject: [PATCH 06/14] Return unique top directory iff it exists
+
+Used in the next commit.
+
+(cherry picked from commit 7b46a0d0c336ee865821de53cdcfff09752669ed)
+---
+ tools/rpmuncompress.c | 18 +++++++++++-------
+ 1 file changed, 11 insertions(+), 7 deletions(-)
+
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index a3b7464d2..88956d173 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -90,11 +90,11 @@ static char *doUncompress(const char *fn)
+  * Detect if an archive has a single top level entry, and it's a directory.
+  *
+  * @param path	path of the archive
+- * @return	1 if archive as only a directory as top level entry,
+- * 		0 if it contains multiple top level entries or a single file
+- * 		-1 on archive error
++ * @return	only top level directory (if any),
++ * 		NULL if it contains multiple top level entries or a single file
++ * 		or on archive error
+  */
+-static int singleRoot(const char *path)
++static char * singleRoot(const char *path)
+ {
+ 	struct archive *a;
+ 	struct archive_entry *entry;
+@@ -128,15 +128,18 @@ static int singleRoot(const char *path)
+ 		goto afree;
+ 	    }
+ 	}
++	*sep = '\0';
+ 	ret = 1;
+ 
+ afree:
+-	free(rootName);
+ 	r = archive_read_free(a);
+ 	if (r != ARCHIVE_OK)
+ 	    ret = -1;
+ 
+-	return ret;
++	if (ret != 1)
++	    rootName = _free(rootName);
++
++	return rootName;
+ }
+ 
+ static char *doUntar(const char *fn)
+@@ -152,7 +155,7 @@ static char *doUntar(const char *fn)
+ 	goto exit;
+ 
+     if (dstpath) {
+-	int sr = singleRoot(fn);
++	char * sr = singleRoot(fn);
+ 
+ 	/* the trick is simple, if the archive has multiple entries,
+ 	 * just extract it into the specified destination path, otherwise
+@@ -160,6 +163,7 @@ static char *doUntar(const char *fn)
+ 	 */
+ 	rasprintf(&mkdir, "mkdir '%s' ; ", dstpath);
+ 	rasprintf(&stripcd, " -C '%s' %s", dstpath, sr ? "--strip-components=1" : "");
++	free(sr);
+     } else {
+ 	mkdir = xstrdup("");
+ 	stripcd = xstrdup("");
+-- 
+2.54.0
+
+
+From 5847a366c9172eaabd2743119c8f8b94b0b3e9ef Mon Sep 17 00:00:00 2001
+From: Florian Festi <ffesti@redhat.com>
+Date: Thu, 29 Aug 2024 15:11:51 +0200
+Subject: [PATCH 07/14] rpmuncompress: Support -C for zip and 7zip archives
+
+As both don't support an equivalent to tars --strip-components=1 we move
+the files out of top directory ourselves with shell commands.
+
+Support for Ruby gems is still missing - iff at all possible or
+desirable.
+
+Resolves: #3249
+(backported from commit abf057616d8f73cc0540520121ea3c4cad5d8cdd)
+---
+ tools/rpmuncompress.c | 58 +++++++++++++++++++++++++++++--------------
+ 1 file changed, 40 insertions(+), 18 deletions(-)
+
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index 88956d173..692ca322f 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -40,19 +40,20 @@ struct archiveType_s {
+     const char *cmd;
+     const char *unpack;
+     const char *quiet;
++    const char *dest;
+     int setTZ;
+ } archiveTypes[] = {
+-    { COMPRESSED_NOT,	0,	"%{__cat}" ,	"",		"", 0 },
+-    { COMPRESSED_OTHER,	0,	"%{__gzip}",	"-dc",		"", 0 },
+-    { COMPRESSED_BZIP2,	0,	"%{__bzip2}",	"-dc",		"", 0 },
+-    { COMPRESSED_ZIP,	1,	"%{__unzip}",	"",		"-qq", 1 },
+-    { COMPRESSED_LZMA,	0,	"%{__xz}",	"-dc",		"", 0 },
+-    { COMPRESSED_XZ,	0,	"%{__xz}",	"-dc",		"", 0 },
+-    { COMPRESSED_LZIP,	0,	"%{__lzip}",	"-dc",		"", 0 },
+-    { COMPRESSED_LRZIP,	0,	"%{__lrzip}",	"-dqo-",	"", 0 },
+-    { COMPRESSED_7ZIP,	1,	"%{__7zip}",	"x",		"", 0 },
+-    { COMPRESSED_ZSTD,	0,	"%{__zstd}",	"-dc",		"", 0 },
+-    { COMPRESSED_GEM,	1,	"%{__gem}",	"unpack",	"", 0 },
++    { COMPRESSED_NOT,	0,	"%{__cat}" ,	"",		"", "", 0 },
++    { COMPRESSED_OTHER,	0,	"%{__gzip}",	"-dc",		"", "", 0 },
++    { COMPRESSED_BZIP2,	0,	"%{__bzip2}",	"-dc",		"", "", 0 },
++    { COMPRESSED_ZIP,	1,	"%{__unzip}",	"",		"-qq", "-d", 1 },
++    { COMPRESSED_LZMA,	0,	"%{__xz}",	"-dc",		"", "", 0 },
++    { COMPRESSED_XZ,	0,	"%{__xz}",	"-dc",		"", "", 0 },
++    { COMPRESSED_LZIP,	0,	"%{__lzip}",	"-dc",		"", "", 0 },
++    { COMPRESSED_LRZIP,	0,	"%{__lrzip}",	"-dqo-",	"", "", 0 },
++    { COMPRESSED_7ZIP,	1,	"%{__7zip}",	"x",		"-bso0 -bsp0", "-o", 0 },
++    { COMPRESSED_ZSTD,	0,	"%{__zstd}",	"-dc",		"", "", 0 },
++    { COMPRESSED_GEM,	1,	"%{__gem}",	"unpack",	"", "--target=", 0 },
+     { -1,		0,	NULL,		NULL,		NULL, 0 },
+ };
+ 
+@@ -154,15 +155,37 @@ static char *doUntar(const char *fn)
+     if ((at = getArchiver(fn)) == NULL)
+ 	goto exit;
+ 
++    int needtar = (at->extractable == 0);
++
+     if (dstpath) {
+ 	char * sr = singleRoot(fn);
+ 
+-	/* the trick is simple, if the archive has multiple entries,
+-	 * just extract it into the specified destination path, otherwise
+-	 * strip the first path entry and extract in the destination path
++	/* if the archive has multiple entries, just extract it into the
++	 * specified destination path, otherwise also strip the first path
++	 * entry
+ 	 */
+-	rasprintf(&mkdir, "mkdir '%s' ; ", dstpath);
+-	rasprintf(&stripcd, " -C '%s' %s", dstpath, sr ? "--strip-components=1" : "");
++	if (needtar) {
++	    rasprintf(&mkdir, "mkdir -p '%s' ; ", dstpath);
++	    rasprintf(&stripcd, " -C '%s' %s", dstpath, sr ? "--strip-components=1" : "");
++	} else {
++	    if (sr) {
++		rasprintf(&mkdir, "mkdir -p '%s' ; tmp=`mktemp -d -p'%s'` ; ",
++			  dstpath, dstpath);
++		char * moveup;
++		/* Extract into temp directory to avoid collisions */
++		/* then move files in top dir two levels up */
++		rasprintf(
++		    &moveup,
++		    " && "
++		    "(shopt -s dotglob; mv \"$tmp\"/'%s'/* '%s') && "
++		    "rmdir \"$tmp\"/'%s' \"$tmp\" ", sr, dstpath, sr);
++		rasprintf(&stripcd, "%s\"$tmp\" %s", at->dest, moveup);
++		free(moveup);
++	    } else {
++		rasprintf(&mkdir, "mkdir -p '%s' ; ", dstpath);
++		rasprintf(&stripcd, "%s'%s'", at->dest, dstpath);
++	    }
++	}
+ 	free(sr);
+     } else {
+ 	mkdir = xstrdup("");
+@@ -171,7 +194,6 @@ static char *doUntar(const char *fn)
+     tar = rpmGetPath("%{__tar}", NULL);
+     if (at->compressed != COMPRESSED_NOT) {
+ 	char *zipper = NULL;
+-	int needtar = (at->extractable == 0);
+ 
+ 	zipper = rpmExpand(at->setTZ ? "TZ=UTC " : "",
+ 			   at->cmd, " ", at->unpack, " ",
+@@ -196,7 +218,7 @@ static char *doUntar(const char *fn)
+ 	    free(gem);
+ 	    free(tmp);
+ 	} else {
+-	    rasprintf(&buf, "%s '%s'", zipper, fn);
++	    rasprintf(&buf, "%s%s '%s' %s", mkdir, zipper, fn, stripcd);
+ 	}
+ 	free(zipper);
+     } else {
+-- 
+2.54.0
+
+
+From 127d4a2947005688e645b9e9545b178efd5c0e6c Mon Sep 17 00:00:00 2001
+From: Panu Matilainen <pmatilai@redhat.com>
+Date: Mon, 2 Sep 2024 10:51:57 +0300
+Subject: [PATCH 08/14] Fix rpmuncompress going interactive on re-extraction of
+ zip / 7zip archives
+
+Testing this is a PITA compared to the issue itself: we can't use the
+existing -C tests because that fails due to mv, and to avoid -C we
+need to control where it runs, and to do that we need --chdir which
+causes extra warnings from brap so we need to ignore stderr which
+we would not want to do here, really.
+
+Fixes: #2779
+(cherry picked from commit fa3ec7e3d73a93911bb8d3d1ac87c2d64eeda680)
+---
+ tools/rpmuncompress.c | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index 692ca322f..30bd74a2a 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -46,12 +46,12 @@ struct archiveType_s {
+     { COMPRESSED_NOT,	0,	"%{__cat}" ,	"",		"", "", 0 },
+     { COMPRESSED_OTHER,	0,	"%{__gzip}",	"-dc",		"", "", 0 },
+     { COMPRESSED_BZIP2,	0,	"%{__bzip2}",	"-dc",		"", "", 0 },
+-    { COMPRESSED_ZIP,	1,	"%{__unzip}",	"",		"-qq", "-d", 1 },
++    { COMPRESSED_ZIP,	1,	"%{__unzip}",	"-u",		"-qq", "-d", 1 },
+     { COMPRESSED_LZMA,	0,	"%{__xz}",	"-dc",		"", "", 0 },
+     { COMPRESSED_XZ,	0,	"%{__xz}",	"-dc",		"", "", 0 },
+     { COMPRESSED_LZIP,	0,	"%{__lzip}",	"-dc",		"", "", 0 },
+     { COMPRESSED_LRZIP,	0,	"%{__lrzip}",	"-dqo-",	"", "", 0 },
+-    { COMPRESSED_7ZIP,	1,	"%{__7zip}",	"x",		"-bso0 -bsp0", "-o", 0 },
++    { COMPRESSED_7ZIP,	1,	"%{__7zip}",	"x -y",		"-bso0 -bsp0", "-o", 0 },
+     { COMPRESSED_ZSTD,	0,	"%{__zstd}",	"-dc",		"", "", 0 },
+     { COMPRESSED_GEM,	1,	"%{__gem}",	"unpack",	"", "--target=", 0 },
+     { -1,		0,	NULL,		NULL,		NULL, 0 },
+-- 
+2.54.0
+
+
+From b28d1c4b8cafc45498b93a324cb8dd52ab2a753b Mon Sep 17 00:00:00 2001
+From: Panu Matilainen <pmatilai@redhat.com>
+Date: Thu, 3 Oct 2024 12:51:05 +0300
+Subject: [PATCH 09/14] Fix up rpmuncompress not being compiled as C++
+
+This one lone source got overlooked in the initial C++ enablement
+in commit 37cdcc29269eb5cd603c69be07f83f113ce479cd. C++ doesn't like
+jumping over variable declarations, so just move the declarations
+early to minimally fix the build.
+
+Nicely goes to show the danger of such hacks - you can always miss
+something. On the library side things are likely to blow up in
+linkage but you never know really.
+
+(backported from commit 826339518a5ee43498ab0fe047a87540a980232e)
+---
+ tools/rpmuncompress.c | 6 ++++--
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index 30bd74a2a..932ae3a3d 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -101,6 +101,7 @@ static char * singleRoot(const char *path)
+ 	struct archive_entry *entry;
+ 	int r, ret = -1, rootLen;
+ 	char *rootName = NULL;
++	char *sep = NULL;
+ 
+ 	a = archive_read_new();
+ 	archive_read_support_filter_all(a);
+@@ -113,7 +114,7 @@ static char * singleRoot(const char *path)
+ 	    goto afree;
+ 	}
+ 	rootName = xstrdup(archive_entry_pathname(entry));
+-	char *sep = strchr(rootName, '/');
++	sep = strchr(rootName, '/');
+ 	if (sep == NULL) {
+ 	    /* No directories in the pathname */
+ 	    ret = 0;
+@@ -151,11 +152,12 @@ static char *doUntar(const char *fn)
+     const char *taropts = verbose ? "-xvvof" : "-xof";
+     char *mkdir = NULL;
+     char *stripcd = NULL;
++    int needtar = 0;
+ 
+     if ((at = getArchiver(fn)) == NULL)
+ 	goto exit;
+ 
+-    int needtar = (at->extractable == 0);
++    needtar = (at->extractable == 0);
+ 
+     if (dstpath) {
+ 	char * sr = singleRoot(fn);
+-- 
+2.54.0
+
+
+From 0988bb56aa86c3fb0719e45910621068830f916b Mon Sep 17 00:00:00 2001
+From: Michal Domonkos <mdomonko@redhat.com>
+Date: Tue, 9 Jun 2026 15:57:08 +0200
+Subject: [PATCH 10/14] Refactor singleRoot()
+
+Locate the slash first in the returned const string and only duplicate
+it when actually needed. This will save us an extra string copy in the
+next commits.
+
+No functional change.
+
+(cherry picked from commit 24744c59d359cc51fc644ddf2707c4ef1a639cf6)
+---
+ tools/rpmuncompress.c | 18 ++++++++++++------
+ 1 file changed, 12 insertions(+), 6 deletions(-)
+
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index 932ae3a3d..68d1481f6 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -100,8 +100,9 @@ static char * singleRoot(const char *path)
+ 	struct archive *a;
+ 	struct archive_entry *entry;
+ 	int r, ret = -1, rootLen;
++	const char *p = NULL;
++	const char *sep = NULL;
+ 	char *rootName = NULL;
+-	char *sep = NULL;
+ 
+ 	a = archive_read_new();
+ 	archive_read_support_filter_all(a);
+@@ -113,24 +114,29 @@ static char * singleRoot(const char *path)
+ 	if (archive_read_next_header(a, &entry) != ARCHIVE_OK) {
+ 	    goto afree;
+ 	}
+-	rootName = xstrdup(archive_entry_pathname(entry));
+-	sep = strchr(rootName, '/');
++
++	/* Extract the lead directory from the first entry */
++	p = archive_entry_pathname(entry);
++	sep = strchr(p, '/');
+ 	if (sep == NULL) {
+ 	    /* No directories in the pathname */
+ 	    ret = 0;
+ 	    goto afree;
++	} else {
++	    rootName = xstrdup(p);
++	    rootLen = sep - p + 1;
+ 	}
+ 
+ 	/* Do all entries in the archive start with the same lead directory? */
+-	rootLen = sep - rootName + 1;
+ 	while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
+-	    const char *p = archive_entry_pathname(entry);
++	    p = archive_entry_pathname(entry);
+ 	    if (strncmp(rootName, p, rootLen)) {
+ 		ret = 0;
+ 		goto afree;
+ 	    }
+ 	}
+-	*sep = '\0';
++
++	rootName[rootLen - 1] = '\0';
+ 	ret = 1;
+ 
+ afree:
+-- 
+2.54.0
+
+
+From 3a441228720f71bcc132e535c36913be9cc69322 Mon Sep 17 00:00:00 2001
+From: Michal Domonkos <mdomonko@redhat.com>
+Date: Tue, 9 Jun 2026 16:34:25 +0200
+Subject: [PATCH 11/14] Invert conditional in singleRoot()
+
+No functional change, just makes the next commit simpler.
+
+(cherry picked from commit 94cfcceff77d730f1c3432fdb572fc175a3ffdf6)
+---
+ tools/rpmuncompress.c | 8 ++++----
+ 1 file changed, 4 insertions(+), 4 deletions(-)
+
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index 68d1481f6..eef2f4f0e 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -118,13 +118,13 @@ static char * singleRoot(const char *path)
+ 	/* Extract the lead directory from the first entry */
+ 	p = archive_entry_pathname(entry);
+ 	sep = strchr(p, '/');
+-	if (sep == NULL) {
++	if (sep) {
++	    rootName = xstrdup(p);
++	    rootLen = sep - p + 1;
++	} else {
+ 	    /* No directories in the pathname */
+ 	    ret = 0;
+ 	    goto afree;
+-	} else {
+-	    rootName = xstrdup(p);
+-	    rootLen = sep - p + 1;
+ 	}
+ 
+ 	/* Do all entries in the archive start with the same lead directory? */
+-- 
+2.54.0
+
+
+From 18cc9e175ac6807148144099acea22147da3dddf Mon Sep 17 00:00:00 2001
+From: Michal Domonkos <mdomonko@redhat.com>
+Date: Tue, 9 Jun 2026 16:20:59 +0200
+Subject: [PATCH 12/14] Handle singleroot archives without trailing slash
+
+While some tar implementations (like GNU tar) do include a trailing
+slash in directory entries, others (like ptar(1) written in Perl) do
+not. Handle the latter in the singleroot detection logic as well.
+
+Add a test archive generated with ptar(1) that has the following layout:
+
+    source-strip
+    source-strip/file2
+    source-strip/file1
+    source-strip/dir1
+    source-strip/dir1/file3
+
+Fixes: #4182
+(cherry picked from commit 27ff4d457ff66673f66b0933234e5ed56932641f)
+---
+ tools/rpmuncompress.c | 3 +++
+ 1 file changed, 3 insertions(+)
+
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index eef2f4f0e..6aff47757 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -121,6 +121,9 @@ static char * singleRoot(const char *path)
+ 	if (sep) {
+ 	    rootName = xstrdup(p);
+ 	    rootLen = sep - p + 1;
++	} else if (archive_entry_filetype(entry) == AE_IFDIR) {
++	    rootName = rstrscat(NULL, p, "/", NULL);
++	    rootLen = strlen(rootName);
+ 	} else {
+ 	    /* No directories in the pathname */
+ 	    ret = 0;
+-- 
+2.54.0
+
+
+From ccdd46de999955b28ff5ae54b738137a8e14a8c7 Mon Sep 17 00:00:00 2001
+From: Michal Domonkos <mdomonko@redhat.com>
+Date: Fri, 12 Jun 2026 13:13:22 +0200
+Subject: [PATCH 13/14] Prevent command injection in rpmuncompress(1)
+
+When extracting a singleroot archive in -C mode with an archiver that
+can uncompress and extract in one go (such as zip), we will include the
+top-level directory name verbatim in the shell command that does our own
+variant of tar's --strip-components=1. This allows an attacker to craft
+such an archive where the top-level directory name contains arbitrary
+shell code enclosed in single quotes, which rpmuncompress will happily
+execute when merely extracting it.
+
+That is nasty since one does not expect a utility like rpmuncompress to
+*execute* anything from the archive when extracting it. This is now even
+more relevant with commit a698d1b6b18e430806c54755af56d47ac401e430 which
+exposed rpmuncompress for general use by installing it into $PATH.
+
+Fix by simply *not* passing the top-level directory name, as returned by
+singleRoot(), to the shell, ever. Luckily, we don't really need to since
+we know that the temp directory will only have a single top-level entry,
+and therefore can use a glob to just let the shell expand to it.
+
+This relies on our singleroot detection logic *and* the archiver having
+a common understanding of what a singleroot archive is, of course, which
+may not be ideal. What could theoretically happen is that we consider an
+archive singleroot but the archiver, for some reason, extracts multiple
+top-level entries, or the single entry will have a different name. This
+could result in the destination directory to have an unexpected layout
+once we do the move. That said, this is all just speculation, and even
+if true, does not seem to be a security flaw unlike the one being fixed.
+
+An alternative fix would be sanitizing the directory name before passing
+it to the shell (like the suggested patch in CVE-2026-44604) but that is
+too brittle, error prone and just unnecessarily complicated.
+
+Ideally, we would just not shell out at all, and use the libarchive API
+to do the whole dance, which we may in the future (see #4236). But for
+now, this minimal fix should do the job.
+
+Add also a test archive created with the following Python 3 code adapted
+from the CVE:
+
+    import zipfile
+    name = "evil'$(touch beenhere)'"
+    with zipfile.ZipFile("source-singleroot-evil.zip", "w") as z:
+        z.writestr(f"{name}/README.txt", "x")
+
+Fixes: CVE-2026-44604
+(cherry picked from commit 0693ce7674c423f382b148763a29f7faa0aacb41)
+---
+ tools/rpmuncompress.c | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index 6aff47757..3652cc6bb 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -188,8 +188,8 @@ static char *doUntar(const char *fn)
+ 		rasprintf(
+ 		    &moveup,
+ 		    " && "
+-		    "(shopt -s dotglob; mv \"$tmp\"/'%s'/* '%s') && "
+-		    "rmdir \"$tmp\"/'%s' \"$tmp\" ", sr, dstpath, sr);
++		    "(shopt -s dotglob; mv \"$tmp\"/*/* '%s') && "
++		    "rmdir \"$tmp\"/* \"$tmp\" ", dstpath);
+ 		rasprintf(&stripcd, "%s\"$tmp\" %s", at->dest, moveup);
+ 		free(moveup);
+ 	    } else {
+-- 
+2.54.0
+
+
+From 6ee2186a8b246d7cd8efff6c449c509669206760 Mon Sep 17 00:00:00 2001
+From: Michal Domonkos <mdomonko@redhat.com>
+Date: Fri, 12 Jun 2026 13:16:39 +0200
+Subject: [PATCH 14/14] Revert "Return unique top directory iff it exists"
+
+Now that we no longer need the singleroot name in the caller, go back to
+just returning an integer. This also prevents an accidental inclusion of
+the name in a shell command in the future (see previous commit).
+
+Adjust and simplify the singleRoot() description comment while at it.
+
+This reverts commit 7b46a0d0c336ee865821de53cdcfff09752669ed.
+
+(cherry picked from commit 3a9145cddff47e6324a9fff3c9a6ae036d7a7db5)
+---
+ tools/rpmuncompress.c | 18 +++++++-----------
+ 1 file changed, 7 insertions(+), 11 deletions(-)
+
+diff --git a/tools/rpmuncompress.c b/tools/rpmuncompress.c
+index 3652cc6bb..7233e4bb9 100644
+--- a/tools/rpmuncompress.c
++++ b/tools/rpmuncompress.c
+@@ -91,11 +91,11 @@ static char *doUncompress(const char *fn)
+  * Detect if an archive has a single top level entry, and it's a directory.
+  *
+  * @param path	path of the archive
+- * @return	only top level directory (if any),
+- * 		NULL if it contains multiple top level entries or a single file
+- * 		or on archive error
++ * @return	1 if the archive has a single top level, directory entry,
++ * 		0 otherwise,
++ * 		-1 on archive error
+  */
+-static char * singleRoot(const char *path)
++static int singleRoot(const char *path)
+ {
+ 	struct archive *a;
+ 	struct archive_entry *entry;
+@@ -139,18 +139,15 @@ static char * singleRoot(const char *path)
+ 	    }
+ 	}
+ 
+-	rootName[rootLen - 1] = '\0';
+ 	ret = 1;
+ 
+ afree:
++	free(rootName);
+ 	r = archive_read_free(a);
+ 	if (r != ARCHIVE_OK)
+ 	    ret = -1;
+ 
+-	if (ret != 1)
+-	    rootName = _free(rootName);
+-
+-	return rootName;
++	return ret;
+ }
+ 
+ static char *doUntar(const char *fn)
+@@ -169,7 +166,7 @@ static char *doUntar(const char *fn)
+     needtar = (at->extractable == 0);
+ 
+     if (dstpath) {
+-	char * sr = singleRoot(fn);
++	int sr = singleRoot(fn);
+ 
+ 	/* if the archive has multiple entries, just extract it into the
+ 	 * specified destination path, otherwise also strip the first path
+@@ -197,7 +194,6 @@ static char *doUntar(const char *fn)
+ 		rasprintf(&stripcd, "%s'%s'", at->dest, dstpath);
+ 	    }
+ 	}
+-	free(sr);
+     } else {
+ 	mkdir = xstrdup("");
+ 	stripcd = xstrdup("");
+-- 
+2.54.0
+

diff --git a/rpm.spec b/rpm.spec
index b6583a2..b49dafd 100644
--- a/rpm.spec
+++ b/rpm.spec
@@ -27,7 +27,7 @@
 
 %global rpmver 4.19.1.1
 #global snapver rc1
-%global baserelease 23
+%global baserelease 24
 %global sover 10
 
 %global srcver %{rpmver}%{?snapver:-%{snapver}}
@@ -175,6 +175,8 @@ rpm-4.19.x-multisig-verify-fixes.patch
 rpm-4.19.x-nsswitch-enable.patch
 0001-Fix-empty-password-field-in-passwd-group-causing-ent.patch
 
+rpm-4.19.x-add-autosetup-C.patch
+
 # These are not yet upstream
 rpm-4.7.1-geode-i686.patch
 
@@ -664,6 +666,9 @@ fi
 %doc %{_defaultdocdir}/rpm/API/
 
 %changelog
+* Thu Jun 18 2026 Michal Domonkos <mdomonko@redhat.com> - 4.19.1.1-24
+- Add support for %%autosetup -C (RHEL-141269)
+
 * Thu Feb 05 2026 Michal Domonkos <mdomonko@redhat.com> - 4.19.1.1-23
 - Fix key import API to return NOTTRUSTED for disabled algorithms (RHEL-112394)
 

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

only message in thread, other threads:[~2026-06-25  7:43 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-25  7:43 [rpms/python3-rpm] epel10: Add support for %autosetup -C Michal Domonkos

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