diff -Nru libcommons-compress-java-1.19/debian/changelog libcommons-compress-java-1.20/debian/changelog --- libcommons-compress-java-1.19/debian/changelog 2020-01-27 11:35:59.000000000 +0000 +++ libcommons-compress-java-1.20/debian/changelog 2020-04-01 02:41:01.000000000 +0000 @@ -1,3 +1,13 @@ +libcommons-compress-java (1.20-1) unstable; urgency=medium + + * Team upload. + * New upstream version 1.20 + * Refresh patches against new upstream version + * Specify debhelper compat 12 via debhelper-compat dependency + * Set "Rules-Requires-Root: no" in debian/control + + -- tony mancill Tue, 31 Mar 2020 19:41:01 -0700 + libcommons-compress-java (1.19-1) unstable; urgency=medium * New upstream release diff -Nru libcommons-compress-java-1.19/debian/compat libcommons-compress-java-1.20/debian/compat --- libcommons-compress-java-1.19/debian/compat 2020-01-27 11:32:00.000000000 +0000 +++ libcommons-compress-java-1.20/debian/compat 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -12 diff -Nru libcommons-compress-java-1.19/debian/control libcommons-compress-java-1.20/debian/control --- libcommons-compress-java-1.19/debian/control 2020-01-27 11:32:00.000000000 +0000 +++ libcommons-compress-java-1.20/debian/control 2020-04-01 02:41:01.000000000 +0000 @@ -7,7 +7,7 @@ Jakub Adam , Emmanuel Bourg Build-Depends: - debhelper (>= 12), + debhelper-compat (= 12), default-jdk, javahelper, junit4, @@ -21,6 +21,7 @@ Vcs-Git: https://salsa.debian.org/java-team/libcommons-compress-java.git Vcs-Browser: https://salsa.debian.org/java-team/libcommons-compress-java Homepage: https://commons.apache.org/proper/commons-compress/ +Rules-Requires-Root: no Package: libcommons-compress-java Architecture: all diff -Nru libcommons-compress-java-1.19/debian/patches/disable-brotli.patch libcommons-compress-java-1.20/debian/patches/disable-brotli.patch --- libcommons-compress-java-1.19/debian/patches/disable-brotli.patch 2020-01-27 11:32:00.000000000 +0000 +++ libcommons-compress-java-1.20/debian/patches/disable-brotli.patch 2020-04-01 02:41:01.000000000 +0000 @@ -1,6 +1,6 @@ --- a/pom.xml +++ b/pom.xml -@@ -331,6 +331,17 @@ +@@ -332,6 +332,17 @@ diff -Nru libcommons-compress-java-1.19/debian/patches/disable-osgi-tests.patch libcommons-compress-java-1.20/debian/patches/disable-osgi-tests.patch --- libcommons-compress-java-1.19/debian/patches/disable-osgi-tests.patch 2020-01-27 11:32:00.000000000 +0000 +++ libcommons-compress-java-1.20/debian/patches/disable-osgi-tests.patch 2020-04-01 02:41:01.000000000 +0000 @@ -1,6 +1,6 @@ --- a/pom.xml +++ b/pom.xml -@@ -340,6 +340,7 @@ +@@ -341,6 +341,7 @@ **/brotli/** **/zstandard/** diff -Nru libcommons-compress-java-1.19/debian/patches/disable-zstd.patch libcommons-compress-java-1.20/debian/patches/disable-zstd.patch --- libcommons-compress-java-1.19/debian/patches/disable-zstd.patch 2020-01-27 11:32:00.000000000 +0000 +++ libcommons-compress-java-1.20/debian/patches/disable-zstd.patch 2020-04-01 02:41:01.000000000 +0000 @@ -1,6 +1,6 @@ --- a/pom.xml +++ b/pom.xml -@@ -335,9 +335,11 @@ +@@ -336,9 +336,11 @@ **/brotli/** diff -Nru libcommons-compress-java-1.19/NOTICE.txt libcommons-compress-java-1.20/NOTICE.txt --- libcommons-compress-java-1.19/NOTICE.txt 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/NOTICE.txt 2020-01-21 12:27:08.000000000 +0000 @@ -1,11 +1,55 @@ Apache Commons Compress -Copyright 2002-2019 The Apache Software Foundation +Copyright 2002-2020 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (https://www.apache.org/). +--- + The files in the package org.apache.commons.compress.archivers.sevenz were derived from the LZMA SDK, version 9.20 (C/ and CPP/7zip/), which has been placed in the public domain: "LZMA SDK is placed in the public domain." (http://www.7-zip.org/sdk.html) + +--- + +The test file lbzip2_32767.bz2 has been copied from libbzip2's source +repository: + +This program, "bzip2", the associated library "libbzip2", and all +documentation, are copyright (C) 1996-2019 Julian R Seward. All +rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +3. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + +4. The name of the author may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Julian Seward, jseward@acm.org diff -Nru libcommons-compress-java-1.19/pom.xml libcommons-compress-java-1.20/pom.xml --- libcommons-compress-java-1.19/pom.xml 2019-08-24 11:29:45.000000000 +0000 +++ libcommons-compress-java-1.20/pom.xml 2020-02-05 05:00:52.000000000 +0000 @@ -24,7 +24,7 @@ commons-compress - 1.19 + 1.20 Apache Commons Compress https://commons.apache.org/proper/commons-compress/ @@ -49,6 +49,7 @@ ${project.version} RC1 + 1.19 1.7.4 3.12.0 @@ -87,13 +88,13 @@ junit junit - 4.12 + 4.13 test com.github.luben zstd-jni - 1.4.0-1 + 1.4.4-7 true @@ -149,7 +150,7 @@ org.apache.felix org.apache.felix.framework - 6.0.2 + 6.0.3 test diff -Nru libcommons-compress-java-1.19/RELEASE-NOTES.txt libcommons-compress-java-1.20/RELEASE-NOTES.txt --- libcommons-compress-java-1.19/RELEASE-NOTES.txt 2019-08-24 16:06:08.000000000 +0000 +++ libcommons-compress-java-1.20/RELEASE-NOTES.txt 2020-02-02 15:03:14.000000000 +0000 @@ -5,6 +5,61 @@ lzma, xz, Snappy, traditional Unix Compress, DEFLATE, DEFLATE64, LZ4, Brotli, Zstandard and ar, cpio, jar, tar, zip, dump, 7z, arj. +Release 1.20 +------------ + +Commons Compress 1.20 like any version of Commons Compress since 1.3 +can not be built from sources using Java 14 as Java 14 removes +support for the Pack200 format. We will address this issue with the +next release. + +Fixed Bugs: +o SevenZFile could throw NullPointerException rather than + IOException for certain archives. In addition it now handles + certain empty archives more gracefully. + Issue: COMPRESS-492. +o Deflate64CompressorInputStream.read would return 0 for some + inputs in violation of the InputStream.read contract. + Issue: COMPRESS-491. +o SeekableInMemoryByteChannel's truncate didn't set position + according to the spec in an edge case. + Issue: COMPRESS-499. +o BZip2CompressorInputStream now incorporates a similar patch as + the one that fixed CVE-2019-12900 in libbzip2. + + Commons Compress has not been vulnerable to this CVE as it + would have rejected a file with too many selectors. With this + patch Commons Compress will be able to read certain archives + that would have caused errors in Compress 1.19. Thanks to Joseph Allemandou. + +Changes: +o Update optional library com.github.luben:zstd-jni from + 1.4.0-1 to 1.4.4-7. + Issue: COMPRESS-493. +o Update tests from org.apache.felix:org.apache.felix.framework + 6.0.2 to 6.0.3. +o SevenZFile can now recover from a certain corruption that + seems to happen occasionally when split archives are created. + Issue: COMPRESS-497. + Thanks to Stefan Schlott. +o Added random access support to SevenZFile. + Issue: COMPRESS-342. + Thanks to Peter Alfred Lee. +o Added support for split ZIP archives. + Issue: COMPRESS-477. + Thanks to Peter Alfred Lee. +o Added support for reading sparse entries to the TAR package. + Issue: COMPRESS-124. + Thanks to Peter Alfred Lee. +o Update JUnit from 4.12 to 4.13. + +Removed: +o Removed the extraction code from the example CLI class inside + of the SevenZ package. Not only is it superseeded by the + examples package, its implementation was vulnerable to the + ZipSlip attack. + Issue: COMPRESS-495. + Release 1.19 ------------ @@ -25,59 +80,59 @@ using ZipFile which may speed up reading the archive at the cost of potentially missing important information. See the javadocs of the ZipFile class for details. - Issue: COMPRESS-466. + Issue: COMPRESS-466. o TarArchiveInputStream has a new constructor-arg lenient that can be used to accept certain broken archives. - Issue: COMPRESS-469. + Issue: COMPRESS-469. o ArjArchiveEntry and SevenZArchiveEntry now implement hashCode and equals. - Issue: COMPRESS-475. + Issue: COMPRESS-475. o Added a MultiReadOnlySeekableByteChannel class that can be used to concatenate the parts of a multi volume 7z archive so that SevenZFile can read them. Issue: COMPRESS-231. - Thanks to Tim Underwood. + Thanks to Tim Underwood. Fixed Bugs: o ZipArchiveInputStream could forget the compression level has - changed under certain circumstances. + changed under certain circumstances. o Fixed another potential resource leak in ParallelScatterZipCreator#writeTo. - Issue: COMPRESS-470. + Issue: COMPRESS-470. o ArArchiveInputStream could think it had hit EOF prematurely. Github Pull Request #74. - Thanks to Alex Bertram. + Thanks to Alex Bertram. o Throw IOException rather than RuntimeExceptions for certain malformed LZ4 or Snappy inputs. - Issue: COMPRESS-490. + Issue: COMPRESS-490. o ZipArchiveInputStream failed to read stored entries with a data descriptor if the data descriptor didn't use the signature invented by InfoZIP. - Issue: COMPRESS-482. + Issue: COMPRESS-482. Changes: o SevenZFile now provides a way to cap memory consumption for LZMA(2) compressed content. Github Pull Request #76. Issue: COMPRESS-481. - Thanks to Robin Schimpf. + Thanks to Robin Schimpf. o The ARJ package has been updated to contain constants for more recent specifications. Issue: COMPRESS-464. - Thanks to Rostislav Krasny. + Thanks to Rostislav Krasny. o Update optional library zstd-jni from 1.3.3-3 to 1.4.0-1. - Issue: COMPRESS-484. + Issue: COMPRESS-484. o ParallelScatterZipCreator now writes the entries to the gathered output in the same order they have been added. Github Pull Requests #78 and #79. Issue: COMPRESS-485. - Thanks to Hervé Boutemy, Tibor Digana. + Thanks to Hervé Boutemy, Tibor Digana. o The Expander and Archive example classes can leak resources they have wrapped around passed in streams or channels. The methods consuming streams and channels have been adapted to give the calling code a chance to deal with those wrapper resources. - Issue: COMPRESS-486. + Issue: COMPRESS-486. o ZipArchiveInputStream and ZipFile no longer assume Commons Compress would understand extra fields better than the writer of the archive and silently turn extra fields that Commons @@ -86,7 +141,7 @@ It is now possible to take more control over the extra field parsing process with a new overload of ZipArchiveEntry#getExtraFields. - Issue: COMPRESS-479. + Issue: COMPRESS-479. o ZipArchiveInputStream will now throw an exception if reading a stored entry with a data descriptor and the data descriptor doesn't match what it has actually read. @@ -102,7 +157,7 @@ The only other explanation is a broken archive. So the exception prevents users from thinking they had successfully read the contents of the archive. - Issue: COMPRESS-483. + Issue: COMPRESS-483. o The 7zip tools provide a default name for archive entries without name; SevenZFile returns a null name for such entries. A new method getDefaultName has been added to derive @@ -114,6 +169,9 @@ Release 1.18 ------------ +This release changes the OSGi Manifest entry Bundle-SymbolicName from +org.apache.commons.compress to org.apache.commons.commons-compress. + New features: o It is now possible to specify the arguments of zstd-jni's ZstdOutputStream constructors via Commons Compress as well. diff -Nru libcommons-compress-java-1.19/src/changes/changes.xml libcommons-compress-java-1.20/src/changes/changes.xml --- libcommons-compress-java-1.19/src/changes/changes.xml 2019-08-21 05:12:39.000000000 +0000 +++ libcommons-compress-java-1.20/src/changes/changes.xml 2020-01-26 12:39:13.000000000 +0000 @@ -42,7 +42,64 @@ Apache Commons Compress Release Notes - + + Update optional library com.github.luben:zstd-jni from 1.4.0-1 to 1.4.4-7. + + + Update tests from org.apache.felix:org.apache.felix.framework 6.0.2 to 6.0.3. + + + SevenZFile could throw NullPointerException rather than + IOException for certain archives. In addition it now handles + certain empty archives more gracefully. + + + Deflate64CompressorInputStream.read would return 0 for some + inputs in violation of the InputStream.read contract. + + + Removed the extraction code from the example CLI class inside + of the SevenZ package. Not only is it superseeded by the + examples package, its implementation was vulnerable to the + ZipSlip attack. + + + SevenZFile can now recover from a certain corruption that + seems to happen occasionally when split archives are created. + + + Added random access support to SevenZFile. + + + Added support for split ZIP archives. + + + Added support for reading sparse entries to the TAR package. + + + SeekableInMemoryByteChannel's truncate didn't set position + according to the spec in an edge case. + + + BZip2CompressorInputStream now incorporates a similar patch as + the one that fixed CVE-2019-12900 in libbzip2. + + Commons Compress has not been vulnerable to this CVE as it + would have rejected a file with too many selectors. With this + patch Commons Compress will be able to read certain archives + that would have caused errors in Compress 1.19. + + + Update JUnit from 4.12 to 4.13. + + + + NioZipEncoding#encode could enter an infinite loop for certain + inputs. + + description="Release 1.18 +---------------------------------------- + +This release changes the OSGi Manifest entry Bundle-SymbolicName from +org.apache.commons.compress to org.apache.commons.commons-compress. +"> The example Expander class has been vulnerable to a path traversal in the edge case that happens when the target diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/ar/ArArchiveInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/ar/ArArchiveInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/ar/ArArchiveInputStream.java 2019-08-12 09:44:17.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/ar/ArArchiveInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -257,6 +257,9 @@ */ @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } if (currentEntry == null) { throw new IllegalStateException("No current ar entry"); } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/ar/package.html libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/ar/package.html --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/ar/package.html 2019-08-23 15:05:24.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/ar/package.html 2020-01-07 14:40:25.000000000 +0000 @@ -24,5 +24,7 @@

Provides stream classes for reading and writing archives using the AR format.

+

The ar format is used - among many other things - for + Debian .deb packages.

diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java 2019-08-12 09:44:17.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -345,6 +345,9 @@ @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } if (currentLocalFileHeader == null) { throw new IllegalStateException("No current arj entry"); } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/dump/DumpArchiveInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/dump/DumpArchiveInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/dump/DumpArchiveInputStream.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/dump/DumpArchiveInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -465,6 +465,9 @@ */ @Override public int read(final byte[] buf, int off, int len) throws IOException { + if (len == 0) { + return 0; + } int totalRead = 0; if (hasHitEOF || isClosed || entryOffset >= entrySize) { diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/dump/TapeInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/dump/TapeInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/dump/TapeInputStream.java 2019-08-12 09:44:17.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/dump/TapeInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -120,6 +120,9 @@ */ @Override public int read(final byte[] b, int off, final int len) throws IOException { + if (len == 0) { + return 0; + } if ((len % RECORD_SIZE) != 0) { throw new IllegalArgumentException( "All reads must be multiple of record size (" + RECORD_SIZE + diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/examples/Archiver.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/examples/Archiver.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/examples/Archiver.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/examples/Archiver.java 2020-01-26 12:39:13.000000000 +0000 @@ -206,10 +206,12 @@ public void create(final ArchiveOutputStream target, File directory) throws IOException, ArchiveException { create(directory, new ArchiveEntryCreator() { + @Override public ArchiveEntry create(File f, String entryName) throws IOException { return target.createArchiveEntry(f, entryName); } }, new ArchiveEntryConsumer() { + @Override public void accept(File source, ArchiveEntry e) throws IOException { target.putArchiveEntry(e); if (!e.isDirectory()) { @@ -220,6 +222,7 @@ target.closeArchiveEntry(); } }, new Finisher() { + @Override public void finish() throws IOException { target.finish(); } @@ -236,10 +239,12 @@ */ public void create(final SevenZOutputFile target, File directory) throws IOException { create(directory, new ArchiveEntryCreator() { + @Override public ArchiveEntry create(File f, String entryName) throws IOException { return target.createArchiveEntry(f, entryName); } }, new ArchiveEntryConsumer() { + @Override public void accept(File source, ArchiveEntry e) throws IOException { target.putArchiveEntry(e); if (!e.isDirectory()) { @@ -256,6 +261,7 @@ target.closeArchiveEntry(); } }, new Finisher() { + @Override public void finish() throws IOException { target.finish(); } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/examples/CloseableConsumerAdapter.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/examples/CloseableConsumerAdapter.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/examples/CloseableConsumerAdapter.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/examples/CloseableConsumerAdapter.java 2020-01-07 14:40:25.000000000 +0000 @@ -20,16 +20,14 @@ import java.io.Closeable; import java.io.IOException; +import java.util.Objects; final class CloseableConsumerAdapter implements Closeable { private final CloseableConsumer consumer; private Closeable closeable; CloseableConsumerAdapter(CloseableConsumer consumer) { - if (consumer == null) { - throw new NullPointerException("consumer must not be null"); - } - this.consumer = consumer; + this.consumer = Objects.requireNonNull(consumer, "consumer"); } C track(C closeable) { diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/sevenz/Archive.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/sevenz/Archive.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/sevenz/Archive.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/sevenz/Archive.java 2020-01-07 14:40:25.000000000 +0000 @@ -23,17 +23,17 @@ /// Offset from beginning of file + SIGNATURE_HEADER_SIZE to packed streams. long packPos; /// Size of each packed stream. - long[] packSizes; + long[] packSizes = new long[0]; /// Whether each particular packed streams has a CRC. BitSet packCrcsDefined; /// CRCs for each packed stream, valid only if that packed stream has one. long[] packCrcs; /// Properties of solid compression blocks. - Folder[] folders; + Folder[] folders = new Folder[0]; /// Temporary properties for non-empty files (subsumed into the files array later). SubStreamsInfo subStreamsInfo; /// The files and directories in the archive. - SevenZArchiveEntry[] files; + SevenZArchiveEntry[] files = new SevenZArchiveEntry[0]; /// Mapping between folders, files and streams. StreamMap streamMap; diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/sevenz/BoundedSeekableByteChannelInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/sevenz/BoundedSeekableByteChannelInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/sevenz/BoundedSeekableByteChannelInputStream.java 2019-08-12 09:44:17.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/sevenz/BoundedSeekableByteChannelInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -52,8 +52,22 @@ return -1; } + /** + * Reads up to len bytes of data from the input stream into an array of bytes. + * + *

An attempt is made to read as many as len bytes, but a + * smaller number may be read. The number of bytes actually read + * is returned as an integer.

+ * + *

This implementation may return 0 if the underlying {@link + * SeekableByteChannel} is non-blocking and currently hasn't got + * any bytes available.

+ */ @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } if (bytesRemaining <= 0) { return -1; } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/sevenz/CLI.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/sevenz/CLI.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/sevenz/CLI.java 2019-08-12 09:44:17.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/sevenz/CLI.java 2020-01-23 05:35:44.000000000 +0000 @@ -19,8 +19,6 @@ import java.io.File; import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; public class CLI { @@ -63,44 +61,6 @@ } return sb.toString(); } - }, - EXTRACT("Extracting") { - private final byte[] buf = new byte[8192]; - @Override - public void takeAction(final SevenZFile archive, final SevenZArchiveEntry entry) - throws IOException { - final File outFile = new File(entry.getName()); - if (entry.isDirectory()) { - if (!outFile.isDirectory() && !outFile.mkdirs()) { - throw new IOException("Cannot create directory " + outFile); - } - System.out.println("created directory " + outFile); - return; - } - - System.out.println("extracting to " + outFile); - final File parent = outFile.getParentFile(); - if (parent != null && !parent.exists() && !parent.mkdirs()) { - throw new IOException("Cannot create " + parent); - } - try (final OutputStream fos = Files.newOutputStream(outFile.toPath())) { - final long total = entry.getSize(); - long off = 0; - while (off < total) { - final int toRead = (int) Math.min(total - off, buf.length); - final int bytesRead = archive.read(buf, 0, toRead); - if (bytesRead < 1) { - throw new IOException("Reached end of entry " - + entry.getName() - + " after " + off - + " bytes, expected " - + total); - } - off += bytesRead; - fos.write(buf, 0, bytesRead); - } - } - } }; private final String message; @@ -134,7 +94,7 @@ } private static void usage() { - System.out.println("Parameters: archive-name [list|extract]"); + System.out.println("Parameters: archive-name [list]"); } private static Mode grabMode(final String[] args) { diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java 2019-08-20 19:23:36.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java 2020-01-26 12:39:13.000000000 +0000 @@ -126,7 +126,7 @@ * @since 1.19 */ public SevenZFile(final File fileName, final char[] password, SevenZFileOptions options) throws IOException { - this(Files.newByteChannel(fileName.toPath(), EnumSet.of(StandardOpenOption.READ)), + this(Files.newByteChannel(fileName.toPath(), EnumSet.of(StandardOpenOption.READ)), // NOSONAR fileName.getAbsolutePath(), utf16Decode(password), true, options); } @@ -140,6 +140,7 @@ * @throws IOException if reading the archive fails * @deprecated use the char[]-arg version for the password instead */ + @Deprecated public SevenZFile(final File fileName, final byte[] password) throws IOException { this(Files.newByteChannel(fileName.toPath(), EnumSet.of(StandardOpenOption.READ)), fileName.getAbsolutePath(), password, true, SevenZFileOptions.DEFAULT); @@ -298,6 +299,7 @@ * @since 1.13 * @deprecated use the char[]-arg version for the password instead */ + @Deprecated public SevenZFile(final SeekableByteChannel channel, final byte[] password) throws IOException { this(channel, DEFAULT_FILE_NAME, password); @@ -319,6 +321,7 @@ * @since 1.13 * @deprecated use the char[]-arg version for the password instead */ + @Deprecated public SevenZFile(final SeekableByteChannel channel, String fileName, final byte[] password) throws IOException { this(channel, fileName, password, false, SevenZFileOptions.DEFAULT); @@ -402,7 +405,7 @@ if (entry.getName() == null && options.getUseDefaultNameForUnnamedEntries()) { entry.setName(getDefaultName()); } - buildDecodingStream(); + buildDecodingStream(currentEntryIndex); uncompressedBytesReadFromCurrentEntry = compressedBytesReadFromCurrentEntry = 0; return entry; } @@ -441,17 +444,86 @@ archiveVersionMajor, archiveVersionMinor)); } + boolean headerLooksValid = false; // See https://www.7-zip.org/recover.html - "There is no correct End Header at the end of archive" final long startHeaderCrc = 0xffffFFFFL & buf.getInt(); - final StartHeader startHeader = readStartHeader(startHeaderCrc); + if (startHeaderCrc == 0) { + // This is an indication of a corrupt header - peek the next 20 bytes + long currentPosition = channel.position(); + ByteBuffer peekBuf = ByteBuffer.allocate(20); + readFully(peekBuf); + channel.position(currentPosition); + // Header invalid if all data is 0 + while (peekBuf.hasRemaining()) { + if (peekBuf.get()!=0) { + headerLooksValid = true; + break; + } + } + } else { + headerLooksValid = true; + } + + if (headerLooksValid) { + final StartHeader startHeader = readStartHeader(startHeaderCrc); + return initializeArchive(startHeader, password, true); + } else { + // No valid header found - probably first file of multipart archive was removed too early. Scan for end header. + return tryToLocateEndHeader(password); + } + } + + private Archive tryToLocateEndHeader(final byte[] password) throws IOException { + ByteBuffer nidBuf = ByteBuffer.allocate(1); + final long searchLimit = 1024l * 1024 * 1; + // Main header, plus bytes that readStartHeader would read + final long previousDataSize = channel.position() + 20; + final long minPos; + // Determine minimal position - can't start before current position + if (channel.position() + searchLimit > channel.size()) { + minPos = channel.position(); + } else { + minPos = channel.size() - searchLimit; + } + long pos = channel.size() - 1; + // Loop: Try from end of archive + while (pos > minPos) { + pos--; + channel.position(pos); + nidBuf.rewind(); + channel.read(nidBuf); + int nid = nidBuf.array()[0]; + // First indicator: Byte equals one of these header identifiers + if (nid == NID.kEncodedHeader || nid == NID.kHeader) { + try { + // Try to initialize Archive structure from here + final StartHeader startHeader = new StartHeader(); + startHeader.nextHeaderOffset = pos - previousDataSize; + startHeader.nextHeaderSize = channel.size() - pos; + Archive result = initializeArchive(startHeader, password, false); + // Sanity check: There must be some data... + if (result.packSizes != null && result.files.length > 0) { + return result; + } + } catch (Exception ignore) { + // Wrong guess... + } + } + } + throw new IOException("Start header corrupt and unable to guess end header"); + } + + private Archive initializeArchive(StartHeader startHeader, final byte[] password, boolean verifyCrc) throws IOException { assertFitsIntoInt("nextHeaderSize", startHeader.nextHeaderSize); final int nextHeaderSizeInt = (int) startHeader.nextHeaderSize; channel.position(SIGNATURE_HEADER_SIZE + startHeader.nextHeaderOffset); - buf = ByteBuffer.allocate(nextHeaderSizeInt).order(ByteOrder.LITTLE_ENDIAN); + ByteBuffer buf = ByteBuffer.allocate(nextHeaderSizeInt).order(ByteOrder.LITTLE_ENDIAN); readFully(buf); - final CRC32 crc = new CRC32(); - crc.update(buf.array()); - if (startHeader.nextHeaderCrc != crc.getValue()) { - throw new IOException("NextHeader CRC mismatch"); + if (verifyCrc) { + final CRC32 crc = new CRC32(); + crc.update(buf.array()); + if (startHeader.nextHeaderCrc != crc.getValue()) { + throw new IOException("NextHeader CRC mismatch"); + } } Archive archive = new Archive(); @@ -781,8 +853,8 @@ } // would need to keep looping as above: while (moreAlternativeMethods) { - throw new IOException("Alternative methods are unsupported, please report. " + - "The reference implementation doesn't support them either."); + throw new IOException("Alternative methods are unsupported, please report. " + // NOSONAR + "The reference implementation doesn't support them either."); } } folder.coders = coders; @@ -1002,6 +1074,9 @@ for (int i = 0; i < files.length; i++) { files[i].setHasStream(isEmptyStream == null || !isEmptyStream.get(i)); if (files[i].hasStream()) { + if (archive.subStreamsInfo == null) { + throw new IOException("Archive contains file with streams but no subStreamsInfo"); + } files[i].setDirectory(false); files[i].setAntiItem(false); files[i].setHasCrc(archive.subStreamsInfo.hasCrc.get(nonEmptyFileCounter)); @@ -1073,15 +1148,23 @@ archive.streamMap = streamMap; } - private void buildDecodingStream() throws IOException { - final int folderIndex = archive.streamMap.fileFolderIndex[currentEntryIndex]; + private void buildDecodingStream(int entryIndex) throws IOException { + if (archive.streamMap == null) { + throw new IOException("Archive doesn't contain stream information to read entries"); + } + final int folderIndex = archive.streamMap.fileFolderIndex[entryIndex]; if (folderIndex < 0) { deferredBlockStreams.clear(); // TODO: previously it'd return an empty stream? // new BoundedInputStream(new ByteArrayInputStream(new byte[0]), 0); return; } - final SevenZArchiveEntry file = archive.files[currentEntryIndex]; + final SevenZArchiveEntry file = archive.files[entryIndex]; + boolean isInSameFolder = false; + final Folder folder = archive.folders[folderIndex]; + final int firstPackStreamIndex = archive.streamMap.folderFirstPackStreamIndex[folderIndex]; + final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos + + archive.streamMap.packStreamOffsets[firstPackStreamIndex]; if (currentFolderIndex == folderIndex) { // (COMPRESS-320). // The current entry is within the same (potentially opened) folder. The @@ -1089,7 +1172,17 @@ // but don't do it eagerly -- if the user skips over the entire folder nothing // is effectively decompressed. - file.setContentMethods(archive.files[currentEntryIndex - 1].getContentMethods()); + file.setContentMethods(archive.files[entryIndex - 1].getContentMethods()); + + // if this is called in a random access, then the content methods of previous entry may be null + // the content methods should be set to methods of the first entry as it must not be null, + // and the content methods would only be set if the content methods was not set + if(currentEntryIndex != entryIndex && file.getContentMethods() == null) { + int folderFirstFileIndex = archive.streamMap.folderFirstFileIndex[folderIndex]; + SevenZArchiveEntry folderFirstFile = archive.files[folderFirstFileIndex]; + file.setContentMethods(folderFirstFile.getContentMethods()); + } + isInSameFolder = true; } else { // We're opening a new folder. Discard any queued streams/ folder stream. currentFolderIndex = folderIndex; @@ -1099,13 +1192,41 @@ currentFolderInputStream = null; } - final Folder folder = archive.folders[folderIndex]; - final int firstPackStreamIndex = archive.streamMap.folderFirstPackStreamIndex[folderIndex]; - final long folderOffset = SIGNATURE_HEADER_SIZE + archive.packPos + - archive.streamMap.packStreamOffsets[firstPackStreamIndex]; currentFolderInputStream = buildDecoderStack(folder, folderOffset, firstPackStreamIndex, file); } + // if this mothod is called in a random access, then some entries need to be skipped, + // if the entry of entryIndex is in the same folder of the currentFolderIndex, + // then the entries between (currentEntryIndex + 1) and (entryIndex - 1) need to be skipped, + // otherwise it's a new folder, and the entries between firstFileInFolderIndex and (entryIndex - 1) need to be skipped + if(currentEntryIndex != entryIndex) { + int filesToSkipStartIndex = archive.streamMap.folderFirstFileIndex[folderIndex]; + if(isInSameFolder) { + if(currentEntryIndex < entryIndex) { + // the entries between filesToSkipStartIndex and currentEntryIndex had already been skipped + filesToSkipStartIndex = currentEntryIndex + 1; + } else { + // the entry is in the same folder of current entry, but it has already been read before, we need to reset + // the position of the currentFolderInputStream to the beginning of folder, and then skip the files + // from the start entry of the folder again + deferredBlockStreams.clear(); + channel.position(folderOffset); + } + } + + for(int i = filesToSkipStartIndex;i < entryIndex;i++) { + SevenZArchiveEntry fileToSkip = archive.files[i]; + InputStream fileStreamToSkip = new BoundedInputStream(currentFolderInputStream, fileToSkip.getSize()); + if (fileToSkip.getHasCrc()) { + fileStreamToSkip = new CRC32VerifyingInputStream(fileStreamToSkip, fileToSkip.getSize(), fileToSkip.getCrcValue()); + } + deferredBlockStreams.add(fileStreamToSkip); + + // set the content methods as well, it equals to file.getContentMethods() because they are in same folder + fileToSkip.setContentMethods(file.getContentMethods()); + } + } + InputStream fileStream = new BoundedInputStream(currentFolderInputStream, file.getSize()); if (file.getHasCrc()) { fileStream = new CRC32VerifyingInputStream(fileStream, file.getSize(), file.getCrcValue()); @@ -1134,6 +1255,9 @@ } @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } final int r = in.read(b, off, len); if (r >= 0) { count(r); @@ -1200,6 +1324,37 @@ } /** + * Returns an InputStream for reading the contents of the given entry. + * + *

For archives using solid compression randomly accessing + * entries will be significantly slower than reading the archive + * sequentiallly.

+ * + * @param entry the entry to get the stream for. + * @return a stream to read the entry from. + * @throws IOException if unable to create an input stream from the zipentry + * @since Compress 1.20 + */ + public InputStream getInputStream(SevenZArchiveEntry entry) throws IOException { + int entryIndex = -1; + for (int i = 0; i < this.archive.files.length;i++) { + if (entry == this.archive.files[i]) { + entryIndex = i; + break; + } + } + + if (entryIndex < 0) { + throw new IllegalArgumentException("Can not find " + entry.getName() + " in " + this.fileName); + } + + buildDecodingStream(entryIndex); + currentEntryIndex = entryIndex; + currentFolderIndex = archive.streamMap.fileFolderIndex[entryIndex]; + return getCurrentStream(); + } + + /** * Reads data into an array of bytes. * * @param b the array to write data to @@ -1222,6 +1377,9 @@ * if an I/O error has occurred */ public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } int cnt = getCurrentStream().read(b, off, len); if (cnt > 0) { uncompressedBytesReadFromCurrentEntry += cnt; diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFile.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFile.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFile.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFile.java 2020-01-23 19:54:20.000000000 +0000 @@ -309,7 +309,8 @@ throw new IllegalStateException("No current 7z entry"); } - OutputStream out = new OutputStreamWrapper(); + // doesn't need to be closed, just wraps the instance field channel + OutputStream out = new OutputStreamWrapper(); // NOSONAR final ArrayList moreStreams = new ArrayList<>(); boolean first = true; for (final SevenZMethodConfiguration m : getContentMethods(files.get(files.size() - 1))) { diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveEntry.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveEntry.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveEntry.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveEntry.java 2020-01-23 17:22:37.000000000 +0000 @@ -20,11 +20,14 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; + import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipEncoding; import org.apache.commons.compress.utils.ArchiveUtils; @@ -200,6 +203,9 @@ /** The entry's minor device number. */ private int devMinor = 0; + /** The sparse headers in tar */ + private List sparseHeaders; + /** If an extension sparse header follows. */ private boolean isExtended; @@ -209,6 +215,10 @@ /** is this entry a GNU sparse entry using one of the PAX formats? */ private boolean paxGNUSparse; + /** is this entry a GNU sparse entry using 1.X PAX formats? + * the sparse headers of 1.x PAX Format is stored in file data block */ + private boolean paxGNU1XSparse = false; + /** is this entry a star sparse entry using the PAX header? */ private boolean starSparse; @@ -742,6 +752,35 @@ } /** + * Set this entry's sparse headers + * @param sparseHeaders The new sparse headers + * @since 1.20 + */ + public void setSparseHeaders(List sparseHeaders) { + this.sparseHeaders = sparseHeaders; + } + + /** + * Get this entry's sparse headers + * + * @return This entry's sparse headers + * @since 1.20 + */ + public List getSparseHeaders() { + return sparseHeaders; + } + + /** + * Get if this entry is a sparse file with 1.X PAX Format or not + * + * @return True if this entry is a sparse file with 1.X PAX Format + * @since 1.20 + */ + public boolean isPaxGNU1XSparse() { + return paxGNU1XSparse; + } + + /** * Set this entry's file size. * * @param size This entry's new file size. @@ -816,10 +855,14 @@ /** * Get this entry's real file size in case of a sparse file. + *

If the file is not a sparse file, return size instead of realSize.

* - * @return This entry's real file size. + * @return This entry's real file size, if the file is not a sparse file, return size instead of realSize. */ public long getRealSize() { + if (!isSparse()) { + return size; + } return realSize; } @@ -1341,6 +1384,16 @@ offset += OFFSETLEN_GNU; offset += LONGNAMESLEN_GNU; offset += PAD2LEN_GNU; + sparseHeaders = new ArrayList<>(); + for (int i = 0; i < SPARSE_HEADERS_IN_OLDGNU_HEADER; i++) { + TarArchiveStructSparse sparseHeader = TarUtils.parseSparse(header, + offset + i * (SPARSE_OFFSET_LEN + SPARSE_NUMBYTES_LEN)); + + // some sparse headers are empty, we need to skip these sparse headers + if(sparseHeader.getOffset() > 0 || sparseHeader.getNumbytes() > 0) { + sparseHeaders.add(sparseHeader); + } + } offset += SPARSELEN_GNU; isExtended = TarUtils.parseBoolean(header, offset); offset += ISEXTENDEDLEN_GNU; @@ -1461,6 +1514,7 @@ void fillGNUSparse1xData(final Map headers) { paxGNUSparse = true; + paxGNU1XSparse = true; realSize = Integer.parseInt(headers.get("GNU.sparse.realsize")); name = headers.get("GNU.sparse.name"); } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStream.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStream.java 2020-02-02 14:52:36.000000000 +0000 @@ -26,7 +26,11 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.commons.compress.archivers.ArchiveEntry; @@ -34,6 +38,7 @@ import org.apache.commons.compress.archivers.zip.ZipEncoding; import org.apache.commons.compress.archivers.zip.ZipEncodingHelper; import org.apache.commons.compress.utils.ArchiveUtils; +import org.apache.commons.compress.utils.BoundedInputStream; import org.apache.commons.compress.utils.CharsetNames; import org.apache.commons.compress.utils.IOUtils; @@ -66,7 +71,13 @@ private long entryOffset; /** An input stream to read from */ - private final InputStream is; + private final InputStream inputStream; + + /** Input streams for reading sparse entries **/ + private List sparseInputStreams; + + /** the index of current input stream being read when reading sparse entries */ + private int currentSparseInputStreamIndex; /** The meta-data about the current entry */ private TarArchiveEntry currEntry; @@ -80,6 +91,9 @@ // the global PAX header private Map globalPaxHeaders = new HashMap<>(); + // the global sparse headers, this is only used in PAX Format 0.X + private final List globalSparseHeaders = new ArrayList<>(); + private final boolean lenient; /** @@ -170,7 +184,7 @@ */ public TarArchiveInputStream(final InputStream is, final int blockSize, final int recordSize, final String encoding, boolean lenient) { - this.is = is; + this.inputStream = is; this.hasHitEOF = false; this.encoding = encoding; this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); @@ -185,7 +199,14 @@ */ @Override public void close() throws IOException { - is.close(); + // Close all the input streams in sparseInputStreams + if(sparseInputStreams != null) { + for (InputStream inputStream : sparseInputStreams) { + inputStream.close(); + } + } + + inputStream.close(); } /** @@ -214,10 +235,11 @@ if (isDirectory()) { return 0; } - if (entrySize - entryOffset > Integer.MAX_VALUE) { + + if (currEntry.getRealSize() - entryOffset > Integer.MAX_VALUE) { return Integer.MAX_VALUE; } - return (int) (entrySize - entryOffset); + return (int) (currEntry.getRealSize() - entryOffset); } @@ -243,14 +265,47 @@ return 0; } - final long available = entrySize - entryOffset; - final long skipped = IOUtils.skip(is, Math.min(n, available)); + final long available = currEntry.getRealSize() - entryOffset; + final long skipped; + if (!currEntry.isSparse()) { + skipped = IOUtils.skip(inputStream, Math.min(n, available)); + } else { + skipped = skipSparse(Math.min(n, available)); + } count(skipped); entryOffset += skipped; return skipped; } /** + * Skip n bytes from current input stream, if the current input stream doesn't have enough data to skip, + * jump to the next input stream and skip the rest bytes, keep doing this until total n bytes are skipped + * or the input streams are all skipped + * + * @param n bytes of data to skip + * @return actual bytes of data skipped + * @throws IOException + */ + private long skipSparse(final long n) throws IOException { + if (sparseInputStreams == null || sparseInputStreams.size() == 0) { + return inputStream.skip(n); + } + + long bytesSkipped = 0; + + while (bytesSkipped < n && currentSparseInputStreamIndex < sparseInputStreams.size()) { + final InputStream currentInputStream = sparseInputStreams.get(currentSparseInputStreamIndex); + bytesSkipped += currentInputStream.skip(n - bytesSkipped); + + if (bytesSkipped < n) { + currentSparseInputStreamIndex++; + } + } + + return bytesSkipped; + } + + /** * Since we do not support marking just yet, we return false. * * @return False. @@ -266,7 +321,7 @@ * @param markLimit The limit to mark. */ @Override - public void mark(final int markLimit) { + public synchronized void mark(final int markLimit) { } /** @@ -348,7 +403,7 @@ if (currEntry.isPaxHeader()){ // Process Pax headers paxHeaders(); } else if (!globalPaxHeaders.isEmpty()) { - applyPaxHeadersToCurrentEntry(globalPaxHeaders); + applyPaxHeadersToCurrentEntry(globalPaxHeaders, globalSparseHeaders); } if (currEntry.isOldGNUSparse()){ // Process sparse files @@ -372,7 +427,7 @@ if (!isDirectory() && this.entrySize > 0 && this.entrySize % this.recordSize != 0) { final long numRecords = (this.entrySize / this.recordSize) + 1; final long padding = (numRecords * this.recordSize) - this.entrySize; - final long skipped = IOUtils.skip(is, padding); + final long skipped = IOUtils.skip(inputStream, padding); count(skipped); } } @@ -456,7 +511,7 @@ final byte[] record = new byte[recordSize]; - final int readNow = IOUtils.readFully(is, record); + final int readNow = IOUtils.readFully(inputStream, record); count(readNow); if (readNow != recordSize) { return null; @@ -466,35 +521,177 @@ } private void readGlobalPaxHeaders() throws IOException { - globalPaxHeaders = parsePaxHeaders(this); + globalPaxHeaders = parsePaxHeaders(this, globalSparseHeaders); getNextEntry(); // Get the actual file entry } + /** + * For PAX Format 0.0, the sparse headers(GNU.sparse.offset and GNU.sparse.numbytes) + * may appear multi times, and they look like: + * + * GNU.sparse.size=size + * GNU.sparse.numblocks=numblocks + * repeat numblocks times + * GNU.sparse.offset=offset + * GNU.sparse.numbytes=numbytes + * end repeat + * + * + * For PAX Format 0.1, the sparse headers are stored in a single variable : GNU.sparse.map + * + * GNU.sparse.map + * Map of non-null data chunks. It is a string consisting of comma-separated values "offset,size[,offset-1,size-1...]" + * + * + * For PAX Format 1.X: + * The sparse map itself is stored in the file data block, preceding the actual file data. + * It consists of a series of decimal numbers delimited by newlines. The map is padded with nulls to the nearest block boundary. + * The first number gives the number of entries in the map. Following are map entries, each one consisting of two numbers + * giving the offset and size of the data block it describes. + * @throws IOException + */ private void paxHeaders() throws IOException{ - final Map headers = parsePaxHeaders(this); + List sparseHeaders = new ArrayList<>(); + final Map headers = parsePaxHeaders(this, sparseHeaders); + + // for 0.1 PAX Headers + if (headers.containsKey("GNU.sparse.map")) { + sparseHeaders = parsePAX01SparseHeaders(headers.get("GNU.sparse.map")); + } getNextEntry(); // Get the actual file entry - applyPaxHeadersToCurrentEntry(headers); + applyPaxHeadersToCurrentEntry(headers, sparseHeaders); + + // for 1.0 PAX Format, the sparse map is stored in the file data block + if (currEntry.isPaxGNU1XSparse()) { + sparseHeaders = parsePAX1XSparseHeaders(); + currEntry.setSparseHeaders(sparseHeaders); + } + + // sparse headers are all done reading, we need to build + // sparse input streams using these sparse headers + buildSparseInputStreams(); } - // NOTE, using a Map here makes it impossible to ever support GNU - // sparse files using the PAX Format 0.0, see - // https://www.gnu.org/software/tar/manual/html_section/tar_92.html#SEC188 - Map parsePaxHeaders(final InputStream i) + /** + * For PAX Format 0.1, the sparse headers are stored in a single variable : GNU.sparse.map + * GNU.sparse.map + * Map of non-null data chunks. It is a string consisting of comma-separated values "offset,size[,offset-1,size-1...]" + * + * @param sparseMap the sparse map string consisting of comma-separated values "offset,size[,offset-1,size-1...]" + * @return sparse headers parsed from sparse map + * @throws IOException + */ + private List parsePAX01SparseHeaders(String sparseMap) throws IOException { + List sparseHeaders = new ArrayList<>(); + String[] sparseHeaderStrings = sparseMap.split(","); + + for (int i = 0; i < sparseHeaderStrings.length;i += 2) { + long sparseOffset = Long.parseLong(sparseHeaderStrings[i]); + long sparseNumbytes = Long.parseLong(sparseHeaderStrings[i + 1]); + sparseHeaders.add(new TarArchiveStructSparse(sparseOffset, sparseNumbytes)); + } + + return sparseHeaders; + } + + /** + * For PAX Format 1.X: + * The sparse map itself is stored in the file data block, preceding the actual file data. + * It consists of a series of decimal numbers delimited by newlines. The map is padded with nulls to the nearest block boundary. + * The first number gives the number of entries in the map. Following are map entries, each one consisting of two numbers + * giving the offset and size of the data block it describes. + * @return sparse headers + * @throws IOException + */ + private List parsePAX1XSparseHeaders() throws IOException { + // for 1.X PAX Headers + List sparseHeaders = new ArrayList<>(); + long bytesRead = 0; + + long[] readResult = readLineOfNumberForPax1X(inputStream); + long sparseHeadersCount = readResult[0]; + bytesRead += readResult[1]; + while (sparseHeadersCount-- > 0) { + readResult = readLineOfNumberForPax1X(inputStream); + long sparseOffset = readResult[0]; + bytesRead += readResult[1]; + + readResult = readLineOfNumberForPax1X(inputStream); + long sparseNumbytes = readResult[0]; + bytesRead += readResult[1]; + sparseHeaders.add(new TarArchiveStructSparse(sparseOffset, sparseNumbytes)); + } + + // skip the rest of this record data + long bytesToSkip = recordSize - bytesRead % recordSize; + IOUtils.skip(inputStream, bytesToSkip); + return sparseHeaders; + } + + /** + * For 1.X PAX Format, the sparse headers are stored in the file data block, preceding the actual file data. + * It consists of a series of decimal numbers delimited by newlines. + * + * @param inputStream the input stream of the tar file + * @return the decimal number delimited by '\n', and the bytes read from input stream + * @throws IOException + */ + private long[] readLineOfNumberForPax1X(InputStream inputStream) throws IOException { + int number; + long result = 0; + long bytesRead = 0; + + while((number = inputStream.read()) != '\n') { + bytesRead += 1; + if(number == -1) { + throw new IOException("Unexpected EOF when reading parse information of 1.X PAX format"); + } + result = result * 10 + (number - '0'); + } + bytesRead += 1; + + return new long[] {result, bytesRead}; + } + + /** + * For PAX Format 0.0, the sparse headers(GNU.sparse.offset and GNU.sparse.numbytes) + * may appear multi times, and they look like: + * + * GNU.sparse.size=size + * GNU.sparse.numblocks=numblocks + * repeat numblocks times + * GNU.sparse.offset=offset + * GNU.sparse.numbytes=numbytes + * end repeat + * + * For PAX Format 0.1, the sparse headers are stored in a single variable : GNU.sparse.map + * + * GNU.sparse.map + * Map of non-null data chunks. It is a string consisting of comma-separated values "offset,size[,offset-1,size-1...]" + * + * @param inputstream inputstream to read keys and values + * @param sparseHeaders used in PAX Format 0.0 & 0.1, as it may appear multi times, + * the sparse headers need to be stored in an array, not a map + * @return map of PAX headers values found inside of the current (local or global) PAX headers tar entry. + * @throws IOException + */ + Map parsePaxHeaders(final InputStream inputStream, List sparseHeaders) throws IOException { final Map headers = new HashMap<>(globalPaxHeaders); + Long offset = null; // Format is "length keyword=value\n"; - while(true){ // get length + while(true) { // get length int ch; int len = 0; int read = 0; - while((ch = i.read()) != -1) { + while((ch = inputStream.read()) != -1) { read++; if (ch == '\n') { // blank line in header break; } else if (ch == ' '){ // End of length string // Get keyword final ByteArrayOutputStream coll = new ByteArrayOutputStream(); - while((ch = i.read()) != -1) { + while((ch = inputStream.read()) != -1) { read++; if (ch == '='){ // end of keyword final String keyword = coll.toString(CharsetNames.UTF_8); @@ -504,7 +701,7 @@ headers.remove(keyword); } else { final byte[] rest = new byte[restLen]; - final int got = IOUtils.readFully(i, rest); + final int got = IOUtils.readFully(inputStream, rest); if (got != restLen) { throw new IOException("Failed to read " + "Paxheader. Expected " @@ -516,6 +713,25 @@ final String value = new String(rest, 0, restLen - 1, CharsetNames.UTF_8); headers.put(keyword, value); + + // for 0.0 PAX Headers + if (keyword.equals("GNU.sparse.offset")) { + if (offset != null) { + // previous GNU.sparse.offset header but but no numBytes + sparseHeaders.add(new TarArchiveStructSparse(offset, 0)); + } + offset = Long.valueOf(value); + } + + // for 0.0 PAX Headers + if (keyword.equals("GNU.sparse.numbytes")) { + if (offset == null) { + throw new IOException("Failed to read Paxheader." + + "GNU.sparse.offset is expected before GNU.sparse.numbytes shows up."); + } + sparseHeaders.add(new TarArchiveStructSparse(offset, Long.parseLong(value))); + offset = null; + } } break; } @@ -530,12 +746,16 @@ break; } } + if (offset != null) { + // offset but no numBytes + sparseHeaders.add(new TarArchiveStructSparse(offset, 0)); + } return headers; } - private void applyPaxHeadersToCurrentEntry(final Map headers) { + private void applyPaxHeadersToCurrentEntry(final Map headers, final List sparseHeaders) { currEntry.updateEntryFromPaxHeaders(headers); - + currEntry.setSparseHeaders(sparseHeaders); } /** @@ -543,14 +763,8 @@ * including any additional sparse entries following the current entry. * * @throws IOException on error - * - * @todo Sparse files get not yet really processed. */ private void readOldGNUSparse() throws IOException { - /* we do not really process sparse files yet - sparses = new ArrayList(); - sparses.addAll(currEntry.getSparses()); - */ if (currEntry.isExtended()) { TarArchiveSparseEntry entry; do { @@ -560,11 +774,13 @@ break; } entry = new TarArchiveSparseEntry(headerBuf); - /* we do not really process sparse files yet - sparses.addAll(entry.getSparses()); - */ + currEntry.getSparseHeaders().addAll(entry.getSparseHeaders()); } while (entry.isExtended()); } + + // sparse headers are all done reading, we need to build + // sparse input streams using these sparse headers + buildSparseInputStreams(); } private boolean isDirectory() { @@ -595,16 +811,16 @@ */ private void tryToConsumeSecondEOFRecord() throws IOException { boolean shouldReset = true; - final boolean marked = is.markSupported(); + final boolean marked = inputStream.markSupported(); if (marked) { - is.mark(recordSize); + inputStream.mark(recordSize); } try { shouldReset = !isEOFRecord(readRecord()); } finally { if (shouldReset && marked) { pushedBackBytes(recordSize); - is.reset(); + inputStream.reset(); } } } @@ -624,9 +840,12 @@ */ @Override public int read(final byte[] buf, final int offset, int numToRead) throws IOException { + if (numToRead == 0) { + return 0; + } int totalRead = 0; - if (isAtEOF() || isDirectory() || entryOffset >= entrySize) { + if (isAtEOF() || isDirectory()) { return -1; } @@ -634,9 +853,25 @@ throw new IllegalStateException("No current tar entry"); } + if (!currEntry.isSparse()) { + if (entryOffset >= entrySize) { + return -1; + } + } else { + // for sparse entries, there are actually currEntry.getRealSize() bytes to read + if (entryOffset >= currEntry.getRealSize()) { + return -1; + } + } + numToRead = Math.min(numToRead, available()); - totalRead = is.read(buf, offset, numToRead); + if (currEntry.isSparse()) { + // for sparse entries, we need to read them in another way + totalRead = readSparse(buf, offset, numToRead); + } else { + totalRead = inputStream.read(buf, offset, numToRead); + } if (totalRead == -1) { if (numToRead > 0) { @@ -652,6 +887,61 @@ } /** + * For sparse tar entries, there are many "holes"(consisting of all 0) in the file. Only the non-zero data is + * stored in tar files, and they are stored separately. The structure of non-zero data is introduced by the + * sparse headers using the offset, where a block of non-zero data starts, and numbytes, the length of the + * non-zero data block. + * When reading sparse entries, the actual data is read out with "holes" and non-zero data combined together + * according to the sparse headers. + * + * @param buf The buffer into which to place bytes read. + * @param offset The offset at which to place bytes read. + * @param numToRead The number of bytes to read. + * @return The number of bytes read, or -1 at EOF. + * @throws IOException on error + */ + private int readSparse(final byte[] buf, final int offset, int numToRead) throws IOException { + // if there are no actual input streams, just read from the original input stream + if (sparseInputStreams == null || sparseInputStreams.size() == 0) { + return inputStream.read(buf, offset, numToRead); + } + + if(currentSparseInputStreamIndex >= sparseInputStreams.size()) { + return -1; + } + + InputStream currentInputStream = sparseInputStreams.get(currentSparseInputStreamIndex); + int readLen = currentInputStream.read(buf, offset, numToRead); + + // if the current input stream is the last input stream, + // just return the number of bytes read from current input stream + if (currentSparseInputStreamIndex == sparseInputStreams.size() - 1) { + return readLen; + } + + // if EOF of current input stream is meet, open a new input stream and recursively call read + if (readLen == -1) { + currentSparseInputStreamIndex++; + return readSparse(buf, offset, numToRead); + } + + // if the rest data of current input stream is not long enough, open a new input stream + // and recursively call read + if (readLen < numToRead) { + currentSparseInputStreamIndex++; + int readLenOfNext = readSparse(buf, offset + readLen, numToRead - readLen); + if (readLenOfNext == -1) { + return readLen; + } + + return readLen + readLenOfNext; + } + + // if the rest data of current input stream is enough(which means readLen == len), just return readLen + return readLen; + } + + /** * Whether this class is able to read the given entry. * *

May return false if the current entry is a sparse file.

@@ -694,7 +984,7 @@ private void consumeRemainderOfLastBlock() throws IOException { final long bytesReadOfLastBlock = getBytesRead() % blockSize; if (bytesReadOfLastBlock > 0) { - final long skipped = IOUtils.skip(is, blockSize - bytesReadOfLastBlock); + final long skipped = IOUtils.skip(inputStream, blockSize - bytesReadOfLastBlock); count(skipped); } } @@ -742,4 +1032,88 @@ signature, TarConstants.VERSION_OFFSET, TarConstants.VERSIONLEN); } + /** + * Build the input streams consisting of all-zero input streams and non-zero input streams. + * When reading from the non-zero input streams, the data is actually read from the original input stream. + * The size of each input stream is introduced by the sparse headers. + * + * NOTE : Some all-zero input streams and non-zero input streams have the size of 0. We DO NOT store the + * 0 size input streams because they are meaningless. + */ + private void buildSparseInputStreams() throws IOException { + currentSparseInputStreamIndex = -1; + sparseInputStreams = new ArrayList<>(); + + final List sparseHeaders = currEntry.getSparseHeaders(); + // sort the sparse headers in case they are written in wrong order + if (sparseHeaders != null && sparseHeaders.size() > 1) { + final Comparator sparseHeaderComparator = new Comparator() { + @Override + public int compare(final TarArchiveStructSparse p, final TarArchiveStructSparse q) { + Long pOffset = p.getOffset(); + Long qOffset = q.getOffset(); + return pOffset.compareTo(qOffset); + } + }; + Collections.sort(sparseHeaders, sparseHeaderComparator); + } + + if (sparseHeaders != null) { + // Stream doesn't need to be closed at all as it doesn't use any resources + final InputStream zeroInputStream = new TarArchiveSparseZeroInputStream(); //NOSONAR + long offset = 0; + for (TarArchiveStructSparse sparseHeader : sparseHeaders) { + if (sparseHeader.getOffset() == 0 && sparseHeader.getNumbytes() == 0) { + break; + } + + if ((sparseHeader.getOffset() - offset) < 0) { + throw new IOException("Corrupted struct sparse detected"); + } + + // only store the input streams with non-zero size + if ((sparseHeader.getOffset() - offset) > 0) { + sparseInputStreams.add(new BoundedInputStream(zeroInputStream, sparseHeader.getOffset() - offset)); + } + + // only store the input streams with non-zero size + if (sparseHeader.getNumbytes() > 0) { + sparseInputStreams.add(new BoundedInputStream(inputStream, sparseHeader.getNumbytes())); + } + + offset = sparseHeader.getOffset() + sparseHeader.getNumbytes(); + } + } + + if (sparseInputStreams.size() > 0) { + currentSparseInputStreamIndex = 0; + } + } + + /** + * This is an inputstream that always return 0, + * this is used when reading the "holes" of a sparse file + */ + private static class TarArchiveSparseZeroInputStream extends InputStream { + /** + * Just return 0 + * @return + * @throws IOException + */ + @Override + public int read() throws IOException { + return 0; + } + + /** + * these's nothing need to do when skipping + * + * @param n bytes to skip + * @return bytes actually skipped + */ + @Override + public long skip(final long n) { + return n; + } + } } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveSparseEntry.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveSparseEntry.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveSparseEntry.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveSparseEntry.java 2020-01-25 16:35:14.000000000 +0000 @@ -19,6 +19,8 @@ package org.apache.commons.compress.archivers.tar; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** * This class represents a sparse entry in a Tar archive. @@ -44,6 +46,8 @@ /** If an extension sparse header follows. */ private final boolean isExtended; + private List sparseHeaders; + /** * Construct an entry from an archive's header bytes. File is set * to null. @@ -53,6 +57,17 @@ */ public TarArchiveSparseEntry(final byte[] headerBuf) throws IOException { int offset = 0; + sparseHeaders = new ArrayList<>(); + for(int i = 0; i < SPARSE_HEADERS_IN_EXTENSION_HEADER;i++) { + TarArchiveStructSparse sparseHeader = TarUtils.parseSparse(headerBuf, + offset + i * (SPARSE_OFFSET_LEN + SPARSE_NUMBYTES_LEN)); + + // some sparse headers are empty, we need to skip these sparse headers + if(sparseHeader.getOffset() > 0 || sparseHeader.getNumbytes() > 0) { + sparseHeaders.add(sparseHeader); + } + } + offset += SPARSELEN_GNU_SPARSE; isExtended = TarUtils.parseBoolean(headerBuf, offset); } @@ -60,4 +75,13 @@ public boolean isExtended() { return isExtended; } + + /** + * Obtains information about the configuration for the sparse entry. + * @since 1.20 + * @return information about the configuration for the sparse entry. + */ + public List getSparseHeaders() { + return sparseHeaders; + } } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveStructSparse.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveStructSparse.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveStructSparse.java 1970-01-01 00:00:00.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveStructSparse.java 2020-01-23 17:22:18.000000000 +0000 @@ -0,0 +1,77 @@ +/* + * 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 + * + * http://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.tar; + +import java.util.Objects; + +/** + * This class represents struct sparse in a Tar archive. + *

+ * Whereas, "struct sparse" is: + *

+ * struct sparse {
+ * char offset[12];   // offset 0
+ * char numbytes[12]; // offset 12
+ * };
+ * 
+ * @since 1.20 + */ +public final class TarArchiveStructSparse { + private final long offset; + private final long numbytes; + + public TarArchiveStructSparse(long offset, long numbytes) { + this.offset = offset; + this.numbytes = numbytes; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TarArchiveStructSparse that = (TarArchiveStructSparse) o; + return offset == that.offset && + numbytes == that.numbytes; + } + + @Override + public int hashCode() { + return Objects.hash(offset, numbytes); + } + + @Override + public String toString() { + return "TarArchiveStructSparse{" + + "offset=" + offset + + ", numbytes=" + numbytes + + '}'; + } + + public long getOffset() { + return offset; + } + + public long getNumbytes() { + return numbytes; + } +} diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/tar/TarConstants.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/tar/TarConstants.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/tar/TarConstants.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/tar/TarConstants.java 2020-01-07 14:40:25.000000000 +0000 @@ -186,6 +186,30 @@ int REALSIZELEN_GNU = 12; /** + * The length of offset in struct sparse + * @since 1.20 + */ + int SPARSE_OFFSET_LEN = 12; + + /** + * The length of numbytes in struct sparse + * @since 1.20 + */ + int SPARSE_NUMBYTES_LEN = 12; + + /** + * The number of sparse headers in an old GNU header + * @since 1.20 + */ + int SPARSE_HEADERS_IN_OLDGNU_HEADER = 4; + + /** + * The number of sparse headers in an extension header + * @since 1.20 + */ + int SPARSE_HEADERS_IN_EXTENSION_HEADER = 21; + + /** * The sum of the length of all sparse headers in a sparse header buffer. * */ diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java 2020-01-25 15:10:59.000000000 +0000 @@ -18,15 +18,17 @@ */ package org.apache.commons.compress.archivers.tar; -import static org.apache.commons.compress.archivers.tar.TarConstants.CHKSUMLEN; -import static org.apache.commons.compress.archivers.tar.TarConstants.CHKSUM_OFFSET; - import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import org.apache.commons.compress.archivers.zip.ZipEncoding; import org.apache.commons.compress.archivers.zip.ZipEncodingHelper; +import static org.apache.commons.compress.archivers.tar.TarConstants.CHKSUMLEN; +import static org.apache.commons.compress.archivers.tar.TarConstants.CHKSUM_OFFSET; +import static org.apache.commons.compress.archivers.tar.TarConstants.SPARSE_NUMBYTES_LEN; +import static org.apache.commons.compress.archivers.tar.TarConstants.SPARSE_OFFSET_LEN; + /** * This class provides static utility methods to work with byte streams. * @@ -302,6 +304,20 @@ } /** + * Parses the content of a PAX 1.0 sparse block. + * @since 1.20 + * @param buffer The buffer from which to parse. + * @param offset The offset into the buffer from which to parse. + * @return a parsed sparse struct + */ + public static TarArchiveStructSparse parseSparse(final byte[] buffer, final int offset) { + long sparseOffset = parseOctalOrBinary(buffer, offset, SPARSE_OFFSET_LEN); + long sparseNumbytes = parseOctalOrBinary(buffer, offset + SPARSE_OFFSET_LEN, SPARSE_NUMBYTES_LEN); + + return new TarArchiveStructSparse(sparseOffset, sparseNumbytes); + } + + /** * Copy a name into a buffer. * Copies characters from the name into the buffer * starting at the specified offset. diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/BinaryTree.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/BinaryTree.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/BinaryTree.java 2019-08-12 09:44:17.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/BinaryTree.java 2020-01-07 14:40:25.000000000 +0000 @@ -113,19 +113,19 @@ /** * Decodes the packed binary tree from the specified stream. */ - static BinaryTree decode(final InputStream in, final int totalNumberOfValues) throws IOException { + static BinaryTree decode(final InputStream inputStream, final int totalNumberOfValues) throws IOException { if (totalNumberOfValues < 0) { throw new IllegalArgumentException("totalNumberOfValues must be bigger than 0, is " + totalNumberOfValues); } // the first byte contains the size of the structure minus one - final int size = in.read() + 1; + final int size = inputStream.read() + 1; if (size == 0) { throw new IOException("Cannot read the size of the encoded tree, unexpected end of stream"); } final byte[] encodedTree = new byte[size]; - final int read = IOUtils.readFully(in, encodedTree); + final int read = IOUtils.readFully(inputStream, encodedTree); if (read != size) { throw new EOFException(); } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ExplodingInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ExplodingInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ExplodingInputStream.java 2019-08-12 09:44:17.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ExplodingInputStream.java 2020-01-26 12:39:13.000000000 +0000 @@ -19,12 +19,13 @@ package org.apache.commons.compress.archivers.zip; -import org.apache.commons.compress.utils.CountingInputStream; -import org.apache.commons.compress.utils.InputStreamStatistics; - import java.io.IOException; import java.io.InputStream; +import org.apache.commons.compress.utils.CloseShieldFilterInputStream; +import org.apache.commons.compress.utils.CountingInputStream; +import org.apache.commons.compress.utils.InputStreamStatistics; + /** * The implode compression method was added to PKZIP 1.01 released in 1989. * It was then dropped from PKZIP 2.0 released in 1993 in favor of the deflate @@ -97,12 +98,8 @@ */ private void init() throws IOException { if (bits == null) { - try (CountingInputStream i = new CountingInputStream(in) { - @Override - public void close() { - // we do not want to close in - } - }) { + // we do not want to close in + try (CountingInputStream i = new CountingInputStream(new CloseShieldFilterInputStream(in))) { if (numberOfTrees == 3) { literalTree = BinaryTree.decode(i, 256); } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/Zip64RequiredException.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/Zip64RequiredException.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/Zip64RequiredException.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/Zip64RequiredException.java 2020-01-21 17:29:50.000000000 +0000 @@ -37,6 +37,18 @@ return ze.getName() + "'s size exceeds the limit of 4GByte."; } + static final String NUMBER_OF_THIS_DISK_TOO_BIG_MESSAGE = + "Number of the disk of End Of Central Directory exceeds the limmit of 65535."; + + static final String NUMBER_OF_THE_DISK_OF_CENTRAL_DIRECTORY_TOO_BIG_MESSAGE = + "Number of the disk with the start of Central Directory exceeds the limmit of 65535."; + + static final String TOO_MANY_ENTRIES_ON_THIS_DISK_MESSAGE = + "Number of entries on this disk exceeds the limmit of 65535."; + + static final String SIZE_OF_CENTRAL_DIRECTORY_TOO_BIG_MESSAGE = + "The size of the entire central directory exceeds the limit of 4GByte."; + static final String ARCHIVE_TOO_BIG_MESSAGE = "Archive's size exceeds the limit of 4GByte."; diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveEntry.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveEntry.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveEntry.java 2019-08-17 10:24:04.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveEntry.java 2020-01-07 14:40:25.000000000 +0000 @@ -27,8 +27,6 @@ import java.util.List; import java.util.zip.ZipException; -import static java.util.Arrays.copyOf; - /** * Extension that adds better handling of extra fields and provides * access to the internal and external file attributes. @@ -146,7 +144,7 @@ private boolean isStreamContiguous = false; private NameSource nameSource = NameSource.NAME; private CommentSource commentSource = CommentSource.COMMENT; - + private long diskNumberStart; /** * Creates a new zip entry with the specified name. @@ -812,7 +810,7 @@ */ public byte[] getRawName() { if (rawName != null) { - return copyOf(rawName, rawName.length); + return Arrays.copyOf(rawName, rawName.length); } return null; } @@ -1082,6 +1080,31 @@ this.commentSource = commentSource; } + /** + * The number of the split segment this entry starts at. + * + * @return the number of the split segment this entry starts at. + * @since 1.20 + */ + public long getDiskNumberStart() { + return diskNumberStart; + } + + /** + * The number of the split segment this entry starts at. + * + * @param diskNumberStart the number of the split segment this entry starts at. + * @since 1.20 + */ + public void setDiskNumberStart(long diskNumberStart) { + this.diskNumberStart = diskNumberStart; + } + + private ZipExtraField[] copyOf(final ZipExtraField[] src, final int length) { + final ZipExtraField[] cpy = new ZipExtraField[length]; + System.arraycopy(src, 0, cpy, 0, Math.min(src.length, length)); + return cpy; + } /** * How to try to parse the extra fields. diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java 2019-08-23 14:55:56.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -120,6 +120,9 @@ /** Count decompressed bytes for current entry */ private long uncompressedCount = 0; + /** Whether the stream will try to skip the zip split signature(08074B50) at the beginning **/ + private final boolean skipSplitSig; + private static final int LFH_LEN = 30; /* local file header signature WORD @@ -213,12 +216,35 @@ final String encoding, final boolean useUnicodeExtraFields, final boolean allowStoredEntriesWithDataDescriptor) { + this(inputStream, encoding, useUnicodeExtraFields, allowStoredEntriesWithDataDescriptor, false); + } + + /** + * Create an instance using the specified encoding + * @param inputStream the stream to wrap + * @param encoding the encoding to use for file names, use null + * for the platform's default encoding + * @param useUnicodeExtraFields whether to use InfoZIP Unicode + * Extra Fields (if present) to set the file names. + * @param allowStoredEntriesWithDataDescriptor whether the stream + * will try to read STORED entries that use a data descriptor + * @param skipSplitSig Whether the stream will try to skip the zip + * split signature(08074B50) at the beginning. You will need to + * set this to true if you want to read a split archive. + * @since 1.20 + */ + public ZipArchiveInputStream(final InputStream inputStream, + final String encoding, + final boolean useUnicodeExtraFields, + final boolean allowStoredEntriesWithDataDescriptor, + final boolean skipSplitSig) { this.encoding = encoding; zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); this.useUnicodeExtraFields = useUnicodeExtraFields; in = new PushbackInputStream(inputStream, buf.capacity()); this.allowStoredEntriesWithDataDescriptor = allowStoredEntriesWithDataDescriptor; + this.skipSplitSig = skipSplitSig; // haven't read anything so far buf.limit(0); } @@ -367,13 +393,14 @@ private void readFirstLocalFileHeader(final byte[] lfh) throws IOException { readFully(lfh); final ZipLong sig = new ZipLong(lfh); - if (sig.equals(ZipLong.DD_SIG)) { + + if (!skipSplitSig && sig.equals(ZipLong.DD_SIG)) { throw new UnsupportedZipFeatureException(UnsupportedZipFeatureException.Feature.SPLITTING); } - if (sig.equals(ZipLong.SINGLE_SEGMENT_SPLIT_MARKER)) { - // The archive is not really split as only one segment was - // needed in the end. Just skip over the marker. + // the split zip signature(08074B50) should only be skipped when the skipSplitSig is set + if (sig.equals(ZipLong.SINGLE_SEGMENT_SPLIT_MARKER) || sig.equals(ZipLong.DD_SIG)) { + // Just skip over the marker. final byte[] missedLfhBytes = new byte[4]; readFully(missedLfhBytes); System.arraycopy(lfh, 4, lfh, 0, LFH_LEN - 4); @@ -428,6 +455,9 @@ @Override public int read(final byte[] buffer, final int offset, final int length) throws IOException { + if (length == 0) { + return 0; + } if (closed) { throw new IOException("The stream is closed"); } @@ -1256,6 +1286,9 @@ @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } if (max >= 0 && pos >= max) { return -1; } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStream.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStream.java 2020-01-23 19:18:13.000000000 +0000 @@ -61,7 +61,8 @@ * *

This class will try to use {@link * java.nio.channels.SeekableByteChannel} when it knows that the - * output is going to go to a file.

+ * output is going to go to a file and no split archive shall be + * created.

* *

If SeekableByteChannel cannot be used, this implementation will use * a Data Descriptor to store size and CRC information for {@link @@ -190,6 +191,16 @@ private long cdLength = 0; /** + * Disk number start of central directory. + */ + private long cdDiskNumberStart = 0; + + /** + * Length of end of central directory + */ + private long eocdLength = 0; + + /** * Helper, a 0 as ZipShort. */ private static final byte[] ZERO = {0, 0}; @@ -267,6 +278,17 @@ private final Calendar calendarInstance = Calendar.getInstance(); /** + * Whether we are creating a split zip + */ + private final boolean isSplitZip; + + /** + * Holds the number of Central Directories on each disk, this is used + * when writing Zip64 End Of Central Directory and End Of Central Directory + */ + private final Map numberOfCDInDiskData = new HashMap<>(); + + /** * Creates a new ZIP OutputStream filtering the underlying stream. * @param out the outputstream to zip */ @@ -275,6 +297,7 @@ this.channel = null; def = new Deflater(level, true); streamCompressor = StreamCompressor.create(out, def); + isSplitZip = false; } /** @@ -304,6 +327,36 @@ out = o; channel = _channel; streamCompressor = _streamCompressor; + isSplitZip = false; + } + + /** + * Creates a split ZIP Archive. + * + *

The files making up the archive will use Z01, Z02, + * ... extensions and the last part of it will be the given {@code + * file}.

+ * + *

Even though the stream writes to a file this stream will + * behave as if no random access was possible. This means the + * sizes of stored entries need to be known before the actual + * entry data is written.

+ * + * @param file the file that will become the last part of the split archive + * @param zipSplitSize maximum size of a single part of the split + * archive created by this stream. Must be between 64kB and about + * 4GB. + * + * @throws IOException on error + * @throws IllegalArgumentException if zipSplitSize is not in the required range + * @since 1.20 + */ + public ZipArchiveOutputStream(final File file, final long zipSplitSize) throws IOException { + def = new Deflater(level, true); + this.out = new ZipSplitOutputStream(file, zipSplitSize); + streamCompressor = StreamCompressor.create(this.out, def); + channel = null; + isSplitZip = true; } /** @@ -323,6 +376,7 @@ def = new Deflater(level, true); streamCompressor = StreamCompressor.create(channel, def); out = null; + isSplitZip = false; } /** @@ -467,15 +521,41 @@ throw new IOException("This archive contains unclosed entries."); } - cdOffset = streamCompressor.getTotalBytesWritten(); + long cdOverallOffset = streamCompressor.getTotalBytesWritten(); + cdOffset = cdOverallOffset; + if (isSplitZip) { + // when creating a split zip, the offset should be + // the offset to the corresponding segment disk + ZipSplitOutputStream zipSplitOutputStream = (ZipSplitOutputStream)this.out; + cdOffset = zipSplitOutputStream.getCurrentSplitSegmentBytesWritten(); + cdDiskNumberStart = zipSplitOutputStream.getCurrentSplitSegmentIndex(); + } writeCentralDirectoryInChunks(); - cdLength = streamCompressor.getTotalBytesWritten() - cdOffset; + cdLength = streamCompressor.getTotalBytesWritten() - cdOverallOffset; + + // calculate the length of end of central directory, as it may be used in writeZip64CentralDirectory + final ByteBuffer commentData = this.zipEncoding.encode(comment); + final long commentLength = (long) commentData.limit() - commentData.position(); + eocdLength = WORD /* length of EOCD_SIG */ + + SHORT /* number of this disk */ + + SHORT /* disk number of start of central directory */ + + SHORT /* total number of entries on this disk */ + + SHORT /* total number of entries */ + + WORD /* size of central directory */ + + WORD /* offset of start of central directory */ + + SHORT /* zip comment length */ + + commentLength /* zip comment */; + writeZip64CentralDirectory(); writeCentralDirectoryEnd(); metaData.clear(); entries.clear(); streamCompressor.close(); + if (isSplitZip) { + // trigger the ZipSplitOutputStream to write the final split segment + out.close(); + } finished = true; } @@ -1036,7 +1116,15 @@ addUnicodeExtraFields(ze, encodable, name); } - final long localHeaderStart = streamCompressor.getTotalBytesWritten(); + long localHeaderStart = streamCompressor.getTotalBytesWritten(); + if (isSplitZip) { + // when creating a split zip, the offset should be + // the offset to the corresponding segment disk + ZipSplitOutputStream splitOutputStream = (ZipSplitOutputStream)this.out; + ze.setDiskNumberStart(splitOutputStream.getCurrentSplitSegmentIndex()); + localHeaderStart = splitOutputStream.getCurrentSplitSegmentBytesWritten(); + } + final byte[] localHeader = createLocalFileHeader(ze, name, encodable, phased, localHeaderStart); metaData.put(ze, new EntryMetaData(localHeaderStart, usesDataDescriptor(ze.getMethod(), phased))); entry.localDataStart = localHeaderStart + LFH_CRC_OFFSET; // At crc offset @@ -1209,6 +1297,7 @@ || ze.getCompressedSize() >= ZIP64_MAGIC || ze.getSize() >= ZIP64_MAGIC || entryMetaData.offset >= ZIP64_MAGIC + || ze.getDiskNumberStart() >= ZIP64_MAGIC_SHORT || zip64Mode == Zip64Mode.Always; if (needsZip64Extra && zip64Mode == Zip64Mode.Never) { @@ -1235,6 +1324,18 @@ private byte[] createCentralFileHeader(final ZipArchiveEntry ze, final ByteBuffer name, final EntryMetaData entryMetaData, final boolean needsZip64Extra) throws IOException { + if(isSplitZip) { + // calculate the disk number for every central file header, + // this will be used in writing End Of Central Directory and Zip64 End Of Central Directory + int currentSplitSegment = ((ZipSplitOutputStream)this.out).getCurrentSplitSegmentIndex(); + if(numberOfCDInDiskData.get(currentSplitSegment) == null) { + numberOfCDInDiskData.put(currentSplitSegment, 1); + } else { + int originalNumberOfCD = numberOfCDInDiskData.get(currentSplitSegment); + numberOfCDInDiskData.put(currentSplitSegment, originalNumberOfCD + 1); + } + } + final byte[] extra = ze.getCentralDirectoryExtra(); // file comment length @@ -1291,7 +1392,15 @@ putShort(commentLen, buf, CFH_COMMENT_LENGTH_OFFSET); // disk number start - System.arraycopy(ZERO, 0, buf, CFH_DISK_NUMBER_OFFSET, SHORT); + if(isSplitZip) { + if (ze.getDiskNumberStart() >= ZIP64_MAGIC_SHORT || zip64Mode == Zip64Mode.Always) { + putShort(ZIP64_MAGIC_SHORT, buf, CFH_DISK_NUMBER_OFFSET); + } else { + putShort((int) ze.getDiskNumberStart(), buf, CFH_DISK_NUMBER_OFFSET); + } + } else { + System.arraycopy(ZERO, 0, buf, CFH_DISK_NUMBER_OFFSET, SHORT); + } // internal file attributes putShort(ze.getInternalAttributes(), buf, CFH_INTERNAL_ATTRIBUTES_OFFSET); @@ -1340,6 +1449,9 @@ if (lfhOffset >= ZIP64_MAGIC || zip64Mode == Zip64Mode.Always) { z64.setRelativeHeaderOffset(new ZipEightByteInteger(lfhOffset)); } + if (ze.getDiskNumberStart() >= ZIP64_MAGIC_SHORT || zip64Mode == Zip64Mode.Always) { + z64.setDiskStartNumber(new ZipLong(ze.getDiskNumberStart())); + } ze.setExtra(); } } @@ -1352,27 +1464,38 @@ * and {@link Zip64Mode #setUseZip64} is {@link Zip64Mode#Never}. */ protected void writeCentralDirectoryEnd() throws IOException { + if(!hasUsedZip64 && isSplitZip) { + ((ZipSplitOutputStream)this.out).prepareToWriteUnsplittableContent(eocdLength); + } + + validateIfZip64IsNeededInEOCD(); + writeCounted(EOCD_SIG); - // disk numbers - writeCounted(ZERO); - writeCounted(ZERO); + // number of this disk + int numberOfThisDisk = 0; + if(isSplitZip) { + numberOfThisDisk = ((ZipSplitOutputStream)this.out).getCurrentSplitSegmentIndex(); + } + writeCounted(ZipShort.getBytes(numberOfThisDisk)); + + // disk number of the start of central directory + writeCounted(ZipShort.getBytes((int)cdDiskNumberStart)); // number of entries final int numberOfEntries = entries.size(); - if (numberOfEntries > ZIP64_MAGIC_SHORT - && zip64Mode == Zip64Mode.Never) { - throw new Zip64RequiredException(Zip64RequiredException - .TOO_MANY_ENTRIES_MESSAGE); - } - if (cdOffset > ZIP64_MAGIC && zip64Mode == Zip64Mode.Never) { - throw new Zip64RequiredException(Zip64RequiredException - .ARCHIVE_TOO_BIG_MESSAGE); - } + // total number of entries in the central directory on this disk + int numOfEntriesOnThisDisk = isSplitZip + ? (numberOfCDInDiskData.get(numberOfThisDisk) == null ? 0 : numberOfCDInDiskData.get(numberOfThisDisk)) + : numberOfEntries; + final byte[] numOfEntriesOnThisDiskData = ZipShort + .getBytes(Math.min(numOfEntriesOnThisDisk, ZIP64_MAGIC_SHORT)); + writeCounted(numOfEntriesOnThisDiskData); + + // number of entries final byte[] num = ZipShort.getBytes(Math.min(numberOfEntries, - ZIP64_MAGIC_SHORT)); - writeCounted(num); + ZIP64_MAGIC_SHORT)); writeCounted(num); // length and location of CD @@ -1387,6 +1510,55 @@ } /** + * If the Zip64 mode is set to never, then all the data in End Of Central Directory + * should not exceed their limits. + * @throws Zip64RequiredException if Zip64 is actually needed + */ + private void validateIfZip64IsNeededInEOCD() throws Zip64RequiredException { + // exception will only be thrown if the Zip64 mode is never while Zip64 is actually needed + if (zip64Mode != Zip64Mode.Never) { + return; + } + + int numberOfThisDisk = 0; + if (isSplitZip) { + numberOfThisDisk = ((ZipSplitOutputStream)this.out).getCurrentSplitSegmentIndex(); + } + if (numberOfThisDisk >= ZIP64_MAGIC_SHORT) { + throw new Zip64RequiredException(Zip64RequiredException + .NUMBER_OF_THIS_DISK_TOO_BIG_MESSAGE); + } + + if (cdDiskNumberStart >= ZIP64_MAGIC_SHORT) { + throw new Zip64RequiredException(Zip64RequiredException + .NUMBER_OF_THE_DISK_OF_CENTRAL_DIRECTORY_TOO_BIG_MESSAGE); + } + + final int numOfEntriesOnThisDisk = numberOfCDInDiskData.get(numberOfThisDisk) == null + ? 0 : numberOfCDInDiskData.get(numberOfThisDisk); + if (numOfEntriesOnThisDisk >= ZIP64_MAGIC_SHORT) { + throw new Zip64RequiredException(Zip64RequiredException + .TOO_MANY_ENTRIES_ON_THIS_DISK_MESSAGE); + } + + // number of entries + if (entries.size() >= ZIP64_MAGIC_SHORT) { + throw new Zip64RequiredException(Zip64RequiredException + .TOO_MANY_ENTRIES_MESSAGE); + } + + if (cdLength >= ZIP64_MAGIC) { + throw new Zip64RequiredException(Zip64RequiredException + .SIZE_OF_CENTRAL_DIRECTORY_TOO_BIG_MESSAGE); + } + + if (cdOffset >= ZIP64_MAGIC) { + throw new Zip64RequiredException(Zip64RequiredException + .ARCHIVE_TOO_BIG_MESSAGE); + } + } + + /** * Writes the "ZIP64 End of central dir record" and * "ZIP64 End of central dir locator". * @throws IOException on error @@ -1397,9 +1569,7 @@ return; } - if (!hasUsedZip64 - && (cdOffset >= ZIP64_MAGIC || cdLength >= ZIP64_MAGIC - || entries.size() >= ZIP64_MAGIC_SHORT)) { + if (!hasUsedZip64 && shouldUseZip64EOCD()) { // actually "will use" hasUsedZip64 = true; } @@ -1408,11 +1578,20 @@ return; } - final long offset = streamCompressor.getTotalBytesWritten(); + long offset = streamCompressor.getTotalBytesWritten(); + long diskNumberStart = 0L; + if(isSplitZip) { + // when creating a split zip, the offset of should be + // the offset to the corresponding segment disk + ZipSplitOutputStream zipSplitOutputStream = (ZipSplitOutputStream)this.out; + offset = zipSplitOutputStream.getCurrentSplitSegmentBytesWritten(); + diskNumberStart = zipSplitOutputStream.getCurrentSplitSegmentIndex(); + } + writeOut(ZIP64_EOCD_SIG); - // size, we don't have any variable length as we don't support - // the extensible data sector, yet + // size of zip64 end of central directory, we don't have any variable length + // as we don't support the extensible data sector, yet writeOut(ZipEightByteInteger .getBytes(SHORT /* version made by */ + SHORT /* version needed to extract */ @@ -1428,14 +1607,26 @@ writeOut(ZipShort.getBytes(ZIP64_MIN_VERSION)); writeOut(ZipShort.getBytes(ZIP64_MIN_VERSION)); - // disk numbers - four bytes this time - writeOut(LZERO); - writeOut(LZERO); + // number of this disk + int numberOfThisDisk = 0; + if (isSplitZip) { + numberOfThisDisk = ((ZipSplitOutputStream)this.out).getCurrentSplitSegmentIndex(); + } + writeOut(ZipLong.getBytes(numberOfThisDisk)); + + // disk number of the start of central directory + writeOut(ZipLong.getBytes(cdDiskNumberStart)); + + // total number of entries in the central directory on this disk + int numOfEntriesOnThisDisk = isSplitZip + ? (numberOfCDInDiskData.get(numberOfThisDisk) == null ? 0 : numberOfCDInDiskData.get(numberOfThisDisk)) + : entries.size(); + final byte[] numOfEntriesOnThisDiskData = ZipEightByteInteger.getBytes(numOfEntriesOnThisDisk); + writeOut(numOfEntriesOnThisDiskData); // number of entries final byte[] num = ZipEightByteInteger.getBytes(entries.size()); writeOut(num); - writeOut(num); // length and location of CD writeOut(ZipEightByteInteger.getBytes(cdLength)); @@ -1443,15 +1634,56 @@ // no "zip64 extensible data sector" for now + if(isSplitZip) { + // based on the zip specification, the End Of Central Directory record and + // the Zip64 End Of Central Directory locator record must be on the same segment + final int zip64EOCDLOCLength = WORD /* length of ZIP64_EOCD_LOC_SIG */ + + WORD /* disk number of ZIP64_EOCD_SIG */ + + DWORD /* offset of ZIP64_EOCD_SIG */ + + WORD /* total number of disks */; + + final long unsplittableContentSize = zip64EOCDLOCLength + eocdLength; + ((ZipSplitOutputStream)this.out).prepareToWriteUnsplittableContent(unsplittableContentSize); + } + // and now the "ZIP64 end of central directory locator" writeOut(ZIP64_EOCD_LOC_SIG); // disk number holding the ZIP64 EOCD record - writeOut(LZERO); + writeOut(ZipLong.getBytes(diskNumberStart)); // relative offset of ZIP64 EOCD record writeOut(ZipEightByteInteger.getBytes(offset)); // total number of disks - writeOut(ONE); + if(isSplitZip) { + // the Zip64 End Of Central Directory Locator and the End Of Central Directory must be + // in the same split disk, it means they must be located in the last disk + final int totalNumberOfDisks = ((ZipSplitOutputStream)this.out).getCurrentSplitSegmentIndex() + 1; + writeOut(ZipLong.getBytes(totalNumberOfDisks)); + } else { + writeOut(ONE); + } + } + + /** + * 4.4.1.4 If one of the fields in the end of central directory + * record is too small to hold required data, the field SHOULD be + * set to -1 (0xFFFF or 0xFFFFFFFF) and the ZIP64 format record + * SHOULD be created. + * @return true if zip64 End Of Central Directory is needed + */ + private boolean shouldUseZip64EOCD() { + int numberOfThisDisk = 0; + if(isSplitZip) { + numberOfThisDisk = ((ZipSplitOutputStream)this.out).getCurrentSplitSegmentIndex(); + } + int numOfEntriesOnThisDisk = numberOfCDInDiskData.get(numberOfThisDisk) == null ? 0 : numberOfCDInDiskData.get(numberOfThisDisk); + return numberOfThisDisk >= ZIP64_MAGIC_SHORT /* number of this disk */ + || cdDiskNumberStart >= ZIP64_MAGIC_SHORT /* number of the disk with the start of the central directory */ + || numOfEntriesOnThisDisk >= ZIP64_MAGIC_SHORT /* total number of entries in the central directory on this disk */ + || entries.size() >= ZIP64_MAGIC_SHORT /* total number of entries in the central directory */ + || cdLength >= ZIP64_MAGIC /* size of the central directory */ + || cdOffset >= ZIP64_MAGIC; /* offset of start of central directory with respect to + the starting disk number */ } /** diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java 2019-08-13 16:41:40.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java 2020-01-07 14:40:25.000000000 +0000 @@ -143,6 +143,11 @@ */ private volatile boolean closed = true; + /** + * Whether the zip archive is a splite zip archive + */ + private final boolean isSplitZipArchive; + // cached buffers - must only be used locally in the class (COMPRESS-172 - reduce garbage collection) private final byte[] dwordBuf = new byte[DWORD]; private final byte[] wordBuf = new byte[WORD]; @@ -151,6 +156,7 @@ private final ByteBuffer dwordBbuf = ByteBuffer.wrap(dwordBuf); private final ByteBuffer wordBbuf = ByteBuffer.wrap(wordBuf); private final ByteBuffer cfhBbuf = ByteBuffer.wrap(cfhBuf); + private final ByteBuffer shortBbuf = ByteBuffer.wrap(shortBuf); /** * Opens the given file for reading, assuming "UTF8" for file names. @@ -352,6 +358,8 @@ final String encoding, final boolean useUnicodeExtraFields, final boolean closeOnError, final boolean ignoreLocalFileHeader) throws IOException { + isSplitZipArchive = (channel instanceof ZipSplitReadOnlySeekableByteChannel); + this.archiveName = archiveName; this.encoding = encoding; this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); @@ -774,7 +782,7 @@ final int commentLen = ZipShort.getValue(cfhBuf, off); off += SHORT; - final int diskStart = ZipShort.getValue(cfhBuf, off); + ze.setDiskNumberStart(ZipShort.getValue(cfhBuf, off)); off += SHORT; ze.setInternalAttributes(ZipShort.getValue(cfhBuf, off)); @@ -796,7 +804,7 @@ IOUtils.readFully(archive, ByteBuffer.wrap(cdExtraData)); ze.setCentralDirectoryExtra(cdExtraData); - setSizesAndOffsetFromZip64Extra(ze, diskStart); + setSizesAndOffsetFromZip64Extra(ze); final byte[] comment = new byte[commentLen]; IOUtils.readFully(archive, ByteBuffer.wrap(comment)); @@ -821,8 +829,7 @@ * even if they are never used - and here a field with only one * size would be invalid.

*/ - private void setSizesAndOffsetFromZip64Extra(final ZipArchiveEntry ze, - final int diskStart) + private void setSizesAndOffsetFromZip64Extra(final ZipArchiveEntry ze) throws IOException { final Zip64ExtendedInformationExtraField z64 = (Zip64ExtendedInformationExtraField) @@ -832,10 +839,11 @@ final boolean hasCompressedSize = ze.getCompressedSize() == ZIP64_MAGIC; final boolean hasRelativeHeaderOffset = ze.getLocalHeaderOffset() == ZIP64_MAGIC; + final boolean hasDiskStart = ze.getDiskNumberStart() == ZIP64_MAGIC_SHORT; z64.reparseCentralDirectoryData(hasUncompressedSize, hasCompressedSize, hasRelativeHeaderOffset, - diskStart == ZIP64_MAGIC_SHORT); + hasDiskStart); if (hasUncompressedSize) { ze.setSize(z64.getSize().getLongValue()); @@ -852,6 +860,10 @@ if (hasRelativeHeaderOffset) { ze.setLocalHeaderOffset(z64.getRelativeHeaderOffset().getLongValue()); } + + if (hasDiskStart) { + ze.setDiskNumberStart(z64.getDiskStartNumber().getValue()); + } } } @@ -900,6 +912,29 @@ /* size of the central directory */ + WORD; /** + * Offset of the field that holds the disk number of the first + * central directory entry inside the "End of central directory + * record" relative to the start of the "End of central directory + * record". + */ + private static final int CFD_DISK_OFFSET = + /* end of central dir signature */ WORD + /* number of this disk */ + SHORT; + + /** + * Offset of the field that holds the location of the first + * central directory entry inside the "End of central directory + * record" relative to the "number of the disk with the start + * of the central directory". + */ + private static final int CFD_LOCATOR_RELATIVE_OFFSET = + /* total number of entries in */ + /* the central dir on this disk */ + SHORT + /* total number of entries in */ + /* the central dir */ + SHORT + /* size of the central directory */ + WORD; + + /** * Length of the "Zip64 end of central directory locator" - which * should be right in front of the "end of central directory * record" if one is present at all. @@ -948,6 +983,34 @@ /* size of the central directory */ + DWORD; /** + * Offset of the field that holds the disk number of the first + * central directory entry inside the "Zip64 end of central + * directory record" relative to the start of the "Zip64 end of + * central directory record". + */ + private static final int ZIP64_EOCD_CFD_DISK_OFFSET = + /* zip64 end of central dir */ + /* signature */ WORD + /* size of zip64 end of central */ + /* directory record */ + DWORD + /* version made by */ + SHORT + /* version needed to extract */ + SHORT + /* number of this disk */ + WORD; + + /** + * Offset of the field that holds the location of the first + * central directory entry inside the "Zip64 end of central + * directory record" relative to the "number of the disk + * with the start of the central directory". + */ + private static final int ZIP64_EOCD_CFD_LOCATOR_RELATIVE_OFFSET = + /* total number of entries in the */ + /* central directory on this disk */ DWORD + /* total number of entries in the */ + /* central directory */ + DWORD + /* size of the central directory */ + DWORD; + + /** * Searches for either the "Zip64 end of central directory * locator" or the "End of central dir record", parses * it and positions the stream at the first central directory @@ -988,22 +1051,52 @@ */ private void positionAtCentralDirectory64() throws IOException { - skipBytes(ZIP64_EOCDL_LOCATOR_OFFSET - - WORD /* signature has already been read */); - dwordBbuf.rewind(); - IOUtils.readFully(archive, dwordBbuf); - archive.position(ZipEightByteInteger.getLongValue(dwordBuf)); + if (isSplitZipArchive) { + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + final long diskNumberOfEOCD = ZipLong.getValue(wordBuf); + + dwordBbuf.rewind(); + IOUtils.readFully(archive, dwordBbuf); + final long relativeOffsetOfEOCD = ZipEightByteInteger.getLongValue(dwordBuf); + ((ZipSplitReadOnlySeekableByteChannel) archive) + .position(diskNumberOfEOCD, relativeOffsetOfEOCD); + } else { + skipBytes(ZIP64_EOCDL_LOCATOR_OFFSET + - WORD /* signature has already been read */); + dwordBbuf.rewind(); + IOUtils.readFully(archive, dwordBbuf); + archive.position(ZipEightByteInteger.getLongValue(dwordBuf)); + } + wordBbuf.rewind(); IOUtils.readFully(archive, wordBbuf); if (!Arrays.equals(wordBuf, ZipArchiveOutputStream.ZIP64_EOCD_SIG)) { throw new ZipException("Archive's ZIP64 end of central " + "directory locator is corrupt."); } - skipBytes(ZIP64_EOCD_CFD_LOCATOR_OFFSET - - WORD /* signature has already been read */); - dwordBbuf.rewind(); - IOUtils.readFully(archive, dwordBbuf); - archive.position(ZipEightByteInteger.getLongValue(dwordBuf)); + + if (isSplitZipArchive) { + skipBytes(ZIP64_EOCD_CFD_DISK_OFFSET + - WORD /* signature has already been read */); + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + final long diskNumberOfCFD = ZipLong.getValue(wordBuf); + + skipBytes(ZIP64_EOCD_CFD_LOCATOR_RELATIVE_OFFSET); + + dwordBbuf.rewind(); + IOUtils.readFully(archive, dwordBbuf); + final long relativeOffsetOfCFD = ZipEightByteInteger.getLongValue(dwordBuf); + ((ZipSplitReadOnlySeekableByteChannel) archive) + .position(diskNumberOfCFD, relativeOffsetOfCFD); + } else { + skipBytes(ZIP64_EOCD_CFD_LOCATOR_OFFSET + - WORD /* signature has already been read */); + dwordBbuf.rewind(); + IOUtils.readFully(archive, dwordBbuf); + archive.position(ZipEightByteInteger.getLongValue(dwordBuf)); + } } /** @@ -1015,10 +1108,25 @@ */ private void positionAtCentralDirectory32() throws IOException { - skipBytes(CFD_LOCATOR_OFFSET); - wordBbuf.rewind(); - IOUtils.readFully(archive, wordBbuf); - archive.position(ZipLong.getValue(wordBuf)); + if (isSplitZipArchive) { + skipBytes(CFD_DISK_OFFSET); + shortBbuf.rewind(); + IOUtils.readFully(archive, shortBbuf); + final int diskNumberOfCFD = ZipShort.getValue(shortBuf); + + skipBytes(CFD_LOCATOR_RELATIVE_OFFSET); + + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + final long relativeOffsetOfCFD = ZipLong.getValue(wordBuf); + ((ZipSplitReadOnlySeekableByteChannel) archive) + .position(diskNumberOfCFD, relativeOffsetOfCFD); + } else { + skipBytes(CFD_LOCATOR_OFFSET); + wordBbuf.rewind(); + IOUtils.readFully(archive, wordBbuf); + archive.position(ZipLong.getValue(wordBuf)); + } } /** @@ -1151,8 +1259,15 @@ } private int[] setDataOffset(ZipArchiveEntry ze) throws IOException { - final long offset = ze.getLocalHeaderOffset(); - archive.position(offset + LFH_OFFSET_FOR_FILENAME_LENGTH); + long offset = ze.getLocalHeaderOffset(); + if (isSplitZipArchive) { + ((ZipSplitReadOnlySeekableByteChannel) archive) + .position(ze.getDiskNumberStart(), offset + LFH_OFFSET_FOR_FILENAME_LENGTH); + // the offset should be updated to the global offset + offset = archive.position() - LFH_OFFSET_FOR_FILENAME_LENGTH; + } else { + archive.position(offset + LFH_OFFSET_FOR_FILENAME_LENGTH); + } wordBbuf.rewind(); IOUtils.readFully(archive, wordBbuf); wordBbuf.flip(); @@ -1322,6 +1437,12 @@ if (ent2 == null) { return -1; } + + // disk number is prior to relative offset + final long diskNumberStartVal = ent1.getDiskNumberStart() - ent2.getDiskNumberStart(); + if (diskNumberStartVal != 0) { + return diskNumberStartVal < 0 ? -1 : +1; + } final long val = (ent1.getLocalHeaderOffset() - ent2.getLocalHeaderOffset()); return val == 0 ? 0 : val < 0 ? -1 : +1; @@ -1350,7 +1471,9 @@ return getLocalHeaderOffset() == otherEntry.getLocalHeaderOffset() && super.getDataOffset() - == otherEntry.getDataOffset(); + == otherEntry.getDataOffset() + && super.getDiskNumberStart() + == otherEntry.getDiskNumberStart(); } return false; } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitOutputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitOutputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitOutputStream.java 1970-01-01 00:00:00.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitOutputStream.java 2020-01-23 17:55:57.000000000 +0000 @@ -0,0 +1,245 @@ +/* + * 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 + * + * http://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.zip; + +import org.apache.commons.compress.utils.FileNameUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Used internally by {@link ZipArchiveOutputStream} when creating a split archive. + * + * @since 1.20 + */ +class ZipSplitOutputStream extends OutputStream { + private OutputStream outputStream; + private File zipFile; + private final long splitSize; + private int currentSplitSegmentIndex = 0; + private long currentSplitSegmentBytesWritten = 0; + private boolean finished = false; + private final byte[] singleByte = new byte[1]; + + /** + * 8.5.1 Capacities for split archives are as follows: + *

+ * Maximum number of segments = 4,294,967,295 - 1 + * Maximum .ZIP segment size = 4,294,967,295 bytes (refer to section 8.5.6) + * Minimum segment size = 64K + * Maximum PKSFX segment size = 2,147,483,647 bytes + */ + private final static long ZIP_SEGMENT_MIN_SIZE = 64 * 1024L; + private final static long ZIP_SEGMENT_MAX_SIZE = 4294967295L; + + /** + * Create a split zip. If the zip file is smaller than the split size, + * then there will only be one split zip, and its suffix is .zip, + * otherwise the split segments should be like .z01, .z02, ... .z(N-1), .zip + * + * @param zipFile the zip file to write to + * @param splitSize the split size + */ + public ZipSplitOutputStream(final File zipFile, final long splitSize) throws IllegalArgumentException, IOException { + if (splitSize < ZIP_SEGMENT_MIN_SIZE || splitSize > ZIP_SEGMENT_MAX_SIZE) { + throw new IllegalArgumentException("zip split segment size should between 64K and 4,294,967,295"); + } + + this.zipFile = zipFile; + this.splitSize = splitSize; + + this.outputStream = new FileOutputStream(zipFile); + // write the zip split signature 0x08074B50 to the zip file + writeZipSplitSignature(); + } + + /** + * Some data can not be written to different split segments, for example: + *

+ * 4.4.1.5 The end of central directory record and the Zip64 end + * of central directory locator record MUST reside on the same + * disk when splitting or spanning an archive. + * + * @param unsplittableContentSize + * @throws IllegalArgumentException + * @throws IOException + */ + public void prepareToWriteUnsplittableContent(long unsplittableContentSize) throws IllegalArgumentException, IOException { + if (unsplittableContentSize > this.splitSize) { + throw new IllegalArgumentException("The unsplittable content size is bigger than the split segment size"); + } + + long bytesRemainingInThisSegment = this.splitSize - this.currentSplitSegmentBytesWritten; + if (bytesRemainingInThisSegment < unsplittableContentSize) { + openNewSplitSegment(); + } + } + + @Override + public void write(int i) throws IOException { + singleByte[0] = (byte)(i & 0xff); + write(singleByte); + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + /** + * Write the data to zip split segments, if the remaining space of current split segment + * is not enough, then a new split segment should be created + * + * @param b data to write + * @param off offset of the start of data in param b + * @param len the length of data to write + * @throws IOException + */ + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (len <= 0) { + return; + } + + if (currentSplitSegmentBytesWritten >= splitSize) { + openNewSplitSegment(); + write(b, off, len); + } else if (currentSplitSegmentBytesWritten + len > splitSize) { + int bytesToWriteForThisSegment = (int) splitSize - (int) currentSplitSegmentBytesWritten; + write(b, off, bytesToWriteForThisSegment); + openNewSplitSegment(); + write(b, off + bytesToWriteForThisSegment, len - bytesToWriteForThisSegment); + } else { + outputStream.write(b, off, len); + currentSplitSegmentBytesWritten += len; + } + } + + @Override + public void close() throws IOException { + if (!finished) { + finish(); + } + } + + /** + * The last zip split segment's suffix should be .zip + * + * @throws IOException + */ + private void finish() throws IOException { + if (finished) { + throw new IOException("This archive has already been finished"); + } + + String zipFileBaseName = FileNameUtils.getBaseName(zipFile.getName()); + File lastZipSplitSegmentFile = new File(zipFile.getParentFile(), zipFileBaseName + ".zip"); + outputStream.close(); + if (!zipFile.renameTo(lastZipSplitSegmentFile)) { + throw new IOException("Failed to rename " + zipFile + " to " + lastZipSplitSegmentFile); + } + finished = true; + } + + /** + * Create a new zip split segment and prepare to write to the new segment + * + * @return + * @throws IOException + */ + private OutputStream openNewSplitSegment() throws IOException { + File newFile; + if (currentSplitSegmentIndex == 0) { + outputStream.close(); + newFile = createNewSplitSegmentFile(1); + if (!zipFile.renameTo(newFile)) { + throw new IOException("Failed to rename " + zipFile + " to " + newFile); + } + } + + newFile = createNewSplitSegmentFile(null); + + outputStream.close(); + outputStream = new FileOutputStream(newFile); + currentSplitSegmentBytesWritten = 0; + zipFile = newFile; + currentSplitSegmentIndex++; + + return outputStream; + } + + /** + * Write the zip split signature (0x08074B50) to the head of the first zip split segment + * + * @throws IOException + */ + private void writeZipSplitSignature() throws IOException { + outputStream.write(ZipArchiveOutputStream.DD_SIG); + currentSplitSegmentBytesWritten += ZipArchiveOutputStream.DD_SIG.length; + } + + /** + * Create the new zip split segment, the last zip segment should be .zip, and the zip split segments' suffix should be + * like .z01, .z02, .z03, ... .z99, .z100, ..., .z(N-1), .zip + *

+ * 8.3.3 Split ZIP files are typically written to the same location + * and are subject to name collisions if the spanned name + * format is used since each segment will reside on the same + * drive. To avoid name collisions, split archives are named + * as follows. + *

+ * Segment 1 = filename.z01 + * Segment n-1 = filename.z(n-1) + * Segment n = filename.zip + *

+ * NOTE: + * The zip split segment begin from 1,2,3,... , and we're creating a new segment, + * so the new segment suffix should be (currentSplitSegmentIndex + 2) + * + * @param zipSplitSegmentSuffixIndex + * @return + * @throws IOException + */ + private File createNewSplitSegmentFile(Integer zipSplitSegmentSuffixIndex) throws IOException { + int newZipSplitSegmentSuffixIndex = zipSplitSegmentSuffixIndex == null ? (currentSplitSegmentIndex + 2) : zipSplitSegmentSuffixIndex; + String baseName = FileNameUtils.getBaseName(zipFile.getName()); + String extension = ".z"; + if (newZipSplitSegmentSuffixIndex <= 9) { + extension += "0" + newZipSplitSegmentSuffixIndex; + } else { + extension += newZipSplitSegmentSuffixIndex; + } + + File newFile = new File(zipFile.getParent(), baseName + extension); + + if (newFile.exists()) { + throw new IOException("split zip segment " + baseName + extension + " already exists"); + } + return newFile; + } + + public int getCurrentSplitSegmentIndex() { + return currentSplitSegmentIndex; + } + + public long getCurrentSplitSegmentBytesWritten() { + return currentSplitSegmentBytesWritten; + } +} diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitReadOnlySeekableByteChannel.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitReadOnlySeekableByteChannel.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitReadOnlySeekableByteChannel.java 1970-01-01 00:00:00.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitReadOnlySeekableByteChannel.java 2020-01-25 15:10:59.000000000 +0000 @@ -0,0 +1,254 @@ +/* + * 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 + * + * http://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.zip; + +import org.apache.commons.compress.archivers.ArchiveStreamFactory; +import org.apache.commons.compress.utils.FileNameUtils; +import org.apache.commons.compress.utils.MultiReadOnlySeekableByteChannel; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * {@link MultiReadOnlySeekableByteChannel} that knows what a split ZIP archive should look like. + * + *

If you want to read a split archive using {@link ZipFile} then create an instance of this class from the parts of + * the archive.

+ * + * @since 1.20 + */ +public class ZipSplitReadOnlySeekableByteChannel extends MultiReadOnlySeekableByteChannel { + private static final int ZIP_SPLIT_SIGNATURE_LENGTH = 4; + private final ByteBuffer zipSplitSignatureByteBuffer = + ByteBuffer.allocate(ZIP_SPLIT_SIGNATURE_LENGTH); + + /** + * Concatenates the given channels. + * + *

The channels should be add in ascending order, e.g. z01, + * z02, ... z99, zip please note that the .zip file is the last + * segment and should be added as the last one in the channels

+ * + * @param channels the channels to concatenate + * @throws NullPointerException if channels is null + * @throws IOException if the first channel doesn't seem to hold + * the beginning of a split archive + */ + public ZipSplitReadOnlySeekableByteChannel(List channels) + throws IOException { + super(channels); + + // the first split zip segment should begin with zip split signature + assertSplitSignature(channels); + } + + /** + * Based on the zip specification: + * + *

+ * 8.5.3 Spanned/Split archives created using PKZIP for Windows + * (V2.50 or greater), PKZIP Command Line (V2.50 or greater), + * or PKZIP Explorer will include a special spanning + * signature as the first 4 bytes of the first segment of + * the archive. This signature (0x08074b50) will be + * followed immediately by the local header signature for + * the first file in the archive. + * + *

+ * the first 4 bytes of the first zip split segment should be the zip split signature(0x08074B50) + * + * @param channels channels to be valided + * @throws IOException + */ + private void assertSplitSignature(final List channels) + throws IOException { + SeekableByteChannel channel = channels.get(0); + // the zip split file signature is at the beginning of the first split segment + channel.position(0L); + + zipSplitSignatureByteBuffer.rewind(); + channel.read(zipSplitSignatureByteBuffer); + final ZipLong signature = new ZipLong(zipSplitSignatureByteBuffer.array()); + if (!signature.equals(ZipLong.DD_SIG)) { + channel.position(0L); + throw new IOException("The first zip split segment does not begin with split zip file signature"); + } + + channel.position(0L); + } + + /** + * Concatenates the given channels. + * + * @param channels the channels to concatenate, note that the LAST CHANNEL of channels should be the LAST SEGMENT(.zip) + * and theses channels should be added in correct order (e.g. .z01, .z02... .z99, .zip) + * @return SeekableByteChannel that concatenates all provided channels + * @throws NullPointerException if channels is null + * @throws IOException if reading channels fails + */ + public static SeekableByteChannel forOrderedSeekableByteChannels(SeekableByteChannel... channels) throws IOException { + if (Objects.requireNonNull(channels, "channels must not be null").length == 1) { + return channels[0]; + } + return new ZipSplitReadOnlySeekableByteChannel(Arrays.asList(channels)); + } + + /** + * Concatenates the given channels. + * + * @param lastSegmentChannel channel of the last segment of split zip segments, its extension should be .zip + * @param channels the channels to concatenate except for the last segment, + * note theses channels should be added in correct order (e.g. .z01, .z02... .z99) + * @return SeekableByteChannel that concatenates all provided channels + * @throws NullPointerException if lastSegmentChannel or channels is null + * @throws IOException if the first channel doesn't seem to hold + * the beginning of a split archive + */ + public static SeekableByteChannel forOrderedSeekableByteChannels(SeekableByteChannel lastSegmentChannel, + Iterable channels) throws IOException { + Objects.requireNonNull(channels, "channels"); + Objects.requireNonNull(lastSegmentChannel, "lastSegmentChannel"); + + List channelsList = new ArrayList<>(); + for (SeekableByteChannel channel : channels) { + channelsList.add(channel); + } + channelsList.add(lastSegmentChannel); + + SeekableByteChannel[] channelArray = new SeekableByteChannel[channelsList.size()]; + return forOrderedSeekableByteChannels(channelsList.toArray(channelArray)); + } + + /** + * Concatenates zip split files from the last segment(the extension SHOULD be .zip) + * + * @param lastSegmentFile the last segment of zip split files, note that the extension SHOULD be .zip + * @return SeekableByteChannel that concatenates all zip split files + * @throws IllegalArgumentException if the lastSegmentFile's extension is NOT .zip + * @throws IOException if the first channel doesn't seem to hold + * the beginning of a split archive + */ + public static SeekableByteChannel buildFromLastSplitSegment(File lastSegmentFile) throws IOException { + String extension = FileNameUtils.getExtension(lastSegmentFile.getCanonicalPath()); + if (!extension.equalsIgnoreCase(ArchiveStreamFactory.ZIP)) { + throw new IllegalArgumentException("The extension of last zip split segment should be .zip"); + } + + File parent = lastSegmentFile.getParentFile(); + String fileBaseName = FileNameUtils.getBaseName(lastSegmentFile.getCanonicalPath()); + ArrayList splitZipSegments = new ArrayList<>(); + + // zip split segments should be like z01,z02....z(n-1) based on the zip specification + Pattern pattern = Pattern.compile(Pattern.quote(fileBaseName) + ".[zZ][0-9]+"); + final File[] children = parent.listFiles(); + if (children != null) { + for (File file : children) { + if (!pattern.matcher(file.getName()).matches()) { + continue; + } + + splitZipSegments.add(file); + } + } + + Collections.sort(splitZipSegments, new ZipSplitSegmentComparator()); + return forFiles(lastSegmentFile, splitZipSegments); + } + + /** + * Concatenates the given files. + * + * @param files the files to concatenate, note that the LAST FILE of files should be the LAST SEGMENT(.zip) + * and theses files should be added in correct order (e.g. .z01, .z02... .z99, .zip) + * @return SeekableByteChannel that concatenates all provided files + * @throws NullPointerException if files is null + * @throws IOException if opening a channel for one of the files fails + * @throws IOException if the first channel doesn't seem to hold + * the beginning of a split archive + */ + public static SeekableByteChannel forFiles(File... files) throws IOException { + List channels = new ArrayList<>(); + for (File f : Objects.requireNonNull(files, "files must not be null")) { + channels.add(Files.newByteChannel(f.toPath(), StandardOpenOption.READ)); + } + if (channels.size() == 1) { + return channels.get(0); + } + return new ZipSplitReadOnlySeekableByteChannel(channels); + } + + /** + * Concatenates the given files. + * + * @param lastSegmentFile the last segment of split zip segments, its extension should be .zip + * @param files the files to concatenate except for the last segment, + * note theses files should be added in correct order (e.g. .z01, .z02... .z99) + * @return SeekableByteChannel that concatenates all provided files + * @throws IOException if the first channel doesn't seem to hold + * the beginning of a split archive + * @throws NullPointerException if files or lastSegmentFile is null + */ + public static SeekableByteChannel forFiles(File lastSegmentFile, Iterable files) throws IOException { + Objects.requireNonNull(files, "files"); + Objects.requireNonNull(lastSegmentFile, "lastSegmentFile"); + + List filesList = new ArrayList<>(); + for (File f : files) { + filesList.add(f); + } + filesList.add(lastSegmentFile); + + File[] filesArray = new File[filesList.size()]; + return forFiles(filesList.toArray(filesArray)); + } + + private static class ZipSplitSegmentComparator implements Comparator, Serializable { + private static final long serialVersionUID = 20200123L; + @Override + public int compare(File file1, File file2) { + String extension1 = FileNameUtils.getExtension(file1.getPath()); + String extension2 = FileNameUtils.getExtension(file2.getPath()); + + if (!extension1.startsWith("z")) { + return -1; + } + + if (!extension2.startsWith("z")) { + return 1; + } + + Integer splitSegmentNumber1 = Integer.parseInt(extension1.substring(1)); + Integer splitSegmentNumber2 = Integer.parseInt(extension2.substring(1)); + + return splitSegmentNumber1.compareTo(splitSegmentNumber2); + } + } +} diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/changes/Change.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/changes/Change.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/changes/Change.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/changes/Change.java 2020-01-07 14:40:25.000000000 +0000 @@ -19,6 +19,7 @@ package org.apache.commons.compress.changes; import java.io.InputStream; +import java.util.Objects; import org.apache.commons.compress.archivers.ArchiveEntry; @@ -47,9 +48,7 @@ * @param fileName the file name of the file to delete */ Change(final String fileName, final int type) { - if(fileName == null) { - throw new NullPointerException(); - } + Objects.requireNonNull(fileName, "fileName"); this.targetFile = fileName; this.type = type; this.input = null; @@ -60,15 +59,14 @@ /** * Construct a change which adds an entry. * - * @param pEntry the entry details - * @param pInput the InputStream for the entry data + * @param archiveEntry the entry details + * @param inputStream the InputStream for the entry data */ - Change(final ArchiveEntry pEntry, final InputStream pInput, final boolean replace) { - if(pEntry == null || pInput == null) { - throw new NullPointerException(); - } - this.entry = pEntry; - this.input = pInput; + Change(final ArchiveEntry archiveEntry, final InputStream inputStream, final boolean replace) { + Objects.requireNonNull(archiveEntry, "archiveEntry"); + Objects.requireNonNull(inputStream, "inputStream"); + this.entry = archiveEntry; + this.input = inputStream; type = TYPE_ADD; targetFile = null; this.replaceMode = replace; diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/brotli/BrotliCompressorInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/brotli/BrotliCompressorInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/brotli/BrotliCompressorInputStream.java 2018-05-23 12:50:54.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/brotli/BrotliCompressorInputStream.java 2020-01-23 19:54:20.000000000 +0000 @@ -63,7 +63,7 @@ } @Override - public void mark(final int readlimit) { + public synchronized void mark(final int readlimit) { decIS.mark(readlimit); } @@ -92,7 +92,7 @@ } @Override - public void reset() throws IOException { + public synchronized void reset() throws IOException { decIS.reset(); } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStream.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStream.java 2020-01-21 12:21:21.000000000 +0000 @@ -494,18 +494,27 @@ final int alphaSize = this.nInUse + 2; /* Now the selectors */ final int nGroups = bsR(bin, 3); - final int nSelectors = bsR(bin, 15); + final int selectors = bsR(bin, 15); + if (selectors < 0) { + throw new IOException("Corrupted input, nSelectors value negative"); + } checkBounds(alphaSize, MAX_ALPHA_SIZE + 1, "alphaSize"); checkBounds(nGroups, N_GROUPS + 1, "nGroups"); - checkBounds(nSelectors, MAX_SELECTORS + 1, "nSelectors"); - for (int i = 0; i < nSelectors; i++) { + // Don't fail on nSelectors overflowing boundaries but discard the values in overflow + // See https://gnu.wildebeest.org/blog/mjw/2019/08/02/bzip2-and-the-cve-that-wasnt/ + // and https://sourceware.org/ml/bzip2-devel/2019-q3/msg00007.html + + for (int i = 0; i < selectors; i++) { int j = 0; while (bsGetBit(bin)) { j++; } - selectorMtf[i] = (byte) j; + if (i < MAX_SELECTORS) { + selectorMtf[i] = (byte) j; + } } + final int nSelectors = selectors > MAX_SELECTORS ? MAX_SELECTORS : selectors; /* Undo the MTF values for the selectors. */ for (int v = nGroups; --v >= 0;) { diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java 2020-01-07 14:40:25.000000000 +0000 @@ -455,7 +455,7 @@ /** * Try to detect the type of compressor stream. * - * @param in input stream + * @param inputStream input stream * @return type of compressor stream detected * @throws CompressorException if no compressor stream type was detected * or if something else went wrong @@ -463,21 +463,21 @@ * * @since 1.14 */ - public static String detect(final InputStream in) throws CompressorException { - if (in == null) { + public static String detect(final InputStream inputStream) throws CompressorException { + if (inputStream == null) { throw new IllegalArgumentException("Stream must not be null."); } - if (!in.markSupported()) { + if (!inputStream.markSupported()) { throw new IllegalArgumentException("Mark is not supported."); } final byte[] signature = new byte[12]; - in.mark(signature.length); + inputStream.mark(signature.length); int signatureLength = -1; try { - signatureLength = IOUtils.readFully(in, signature); - in.reset(); + signatureLength = IOUtils.readFully(inputStream, signature); + inputStream.reset(); } catch (IOException e) { throw new CompressorException("IOException while reading signature.", e); } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/deflate/DeflateCompressorInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/deflate/DeflateCompressorInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/deflate/DeflateCompressorInputStream.java 2018-05-23 12:50:54.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/deflate/DeflateCompressorInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -80,6 +80,9 @@ /** {@inheritDoc} */ @Override public int read(final byte[] buf, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } final int ret = in.read(buf, off, len); count(ret); return ret; diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/deflate64/Deflate64CompressorInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/deflate64/Deflate64CompressorInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/deflate64/Deflate64CompressorInputStream.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/deflate64/Deflate64CompressorInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -72,10 +72,13 @@ } /** - * @throws java.io.EOFException if the underlying stream is exhausted before the end of defalted data was reached. + * @throws java.io.EOFException if the underlying stream is exhausted before the end of deflated data was reached. */ @Override public int read(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } int read = -1; if (decoder != null) { read = decoder.decode(b, off, len); diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/deflate64/HuffmanDecoder.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/deflate64/HuffmanDecoder.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/deflate64/HuffmanDecoder.java 2019-08-10 12:52:34.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/deflate64/HuffmanDecoder.java 2020-01-07 14:40:25.000000000 +0000 @@ -149,7 +149,10 @@ throw new IllegalStateException("Unsupported compression: " + mode); } } else { - return state.read(b, off, len); + int r = state.read(b, off, len); + if (r != 0) { + return r; + } } } return -1; @@ -214,6 +217,9 @@ @Override int read(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } // as len is an int and (blockLength - read) is >= 0 the min must fit into an int as well int max = (int) Math.min(blockLength - read, len); int readSoFar = 0; @@ -255,6 +261,9 @@ @Override int read(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } throw new IllegalStateException("Cannot read in this state"); } @@ -292,6 +301,9 @@ @Override int read(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } return decodeNext(b, off, len); } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/FileNameUtil.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/FileNameUtil.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/FileNameUtil.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/FileNameUtil.java 2020-01-07 14:40:25.000000000 +0000 @@ -192,5 +192,4 @@ // No custom suffix found, just append the default return fileName + defaultExtension; } - } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/gzip/GzipCompressorInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/gzip/GzipCompressorInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/gzip/GzipCompressorInputStream.java 2019-08-12 09:44:17.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/gzip/GzipCompressorInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -286,6 +286,9 @@ */ @Override public int read(final byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } if (endReached) { return -1; } @@ -322,9 +325,6 @@ if (inf.finished()) { // We may have read too many bytes. Rewind the read // position to match the actual amount used. - // - // NOTE: The "if" is there just in case. Since we used - // in.mark earlier, it should always skip enough. in.reset(); final int skipAmount = bufUsed - inf.getRemaining(); diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/lz4/BlockLZ4CompressorInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/lz4/BlockLZ4CompressorInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/lz4/BlockLZ4CompressorInputStream.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/lz4/BlockLZ4CompressorInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -61,6 +61,9 @@ */ @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } switch (state) { case EOF: return -1; diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStream.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -70,7 +70,7 @@ } }; - private final CountingInputStream in; + private final CountingInputStream inputStream; private final boolean decompressConcatenated; private boolean expectBlockChecksum; @@ -112,7 +112,7 @@ * @throws IOException if reading fails */ public FramedLZ4CompressorInputStream(InputStream in, boolean decompressConcatenated) throws IOException { - this.in = new CountingInputStream(in); + this.inputStream = new CountingInputStream(in); this.decompressConcatenated = decompressConcatenated; init(true); } @@ -132,13 +132,16 @@ currentBlock = null; } } finally { - in.close(); + inputStream.close(); } } /** {@inheritDoc} */ @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } if (endReached) { return -1; } @@ -165,7 +168,7 @@ */ @Override public long getCompressedCount() { - return in.getBytesRead(); + return inputStream.getBytesRead(); } private void init(boolean firstFrame) throws IOException { @@ -178,7 +181,7 @@ private boolean readSignature(boolean firstFrame) throws IOException { String garbageMessage = firstFrame ? "Not a LZ4 frame stream" : "LZ4 frame stream followed by garbage"; final byte[] b = new byte[4]; - int read = IOUtils.readFully(in, b); + int read = IOUtils.readFully(inputStream, b); count(read); if (0 == read && !firstFrame) { // good LZ4 frame and nothing after it @@ -228,7 +231,7 @@ contentHash.update(bdByte); if (expectContentSize) { // for now we don't care, contains the uncompressed size byte[] contentSize = new byte[8]; - int skipped = IOUtils.readFully(in, contentSize); + int skipped = IOUtils.readFully(inputStream, contentSize); count(skipped); if (8 != skipped) { throw new IOException("Premature end of stream while reading content size"); @@ -263,7 +266,7 @@ } return; } - InputStream capped = new BoundedInputStream(in, realLen); + InputStream capped = new BoundedInputStream(inputStream, realLen); if (expectBlockChecksum) { capped = new ChecksumCalculatingInputStream(blockHash, capped); } @@ -300,7 +303,7 @@ private void verifyChecksum(XXHash32 hash, String kind) throws IOException { byte[] checksum = new byte[4]; - int read = IOUtils.readFully(in, checksum); + int read = IOUtils.readFully(inputStream, checksum); count(read); if (4 != read) { throw new IOException("Premature end of stream while reading " + kind + " checksum"); @@ -312,7 +315,7 @@ } private int readOneByte() throws IOException { - final int b = in.read(); + final int b = inputStream.read(); if (b != -1) { count(1); return b & 0xFF; @@ -360,12 +363,12 @@ if (len < 0) { throw new IOException("Found illegal skippable frame with negative size"); } - long skipped = IOUtils.skip(in, len); + long skipped = IOUtils.skip(inputStream, len); count(skipped); if (len != skipped) { throw new IOException("Premature end of stream while skipping frame"); } - read = IOUtils.readFully(in, b); + read = IOUtils.readFully(inputStream, b); count(read); } return read; diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/lz77support/LZ77Compressor.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/lz77support/LZ77Compressor.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/lz77support/LZ77Compressor.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/lz77support/LZ77Compressor.java 2020-01-07 14:40:25.000000000 +0000 @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Objects; /** * Helper class for compression algorithms that use the ideas of LZ77. @@ -255,12 +256,9 @@ * @throws NullPointerException if either parameter is null */ public LZ77Compressor(Parameters params, Callback callback) { - if (params == null) { - throw new NullPointerException("params must not be null"); - } - if (callback == null) { - throw new NullPointerException("callback must not be null"); - } + Objects.requireNonNull(params, "params"); + Objects.requireNonNull(callback, "callback"); + this.params = params; this.callback = callback; diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/lzw/LZWInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/lzw/LZWInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/lzw/LZWInputStream.java 2019-08-12 09:44:17.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/lzw/LZWInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -72,6 +72,9 @@ @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } int bytesRead = readFromStack(b, off, len); while (len - bytesRead > 0) { final int result = decompressNextSymbol(); diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/pack200/Pack200CompressorInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/pack200/Pack200CompressorInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/pack200/Pack200CompressorInputStream.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/pack200/Pack200CompressorInputStream.java 2020-01-26 12:39:13.000000000 +0000 @@ -20,7 +20,6 @@ package org.apache.commons.compress.compressors.pack200; import java.io.File; -import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Map; @@ -28,6 +27,7 @@ import java.util.jar.Pack200; import org.apache.commons.compress.compressors.CompressorInputStream; +import org.apache.commons.compress.utils.CloseShieldFilterInputStream; import org.apache.commons.compress.utils.IOUtils; /** @@ -178,13 +178,9 @@ u.properties().putAll(props); } if (f == null) { - u.unpack(new FilterInputStream(in) { - @Override - public void close() { - // unpack would close this stream but we - // want to give the user code more control - } - }, jarOut); + // unpack would close this stream but we + // want to give the user code more control + u.unpack(new CloseShieldFilterInputStream(in), jarOut); } else { u.unpack(f, jarOut); } @@ -221,7 +217,7 @@ } @Override - public void mark(final int limit) { + public synchronized void mark(final int limit) { try { streamBridge.getInput().mark(limit); } catch (final IOException ex) { @@ -230,7 +226,7 @@ } @Override - public void reset() throws IOException { + public synchronized void reset() throws IOException { streamBridge.getInput().reset(); } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/pack200/TempFileCachingStreamBridge.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/pack200/TempFileCachingStreamBridge.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/pack200/TempFileCachingStreamBridge.java 2019-08-12 09:44:17.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/pack200/TempFileCachingStreamBridge.java 2020-01-23 17:36:50.000000000 +0000 @@ -48,7 +48,8 @@ try { super.close(); } finally { - f.delete(); + // if this fails the only thing we can do is to rely on deleteOnExit + f.delete(); // NOSONAR } } }; diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStream.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -65,7 +65,7 @@ private final CountingInputStream countingStream; /** The underlying stream to read compressed data from */ - private final PushbackInputStream in; + private final PushbackInputStream inputStream; /** The dialect to expect */ private final FramedSnappyDialect dialect; @@ -131,7 +131,7 @@ throw new IllegalArgumentException("blockSize must be bigger than 0"); } countingStream = new CountingInputStream(in); - this.in = new PushbackInputStream(countingStream, 1); + this.inputStream = new PushbackInputStream(countingStream, 1); this.blockSize = blockSize; this.dialect = dialect; if (dialect.hasStreamIdentifier()) { @@ -154,13 +154,16 @@ currentCompressedChunk = null; } } finally { - in.close(); + inputStream.close(); } } /** {@inheritDoc} */ @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } int read = readOnce(b, off, len); if (read == -1) { readNextBlock(); @@ -177,7 +180,7 @@ public int available() throws IOException { if (inUncompressedChunk) { return Math.min(uncompressedBytesRemaining, - in.available()); + inputStream.available()); } else if (currentCompressedChunk != null) { return currentCompressedChunk.available(); } @@ -206,7 +209,7 @@ if (amount == 0) { return -1; } - read = in.read(b, off, amount); + read = inputStream.read(b, off, amount); if (read != -1) { uncompressedBytesRemaining -= read; count(read); @@ -234,7 +237,7 @@ if (type == -1) { endReached = true; } else if (type == STREAM_IDENTIFIER_TYPE) { - in.unread(type); + inputStream.unread(type); unreadBytes++; pushedBackBytes(1); readStreamIdentifier(); @@ -266,7 +269,7 @@ expectedChecksum = -1; } currentCompressedChunk = - new SnappyCompressorInputStream(new BoundedInputStream(in, size), blockSize); + new SnappyCompressorInputStream(new BoundedInputStream(inputStream, size), blockSize); // constructor reads uncompressed size count(currentCompressedChunk.getBytesRead()); } else { @@ -278,7 +281,7 @@ private long readCrc() throws IOException { final byte[] b = new byte[4]; - final int read = IOUtils.readFully(in, b); + final int read = IOUtils.readFully(inputStream, b); count(read); if (read != 4) { throw new IOException("Premature end of stream"); @@ -303,7 +306,7 @@ if (size < 0) { throw new IOException("Found illegal chunk with negative size"); } - final long read = IOUtils.skip(in, size); + final long read = IOUtils.skip(inputStream, size); count(read); if (read != size) { throw new IOException("Premature end of stream"); @@ -312,7 +315,7 @@ private void readStreamIdentifier() throws IOException { final byte[] b = new byte[10]; - final int read = IOUtils.readFully(in, b); + final int read = IOUtils.readFully(inputStream, b); count(read); if (10 != read || !matches(b, 10)) { throw new IOException("Not a framed Snappy stream"); @@ -320,7 +323,7 @@ } private int readOneByte() throws IOException { - final int b = in.read(); + final int b = inputStream.read(); if (b != -1) { count(1); return b & 0xFF; diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/snappy/SnappyCompressorInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/snappy/SnappyCompressorInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/snappy/SnappyCompressorInputStream.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/snappy/SnappyCompressorInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -91,6 +91,9 @@ */ @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } if (endReached) { return -1; } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorInputStream.java 2018-05-23 12:50:54.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -149,6 +149,9 @@ @Override public int read(final byte[] buf, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } try { final int ret = in.read(buf, off, len); count(ret); diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/zstandard/ZstdCompressorInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/zstandard/ZstdCompressorInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/compressors/zstandard/ZstdCompressorInputStream.java 2019-08-12 09:44:17.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/compressors/zstandard/ZstdCompressorInputStream.java 2020-01-23 19:54:20.000000000 +0000 @@ -64,7 +64,7 @@ } @Override - public void mark(final int readlimit) { + public synchronized void mark(final int readlimit) { decIS.mark(readlimit); } @@ -82,6 +82,9 @@ @Override public int read(final byte[] buf, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } final int ret = decIS.read(buf, off, len); count(ret); return ret; @@ -93,7 +96,7 @@ } @Override - public void reset() throws IOException { + public synchronized void reset() throws IOException { decIS.reset(); } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/BoundedInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/BoundedInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/BoundedInputStream.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/BoundedInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -51,6 +51,9 @@ @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } if (bytesRemaining == 0) { return -1; } @@ -70,4 +73,16 @@ // there isn't anything to close in this stream and the nested // stream is controlled externally } + + /** + * @since 1.20 + */ + @Override + public long skip(final long n) throws IOException { + long bytesToSkip = Math.min(bytesRemaining, n); + long bytesSkipped = in.skip(bytesToSkip); + bytesRemaining -= bytesSkipped; + + return bytesSkipped; + } } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/Charsets.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/Charsets.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/Charsets.java 2018-05-23 12:50:54.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/Charsets.java 2020-01-26 12:39:13.000000000 +0000 @@ -97,6 +97,7 @@ * @see Standard charsets * @deprecated replaced by {@link StandardCharsets} in Java 7 */ + @Deprecated public static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1; /** @@ -110,6 +111,7 @@ * @see Standard charsets * @deprecated replaced by {@link StandardCharsets} in Java 7 */ + @Deprecated public static final Charset US_ASCII = StandardCharsets.US_ASCII; /** @@ -124,6 +126,7 @@ * @see Standard charsets * @deprecated replaced by {@link StandardCharsets} in Java 7 */ + @Deprecated public static final Charset UTF_16 = StandardCharsets.UTF_16; /** @@ -137,6 +140,7 @@ * @see Standard charsets * @deprecated replaced by {@link StandardCharsets} in Java 7 */ + @Deprecated public static final Charset UTF_16BE = StandardCharsets.UTF_16BE; /** @@ -150,6 +154,7 @@ * @see Standard charsets * @deprecated replaced by {@link StandardCharsets} in Java 7 */ + @Deprecated public static final Charset UTF_16LE = StandardCharsets.UTF_16LE; /** @@ -163,5 +168,6 @@ * @see Standard charsets * @deprecated replaced by {@link StandardCharsets} in Java 7 */ + @Deprecated public static final Charset UTF_8 = StandardCharsets.UTF_8; } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/ChecksumCalculatingInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/ChecksumCalculatingInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/ChecksumCalculatingInputStream.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/ChecksumCalculatingInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; +import java.util.Objects; import java.util.zip.Checksum; /** @@ -30,18 +31,13 @@ private final InputStream in; private final Checksum checksum; - public ChecksumCalculatingInputStream(final Checksum checksum, final InputStream in) { + public ChecksumCalculatingInputStream(final Checksum checksum, final InputStream inputStream) { - if ( checksum == null ){ - throw new NullPointerException("Parameter checksum must not be null"); - } - - if ( in == null ){ - throw new NullPointerException("Parameter in must not be null"); - } + Objects.requireNonNull(checksum, "checksum"); + Objects.requireNonNull(inputStream, "in"); this.checksum = checksum; - this.in = in; + this.in = inputStream; } /** @@ -78,6 +74,9 @@ */ @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } final int ret = in.read(b, off, len); if (ret >= 0) { checksum.update(b, off, ret); diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/ChecksumVerifyingInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/ChecksumVerifyingInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/ChecksumVerifyingInputStream.java 2019-08-20 19:23:37.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/ChecksumVerifyingInputStream.java 2020-01-07 14:40:25.000000000 +0000 @@ -82,6 +82,9 @@ */ @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } final int ret = in.read(b, off, len); if (ret >= 0) { checksum.update(b, off, ret); diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/CountingInputStream.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/CountingInputStream.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/CountingInputStream.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/CountingInputStream.java 2020-01-26 12:39:13.000000000 +0000 @@ -23,7 +23,7 @@ import java.io.InputStream; /** - * Stream that tracks the number of bytes read. + * Input stream that tracks the number of bytes read. * @since 1.3 * @NotThreadSafe */ @@ -42,18 +42,24 @@ } return r; } + @Override public int read(final byte[] b) throws IOException { return read(b, 0, b.length); } + @Override public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } final int r = in.read(b, off, len); if (r >= 0) { count(r); } return r; } + /** * Increments the counter of already read bytes. * Doesn't increment if the EOF has been hit (read == -1) diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/FileNameUtils.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/FileNameUtils.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/FileNameUtils.java 1970-01-01 00:00:00.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/FileNameUtils.java 2020-01-07 14:40:25.000000000 +0000 @@ -0,0 +1,78 @@ +/* + * 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 + * + * http://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.utils; + +import java.io.File; + +/** + * Generic file name utilities. + * @since 1.20 + */ +public class FileNameUtils { + + /** + * Returns the extension (i.e. the part after the last ".") of a file. + * + *

Will return an empty string if the file name doesn't contain + * any dots. Only the last segment of a the file name is consulted + * - i.e. all leading directories of the {@code filename} + * parameter are skipped.

+ * + * @return the extension of filename + * @param filename the name of the file to obtain the extension of. + */ + public static String getExtension(String filename) { + if (filename == null) { + return null; + } + + String name = new File(filename).getName(); + int extensionPosition = name.lastIndexOf('.'); + if (extensionPosition < 0) { + return ""; + } + return name.substring(extensionPosition + 1); + } + + /** + * Returns the basename (i.e. the part up to and not including the + * last ".") of the last path segment of a filename. + * + *

Will return the file name itself if it doesn't contain any + * dots. All leading directories of the {@code filename} parameter + * are skipped.

+ * + * @return the basename of filename + * @param filename the name of the file to obtain the basename of. + */ + public static String getBaseName(String filename) { + if (filename == null) { + return null; + } + + String name = new File(filename).getName(); + + int extensionPosition = name.lastIndexOf('.'); + if (extensionPosition < 0) { + return name; + } + + return name.substring(0, extensionPosition); + } +} diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/IOUtils.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/IOUtils.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/IOUtils.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/IOUtils.java 2020-01-07 14:40:25.000000000 +0000 @@ -21,6 +21,8 @@ import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -127,6 +129,25 @@ } /** + * Reads as much from the file as possible to fill the given array. + * + *

This method may invoke read repeatedly to fill the array and + * only read less bytes than the length of the array if the end of + * the stream has been reached.

+ * + * @param file file to read + * @param array buffer to fill + * @return the number of bytes actually read + * @throws IOException on error + * @since 1.20 + */ + public static int read(final File file, final byte[] array) throws IOException { + try (FileInputStream inputStream = new FileInputStream(file)) { + return readFully(inputStream, array, 0, array.length); + } + } + + /** * Reads as much from input as possible to fill the given array. * *

This method may invoke read repeatedly to fill the array and @@ -134,12 +155,12 @@ * the stream has been reached.

* * @param input stream to read from - * @param b buffer to fill + * @param array buffer to fill * @return the number of bytes actually read * @throws IOException on error */ - public static int readFully(final InputStream input, final byte[] b) throws IOException { - return readFully(input, b, 0, b.length); + public static int readFully(final InputStream input, final byte[] array) throws IOException { + return readFully(input, array, 0, array.length); } /** @@ -151,21 +172,21 @@ * the stream has been reached.

* * @param input stream to read from - * @param b buffer to fill + * @param array buffer to fill * @param offset offset into the buffer to start filling at * @param len of bytes to read * @return the number of bytes actually read * @throws IOException * if an I/O error has occurred */ - public static int readFully(final InputStream input, final byte[] b, final int offset, final int len) + public static int readFully(final InputStream input, final byte[] array, final int offset, final int len) throws IOException { - if (len < 0 || offset < 0 || len + offset > b.length) { + if (len < 0 || offset < 0 || len + offset > array.length) { throw new IndexOutOfBoundsException(); } int count = 0, x = 0; while (count != len) { - x = input.read(b, offset + count, len - count); + x = input.read(array, offset + count, len - count); if (x == -1) { break; } diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/MultiReadOnlySeekableByteChannel.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/MultiReadOnlySeekableByteChannel.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/MultiReadOnlySeekableByteChannel.java 2019-08-18 15:10:03.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/MultiReadOnlySeekableByteChannel.java 2020-01-25 15:10:59.000000000 +0000 @@ -117,13 +117,43 @@ return true; } + /** + * Returns this channel's position. + * + *

This method violates the contract of {@link SeekableByteChannel#position()} as it will not throw any exception + * when invoked on a closed channel. Instead it will return the position the channel had when close has been + * called.

+ */ @Override public long position() { return globalPosition; } + /** + * set the position based on the given channel number and relative offset + * + * @param channelNumber the channel number + * @param relativeOffset the relative offset in the corresponding channel + * @return global position of all channels as if they are a single channel + * @throws IOException if positioning fails + */ + public synchronized SeekableByteChannel position(long channelNumber, long relativeOffset) throws IOException { + if (!isOpen()) { + throw new ClosedChannelException(); + } + long globalPosition = relativeOffset; + for (int i = 0; i < channelNumber; i++) { + globalPosition += channels.get(i).size(); + } + + return position(globalPosition); + } + @Override public long size() throws IOException { + if (!isOpen()) { + throw new ClosedChannelException(); + } long acc = 0; for (SeekableByteChannel ch : channels) { acc += ch.size(); diff -Nru libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/SeekableInMemoryByteChannel.java libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/SeekableInMemoryByteChannel.java --- libcommons-compress-java-1.19/src/main/java/org/apache/commons/compress/utils/SeekableInMemoryByteChannel.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/main/java/org/apache/commons/compress/utils/SeekableInMemoryByteChannel.java 2020-01-07 14:40:25.000000000 +0000 @@ -28,9 +28,10 @@ /** * A {@link SeekableByteChannel} implementation that wraps a byte[]. * - *

When this channel is used for writing an internal buffer grows to accommodate - * incoming data. A natural size limit is the value of {@link Integer#MAX_VALUE}. - * Internal buffer can be accessed via {@link SeekableInMemoryByteChannel#array()}.

+ *

When this channel is used for writing an internal buffer grows to accommodate incoming data. The natural size + * limit is the value of {@link Integer#MAX_VALUE} and it is not possible to {@link #position(long) set the position} or + * {@link #truncate truncate} to a value bigger than that. Internal buffer can be accessed via {@link + * SeekableInMemoryByteChannel#array()}.

* * @since 1.13 * @NotThreadSafe @@ -74,6 +75,13 @@ this(new byte[size]); } + /** + * Returns this channel's position. + * + *

This method violates the contract of {@link SeekableByteChannel#position()} as it will not throw any exception + * when invoked on a closed channel. Instead it will return the position the channel had when close has been + * called.

+ */ @Override public long position() { return position; @@ -89,24 +97,40 @@ return this; } + /** + * Returns the current size of entity to which this channel is connected. + * + *

This method violates the contract of {@link SeekableByteChannel#size} as it will not throw any exception when + * invoked on a closed channel. Instead it will return the size the channel had when close has been called.

+ */ @Override public long size() { return size; } + /** + * Truncates the entity, to which this channel is connected, to the given size. + * + *

This method violates the contract of {@link SeekableByteChannel#truncate} as it will not throw any exception when + * invoked on a closed channel.

+ */ @Override public SeekableByteChannel truncate(long newSize) { + if (newSize < 0L || newSize > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Size has to be in range 0.. " + Integer.MAX_VALUE); + } if (size > newSize) { size = (int) newSize; } - repositionIfNecessary(); + if (position > newSize) { + position = (int) newSize; + } return this; } @Override public int read(ByteBuffer buf) throws IOException { ensureOpen(); - repositionIfNecessary(); int wanted = buf.remaining(); int possible = size - position; if (possible <= 0) { @@ -186,10 +210,4 @@ } } - private void repositionIfNecessary() { - if (position > size) { - position = size; - } - } - } diff -Nru libcommons-compress-java-1.19/src/site/site.xml libcommons-compress-java-1.20/src/site/site.xml --- libcommons-compress-java-1.19/src/site/site.xml 2019-08-24 11:29:45.000000000 +0000 +++ libcommons-compress-java-1.20/src/site/site.xml 2020-02-05 05:00:52.000000000 +0000 @@ -38,6 +38,7 @@ + diff -Nru libcommons-compress-java-1.19/src/site/xdoc/download_compress.xml libcommons-compress-java-1.20/src/site/xdoc/download_compress.xml --- libcommons-compress-java-1.19/src/site/xdoc/download_compress.xml 2019-08-24 16:00:55.000000000 +0000 +++ libcommons-compress-java-1.20/src/site/xdoc/download_compress.xml 2020-02-05 05:00:52.000000000 +0000 @@ -113,32 +113,32 @@

-
+
- - - + + + - - - + + +
commons-compress-1.19-bin.tar.gzsha512pgpcommons-compress-1.20-bin.tar.gzsha512pgp
commons-compress-1.19-bin.zipsha512pgpcommons-compress-1.20-bin.zipsha512pgp
- - - + + + - - - + + +
commons-compress-1.19-src.tar.gzsha512pgpcommons-compress-1.20-src.tar.gzsha512pgp
commons-compress-1.19-src.zipsha512pgpcommons-compress-1.20-src.zipsha512pgp
diff -Nru libcommons-compress-java-1.19/src/site/xdoc/examples.xml libcommons-compress-java-1.20/src/site/xdoc/examples.xml --- libcommons-compress-java-1.19/src/site/xdoc/examples.xml 2019-08-23 15:10:03.000000000 +0000 +++ libcommons-compress-java-1.20/src/site/xdoc/examples.xml 2020-01-07 14:40:25.000000000 +0000 @@ -420,6 +420,16 @@ } ]]> +

Random-Access to 7z Archives

+ +

Prior to Compress 1.20 7z archives could only be read + sequentially. The + getInputStream(SevenZArchiveEntry) method + introduced with Compress 1.20 now provides random access but + at least when the archive uses solid compression random access + will likely be significantly slower than sequential + access.

+ diff -Nru libcommons-compress-java-1.19/src/site/xdoc/index.xml libcommons-compress-java-1.20/src/site/xdoc/index.xml --- libcommons-compress-java-1.19/src/site/xdoc/index.xml 2019-08-24 11:29:45.000000000 +0000 +++ libcommons-compress-java-1.20/src/site/xdoc/index.xml 2020-02-05 05:00:52.000000000 +0000 @@ -52,23 +52,18 @@
-

The current release is 1.19 and requires Java 7.

+

The current release is 1.20 and requires Java 7.

Below we highlight some new features, for a full list of changes see the Changes Report.

- +
    -
  • ParallelScatterZipCreator now writes - entries in the same order they have been added to the - archive.
  • -
  • ZipArchiveInputStream and - ZipFile are more forgiving when parsing - extra fields by default now.
  • -
  • TarArchiveInputStream has a new lenient - mode that may allow it to read certain broken - archives.
  • +
  • SevenZFile now supports random + access.
  • +
  • The zip package now supports split archives.
  • +
  • The tar package now supports reading sparse entries.
@@ -93,14 +88,14 @@ licensed Google Brotli decoder. Zstandard support is provided by the BSD licensed Zstd-jni. - As of Commons Compress 1.19 support for the DEFLATE64, Z and Brotli + As of Commons Compress 1.20 support for the DEFLATE64, Z and Brotli formats is read-only.

The ar, arj, cpio, dump, tar, 7z and zip formats are supported as archivers where the zip implementation provides capabilities that go beyond the features found in java.util.zip. As of Commons Compress - 1.19 support for the dump and arj formats is + 1.20 support for the dump and arj formats is read-only - 7z can read most compressed and encrypted archives but only write unencrypted ones. LZMA(2) support in 7z requires XZ for diff -Nru libcommons-compress-java-1.19/src/site/xdoc/limitations.xml libcommons-compress-java-1.20/src/site/xdoc/limitations.xml --- libcommons-compress-java-1.19/src/site/xdoc/limitations.xml 2019-08-19 20:06:45.000000000 +0000 +++ libcommons-compress-java-1.20/src/site/xdoc/limitations.xml 2020-01-07 14:40:25.000000000 +0000 @@ -53,13 +53,13 @@ archives, starting with 1.8 it will throw a StreamingNotSupportedException when reading from a 7z archive. -

  • Encryption, solid compression and header compression and +
  • Encryption, solid compression and header compression are only supported when reading archives
  • Commons Compress 1.12 and earlier didn't support writing LZMA.
  • Several of the "methods" supported by 7z are not implemented in Compress.
  • -
  • No support for writing multi-volume archives Such +
  • No support for writing multi-volume archives. Such archives can be read by simply concatenating the parts, for example by using MultiReadOnlySeekableByteChannel.
  • @@ -169,7 +169,9 @@
      -
    • sparse files can neither be read nor written
    • +
    • sparse files could not be read in version prior to + Compress 1.20
    • +
    • sparse files can not be written
    • only a subset of the GNU and POSIX extensions are supported
    • In Compress 1.6 TarArchiveInputStream could @@ -204,7 +206,13 @@
    • only a subset of compression methods are supported, including the most common STORED and DEFLATEd. IMPLODE, SHRINK, DEFLATE64 and BZIP2 support is read-only.
    • -
    • no support for encryption or multi-volume archives
    • +
    • no support for encryption
    • +
    • or multi-volume archives prior to Compress 1.20
    • +
    • It is currently not possible to write split archives with + more than 64k segments. When creating split archives with more + than 100 segments you will need to adjust the file names as + ZipArchiveOutputStream assumes extensions will be + three characters long.
    • In versions prior to Compress 1.6 ZipArchiveEntries read from an archive will contain non-zero millisecond values when using Java 8 or later rather diff -Nru libcommons-compress-java-1.19/src/site/xdoc/security-reports.xml libcommons-compress-java-1.20/src/site/xdoc/security-reports.xml --- libcommons-compress-java-1.19/src/site/xdoc/security-reports.xml 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/site/xdoc/security-reports.xml 2020-01-07 14:40:25.000000000 +0000 @@ -54,6 +54,26 @@ the descriptions here are incomplete, please report them privately to the Apache Security Team. Thank you.

      + +

      Low: Denial of Service CVE-2019-12402

      + +

      The file name encoding algorithm used internally in Apache Commons + Compress can get into an infinite loop when faced with specially + crafted inputs. This can lead to a denial of service attack if an + attacker can choose the file names inside of an archive created by + Compress.

      + +

      This was fixed in revision 4ad5d80a.

      + +

      This was first reported to the Commons Security Team on 22 August + 2019 and made public on 27 August 2019.

      + +

      Affects: 1.15 - 1.18

      + + +

      Low: Denial of Service CVE-2018-11771

      diff -Nru libcommons-compress-java-1.19/src/site/xdoc/zip.xml libcommons-compress-java-1.20/src/site/xdoc/zip.xml --- libcommons-compress-java-1.19/src/site/xdoc/zip.xml 2019-08-18 10:41:29.000000000 +0000 +++ libcommons-compress-java-1.20/src/site/xdoc/zip.xml 2020-01-07 14:40:25.000000000 +0000 @@ -107,7 +107,7 @@ explicitly. For example it will completely fail if the stored entry is a ZIP archive itself. Starting with Compress 1.19 ZipArchiveInputStream will perform a few sanity - for STORED entries with data descriptors and throw an + checks for STORED entries with data descriptors and throw an exception if they fail.

      If possible, you should always prefer ZipFile @@ -122,10 +122,19 @@ -

      ZipArchiveOutputStream has three constructors, - one of them uses a File argument, one a +

      ZipArchiveOutputStream has four constructors, + two of them uses a File argument, one a SeekableByteChannel and the last uses an - OutputStream. The File version will + OutputStream.

      + +

      The constructor accepting a File and a size is + used exclusively for creating a split ZIP archive and is + described in th next section. For the remainder of this + section this constructor is equivalent to the one using the + OutputStream argument and thus it is not possible + to add uncompressed entries of unknown size.

      + +

      Of the remaining three constructors the File version will try to use SeekableByteChannel and fall back to using a FileOutputStream internally if that fails.

      @@ -147,6 +156,44 @@
      + +

      The ZIP format knows so called split and spanned + archives. Spanned archives cross several removable media and + are not supported by Commons Compress.

      + +

      Split archives consist of multiple files that reside in the + same directory with the same base name (the file name without + the file extension). The last file of the the archive has the + extension zip the remaining files conventionally + use extensions z01, z02 and so + on. Support for splitted archives has been added with Compress + 1.20.

      + +

      If you want to create a split ZIP archive you use the + constructor of ZipArchiveOutputStream that + accepts a File argument and a size. The size + determines the maximum size of a split segment - the size must + be between 64kB and 4GB. While creating the archive, this will + create several files fillowing the naming convention described + above. The name of the File argument used inside + of the constructor must use the extension + zip.

      + +

      It is currently not possible to write split archives with + more than 64k segments. When creating split archives with more + than 100 segments you will need to adjust the file names as + ZipArchiveOutputStream assumes extensions will be + three characters long.

      + +

      If you want to read a split archive you must create a + ZipSplitReadOnlySeekableByteChannel from the + parts. Both ZipFile and + ZipArchiveInputStream support reading streams of + this type, in the case of ZipArchiveInputStream + you need to use a constructor where you can set + skipSplitSig to true.

      +
      +

      Inside a ZIP archive, additional data can be attached to diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/ar/ArArchiveInputStreamTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/ar/ArArchiveInputStreamTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/ar/ArArchiveInputStreamTest.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/ar/ArArchiveInputStreamTest.java 2020-01-26 12:39:13.000000000 +0000 @@ -97,17 +97,18 @@ } }; - ArArchiveInputStream archiveInputStream = new ArArchiveInputStream(simpleInputStream); - ArArchiveEntry entry1 = archiveInputStream.getNextArEntry(); - assertThat(entry1, not(nullValue())); - assertThat(entry1.getName(), equalTo("test1.xml")); - assertThat(entry1.getLength(), equalTo(610L)); + try (ArArchiveInputStream archiveInputStream = new ArArchiveInputStream(simpleInputStream)) { + ArArchiveEntry entry1 = archiveInputStream.getNextArEntry(); + assertThat(entry1, not(nullValue())); + assertThat(entry1.getName(), equalTo("test1.xml")); + assertThat(entry1.getLength(), equalTo(610L)); - ArArchiveEntry entry2 = archiveInputStream.getNextArEntry(); - assertThat(entry2.getName(), equalTo("test2.xml")); - assertThat(entry2.getLength(), equalTo(82L)); + ArArchiveEntry entry2 = archiveInputStream.getNextArEntry(); + assertThat(entry2.getName(), equalTo("test2.xml")); + assertThat(entry2.getLength(), equalTo(82L)); - assertThat(archiveInputStream.getNextArEntry(), nullValue()); + assertThat(archiveInputStream.getNextArEntry(), nullValue()); + } } } diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStreamTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStreamTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStreamTest.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStreamTest.java 2020-01-26 12:39:13.000000000 +0000 @@ -39,49 +39,48 @@ expected.append("test2.xml\n"); expected.append("\n"); - - final ArjArchiveInputStream in = new ArjArchiveInputStream(new FileInputStream(getFile("bla.arj"))); - ArjArchiveEntry entry; - final StringBuilder result = new StringBuilder(); - while ((entry = in.getNextEntry()) != null) { - result.append(entry.getName()); - int tmp; - while ((tmp = in.read()) != -1) { - result.append((char) tmp); + try (final ArjArchiveInputStream in = new ArjArchiveInputStream(new FileInputStream(getFile("bla.arj")))) { + ArjArchiveEntry entry; + + while ((entry = in.getNextEntry()) != null) { + result.append(entry.getName()); + int tmp; + while ((tmp = in.read()) != -1) { + result.append((char) tmp); + } + assertFalse(entry.isDirectory()); } - assertFalse(entry.isDirectory()); } - in.close(); assertEquals(result.toString(), expected.toString()); } @Test public void testReadingOfAttributesDosVersion() throws Exception { - final ArjArchiveInputStream in = new ArjArchiveInputStream(new FileInputStream(getFile("bla.arj"))); - final ArjArchiveEntry entry = in.getNextEntry(); - assertEquals("test1.xml", entry.getName()); - assertEquals(30, entry.getSize()); - assertEquals(0, entry.getUnixMode()); - final Calendar cal = Calendar.getInstance(); - cal.set(2008, 9, 6, 23, 50, 52); - cal.set(Calendar.MILLISECOND, 0); - assertEquals(cal.getTime(), entry.getLastModifiedDate()); - in.close(); + try (final ArjArchiveInputStream in = new ArjArchiveInputStream(new FileInputStream(getFile("bla.arj")))) { + final ArjArchiveEntry entry = in.getNextEntry(); + assertEquals("test1.xml", entry.getName()); + assertEquals(30, entry.getSize()); + assertEquals(0, entry.getUnixMode()); + final Calendar cal = Calendar.getInstance(); + cal.set(2008, 9, 6, 23, 50, 52); + cal.set(Calendar.MILLISECOND, 0); + assertEquals(cal.getTime(), entry.getLastModifiedDate()); + } } @Test public void testReadingOfAttributesUnixVersion() throws Exception { - final ArjArchiveInputStream in = new ArjArchiveInputStream(new FileInputStream(getFile("bla.unix.arj"))); - final ArjArchiveEntry entry = in.getNextEntry(); - assertEquals("test1.xml", entry.getName()); - assertEquals(30, entry.getSize()); - assertEquals(0664, entry.getUnixMode() & 07777 /* UnixStat.PERM_MASK */); - final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0000")); - cal.set(2008, 9, 6, 21, 50, 52); - cal.set(Calendar.MILLISECOND, 0); - assertEquals(cal.getTime(), entry.getLastModifiedDate()); - in.close(); + try (final ArjArchiveInputStream in = new ArjArchiveInputStream(new FileInputStream(getFile("bla.unix.arj")))) { + final ArjArchiveEntry entry = in.getNextEntry(); + assertEquals("test1.xml", entry.getName()); + assertEquals(30, entry.getSize()); + assertEquals(0664, entry.getUnixMode() & 07777 /* UnixStat.PERM_MASK */); + final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0000")); + cal.set(2008, 9, 6, 21, 50, 52); + cal.set(Calendar.MILLISECOND, 0); + assertEquals(cal.getTime(), entry.getLastModifiedDate()); + } } @Test diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/cpio/CpioArchiveInputStreamTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/cpio/CpioArchiveInputStreamTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/cpio/CpioArchiveInputStreamTest.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/cpio/CpioArchiveInputStreamTest.java 2020-01-26 12:39:13.000000000 +0000 @@ -36,50 +36,49 @@ expected.append("./test2.xml\n"); expected.append("\n"); - - final CpioArchiveInputStream in = new CpioArchiveInputStream(new FileInputStream(getFile("bla.cpio"))); - CpioArchiveEntry entry; - final StringBuilder result = new StringBuilder(); - while ((entry = (CpioArchiveEntry) in.getNextEntry()) != null) { - result.append(entry.getName()); - int tmp; - while ((tmp = in.read()) != -1) { - result.append((char) tmp); + try (final CpioArchiveInputStream in = new CpioArchiveInputStream(new FileInputStream(getFile("bla.cpio")))) { + CpioArchiveEntry entry; + + while ((entry = (CpioArchiveEntry) in.getNextEntry()) != null) { + result.append(entry.getName()); + int tmp; + while ((tmp = in.read()) != -1) { + result.append((char) tmp); + } } } - in.close(); assertEquals(result.toString(), expected.toString()); } @Test public void testCpioUnarchiveCreatedByRedlineRpm() throws Exception { - final CpioArchiveInputStream in = - new CpioArchiveInputStream(new FileInputStream(getFile("redline.cpio"))); - CpioArchiveEntry entry= null; - int count = 0; - while ((entry = (CpioArchiveEntry) in.getNextEntry()) != null) { - count++; - assertNotNull(entry); + try (final CpioArchiveInputStream in = new CpioArchiveInputStream( + new FileInputStream(getFile("redline.cpio")))) { + CpioArchiveEntry entry = null; + + while ((entry = (CpioArchiveEntry) in.getNextEntry()) != null) { + count++; + assertNotNull(entry); + } } - in.close(); assertEquals(count, 1); } @Test public void testCpioUnarchiveMultibyteCharName() throws Exception { - final CpioArchiveInputStream in = - new CpioArchiveInputStream(new FileInputStream(getFile("COMPRESS-459.cpio")), "UTF-8"); - CpioArchiveEntry entry= null; - int count = 0; - while ((entry = (CpioArchiveEntry) in.getNextEntry()) != null) { - count++; - assertNotNull(entry); + try (final CpioArchiveInputStream in = new CpioArchiveInputStream( + new FileInputStream(getFile("COMPRESS-459.cpio")), "UTF-8")) { + CpioArchiveEntry entry = null; + + while ((entry = (CpioArchiveEntry) in.getNextEntry()) != null) { + count++; + assertNotNull(entry); + } } - in.close(); assertEquals(2, count); } diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/examples/ExpanderTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/examples/ExpanderTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/examples/ExpanderTest.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/examples/ExpanderTest.java 2020-01-26 12:39:13.000000000 +0000 @@ -28,13 +28,9 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardOpenOption; -import java.util.Arrays; -import java.util.Collection; import org.apache.commons.compress.AbstractTestCase; -import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.ArchiveException; -import org.apache.commons.compress.archivers.ArchiveInputStream; import org.apache.commons.compress.archivers.ArchiveOutputStream; import org.apache.commons.compress.archivers.ArchiveStreamFactory; import org.apache.commons.compress.archivers.StreamingNotSupportedException; diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZFileTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZFileTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZFileTest.java 2019-08-20 19:29:54.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZFileTest.java 2020-01-26 12:39:13.000000000 +0000 @@ -23,6 +23,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.security.NoSuchAlgorithmException; import java.util.Arrays; @@ -39,25 +40,30 @@ import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.compress.utils.MultiReadOnlySeekableByteChannel; import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; public class SevenZFileTest extends AbstractTestCase { private static final String TEST2_CONTENT = "\r\n\r\n\r\n\t\r\n\n"; + @Rule + public ExpectedException thrown = ExpectedException.none(); + // https://issues.apache.org/jira/browse/COMPRESS-320 @Test public void testRandomlySkippingEntries() throws Exception { // Read sequential reference. final Map entriesByName = new HashMap<>(); - SevenZFile archive = new SevenZFile(getFile("COMPRESS-320/Copy.7z")); - SevenZArchiveEntry entry; - while ((entry = archive.getNextEntry()) != null) { - if (entry.hasStream()) { - entriesByName.put(entry.getName(), readFully(archive)); + try (SevenZFile archive = new SevenZFile(getFile("COMPRESS-320/Copy.7z"))) { + SevenZArchiveEntry entry; + while ((entry = archive.getNextEntry()) != null) { + if (entry.hasStream()) { + entriesByName.put(entry.getName(), readFully(archive)); + } } } - archive.close(); final String[] variants = { "BZip2-solid.7z", @@ -75,26 +81,27 @@ // "PPMd.7z", }; - // TODO: use randomizedtesting for predictable, but different, randomness. + // TODO: use randomized testing for predictable, but different, randomness. final Random rnd = new Random(0xdeadbeef); for (final String fileName : variants) { - archive = new SevenZFile(getFile("COMPRESS-320/" + fileName)); + try (SevenZFile archive = new SevenZFile(getFile("COMPRESS-320/" + fileName))) { - while ((entry = archive.getNextEntry()) != null) { - // Sometimes skip reading entries. - if (rnd.nextBoolean()) { - continue; + SevenZArchiveEntry entry; + while ((entry = archive.getNextEntry()) != null) { + // Sometimes skip reading entries. + if (rnd.nextBoolean()) { + continue; + } + + if (entry.hasStream()) { + assertTrue(entriesByName.containsKey(entry.getName())); + final byte[] content = readFully(archive); + assertTrue("Content mismatch on: " + fileName + "!" + entry.getName(), + Arrays.equals(content, entriesByName.get(entry.getName()))); + } } - if (entry.hasStream()) { - assertTrue(entriesByName.containsKey(entry.getName())); - final byte [] content = readFully(archive); - assertTrue("Content mismatch on: " + fileName + "!" + entry.getName(), - Arrays.equals(content, entriesByName.get(entry.getName()))); - } } - - archive.close(); } } @@ -370,6 +377,188 @@ } } + /** + * @see https://issues.apache.org/jira/browse/COMPRESS-492 + */ + @Test + public void handlesEmptyArchiveWithFilesInfo() throws Exception { + File f = new File(dir, "empty.7z"); + try (SevenZOutputFile s = new SevenZOutputFile(f)) { + } + try (SevenZFile z = new SevenZFile(f)) { + assertFalse(z.getEntries().iterator().hasNext()); + assertNull(z.getNextEntry()); + } + } + + /** + * @see https://issues.apache.org/jira/browse/COMPRESS-492 + */ + @Test + public void handlesEmptyArchiveWithoutFilesInfo() throws Exception { + try (SevenZFile z = new SevenZFile(getFile("COMPRESS-492.7z"))) { + assertFalse(z.getEntries().iterator().hasNext()); + assertNull(z.getNextEntry()); + } + } + + @Test + public void test7zUnarchiveWithDefectHeader() throws Exception { + test7zUnarchive(getFile("bla.noendheaderoffset.7z"), SevenZMethod.LZMA); + } + + @Test + public void extractSpecifiedFile() throws Exception { + try (SevenZFile sevenZFile = new SevenZFile(getFile("COMPRESS-256.7z"))) { + final String testTxtContents = "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011"; + + for(SevenZArchiveEntry entry : sevenZFile.getEntries()) { + if(entry.getName().equals("commons-compress-1.7-src/src/test/resources/test.txt")) { + final byte[] contents = new byte[(int) entry.getSize()]; + int off = 0; + InputStream inputStream = sevenZFile.getInputStream(entry); + while (off < contents.length) { + final int bytesRead = inputStream.read(contents, off, contents.length - off); + assert (bytesRead >= 0); + off += bytesRead; + } + assertEquals(testTxtContents, new String(contents, "UTF-8")); + break; + } + } + } + } + + @Test + public void testRandomAccessTogetherWithSequentialAccess() throws Exception { + try (SevenZFile sevenZFile = new SevenZFile(getFile("COMPRESS-256.7z"))) { + final String testTxtContents = "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011"; + final String filesTxtContents = "0xxxxxxxxx10xxxxxxxx20xxxxxxxx30xxxxxxxx40xxxxxxxx50xxxxxxxx60xxxxxxxx70xxxxxxxx80xxxxxxxx90xxxxxxxx100xxxxxxx110xxxxxxx120xxxxxxx130xxxxxxx -> 0yyyyyyyyy10yyyyyyyy20yyyyyyyy30yyyyyyyy40yyyyyyyy50yyyyyyyy60yyyyyyyy70yyyyyyyy80yyyyyyyy90yyyyyyyy100yyyyyyy110yyyyyyy120yyyyyyy130yyyyyyy\n"; + int off; + byte[] contents; + + // call getNextEntry and read before calling getInputStream + sevenZFile.getNextEntry(); + SevenZArchiveEntry nextEntry = sevenZFile.getNextEntry(); + contents = new byte[(int) nextEntry.getSize()]; + off = 0; + + assertEquals(SevenZMethod.LZMA2, nextEntry.getContentMethods().iterator().next().getMethod()); + + // just read them + while (off < contents.length) { + final int bytesRead = sevenZFile.read(contents, off, contents.length - off); + assert (bytesRead >= 0); + off += bytesRead; + } + + sevenZFile.getNextEntry(); + sevenZFile.getNextEntry(); + + for(SevenZArchiveEntry entry : sevenZFile.getEntries()) { + // commons-compress-1.7-src/src/test/resources/test.txt + if(entry.getName().equals("commons-compress-1.7-src/src/test/resources/longsymlink/files.txt")) { + contents = new byte[(int) entry.getSize()]; + off = 0; + InputStream inputStream = sevenZFile.getInputStream(entry); + while (off < contents.length) { + final int bytesRead = inputStream.read(contents, off, contents.length - off); + assert (bytesRead >= 0); + off += bytesRead; + } + assertEquals(SevenZMethod.LZMA2, entry.getContentMethods().iterator().next().getMethod()); + assertEquals(filesTxtContents, new String(contents, "UTF-8")); + break; + } + } + + // call getNextEntry after getInputStream + nextEntry = sevenZFile.getNextEntry(); + while(!nextEntry.getName().equals("commons-compress-1.7-src/src/test/resources/test.txt")) { + nextEntry = sevenZFile.getNextEntry(); + } + + contents = new byte[(int) nextEntry.getSize()]; + off = 0; + while (off < contents.length) { + final int bytesRead = sevenZFile.read(contents, off, contents.length - off); + assert (bytesRead >= 0); + off += bytesRead; + } + assertEquals(SevenZMethod.LZMA2, nextEntry.getContentMethods().iterator().next().getMethod()); + assertEquals(testTxtContents, new String(contents, "UTF-8")); + } + } + + @Test + public void testRandomAccessWhenJumpingBackwards() throws Exception { + try (SevenZFile sevenZFile = new SevenZFile(getFile("COMPRESS-256.7z"))) { + final String testTxtContents = "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011\n" + + "111111111111111111111111111000101011"; + + SevenZArchiveEntry entry; + SevenZArchiveEntry testTxtEntry = null; + while((entry = sevenZFile.getNextEntry()) != null ) { + if(entry.getName().equals("commons-compress-1.7-src/src/test/resources/test.txt")) { + testTxtEntry = entry; + break; + } + } + + sevenZFile.getNextEntry(); + sevenZFile.getNextEntry(); + // skip all the entries and jump backwards + byte[] contents = new byte[(int) testTxtEntry.getSize()]; + try (InputStream inputStream = sevenZFile.getInputStream(testTxtEntry)) { + int off = 0; + while (off < contents.length) { + final int bytesRead = inputStream.read(contents, off, contents.length - off); + assert (bytesRead >= 0); + off += bytesRead; + } + assertEquals(SevenZMethod.LZMA2, testTxtEntry.getContentMethods().iterator().next().getMethod()); + assertEquals(testTxtContents, new String(contents, "UTF-8")); + } + } + } + + @Test + public void extractNonExistSpecifiedFile() throws Exception { + try (SevenZFile sevenZFile = new SevenZFile(getFile("COMPRESS-256.7z")); + SevenZFile anotherSevenZFile = new SevenZFile(getFile("bla.7z"))) { + for (SevenZArchiveEntry nonExistEntry : anotherSevenZFile.getEntries()) { + thrown.expect(IllegalArgumentException.class); + sevenZFile.getInputStream(nonExistEntry); + } + } + } + private void test7zUnarchive(final File f, final SevenZMethod m, final byte[] password) throws Exception { try (SevenZFile sevenZFile = new SevenZFile(f, password)) { test7zUnarchive(sevenZFile, m); diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZNativeHeapTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZNativeHeapTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZNativeHeapTest.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZNativeHeapTest.java 2020-01-26 12:39:13.000000000 +0000 @@ -33,29 +33,28 @@ public class SevenZNativeHeapTest extends AbstractTestCase { - @Test public void testEndDeflaterOnCloseStream() throws Exception { - Coders.DeflateDecoder deflateDecoder = new DeflateDecoder(); - - final DeflateDecoderOutputStream outputStream = - (DeflateDecoderOutputStream) deflateDecoder.encode(new ByteArrayOutputStream(), 9); - DelegatingDeflater delegatingDeflater = new DelegatingDeflater(outputStream.deflater); - outputStream.deflater = delegatingDeflater; - outputStream.close(); + final Coders.DeflateDecoder deflateDecoder = new DeflateDecoder(); + final DelegatingDeflater delegatingDeflater; + try (final DeflateDecoderOutputStream outputStream = (DeflateDecoderOutputStream) deflateDecoder + .encode(new ByteArrayOutputStream(), 9)) { + delegatingDeflater = new DelegatingDeflater(outputStream.deflater); + outputStream.deflater = delegatingDeflater; + } assertTrue(delegatingDeflater.isEnded.get()); } @Test public void testEndInflaterOnCloseStream() throws Exception { - Coders.DeflateDecoder deflateDecoder = new DeflateDecoder(); - final DeflateDecoderInputStream inputStream = - (DeflateDecoderInputStream) deflateDecoder - .decode("dummy", new ByteArrayInputStream(new byte[0]), 0, null, null, Integer.MAX_VALUE); - DelegatingInflater delegatingInflater = new DelegatingInflater(inputStream.inflater); - inputStream.inflater = delegatingInflater; - inputStream.close(); + final Coders.DeflateDecoder deflateDecoder = new DeflateDecoder(); + final DelegatingInflater delegatingInflater; + try (final DeflateDecoderInputStream inputStream = (DeflateDecoderInputStream) deflateDecoder.decode("dummy", + new ByteArrayInputStream(new byte[0]), 0, null, null, Integer.MAX_VALUE)) { + delegatingInflater = new DelegatingInflater(inputStream.inflater); + inputStream.inflater = delegatingInflater; + } assertTrue(delegatingInflater.isEnded.get()); } diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFileTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFileTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFileTest.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFileTest.java 2020-01-26 12:39:13.000000000 +0000 @@ -479,12 +479,9 @@ } private void createAndReadBack(final File output, final Iterable methods) throws Exception { - final SevenZOutputFile outArchive = new SevenZOutputFile(output); - outArchive.setContentMethods(methods); - try { + try (final SevenZOutputFile outArchive = new SevenZOutputFile(output)) { + outArchive.setContentMethods(methods); addFile(outArchive, 0, true); - } finally { - outArchive.close(); } try (SevenZFile archive = new SevenZFile(output)) { @@ -493,12 +490,9 @@ } private void createAndReadBack(final SeekableInMemoryByteChannel output, final Iterable methods) throws Exception { - final SevenZOutputFile outArchive = new SevenZOutputFile(output); - outArchive.setContentMethods(methods); - try { + try (final SevenZOutputFile outArchive = new SevenZOutputFile(output)) { + outArchive.setContentMethods(methods); addFile(outArchive, 0, true); - } finally { - outArchive.close(); } try (SevenZFile archive = new SevenZFile(new SeekableInMemoryByteChannel(output.array()), "in memory")) { diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/SevenZTestCase.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/SevenZTestCase.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/SevenZTestCase.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/SevenZTestCase.java 2020-01-26 12:39:13.000000000 +0000 @@ -44,6 +44,7 @@ file2 = getFile("test2.xml"); } + @Override @Before public void setUp() throws Exception { super.setUp(); diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/tar/SparseFilesTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/tar/SparseFilesTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/tar/SparseFilesTest.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/tar/SparseFilesTest.java 2020-01-25 14:01:31.000000000 +0000 @@ -18,15 +18,24 @@ package org.apache.commons.compress.archivers.tar; -import static org.apache.commons.compress.AbstractTestCase.getFile; import static org.junit.Assert.*; + +import org.apache.commons.compress.AbstractTestCase; +import org.apache.commons.compress.utils.IOUtils; +import org.junit.Assert; import org.junit.Test; import java.io.File; import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Locale; + +public class SparseFilesTest extends AbstractTestCase { -public class SparseFilesTest { + private final boolean isOnWindows = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"); @Test public void testOldGNU() throws Throwable { @@ -40,6 +49,18 @@ assertTrue(ae.isGNUSparse()); assertFalse(ae.isPaxGNUSparse()); assertFalse(tin.canReadEntryData(ae)); + + List sparseHeaders = ae.getSparseHeaders(); + assertEquals(3, sparseHeaders.size()); + + assertEquals(0, sparseHeaders.get(0).getOffset()); + assertEquals(2048, sparseHeaders.get(0).getNumbytes()); + + assertEquals(1050624L, sparseHeaders.get(1).getOffset()); + assertEquals(2560, sparseHeaders.get(1).getNumbytes()); + + assertEquals(3101184L, sparseHeaders.get(2).getOffset()); + assertEquals(0, sparseHeaders.get(2).getNumbytes()); } finally { if (tin != null) { tin.close(); @@ -63,6 +84,133 @@ } } + @Test + public void testExtractSparseTarsOnWindows() throws IOException { + if (!isOnWindows) { + return; + } + + final File oldGNUSparseTar = getFile("oldgnu_sparse.tar"); + final File paxGNUSparseTar = getFile("pax_gnu_sparse.tar"); + try (TarArchiveInputStream paxGNUSparseInputStream = new TarArchiveInputStream(new FileInputStream(paxGNUSparseTar))) { + + // compare between old GNU and PAX 0.0 + paxGNUSparseInputStream.getNextTarEntry(); + try (TarArchiveInputStream oldGNUSparseInputStream = new TarArchiveInputStream(new FileInputStream(oldGNUSparseTar))) { + oldGNUSparseInputStream.getNextTarEntry(); + assertArrayEquals(IOUtils.toByteArray(oldGNUSparseInputStream), + IOUtils.toByteArray(paxGNUSparseInputStream)); + } + + // compare between old GNU and PAX 0.1 + paxGNUSparseInputStream.getNextTarEntry(); + try (TarArchiveInputStream oldGNUSparseInputStream = new TarArchiveInputStream(new FileInputStream(oldGNUSparseTar))) { + oldGNUSparseInputStream.getNextTarEntry(); + assertArrayEquals(IOUtils.toByteArray(oldGNUSparseInputStream), + IOUtils.toByteArray(paxGNUSparseInputStream)); + } + + // compare between old GNU and PAX 1.0 + paxGNUSparseInputStream.getNextTarEntry(); + try (TarArchiveInputStream oldGNUSparseInputStream = new TarArchiveInputStream(new FileInputStream(oldGNUSparseTar))) { + oldGNUSparseInputStream.getNextTarEntry(); + assertArrayEquals(IOUtils.toByteArray(oldGNUSparseInputStream), + IOUtils.toByteArray(paxGNUSparseInputStream)); + } + } + } + + @Test + public void testExtractOldGNU() throws IOException, InterruptedException { + if (isOnWindows) { + return; + } + + try { + final File file = getFile("oldgnu_sparse.tar"); + try (InputStream sparseFileInputStream = extractTarAndGetInputStream(file, "sparsefile"); + TarArchiveInputStream tin = new TarArchiveInputStream(new FileInputStream(file))) { + tin.getNextTarEntry(); + assertArrayEquals(IOUtils.toByteArray(tin), + IOUtils.toByteArray(sparseFileInputStream)); + } + } catch (RuntimeException | IOException ex) { + ex.printStackTrace(); + throw ex; + } + } + + @Test + public void testExtractExtendedOldGNU() throws IOException, InterruptedException { + if (isOnWindows) { + return; + } + + final File file = getFile("oldgnu_extended_sparse.tar"); + try (InputStream sparseFileInputStream = extractTarAndGetInputStream(file, "sparse6"); + TarArchiveInputStream tin = new TarArchiveInputStream(new FileInputStream(file))) { + final TarArchiveEntry ae = tin.getNextTarEntry(); + + assertArrayEquals(IOUtils.toByteArray(tin), + IOUtils.toByteArray(sparseFileInputStream)); + + List sparseHeaders = ae.getSparseHeaders(); + assertEquals(7, sparseHeaders.size()); + + assertEquals(0, sparseHeaders.get(0).getOffset()); + assertEquals(1024, sparseHeaders.get(0).getNumbytes()); + + assertEquals(10240, sparseHeaders.get(1).getOffset()); + assertEquals(1024, sparseHeaders.get(1).getNumbytes()); + + assertEquals(16384, sparseHeaders.get(2).getOffset()); + assertEquals(1024, sparseHeaders.get(2).getNumbytes()); + + assertEquals(24576, sparseHeaders.get(3).getOffset()); + assertEquals(1024, sparseHeaders.get(3).getNumbytes()); + + assertEquals(29696, sparseHeaders.get(4).getOffset()); + assertEquals(1024, sparseHeaders.get(4).getNumbytes()); + + assertEquals(36864, sparseHeaders.get(5).getOffset()); + assertEquals(1024, sparseHeaders.get(5).getNumbytes()); + + assertEquals(51200, sparseHeaders.get(6).getOffset()); + assertEquals(0, sparseHeaders.get(6).getNumbytes()); + } + } + + @Test + public void testExtractPaxGNU() throws IOException, InterruptedException { + if (isOnWindows) { + return; + } + + final File file = getFile("pax_gnu_sparse.tar"); + try (TarArchiveInputStream tin = new TarArchiveInputStream(new FileInputStream(file))) { + + tin.getNextTarEntry(); + try (InputStream sparseFileInputStream = extractTarAndGetInputStream(file, "sparsefile-0.0")) { + assertArrayEquals(IOUtils.toByteArray(tin), + IOUtils.toByteArray(sparseFileInputStream)); + } + + // TODO : it's wired that I can only get a 0 size sparsefile-0.1 on my Ubuntu 16.04 + // using "tar -xf pax_gnu_sparse.tar" + tin.getNextTarEntry(); + try (InputStream sparseFileInputStream = extractTarAndGetInputStream(file, "sparsefile-0.0")) { + assertArrayEquals(IOUtils.toByteArray(tin), + IOUtils.toByteArray(sparseFileInputStream)); + } + + tin.getNextTarEntry(); + try (InputStream sparseFileInputStream = extractTarAndGetInputStream(file, "sparsefile-1.0")) { + assertArrayEquals(IOUtils.toByteArray(tin), + IOUtils.toByteArray(sparseFileInputStream)); + } + } + } + private void assertPaxGNUEntry(final TarArchiveInputStream tin, final String suffix) throws Throwable { final TarArchiveEntry ae = tin.getNextTarEntry(); assertEquals("sparsefile-" + suffix, ae.getName()); @@ -70,6 +218,34 @@ assertTrue(ae.isPaxGNUSparse()); assertFalse(ae.isOldGNUSparse()); assertFalse(tin.canReadEntryData(ae)); + + List sparseHeaders = ae.getSparseHeaders(); + assertEquals(3, sparseHeaders.size()); + + assertEquals(0, sparseHeaders.get(0).getOffset()); + assertEquals(2048, sparseHeaders.get(0).getNumbytes()); + + assertEquals(1050624L, sparseHeaders.get(1).getOffset()); + assertEquals(2560, sparseHeaders.get(1).getNumbytes()); + + assertEquals(3101184L, sparseHeaders.get(2).getOffset()); + assertEquals(0, sparseHeaders.get(2).getNumbytes()); + } + + private InputStream extractTarAndGetInputStream(File tarFile, String sparseFileName) throws IOException, InterruptedException { + ProcessBuilder pb = new ProcessBuilder("tar", "-xf", tarFile.getPath(), "-C", resultDir.getPath()); + pb.redirectErrorStream(true); + Process process = pb.start(); + // wait until the extract finishes + assertEquals(new String(IOUtils.toByteArray(process.getInputStream())), 0, process.waitFor()); + + for (File file : resultDir.listFiles()) { + if (file.getName().equals(sparseFileName)) { + return new FileInputStream(file); + } + } + fail("didn't find " + sparseFileName + " after extracting " + tarFile); + return null; } } diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStreamTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStreamTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStreamTest.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveInputStreamTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -54,7 +54,7 @@ final TarArchiveInputStream tais = new TarArchiveInputStream(is); final Map headers = tais .parsePaxHeaders(new ByteArrayInputStream("30 atime=1321711775.972059463\n" - .getBytes(CharsetNames.UTF_8))); + .getBytes(CharsetNames.UTF_8)), null); assertEquals(1, headers.size()); assertEquals("1321711775.972059463", headers.get("atime")); tais.close(); @@ -66,7 +66,7 @@ final TarArchiveInputStream tais = new TarArchiveInputStream(is); final Map headers = tais .parsePaxHeaders(new ByteArrayInputStream("11 foo=bar\n11 foo=baz\n" - .getBytes(CharsetNames.UTF_8))); + .getBytes(CharsetNames.UTF_8)), null); assertEquals(1, headers.size()); assertEquals("baz", headers.get("foo")); tais.close(); @@ -78,7 +78,7 @@ final TarArchiveInputStream tais = new TarArchiveInputStream(is); final Map headers = tais .parsePaxHeaders(new ByteArrayInputStream("11 foo=bar\n7 foo=\n" - .getBytes(CharsetNames.UTF_8))); + .getBytes(CharsetNames.UTF_8)), null); assertEquals(0, headers.size()); tais.close(); } @@ -89,7 +89,7 @@ final TarArchiveInputStream tais = new TarArchiveInputStream(is); final Map headers = tais .parsePaxHeaders(new ByteArrayInputStream("28 comment=line1\nline2\nand3\n" - .getBytes(CharsetNames.UTF_8))); + .getBytes(CharsetNames.UTF_8)), null); assertEquals(1, headers.size()); assertEquals("line1\nline2\nand3", headers.get("comment")); tais.close(); @@ -103,7 +103,7 @@ final InputStream is = new ByteArrayInputStream(new byte[1]); final TarArchiveInputStream tais = new TarArchiveInputStream(is); final Map headers = tais - .parsePaxHeaders(new ByteArrayInputStream(line.getBytes(CharsetNames.UTF_8))); + .parsePaxHeaders(new ByteArrayInputStream(line.getBytes(CharsetNames.UTF_8)), null); assertEquals(1, headers.size()); assertEquals(ae, headers.get("path")); tais.close(); diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/tar/TarUtilsTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -381,4 +381,16 @@ } } + @Test + public void testParseSparse() { + final long expectedOffset = 0100000; + final long expectedNumbytes = 0111000; + final byte [] buffer = new byte[] { + ' ', ' ', ' ', ' ', ' ', '0', '1', '0', '0', '0', '0', '0', // sparseOffset + ' ', ' ', ' ', ' ', ' ', '0', '1', '1', '1', '0', '0', '0'}; + TarArchiveStructSparse sparse = TarUtils.parseSparse(buffer, 0); + assertEquals(sparse.getOffset(), expectedOffset); + assertEquals(sparse.getNumbytes(), expectedNumbytes); + } + } diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/zip/ScatterSampleTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/zip/ScatterSampleTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/zip/ScatterSampleTest.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/zip/ScatterSampleTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -52,22 +52,23 @@ }; scatterSample.addEntry(archiveEntry, supp); - final ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(result); - scatterSample.writeTo(zipArchiveOutputStream); - zipArchiveOutputStream.close(); + try (final ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(result)) { + scatterSample.writeTo(zipArchiveOutputStream); + } } private void checkFile(final File result) throws IOException { - final ZipFile zf = new ZipFile(result); - final ZipArchiveEntry archiveEntry1 = zf.getEntries().nextElement(); - assertEquals( "test1.xml", archiveEntry1.getName()); - final InputStream inputStream = zf.getInputStream(archiveEntry1); - final byte[] b = new byte[6]; - final int i = IOUtils.readFully(inputStream, b); - assertEquals(5, i); - assertEquals('H', b[0]); - assertEquals('o', b[4]); - zf.close(); + try (final ZipFile zipFile = new ZipFile(result)) { + final ZipArchiveEntry archiveEntry1 = zipFile.getEntries().nextElement(); + assertEquals("test1.xml", archiveEntry1.getName()); + try (final InputStream inputStream = zipFile.getInputStream(archiveEntry1)) { + final byte[] b = new byte[6]; + final int i = IOUtils.readFully(inputStream, b); + assertEquals(5, i); + assertEquals('H', b[0]); + assertEquals('o', b[4]); + } + } result.delete(); } } \ No newline at end of file diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/zip/Zip64SupportIT.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/zip/Zip64SupportIT.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/zip/Zip64SupportIT.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/zip/Zip64SupportIT.java 2020-01-21 17:43:20.000000000 +0000 @@ -35,6 +35,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; +import java.nio.file.Files; import java.util.Enumeration; import java.util.Random; import java.util.zip.ZipEntry; @@ -362,6 +363,10 @@ return write3EntriesCreatingBigArchive(Zip64Mode.AsNeeded); } + private static ZipOutputTest write3EntriesCreatingBigArchive(final Zip64Mode mode) { + return write3EntriesCreatingBigArchive(mode, false); + } + /* * Individual sizes don't require ZIP64 but the offset of the * third entry is bigger than 0xFFFFFFFF so a ZIP64 extended @@ -370,7 +375,7 @@ * Creates a temporary archive of approx 5GB in size */ private static ZipOutputTest - write3EntriesCreatingBigArchive(final Zip64Mode mode) { + write3EntriesCreatingBigArchive(final Zip64Mode mode, final boolean isSplitArchive) { return new ZipOutputTest() { @Override public void test(final File f, final ZipArchiveOutputStream zos) @@ -386,7 +391,7 @@ a.skipBytes(2 * 47 /* CD entry of file with file name length 1 and no extra data */ - + 2 * (mode == Zip64Mode.Always ? 28 : 0) + + 2 * (mode == Zip64Mode.Always ? 32 : 0) /* ZIP64 extra fields if mode is Always */ ); @@ -427,11 +432,11 @@ // file name length 1, 0, // extra field length - (byte) (mode == Zip64Mode.Always? 28 : 12), 0, + (byte) (mode == Zip64Mode.Always? 32 : 12), 0, // comment length 0, 0, // disk number - 0, 0, + (byte) (isSplitArchive? 0xFF : 0), (byte) (isSplitArchive? 0xFF : 0), // attributes 0, 0, 0, 0, 0, 0, @@ -448,7 +453,7 @@ // Header-ID 1, 0, // size - 24, 0, + 28, 0, // Original Size 1, 0, 0, 0, 0, 0, 0, 0, }, extra); @@ -645,7 +650,7 @@ // file name length 1, 0, // extra field length - (byte) (mode == Zip64Mode.Always? 28 : 20), 0, + (byte) (mode == Zip64Mode.Always? 32 : 20), 0, // comment length 0, 0, // disk number @@ -673,7 +678,7 @@ // Header-ID 1, 0, // size of extra - (byte) (mode == Zip64Mode.Always? 24 : 16), 0, + (byte) (mode == Zip64Mode.Always? 28 : 16), 0, // original size 0, (byte) 0xF2, 5, (byte) 0x2A, 1, 0, 0, 0, @@ -899,7 +904,7 @@ // file name length 1, 0, // extra field length - (byte) (mode == Zip64Mode.Always? 28 : 20), 0, + (byte) (mode == Zip64Mode.Always? 32 : 20), 0, // comment length 0, 0, // disk number @@ -927,7 +932,7 @@ // Header-ID 1, 0, // size of extra - (byte) (mode == Zip64Mode.Always? 24 : 16), 0, + (byte) (mode == Zip64Mode.Always? 28 : 16), 0, // original size 0, (byte) 0xF2, 5, (byte) 0x2A, 1, 0, 0, 0, @@ -1153,7 +1158,7 @@ // file name length 1, 0, // extra field length - (byte) (mode == Zip64Mode.Always? 28 : 20), 0, + (byte) (mode == Zip64Mode.Always? 32 : 20), 0, // comment length 0, 0, // disk number @@ -1181,7 +1186,7 @@ // Header-ID 1, 0, // size of extra - (byte) (mode == Zip64Mode.Always? 24 : 16), 0, + (byte) (mode == Zip64Mode.Always? 28 : 16), 0, // original size 0, (byte) 0xF2, 5, (byte) 0x2A, 1, 0, 0, 0, @@ -1594,7 +1599,7 @@ // file name length 1, 0, // extra field length - 28, 0, + 32, 0, // comment length 0, 0, // disk number @@ -1614,7 +1619,7 @@ // Header-ID 1, 0, // size of extra - 24, 0, + 28, 0, // original size (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0, @@ -1935,7 +1940,7 @@ // file name length 1, 0, // extra field length - 28, 0, + 32, 0, // comment length 0, 0, // disk number @@ -1954,7 +1959,7 @@ // Header-ID 1, 0, // size of extra - 24, 0, + 28, 0, // original size (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0, @@ -2294,7 +2299,7 @@ // file name length 1, 0, // extra field length - 28, 0, + 32, 0, // comment length 0, 0, // disk number @@ -2313,7 +2318,7 @@ // Header-ID 1, 0, // size of extra - 24, 0, + 28, 0, // original size (byte) 0x40, (byte) 0x42, (byte) 0x0F, 0, 0, 0, 0, 0, @@ -2398,19 +2403,54 @@ true); } + @Test + public void write3EntriesCreatingManySplitArchiveFileModeNever() + throws Throwable { + withTemporaryArchive("write3EntriesCreatingManySplitArchiveFileModeNever", + write3EntriesCreatingBigArchiveModeNever, + true, 65536L); + } + + @Test + public void write3EntriesCreatingManySplitArchiveFileModeAlways() + throws Throwable { + // about 76,293 zip split segments will be created + withTemporaryArchive("write3EntriesCreatingManySplitArchiveFileModeAlways", + write3EntriesCreatingBigArchive(Zip64Mode.Always, true), + true, 65536L); + } + static interface ZipOutputTest { void test(File f, ZipArchiveOutputStream zos) throws IOException; } private static void withTemporaryArchive(final String testName, final ZipOutputTest test, - final boolean useRandomAccessFile) + final boolean useRandomAccessFile) throws Throwable { + withTemporaryArchive(testName, test, useRandomAccessFile, null); + } + + private static void withTemporaryArchive(final String testName, + final ZipOutputTest test, + final boolean useRandomAccessFile, + final Long splitSize) throws Throwable { - final File f = getTempFile(testName); + File f = getTempFile(testName); + File dir = null; + if (splitSize != null) { + dir = Files.createTempDirectory("commons-compress-" + testName).toFile(); + dir.deleteOnExit(); + + f = new File(dir, "commons-compress-" + testName + ".zip"); + } BufferedOutputStream os = null; - final ZipArchiveOutputStream zos = useRandomAccessFile + ZipArchiveOutputStream zos = useRandomAccessFile ? new ZipArchiveOutputStream(f) : new ZipArchiveOutputStream(os = new BufferedOutputStream(new FileOutputStream(f))); + if (splitSize != null) { + zos = new ZipArchiveOutputStream(f, splitSize); + } + try { test.test(f, zos); } catch (final IOException ex) { @@ -2422,10 +2462,16 @@ try { zos.destroy(); } finally { - if (os != null) { - os.close(); + try { + if (os != null) { + os.close(); + } + AbstractTestCase.tryHardToDelete(f); + } finally { + if (dir != null) { + AbstractTestCase.rmdir(dir); + } } - AbstractTestCase.tryHardToDelete(f); } } } diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStreamTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStreamTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStreamTest.java 2019-08-18 10:32:58.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStreamTest.java 2020-01-24 18:49:27.000000000 +0000 @@ -34,6 +34,8 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.channels.Channels; +import java.nio.channels.SeekableByteChannel; import java.util.Arrays; import java.util.zip.ZipException; @@ -596,6 +598,85 @@ } } + @Test + public void testSplitZipCreatedByZip() throws IOException { + File lastFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.zip"); + try (SeekableByteChannel channel = ZipSplitReadOnlySeekableByteChannel.buildFromLastSplitSegment(lastFile); + InputStream inputStream = Channels.newInputStream(channel); + ZipArchiveInputStream splitInputStream = new ZipArchiveInputStream(inputStream, ZipEncodingHelper.UTF8, true, false, true)) { + + File fileToCompare = getFile("COMPRESS-477/split_zip_created_by_zip/zip_to_compare_created_by_zip.zip"); + try (ZipArchiveInputStream inputStreamToCompare = new ZipArchiveInputStream(new FileInputStream(fileToCompare), ZipEncodingHelper.UTF8, true, false, true)) { + + ArchiveEntry entry; + while((entry = splitInputStream.getNextEntry()) != null && inputStreamToCompare.getNextEntry() != null) { + if(entry.isDirectory()) { + continue; + } + assertArrayEquals(IOUtils.toByteArray(splitInputStream), + IOUtils.toByteArray(inputStreamToCompare)); + } + } + } + } + + @Test + public void testSplitZipCreatedByZipOfZip64() throws IOException { + File lastFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip_zip64.zip"); + try (SeekableByteChannel channel = ZipSplitReadOnlySeekableByteChannel.buildFromLastSplitSegment(lastFile); + InputStream inputStream = Channels.newInputStream(channel); + ZipArchiveInputStream splitInputStream = new ZipArchiveInputStream(inputStream, ZipEncodingHelper.UTF8, true, false, true)) { + + File fileToCompare = getFile("COMPRESS-477/split_zip_created_by_zip/zip_to_compare_created_by_zip_zip64.zip"); + try (ZipArchiveInputStream inputStreamToCompare = new ZipArchiveInputStream(new FileInputStream(fileToCompare), ZipEncodingHelper.UTF8, true, false, true)) { + + ArchiveEntry entry; + while((entry = splitInputStream.getNextEntry()) != null && inputStreamToCompare.getNextEntry() != null) { + if(entry.isDirectory()) { + continue; + } + assertArrayEquals(IOUtils.toByteArray(splitInputStream), + IOUtils.toByteArray(inputStreamToCompare)); + } + } + } + } + + @Test + public void testSplitZipCreatedByWinrar() throws IOException { + File lastFile = getFile("COMPRESS-477/split_zip_created_by_winrar/split_zip_created_by_winrar.zip"); + try (SeekableByteChannel channel = ZipSplitReadOnlySeekableByteChannel.buildFromLastSplitSegment(lastFile); + InputStream inputStream = Channels.newInputStream(channel); + ZipArchiveInputStream splitInputStream = new ZipArchiveInputStream(inputStream, ZipEncodingHelper.UTF8, true, false, true)) { + + File fileToCompare = getFile("COMPRESS-477/split_zip_created_by_winrar/zip_to_compare_created_by_winrar.zip"); + try (ZipArchiveInputStream inputStreamToCompare = new ZipArchiveInputStream(new FileInputStream(fileToCompare), ZipEncodingHelper.UTF8, true, false, true)) { + + ArchiveEntry entry; + while((entry = splitInputStream.getNextEntry()) != null && inputStreamToCompare.getNextEntry() != null) { + if(entry.isDirectory()) { + continue; + } + assertArrayEquals(IOUtils.toByteArray(splitInputStream), + IOUtils.toByteArray(inputStreamToCompare)); + } + } + } + } + + @Test + public void testSplitZipCreatedByZipThrowsException() throws IOException { + thrown.expect(EOFException.class); + File zipSplitFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z01"); + InputStream fileInputStream = new FileInputStream(zipSplitFile); + ZipArchiveInputStream inputStream = new ZipArchiveInputStream(fileInputStream, ZipEncodingHelper.UTF8, true, false, true); + + ArchiveEntry entry = inputStream.getNextEntry(); + while(entry != null){ + entry = inputStream.getNextEntry(); + } + } + private static byte[] readEntry(ZipArchiveInputStream zip, ZipArchiveEntry zae) throws IOException { final int len = (int)zae.getSize(); final byte[] buff = new byte[len]; diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/zip/ZipFileTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/zip/ZipFileTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/zip/ZipFileTest.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/zip/ZipFileTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -28,12 +28,16 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.channels.SeekableByteChannel; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicInteger; @@ -687,6 +691,52 @@ multiByteReadConsistentlyReturnsMinusOneAtEof(getFile("bzip2-zip.zip")); } + @Test + public void extractFileLiesAcrossSplitZipSegmentsCreatedByZip() throws Exception { + File lastFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.zip"); + SeekableByteChannel channel = ZipSplitReadOnlySeekableByteChannel.buildFromLastSplitSegment(lastFile); + zf = new ZipFile(channel); + + // the compressed content of UnsupportedCompressionAlgorithmException.java lies between .z01 and .z02 + ZipArchiveEntry zipEntry = zf.getEntry("commons-compress/src/main/java/org/apache/commons/compress/archivers/dump/UnsupportedCompressionAlgorithmException.java"); + File fileToCompare = getFile("COMPRESS-477/split_zip_created_by_zip/file_to_compare_1"); + assertFileEqualsToEntry(fileToCompare, zipEntry, zf); + + // the compressed content of DeflateParameters.java lies between .z02 and .zip + zipEntry = zf.getEntry("commons-compress/src/main/java/org/apache/commons/compress/compressors/deflate/DeflateParameters.java"); + fileToCompare = getFile("COMPRESS-477/split_zip_created_by_zip/file_to_compare_2"); + assertFileEqualsToEntry(fileToCompare, zipEntry, zf); + } + + @Test + public void extractFileLiesAcrossSplitZipSegmentsCreatedByZipOfZip64() throws Exception { + File lastFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip_zip64.zip"); + SeekableByteChannel channel = ZipSplitReadOnlySeekableByteChannel.buildFromLastSplitSegment(lastFile); + zf = new ZipFile(channel); + + // the compressed content of UnsupportedCompressionAlgorithmException.java lies between .z01 and .z02 + ZipArchiveEntry zipEntry = zf.getEntry("commons-compress/src/main/java/org/apache/commons/compress/archivers/dump/UnsupportedCompressionAlgorithmException.java"); + File fileToCompare = getFile("COMPRESS-477/split_zip_created_by_zip/file_to_compare_1"); + assertFileEqualsToEntry(fileToCompare, zipEntry, zf); + + // the compressed content of DeflateParameters.java lies between .z02 and .zip + zipEntry = zf.getEntry("commons-compress/src/main/java/org/apache/commons/compress/compressors/deflate/DeflateParameters.java"); + fileToCompare = getFile("COMPRESS-477/split_zip_created_by_zip/file_to_compare_2"); + assertFileEqualsToEntry(fileToCompare, zipEntry, zf); + } + + @Test + public void extractFileLiesAcrossSplitZipSegmentsCreatedByWinrar() throws Exception { + File lastFile = getFile("COMPRESS-477/split_zip_created_by_winrar/split_zip_created_by_winrar.zip"); + SeekableByteChannel channel = ZipSplitReadOnlySeekableByteChannel.buildFromLastSplitSegment(lastFile); + zf = new ZipFile(channel); + + // the compressed content of ZipArchiveInputStream.java lies between .z01 and .z02 + ZipArchiveEntry zipEntry = zf.getEntry("commons-compress/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java"); + File fileToCompare = getFile("COMPRESS-477/split_zip_created_by_winrar/file_to_compare_1"); + assertFileEqualsToEntry(fileToCompare, zipEntry, zf); + } + private void multiByteReadConsistentlyReturnsMinusOneAtEof(File file) throws Exception { byte[] buf = new byte[2]; try (ZipFile archive = new ZipFile(file)) { @@ -799,4 +849,37 @@ assertEquals(expected, ze.getNameSource()); } } + + private void assertFileEqualsToEntry(File fileToCompare, ZipArchiveEntry entry, ZipFile zipFile) throws IOException { + byte[] buffer = new byte[10240]; + File tempFile = File.createTempFile("temp","txt"); + OutputStream outputStream = new FileOutputStream(tempFile); + InputStream inputStream = zipFile.getInputStream(entry); + int readLen; + while((readLen = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, readLen); + } + + outputStream.close(); + inputStream.close(); + + assertFileEqualIgnoreEndOfLine(fileToCompare, tempFile); + } + + private void assertFileEqualIgnoreEndOfLine(File file1, File file2) throws IOException { + List linesOfFile1 = Files.readAllLines(Paths.get(file1.getCanonicalPath()), Charset.forName("UTF-8")); + List linesOfFile2 = Files.readAllLines(Paths.get(file2.getCanonicalPath()), Charset.forName("UTF-8")); + + if(linesOfFile1.size() != linesOfFile2.size()) { + fail("files not equal : " + file1.getName() + " , " + file2.getName()); + } + + String tempLineInFile1; + String tempLineInFile2; + for(int i = 0;i < linesOfFile1.size();i++) { + tempLineInFile1 = linesOfFile1.get(i).replaceAll("\r\n", "\n"); + tempLineInFile2 = linesOfFile1.get(i).replaceAll("\r\n", "\n"); + Assert.assertEquals(tempLineInFile1, tempLineInFile2); + } + } } diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/zip/ZipSplitOutputStreamTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/zip/ZipSplitOutputStreamTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/zip/ZipSplitOutputStreamTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/zip/ZipSplitOutputStreamTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -0,0 +1,104 @@ +/* + * 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 + * + * http://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.zip; + +import org.apache.commons.compress.AbstractTestCase; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +public class ZipSplitOutputStreamTest extends AbstractTestCase { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void throwsExceptionIfSplitSizeIsTooSmall() throws IOException { + thrown.expect(IllegalArgumentException.class); + new ZipSplitOutputStream(File.createTempFile("temp", "zip"), (64 * 1024 - 1)); + } + + @Test + public void throwsExceptionIfSplitSizeIsTooLarge() throws IOException { + thrown.expect(IllegalArgumentException.class); + new ZipSplitOutputStream(File.createTempFile("temp", "zip"), (4 * 1024 * 1024 * 1024L)); + } + + @Test + public void throwsIfUnsplittableSizeLargerThanSplitSize() throws IOException { + thrown.expect(IllegalArgumentException.class); + long splitSize = 100 * 1024; + ZipSplitOutputStream output = new ZipSplitOutputStream(File.createTempFile("temp", "zip"), splitSize); + output.prepareToWriteUnsplittableContent(splitSize + 1); + } + + @Test + public void splitZipBeginsWithZipSplitSignature() throws IOException { + File tempFile = File.createTempFile("temp", "zip"); + new ZipSplitOutputStream(tempFile, 100 * 1024L); + + InputStream inputStream = new FileInputStream(tempFile); + byte[] buffer = new byte[4]; + inputStream.read(buffer); + + Assert.assertEquals(ByteBuffer.wrap(ZipArchiveOutputStream.DD_SIG).getInt(), ByteBuffer.wrap(buffer).getInt()); + } + + @Test + public void testCreateSplittedFiles() throws IOException { + File testOutputFile = new File(dir, "testCreateSplittedFiles.zip"); + int splitSize = 100 * 1024; /* 100KB */ + ZipSplitOutputStream zipSplitOutputStream = new ZipSplitOutputStream(testOutputFile, splitSize); + + File fileToTest = getFile("COMPRESS-477/split_zip_created_by_zip/zip_to_compare_created_by_zip.zip"); + InputStream inputStream = new FileInputStream(fileToTest); + byte[] buffer = new byte[4096]; + int readLen; + + while ((readLen = inputStream.read(buffer)) > 0) { + zipSplitOutputStream.write(buffer, 0, readLen); + } + + inputStream.close(); + zipSplitOutputStream.close(); + + File zipFile = new File(dir.getPath(), "testCreateSplittedFiles.z01"); + Assert.assertEquals(zipFile.length(), splitSize); + + zipFile = new File(dir.getPath(), "testCreateSplittedFiles.z02"); + Assert.assertEquals(zipFile.length(), splitSize); + + zipFile = new File(dir.getPath(), "testCreateSplittedFiles.z03"); + Assert.assertEquals(zipFile.length(), splitSize); + + zipFile = new File(dir.getPath(), "testCreateSplittedFiles.z04"); + Assert.assertEquals(zipFile.length(), splitSize); + + zipFile = new File(dir.getPath(), "testCreateSplittedFiles.z05"); + Assert.assertEquals(zipFile.length(), splitSize); + + zipFile = new File(dir.getPath(), "testCreateSplittedFiles.zip"); + Assert.assertEquals(zipFile.length(), (fileToTest.length() + 4 - splitSize * 5)); + } +} diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/ZipTestCase.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/ZipTestCase.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/archivers/ZipTestCase.java 2019-08-17 16:01:50.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/archivers/ZipTestCase.java 2020-01-24 18:45:56.000000000 +0000 @@ -27,6 +27,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.channels.Channels; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; @@ -44,6 +47,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.compress.archivers.zip.ZipMethod; +import org.apache.commons.compress.archivers.zip.ZipSplitReadOnlySeekableByteChannel; import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.compress.utils.InputStreamStatistics; import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; @@ -51,6 +55,7 @@ import org.junit.Test; public final class ZipTestCase extends AbstractTestCase { + /** * Archives 2 files and unarchives it again. If the file length of result * and source is the same, it looks like the operations have worked @@ -63,49 +68,39 @@ final File file1 = getFile("test1.xml"); final File file2 = getFile("test2.xml"); - final OutputStream out = new FileOutputStream(output); - ArchiveOutputStream os = null; - try { - os = new ArchiveStreamFactory() - .createArchiveOutputStream("zip", out); - os.putArchiveEntry(new ZipArchiveEntry("testdata/test1.xml")); - IOUtils.copy(new FileInputStream(file1), os); - os.closeArchiveEntry(); - - os.putArchiveEntry(new ZipArchiveEntry("testdata/test2.xml")); - IOUtils.copy(new FileInputStream(file2), os); - os.closeArchiveEntry(); - } finally { - if (os != null) { - os.close(); + try (final OutputStream out = new FileOutputStream(output)) { + try (ArchiveOutputStream os = new ArchiveStreamFactory().createArchiveOutputStream("zip", out)) { + os.putArchiveEntry(new ZipArchiveEntry("testdata/test1.xml")); + try (final FileInputStream input = new FileInputStream(file1)) { + IOUtils.copy(input, os); + } + os.closeArchiveEntry(); + + os.putArchiveEntry(new ZipArchiveEntry("testdata/test2.xml")); + try (final FileInputStream input = new FileInputStream(file2)) { + IOUtils.copy(input, os); + } + os.closeArchiveEntry(); } } - out.close(); // Unarchive the same final List results = new ArrayList<>(); - final InputStream is = new FileInputStream(output); - ArchiveInputStream in = null; - try { - in = new ArchiveStreamFactory() - .createArchiveInputStream("zip", is); - - ZipArchiveEntry entry = null; - while((entry = (ZipArchiveEntry)in.getNextEntry()) != null) { - final File outfile = new File(resultDir.getCanonicalPath() + "/result/" + entry.getName()); - outfile.getParentFile().mkdirs(); - try (OutputStream o = new FileOutputStream(outfile)) { - IOUtils.copy(in, o); + try (final InputStream fileInputStream = new FileInputStream(output)) { + try (ArchiveInputStream archiveInputStream = new ArchiveStreamFactory().createArchiveInputStream("zip", + fileInputStream)) { + ZipArchiveEntry entry = null; + while ((entry = (ZipArchiveEntry) archiveInputStream.getNextEntry()) != null) { + final File outfile = new File(resultDir.getCanonicalPath() + "/result/" + entry.getName()); + outfile.getParentFile().mkdirs(); + try (OutputStream o = new FileOutputStream(outfile)) { + IOUtils.copy(archiveInputStream, o); + } + results.add(outfile); } - results.add(outfile); - } - } finally { - if (in != null) { - in.close(); } } - is.close(); assertEquals(results.size(), 2); File result = results.get(0); @@ -125,34 +120,33 @@ final File file2 = getFile("test2.xml"); final byte[] file1Contents = new byte[(int) file1.length()]; final byte[] file2Contents = new byte[(int) file2.length()]; - IOUtils.readFully(new FileInputStream(file1), file1Contents); - IOUtils.readFully(new FileInputStream(file2), file2Contents); - - SeekableInMemoryByteChannel channel = new SeekableInMemoryByteChannel(); - try (ZipArchiveOutputStream os = new ZipArchiveOutputStream(channel)) { - os.putArchiveEntry(new ZipArchiveEntry("testdata/test1.xml")); - os.write(file1Contents); - os.closeArchiveEntry(); - - os.putArchiveEntry(new ZipArchiveEntry("testdata/test2.xml")); - os.write(file2Contents); - os.closeArchiveEntry(); - } - - // Unarchive the same + IOUtils.read(file1, file1Contents); + IOUtils.read(file2, file2Contents); final List results = new ArrayList<>(); - try (ArchiveInputStream in = new ArchiveStreamFactory() - .createArchiveInputStream("zip", new ByteArrayInputStream(channel.array()))) { - - ZipArchiveEntry entry; - while((entry = (ZipArchiveEntry)in.getNextEntry()) != null) { - byte[] result = new byte[(int) entry.getSize()]; - IOUtils.readFully(in, result); - results.add(result); + try (SeekableInMemoryByteChannel channel = new SeekableInMemoryByteChannel()) { + try (ZipArchiveOutputStream os = new ZipArchiveOutputStream(channel)) { + os.putArchiveEntry(new ZipArchiveEntry("testdata/test1.xml")); + os.write(file1Contents); + os.closeArchiveEntry(); + + os.putArchiveEntry(new ZipArchiveEntry("testdata/test2.xml")); + os.write(file2Contents); + os.closeArchiveEntry(); + } + + // Unarchive the same + try (ArchiveInputStream inputStream = new ArchiveStreamFactory().createArchiveInputStream("zip", + new ByteArrayInputStream(channel.array()))) { + + ZipArchiveEntry entry; + while ((entry = (ZipArchiveEntry) inputStream.getNextEntry()) != null) { + byte[] result = new byte[(int) entry.getSize()]; + IOUtils.readFully(inputStream, result); + results.add(result); + } } } - assertArrayEquals(results.get(0), file1Contents); assertArrayEquals(results.get(1), file2Contents); } @@ -184,8 +178,8 @@ final ArrayList al = new ArrayList<>(); al.add("test1.xml"); al.add("test2.xml"); - try (InputStream is = new FileInputStream(input)) { - checkArchiveContent(new ZipArchiveInputStream(is), al); + try (InputStream fis = new FileInputStream(input)) { + checkArchiveContent(new ZipArchiveInputStream(fis), al); } } @@ -196,11 +190,11 @@ */ @Test public void testTokenizationCompressionMethod() throws IOException { - final ZipFile moby = new ZipFile(getFile("moby.zip")); - final ZipArchiveEntry entry = moby.getEntry("README"); - assertEquals("method", ZipMethod.TOKENIZATION.getCode(), entry.getMethod()); - assertFalse(moby.canReadEntryData(entry)); - moby.close(); + try (final ZipFile moby = new ZipFile(getFile("moby.zip"))) { + final ZipArchiveEntry entry = moby.getEntry("README"); + assertEquals("method", ZipMethod.TOKENIZATION.getCode(), entry.getMethod()); + assertFalse(moby.canReadEntryData(entry)); + } } /** @@ -244,10 +238,8 @@ final List results = new ArrayList<>(); final List expectedExceptions = new ArrayList<>(); - final InputStream is = new FileInputStream(input); - ArchiveInputStream in = null; - try { - in = new ArchiveStreamFactory().createArchiveInputStream("zip", is); + try (final InputStream fis = new FileInputStream(input); + ArchiveInputStream in = new ArchiveStreamFactory().createArchiveInputStream("zip", fis)) { ZipArchiveEntry entry = null; while ((entry = (ZipArchiveEntry) in.getNextEntry()) != null) { @@ -265,12 +257,7 @@ } // nested stream must not be closed here } - } finally { - if (in != null) { - in.close(); - } } - is.close(); assertTrue(results.contains("NestedArchiv.zip")); assertTrue(results.contains("test1.xml")); @@ -361,28 +348,28 @@ @Test public void testCopyRawEntriesFromFile() - throws IOException { + throws IOException { final File[] tmp = createTempDirAndFile(); final File reference = createReferenceFile(tmp[0], Zip64Mode.Never, "expected."); - final File a1 = File.createTempFile("src1.", ".zip", tmp[0]); - try (final ZipArchiveOutputStream zos = new ZipArchiveOutputStream(a1)) { + final File file1 = File.createTempFile("src1.", ".zip", tmp[0]); + try (final ZipArchiveOutputStream zos = new ZipArchiveOutputStream(file1)) { zos.setUseZip64(Zip64Mode.Never); createFirstEntry(zos).close(); } - final File a2 = File.createTempFile("src2.", ".zip", tmp[0]); - try (final ZipArchiveOutputStream zos1 = new ZipArchiveOutputStream(a2)) { + final File file2 = File.createTempFile("src2.", ".zip", tmp[0]); + try (final ZipArchiveOutputStream zos1 = new ZipArchiveOutputStream(file2)) { zos1.setUseZip64(Zip64Mode.Never); createSecondEntry(zos1).close(); } - try (final ZipFile zf1 = new ZipFile(a1); final ZipFile zf2 = new ZipFile(a2)) { + try (final ZipFile zipFile1 = new ZipFile(file1); final ZipFile zipFile2 = new ZipFile(file2)) { final File fileResult = File.createTempFile("file-actual.", ".zip", tmp[0]); try (final ZipArchiveOutputStream zos2 = new ZipArchiveOutputStream(fileResult)) { - zf1.copyRawEntries(zos2, allFilesPredicate); - zf2.copyRawEntries(zos2, allFilesPredicate); + zipFile1.copyRawEntries(zos2, allFilesPredicate); + zipFile2.copyRawEntries(zos2, allFilesPredicate); } // copyRawEntries does not add superfluous zip64 header like regular zip output stream // does when using Zip64Mode.AsNeeded so all the source material has to be Zip64Mode.Never, @@ -402,17 +389,17 @@ createFirstEntry(zos1); } - final File a1 = File.createTempFile("zip64src.", ".zip", tmp[0]); - try (final ZipArchiveOutputStream zos = new ZipArchiveOutputStream(a1)) { + final File file1 = File.createTempFile("zip64src.", ".zip", tmp[0]); + try (final ZipArchiveOutputStream zos = new ZipArchiveOutputStream(file1)) { zos.setUseZip64(Zip64Mode.Always); createFirstEntry(zos).close(); } final File fileResult = File.createTempFile("file-actual.", ".zip", tmp[0]); - try (final ZipFile zf1 = new ZipFile(a1)) { + try (final ZipFile zipFile1 = new ZipFile(file1)) { try (final ZipArchiveOutputStream zos2 = new ZipArchiveOutputStream(fileResult)) { zos2.setUseZip64(Zip64Mode.Always); - zf1.copyRawEntries(zos2, allFilesPredicate); + zipFile1.copyRawEntries(zos2, allFilesPredicate); } assertSameFileContents(reference, fileResult); } @@ -423,8 +410,8 @@ final File[] tmp = createTempDirAndFile(); - final File a1 = File.createTempFile("unixModeBits.", ".zip", tmp[0]); - try (final ZipArchiveOutputStream zos = new ZipArchiveOutputStream(a1)) { + final File file1 = File.createTempFile("unixModeBits.", ".zip", tmp[0]); + try (final ZipArchiveOutputStream zos = new ZipArchiveOutputStream(file1)) { final ZipArchiveEntry archiveEntry = new ZipArchiveEntry("fred"); archiveEntry.setUnixMode(0664); @@ -432,7 +419,7 @@ zos.addRawArchiveEntry(archiveEntry, new ByteArrayInputStream("fud".getBytes())); } - try (final ZipFile zf1 = new ZipFile(a1)) { + try (final ZipFile zf1 = new ZipFile(file1)) { final ZipArchiveEntry fred = zf1.getEntry("fred"); assertEquals(0664, fred.getUnixMode()); } @@ -483,12 +470,11 @@ assertEquals(expectedElement.getExternalAttributes(), actualElement.getExternalAttributes()); assertEquals(expectedElement.getInternalAttributes(), actualElement.getInternalAttributes()); - final InputStream actualIs = actual.getInputStream(actualElement); - final InputStream expectedIs = expected.getInputStream(expectedElement); - IOUtils.readFully(expectedIs, expectedBuf); - IOUtils.readFully(actualIs, actualBuf); - expectedIs.close(); - actualIs.close(); + try (final InputStream actualIs = actual.getInputStream(actualElement); + final InputStream expectedIs = expected.getInputStream(expectedElement)) { + IOUtils.readFully(expectedIs, expectedBuf); + IOUtils.readFully(actualIs, actualBuf); + } Assert.assertArrayEquals(expectedBuf, actualBuf); // Buffers are larger than payload. dont care } @@ -647,6 +633,63 @@ testInputStreamStatistics("COMPRESS-380/COMPRESS-380.zip", expected); } + @Test(expected = IllegalArgumentException.class) + public void buildSplitZipWithTooSmallSizeThrowsException() throws IOException { + new ZipArchiveOutputStream(File.createTempFile("temp", "zip"), 64 * 1024 - 1); + } + + @Test(expected = IllegalArgumentException.class) + public void buildSplitZipWithTooLargeSizeThrowsException() throws IOException { + new ZipArchiveOutputStream(File.createTempFile("temp", "zip"), 4294967295L + 1); + } + + @Test(expected = IOException.class) + public void buildSplitZipWithSegmentAlreadyExistThrowsException() throws IOException { + File directoryToZip = getFilesToZip(); + File outputZipFile = new File(dir, "splitZip.zip"); + long splitSize = 100 * 1024L; /* 100 KB */ + try (final ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(outputZipFile, + splitSize)) { + + // create a file that has the same name of one of the created split segments + File sameNameFile = new File(dir, "splitZip.z01"); + sameNameFile.createNewFile(); + + addFilesToZip(zipArchiveOutputStream, directoryToZip); + } + } + + @Test + public void buildSplitZipTest() throws IOException { + File directoryToZip = getFilesToZip(); + createTestSplitZipSegments(); + + File lastFile = new File(dir, "splitZip.zip"); + try (SeekableByteChannel channel = ZipSplitReadOnlySeekableByteChannel.buildFromLastSplitSegment(lastFile); + InputStream inputStream = Channels.newInputStream(channel); + ZipArchiveInputStream splitInputStream = new ZipArchiveInputStream(inputStream, + StandardCharsets.UTF_8.toString(), true, false, true)) { + + ArchiveEntry entry; + int filesNum = countNonDirectories(directoryToZip); + int filesCount = 0; + while ((entry = splitInputStream.getNextEntry()) != null) { + if (entry.isDirectory()) { + continue; + } + // compare all files one by one + File fileToCompare = new File(entry.getName()); + try (InputStream inputStreamToCompare = new FileInputStream(fileToCompare)) { + assertArrayEquals(IOUtils.toByteArray(splitInputStream), + IOUtils.toByteArray(inputStreamToCompare)); + } + filesCount++; + } + // and the number of files should equal + assertEquals(filesCount, filesNum); + } + } + private void testInputStreamStatistics(String fileName, Map> expectedStatistics) throws IOException, ArchiveException { final File input = getFile(fileName); @@ -701,4 +744,77 @@ final long b = stats.getCompressedCount(); l.add(Arrays.asList(t, b)); } + + private File getFilesToZip() throws IOException { + File originalZipFile = getFile("COMPRESS-477/split_zip_created_by_zip/zip_to_compare_created_by_zip.zip"); + try (ZipFile zipFile = new ZipFile(originalZipFile)) { + Enumeration zipEntries = zipFile.getEntries(); + ZipArchiveEntry zipEntry; + File outputFile; + byte[] buffer; + int readLen; + + while (zipEntries.hasMoreElements()) { + zipEntry = zipEntries.nextElement(); + if (zipEntry.isDirectory()) { + continue; + } + + outputFile = new File(dir, zipEntry.getName()); + if (!outputFile.getParentFile().exists()) { + outputFile.getParentFile().mkdirs(); + } + outputFile = new File(dir, zipEntry.getName()); + + try (InputStream inputStream = zipFile.getInputStream(zipEntry); + OutputStream outputStream = new FileOutputStream(outputFile)) { + buffer = new byte[(int) zipEntry.getSize()]; + while ((readLen = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, readLen); + } + } + } + } + return dir.listFiles()[0]; + } + + private void createTestSplitZipSegments() throws IOException { + File directoryToZip = getFilesToZip(); + File outputZipFile = new File(dir, "splitZip.zip"); + long splitSize = 100 * 1024L; /* 100 KB */ + try (final ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(outputZipFile, + splitSize)) { + addFilesToZip(zipArchiveOutputStream, directoryToZip); + } + } + + private void addFilesToZip(ZipArchiveOutputStream zipArchiveOutputStream, File fileToAdd) throws IOException { + if (fileToAdd.isDirectory()) { + for (File file : fileToAdd.listFiles()) { + addFilesToZip(zipArchiveOutputStream, file); + } + } else { + ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(fileToAdd.getPath()); + zipArchiveEntry.setMethod(ZipEntry.DEFLATED); + + zipArchiveOutputStream.putArchiveEntry(zipArchiveEntry); + try (final FileInputStream input = new FileInputStream(fileToAdd)) { + IOUtils.copy(input, zipArchiveOutputStream); + } + zipArchiveOutputStream.closeArchiveEntry(); + } + } + + private int countNonDirectories(File file) { + if(!file.isDirectory()) { + return 1; + } + + int result = 0; + for (File fileInDirectory : file.listFiles()) { + result += countNonDirectories(fileInDirectory); + } + + return result; + } } diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/compressors/brotli/BrotliCompressorInputStreamTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/compressors/brotli/BrotliCompressorInputStreamTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/compressors/brotli/BrotliCompressorInputStreamTest.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/compressors/brotli/BrotliCompressorInputStreamTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -45,13 +45,12 @@ final File input = getFile("brotli.testdata.compressed"); final File expected = getFile("brotli.testdata.uncompressed"); try (InputStream inputStream = new FileInputStream(input); - InputStream expectedStream = new FileInputStream(expected); - BrotliCompressorInputStream brotliInputStream = new BrotliCompressorInputStream(inputStream)) { + BrotliCompressorInputStream brotliInputStream = new BrotliCompressorInputStream(inputStream)) { final byte[] b = new byte[20]; - IOUtils.readFully(expectedStream, b); + IOUtils.read(expected, b); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); int readByte = -1; - while((readByte = brotliInputStream.read()) != -1) { + while ((readByte = brotliInputStream.read()) != -1) { bos.write(readByte); } Assert.assertArrayEquals(b, bos.toByteArray()); diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2NSelectorsOverflowTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2NSelectorsOverflowTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2NSelectorsOverflowTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2NSelectorsOverflowTest.java 2020-01-21 12:21:21.000000000 +0000 @@ -0,0 +1,47 @@ +/* + * 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 + * + * http://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.compressors.bzip2; + +import org.apache.commons.compress.AbstractTestCase; +import org.junit.Test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; + +public class BZip2NSelectorsOverflowTest extends AbstractTestCase { + + /** + * See https://sourceware.org/ml/bzip2-devel/2019-q3/msg00007.html + */ + @Test + public void shouldDecompressBlockWithNSelectorOverflow() throws Exception { + final File toDecompress = getFile("lbzip2_32767.bz2"); + try (final InputStream is = new FileInputStream(toDecompress); + final BZip2CompressorInputStream in = new BZip2CompressorInputStream(is)) { + int l = 0; + while (in.read() != -1) { + l++; + } + assertEquals(5, l); + } + } +} diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/compressors/deflate64/HuffmanDecoderTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/compressors/deflate64/HuffmanDecoderTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/compressors/deflate64/HuffmanDecoderTest.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/compressors/deflate64/HuffmanDecoderTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -214,9 +214,6 @@ assertEquals("Hello World\nHello World\nHello World\nHello World\n", new String(result, 0, len)); len = decoder.decode(result); - assertEquals(0, len); - - len = decoder.decode(result); assertEquals(-1, len); } } diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStreamTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStreamTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStreamTest.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/compressors/lz4/FramedLZ4CompressorInputStreamTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -46,10 +46,7 @@ public void testMatches() throws IOException { assertFalse(FramedLZ4CompressorInputStream.matches(new byte[10], 4)); final byte[] b = new byte[12]; - final File input = getFile("bla.tar.lz4"); - try (FileInputStream in = new FileInputStream(input)) { - IOUtils.readFully(in, b); - } + IOUtils.read(getFile("bla.tar.lz4"), b); assertFalse(FramedLZ4CompressorInputStream.matches(b, 3)); assertTrue(FramedLZ4CompressorInputStream.matches(b, 4)); assertTrue(FramedLZ4CompressorInputStream.matches(b, 5)); diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStreamTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStreamTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStreamTest.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/compressors/snappy/FramedSnappyCompressorInputStreamTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -40,10 +40,7 @@ public void testMatches() throws IOException { assertFalse(FramedSnappyCompressorInputStream.matches(new byte[10], 10)); final byte[] b = new byte[12]; - final File input = getFile("bla.tar.sz"); - try (FileInputStream in = new FileInputStream(input)) { - IOUtils.readFully(in, b); - } + IOUtils.read(getFile("bla.tar.sz"), b); assertFalse(FramedSnappyCompressorInputStream.matches(b, 9)); assertTrue(FramedSnappyCompressorInputStream.matches(b, 10)); assertTrue(FramedSnappyCompressorInputStream.matches(b, 12)); diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/compressors/zstandard/ZstdCompressorInputStreamTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/compressors/zstandard/ZstdCompressorInputStreamTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/compressors/zstandard/ZstdCompressorInputStreamTest.java 2019-08-09 15:51:09.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/compressors/zstandard/ZstdCompressorInputStreamTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -47,10 +47,9 @@ final File input = getFile("zstandard.testdata.zst"); final File expected = getFile("zstandard.testdata"); try (InputStream inputStream = new FileInputStream(input); - InputStream expectedStream = new FileInputStream(expected); ZstdCompressorInputStream zstdInputStream = new ZstdCompressorInputStream(inputStream)) { final byte[] b = new byte[97]; - IOUtils.readFully(expectedStream, b); + IOUtils.read(expected, b); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); int readByte = -1; while((readByte = zstdInputStream.read()) != -1) { diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/OsgiITest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/OsgiITest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/OsgiITest.java 2018-05-23 12:50:54.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/OsgiITest.java 2020-02-05 04:59:32.000000000 +0000 @@ -18,6 +18,7 @@ */ package org.apache.commons.compress; +import static org.junit.Assert.assertTrue; import static org.ops4j.pax.exam.CoreOptions.bundle; import static org.ops4j.pax.exam.CoreOptions.composite; import static org.ops4j.pax.exam.CoreOptions.mavenBundle; @@ -28,14 +29,25 @@ import org.ops4j.pax.exam.Configuration; import org.ops4j.pax.exam.Option; import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +import javax.inject.Inject; @RunWith(PaxExam.class) public class OsgiITest { + private static final String EXPECTED_BUNDLE_NAME = "org.apache.commons.commons-compress"; + + @Inject + private BundleContext ctx; + @Configuration public Option[] config() { return new Option[] { systemProperty("pax.exam.osgi.unresolved.fail").value("true"), + systemProperty("org.ops4j.pax.url.mvn.useFallbackRepositories").value("false"), + systemProperty("org.ops4j.pax.url.mvn.repositories").value("https://repo.maven.apache.org/maven2"), mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.scr") .version("2.0.14"), mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.configadmin") @@ -51,5 +63,18 @@ @Test public void loadBundle() { + final StringBuilder bundles = new StringBuilder(); + boolean foundCompressBundle = false, first = true; + for (final Bundle b : ctx.getBundles()) { + final String symbolicName = b.getSymbolicName(); + foundCompressBundle |= EXPECTED_BUNDLE_NAME.equals(symbolicName); + if (!first) { + bundles.append(", "); + } + first = false; + bundles.append(symbolicName); + } + assertTrue("Expected to find bundle " + EXPECTED_BUNDLE_NAME + " in " + bundles, + foundCompressBundle); } } diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/utils/FileNameUtilsTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/utils/FileNameUtilsTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/utils/FileNameUtilsTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/utils/FileNameUtilsTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -0,0 +1,53 @@ +/* + * 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 + * + * http://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.utils; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class FileNameUtilsTest { + + @Test + public void getExtensionBaseCases() { + assertEquals("foo", FileNameUtils.getExtension("a/b/c/bar.foo")); + assertEquals("", FileNameUtils.getExtension("foo")); + } + + @Test + public void getExtensionCornerCases() { + assertNull(FileNameUtils.getExtension(null)); + assertEquals("", FileNameUtils.getExtension("foo.")); + assertEquals("foo", FileNameUtils.getExtension("bar/.foo")); + } + + @Test + public void getBaseNameBaseCases() { + assertEquals("bar", FileNameUtils.getBaseName("a/b/c/bar.foo")); + assertEquals("foo", FileNameUtils.getBaseName("foo")); + } + + @Test + public void getBaseNameCornerCases() { + assertNull(FileNameUtils.getBaseName(null)); + assertEquals("foo", FileNameUtils.getBaseName("foo.")); + assertEquals("", FileNameUtils.getBaseName("bar/.foo")); + } +} diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/utils/MultiReadOnlySeekableByteChannelTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/utils/MultiReadOnlySeekableByteChannelTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/utils/MultiReadOnlySeekableByteChannelTest.java 2019-08-18 15:27:29.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/utils/MultiReadOnlySeekableByteChannelTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -29,6 +29,7 @@ import java.util.List; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -49,7 +50,7 @@ } @Test - public void forSeekableByteChannelsThrowsOnNullArg() { + public void forSeekableByteChannelsThrowsOnNullArg() throws IOException { thrown.expect(NullPointerException.class); MultiReadOnlySeekableByteChannel.forSeekableByteChannels(null); } @@ -61,7 +62,7 @@ } @Test - public void forSeekableByteChannelsReturnsIdentityForSingleElement() { + public void forSeekableByteChannelsReturnsIdentityForSingleElement() throws IOException { final SeekableByteChannel e = makeEmpty(); final SeekableByteChannel m = MultiReadOnlySeekableByteChannel.forSeekableByteChannels(e); Assert.assertSame(e, m); @@ -106,7 +107,7 @@ } @Test - public void closesAllAndThrowsExceptionIfCloseThrows() { + public void closesAllAndThrowsExceptionIfCloseThrows() throws IOException { SeekableByteChannel[] ts = new ThrowingSeekableByteChannel[] { new ThrowingSeekableByteChannel(), new ThrowingSeekableByteChannel() @@ -150,7 +151,7 @@ return new SeekableInMemoryByteChannel(arr); } - private SeekableByteChannel makeMulti(byte[][] arr) { + private SeekableByteChannel makeMulti(byte[][] arr) throws IOException { SeekableByteChannel[] s = new SeekableByteChannel[arr.length]; for (int i = 0; i < s.length; i++) { s[i] = makeSingle(arr[i]); @@ -291,4 +292,96 @@ return this; } } + + // Contract Tests added in response to https://issues.apache.org/jira/browse/COMPRESS-499 + + private SeekableByteChannel testChannel() { + return MultiReadOnlySeekableByteChannel + .forSeekableByteChannels(makeEmpty(), makeEmpty()); + } + + // https://docs.oracle.com/javase/7/docs/api/java/io/Closeable.html#close() + + /* + * If the stream is already closed then invoking this method has no effect. + */ + @Test + public void closeIsIdempotent() throws Exception { + try (SeekableByteChannel c = testChannel()) { + c.close(); + Assert.assertFalse(c.isOpen()); + c.close(); + Assert.assertFalse(c.isOpen()); + } + } + + // https://docs.oracle.com/javase/7/docs/api/java/nio/channels/SeekableByteChannel.html#position() + + /* + * ClosedChannelException - If this channel is closed + */ + @Test + @Ignore("we deliberately violate the spec") + public void throwsClosedChannelExceptionWhenPositionIsReadOnClosedChannel() throws Exception { + thrown.expect(ClosedChannelException.class); + try (SeekableByteChannel c = testChannel()) { + c.close(); + c.position(); + } + } + + // https://docs.oracle.com/javase/7/docs/api/java/nio/channels/SeekableByteChannel.html#size() + + /* + * ClosedChannelException - If this channel is closed + */ + @Test + public void throwsClosedChannelExceptionWhenSizeIsReadOnClosedChannel() throws Exception { + thrown.expect(ClosedChannelException.class); + try (SeekableByteChannel c = testChannel()) { + c.close(); + c.size(); + } + } + + // https://docs.oracle.com/javase/7/docs/api/java/nio/channels/SeekableByteChannel.html#position(long) + + /* + * ClosedChannelException - If this channel is closed + */ + @Test + public void throwsClosedChannelExceptionWhenPositionIsSetOnClosedChannel() throws Exception { + thrown.expect(ClosedChannelException.class); + try (SeekableByteChannel c = testChannel()) { + c.close(); + c.position(0); + } + } + + /* + * Setting the position to a value that is greater than the current size is legal but does not change the size of + * the entity. A later attempt to read bytes at such a position will immediately return an end-of-file + * indication + */ + @Test + public void readingFromAPositionAfterEndReturnsEOF() throws Exception { + try (SeekableByteChannel c = testChannel()) { + c.position(2); + Assert.assertEquals(2, c.position()); + ByteBuffer readBuffer = ByteBuffer.allocate(5); + Assert.assertEquals(-1, c.read(readBuffer)); + } + } + + /* + * IllegalArgumentException - If the new position is negative + */ + @Test + public void throwsIllegalArgumentExceptionWhenPositionIsSetToANegativeValue() throws Exception { + thrown.expect(IllegalArgumentException.class); + try (SeekableByteChannel c = testChannel()) { + c.position(-1); + } + } + } diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/utils/SeekableInMemoryByteChannelTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/utils/SeekableInMemoryByteChannelTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/utils/SeekableInMemoryByteChannelTest.java 2018-05-02 20:17:13.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/utils/SeekableInMemoryByteChannelTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -18,17 +18,20 @@ */ package org.apache.commons.compress.utils; +import org.junit.Ignore; import org.junit.Test; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; +import java.nio.channels.SeekableByteChannel; import java.nio.charset.Charset; import java.util.Arrays; import static org.apache.commons.compress.utils.CharsetNames.UTF_8; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; public class SeekableInMemoryByteChannelTest { @@ -88,6 +91,7 @@ //then assertEquals(0L, readBuffer.position()); assertEquals(-1, readCount); + assertEquals(-1, c.read(readBuffer)); c.close(); } @@ -177,7 +181,7 @@ //then assertEquals(4L, posAtFour); assertEquals(c.size(), posAtTheEnd); - assertEquals(posPastTheEnd, posPastTheEnd); + assertEquals(testData.length + 1L, posPastTheEnd); c.close(); } @@ -190,13 +194,223 @@ c.close(); } - @Test(expected = ClosedChannelException.class) - public void shouldThrowExceptionWhenSettingPositionOnClosedChannel() throws IOException { + @Test(expected = IllegalArgumentException.class) + public void shouldThrowExceptionWhenTruncatingToIncorrectSize() throws IOException { //given SeekableInMemoryByteChannel c = new SeekableInMemoryByteChannel(); //when + c.truncate(Integer.MAX_VALUE + 1L); c.close(); - c.position(1L); + } + + // Contract Tests added in response to https://issues.apache.org/jira/browse/COMPRESS-499 + + // https://docs.oracle.com/javase/7/docs/api/java/io/Closeable.html#close() + + /* + * If the stream is already closed then invoking this method has no effect. + */ + @Test + public void closeIsIdempotent() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel()) { + c.close(); + assertFalse(c.isOpen()); + c.close(); + assertFalse(c.isOpen()); + } + } + + // https://docs.oracle.com/javase/7/docs/api/java/nio/channels/SeekableByteChannel.html#position() + + /* + * ClosedChannelException - If this channel is closed + */ + @Test(expected = ClosedChannelException.class) + @Ignore("we deliberately violate the spec") + public void throwsClosedChannelExceptionWhenPositionIsReadOnClosedChannel() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel()) { + c.close(); + c.position(); + } + } + + // https://docs.oracle.com/javase/7/docs/api/java/nio/channels/SeekableByteChannel.html#size() + + /* + * ClosedChannelException - If this channel is closed + */ + @Test(expected = ClosedChannelException.class) + @Ignore("we deliberately violate the spec") + public void throwsClosedChannelExceptionWhenSizeIsReadOnClosedChannel() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel()) { + c.close(); + c.size(); + } + } + + // https://docs.oracle.com/javase/7/docs/api/java/nio/channels/SeekableByteChannel.html#position(long) + + /* + * ClosedChannelException - If this channel is closed + */ + @Test(expected = ClosedChannelException.class) + public void throwsClosedChannelExceptionWhenPositionIsSetOnClosedChannel() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel()) { + c.close(); + c.position(0); + } + } + + /* + * Setting the position to a value that is greater than the current size is legal but does not change the size of + * the entity. A later attempt to read bytes at such a position will immediately return an end-of-file + * indication + */ + @Test + public void readingFromAPositionAfterEndReturnsEOF() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel()) { + c.position(2); + assertEquals(2, c.position()); + ByteBuffer readBuffer = ByteBuffer.allocate(5); + assertEquals(-1, c.read(readBuffer)); + } + } + + /* + * Setting the position to a value that is greater than the current size is legal but does not change the size of + * the entity. A later attempt to write bytes at such a position will cause the entity to grow to accommodate the + * new bytes; the values of any bytes between the previous end-of-file and the newly-written bytes are + * unspecified. + */ + public void writingToAPositionAfterEndGrowsChannel() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel()) { + c.position(2); + assertEquals(2, c.position()); + ByteBuffer inData = ByteBuffer.wrap(testData); + assertEquals(testData.length, c.write(inData)); + assertEquals(testData.length + 2, c.size()); + + c.position(2); + ByteBuffer readBuffer = ByteBuffer.allocate(testData.length); + c.read(readBuffer); + assertArrayEquals(testData, Arrays.copyOf(readBuffer.array(), testData.length)); + } + } + + /* + * IllegalArgumentException - If the new position is negative + */ + @Test(expected = IllegalArgumentException.class) + public void throwsIllegalArgumentExceptionWhenPositionIsSetToANegativeValue() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel()) { + c.position(-1); + } + } + + // https://docs.oracle.com/javase/7/docs/api/java/nio/channels/SeekableByteChannel.html#truncate(long) + + /* + * If the given size is greater than or equal to the current size then the entity is not modified. + */ + @Test + public void truncateToCurrentSizeDoesntChangeAnything() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel(testData)) { + assertEquals(testData.length, c.size()); + c.truncate(testData.length); + assertEquals(testData.length, c.size()); + ByteBuffer readBuffer = ByteBuffer.allocate(testData.length); + assertEquals(testData.length, c.read(readBuffer)); + assertArrayEquals(testData, Arrays.copyOf(readBuffer.array(), testData.length)); + } + } + + /* + * If the given size is greater than or equal to the current size then the entity is not modified. + */ + @Test + public void truncateToBiggerSizeDoesntChangeAnything() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel(testData)) { + assertEquals(testData.length, c.size()); + c.truncate(testData.length + 1); + assertEquals(testData.length, c.size()); + ByteBuffer readBuffer = ByteBuffer.allocate(testData.length); + assertEquals(testData.length, c.read(readBuffer)); + assertArrayEquals(testData, Arrays.copyOf(readBuffer.array(), testData.length)); + } + } + + /* + * In either case, if the current position is greater than the given size then it is set to that size. + */ + @Test + public void truncateDoesntChangeSmallPosition() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel(testData)) { + c.position(1); + c.truncate(testData.length - 1); + assertEquals(testData.length - 1, c.size()); + assertEquals(1, c.position()); + } + } + + /* + * In either case, if the current position is greater than the given size then it is set to that size. + */ + @Test + public void truncateMovesPositionWhenShrinkingBeyondPosition() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel(testData)) { + c.position(4); + c.truncate(3); + assertEquals(3, c.size()); + assertEquals(3, c.position()); + } + } + + /* + * In either case, if the current position is greater than the given size then it is set to that size. + */ + @Test + public void truncateMovesPositionWhenNotResizingButPositionBiggerThanSize() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel(testData)) { + c.position(2 * testData.length); + c.truncate(testData.length); + assertEquals(testData.length, c.size()); + assertEquals(testData.length, c.position()); + } + } + + /* + * In either case, if the current position is greater than the given size then it is set to that size. + */ + @Test + public void truncateMovesPositionWhenNewSizeIsBiggerThanSizeAndPositionIsEvenBigger() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel(testData)) { + c.position(2 * testData.length); + c.truncate(testData.length + 1); + assertEquals(testData.length, c.size()); + assertEquals(testData.length + 1, c.position()); + } + } + + /* + * IllegalArgumentException - If the new position is negative + */ + @Test(expected = IllegalArgumentException.class) + public void throwsIllegalArgumentExceptionWhenTruncatingToANegativeSize() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel()) { + c.truncate(-1); + } + } + + /* + * ClosedChannelException - If this channel is closed + */ + @Test(expected = ClosedChannelException.class) + @Ignore("we deliberately violate the spec") + public void throwsClosedChannelExceptionWhenTruncateIsCalledOnClosedChannel() throws Exception { + try (SeekableByteChannel c = new SeekableInMemoryByteChannel()) { + c.close(); + c.truncate(0); + } } } diff -Nru libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/utils/ZipSplitReadOnlySeekableByteChannelTest.java libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/utils/ZipSplitReadOnlySeekableByteChannelTest.java --- libcommons-compress-java-1.19/src/test/java/org/apache/commons/compress/utils/ZipSplitReadOnlySeekableByteChannelTest.java 1970-01-01 00:00:00.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/java/org/apache/commons/compress/utils/ZipSplitReadOnlySeekableByteChannelTest.java 2020-01-07 14:40:25.000000000 +0000 @@ -0,0 +1,181 @@ +/* + * 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 + * + * http://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.utils; + +import org.apache.commons.compress.archivers.zip.ZipSplitReadOnlySeekableByteChannel; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.io.File; +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static org.apache.commons.compress.AbstractTestCase.getFile; + +public class ZipSplitReadOnlySeekableByteChannelTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void constructorThrowsOnNullArg() throws IOException { + thrown.expect(NullPointerException.class); + new ZipSplitReadOnlySeekableByteChannel(null); + } + + @Test + public void constructorThrowsOnNonSplitZipFiles() throws IOException { + thrown.expect(IOException.class); + List channels = new ArrayList<>(); + File file = getFile("COMPRESS-189.zip"); + channels.add(Files.newByteChannel(file.toPath(), StandardOpenOption.READ)); + new ZipSplitReadOnlySeekableByteChannel(channels); + } + + @Test + public void channelsPositionIsZeroAfterConstructor() throws IOException { + List channels = getSplitZipChannels(); + new ZipSplitReadOnlySeekableByteChannel(channels); + for (SeekableByteChannel channel : channels) { + Assert.assertEquals(0, channel.position()); + } + } + + @Test + public void forOrderedSeekableByteChannelsThrowsOnNullArg() throws IOException { + thrown.expect(NullPointerException.class); + ZipSplitReadOnlySeekableByteChannel.forOrderedSeekableByteChannels(null); + } + + @Test + public void forOrderedSeekableByteChannelsOfTwoParametersThrowsOnNullArg() throws IOException { + thrown.expect(NullPointerException.class); + ZipSplitReadOnlySeekableByteChannel.forOrderedSeekableByteChannels(null, null); + } + + @Test + public void forOrderedSeekableByteChannelsReturnCorrectClass() throws IOException { + File file1 = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z01"); + SeekableByteChannel firstChannel = Files.newByteChannel(file1.toPath(), StandardOpenOption.READ); + + File file2 = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z02"); + SeekableByteChannel secondChannel = Files.newByteChannel(file2.toPath(), StandardOpenOption.READ); + + File lastFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.zip"); + SeekableByteChannel lastChannel = Files.newByteChannel(lastFile.toPath(), StandardOpenOption.READ); + + List channels = new ArrayList<>(); + channels.add(firstChannel); + channels.add(secondChannel); + + SeekableByteChannel channel = ZipSplitReadOnlySeekableByteChannel.forOrderedSeekableByteChannels(lastChannel, channels); + Assert.assertTrue(channel instanceof ZipSplitReadOnlySeekableByteChannel); + + channel = ZipSplitReadOnlySeekableByteChannel.forOrderedSeekableByteChannels(firstChannel, secondChannel, lastChannel); + Assert.assertTrue(channel instanceof ZipSplitReadOnlySeekableByteChannel); + } + + @Test + public void forOrderedSeekableByteChannelsReturnsIdentityForSingleElement() throws IOException { + SeekableByteChannel emptyChannel = new SeekableInMemoryByteChannel(new byte[0]); + final SeekableByteChannel channel = ZipSplitReadOnlySeekableByteChannel.forOrderedSeekableByteChannels(emptyChannel); + Assert.assertSame(emptyChannel, channel); + } + + @Test + public void forFilesThrowsOnNullArg() throws IOException { + thrown.expect(NullPointerException.class); + ZipSplitReadOnlySeekableByteChannel.forFiles(null); + } + + @Test + public void forFilesOfTwoParametersThrowsOnNullArg() throws IOException { + thrown.expect(NullPointerException.class); + ZipSplitReadOnlySeekableByteChannel.forFiles(null, null); + } + + @Test + public void forFilesReturnCorrectClass() throws IOException { + File firstFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z01"); + File secondFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z02"); + File lastFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z01"); + + ArrayList list = new ArrayList<>(); + list.add(firstFile); + list.add(secondFile); + + SeekableByteChannel channel = ZipSplitReadOnlySeekableByteChannel.forFiles(lastFile, list); + Assert.assertTrue(channel instanceof ZipSplitReadOnlySeekableByteChannel); + + channel = ZipSplitReadOnlySeekableByteChannel.forFiles(firstFile, secondFile, lastFile); + Assert.assertTrue(channel instanceof ZipSplitReadOnlySeekableByteChannel); + } + + @Test + public void buildFromLastSplitSegmentThrowsOnNotZipFile() throws IOException { + thrown.expect(IllegalArgumentException.class); + File lastFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z01"); + ZipSplitReadOnlySeekableByteChannel.buildFromLastSplitSegment(lastFile); + } + + @Test + public void positionToSomeZipSplitSegment() throws IOException { + File firstFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z01"); + int firstFileSize = (int) firstFile.length(); + + File secondFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z02"); + int secondFileSize = (int) secondFile.length(); + + File lastFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.zip"); + int lastFileSize = (int) lastFile.length(); + + Random random = new Random(); + int randomDiskNumber = random.nextInt(3); + int randomOffset = randomDiskNumber < 2 ? random.nextInt(firstFileSize) : random.nextInt(lastFileSize); + + ZipSplitReadOnlySeekableByteChannel channel = (ZipSplitReadOnlySeekableByteChannel) ZipSplitReadOnlySeekableByteChannel.buildFromLastSplitSegment(lastFile); + channel.position(randomDiskNumber, randomOffset); + long expectedPosition = randomOffset; + + expectedPosition += randomDiskNumber > 0 ? firstFileSize : 0; + expectedPosition += randomDiskNumber > 1 ? secondFileSize : 0; + + Assert.assertEquals(expectedPosition, channel.position()); + } + + private List getSplitZipChannels() throws IOException { + List channels = new ArrayList<>(); + File file1 = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z01"); + channels.add(Files.newByteChannel(file1.toPath(), StandardOpenOption.READ)); + + File file2 = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z02"); + channels.add(Files.newByteChannel(file2.toPath(), StandardOpenOption.READ)); + + File lastFile = getFile("COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.zip"); + channels.add(Files.newByteChannel(lastFile.toPath(), StandardOpenOption.READ)); + + return channels; + } +} Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/bla.noendheaderoffset.7z and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/bla.noendheaderoffset.7z differ diff -Nru libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_winrar/file_to_compare_1 libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_winrar/file_to_compare_1 --- libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_winrar/file_to_compare_1 1970-01-01 00:00:00.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_winrar/file_to_compare_1 2020-01-07 14:40:25.000000000 +0000 @@ -0,0 +1,1297 @@ +/* + * 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 + * + * http://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.zip; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.zip.CRC32; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; + +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.ArchiveInputStream; +import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; +import org.apache.commons.compress.compressors.deflate64.Deflate64CompressorInputStream; +import org.apache.commons.compress.utils.ArchiveUtils; +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.compress.utils.InputStreamStatistics; + +import static org.apache.commons.compress.archivers.zip.ZipConstants.DWORD; +import static org.apache.commons.compress.archivers.zip.ZipConstants.SHORT; +import static org.apache.commons.compress.archivers.zip.ZipConstants.WORD; +import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC; + +/** + * Implements an input stream that can read Zip archives. + * + *

      As of Apache Commons Compress it transparently supports Zip64 + * extensions and thus individual entries and archives larger than 4 + * GB or with more than 65536 entries.

      + * + *

      The {@link ZipFile} class is preferred when reading from files + * as {@link ZipArchiveInputStream} is limited by not being able to + * read the central directory header before returning entries. In + * particular {@link ZipArchiveInputStream}

      + * + *
        + * + *
      • may return entries that are not part of the central directory + * at all and shouldn't be considered part of the archive.
      • + * + *
      • may return several entries with the same name.
      • + * + *
      • will not return internal or external attributes.
      • + * + *
      • may return incomplete extra field data.
      • + * + *
      • may return unknown sizes and CRC values for entries until the + * next entry has been reached if the archive uses the data + * descriptor feature.
      • + * + *
      + * + * @see ZipFile + * @NotThreadSafe + */ +public class ZipArchiveInputStream extends ArchiveInputStream implements InputStreamStatistics { + + /** The zip encoding to use for file names and the file comment. */ + private final ZipEncoding zipEncoding; + + // the provided encoding (for unit tests) + final String encoding; + + /** Whether to look for and use Unicode extra fields. */ + private final boolean useUnicodeExtraFields; + + /** Wrapped stream, will always be a PushbackInputStream. */ + private final InputStream in; + + /** Inflater used for all deflated entries. */ + private final Inflater inf = new Inflater(true); + + /** Buffer used to read from the wrapped stream. */ + private final ByteBuffer buf = ByteBuffer.allocate(ZipArchiveOutputStream.BUFFER_SIZE); + + /** The entry that is currently being read. */ + private CurrentEntry current = null; + + /** Whether the stream has been closed. */ + private boolean closed = false; + + /** Whether the stream has reached the central directory - and thus found all entries. */ + private boolean hitCentralDirectory = false; + + /** + * When reading a stored entry that uses the data descriptor this + * stream has to read the full entry and caches it. This is the + * cache. + */ + private ByteArrayInputStream lastStoredEntry = null; + + /** Whether the stream will try to read STORED entries that use a data descriptor. */ + private boolean allowStoredEntriesWithDataDescriptor = false; + + /** Count decompressed bytes for current entry */ + private long uncompressedCount = 0; + + private static final int LFH_LEN = 30; + /* + local file header signature WORD + version needed to extract SHORT + general purpose bit flag SHORT + compression method SHORT + last mod file time SHORT + last mod file date SHORT + crc-32 WORD + compressed size WORD + uncompressed size WORD + file name length SHORT + extra field length SHORT + */ + + private static final int CFH_LEN = 46; + /* + central file header signature WORD + version made by SHORT + version needed to extract SHORT + general purpose bit flag SHORT + compression method SHORT + last mod file time SHORT + last mod file date SHORT + crc-32 WORD + compressed size WORD + uncompressed size WORD + file name length SHORT + extra field length SHORT + file comment length SHORT + disk number start SHORT + internal file attributes SHORT + external file attributes WORD + relative offset of local header WORD + */ + + private static final long TWO_EXP_32 = ZIP64_MAGIC + 1; + + // cached buffers - must only be used locally in the class (COMPRESS-172 - reduce garbage collection) + private final byte[] lfhBuf = new byte[LFH_LEN]; + private final byte[] skipBuf = new byte[1024]; + private final byte[] shortBuf = new byte[SHORT]; + private final byte[] wordBuf = new byte[WORD]; + private final byte[] twoDwordBuf = new byte[2 * DWORD]; + + private int entriesRead = 0; + + /** + * Create an instance using UTF-8 encoding + * @param inputStream the stream to wrap + */ + public ZipArchiveInputStream(final InputStream inputStream) { + this(inputStream, ZipEncodingHelper.UTF8); + } + + /** + * Create an instance using the specified encoding + * @param inputStream the stream to wrap + * @param encoding the encoding to use for file names, use null + * for the platform's default encoding + * @since 1.5 + */ + public ZipArchiveInputStream(final InputStream inputStream, final String encoding) { + this(inputStream, encoding, true); + } + + /** + * Create an instance using the specified encoding + * @param inputStream the stream to wrap + * @param encoding the encoding to use for file names, use null + * for the platform's default encoding + * @param useUnicodeExtraFields whether to use InfoZIP Unicode + * Extra Fields (if present) to set the file names. + */ + public ZipArchiveInputStream(final InputStream inputStream, final String encoding, final boolean useUnicodeExtraFields) { + this(inputStream, encoding, useUnicodeExtraFields, false); + } + + /** + * Create an instance using the specified encoding + * @param inputStream the stream to wrap + * @param encoding the encoding to use for file names, use null + * for the platform's default encoding + * @param useUnicodeExtraFields whether to use InfoZIP Unicode + * Extra Fields (if present) to set the file names. + * @param allowStoredEntriesWithDataDescriptor whether the stream + * will try to read STORED entries that use a data descriptor + * @since 1.1 + */ + public ZipArchiveInputStream(final InputStream inputStream, + final String encoding, + final boolean useUnicodeExtraFields, + final boolean allowStoredEntriesWithDataDescriptor) { + this.encoding = encoding; + zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); + this.useUnicodeExtraFields = useUnicodeExtraFields; + in = new PushbackInputStream(inputStream, buf.capacity()); + this.allowStoredEntriesWithDataDescriptor = + allowStoredEntriesWithDataDescriptor; + // haven't read anything so far + buf.limit(0); + } + + public ZipArchiveEntry getNextZipEntry() throws IOException { + uncompressedCount = 0; + + boolean firstEntry = true; + if (closed || hitCentralDirectory) { + return null; + } + if (current != null) { + closeEntry(); + firstEntry = false; + } + + long currentHeaderOffset = getBytesRead(); + try { + if (firstEntry) { + // split archives have a special signature before the + // first local file header - look for it and fail with + // the appropriate error message if this is a split + // archive. + readFirstLocalFileHeader(lfhBuf); + } else { + readFully(lfhBuf); + } + } catch (final EOFException e) { //NOSONAR + return null; + } + + final ZipLong sig = new ZipLong(lfhBuf); + if (!sig.equals(ZipLong.LFH_SIG)) { + if (sig.equals(ZipLong.CFH_SIG) || sig.equals(ZipLong.AED_SIG) || isApkSigningBlock(lfhBuf)) { + hitCentralDirectory = true; + skipRemainderOfArchive(); + return null; + } + throw new ZipException(String.format("Unexpected record signature: 0X%X", sig.getValue())); + } + + int off = WORD; + current = new CurrentEntry(); + + final int versionMadeBy = ZipShort.getValue(lfhBuf, off); + off += SHORT; + current.entry.setPlatform((versionMadeBy >> ZipFile.BYTE_SHIFT) & ZipFile.NIBLET_MASK); + + final GeneralPurposeBit gpFlag = GeneralPurposeBit.parse(lfhBuf, off); + final boolean hasUTF8Flag = gpFlag.usesUTF8ForNames(); + final ZipEncoding entryEncoding = hasUTF8Flag ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding; + current.hasDataDescriptor = gpFlag.usesDataDescriptor(); + current.entry.setGeneralPurposeBit(gpFlag); + + off += SHORT; + + current.entry.setMethod(ZipShort.getValue(lfhBuf, off)); + off += SHORT; + + final long time = ZipUtil.dosToJavaTime(ZipLong.getValue(lfhBuf, off)); + current.entry.setTime(time); + off += WORD; + + ZipLong size = null, cSize = null; + if (!current.hasDataDescriptor) { + current.entry.setCrc(ZipLong.getValue(lfhBuf, off)); + off += WORD; + + cSize = new ZipLong(lfhBuf, off); + off += WORD; + + size = new ZipLong(lfhBuf, off); + off += WORD; + } else { + off += 3 * WORD; + } + + final int fileNameLen = ZipShort.getValue(lfhBuf, off); + + off += SHORT; + + final int extraLen = ZipShort.getValue(lfhBuf, off); + off += SHORT; // NOSONAR - assignment as documentation + + final byte[] fileName = new byte[fileNameLen]; + readFully(fileName); + current.entry.setName(entryEncoding.decode(fileName), fileName); + if (hasUTF8Flag) { + current.entry.setNameSource(ZipArchiveEntry.NameSource.NAME_WITH_EFS_FLAG); + } + + final byte[] extraData = new byte[extraLen]; + readFully(extraData); + current.entry.setExtra(extraData); + + if (!hasUTF8Flag && useUnicodeExtraFields) { + ZipUtil.setNameAndCommentFromExtraFields(current.entry, fileName, null); + } + + processZip64Extra(size, cSize); + + current.entry.setLocalHeaderOffset(currentHeaderOffset); + current.entry.setDataOffset(getBytesRead()); + current.entry.setStreamContiguous(true); + + ZipMethod m = ZipMethod.getMethodByCode(current.entry.getMethod()); + if (current.entry.getCompressedSize() != ArchiveEntry.SIZE_UNKNOWN) { + if (ZipUtil.canHandleEntryData(current.entry) && m != ZipMethod.STORED && m != ZipMethod.DEFLATED) { + InputStream bis = new BoundedInputStream(in, current.entry.getCompressedSize()); + switch (m) { + case UNSHRINKING: + current.in = new UnshrinkingInputStream(bis); + break; + case IMPLODING: + current.in = new ExplodingInputStream( + current.entry.getGeneralPurposeBit().getSlidingDictionarySize(), + current.entry.getGeneralPurposeBit().getNumberOfShannonFanoTrees(), + bis); + break; + case BZIP2: + current.in = new BZip2CompressorInputStream(bis); + break; + case ENHANCED_DEFLATED: + current.in = new Deflate64CompressorInputStream(bis); + break; + default: + // we should never get here as all supported methods have been covered + // will cause an error when read is invoked, don't throw an exception here so people can + // skip unsupported entries + break; + } + } + } else if (m == ZipMethod.ENHANCED_DEFLATED) { + current.in = new Deflate64CompressorInputStream(in); + } + + entriesRead++; + return current.entry; + } + + /** + * Fills the given array with the first local file header and + * deals with splitting/spanning markers that may prefix the first + * LFH. + */ + private void readFirstLocalFileHeader(final byte[] lfh) throws IOException { + readFully(lfh); + final ZipLong sig = new ZipLong(lfh); + if (sig.equals(ZipLong.DD_SIG)) { + throw new UnsupportedZipFeatureException(UnsupportedZipFeatureException.Feature.SPLITTING); + } + + if (sig.equals(ZipLong.SINGLE_SEGMENT_SPLIT_MARKER)) { + // The archive is not really split as only one segment was + // needed in the end. Just skip over the marker. + final byte[] missedLfhBytes = new byte[4]; + readFully(missedLfhBytes); + System.arraycopy(lfh, 4, lfh, 0, LFH_LEN - 4); + System.arraycopy(missedLfhBytes, 0, lfh, LFH_LEN - 4, 4); + } + } + + /** + * Records whether a Zip64 extra is present and sets the size + * information from it if sizes are 0xFFFFFFFF and the entry + * doesn't use a data descriptor. + */ + private void processZip64Extra(final ZipLong size, final ZipLong cSize) { + final Zip64ExtendedInformationExtraField z64 = + (Zip64ExtendedInformationExtraField) + current.entry.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID); + current.usesZip64 = z64 != null; + if (!current.hasDataDescriptor) { + if (z64 != null // same as current.usesZip64 but avoids NPE warning + && (ZipLong.ZIP64_MAGIC.equals(cSize) || ZipLong.ZIP64_MAGIC.equals(size)) ) { + current.entry.setCompressedSize(z64.getCompressedSize().getLongValue()); + current.entry.setSize(z64.getSize().getLongValue()); + } else if (cSize != null && size != null) { + current.entry.setCompressedSize(cSize.getValue()); + current.entry.setSize(size.getValue()); + } + } + } + + @Override + public ArchiveEntry getNextEntry() throws IOException { + return getNextZipEntry(); + } + + /** + * Whether this class is able to read the given entry. + * + *

      May return false if it is set up to use encryption or a + * compression method that hasn't been implemented yet.

      + * @since 1.1 + */ + @Override + public boolean canReadEntryData(final ArchiveEntry ae) { + if (ae instanceof ZipArchiveEntry) { + final ZipArchiveEntry ze = (ZipArchiveEntry) ae; + return ZipUtil.canHandleEntryData(ze) + && supportsDataDescriptorFor(ze) + && supportsCompressedSizeFor(ze); + } + return false; + } + + @Override + public int read(final byte[] buffer, final int offset, final int length) throws IOException { + if (length == 0) { + return 0; + } + if (closed) { + throw new IOException("The stream is closed"); + } + + if (current == null) { + return -1; + } + + // avoid int overflow, check null buffer + if (offset > buffer.length || length < 0 || offset < 0 || buffer.length - offset < length) { + throw new ArrayIndexOutOfBoundsException(); + } + + ZipUtil.checkRequestedFeatures(current.entry); + if (!supportsDataDescriptorFor(current.entry)) { + throw new UnsupportedZipFeatureException(UnsupportedZipFeatureException.Feature.DATA_DESCRIPTOR, + current.entry); + } + if (!supportsCompressedSizeFor(current.entry)) { + throw new UnsupportedZipFeatureException(UnsupportedZipFeatureException.Feature.UNKNOWN_COMPRESSED_SIZE, + current.entry); + } + + int read; + if (current.entry.getMethod() == ZipArchiveOutputStream.STORED) { + read = readStored(buffer, offset, length); + } else if (current.entry.getMethod() == ZipArchiveOutputStream.DEFLATED) { + read = readDeflated(buffer, offset, length); + } else if (current.entry.getMethod() == ZipMethod.UNSHRINKING.getCode() + || current.entry.getMethod() == ZipMethod.IMPLODING.getCode() + || current.entry.getMethod() == ZipMethod.ENHANCED_DEFLATED.getCode() + || current.entry.getMethod() == ZipMethod.BZIP2.getCode()) { + read = current.in.read(buffer, offset, length); + } else { + throw new UnsupportedZipFeatureException(ZipMethod.getMethodByCode(current.entry.getMethod()), + current.entry); + } + + if (read >= 0) { + current.crc.update(buffer, offset, read); + uncompressedCount += read; + } + + return read; + } + + /** + * @since 1.17 + */ + @Override + public long getCompressedCount() { + if (current.entry.getMethod() == ZipArchiveOutputStream.STORED) { + return current.bytesRead; + } else if (current.entry.getMethod() == ZipArchiveOutputStream.DEFLATED) { + return getBytesInflated(); + } else if (current.entry.getMethod() == ZipMethod.UNSHRINKING.getCode()) { + return ((UnshrinkingInputStream) current.in).getCompressedCount(); + } else if (current.entry.getMethod() == ZipMethod.IMPLODING.getCode()) { + return ((ExplodingInputStream) current.in).getCompressedCount(); + } else if (current.entry.getMethod() == ZipMethod.ENHANCED_DEFLATED.getCode()) { + return ((Deflate64CompressorInputStream) current.in).getCompressedCount(); + } else if (current.entry.getMethod() == ZipMethod.BZIP2.getCode()) { + return ((BZip2CompressorInputStream) current.in).getCompressedCount(); + } else { + return -1; + } + } + + /** + * @since 1.17 + */ + @Override + public long getUncompressedCount() { + return uncompressedCount; + } + + /** + * Implementation of read for STORED entries. + */ + private int readStored(final byte[] buffer, final int offset, final int length) throws IOException { + + if (current.hasDataDescriptor) { + if (lastStoredEntry == null) { + readStoredEntry(); + } + return lastStoredEntry.read(buffer, offset, length); + } + + final long csize = current.entry.getSize(); + if (current.bytesRead >= csize) { + return -1; + } + + if (buf.position() >= buf.limit()) { + buf.position(0); + final int l = in.read(buf.array()); + if (l == -1) { + buf.limit(0); + throw new IOException("Truncated ZIP file"); + } + buf.limit(l); + + count(l); + current.bytesReadFromStream += l; + } + + int toRead = Math.min(buf.remaining(), length); + if ((csize - current.bytesRead) < toRead) { + // if it is smaller than toRead then it fits into an int + toRead = (int) (csize - current.bytesRead); + } + buf.get(buffer, offset, toRead); + current.bytesRead += toRead; + return toRead; + } + + /** + * Implementation of read for DEFLATED entries. + */ + private int readDeflated(final byte[] buffer, final int offset, final int length) throws IOException { + final int read = readFromInflater(buffer, offset, length); + if (read <= 0) { + if (inf.finished()) { + return -1; + } else if (inf.needsDictionary()) { + throw new ZipException("This archive needs a preset dictionary" + + " which is not supported by Commons" + + " Compress."); + } else if (read == -1) { + throw new IOException("Truncated ZIP file"); + } + } + return read; + } + + /** + * Potentially reads more bytes to fill the inflater's buffer and + * reads from it. + */ + private int readFromInflater(final byte[] buffer, final int offset, final int length) throws IOException { + int read = 0; + do { + if (inf.needsInput()) { + final int l = fill(); + if (l > 0) { + current.bytesReadFromStream += buf.limit(); + } else if (l == -1) { + return -1; + } else { + break; + } + } + try { + read = inf.inflate(buffer, offset, length); + } catch (final DataFormatException e) { + throw (IOException) new ZipException(e.getMessage()).initCause(e); + } + } while (read == 0 && inf.needsInput()); + return read; + } + + @Override + public void close() throws IOException { + if (!closed) { + closed = true; + try { + in.close(); + } finally { + inf.end(); + } + } + } + + /** + * Skips over and discards value bytes of data from this input + * stream. + * + *

      This implementation may end up skipping over some smaller + * number of bytes, possibly 0, if and only if it reaches the end + * of the underlying stream.

      + * + *

      The actual number of bytes skipped is returned.

      + * + * @param value the number of bytes to be skipped. + * @return the actual number of bytes skipped. + * @throws IOException - if an I/O error occurs. + * @throws IllegalArgumentException - if value is negative. + */ + @Override + public long skip(final long value) throws IOException { + if (value >= 0) { + long skipped = 0; + while (skipped < value) { + final long rem = value - skipped; + final int x = read(skipBuf, 0, (int) (skipBuf.length > rem ? rem : skipBuf.length)); + if (x == -1) { + return skipped; + } + skipped += x; + } + return skipped; + } + throw new IllegalArgumentException(); + } + + /** + * Checks if the signature matches what is expected for a zip file. + * Does not currently handle self-extracting zips which may have arbitrary + * leading content. + * + * @param signature the bytes to check + * @param length the number of bytes to check + * @return true, if this stream is a zip archive stream, false otherwise + */ + public static boolean matches(final byte[] signature, final int length) { + if (length < ZipArchiveOutputStream.LFH_SIG.length) { + return false; + } + + return checksig(signature, ZipArchiveOutputStream.LFH_SIG) // normal file + || checksig(signature, ZipArchiveOutputStream.EOCD_SIG) // empty zip + || checksig(signature, ZipArchiveOutputStream.DD_SIG) // split zip + || checksig(signature, ZipLong.SINGLE_SEGMENT_SPLIT_MARKER.getBytes()); + } + + private static boolean checksig(final byte[] signature, final byte[] expected) { + for (int i = 0; i < expected.length; i++) { + if (signature[i] != expected[i]) { + return false; + } + } + return true; + } + + /** + * Closes the current ZIP archive entry and positions the underlying + * stream to the beginning of the next entry. All per-entry variables + * and data structures are cleared. + *

      + * If the compressed size of this entry is included in the entry header, + * then any outstanding bytes are simply skipped from the underlying + * stream without uncompressing them. This allows an entry to be safely + * closed even if the compression method is unsupported. + *

      + * In case we don't know the compressed size of this entry or have + * already buffered too much data from the underlying stream to support + * uncompression, then the uncompression process is completed and the + * end position of the stream is adjusted based on the result of that + * process. + * + * @throws IOException if an error occurs + */ + private void closeEntry() throws IOException { + if (closed) { + throw new IOException("The stream is closed"); + } + if (current == null) { + return; + } + + // Ensure all entry bytes are read + if (currentEntryHasOutstandingBytes()) { + drainCurrentEntryData(); + } else { + // this is guaranteed to exhaust the stream + skip(Long.MAX_VALUE); //NOSONAR + + final long inB = current.entry.getMethod() == ZipArchiveOutputStream.DEFLATED + ? getBytesInflated() : current.bytesRead; + + // this is at most a single read() operation and can't + // exceed the range of int + final int diff = (int) (current.bytesReadFromStream - inB); + + // Pushback any required bytes + if (diff > 0) { + pushback(buf.array(), buf.limit() - diff, diff); + current.bytesReadFromStream -= diff; + } + + // Drain remainder of entry if not all data bytes were required + if (currentEntryHasOutstandingBytes()) { + drainCurrentEntryData(); + } + } + + if (lastStoredEntry == null && current.hasDataDescriptor) { + readDataDescriptor(); + } + + inf.reset(); + buf.clear().flip(); + current = null; + lastStoredEntry = null; + } + + /** + * If the compressed size of the current entry is included in the entry header + * and there are any outstanding bytes in the underlying stream, then + * this returns true. + * + * @return true, if current entry is determined to have outstanding bytes, false otherwise + */ + private boolean currentEntryHasOutstandingBytes() { + return current.bytesReadFromStream <= current.entry.getCompressedSize() + && !current.hasDataDescriptor; + } + + /** + * Read all data of the current entry from the underlying stream + * that hasn't been read, yet. + */ + private void drainCurrentEntryData() throws IOException { + long remaining = current.entry.getCompressedSize() - current.bytesReadFromStream; + while (remaining > 0) { + final long n = in.read(buf.array(), 0, (int) Math.min(buf.capacity(), remaining)); + if (n < 0) { + throw new EOFException("Truncated ZIP entry: " + + ArchiveUtils.sanitize(current.entry.getName())); + } + count(n); + remaining -= n; + } + } + + /** + * Get the number of bytes Inflater has actually processed. + * + *

      for Java < Java7 the getBytes* methods in + * Inflater/Deflater seem to return unsigned ints rather than + * longs that start over with 0 at 2^32.

      + * + *

      The stream knows how many bytes it has read, but not how + * many the Inflater actually consumed - it should be between the + * total number of bytes read for the entry and the total number + * minus the last read operation. Here we just try to make the + * value close enough to the bytes we've read by assuming the + * number of bytes consumed must be smaller than (or equal to) the + * number of bytes read but not smaller by more than 2^32.

      + */ + private long getBytesInflated() { + long inB = inf.getBytesRead(); + if (current.bytesReadFromStream >= TWO_EXP_32) { + while (inB + TWO_EXP_32 <= current.bytesReadFromStream) { + inB += TWO_EXP_32; + } + } + return inB; + } + + private int fill() throws IOException { + if (closed) { + throw new IOException("The stream is closed"); + } + final int length = in.read(buf.array()); + if (length > 0) { + buf.limit(length); + count(buf.limit()); + inf.setInput(buf.array(), 0, buf.limit()); + } + return length; + } + + private void readFully(final byte[] b) throws IOException { + readFully(b, 0); + } + + private void readFully(final byte[] b, final int off) throws IOException { + final int len = b.length - off; + final int count = IOUtils.readFully(in, b, off, len); + count(count); + if (count < len) { + throw new EOFException(); + } + } + + private void readDataDescriptor() throws IOException { + readFully(wordBuf); + ZipLong val = new ZipLong(wordBuf); + if (ZipLong.DD_SIG.equals(val)) { + // data descriptor with signature, skip sig + readFully(wordBuf); + val = new ZipLong(wordBuf); + } + current.entry.setCrc(val.getValue()); + + // if there is a ZIP64 extra field, sizes are eight bytes + // each, otherwise four bytes each. Unfortunately some + // implementations - namely Java7 - use eight bytes without + // using a ZIP64 extra field - + // https://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7073588 + + // just read 16 bytes and check whether bytes nine to twelve + // look like one of the signatures of what could follow a data + // descriptor (ignoring archive decryption headers for now). + // If so, push back eight bytes and assume sizes are four + // bytes, otherwise sizes are eight bytes each. + readFully(twoDwordBuf); + final ZipLong potentialSig = new ZipLong(twoDwordBuf, DWORD); + if (potentialSig.equals(ZipLong.CFH_SIG) || potentialSig.equals(ZipLong.LFH_SIG)) { + pushback(twoDwordBuf, DWORD, DWORD); + current.entry.setCompressedSize(ZipLong.getValue(twoDwordBuf)); + current.entry.setSize(ZipLong.getValue(twoDwordBuf, WORD)); + } else { + current.entry.setCompressedSize(ZipEightByteInteger.getLongValue(twoDwordBuf)); + current.entry.setSize(ZipEightByteInteger.getLongValue(twoDwordBuf, DWORD)); + } + } + + /** + * Whether this entry requires a data descriptor this library can work with. + * + * @return true if allowStoredEntriesWithDataDescriptor is true, + * the entry doesn't require any data descriptor or the method is + * DEFLATED or ENHANCED_DEFLATED. + */ + private boolean supportsDataDescriptorFor(final ZipArchiveEntry entry) { + return !entry.getGeneralPurposeBit().usesDataDescriptor() + + || (allowStoredEntriesWithDataDescriptor && entry.getMethod() == ZipEntry.STORED) + || entry.getMethod() == ZipEntry.DEFLATED + || entry.getMethod() == ZipMethod.ENHANCED_DEFLATED.getCode(); + } + + /** + * Whether the compressed size for the entry is either known or + * not required by the compression method being used. + */ + private boolean supportsCompressedSizeFor(final ZipArchiveEntry entry) { + return entry.getCompressedSize() != ArchiveEntry.SIZE_UNKNOWN + || entry.getMethod() == ZipEntry.DEFLATED + || entry.getMethod() == ZipMethod.ENHANCED_DEFLATED.getCode() + || (entry.getGeneralPurposeBit().usesDataDescriptor() + && allowStoredEntriesWithDataDescriptor + && entry.getMethod() == ZipEntry.STORED); + } + + private static final String USE_ZIPFILE_INSTEAD_OF_STREAM_DISCLAIMER = + " while reading a stored entry using data descriptor. Either the archive is broken" + + " or it can not be read using ZipArchiveInputStream and you must use ZipFile." + + " A common cause for this is a ZIP archive containing a ZIP archive." + + " See http://commons.apache.org/proper/commons-compress/zip.html#ZipArchiveInputStream_vs_ZipFile"; + + /** + * Caches a stored entry that uses the data descriptor. + * + *
        + *
      • Reads a stored entry until the signature of a local file + * header, central directory header or data descriptor has been + * found.
      • + *
      • Stores all entry data in lastStoredEntry.

        + *
      • Rewinds the stream to position at the data + * descriptor.
      • + *
      • reads the data descriptor
      • + *
      + * + *

      After calling this method the entry should know its size, + * the entry's data is cached and the stream is positioned at the + * next local file or central directory header.

      + */ + private void readStoredEntry() throws IOException { + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + int off = 0; + boolean done = false; + + // length of DD without signature + final int ddLen = current.usesZip64 ? WORD + 2 * DWORD : 3 * WORD; + + while (!done) { + final int r = in.read(buf.array(), off, ZipArchiveOutputStream.BUFFER_SIZE - off); + if (r <= 0) { + // read the whole archive without ever finding a + // central directory + throw new IOException("Truncated ZIP file"); + } + if (r + off < 4) { + // buffer too small to check for a signature, loop + off += r; + continue; + } + + done = bufferContainsSignature(bos, off, r, ddLen); + if (!done) { + off = cacheBytesRead(bos, off, r, ddLen); + } + } + if (current.entry.getCompressedSize() != current.entry.getSize()) { + throw new ZipException("compressed and uncompressed size don't match" + + USE_ZIPFILE_INSTEAD_OF_STREAM_DISCLAIMER); + } + final byte[] b = bos.toByteArray(); + if (b.length != current.entry.getSize()) { + throw new ZipException("actual and claimed size don't match" + + USE_ZIPFILE_INSTEAD_OF_STREAM_DISCLAIMER); + } + lastStoredEntry = new ByteArrayInputStream(b); + } + + private static final byte[] LFH = ZipLong.LFH_SIG.getBytes(); + private static final byte[] CFH = ZipLong.CFH_SIG.getBytes(); + private static final byte[] DD = ZipLong.DD_SIG.getBytes(); + + /** + * Checks whether the current buffer contains the signature of a + * "data descriptor", "local file header" or + * "central directory entry". + * + *

      If it contains such a signature, reads the data descriptor + * and positions the stream right after the data descriptor.

      + */ + private boolean bufferContainsSignature(final ByteArrayOutputStream bos, final int offset, final int lastRead, final int expectedDDLen) + throws IOException { + + boolean done = false; + for (int i = 0; !done && i < offset + lastRead - 4; i++) { + if (buf.array()[i] == LFH[0] && buf.array()[i + 1] == LFH[1]) { + int expectDDPos = i; + if (i >= expectedDDLen && + (buf.array()[i + 2] == LFH[2] && buf.array()[i + 3] == LFH[3]) + || (buf.array()[i] == CFH[2] && buf.array()[i + 3] == CFH[3])) { + // found a LFH or CFH: + expectDDPos = i - expectedDDLen; + done = true; + } + else if (buf.array()[i + 2] == DD[2] && buf.array()[i + 3] == DD[3]) { + // found DD: + done = true; + } + if (done) { + // * push back bytes read in excess as well as the data + // descriptor + // * copy the remaining bytes to cache + // * read data descriptor + pushback(buf.array(), expectDDPos, offset + lastRead - expectDDPos); + bos.write(buf.array(), 0, expectDDPos); + readDataDescriptor(); + } + } + } + return done; + } + + /** + * If the last read bytes could hold a data descriptor and an + * incomplete signature then save the last bytes to the front of + * the buffer and cache everything in front of the potential data + * descriptor into the given ByteArrayOutputStream. + * + *

      Data descriptor plus incomplete signature (3 bytes in the + * worst case) can be 20 bytes max.

      + */ + private int cacheBytesRead(final ByteArrayOutputStream bos, int offset, final int lastRead, final int expecteDDLen) { + final int cacheable = offset + lastRead - expecteDDLen - 3; + if (cacheable > 0) { + bos.write(buf.array(), 0, cacheable); + System.arraycopy(buf.array(), cacheable, buf.array(), 0, expecteDDLen + 3); + offset = expecteDDLen + 3; + } else { + offset += lastRead; + } + return offset; + } + + private void pushback(final byte[] buf, final int offset, final int length) throws IOException { + ((PushbackInputStream) in).unread(buf, offset, length); + pushedBackBytes(length); + } + + // End of Central Directory Record + // end of central dir signature WORD + // number of this disk SHORT + // number of the disk with the + // start of the central directory SHORT + // total number of entries in the + // central directory on this disk SHORT + // total number of entries in + // the central directory SHORT + // size of the central directory WORD + // offset of start of central + // directory with respect to + // the starting disk number WORD + // .ZIP file comment length SHORT + // .ZIP file comment up to 64KB + // + + /** + * Reads the stream until it find the "End of central directory + * record" and consumes it as well. + */ + private void skipRemainderOfArchive() throws IOException { + // skip over central directory. One LFH has been read too much + // already. The calculation discounts file names and extra + // data so it will be too short. + realSkip((long) entriesRead * CFH_LEN - LFH_LEN); + findEocdRecord(); + realSkip((long) ZipFile.MIN_EOCD_SIZE - WORD /* signature */ - SHORT /* comment len */); + readFully(shortBuf); + // file comment + realSkip(ZipShort.getValue(shortBuf)); + } + + /** + * Reads forward until the signature of the "End of central + * directory" record is found. + */ + private void findEocdRecord() throws IOException { + int currentByte = -1; + boolean skipReadCall = false; + while (skipReadCall || (currentByte = readOneByte()) > -1) { + skipReadCall = false; + if (!isFirstByteOfEocdSig(currentByte)) { + continue; + } + currentByte = readOneByte(); + if (currentByte != ZipArchiveOutputStream.EOCD_SIG[1]) { + if (currentByte == -1) { + break; + } + skipReadCall = isFirstByteOfEocdSig(currentByte); + continue; + } + currentByte = readOneByte(); + if (currentByte != ZipArchiveOutputStream.EOCD_SIG[2]) { + if (currentByte == -1) { + break; + } + skipReadCall = isFirstByteOfEocdSig(currentByte); + continue; + } + currentByte = readOneByte(); + if (currentByte == -1 + || currentByte == ZipArchiveOutputStream.EOCD_SIG[3]) { + break; + } + skipReadCall = isFirstByteOfEocdSig(currentByte); + } + } + + /** + * Skips bytes by reading from the underlying stream rather than + * the (potentially inflating) archive stream - which {@link + * #skip} would do. + * + * Also updates bytes-read counter. + */ + private void realSkip(final long value) throws IOException { + if (value >= 0) { + long skipped = 0; + while (skipped < value) { + final long rem = value - skipped; + final int x = in.read(skipBuf, 0, (int) (skipBuf.length > rem ? rem : skipBuf.length)); + if (x == -1) { + return; + } + count(x); + skipped += x; + } + return; + } + throw new IllegalArgumentException(); + } + + /** + * Reads bytes by reading from the underlying stream rather than + * the (potentially inflating) archive stream - which {@link #read} would do. + * + * Also updates bytes-read counter. + */ + private int readOneByte() throws IOException { + final int b = in.read(); + if (b != -1) { + count(1); + } + return b; + } + + private boolean isFirstByteOfEocdSig(final int b) { + return b == ZipArchiveOutputStream.EOCD_SIG[0]; + } + + private static final byte[] APK_SIGNING_BLOCK_MAGIC = new byte[] { + 'A', 'P', 'K', ' ', 'S', 'i', 'g', ' ', 'B', 'l', 'o', 'c', 'k', ' ', '4', '2', + }; + private static final BigInteger LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE); + + /** + * Checks whether this might be an APK Signing Block. + * + *

      Unfortunately the APK signing block does not start with some kind of signature, it rather ends with one. It + * starts with a length, so what we do is parse the suspect length, skip ahead far enough, look for the signature + * and if we've found it, return true.

      + * + * @param suspectLocalFileHeader the bytes read from the underlying stream in the expectation that they would hold + * the local file header of the next entry. + * + * @return true if this looks like a APK signing block + * + * @see https://source.android.com/security/apksigning/v2 + */ + private boolean isApkSigningBlock(byte[] suspectLocalFileHeader) throws IOException { + // length of block excluding the size field itself + BigInteger len = ZipEightByteInteger.getValue(suspectLocalFileHeader); + // LFH has already been read and all but the first eight bytes contain (part of) the APK signing block, + // also subtract 16 bytes in order to position us at the magic string + BigInteger toSkip = len.add(BigInteger.valueOf(DWORD - suspectLocalFileHeader.length + - (long) APK_SIGNING_BLOCK_MAGIC.length)); + byte[] magic = new byte[APK_SIGNING_BLOCK_MAGIC.length]; + + try { + if (toSkip.signum() < 0) { + // suspectLocalFileHeader contains the start of suspect magic string + int off = suspectLocalFileHeader.length + toSkip.intValue(); + // length was shorter than magic length + if (off < DWORD) { + return false; + } + int bytesInBuffer = Math.abs(toSkip.intValue()); + System.arraycopy(suspectLocalFileHeader, off, magic, 0, Math.min(bytesInBuffer, magic.length)); + if (bytesInBuffer < magic.length) { + readFully(magic, bytesInBuffer); + } + } else { + while (toSkip.compareTo(LONG_MAX) > 0) { + realSkip(Long.MAX_VALUE); + toSkip = toSkip.add(LONG_MAX.negate()); + } + realSkip(toSkip.longValue()); + readFully(magic); + } + } catch (EOFException ex) { //NOSONAR + // length was invalid + return false; + } + return Arrays.equals(magic, APK_SIGNING_BLOCK_MAGIC); + } + + /** + * Structure collecting information for the entry that is + * currently being read. + */ + private static final class CurrentEntry { + + /** + * Current ZIP entry. + */ + private final ZipArchiveEntry entry = new ZipArchiveEntry(); + + /** + * Does the entry use a data descriptor? + */ + private boolean hasDataDescriptor; + + /** + * Does the entry have a ZIP64 extended information extra field. + */ + private boolean usesZip64; + + /** + * Number of bytes of entry content read by the client if the + * entry is STORED. + */ + private long bytesRead; + + /** + * Number of bytes of entry content read from the stream. + * + *

      This may be more than the actual entry's length as some + * stuff gets buffered up and needs to be pushed back when the + * end of the entry has been reached.

      + */ + private long bytesReadFromStream; + + /** + * The checksum calculated as the current entry is read. + */ + private final CRC32 crc = new CRC32(); + + /** + * The input stream decompressing the data for shrunk and imploded entries. + */ + private InputStream in; + } + + /** + * Bounded input stream adapted from commons-io + */ + private class BoundedInputStream extends InputStream { + + /** the wrapped input stream */ + private final InputStream in; + + /** the max length to provide */ + private final long max; + + /** the number of bytes already returned */ + private long pos = 0; + + /** + * Creates a new BoundedInputStream that wraps the given input + * stream and limits it to a certain size. + * + * @param in The wrapped input stream + * @param size The maximum number of bytes to return + */ + public BoundedInputStream(final InputStream in, final long size) { + this.max = size; + this.in = in; + } + + @Override + public int read() throws IOException { + if (max >= 0 && pos >= max) { + return -1; + } + final int result = in.read(); + pos++; + count(1); + current.bytesReadFromStream++; + return result; + } + + @Override + public int read(final byte[] b) throws IOException { + return this.read(b, 0, b.length); + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } + if (max >= 0 && pos >= max) { + return -1; + } + final long maxRead = max >= 0 ? Math.min(len, max - pos) : len; + final int bytesRead = in.read(b, off, (int) maxRead); + + if (bytesRead == -1) { + return -1; + } + + pos += bytesRead; + count(bytesRead); + current.bytesReadFromStream += bytesRead; + return bytesRead; + } + + @Override + public long skip(final long n) throws IOException { + final long toSkip = max >= 0 ? Math.min(n, max - pos) : n; + final long skippedBytes = IOUtils.skip(in, toSkip); + pos += skippedBytes; + return skippedBytes; + } + + @Override + public int available() throws IOException { + if (max >= 0 && pos >= max) { + return 0; + } + return in.available(); + } + } +} Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_winrar/split_zip_created_by_winrar.z01 and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_winrar/split_zip_created_by_winrar.z01 differ Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_winrar/split_zip_created_by_winrar.z02 and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_winrar/split_zip_created_by_winrar.z02 differ Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_winrar/split_zip_created_by_winrar.zip and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_winrar/split_zip_created_by_winrar.zip differ Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_winrar/zip_to_compare_created_by_winrar.zip and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_winrar/zip_to_compare_created_by_winrar.zip differ diff -Nru libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_zip/file_to_compare_1 libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_zip/file_to_compare_1 --- libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_zip/file_to_compare_1 1970-01-01 00:00:00.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_zip/file_to_compare_1 2020-01-07 14:40:25.000000000 +0000 @@ -0,0 +1,38 @@ +/* + * 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 + * + * http://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.dump; + + +/** + * Unsupported compression algorithm. The dump archive uses an unsupported + * compression algorithm (BZLIB2 or LZO). + */ +public class UnsupportedCompressionAlgorithmException + extends DumpArchiveException { + private static final long serialVersionUID = 1L; + + public UnsupportedCompressionAlgorithmException() { + super("this file uses an unsupported compression algorithm."); + } + + public UnsupportedCompressionAlgorithmException(final String alg) { + super("this file uses an unsupported compression algorithm: " + alg + + "."); + } +} diff -Nru libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_zip/file_to_compare_2 libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_zip/file_to_compare_2 --- libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_zip/file_to_compare_2 1970-01-01 00:00:00.000000000 +0000 +++ libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_zip/file_to_compare_2 2020-01-07 14:40:25.000000000 +0000 @@ -0,0 +1,79 @@ +/* + * 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 + * + * http://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.compressors.deflate; + +import java.util.zip.Deflater; + +/** + * Parameters for the Deflate compressor. + * @since 1.9 + */ +public class DeflateParameters { + + private boolean zlibHeader = true; + private int compressionLevel = Deflater.DEFAULT_COMPRESSION; + + /** + * Whether or not the zlib header shall be written (when + * compressing) or expected (when decompressing). + * @return true if zlib header shall be written + */ + public boolean withZlibHeader() { + return zlibHeader; + } + + /** + * Sets the zlib header presence parameter. + * + *

      This affects whether or not the zlib header will be written + * (when compressing) or expected (when decompressing).

      + * + * @param zlibHeader true if zlib header shall be written + */ + public void setWithZlibHeader(final boolean zlibHeader) { + this.zlibHeader = zlibHeader; + } + + /** + * The compression level. + * @see #setCompressionLevel + * @return the compression level + */ + public int getCompressionLevel() { + return compressionLevel; + } + + /** + * Sets the compression level. + * + * @param compressionLevel the compression level (between 0 and 9) + * @see Deflater#NO_COMPRESSION + * @see Deflater#BEST_SPEED + * @see Deflater#DEFAULT_COMPRESSION + * @see Deflater#BEST_COMPRESSION + */ + public void setCompressionLevel(final int compressionLevel) { + if (compressionLevel < -1 || compressionLevel > 9) { + throw new IllegalArgumentException("Invalid Deflate compression level: " + compressionLevel); + } + this.compressionLevel = compressionLevel; + } + +} Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z01 and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z01 differ Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z02 and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.z02 differ Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.zip and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip.zip differ Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip_zip64.z01 and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip_zip64.z01 differ Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip_zip64.z02 and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip_zip64.z02 differ Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip_zip64.zip and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_zip/split_zip_created_by_zip_zip64.zip differ Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_zip/zip_to_compare_created_by_zip.zip and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_zip/zip_to_compare_created_by_zip.zip differ Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/COMPRESS-477/split_zip_created_by_zip/zip_to_compare_created_by_zip_zip64.zip and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/COMPRESS-477/split_zip_created_by_zip/zip_to_compare_created_by_zip_zip64.zip differ Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/COMPRESS-492.7z and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/COMPRESS-492.7z differ Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/lbzip2_32767.bz2 and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/lbzip2_32767.bz2 differ Binary files /tmp/tmpLFVd1I/ijW45TajB9/libcommons-compress-java-1.19/src/test/resources/oldgnu_extended_sparse.tar and /tmp/tmpLFVd1I/uir1NM4tP0/libcommons-compress-java-1.20/src/test/resources/oldgnu_extended_sparse.tar differ