From 9140c906f5bc3cd5e0a8be248a70cf1ca82a103d Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Tue, 23 Sep 2025 13:22:29 +0200
Subject: [PATCH 1/7] feat: introduce `ArchiveFile` abstraction for file-based
archives
This PR adds a new `ArchiveFile` interface to unify the handling of file-based archive utilities such as `SevenZFile`, `TarFile`, and `ZipFile`.
Although these classes target different archive formats, they share several core characteristics:
* All are `Closeable`.
* Each provides the same method to open an `InputStream` for a given entry (`InputStream getInputStream(T)` where `T extends ArchiveEntry`).
* Historically, their `getEntries()` methods returned incompatible types. This PR introduces a common `List extends T> entries()` method, aligning with `java.util.zip.ZipFile` in name but offering a modern return value.
* The `ZipFile#stream()` method (added in 1.28.0) is now part of this abstraction.
This change establishes a consistent, format-agnostic API for working with archive files, reducing duplication and improving discoverability for users.
---
src/changes/changes.xml | 3 +-
.../compress/archivers/ArchiveFile.java | 82 +++++++++++
.../compress/archivers/sevenz/SevenZFile.java | 17 ++-
.../compress/archivers/tar/TarFile.java | 17 ++-
.../compress/archivers/zip/ZipFile.java | 8 +-
.../archivers/AbstractArchiveFileTest.java | 138 ++++++++++++++++++
.../archivers/sevenz/SevenZFileTest.java | 9 +-
.../compress/archivers/tar/TarFileTest.java | 12 +-
.../compress/archivers/zip/ZipFileTest.java | 8 +-
9 files changed, 279 insertions(+), 15 deletions(-)
create mode 100644 src/main/java/org/apache/commons/compress/archivers/ArchiveFile.java
create mode 100644 src/test/java/org/apache/commons/compress/archivers/AbstractArchiveFileTest.java
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 7f46fc8c5ff..acf52a01dab 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -70,7 +70,7 @@ The type attribute can be add,update,fix,remove.
ZipArchiveInputStream.read() now throws an IOException instead of java.lang.ArrayIndexOutOfBoundsException.
ZipArchiveInputStream now throws an MemoryLimitException instead of ArchiveException, or OutOfMemoryError when running with low memory settings set on the command line.
ZstdCompressorInputStream closes the InputStream held by ZipArchiveInputStream garbage collection.
-
+
>Uniform handling of special tar records in TarFile and TarArchiveInputStream.
TarArchiveOutputStream now throws a IllegalArgumentException instead of an OutOfMemoryError.
TarUtils.verifyCheckSum() throws an Exception when checksum could not be parsed.
@@ -123,6 +123,7 @@ The type attribute can be add,update,fix,remove.
Introduce builders for all ArchiveInputStream implementations and deprecate some constructors.
TarFile now implements IOIterable<TarArchiveEntry>.
Add a builder for the TarFile class and deprecate some constructors.
+ Introduce an ArchiveFile abstraction to unify the APIs of SevenZFile, TarFile, and ZipFile.
Bump org.apache.commons:commons-parent from 85 to 88 #707.
Extract duplicate code in org.apache.commons.compress.harmony.pack200.IntList.
diff --git a/src/main/java/org/apache/commons/compress/archivers/ArchiveFile.java b/src/main/java/org/apache/commons/compress/archivers/ArchiveFile.java
new file mode 100644
index 00000000000..b6c236eab16
--- /dev/null
+++ b/src/main/java/org/apache/commons/compress/archivers/ArchiveFile.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.commons.compress.archivers;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.zip.ZipFile;
+
+import org.apache.commons.io.function.IOStream;
+
+/**
+ * A file-based representation of an archive containing multiple {@link ArchiveEntry entries}.
+ *
+ * This interface provides a higher-level abstraction over archive files, similar to
+ * {@link ZipFile}, but generalized for a variety of archive formats.
+ *
+ * Implementations are {@link Closeable} and should be closed once they are no longer
+ * needed in order to release any underlying system resources.
+ *
+ * @param the type of {@link ArchiveEntry} produced by this archive
+ * @since 1.29.0
+ */
+public interface ArchiveFile extends Closeable {
+
+ /**
+ * Returns all entries contained in the archive as a list.
+ *
+ * The order of entries is format-dependent but guaranteed to be consistent
+ * across multiple invocations on the same archive.
+ *
+ * @return An immutable list of all entries in this archive.
+ * @throws IOException If an I/O error occurs while reading entries.
+ */
+ default List extends T> entries() throws IOException {
+ try (IOStream extends T> stream = stream()) {
+ return stream.collect(Collectors.toList());
+ }
+ }
+
+ /**
+ * Returns a sequential stream of archive entries.
+ *
+ * The order of entries is format-dependent but stable for a given archive.
+ * The returned stream must be closed after use to free
+ * associated resources.
+ *
+ * @return A stream of entries in this archive.
+ * @throws IOException If an I/O error occurs while creating the stream.
+ */
+ IOStream extends T> stream() throws IOException;
+
+ /**
+ * Opens an input stream for the specified entry's contents.
+ *
+ * The caller is responsible for closing the returned stream after use.
+ *
+ * @param entry The archive entry to read.
+ * @return An input stream providing the contents of the given entry.
+ * @throws IOException If an I/O error occurs while opening the entry stream.
+ */
+ InputStream getInputStream(T entry) throws IOException;
+}
+
diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java
index 28dfddb274a..6815c242c53 100644
--- a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java
+++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java
@@ -23,7 +23,6 @@
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
@@ -52,11 +51,13 @@
import org.apache.commons.compress.MemoryLimitException;
import org.apache.commons.compress.archivers.ArchiveException;
+import org.apache.commons.compress.archivers.ArchiveFile;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.compress.utils.InputStreamStatistics;
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
import org.apache.commons.io.build.AbstractOrigin.ByteArrayOrigin;
import org.apache.commons.io.build.AbstractStreamBuilder;
+import org.apache.commons.io.function.IOStream;
import org.apache.commons.io.input.BoundedInputStream;
import org.apache.commons.io.input.ChecksumInputStream;
import org.apache.commons.lang3.ArrayUtils;
@@ -84,7 +85,7 @@
* @NotThreadSafe
* @since 1.6
*/
-public class SevenZFile implements Closeable {
+public class SevenZFile implements ArchiveFile {
private static final class ArchiveStatistics {
private int numberOfPackedStreams;
@@ -1057,11 +1058,22 @@ public String getDefaultName() {
*
* @return a copy of meta-data of all archive entries.
* @since 1.11
+ * @deprecated Since 1.29.0, use {@link #entries()} or {@link #stream()} instead.
*/
+ @Deprecated
public Iterable getEntries() {
return new ArrayList<>(Arrays.asList(archive.files));
}
+ /**
+ * {@inheritDoc}
+ * @since 1.29.0
+ */
+ @Override
+ public IOStream extends SevenZArchiveEntry> stream() throws IOException {
+ return IOStream.of(archive.files);
+ }
+
/**
* Gets an InputStream for reading the contents of the given entry.
*
@@ -1073,6 +1085,7 @@ public Iterable getEntries() {
* @throws IOException if unable to create an input stream from the entry
* @since 1.20
*/
+ @Override
public InputStream getInputStream(final SevenZArchiveEntry entry) throws IOException {
int entryIndex = -1;
for (int i = 0; i < archive.files.length; i++) {
diff --git a/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java b/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
index 9ebb6c1d6e1..6fefb29a156 100644
--- a/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
+++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
@@ -18,7 +18,6 @@
*/
package org.apache.commons.compress.archivers.tar;
-import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -34,6 +33,7 @@
import java.util.Map;
import org.apache.commons.compress.archivers.ArchiveException;
+import org.apache.commons.compress.archivers.ArchiveFile;
import org.apache.commons.compress.archivers.zip.ZipEncoding;
import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
@@ -43,6 +43,7 @@
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
import org.apache.commons.io.function.IOIterable;
import org.apache.commons.io.function.IOIterator;
+import org.apache.commons.io.function.IOStream;
import org.apache.commons.io.input.BoundedInputStream;
/**
@@ -50,7 +51,7 @@
*
* @since 1.21
*/
-public class TarFile implements Closeable, IOIterable {
+public class TarFile implements ArchiveFile, IOIterable {
private final class BoundedTarEntryInputStream extends BoundedArchiveInputStream {
@@ -448,11 +449,22 @@ private void consumeRemainderOfLastBlock() throws IOException {
* Gets all TAR Archive Entries from the TarFile.
*
* @return All entries from the tar file.
+ * @deprecated Since 1.29.0, use {@link #entries()} or {@link #stream()} instead.
*/
+ @Deprecated
public List getEntries() {
return new ArrayList<>(entries);
}
+ /**
+ * {@inheritDoc}
+ * @since 1.29.0
+ */
+ @Override
+ public IOStream extends TarArchiveEntry> stream() throws IOException {
+ return IOStream.of(entries);
+ }
+
/**
* Gets the input stream for the provided Tar Archive Entry.
*
@@ -460,6 +472,7 @@ public List getEntries() {
* @return Input stream of the provided entry.
* @throws IOException Corrupted TAR archive. Can't read entry.
*/
+ @Override
public InputStream getInputStream(final TarArchiveEntry entry) throws IOException {
try {
return new BoundedTarEntryInputStream(entry, archive);
diff --git a/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java b/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java
index 3e28361c33d..051c52b89e3 100644
--- a/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java
+++ b/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java
@@ -20,7 +20,6 @@
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
-import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
@@ -52,6 +51,7 @@
import java.util.zip.ZipException;
import org.apache.commons.compress.archivers.ArchiveException;
+import org.apache.commons.compress.archivers.ArchiveFile;
import org.apache.commons.compress.archivers.EntryStreamOffsets;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import org.apache.commons.compress.compressors.deflate64.Deflate64CompressorInputStream;
@@ -92,7 +92,7 @@
* close is allowed to throw IOException.
*
*/
-public class ZipFile implements Closeable {
+public class ZipFile implements ArchiveFile {
/**
* Lock-free implementation of BoundedInputStream. The implementation uses positioned reads on the underlying archive file channel and therefore performs
@@ -1154,7 +1154,9 @@ public String getEncoding() {
*
*
* @return all entries as {@link ZipArchiveEntry} instances
+ * @deprecated Since 1.29.0, use {@link #entries()} or {@link #stream()} instead.
*/
+ @Deprecated
public Enumeration getEntries() {
return Collections.enumeration(entries);
}
@@ -1227,6 +1229,7 @@ public long getFirstLocalFileHeaderOffset() {
* @return a stream to read the entry from. The returned stream implements {@link InputStreamStatistics}.
* @throws IOException if unable to create an input stream from the zipEntry.
*/
+ @Override
public InputStream getInputStream(final ZipArchiveEntry entry) throws IOException {
if (!(entry instanceof Entry)) {
return null;
@@ -1757,6 +1760,7 @@ private boolean startsWithLocalFileHeader() throws IOException {
* @throws IllegalStateException if the ZIP file has been closed.
* @since 1.28.0
*/
+ @Override
public IOStream extends ZipArchiveEntry> stream() {
return IOStream.adapt(entries.stream());
}
diff --git a/src/test/java/org/apache/commons/compress/archivers/AbstractArchiveFileTest.java b/src/test/java/org/apache/commons/compress/archivers/AbstractArchiveFileTest.java
new file mode 100644
index 00000000000..cb2d0268ce8
--- /dev/null
+++ b/src/test/java/org/apache/commons/compress/archivers/AbstractArchiveFileTest.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.commons.compress.archivers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.io.InputStream;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.commons.compress.AbstractTest;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.function.IOStream;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Abstract base class for tests of {@link ArchiveFile} implementations.
+ *
+ * @param The type of {@link ArchiveEntry} produced.
+ */
+public abstract class AbstractArchiveFileTest extends AbstractTest {
+
+ private static ArchiveEntry newEntryUtc(String name, long size, LocalDateTime lastModified) {
+ return newEntry(name, size, lastModified.toInstant(ZoneOffset.UTC));
+ }
+
+ private static ArchiveEntry newEntry(String name, long size, Instant lastModified) {
+ return new ArchiveEntry() {
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public long getSize() {
+ return size;
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return false;
+ }
+
+ @Override
+ public Date getLastModifiedDate() {
+ return Date.from(lastModified);
+ }
+ };
+ }
+
+ /**
+ * Returns an {@link ArchiveFile} to be tested.
+ *
+ * @return The archive file to be tested.
+ */
+ protected abstract ArchiveFile getArchiveFile() throws Exception;
+
+ /**
+ * Returns the expected entries in the test archive.
+ *
+ * @return The expected entries.
+ */
+ private List extends ArchiveEntry> getExpectedEntries() {
+ return Arrays.asList(
+ newEntryUtc("test1.xml", 610, LocalDateTime.of(2007, 11, 14, 10, 19, 2)),
+ newEntryUtc("test2.xml", 82, LocalDateTime.of(2007, 11, 14, 10, 19, 2)));
+ }
+
+ /**
+ * Tests that the entries returned by {@link ArchiveFile#entries()} match the expected entries.
+ */
+ @Test
+ void testEntries() throws Exception {
+ try (ArchiveFile archiveFile = getArchiveFile()) {
+ final List extends T> entries = archiveFile.entries();
+ final List extends ArchiveEntry> expectedEntries = getExpectedEntries();
+ assertEquals(expectedEntries.size(), entries.size(), "Number of entries");
+ for (int i = 0; i < expectedEntries.size(); i++) {
+ final ArchiveEntry expected = expectedEntries.get(i);
+ final ArchiveEntry actual = entries.get(i);
+ assertEquals(expected.getName(), actual.getName(), "Entry name at index " + i);
+ assertEquals(expected.getSize(), actual.getSize(), "Size of entry " + expected.getName());
+ assertEquals(
+ expected.getLastModifiedDate(),
+ actual.getLastModifiedDate(),
+ "Last modified date of entry " + expected.getName());
+ }
+ }
+ }
+
+ /**
+ * Tests that the input streams returned by {@link ArchiveFile#getInputStream(ArchiveEntry)} match the expected
+ * entries.
+ */
+ @Test
+ void testGetInputStream() throws Exception {
+ try (ArchiveFile archiveFile = getArchiveFile()) {
+ final List extends ArchiveEntry> expectedEntries = getExpectedEntries();
+ for (final ArchiveEntry expected : expectedEntries) {
+ final T actual = getMatchingEntry(archiveFile, expected.getName());
+ assertNotNull(actual, "Entry " + expected.getName() + " not found");
+ try (InputStream inputStream = archiveFile.getInputStream(actual)) {
+ assertNotNull(inputStream, "Input stream for entry " + expected.getName());
+ final byte[] content = IOUtils.toByteArray(inputStream);
+ assertEquals(expected.getSize(), content.length, "Size of entry " + expected.getName());
+ }
+ }
+ }
+ }
+
+ private T getMatchingEntry(ArchiveFile extends T> archiveFile, String name) throws Exception {
+ try (IOStream extends T> stream = archiveFile.stream()) {
+ return stream.filter(e -> e.getName().equals(name)).findFirst().orElse(null);
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZFileTest.java b/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZFileTest.java
index 446a2ba6436..21667b80d39 100644
--- a/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZFileTest.java
+++ b/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZFileTest.java
@@ -51,9 +51,9 @@
import javax.crypto.Cipher;
-import org.apache.commons.compress.AbstractTest;
import org.apache.commons.compress.MemoryLimitException;
import org.apache.commons.compress.PasswordRequiredException;
+import org.apache.commons.compress.archivers.AbstractArchiveFileTest;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.utils.MultiReadOnlySeekableByteChannel;
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
@@ -62,7 +62,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-class SevenZFileTest extends AbstractTest {
+class SevenZFileTest extends AbstractArchiveFileTest {
private static final String TEST2_CONTENT = "\r\n\r\n\r\n\t\r\n\n";
private static boolean isStrongCryptoAvailable() throws NoSuchAlgorithmException {
@@ -1050,4 +1050,9 @@ void testSignatureCheck() {
final byte[] data3 = { '7', 'z', (byte) 0xBC, (byte) 0xAF, 0x27, 0x1D };
assertFalse(SevenZFile.matches(data3, data3.length));
}
+
+ @Override
+ protected SevenZFile getArchiveFile() throws Exception {
+ return SevenZFile.builder().setPath(getPath("bla.7z")).get();
+ }
}
diff --git a/src/test/java/org/apache/commons/compress/archivers/tar/TarFileTest.java b/src/test/java/org/apache/commons/compress/archivers/tar/TarFileTest.java
index 5b8b9a18f3a..1101336c574 100644
--- a/src/test/java/org/apache/commons/compress/archivers/tar/TarFileTest.java
+++ b/src/test/java/org/apache/commons/compress/archivers/tar/TarFileTest.java
@@ -40,15 +40,14 @@
import java.util.List;
import java.util.zip.GZIPInputStream;
-import org.apache.commons.compress.AbstractTest;
+import org.apache.commons.compress.archivers.AbstractArchiveFileTest;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.TimeZones;
import org.junit.jupiter.api.Test;
-import shaded.org.apache.commons.lang3.StringUtils;
-
-class TarFileTest extends AbstractTest {
+class TarFileTest extends AbstractArchiveFileTest {
private void datePriorToEpoch(final String archive) throws Exception {
try (TarFile tarFile = new TarFile(getPath(archive))) {
@@ -379,4 +378,9 @@ void testWorkaroundForBrokenTimeHeader() throws IOException {
assertTrue(entry.isCheckSumOK());
}
}
+
+ @Override
+ protected TarFile getArchiveFile() throws Exception {
+ return TarFile.builder().setPath(getPath("bla.tar")).get();
+ }
}
diff --git a/src/test/java/org/apache/commons/compress/archivers/zip/ZipFileTest.java b/src/test/java/org/apache/commons/compress/archivers/zip/ZipFileTest.java
index ba97587aaa9..a07238e0c12 100644
--- a/src/test/java/org/apache/commons/compress/archivers/zip/ZipFileTest.java
+++ b/src/test/java/org/apache/commons/compress/archivers/zip/ZipFileTest.java
@@ -56,7 +56,7 @@
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
-import org.apache.commons.compress.AbstractTest;
+import org.apache.commons.compress.archivers.AbstractArchiveFileTest;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
@@ -72,7 +72,7 @@
import io.airlift.compress.zstd.ZstdInputStream;
-class ZipFileTest extends AbstractTest {
+class ZipFileTest extends AbstractArchiveFileTest {
/**
* This Class simulates the case where the Zip File uses the aircompressors {@link ZstdInputStream}
@@ -1097,4 +1097,8 @@ void testZstdInputStreamErrorCloseWhenGc() throws Exception {
}
}
+ @Override
+ protected ZipFile getArchiveFile() throws Exception {
+ return ZipFile.builder().setPath(getPath("bla.zip")).get();
+ }
}
From 3234bdb8a1fcb2722a839204e8d59731efbf967d Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Tue, 23 Sep 2025 14:01:59 +0200
Subject: [PATCH 2/7] fix: add `IOIterator` interface
---
.../compress/archivers/ArchiveFile.java | 23 +++++++--
.../compress/archivers/sevenz/SevenZFile.java | 2 +-
.../compress/archivers/tar/TarFile.java | 47 +------------------
.../archivers/AbstractArchiveFileTest.java | 24 ++++++++++
4 files changed, 45 insertions(+), 51 deletions(-)
diff --git a/src/main/java/org/apache/commons/compress/archivers/ArchiveFile.java b/src/main/java/org/apache/commons/compress/archivers/ArchiveFile.java
index b6c236eab16..6e530d43773 100644
--- a/src/main/java/org/apache/commons/compress/archivers/ArchiveFile.java
+++ b/src/main/java/org/apache/commons/compress/archivers/ArchiveFile.java
@@ -25,6 +25,8 @@
import java.util.stream.Collectors;
import java.util.zip.ZipFile;
+import org.apache.commons.io.function.IOIterable;
+import org.apache.commons.io.function.IOIterator;
import org.apache.commons.io.function.IOStream;
/**
@@ -39,7 +41,7 @@
* @param the type of {@link ArchiveEntry} produced by this archive
* @since 1.29.0
*/
-public interface ArchiveFile extends Closeable {
+public interface ArchiveFile extends Closeable, IOIterable {
/**
* Returns all entries contained in the archive as a list.
@@ -48,9 +50,8 @@ public interface ArchiveFile extends Closeable {
* across multiple invocations on the same archive.
*
* @return An immutable list of all entries in this archive.
- * @throws IOException If an I/O error occurs while reading entries.
*/
- default List extends T> entries() throws IOException {
+ default List extends T> entries() {
try (IOStream extends T> stream = stream()) {
return stream.collect(Collectors.toList());
}
@@ -64,9 +65,8 @@ default List extends T> entries() throws IOException {
* associated resources.
*
* @return A stream of entries in this archive.
- * @throws IOException If an I/O error occurs while creating the stream.
*/
- IOStream extends T> stream() throws IOException;
+ IOStream extends T> stream();
/**
* Opens an input stream for the specified entry's contents.
@@ -78,5 +78,18 @@ default List extends T> entries() throws IOException {
* @throws IOException If an I/O error occurs while opening the entry stream.
*/
InputStream getInputStream(T entry) throws IOException;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ default IOIterator iterator() {
+ return IOIterator.adapt((Iterable) entries());
+ }
+
+ @Override
+ default Iterable unwrap() {
+ // Commons IO 2.21.0:
+ // return asIterable();
+ return null;
+ }
}
diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java
index 6815c242c53..5cef5a970e1 100644
--- a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java
+++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java
@@ -1070,7 +1070,7 @@ public Iterable getEntries() {
* @since 1.29.0
*/
@Override
- public IOStream extends SevenZArchiveEntry> stream() throws IOException {
+ public IOStream extends SevenZArchiveEntry> stream() {
return IOStream.of(archive.files);
}
diff --git a/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java b/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
index 6fefb29a156..aa579bcee77 100644
--- a/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
+++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
@@ -27,7 +27,6 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -41,8 +40,6 @@
import org.apache.commons.compress.utils.BoundedArchiveInputStream;
import org.apache.commons.compress.utils.BoundedSeekableByteChannelInputStream;
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
-import org.apache.commons.io.function.IOIterable;
-import org.apache.commons.io.function.IOIterator;
import org.apache.commons.io.function.IOStream;
import org.apache.commons.io.input.BoundedInputStream;
@@ -51,7 +48,7 @@
*
* @since 1.21
*/
-public class TarFile implements ArchiveFile, IOIterable {
+public class TarFile implements ArchiveFile {
private final class BoundedTarEntryInputStream extends BoundedArchiveInputStream {
@@ -461,7 +458,7 @@ public List getEntries() {
* @since 1.29.0
*/
@Override
- public IOStream extends TarArchiveEntry> stream() throws IOException {
+ public IOStream extends TarArchiveEntry> stream() {
return IOStream.of(entries);
}
@@ -586,38 +583,6 @@ private boolean isEOFRecord(final ByteBuffer headerBuf) {
return headerBuf == null || ArchiveUtils.isArrayZero(headerBuf.array(), recordSize);
}
- @Override
- public IOIterator iterator() {
- return new IOIterator() {
-
- private TarArchiveEntry next;
-
- @Override
- public boolean hasNext() throws IOException {
- if (next == null) {
- next = getNextTarEntry();
- }
- return next != null;
- }
-
- @Override
- public TarArchiveEntry next() throws IOException {
- if (next == null) {
- next = getNextTarEntry();
- }
- final TarArchiveEntry tmp = next;
- next = null;
- return tmp;
- }
-
- @Override
- public Iterator unwrap() {
- return null;
- }
-
- };
- }
-
/**
* Adds the sparse chunks from the current entry to the sparse chunks, including any additional sparse entries following the current entry.
*
@@ -723,12 +688,4 @@ private void tryToConsumeSecondEOFRecord() throws IOException {
}
}
}
-
- @Override
- public Iterable unwrap() {
- // Commons IO 2.21.0:
- // return asIterable();
- return null;
- }
-
}
diff --git a/src/test/java/org/apache/commons/compress/archivers/AbstractArchiveFileTest.java b/src/test/java/org/apache/commons/compress/archivers/AbstractArchiveFileTest.java
index cb2d0268ce8..36cc19f95e5 100644
--- a/src/test/java/org/apache/commons/compress/archivers/AbstractArchiveFileTest.java
+++ b/src/test/java/org/apache/commons/compress/archivers/AbstractArchiveFileTest.java
@@ -31,6 +31,7 @@
import org.apache.commons.compress.AbstractTest;
import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.function.IOIterator;
import org.apache.commons.io.function.IOStream;
import org.junit.jupiter.api.Test;
@@ -110,6 +111,29 @@ void testEntries() throws Exception {
}
}
+ /**
+ * Tests that the iterator returned by {@link ArchiveFile#iterator()} matches the expected entries.
+ */
+ @Test
+ void testIterator() throws Exception {
+ try (ArchiveFile archiveFile = getArchiveFile()) {
+ final IOIterator iterator = archiveFile.iterator();
+ final List extends ArchiveEntry> entries = getExpectedEntries();
+ int count = 0;
+ while (iterator.hasNext()) {
+ final ArchiveEntry expected = entries.get(count);
+ final ArchiveEntry actual = iterator.next();
+ assertEquals(expected.getName(), actual.getName(), "Entry name at index " + count);
+ assertEquals(expected.getSize(), actual.getSize(), "Size of entry " + expected.getName());
+ assertEquals(
+ expected.getLastModifiedDate(),
+ actual.getLastModifiedDate(),
+ "Last modified date of entry " + expected.getName());
+ count++;
+ }
+ }
+ }
+
/**
* Tests that the input streams returned by {@link ArchiveFile#getInputStream(ArchiveEntry)} match the expected
* entries.
From f5dd39c8bcb1abb43270c75167008ba0330c6818 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Tue, 23 Sep 2025 15:25:42 +0200
Subject: [PATCH 3/7] fix: failing tests
---
.../org/apache/commons/compress/archivers/tar/TarFile.java | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java b/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
index aa579bcee77..548e265fd90 100644
--- a/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
+++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
@@ -238,7 +238,11 @@ private TarFile(final Builder builder) throws IOException {
this.recordBuffer = ByteBuffer.allocate(this.recordSize);
this.blockSize = builder.getBlockSize();
this.lenient = builder.isLenient();
- forEach(entries::add);
+ // Read all entries eagerly
+ TarArchiveEntry entry;
+ while ((entry = getNextTarEntry()) != null) {
+ entries.add(entry);
+ }
}
/**
From 9b16aeb26c3f20ccf18cc7f978e242360f9e70e9 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Mon, 6 Oct 2025 21:56:37 +0200
Subject: [PATCH 4/7] fix: comment on usage of `getNextTarEntry`
---
.../org/apache/commons/compress/archivers/tar/TarFile.java | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java b/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
index cda8e26ec0f..ddd76fc1b08 100644
--- a/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
+++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarFile.java
@@ -224,7 +224,9 @@ private TarFile(final Builder builder) throws IOException {
this.recordBuffer = ByteBuffer.allocate(this.recordSize);
this.blockSize = builder.getBlockSize();
this.lenient = builder.isLenient();
- // Read all entries eagerly
+ // Populate `entries` explicitly here instead of using `forEach`/`stream`,
+ // because both rely on `entries` internally.
+ // Using them would cause a self-referential loop and leave `entries` empty.
TarArchiveEntry entry;
while ((entry = getNextTarEntry()) != null) {
entries.add(entry);
From 6a6fe166602d08afd9e3b2d88850df73efafdb39 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Mon, 6 Oct 2025 22:02:05 +0200
Subject: [PATCH 5/7] empty commit to trigger CI
From 12ef7b9202bbd3b7ecdd5e3e527cc7abd30e0914 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Tue, 7 Oct 2025 14:24:15 +0200
Subject: [PATCH 6/7] empty commit to trigger CI (2)
From 93c519b3977aa0361e848a80961160e13d492370 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Tue, 7 Oct 2025 14:28:14 +0200
Subject: [PATCH 7/7] fix: use `asIterable()` to provide `unwrap()`
---
.../org/apache/commons/compress/archivers/ArchiveFile.java | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/main/java/org/apache/commons/compress/archivers/ArchiveFile.java b/src/main/java/org/apache/commons/compress/archivers/ArchiveFile.java
index 6e530d43773..9958c16e6a6 100644
--- a/src/main/java/org/apache/commons/compress/archivers/ArchiveFile.java
+++ b/src/main/java/org/apache/commons/compress/archivers/ArchiveFile.java
@@ -87,9 +87,7 @@ default IOIterator iterator() {
@Override
default Iterable unwrap() {
- // Commons IO 2.21.0:
- // return asIterable();
- return null;
+ return asIterable();
}
}