[PATCH v1] tar: add --occurrence[=NUM] option

Gavin Li gavinli at thegavinli.com
Mon Aug 21 19:17:29 UTC 2023


Enable GNU tar's --occurrence option when FEATURE_TAR_LONG_OPTIONS is enabled.

Signed-off-by: Gavin Li <gavinli at thegavinli.com>
---
 archival/ar.c                                 |  6 +-
 archival/cpio.c                               |  2 +-
 archival/dpkg.c                               | 38 ++++++------
 archival/dpkg_deb.c                           | 24 ++++----
 archival/libarchive/Kbuild.src                |  2 +
 archival/libarchive/accept_add_to.c           | 15 +++++
 archival/libarchive/accept_add_to_end.c       | 15 +++++
 archival/libarchive/filter_accept_list.c      |  2 +-
 .../libarchive/filter_accept_list_reassign.c  |  2 +-
 .../libarchive/filter_accept_reject_list.c    | 18 +++++-
 archival/libarchive/get_header_tar.c          |  7 +++
 archival/tar.c                                | 59 ++++++++++++++-----
 include/bb_archive.h                          | 14 ++++-
 testsuite/tar/tar-handles-occurrence          | 18 ++++++
 14 files changed, 168 insertions(+), 54 deletions(-)
 create mode 100644 archival/libarchive/accept_add_to.c
 create mode 100644 archival/libarchive/accept_add_to_end.c
 create mode 100644 testsuite/tar/tar-handles-occurrence

diff --git a/archival/ar.c b/archival/ar.c
index 320cbae72..f1df21e95 100644
--- a/archival/ar.c
+++ b/archival/ar.c
@@ -56,7 +56,7 @@
 /* filter out entries with same names as specified on the command line */
 static char FAST_FUNC filter_replaceable(archive_handle_t *handle)
 {
-	if (find_list_entry(handle->accept, handle->file_header->name))
+	if (find_list_entry((llist_t *)handle->accept, handle->file_header->name))
 		return EXIT_FAILURE;
 
 	return EXIT_SUCCESS;
@@ -124,7 +124,7 @@ static int write_ar_header(archive_handle_t *handle)
 	struct stat st;
 	int fd;
 
-	fn = llist_pop(&handle->accept);
+	fn = llist_pop((llist_t **)&handle->accept);
 	if (!fn)
 		return -1;
 
@@ -287,7 +287,7 @@ int ar_main(int argc UNUSED_PARAM, char **argv)
 	if (*argv)
 		archive_handle->filter = filter_accept_list;
 	while (*argv) {
-		llist_add_to_end(&archive_handle->accept, *argv++);
+		accept_add_to_end(&archive_handle->accept, *argv++);
 	}
 
 #if ENABLE_FEATURE_AR_CREATE
diff --git a/archival/cpio.c b/archival/cpio.c
index f0d990048..4950d6ead 100644
--- a/archival/cpio.c
+++ b/archival/cpio.c
@@ -559,7 +559,7 @@ int cpio_main(int argc UNUSED_PARAM, char **argv)
 
 	while (*argv) {
 		archive_handle->filter = filter_accept_list;
-		llist_add_to(&archive_handle->accept, *argv);
+		accept_add_to(&archive_handle->accept, *argv);
 		argv++;
 	}
 
diff --git a/archival/dpkg.c b/archival/dpkg.c
index 8031956e9..23e46e062 100644
--- a/archival/dpkg.c
+++ b/archival/dpkg.c
@@ -1495,15 +1495,15 @@ static void init_archive_deb_control(archive_handle_t *ar_handle)
 	tar_handle->src_fd = ar_handle->src_fd;
 
 	/* We don't care about data.tar.* or debian-binary, just control.tar.* */
-	llist_add_to(&(ar_handle->accept), (char*)"control.tar");
+	accept_add_to(&(ar_handle->accept), (char*)"control.tar");
 #if ENABLE_FEATURE_SEAMLESS_GZ
-	llist_add_to(&(ar_handle->accept), (char*)"control.tar.gz");
+	accept_add_to(&(ar_handle->accept), (char*)"control.tar.gz");
 #endif
 #if ENABLE_FEATURE_SEAMLESS_BZ2
-	llist_add_to(&(ar_handle->accept), (char*)"control.tar.bz2");
+	accept_add_to(&(ar_handle->accept), (char*)"control.tar.bz2");
 #endif
 #if ENABLE_FEATURE_SEAMLESS_XZ
-	llist_add_to(&(ar_handle->accept), (char*)"control.tar.xz");
+	accept_add_to(&(ar_handle->accept), (char*)"control.tar.xz");
 #endif
 
 	/* Assign the tar handle as a subarchive of the ar handle */
@@ -1519,18 +1519,18 @@ static void init_archive_deb_data(archive_handle_t *ar_handle)
 	tar_handle->src_fd = ar_handle->src_fd;
 
 	/* We don't care about control.tar.* or debian-binary, just data.tar.* */
-	llist_add_to(&(ar_handle->accept), (char*)"data.tar");
+	accept_add_to(&(ar_handle->accept), (char*)"data.tar");
 #if ENABLE_FEATURE_SEAMLESS_GZ
-	llist_add_to(&(ar_handle->accept), (char*)"data.tar.gz");
+	accept_add_to(&(ar_handle->accept), (char*)"data.tar.gz");
 #endif
 #if ENABLE_FEATURE_SEAMLESS_BZ2
-	llist_add_to(&(ar_handle->accept), (char*)"data.tar.bz2");
+	accept_add_to(&(ar_handle->accept), (char*)"data.tar.bz2");
 #endif
 #if ENABLE_FEATURE_SEAMLESS_LZMA
-	llist_add_to(&(ar_handle->accept), (char*)"data.tar.lzma");
+	accept_add_to(&(ar_handle->accept), (char*)"data.tar.lzma");
 #endif
 #if ENABLE_FEATURE_SEAMLESS_XZ
-	llist_add_to(&(ar_handle->accept), (char*)"data.tar.xz");
+	accept_add_to(&(ar_handle->accept), (char*)"data.tar.xz");
 #endif
 
 	/* Assign the tar handle as a subarchive of the ar handle */
@@ -1545,7 +1545,7 @@ static void FAST_FUNC data_extract_to_buffer(archive_handle_t *archive_handle)
 	xread(archive_handle->src_fd, archive_handle->dpkg__buffer, size);
 }
 
-static char *deb_extract_control_file_to_buffer(archive_handle_t *ar_handle, llist_t *myaccept)
+static char *deb_extract_control_file_to_buffer(archive_handle_t *ar_handle, accept_llist_t *myaccept)
 {
 	ar_handle->dpkg__sub_archive->action_data = data_extract_to_buffer;
 	ar_handle->dpkg__sub_archive->accept = myaccept;
@@ -1557,7 +1557,7 @@ static char *deb_extract_control_file_to_buffer(archive_handle_t *ar_handle, lli
 	return ar_handle->dpkg__sub_archive->dpkg__buffer;
 }
 
-static void append_control_file_to_llist(const char *package_name, const char *control_name, llist_t **ll)
+static void append_control_file_to_llist(const char *package_name, const char *control_name, accept_llist_t **ll)
 {
 	FILE *fp;
 	char *filename, *line;
@@ -1567,7 +1567,7 @@ static void append_control_file_to_llist(const char *package_name, const char *c
 	free(filename);
 	if (fp != NULL) {
 		while ((line = xmalloc_fgetline(fp)) != NULL)
-			llist_add_to(ll, line);
+			accept_add_to(ll, line);
 		fclose(fp);
 	}
 }
@@ -1578,7 +1578,7 @@ static char FAST_FUNC filter_rename_config(archive_handle_t *archive_handle)
 	char *name_ptr = archive_handle->file_header->name + 1;
 
 	/* Is this file marked as config file? */
-	if (!find_list_entry(archive_handle->accept, name_ptr))
+	if (!find_list_entry((llist_t *)archive_handle->accept, name_ptr))
 		return EXIT_SUCCESS; /* no */
 
 	fd = open(name_ptr, O_RDONLY);
@@ -1600,7 +1600,7 @@ static char FAST_FUNC filter_rename_config(archive_handle_t *archive_handle)
 		free(buf);
 
 		/* Is it changed after install? */
-		if (find_list_entry(archive_handle->accept, md5line) == NULL) {
+		if (find_list_entry((llist_t *)archive_handle->accept, md5line) == NULL) {
 			printf("Warning: Creating %s as %s.dpkg-new\n", name_ptr, name_ptr);
 			archive_handle->file_header->name = xasprintf("%s.dpkg-new", archive_handle->file_header->name);
 		}
@@ -1660,8 +1660,8 @@ static void unpack_package(deb_file_t *deb_file)
 	char *list_filename;
 	archive_handle_t *archive_handle;
 	FILE *out_stream;
-	llist_t *accept_list;
-	llist_t *conffile_list;
+	accept_llist_t *accept_list;
+	accept_llist_t *conffile_list;
 	int i;
 
 	/* If existing version, remove it first */
@@ -1690,7 +1690,7 @@ static void unpack_package(deb_file_t *deb_file)
 	i = 0;
 	while (i < ARRAY_SIZE(all_control_files)) {
 		char *c = xasprintf("./%s", all_control_files[i]);
-		llist_add_to(&accept_list, c);
+		accept_add_to(&accept_list, c);
 		i++;
 	}
 	archive_handle->dpkg__sub_archive->accept = accept_list;
@@ -1831,10 +1831,10 @@ int dpkg_main(int argc UNUSED_PARAM, char **argv)
 		if (opt & (OPT_install | OPT_unpack)) {
 			/* -i/-u: require filename */
 			archive_handle_t *archive_handle;
-			llist_t *control_list = NULL;
+			accept_llist_t *control_list = NULL;
 
 			/* Extract the control file */
-			llist_add_to(&control_list, (char*)"./control");
+			accept_add_to(&control_list, (char*)"./control");
 			archive_handle = init_archive_deb_ar(argv[0]);
 			init_archive_deb_control(archive_handle);
 			deb_file[deb_count]->control_file = deb_extract_control_file_to_buffer(archive_handle, control_list);
diff --git a/archival/dpkg_deb.c b/archival/dpkg_deb.c
index dda931169..988c6bcad 100644
--- a/archival/dpkg_deb.c
+++ b/archival/dpkg_deb.c
@@ -47,7 +47,7 @@ int dpkg_deb_main(int argc UNUSED_PARAM, char **argv)
 {
 	archive_handle_t *ar_archive;
 	archive_handle_t *tar_archive;
-	llist_t *control_tar_llist = NULL;
+	accept_llist_t *control_tar_llist = NULL;
 	unsigned opt;
 	const char *extract_dir;
 
@@ -59,23 +59,23 @@ int dpkg_deb_main(int argc UNUSED_PARAM, char **argv)
 	ar_archive->dpkg__sub_archive = tar_archive;
 	ar_archive->filter = filter_accept_list_reassign;
 
-	llist_add_to(&ar_archive->accept, (char*)"data.tar");
-	llist_add_to(&control_tar_llist, (char*)"control.tar");
+	accept_add_to(&ar_archive->accept, (char*)"data.tar");
+	accept_add_to(&control_tar_llist, (char*)"control.tar");
 #if ENABLE_FEATURE_SEAMLESS_GZ
-	llist_add_to(&ar_archive->accept, (char*)"data.tar.gz");
-	llist_add_to(&control_tar_llist, (char*)"control.tar.gz");
+	accept_add_to(&ar_archive->accept, (char*)"data.tar.gz");
+	accept_add_to(&control_tar_llist, (char*)"control.tar.gz");
 #endif
 #if ENABLE_FEATURE_SEAMLESS_BZ2
-	llist_add_to(&ar_archive->accept, (char*)"data.tar.bz2");
-	llist_add_to(&control_tar_llist, (char*)"control.tar.bz2");
+	accept_add_to(&ar_archive->accept, (char*)"data.tar.bz2");
+	accept_add_to(&control_tar_llist, (char*)"control.tar.bz2");
 #endif
 #if ENABLE_FEATURE_SEAMLESS_LZMA
-	llist_add_to(&ar_archive->accept, (char*)"data.tar.lzma");
-	llist_add_to(&control_tar_llist, (char*)"control.tar.lzma");
+	accept_add_to(&ar_archive->accept, (char*)"data.tar.lzma");
+	accept_add_to(&control_tar_llist, (char*)"control.tar.lzma");
 #endif
 #if ENABLE_FEATURE_SEAMLESS_XZ
-	llist_add_to(&ar_archive->accept, (char*)"data.tar.xz");
-	llist_add_to(&control_tar_llist, (char*)"control.tar.xz");
+	accept_add_to(&ar_archive->accept, (char*)"data.tar.xz");
+	accept_add_to(&control_tar_llist, (char*)"control.tar.xz");
 #endif
 
 	/* Must have 1 or 2 args */
@@ -95,7 +95,7 @@ int dpkg_deb_main(int argc UNUSED_PARAM, char **argv)
 		/* Print the entire control file */
 //TODO: standard tool accepts an optional list of fields to print
 		ar_archive->accept = control_tar_llist;
-		llist_add_to(&(tar_archive->accept), (char*)"./control");
+		accept_add_to(&(tar_archive->accept), (char*)"./control");
 		tar_archive->filter = filter_accept_list;
 		tar_archive->action_data = data_extract_to_stdout;
 		if (extract_dir)
diff --git a/archival/libarchive/Kbuild.src b/archival/libarchive/Kbuild.src
index d2f284b08..ccc83a9f0 100644
--- a/archival/libarchive/Kbuild.src
+++ b/archival/libarchive/Kbuild.src
@@ -7,6 +7,8 @@
 lib-y:= common.o
 
 COMMON_FILES:= \
+	accept_add_to.o \
+	accept_add_to_end.o \
 \
 	data_skip.o \
 	data_extract_all.o \
diff --git a/archival/libarchive/accept_add_to.c b/archival/libarchive/accept_add_to.c
new file mode 100644
index 000000000..36310ba9d
--- /dev/null
+++ b/archival/libarchive/accept_add_to.c
@@ -0,0 +1,15 @@
+/* vi: set sw=4 ts=4: */
+/*
+ * Licensed under GPLv2 or later, see file LICENSE in this source tree.
+ */
+#include "libbb.h"
+#include "bb_archive.h"
+
+void FAST_FUNC accept_add_to(accept_llist_t **old_head, char *data)
+{
+	accept_llist_t *new_head = xzalloc(sizeof(accept_llist_t));
+
+	new_head->data = data;
+	new_head->link = *old_head;
+	*old_head = new_head;
+}
diff --git a/archival/libarchive/accept_add_to_end.c b/archival/libarchive/accept_add_to_end.c
new file mode 100644
index 000000000..8260ea23e
--- /dev/null
+++ b/archival/libarchive/accept_add_to_end.c
@@ -0,0 +1,15 @@
+/* vi: set sw=4 ts=4: */
+/*
+ * Licensed under GPLv2 or later, see file LICENSE in this source tree.
+ */
+#include "libbb.h"
+#include "bb_archive.h"
+
+void FAST_FUNC accept_add_to_end(accept_llist_t **list_head, char *data)
+{
+	while (*list_head)
+		list_head = &(*list_head)->link;
+	*list_head = xzalloc(sizeof(accept_llist_t));
+	(*list_head)->data = data;
+	/*(*list_head)->link = NULL;*/
+}
diff --git a/archival/libarchive/filter_accept_list.c b/archival/libarchive/filter_accept_list.c
index 32f806574..0319ff927 100644
--- a/archival/libarchive/filter_accept_list.c
+++ b/archival/libarchive/filter_accept_list.c
@@ -12,7 +12,7 @@
  */
 char FAST_FUNC filter_accept_list(archive_handle_t *archive_handle)
 {
-	if (find_list_entry(archive_handle->accept, archive_handle->file_header->name))
+	if (find_list_entry((llist_t *)archive_handle->accept, archive_handle->file_header->name))
 		return EXIT_SUCCESS;
 	return EXIT_FAILURE;
 }
diff --git a/archival/libarchive/filter_accept_list_reassign.c b/archival/libarchive/filter_accept_list_reassign.c
index 826c5c29d..c6428f9c3 100644
--- a/archival/libarchive/filter_accept_list_reassign.c
+++ b/archival/libarchive/filter_accept_list_reassign.c
@@ -17,7 +17,7 @@
 char FAST_FUNC filter_accept_list_reassign(archive_handle_t *archive_handle)
 {
 	/* Check the file entry is in the accept list */
-	if (find_list_entry(archive_handle->accept, archive_handle->file_header->name)) {
+	if (find_list_entry((llist_t *)archive_handle->accept, archive_handle->file_header->name)) {
 		const char *name_ptr;
 
 		/* Find extension */
diff --git a/archival/libarchive/filter_accept_reject_list.c b/archival/libarchive/filter_accept_reject_list.c
index 939e626fa..935e60c29 100644
--- a/archival/libarchive/filter_accept_reject_list.c
+++ b/archival/libarchive/filter_accept_reject_list.c
@@ -14,7 +14,7 @@ char FAST_FUNC filter_accept_reject_list(archive_handle_t *archive_handle)
 {
 	const char *key;
 	const llist_t *reject_entry;
-	const llist_t *accept_entry;
+	accept_llist_t *accept_entry;
 
 	key = archive_handle->file_header->name;
 
@@ -26,10 +26,24 @@ char FAST_FUNC filter_accept_reject_list(archive_handle_t *archive_handle)
 
 	/* Fail if an accept list was specified and the key wasnt in there */
 	if (archive_handle->accept) {
-		accept_entry = find_list_entry2(archive_handle->accept, key);
+		accept_entry = (accept_llist_t *)find_list_entry2((llist_t *)archive_handle->accept, key);
 		if (!accept_entry) {
 			return EXIT_FAILURE;
 		}
+
+		/* Mark the file as seen */
+		accept_entry->tar__seen_count++;
+
+#if ENABLE_FEATURE_TAR_LONG_OPTIONS
+		/* Support tar --occurrence */
+		if (archive_handle->tar__occurrence) {
+			if (accept_entry->tar__seen_count == archive_handle->tar__occurrence) {
+				archive_handle->tar__occurrence_remaining--;
+			} else {
+				return EXIT_FAILURE;
+			}
+		}
+#endif
 	}
 
 	/* Accepted */
diff --git a/archival/libarchive/get_header_tar.c b/archival/libarchive/get_header_tar.c
index cc6f3f0ad..9ae190609 100644
--- a/archival/libarchive/get_header_tar.c
+++ b/archival/libarchive/get_header_tar.c
@@ -176,6 +176,13 @@ char FAST_FUNC get_header_tar(archive_handle_t *archive_handle)
 # define p_linkname 0
 #endif
 
+#if ENABLE_FEATURE_TAR_LONG_OPTIONS
+	if (archive_handle->tar__occurrence && archive_handle->tar__occurrence_remaining == 0) {
+		/* We've found all of the occurrences we were looking for, signal end of archive */
+		return EXIT_FAILURE;
+	}
+#endif
+
 #if ENABLE_FEATURE_TAR_GNU_EXTENSIONS || ENABLE_FEATURE_TAR_SELINUX
  again:
 #endif
diff --git a/archival/tar.c b/archival/tar.c
index d6ca6c1e0..7aa393dc2 100644
--- a/archival/tar.c
+++ b/archival/tar.c
@@ -655,7 +655,7 @@ static void NOINLINE vfork_compressor(int tar_fd, const char *gzip)
 static NOINLINE int writeTarFile(
 	struct TarBallInfo *tbInfo,
 	int recurseFlags,
-	const llist_t *filelist,
+	const accept_llist_t *filelist,
 	const char *gzip)
 {
 	int errorFlag = FALSE;
@@ -715,9 +715,9 @@ static NOINLINE int writeTarFile(
 #endif /* FEATURE_TAR_CREATE */
 
 #if ENABLE_FEATURE_TAR_FROM
-static llist_t *append_file_list_to_list(llist_t *list)
+static accept_llist_t *append_file_list_to_list(llist_t *list)
 {
-	llist_t *newlist = NULL;
+	accept_llist_t *newlist = NULL;
 
 	while (list) {
 		FILE *src_stream;
@@ -729,7 +729,7 @@ static llist_t *append_file_list_to_list(llist_t *list)
 			char *cp = last_char_is(line, '/');
 			if (cp > line)
 				*cp = '\0';
-			llist_add_to_end(&newlist, line);
+			accept_add_to_end(&newlist, line);
 		}
 		fclose(src_stream);
 	}
@@ -799,6 +799,7 @@ static llist_t *append_file_list_to_list(llist_t *list)
 //usage:	)
 //usage:	)
 //usage:	IF_FEATURE_TAR_LONG_OPTIONS(
+//usage:     "\n	--occurrence [NUM]	Exit after NUM (default 1) occurrences"
 //usage:     "\n	--overwrite		Replace existing files"
 //usage:     "\n	--strip-components NUM	NUM of leading components to strip"
 //usage:     "\n	--no-recursion		Don't descend in directories"
@@ -826,6 +827,7 @@ enum {
 	OPTBIT_AUTOCOMPRESS_BY_EXT,
 	IF_FEATURE_TAR_NOPRESERVE_TIME(OPTBIT_NOPRESERVE_TIME,)
 #if ENABLE_FEATURE_TAR_LONG_OPTIONS
+	OPTBIT_OCCURRENCE,
 	OPTBIT_STRIP_COMPONENTS,
 	IF_FEATURE_SEAMLESS_LZMA(OPTBIT_LZMA        ,)
 	OPTBIT_NORECURSION,
@@ -853,6 +855,7 @@ enum {
 	OPT_COMPRESS     = IF_FEATURE_SEAMLESS_Z(   (1 << OPTBIT_COMPRESS    )) + 0, // Z
 	OPT_AUTOCOMPRESS_BY_EXT = 1 << OPTBIT_AUTOCOMPRESS_BY_EXT,                   // a
 	OPT_NOPRESERVE_TIME  = IF_FEATURE_TAR_NOPRESERVE_TIME((1 << OPTBIT_NOPRESERVE_TIME)) + 0, // m
+	OPT_OCCURRENCE       = IF_FEATURE_TAR_LONG_OPTIONS((1 << OPTBIT_OCCURRENCE      )) + 0, // occurrence
 	OPT_STRIP_COMPONENTS = IF_FEATURE_TAR_LONG_OPTIONS((1 << OPTBIT_STRIP_COMPONENTS)) + 0, // strip-components
 	OPT_LZMA             = IF_FEATURE_TAR_LONG_OPTIONS(IF_FEATURE_SEAMLESS_LZMA((1 << OPTBIT_LZMA))) + 0, // lzma
 	OPT_NORECURSION      = IF_FEATURE_TAR_LONG_OPTIONS((1 << OPTBIT_NORECURSION    )) + 0, // no-recursion
@@ -901,6 +904,7 @@ static const char tar_longopts[] ALIGN1 =
 # if ENABLE_FEATURE_TAR_NOPRESERVE_TIME
 	"touch\0"               No_argument       "m"
 # endif
+	"occurrence\0"      	Optional_argument "\xf7"
 	"strip-components\0"	Required_argument "\xf8"
 # if ENABLE_FEATURE_SEAMLESS_LZMA
 	"lzma\0"                No_argument       "\xf9"
@@ -936,6 +940,9 @@ int tar_main(int argc UNUSED_PARAM, char **argv)
 	const char *tar_filename = "-";
 	unsigned opt;
 	int verboseFlag = 0;
+#if ENABLE_FEATURE_TAR_FROM
+	llist_t *accept = NULL, *reject = NULL;
+#endif
 #if ENABLE_FEATURE_TAR_LONG_OPTIONS && ENABLE_FEATURE_TAR_FROM
 	llist_t *excludes = NULL;
 #endif
@@ -999,6 +1006,7 @@ int tar_main(int argc UNUSED_PARAM, char **argv)
 		IF_FEATURE_SEAMLESS_Z(   "Z"     )
 		"a"
 		IF_FEATURE_TAR_NOPRESERVE_TIME("m")
+		IF_FEATURE_TAR_LONG_OPTIONS("\xf7:") // --occurrence
 		IF_FEATURE_TAR_LONG_OPTIONS("\xf8:") // --strip-components
 		"\0"
 		"tt:vv:" // count -t,-v
@@ -1009,14 +1017,16 @@ int tar_main(int argc UNUSED_PARAM, char **argv)
 		IF_FEATURE_TAR_CREATE("c--tx:t--cx:x--ct") // mutually exclusive
 		IF_NOT_FEATURE_TAR_CREATE("t--x:x--t") // mutually exclusive
 #if ENABLE_FEATURE_TAR_LONG_OPTIONS
+		":\xf7+" // --occurrence[=NUM]
 		":\xf8+" // --strip-components=NUM
 #endif
 		LONGOPTS
 		, &base_dir // -C dir
 		, &tar_filename // -f filename
-		IF_FEATURE_TAR_FROM(, &(tar_handle->accept)) // T
-		IF_FEATURE_TAR_FROM(, &(tar_handle->reject)) // X
+		IF_FEATURE_TAR_FROM(, &accept) // T
+		IF_FEATURE_TAR_FROM(, &reject) // X
 #if ENABLE_FEATURE_TAR_LONG_OPTIONS
+		, &tar_handle->tar__occurrence       // --occurrence
 		, &tar_handle->tar__strip_components // --strip-components
 #endif
 		IF_FEATURE_TAR_TO_COMMAND(, &(tar_handle->tar__to_command)) // --to-command
@@ -1061,6 +1071,7 @@ int tar_main(int argc UNUSED_PARAM, char **argv)
 	bb_error_msg("verboseFlag:%d", verboseFlag);
 	bb_error_msg("tar_handle->tar__to_command:'%s'", tar_handle->tar__to_command);
 	bb_error_msg("tar_handle->tar__strip_components:%u", tar_handle->tar__strip_components);
+	bb_error_msg("tar_handle->tar__occurrence:%u", tar_handle->tar__occurrence);
 	return 0;
 # undef showopt
 #endif
@@ -1106,7 +1117,7 @@ int tar_main(int argc UNUSED_PARAM, char **argv)
 
 #if ENABLE_FEATURE_TAR_FROM
 	/* Convert each -X EXCLFILE to list of to-be-rejected glob patterns */
-	tar_handle->reject = append_file_list_to_list(tar_handle->reject);
+	tar_handle->reject = (llist_t *)append_file_list_to_list(reject);
 # if ENABLE_FEATURE_TAR_LONG_OPTIONS
 	/* Append --exclude=GLOBPATTERNs to reject */
 	if (excludes) {
@@ -1116,7 +1127,7 @@ int tar_main(int argc UNUSED_PARAM, char **argv)
 		*p2next = excludes;
 	}
 # endif
-	tar_handle->accept = append_file_list_to_list(tar_handle->accept);
+	tar_handle->accept = append_file_list_to_list(accept);
 #endif
 
 	/* Setup an array of filenames to work with */
@@ -1126,13 +1137,26 @@ int tar_main(int argc UNUSED_PARAM, char **argv)
 		char *cp = last_char_is(*argv, '/');
 		if (cp > *argv)
 			*cp = '\0';
-		llist_add_to_end(&tar_handle->accept, *argv);
+		accept_add_to_end(&tar_handle->accept, *argv);
 		argv++;
 	}
 
 	if (tar_handle->accept || tar_handle->reject)
 		tar_handle->filter = filter_accept_reject_list;
 
+#if ENABLE_FEATURE_TAR_LONG_OPTIONS
+	if (tar_handle->tar__occurrence) {
+		accept_llist_t *a = tar_handle->accept;
+
+		if (opt & OPT_CREATE)
+			bb_simple_error_msg_and_die("--occurrence cannot be used with -c");
+		if (!a)
+			bb_simple_error_msg_and_die("--occurrence requires a file list");
+		for (; a; a = a->link)
+			tar_handle->tar__occurrence_remaining++;
+	}
+#endif
+
 	/* Open the tar file */
 	{
 		int tar_fd = STDIN_FILENO;
@@ -1265,14 +1289,21 @@ int tar_main(int argc UNUSED_PARAM, char **argv)
 	create_links_from_list(tar_handle->link_placeholders);
 
 	/* Check that every file that should have been extracted was */
-	while (tar_handle->accept) {
-		if (!find_list_entry(tar_handle->reject, tar_handle->accept->data)
-		 && !find_list_entry(tar_handle->passed, tar_handle->accept->data)
-		) {
+	for (; tar_handle->accept; tar_handle->accept = tar_handle->accept->link) {
+		if (find_list_entry(tar_handle->reject, tar_handle->accept->data)) {
+			continue;
+		}
+		if (tar_handle->accept->tar__seen_count == 0) {
 			bb_error_msg_and_die("%s: not found in archive",
 				tar_handle->accept->data);
 		}
-		tar_handle->accept = tar_handle->accept->link;
+#if ENABLE_FEATURE_TAR_LONG_OPTIONS
+		if (tar_handle->tar__occurrence
+		 && tar_handle->accept->tar__seen_count < tar_handle->tar__occurrence) {
+			bb_error_msg_and_die("%s: required occurrence not found in archive",
+				tar_handle->accept->data);
+		}
+#endif
 	}
 	if (ENABLE_FEATURE_CLEAN_UP /* && tar_handle->src_fd != STDIN_FILENO */)
 		close(tar_handle->src_fd);
diff --git a/include/bb_archive.h b/include/bb_archive.h
index e0ef8fc4e..74b55236f 100644
--- a/include/bb_archive.h
+++ b/include/bb_archive.h
@@ -43,6 +43,13 @@ typedef struct file_header_t {
 	dev_t device;
 } file_header_t;
 
+typedef struct accept_llist_t {
+	/* link/data must be first: this struct needs to be llist-compatible */
+	struct accept_llist_t *link;
+	char *data;
+	unsigned tar__seen_count;
+} accept_llist_t;
+
 struct hardlinks_t;
 
 typedef struct archive_handle_t {
@@ -55,7 +62,7 @@ typedef struct archive_handle_t {
 	/* Define if the header and data component should be processed */
 	char FAST_FUNC (*filter)(struct archive_handle_t *);
 	/* List of files that have been accepted */
-	llist_t *accept;
+	accept_llist_t *accept;
 	/* List of files that have been rejected */
 	llist_t *reject;
 	/* List of files that have successfully been worked on */
@@ -82,6 +89,8 @@ typedef struct archive_handle_t {
 	/* Archiver specific. Can make it a union if it ever gets big */
 #if ENABLE_FEATURE_TAR_LONG_OPTIONS
 	unsigned tar__strip_components;
+	unsigned tar__occurrence;
+	unsigned tar__occurrence_remaining;
 #endif
 #define PAX_NEXT_FILE 0
 #define PAX_GLOBAL    1
@@ -175,6 +184,9 @@ extern const char cpio_TRAILER[];
 
 archive_handle_t *init_handle(void) FAST_FUNC;
 
+void accept_add_to(accept_llist_t **old_head, char *data) FAST_FUNC;
+void accept_add_to_end(accept_llist_t **list_head, char *data) FAST_FUNC;
+
 char filter_accept_all(archive_handle_t *archive_handle) FAST_FUNC;
 char filter_accept_list(archive_handle_t *archive_handle) FAST_FUNC;
 char filter_accept_list_reassign(archive_handle_t *archive_handle) FAST_FUNC;
diff --git a/testsuite/tar/tar-handles-occurrence b/testsuite/tar/tar-handles-occurrence
new file mode 100644
index 000000000..4d7a8c5db
--- /dev/null
+++ b/testsuite/tar/tar-handles-occurrence
@@ -0,0 +1,18 @@
+# FEATURE: CONFIG_FEATURE_TAR_LONG_OPTIONS
+
+echo one > test.txt
+busybox tar -cf one.tar test.txt
+
+echo two > test.txt
+busybox tar -cf two.tar test.txt
+
+(head -c 1024 one.tar; head -c 1024 two.tar) > combined.tar
+
+data=$(busybox tar -xO --occurrence=1 test.txt < combined.tar)
+test "$data" = "one"
+
+data=$(busybox tar -xO --occurrence=2 test.txt < combined.tar)
+test "$data" = "two"
+
+data=$(busybox tar -xO --occurrence=3 test.txt < combined.tar) && exit 1
+test "$data" = ""
-- 
2.39.2



More information about the busybox mailing list