This is an automated email from the ASF dual-hosted git repository.
ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-compress.git
The following commit(s) were added to refs/heads/master by this push:
new 07323f25f COMPRESS-653: Fix split archive updating incorrect file
(#455)
07323f25f is described below
commit 07323f25f7236ffb7e53aceadc1abef8897ff35c
Author: Zbynek Vyskovsky <[email protected]>
AuthorDate: Mon Jan 15 07:52:17 2024 -0800
COMPRESS-653: Fix split archive updating incorrect file (#455)
* COMPRESS-653: Fix split archive updating incorrect file
* COMPRESS-653: Use ZipOpenOptions builder to specify increasing amount of
Zip options
* Don't initialize instance variable to its default
* Use final
---------
Co-authored-by: Gary Gregory <[email protected]>
---
.../zip/FileRandomAccessOutputStream.java | 79 +++++++++
.../archivers/zip/RandomAccessOutputStream.java | 70 ++++++++
.../SeekableChannelRandomAccessOutputStream.java | 62 +++++++
.../archivers/zip/ZipArchiveOutputStream.java | 95 ++++-------
.../commons/compress/archivers/zip/ZipFile.java | 153 +++++++++++++----
.../commons/compress/archivers/zip/ZipIoUtil.java | 78 +++++++++
.../archivers/zip/ZipSplitOutputStream.java | 93 ++++++++++-
.../zip/ZipSplitReadOnlySeekableByteChannel.java | 18 +-
.../apache/commons/compress/archivers/ZipTest.java | 158 ++++++++++++++++++
.../zip/FileRandomAccessOutputStreamTest.java | 150 +++++++++++++++++
.../zip/RandomAccessOutputStreamTest.java | 59 +++++++
...eekableChannelRandomAccessOutputStreamTest.java | 168 +++++++++++++++++++
.../archivers/zip/ZipArchiveOutputStreamTest.java | 44 +++++
.../compress/archivers/zip/ZipIoUtilTest.java | 184 +++++++++++++++++++++
.../ZipSplitReadOnlySeekableByteChannelTest.java | 2 +-
15 files changed, 1312 insertions(+), 101 deletions(-)
diff --git
a/src/main/java/org/apache/commons/compress/archivers/zip/FileRandomAccessOutputStream.java
b/src/main/java/org/apache/commons/compress/archivers/zip/FileRandomAccessOutputStream.java
new file mode 100644
index 000000000..8a431c4b5
--- /dev/null
+++
b/src/main/java/org/apache/commons/compress/archivers/zip/FileRandomAccessOutputStream.java
@@ -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.archivers.zip;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+
+/**
+ * {@link RandomAccessOutputStream} implementation based on Path file.
+ */
+class FileRandomAccessOutputStream extends RandomAccessOutputStream {
+
+ private final FileChannel channel;
+
+ private long position;
+
+ FileRandomAccessOutputStream(final Path file) throws IOException {
+ this(file, StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
+ }
+
+ FileRandomAccessOutputStream(final Path file, OpenOption... options)
throws IOException {
+ this(FileChannel.open(file, options));
+ }
+
+ FileRandomAccessOutputStream(final FileChannel channel) throws IOException
{
+ this.channel = channel;
+ }
+
+ FileChannel channel() {
+ return channel;
+ }
+
+ @Override
+ public synchronized void write(final byte[] b, final int off, final int
len) throws IOException {
+ ZipIoUtil.writeFully(this.channel, ByteBuffer.wrap(b, off, len));
+ position += len;
+ }
+
+ @Override
+ public synchronized long position() {
+ return position;
+ }
+
+ @Override
+ public void writeFullyAt(final byte[] b, final int off, final int len,
final long atPosition) throws IOException {
+ ByteBuffer buf = ByteBuffer.wrap(b, off, len);
+ for (long currentPos = atPosition; buf.hasRemaining(); ) {
+ int written = this.channel.write(buf, currentPos);
+ if (written <= 0) {
+ throw new IOException("Failed to fully write to file:
written=" + written);
+ }
+ currentPos += written;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ channel.close();
+ }
+}
diff --git
a/src/main/java/org/apache/commons/compress/archivers/zip/RandomAccessOutputStream.java
b/src/main/java/org/apache/commons/compress/archivers/zip/RandomAccessOutputStream.java
new file mode 100644
index 000000000..87bfd5913
--- /dev/null
+++
b/src/main/java/org/apache/commons/compress/archivers/zip/RandomAccessOutputStream.java
@@ -0,0 +1,70 @@
+/*
+ * 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.IOException;
+import java.io.OutputStream;
+
+
+/**
+ * Abstraction over OutputStream which also allows random access writes.
+ */
+abstract class RandomAccessOutputStream extends OutputStream {
+
+ /**
+ * Provides current position in output.
+ *
+ * @return
+ * current position.
+ */
+ public abstract long position() throws IOException;
+
+ /**
+ * Writes given data to specific position.
+ *
+ * @param position
+ * position in the stream
+ * @param b
+ * data to write
+ * @throws IOException
+ * when write fails.
+ */
+ public void writeFullyAt(final byte[] b, final long position) throws
IOException {
+ writeFullyAt(b, 0, b.length, position);
+ }
+
+ /**
+ * Writes given data to specific position.
+ *
+ * @param position
+ * position in the stream
+ * @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
+ * when write fails.
+ */
+ abstract void writeFullyAt(byte[] b, int off, int len, long position)
throws IOException;
+
+ @Override
+ public void write(final int b) throws IOException {
+ write(new byte[]{ (byte) b });
+ }
+}
diff --git
a/src/main/java/org/apache/commons/compress/archivers/zip/SeekableChannelRandomAccessOutputStream.java
b/src/main/java/org/apache/commons/compress/archivers/zip/SeekableChannelRandomAccessOutputStream.java
new file mode 100644
index 000000000..accad6879
--- /dev/null
+++
b/src/main/java/org/apache/commons/compress/archivers/zip/SeekableChannelRandomAccessOutputStream.java
@@ -0,0 +1,62 @@
+/*
+ * 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.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SeekableByteChannel;
+
+
+/**
+ * {@link RandomAccessOutputStream} implementation for SeekableByteChannel.
+ */
+class SeekableChannelRandomAccessOutputStream extends RandomAccessOutputStream
{
+
+ private final SeekableByteChannel channel;
+
+ private long position;
+
+ SeekableChannelRandomAccessOutputStream(final SeekableByteChannel channel)
{
+ this.channel = channel;
+ }
+
+ @Override
+ public synchronized void write(final byte[] b, final int off, final int
len) throws IOException {
+ ZipIoUtil.writeFully(this.channel, ByteBuffer.wrap(b, off, len));
+ }
+
+ @Override
+ public synchronized long position() throws IOException {
+ return channel.position();
+ }
+
+ @Override
+ public synchronized void writeFullyAt(final byte[] b, final int off, final
int len, final long position) throws IOException {
+ long saved = channel.position();
+ try {
+ channel.position(position);
+ ZipIoUtil.writeFully(channel, ByteBuffer.wrap(b, off, len));
+ } finally {
+ channel.position(saved);
+ }
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ channel.close();
+ }
+}
diff --git
a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStream.java
b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStream.java
index f9820535c..70aaa8979 100644
---
a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStream.java
+++
b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStream.java
@@ -17,19 +17,15 @@
package org.apache.commons.compress.archivers.zip;
import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
-import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -340,11 +336,6 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
*/
protected final Deflater def;
- /**
- * Optional random access output.
- */
- private final SeekableByteChannel channel;
-
private final OutputStream outputStream;
/**
@@ -424,7 +415,6 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
*/
public ZipArchiveOutputStream(final OutputStream out) {
this.outputStream = out;
- this.channel = null;
def = new Deflater(level, true);
streamCompressor = StreamCompressor.create(out, def);
isSplitZip = false;
@@ -451,7 +441,6 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
def = new Deflater(level, true);
this.outputStream = new ZipSplitOutputStream(path, zipSplitSize);
streamCompressor = StreamCompressor.create(this.outputStream, def);
- channel = null;
isSplitZip = true;
}
@@ -465,24 +454,8 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
*/
public ZipArchiveOutputStream(final Path file, final OpenOption...
options) throws IOException {
def = new Deflater(level, true);
- OutputStream outputStream = null;
- SeekableByteChannel channel = null;
- StreamCompressor streamCompressor;
- try {
- channel = Files.newByteChannel(file,
- EnumSet.of(StandardOpenOption.CREATE,
StandardOpenOption.WRITE, StandardOpenOption.READ,
StandardOpenOption.TRUNCATE_EXISTING));
- // will never get opened properly when an exception is thrown so
doesn't need to get closed
- streamCompressor = StreamCompressor.create(channel, def); //
NOSONAR
- } catch (final IOException e) { // NOSONAR
- final Closeable c = channel;
- org.apache.commons.io.IOUtils.closeQuietly(c);
- channel = null;
- outputStream = Files.newOutputStream(file, options);
- streamCompressor = StreamCompressor.create(outputStream, def);
- }
- this.outputStream = outputStream;
- this.channel = channel;
- this.streamCompressor = streamCompressor;
+ this.outputStream = options.length == 0 ? new
FileRandomAccessOutputStream(file) : new FileRandomAccessOutputStream(file,
options);
+ this.streamCompressor = StreamCompressor.create(outputStream, def);
this.isSplitZip = false;
}
@@ -497,10 +470,9 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
* @since 1.13
*/
public ZipArchiveOutputStream(final SeekableByteChannel channel) {
- this.channel = channel;
+ this.outputStream = new
SeekableChannelRandomAccessOutputStream(channel);
def = new Deflater(level, true);
- streamCompressor = StreamCompressor.create(channel, def);
- outputStream = null;
+ streamCompressor = StreamCompressor.create(outputStream, def);
isSplitZip = false;
}
@@ -636,7 +608,7 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
}
private void closeEntry(final boolean actuallyNeedsZip64, final boolean
phased) throws IOException {
- if (!phased && channel != null) {
+ if (!phased && outputStream instanceof RandomAccessOutputStream) {
rewriteSizesAndCrc(actuallyNeedsZip64);
}
@@ -876,7 +848,7 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
ZipUtil.toDosTime(ze.getTime(), buf, LFH_TIME_OFFSET);
// CRC
- if (phased || !(zipMethod == DEFLATED || channel != null)) {
+ if (phased || !(zipMethod == DEFLATED || outputStream instanceof
RandomAccessOutputStream)) {
ZipLong.putLong(ze.getCrc(), buf, LFH_CRC_OFFSET);
} else {
System.arraycopy(LZERO, 0, buf, LFH_CRC_OFFSET, ZipConstants.WORD);
@@ -893,7 +865,7 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
} else if (phased) {
ZipLong.putLong(ze.getCompressedSize(), buf,
LFH_COMPRESSED_SIZE_OFFSET);
ZipLong.putLong(ze.getSize(), buf, LFH_ORIGINAL_SIZE_OFFSET);
- } else if (zipMethod == DEFLATED || channel != null) {
+ } else if (zipMethod == DEFLATED || outputStream instanceof
RandomAccessOutputStream) {
System.arraycopy(LZERO, 0, buf, LFH_COMPRESSED_SIZE_OFFSET,
ZipConstants.WORD);
System.arraycopy(LZERO, 0, buf, LFH_ORIGINAL_SIZE_OFFSET,
ZipConstants.WORD);
} else { // Stored
@@ -931,14 +903,8 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
* </p>
*/
void destroy() throws IOException {
- try {
- if (channel != null) {
- channel.close();
- }
- } finally {
- if (outputStream != null) {
- outputStream.close();
- }
+ if (outputStream != null) {
+ outputStream.close();
}
}
@@ -1034,7 +1000,8 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
* @since 1.3
*/
private Zip64Mode getEffectiveZip64Mode(final ZipArchiveEntry ze) {
- if (zip64Mode != Zip64Mode.AsNeeded || channel != null ||
ze.getMethod() != DEFLATED || ze.getSize() != ArchiveEntry.SIZE_UNKNOWN) {
+ if (zip64Mode != Zip64Mode.AsNeeded || outputStream instanceof
RandomAccessOutputStream ||
+ ze.getMethod() != DEFLATED || ze.getSize() !=
ArchiveEntry.SIZE_UNKNOWN) {
return zip64Mode;
}
return Zip64Mode.Never;
@@ -1106,7 +1073,7 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
entry.entry.setCompressedSize(bytesWritten);
entry.entry.setCrc(crc);
- } else if (channel == null) {
+ } else if (!(outputStream instanceof RandomAccessOutputStream)) {
if (entry.entry.getCrc() != crc) {
throw new ZipException("Bad CRC checksum for entry " +
entry.entry.getName() + ": " + Long.toHexString(entry.entry.getCrc()) + "
instead of "
+ Long.toHexString(crc));
@@ -1172,7 +1139,7 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
* @return true if seekable
*/
public boolean isSeekable() {
- return channel != null;
+ return outputStream instanceof RandomAccessOutputStream;
}
private boolean isTooLargeForZip32(final ZipArchiveEntry zipArchiveEntry) {
@@ -1271,33 +1238,38 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
* sizes.
*/
private void rewriteSizesAndCrc(final boolean actuallyNeedsZip64) throws
IOException {
- final long save = channel.position();
+ RandomAccessOutputStream randomStream = (RandomAccessOutputStream)
outputStream;
+ long dataStart = entry.localDataStart;
+ if (randomStream instanceof ZipSplitOutputStream) {
+ dataStart = ((ZipSplitOutputStream)
randomStream).calculateDiskPosition(entry.entry.getDiskNumberStart(),
dataStart);
+ }
- channel.position(entry.localDataStart);
- writeOut(ZipLong.getBytes(entry.entry.getCrc()));
+ long position = dataStart;
+ randomStream.writeFullyAt(ZipLong.getBytes(entry.entry.getCrc()),
position); position += ZipConstants.WORD;
if (!hasZip64Extra(entry.entry) || !actuallyNeedsZip64) {
- writeOut(ZipLong.getBytes(entry.entry.getCompressedSize()));
- writeOut(ZipLong.getBytes(entry.entry.getSize()));
+
randomStream.writeFullyAt(ZipLong.getBytes(entry.entry.getCompressedSize()),
position); position += ZipConstants.WORD;
+ randomStream.writeFullyAt(ZipLong.getBytes(entry.entry.getSize()),
position); position += ZipConstants.WORD;
} else {
- writeOut(ZipLong.ZIP64_MAGIC.getBytes());
- writeOut(ZipLong.ZIP64_MAGIC.getBytes());
+ randomStream.writeFullyAt(ZipLong.ZIP64_MAGIC.getBytes(),
position); position += ZipConstants.WORD;
+ randomStream.writeFullyAt(ZipLong.ZIP64_MAGIC.getBytes(),
position); position += ZipConstants.WORD;
}
if (hasZip64Extra(entry.entry)) {
final ByteBuffer name = getName(entry.entry);
final int nameLen = name.limit() - name.position();
// seek to ZIP64 extra, skip header and size information
- channel.position(entry.localDataStart + 3 * ZipConstants.WORD + 2
* ZipConstants.SHORT + nameLen + 2 * ZipConstants.SHORT);
+ position = dataStart + 3 * ZipConstants.WORD + 2 *
ZipConstants.SHORT + nameLen + 2 * ZipConstants.SHORT;
// inside the ZIP64 extra uncompressed size comes
// first, unlike the LFH, CD or data descriptor
- writeOut(ZipEightByteInteger.getBytes(entry.entry.getSize()));
-
writeOut(ZipEightByteInteger.getBytes(entry.entry.getCompressedSize()));
+
randomStream.writeFullyAt(ZipEightByteInteger.getBytes(entry.entry.getSize()),
position); position += ZipConstants.DWORD;
+
randomStream.writeFullyAt(ZipEightByteInteger.getBytes(entry.entry.getCompressedSize()),
position); position += ZipConstants.DWORD;
if (!actuallyNeedsZip64) {
// do some cleanup:
// * rewrite version needed to extract
- channel.position(entry.localDataStart - 5 *
ZipConstants.SHORT);
-
writeOut(ZipShort.getBytes(versionNeededToExtract(entry.entry.getMethod(),
false, false)));
+ position = dataStart - 5 * ZipConstants.SHORT;
+
randomStream.writeFullyAt(ZipShort.getBytes(versionNeededToExtract(entry.entry.getMethod(),
false, false)), position);
+ position += ZipConstants.SHORT;
// * remove ZIP64 extra, so it doesn't get written
// to the central directory
@@ -1311,7 +1283,6 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
}
}
}
- channel.position(save);
}
/**
@@ -1470,7 +1441,7 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
private boolean shouldAddZip64Extra(final ZipArchiveEntry entry, final
Zip64Mode mode) {
return mode == Zip64Mode.Always || mode ==
Zip64Mode.AlwaysWithCompatibility || entry.getSize() >= ZipConstants.ZIP64_MAGIC
|| entry.getCompressedSize() >= ZipConstants.ZIP64_MAGIC
- || entry.getSize() == ArchiveEntry.SIZE_UNKNOWN && channel !=
null && mode != Zip64Mode.Never;
+ || entry.getSize() == ArchiveEntry.SIZE_UNKNOWN &&
outputStream instanceof RandomAccessOutputStream && mode != Zip64Mode.Never;
}
/**
@@ -1496,7 +1467,7 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
}
private boolean usesDataDescriptor(final int zipMethod, final boolean
phased) {
- return !phased && zipMethod == DEFLATED && channel == null;
+ return !phased && zipMethod == DEFLATED && !(outputStream instanceof
RandomAccessOutputStream);
}
/**
@@ -1547,7 +1518,7 @@ public class ZipArchiveOutputStream extends
ArchiveOutputStream<ZipArchiveEntry>
*/
private void validateSizeInformation(final Zip64Mode effectiveMode) throws
ZipException {
// Size/CRC not required if SeekableByteChannel is used
- if (entry.entry.getMethod() == STORED && channel == null) {
+ if (entry.entry.getMethod() == STORED && !(outputStream instanceof
RandomAccessOutputStream)) {
if (entry.entry.getSize() == ArchiveEntry.SIZE_UNKNOWN) {
throw new ZipException("Uncompressed size is required for" + "
STORED method when not writing to a" + " file");
}
diff --git
a/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java
b/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java
index ded114350..3337308fd 100644
--- a/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java
+++ b/src/main/java/org/apache/commons/compress/archivers/zip/ZipFile.java
@@ -25,6 +25,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.Charset;
@@ -33,6 +34,7 @@ import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
@@ -42,6 +44,8 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import java.util.zip.Inflater;
import java.util.zip.ZipException;
@@ -55,6 +59,7 @@ import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.compress.utils.InputStreamStatistics;
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
import org.apache.commons.io.Charsets;
+import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.build.AbstractOrigin.ByteArrayOrigin;
import org.apache.commons.io.build.AbstractStreamBuilder;
import org.apache.commons.io.input.CountingInputStream;
@@ -128,6 +133,7 @@ public class ZipFile implements Closeable {
private SeekableByteChannel seekableByteChannel;
private boolean useUnicodeExtraFields = true;
private boolean ignoreLocalFileHeader;
+ private long maxNumberOfDisks = 1;
public Builder() {
setCharset(DEFAULT_CHARSET);
@@ -151,7 +157,7 @@ public class ZipFile implements Closeable {
openOptions = new OpenOption[] { StandardOpenOption.READ };
}
final Path path = getPath();
- actualChannel = Files.newByteChannel(path, openOptions);
+ actualChannel = openZipChannel(path, maxNumberOfDisks,
openOptions);
actualDescription = path.toString();
}
final boolean closeOnError = seekableByteChannel != null;
@@ -191,6 +197,19 @@ public class ZipFile implements Closeable {
return this;
}
+ /**
+ * Sets max number of multi archive disks, default is 1 (no multi
archive).
+ *
+ * @param maxNumberOfDisks
+ * max number of multi archive disks
+ *
+ * @return this instance
+ */
+ public Builder setMaxNumberOfDisks(final long maxNumberOfDisks) {
+ this.maxNumberOfDisks = maxNumberOfDisks;
+ return this;
+ }
+
}
/**
@@ -545,6 +564,7 @@ public class ZipFile implements Closeable {
private long firstLocalFileHeaderOffset;
+
/**
* Opens the given file for reading, assuming "UTF8" for file names.
*
@@ -812,6 +832,66 @@ public class ZipFile implements Closeable {
this(new File(name).toPath(), encoding, true);
}
+ private static SeekableByteChannel openZipChannel(Path path, long
maxNumberOfDisks, OpenOption[] openOptions) throws IOException {
+ FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
+ List<FileChannel> channels = new ArrayList<>();
+ try {
+ boolean is64 = positionAtEndOfCentralDirectoryRecord(channel);
+ long numberOfDisks;
+ if (is64) {
+ channel.position(channel.position() + ZipConstants.WORD +
ZipConstants.WORD + ZipConstants.DWORD);
+ ByteBuffer buf = ByteBuffer.allocate(ZipConstants.WORD);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ IOUtils.readFully(channel, buf);
+ buf.flip();
+ numberOfDisks = buf.getInt() & 0xffffffffL;
+ } else {
+ channel.position(channel.position() + ZipConstants.WORD);
+ ByteBuffer buf = ByteBuffer.allocate(ZipConstants.SHORT);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ IOUtils.readFully(channel, buf);
+ buf.flip();
+ numberOfDisks = (buf.getShort() & 0xffff) + 1;
+ }
+ if (numberOfDisks > Math.min(maxNumberOfDisks, Integer.MAX_VALUE))
{
+ throw new IOException("Too many disks for zip archive, max=" +
+ Math.min(maxNumberOfDisks, Integer.MAX_VALUE) + "
actual=" + numberOfDisks);
+ }
+
+ if (numberOfDisks <= 1) {
+ return channel;
+ }
+ channel.close();
+
+ Path parent = path.getParent();
+ String basename =
FilenameUtils.removeExtension(path.getFileName().toString());
+
+ return ZipSplitReadOnlySeekableByteChannel.forPaths(
+ IntStream.range(0, (int) numberOfDisks)
+ .mapToObj(i -> {
+ if (i == numberOfDisks - 1) {
+ return path;
+ }
+ Path lowercase =
parent.resolve(String.format("%s.z%02d", basename, i + 1));
+ if (Files.exists(lowercase)) {
+ return lowercase;
+ }
+ Path uppercase =
parent.resolve(String.format("%s.Z%02d", basename, i + 1));
+ if (Files.exists(uppercase)) {
+ return uppercase;
+ }
+ return lowercase;
+ })
+ .collect(Collectors.toList()),
+ openOptions
+ );
+ } catch (Throwable ex) {
+ IOUtils.closeQuietly(channel);
+ channels.forEach(IOUtils::closeQuietly);
+ throw ex;
+ }
+ }
+
/**
* Whether this class is able to read the given entry.
* <p>
@@ -1152,20 +1232,8 @@ public class ZipFile implements Closeable {
* stream at the first central directory record.
*/
private void positionAtCentralDirectory() throws IOException {
- positionAtEndOfCentralDirectoryRecord();
- boolean found = false;
- final boolean searchedForZip64EOCD = archive.position() >
ZIP64_EOCDL_LENGTH;
- if (searchedForZip64EOCD) {
- archive.position(archive.position() - ZIP64_EOCDL_LENGTH);
- wordBbuf.rewind();
- IOUtils.readFully(archive, wordBbuf);
- found = Arrays.equals(ZipArchiveOutputStream.ZIP64_EOCD_LOC_SIG,
wordBuf);
- }
- if (!found) {
- // not a ZIP64 archive
- if (searchedForZip64EOCD) {
- skipBytes(ZIP64_EOCDL_LENGTH - ZipConstants.WORD);
- }
+ boolean is64 = positionAtEndOfCentralDirectoryRecord(archive);
+ if (!is64) {
positionAtCentralDirectory32();
} else {
positionAtCentralDirectory64();
@@ -1214,6 +1282,7 @@ public class ZipFile implements Closeable {
* Expects stream to be positioned right behind the "Zip64 end of
central directory locator"'s signature.
*/
private void positionAtCentralDirectory64() throws IOException {
+ skipBytes(ZipConstants.WORD);
if (isSplitZipArchive) {
wordBbuf.rewind();
IOUtils.readFully(archive, wordBbuf);
@@ -1260,12 +1329,32 @@ public class ZipFile implements Closeable {
/**
* Searches for the and positions the stream at the start of the "End
of central dir record".
+ *
+ * @return
+ * true if it's Zip64 end of central directory or false if it's Zip32
*/
- private void positionAtEndOfCentralDirectoryRecord() throws IOException {
- final boolean found = tryToLocateSignature(MIN_EOCD_SIZE,
MAX_EOCD_SIZE, ZipArchiveOutputStream.EOCD_SIG);
+ private static boolean
positionAtEndOfCentralDirectoryRecord(SeekableByteChannel channel) throws
IOException {
+ final boolean found = tryToLocateSignature(channel, MIN_EOCD_SIZE,
MAX_EOCD_SIZE, ZipArchiveOutputStream.EOCD_SIG);
if (!found) {
throw new ZipException("Archive is not a ZIP archive");
}
+ boolean found64 = false;
+ long position = channel.position();
+ if (position > ZIP64_EOCDL_LENGTH) {
+ ByteBuffer wordBuf = ByteBuffer.allocate(4);
+ channel.position(channel.position() - ZIP64_EOCDL_LENGTH);
+ wordBuf.rewind();
+ IOUtils.readFully(channel, wordBuf);
+ wordBuf.flip();
+ found64 =
wordBuf.equals(ByteBuffer.wrap(ZipArchiveOutputStream.ZIP64_EOCD_LOC_SIG));
+ if (!found64) {
+ channel.position(position);
+ } else {
+ channel.position(channel.position() - ZipConstants.WORD);
+ }
+ }
+
+ return found64;
}
/**
@@ -1553,27 +1642,33 @@ public class ZipFile implements Closeable {
* Searches the archive backwards from minDistance to maxDistance for the
given signature, positions the RandomaccessFile right at the signature if it has
* been found.
*/
- private boolean tryToLocateSignature(final long minDistanceFromEnd, final
long maxDistanceFromEnd, final byte[] sig) throws IOException {
+ private static boolean tryToLocateSignature(
+ final SeekableByteChannel channel,
+ final long minDistanceFromEnd,
+ final long maxDistanceFromEnd,
+ final byte[] sig
+ ) throws IOException {
+ ByteBuffer wordBuf = ByteBuffer.allocate(ZipConstants.WORD);
boolean found = false;
- long off = archive.size() - minDistanceFromEnd;
- final long stopSearching = Math.max(0L, archive.size() -
maxDistanceFromEnd);
+ long off = channel.size() - minDistanceFromEnd;
+ final long stopSearching = Math.max(0L, channel.size() -
maxDistanceFromEnd);
if (off >= 0) {
for (; off >= stopSearching; off--) {
- archive.position(off);
+ channel.position(off);
try {
- wordBbuf.rewind();
- IOUtils.readFully(archive, wordBbuf);
- wordBbuf.flip();
+ wordBuf.rewind();
+ IOUtils.readFully(channel, wordBuf);
+ wordBuf.flip();
} catch (final EOFException ex) { // NOSONAR
break;
}
- int curr = wordBbuf.get();
+ int curr = wordBuf.get();
if (curr == sig[POS_0]) {
- curr = wordBbuf.get();
+ curr = wordBuf.get();
if (curr == sig[POS_1]) {
- curr = wordBbuf.get();
+ curr = wordBuf.get();
if (curr == sig[POS_2]) {
- curr = wordBbuf.get();
+ curr = wordBuf.get();
if (curr == sig[POS_3]) {
found = true;
break;
@@ -1584,7 +1679,7 @@ public class ZipFile implements Closeable {
}
}
if (found) {
- archive.position(off);
+ channel.position(off);
}
return found;
}
diff --git
a/src/main/java/org/apache/commons/compress/archivers/zip/ZipIoUtil.java
b/src/main/java/org/apache/commons/compress/archivers/zip/ZipIoUtil.java
new file mode 100644
index 000000000..bc6840bd0
--- /dev/null
+++ b/src/main/java/org/apache/commons/compress/archivers/zip/ZipIoUtil.java
@@ -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.archivers.zip;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+
+
+/**
+ * IO utilities for Zip operations.
+ *
+ * Package private to potentially move to something reusable.
+ */
+class ZipIoUtil {
+ /**
+ * Writes full buffer to channel.
+ *
+ * @param channel
+ * channel to write to
+ * @param buf
+ * buffer to write
+ * @throws IOException
+ * when writing fails or fails to write fully
+ */
+ static void writeFully(SeekableByteChannel channel, ByteBuffer buf) throws
IOException {
+ while (buf.hasRemaining()) {
+ int remaining = buf.remaining();
+ int written = channel.write(buf);
+ if (written <= 0) {
+ throw new IOException("Failed to fully write: channel=" +
channel + " length=" + remaining + " written=" + written);
+ }
+ }
+ }
+
+ /**
+ * Writes full buffer to channel at specified position.
+ *
+ * @param channel
+ * channel to write to
+ * @param buf
+ * buffer to write
+ * @param position
+ * position to write at
+ * @throws IOException
+ * when writing fails or fails to write fully
+ */
+ static void writeFullyAt(FileChannel channel, ByteBuffer buf, long
position) throws IOException {
+ for (long currentPosition = position; buf.hasRemaining(); ) {
+ int remaining = buf.remaining();
+ int written = channel.write(buf, currentPosition);
+ if (written <= 0) {
+ throw new IOException("Failed to fully write: channel=" +
channel + " length=" + remaining + " written=" + written);
+ }
+ currentPosition += written;
+ }
+ }
+
+ private ZipIoUtil() {
+ }
+}
diff --git
a/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitOutputStream.java
b/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitOutputStream.java
index c8aae07ca..8d05ecfe5 100644
---
a/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitOutputStream.java
+++
b/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitOutputStream.java
@@ -18,11 +18,17 @@ package org.apache.commons.compress.archivers.zip;
import java.io.File;
import java.io.IOException;
-import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.TreeMap;
import org.apache.commons.compress.utils.FileNameUtils;
@@ -31,7 +37,7 @@ import org.apache.commons.compress.utils.FileNameUtils;
*
* @since 1.20
*/
-final class ZipSplitOutputStream extends OutputStream {
+final class ZipSplitOutputStream extends RandomAccessOutputStream {
/**
* 8.5.1 Capacities for split archives are as follows:
@@ -42,14 +48,20 @@ final class ZipSplitOutputStream extends OutputStream {
private static final long ZIP_SEGMENT_MIN_SIZE = 64 * 1024L;
private static final long ZIP_SEGMENT_MAX_SIZE = 4294967295L;
- private OutputStream outputStream;
+ private FileChannel currentChannel;
+ private FileRandomAccessOutputStream outputStream;
private Path zipFile;
private final long splitSize;
+ private long totalPosition;
private int currentSplitSegmentIndex;
private long currentSplitSegmentBytesWritten;
private boolean finished;
private final byte[] singleByte = new byte[1];
+ private List<Long> diskToPosition = new ArrayList<>();
+
+ private TreeMap<Long, Path> positionToFiles = new TreeMap<>();
+
/**
* Creates 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
@@ -79,7 +91,10 @@ final class ZipSplitOutputStream extends OutputStream {
}
this.zipFile = zipFile;
this.splitSize = splitSize;
- this.outputStream = Files.newOutputStream(zipFile);
+ this.outputStream = new FileRandomAccessOutputStream(zipFile);
+ this.currentChannel = this.outputStream.channel();
+ this.positionToFiles.put(0L, this.zipFile);
+ this.diskToPosition.add(0L);
// write the ZIP split signature 0x08074B50 to the ZIP file
writeZipSplitSignature();
}
@@ -107,6 +122,15 @@ final class ZipSplitOutputStream extends OutputStream {
* @throws IOException
*/
private Path createNewSplitSegmentFile(final Integer
zipSplitSegmentSuffixIndex) throws IOException {
+ Path newFile = getSplitSegmentFilename(zipSplitSegmentSuffixIndex);
+
+ if (Files.exists(newFile)) {
+ throw new IOException("split ZIP segment " + newFile + " already
exists");
+ }
+ return newFile;
+ }
+
+ private Path getSplitSegmentFilename(final Integer
zipSplitSegmentSuffixIndex) throws IOException {
final int newZipSplitSegmentSuffixIndex = zipSplitSegmentSuffixIndex
== null ? currentSplitSegmentIndex + 2 : zipSplitSegmentSuffixIndex;
final String baseName = FileNameUtils.getBaseName(zipFile);
String extension = ".z";
@@ -120,12 +144,10 @@ final class ZipSplitOutputStream extends OutputStream {
final String dir = Objects.nonNull(parent) ?
parent.toAbsolutePath().toString() : ".";
final Path newFile = zipFile.getFileSystem().getPath(dir, baseName +
extension);
- if (Files.exists(newFile)) {
- throw new IOException("split ZIP segment " + baseName + extension
+ " already exists");
- }
return newFile;
}
+
/**
* The last ZIP split segment's suffix should be .zip
*
@@ -161,16 +183,26 @@ final class ZipSplitOutputStream extends OutputStream {
outputStream.close();
newFile = createNewSplitSegmentFile(1);
Files.move(zipFile, newFile, StandardCopyOption.ATOMIC_MOVE);
+ this.positionToFiles.put(0L, newFile);
}
newFile = createNewSplitSegmentFile(null);
outputStream.close();
- outputStream = Files.newOutputStream(newFile);
+ outputStream = new FileRandomAccessOutputStream(newFile);
+ currentChannel = outputStream.channel();
currentSplitSegmentBytesWritten = 0;
zipFile = newFile;
currentSplitSegmentIndex++;
+ this.diskToPosition.add(this.totalPosition);
+ this.positionToFiles.put(this.totalPosition, newFile);
+ }
+ public long calculateDiskPosition(long disk, long localOffset) throws
IOException {
+ if (disk >= Integer.MAX_VALUE) {
+ throw new IOException("Disk number exceeded internal limits:
limit=" + Integer.MAX_VALUE + " requested=" + disk);
+ }
+ return diskToPosition.get((int) disk) + localOffset;
}
/**
@@ -224,6 +256,7 @@ final class ZipSplitOutputStream extends OutputStream {
} else {
outputStream.write(b, off, len);
currentSplitSegmentBytesWritten += len;
+ totalPosition += len;
}
}
@@ -233,6 +266,49 @@ final class ZipSplitOutputStream extends OutputStream {
write(singleByte);
}
+ @Override
+ public long position() {
+ return totalPosition;
+ }
+
+ @Override
+ public void writeFullyAt(final byte[] b, final int off, final int len,
final long atPosition) throws IOException {
+ long remainingPosition = atPosition;
+ for (int remainingOff = off, remainingLen = len; remainingLen > 0; ) {
+ Map.Entry<Long, Path> segment =
positionToFiles.floorEntry(remainingPosition);
+ Long segmentEnd = positionToFiles.higherKey(remainingPosition);
+ if (segmentEnd == null) {
+ ZipIoUtil.writeFullyAt(this.currentChannel, ByteBuffer.wrap(b,
remainingOff, remainingLen), remainingPosition - segment.getKey());
+ remainingPosition += remainingLen;
+ remainingOff += remainingLen;
+ remainingLen = 0;
+ } else if (remainingPosition + remainingLen <= segmentEnd) {
+ writeToSegment(segment.getValue(), remainingPosition -
segment.getKey(), b, remainingOff, remainingLen);
+ remainingPosition += remainingLen;
+ remainingOff += remainingLen;
+ remainingLen = 0;
+ } else {
+ int toWrite = Math.toIntExact(segmentEnd - remainingPosition);
+ writeToSegment(segment.getValue(), remainingPosition -
segment.getKey(), b, remainingOff, toWrite);
+ remainingPosition += toWrite;
+ remainingOff += toWrite;
+ remainingLen -= toWrite;
+ }
+ }
+ }
+
+ private void writeToSegment(
+ final Path segment,
+ final long position,
+ final byte[] b,
+ final int off,
+ final int len
+ ) throws IOException {
+ try (FileChannel channel = FileChannel.open(segment,
StandardOpenOption.WRITE)) {
+ ZipIoUtil.writeFullyAt(channel, ByteBuffer.wrap(b, off, len),
position);
+ }
+ }
+
/**
* Write the ZIP split signature (0x08074B50) to the head of the first ZIP
split segment
*
@@ -241,5 +317,6 @@ final class ZipSplitOutputStream extends OutputStream {
private void writeZipSplitSignature() throws IOException {
outputStream.write(ZipArchiveOutputStream.DD_SIG);
currentSplitSegmentBytesWritten +=
ZipArchiveOutputStream.DD_SIG.length;
+ totalPosition += ZipArchiveOutputStream.DD_SIG.length;
}
}
diff --git
a/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitReadOnlySeekableByteChannel.java
b/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitReadOnlySeekableByteChannel.java
index a79b4c43b..20d7b8bed 100644
---
a/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitReadOnlySeekableByteChannel.java
+++
b/src/main/java/org/apache/commons/compress/archivers/zip/ZipSplitReadOnlySeekableByteChannel.java
@@ -23,6 +23,7 @@ import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
+import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
@@ -203,9 +204,24 @@ public class ZipSplitReadOnlySeekableByteChannel extends
MultiReadOnlySeekableBy
* @since 1.22
*/
public static SeekableByteChannel forPaths(final Path... paths) throws
IOException {
+ return forPaths(Arrays.asList(paths), new OpenOption[]{
StandardOpenOption.READ });
+ }
+
+ /**
+ * Concatenates the given file paths.
+ *
+ * @param paths the file paths to concatenate, note that the LAST FILE of
files should be the LAST SEGMENT(.zip) and these 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
+ * @since 1.22
+ */
+ public static SeekableByteChannel forPaths(final List<Path> paths,
OpenOption[] openOptions) throws IOException {
final List<SeekableByteChannel> channels = new ArrayList<>();
for (final Path path : Objects.requireNonNull(paths, "paths must not
be null")) {
- channels.add(Files.newByteChannel(path, StandardOpenOption.READ));
+ channels.add(Files.newByteChannel(path, openOptions));
}
if (channels.size() == 1) {
return channels.get(0);
diff --git a/src/test/java/org/apache/commons/compress/archivers/ZipTest.java
b/src/test/java/org/apache/commons/compress/archivers/ZipTest.java
index fb2b6493b..c48e7dcd5 100644
--- a/src/test/java/org/apache/commons/compress/archivers/ZipTest.java
+++ b/src/test/java/org/apache/commons/compress/archivers/ZipTest.java
@@ -29,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -135,6 +136,14 @@ public final class ZipTest extends AbstractTest {
return result;
}
+ private byte[] createArtificialData(int size) {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ for (int i = 0; i < size; i += 1) {
+ output.write((byte) ((i & 1) == 0 ? (i / 2 % 256) : (i / 2 /
256)));
+ }
+ return output.toByteArray();
+ }
+
private void createArchiveEntry(final String payload, final
ZipArchiveOutputStream zos, final String name) throws IOException {
final ZipArchiveEntry in = new ZipArchiveEntry(name);
zos.putArchiveEntry(in);
@@ -250,6 +259,155 @@ public final class ZipTest extends AbstractTest {
}
}
+ /**
+ * Tests split archive with 32-bit limit, both STORED and DEFLATED.
+ */
+ @Test
+ public void testBuildArtificialSplitZip32Test() throws IOException {
+ final File outputZipFile = newTempFile("artificialSplitZip.zip");
+ final long splitSize = 64 * 1024L; /* 64 KB */
+ try (ZipArchiveOutputStream zipArchiveOutputStream = new
ZipArchiveOutputStream(outputZipFile, splitSize)) {
+ zipArchiveOutputStream.setUseZip64(Zip64Mode.Never);
+ ZipArchiveEntry ze1 = new ZipArchiveEntry("file01");
+ ze1.setMethod(ZipEntry.STORED);
+ zipArchiveOutputStream.putArchiveEntry(ze1);
+ zipArchiveOutputStream.write(createArtificialData(65536));
+ zipArchiveOutputStream.closeArchiveEntry();
+ ZipArchiveEntry ze2 = new ZipArchiveEntry("file02");
+ ze2.setMethod(ZipEntry.DEFLATED);
+ zipArchiveOutputStream.putArchiveEntry(ze2);
+ zipArchiveOutputStream.write(createArtificialData(65536));
+ zipArchiveOutputStream.closeArchiveEntry();
+ }
+
+ try (ZipFile zipFile = ZipFile.builder()
+ .setPath(outputZipFile.toPath())
+ .setMaxNumberOfDisks(Integer.MAX_VALUE)
+ .get()
+ ) {
+ assertArrayEquals(createArtificialData(65536),
IOUtils.toByteArray(zipFile.getInputStream(zipFile.getEntry("file01"))));
+ assertArrayEquals(createArtificialData(65536),
IOUtils.toByteArray(zipFile.getInputStream(zipFile.getEntry("file02"))));
+ }
+ }
+
+ /**
+ * Tests split archive with 64-bit limit, both STORED and DEFLATED.
+ */
+ @Test
+ public void testBuildArtificialSplitZip64Test() throws IOException {
+ final File outputZipFile = newTempFile("artificialSplitZip.zip");
+ final long splitSize = 64 * 1024L; /* 64 KB */
+ byte[] data = createArtificialData(128 * 1024);
+ try (ZipArchiveOutputStream zipArchiveOutputStream = new
ZipArchiveOutputStream(outputZipFile, splitSize)) {
+ zipArchiveOutputStream.setUseZip64(Zip64Mode.Always);
+ ZipArchiveEntry ze1 = new ZipArchiveEntry("file01");
+ ze1.setMethod(ZipEntry.STORED);
+ zipArchiveOutputStream.putArchiveEntry(ze1);
+ zipArchiveOutputStream.write(data);
+ zipArchiveOutputStream.closeArchiveEntry();
+ ZipArchiveEntry ze2 = new ZipArchiveEntry("file02");
+ ze2.setMethod(ZipEntry.DEFLATED);
+ zipArchiveOutputStream.putArchiveEntry(ze2);
+ zipArchiveOutputStream.write(data);
+ zipArchiveOutputStream.closeArchiveEntry();
+ }
+
+ try (ZipFile zipFile = ZipFile.builder()
+ .setPath(outputZipFile.toPath())
+ .setMaxNumberOfDisks(Integer.MAX_VALUE)
+ .get()
+ ) {
+ assertArrayEquals(data,
IOUtils.toByteArray(zipFile.getInputStream(zipFile.getEntry("file01"))));
+ assertArrayEquals(data,
IOUtils.toByteArray(zipFile.getInputStream(zipFile.getEntry("file02"))));
+ }
+ }
+
+ /**
+ * Tests split archive with 32-bit limit, with file local headers crossing
segment boundaries.
+ */
+ @Test
+ public void testBuildSplitZip32_metaCrossBoundary() throws IOException {
+ final File outputZipFile = newTempFile("artificialSplitZip.zip");
+ final long splitSize = 64 * 1024L; /* 64 KB */
+ // 4 is PK signature, 36 is size of header + local file header,
+ // 15 is next local file header up to second byte of CRC
+ byte[] data1 = createArtificialData(64 * 1024 - 4 - 36 - 15);
+ // 21 is remaining size of second local file header
+ // 19 is next local file header up to second byte of compressed size
+ byte[] data2 = createArtificialData(64 * 1024 - 21 - 19);
+ // 17 is remaining size of third local file header
+ // 23 is next local file header up to second byte of uncompressed size
+ byte[] data3 = createArtificialData(64 * 1024 - 17 - 23);
+ // 13 is remaining size of third local file header
+ // 1 is to wrap to next part
+ byte[] data4 = createArtificialData(64 * 1024 - 13 + 1);
+ try (ZipArchiveOutputStream zipArchiveOutputStream = new
ZipArchiveOutputStream(outputZipFile, splitSize)) {
+ zipArchiveOutputStream.setUseZip64(Zip64Mode.Never);
+ ZipArchiveEntry ze1 = new ZipArchiveEntry("file01");
+ ze1.setMethod(ZipEntry.STORED);
+ zipArchiveOutputStream.putArchiveEntry(ze1);
+ zipArchiveOutputStream.write(data1);
+ zipArchiveOutputStream.closeArchiveEntry();
+ ZipArchiveEntry ze2 = new ZipArchiveEntry("file02");
+ ze2.setMethod(ZipEntry.STORED);
+ zipArchiveOutputStream.putArchiveEntry(ze2);
+ zipArchiveOutputStream.write(data2);
+ zipArchiveOutputStream.closeArchiveEntry();
+ ZipArchiveEntry ze3 = new ZipArchiveEntry("file03");
+ ze3.setMethod(ZipEntry.STORED);
+ zipArchiveOutputStream.putArchiveEntry(ze3);
+ zipArchiveOutputStream.write(data3);
+ zipArchiveOutputStream.closeArchiveEntry();
+ ZipArchiveEntry ze4 = new ZipArchiveEntry("file04");
+ ze4.setMethod(ZipEntry.STORED);
+ zipArchiveOutputStream.putArchiveEntry(ze4);
+ zipArchiveOutputStream.write(data4);
+ zipArchiveOutputStream.closeArchiveEntry();
+ }
+
+ try (ZipFile zipFile = ZipFile.builder()
+ .setPath(outputZipFile.toPath())
+ .setMaxNumberOfDisks(Integer.MAX_VALUE)
+ .get()
+ ) {
+ assertArrayEquals(data1,
IOUtils.toByteArray(zipFile.getInputStream(zipFile.getEntry("file01"))));
+ assertArrayEquals(data2,
IOUtils.toByteArray(zipFile.getInputStream(zipFile.getEntry("file02"))));
+ assertArrayEquals(data3,
IOUtils.toByteArray(zipFile.getInputStream(zipFile.getEntry("file03"))));
+ assertArrayEquals(data4,
IOUtils.toByteArray(zipFile.getInputStream(zipFile.getEntry("file04"))));
+ }
+ }
+
+ /**
+ * Tests split archive with 32-bit limit, with end of central directory
skipping lack of space in segment.
+ */
+ @Test
+ public void testBuildSplitZip32_endOfCentralDirectorySkipBoundary() throws
IOException {
+ final File outputZipFile = newTempFile("artificialSplitZip.zip");
+ final long splitSize = 64 * 1024L; /* 64 KB */
+ // 4 is PK signature, 36 is size of header + local file header,
+ // 36 is length of central directory entry
+ // 1 is remaining byte in first archive, this should be skipped
+ byte[] data1 = createArtificialData(64 * 1024 - 4 - 36 - 52 - 1);
+ try (ZipArchiveOutputStream zipArchiveOutputStream = new
ZipArchiveOutputStream(outputZipFile, splitSize)) {
+ zipArchiveOutputStream.setUseZip64(Zip64Mode.Never);
+ ZipArchiveEntry ze1 = new ZipArchiveEntry("file01");
+ ze1.setMethod(ZipEntry.STORED);
+ zipArchiveOutputStream.putArchiveEntry(ze1);
+ zipArchiveOutputStream.write(data1);
+ zipArchiveOutputStream.closeArchiveEntry();
+ }
+
+ assertEquals(64 * 1024L - 1,
Files.size(outputZipFile.toPath().getParent().resolve("artificialSplitZip.z01")));
+
+ try (ZipFile zipFile = ZipFile.builder()
+ .setPath(outputZipFile.toPath())
+ .setMaxNumberOfDisks(Integer.MAX_VALUE)
+ .get()
+ ) {
+ assertArrayEquals(data1,
IOUtils.toByteArray(zipFile.getInputStream(zipFile.getEntry("file01"))));
+ }
+ }
+
@Test
public void testBuildSplitZipWithSegmentAlreadyExistThrowsException()
throws IOException {
final File directoryToZip = getFilesToZip();
diff --git
a/src/test/java/org/apache/commons/compress/archivers/zip/FileRandomAccessOutputStreamTest.java
b/src/test/java/org/apache/commons/compress/archivers/zip/FileRandomAccessOutputStreamTest.java
new file mode 100644
index 000000000..e23dbf05b
--- /dev/null
+++
b/src/test/java/org/apache/commons/compress/archivers/zip/FileRandomAccessOutputStreamTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+
+import org.apache.commons.compress.AbstractTempDirTest;
+import org.junit.jupiter.api.Test;
+
+
+public class FileRandomAccessOutputStreamTest extends AbstractTempDirTest {
+ @Test
+ public void testChannelReturn() throws IOException {
+ Path file = newTempPath("testChannel");
+ try (FileRandomAccessOutputStream stream = new
FileRandomAccessOutputStream(file)) {
+ assertNotNull(stream.channel());
+ }
+ }
+
+ @Test
+ public void testWrite() throws IOException {
+ FileChannel channel = mock(FileChannel.class);
+ FileRandomAccessOutputStream stream = new
FileRandomAccessOutputStream(channel);
+ when(channel.write((ByteBuffer) any()))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(5);
+ return 5;
+ })
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(6);
+ return 6;
+ });
+ stream.write("hello".getBytes(StandardCharsets.UTF_8));
+ stream.write("world\n".getBytes(StandardCharsets.UTF_8));
+ verify(channel, times(2))
+ .write((ByteBuffer) any());
+
+ assertEquals(11, stream.position());
+ }
+
+ @Test
+ public void testWriteFullyAt_whenFullAtOnce_thenSucceed() throws
IOException {
+ FileChannel channel = mock(FileChannel.class);
+ FileRandomAccessOutputStream stream = new
FileRandomAccessOutputStream(channel);
+ when(channel.write((ByteBuffer) any(), eq(20L)))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(5);
+ return 5;
+ });
+ when(channel.write((ByteBuffer) any(), eq(30L)))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(6);
+ return 6;
+ });
+ stream.writeFullyAt("hello".getBytes(StandardCharsets.UTF_8), 20);
+ stream.writeFullyAt("world\n".getBytes(StandardCharsets.UTF_8), 30);
+
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(20L));
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(30L));
+
+ assertEquals(0, stream.position());
+ }
+
+ @Test
+ public void testWriteFullyAt_whenFullButPartial_thenSucceed() throws
IOException {
+ FileChannel channel = mock(FileChannel.class);
+ FileRandomAccessOutputStream stream = new
FileRandomAccessOutputStream(channel);
+ when(channel.write((ByteBuffer) any(), eq(20L)))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(3);
+ return 3;
+ });
+ when(channel.write((ByteBuffer) any(), eq(23L)))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(5);
+ return 2;
+ });
+ when(channel.write((ByteBuffer) any(), eq(30L)))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(6);
+ return 6;
+ });
+ stream.writeFullyAt("hello".getBytes(StandardCharsets.UTF_8), 20);
+ stream.writeFullyAt("world\n".getBytes(StandardCharsets.UTF_8), 30);
+
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(20L));
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(23L));
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(30L));
+
+ assertEquals(0, stream.position());
+ }
+
+ @Test
+ public void testWriteFullyAt_whenPartial_thenFail() throws IOException {
+ FileChannel channel = mock(FileChannel.class);
+ FileRandomAccessOutputStream stream = new
FileRandomAccessOutputStream(channel);
+ when(channel.write((ByteBuffer) any(), eq(20L)))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(3);
+ return 3;
+ });
+ when(channel.write((ByteBuffer) any(), eq(23L)))
+ .thenAnswer(answer -> {
+ return 0;
+ });
+ assertThrows(IOException.class, () ->
stream.writeFullyAt("hello".getBytes(StandardCharsets.UTF_8), 20));
+
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(20L));
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(23L));
+ verify(channel, times(0))
+ .write((ByteBuffer) any(), eq(25L));
+
+ assertEquals(0, stream.position());
+ }
+}
diff --git
a/src/test/java/org/apache/commons/compress/archivers/zip/RandomAccessOutputStreamTest.java
b/src/test/java/org/apache/commons/compress/archivers/zip/RandomAccessOutputStreamTest.java
new file mode 100644
index 000000000..d6ccc62ab
--- /dev/null
+++
b/src/test/java/org/apache/commons/compress/archivers/zip/RandomAccessOutputStreamTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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 static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+
+import org.apache.commons.compress.AbstractTempDirTest;
+import org.junit.jupiter.api.Test;
+
+
+public class RandomAccessOutputStreamTest extends AbstractTempDirTest {
+
+ @Test
+ public void testWrite() throws IOException {
+ RandomAccessOutputStream delegate =
mock(RandomAccessOutputStream.class);
+
+ RandomAccessOutputStream stream = new RandomAccessOutputStream() {
+ @Override
+ public long position() throws IOException {
+ return delegate.position();
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ delegate.write(b, off, len);
+ }
+
+ @Override
+ void writeFullyAt(byte[] b, int off, int len, long position)
throws IOException {
+ delegate.writeFullyAt(b, off, len, position);
+ }
+ };
+
+ stream.write('\n');
+
+ verify(delegate, times(1))
+ .write(any(), eq(0), eq(1));
+ }
+}
diff --git
a/src/test/java/org/apache/commons/compress/archivers/zip/SeekableChannelRandomAccessOutputStreamTest.java
b/src/test/java/org/apache/commons/compress/archivers/zip/SeekableChannelRandomAccessOutputStreamTest.java
new file mode 100644
index 000000000..2ea7ee6c4
--- /dev/null
+++
b/src/test/java/org/apache/commons/compress/archivers/zip/SeekableChannelRandomAccessOutputStreamTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+import org.apache.commons.compress.AbstractTempDirTest;
+import org.junit.jupiter.api.Test;
+
+
+public class SeekableChannelRandomAccessOutputStreamTest extends
AbstractTempDirTest {
+
+ @Test
+ public void testInitialization() throws IOException {
+ Path file = newTempPath("testChannel");
+ try (SeekableChannelRandomAccessOutputStream stream = new
SeekableChannelRandomAccessOutputStream(
+ Files.newByteChannel(file, StandardOpenOption.CREATE,
StandardOpenOption.WRITE)
+ )) {
+ assertEquals(0, stream.position());
+ }
+ }
+
+ @Test
+ public void testWrite() throws IOException {
+ FileChannel channel = mock(FileChannel.class);
+ SeekableChannelRandomAccessOutputStream stream = new
SeekableChannelRandomAccessOutputStream(channel);
+
+ when(channel.position())
+ .thenReturn(11L);
+ when(channel.write((ByteBuffer) any()))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(5);
+ return 5;
+ })
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(6);
+ return 6;
+ });
+
+ stream.write("hello".getBytes(StandardCharsets.UTF_8));
+ stream.write("world\n".getBytes(StandardCharsets.UTF_8));
+
+ verify(channel, times(2))
+ .write((ByteBuffer) any());
+
+ assertEquals(11, stream.position());
+ }
+
+ @Test
+ public void testWriteFullyAt_whenFullAtOnce_thenSucceed() throws
IOException {
+ SeekableByteChannel channel = mock(SeekableByteChannel.class);
+ SeekableChannelRandomAccessOutputStream stream = new
SeekableChannelRandomAccessOutputStream(channel);
+
+ when(channel.position())
+ .thenReturn(50L)
+ .thenReturn(60L);
+ when(channel.write((ByteBuffer) any()))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(5);
+ return 5;
+ })
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(6);
+ return 6;
+ });
+
+ stream.writeFullyAt("hello".getBytes(StandardCharsets.UTF_8), 20);
+ stream.writeFullyAt("world\n".getBytes(StandardCharsets.UTF_8), 30);
+
+ verify(channel, times(2))
+ .write((ByteBuffer) any());
+ verify(channel, times(1))
+ .position(eq(50L));
+ verify(channel, times(1))
+ .position(eq(60L));
+
+ assertEquals(60L, stream.position());
+ }
+
+ @Test
+ public void testWriteFullyAt_whenFullButPartial_thenSucceed() throws
IOException {
+ SeekableByteChannel channel = mock(SeekableByteChannel.class);
+ SeekableChannelRandomAccessOutputStream stream = new
SeekableChannelRandomAccessOutputStream(channel);
+
+ when(channel.position())
+ .thenReturn(50L)
+ .thenReturn(60L);
+ when(channel.write((ByteBuffer) any()))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(3);
+ return 3;
+ })
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(5);
+ return 2;
+ })
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(6);
+ return 6;
+ });
+
+ stream.writeFullyAt("hello".getBytes(StandardCharsets.UTF_8), 20);
+ stream.writeFullyAt("world\n".getBytes(StandardCharsets.UTF_8), 30);
+
+ verify(channel, times(3))
+ .write((ByteBuffer) any());
+ verify(channel, times(1))
+ .position(eq(50L));
+ verify(channel, times(1))
+ .position(eq(60L));
+
+ assertEquals(60L, stream.position());
+ }
+
+ @Test
+ public void testWriteFullyAt_whenPartial_thenFail() throws IOException {
+ SeekableByteChannel channel = mock(SeekableByteChannel.class);
+ SeekableChannelRandomAccessOutputStream stream = new
SeekableChannelRandomAccessOutputStream(channel);
+
+ when(channel.position())
+ .thenReturn(50L);
+ when(channel.write((ByteBuffer) any()))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(3);
+ return 3;
+ })
+ .thenAnswer(answer -> {
+ return 0;
+ });
+
+ assertThrows(IOException.class, () ->
stream.writeFullyAt("hello".getBytes(StandardCharsets.UTF_8), 20));
+
+ verify(channel, times(2))
+ .write((ByteBuffer) any());
+
+ assertEquals(50L, stream.position());
+ }
+}
diff --git
a/src/test/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStreamTest.java
b/src/test/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStreamTest.java
new file mode 100644
index 000000000..0c3fb4899
--- /dev/null
+++
b/src/test/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStreamTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.apache.commons.compress.AbstractTempDirTest;
+import org.junit.jupiter.api.Test;
+
+
+public class ZipArchiveOutputStreamTest extends AbstractTempDirTest {
+
+ @Test
+ public void testOutputStreamBasics() throws IOException {
+ try (ZipArchiveOutputStream stream = new ZipArchiveOutputStream(new
ByteArrayOutputStream())) {
+ assertFalse(stream.isSeekable());
+ }
+ }
+
+ @Test
+ public void testFileBasics() throws IOException {
+ try (ZipArchiveOutputStream stream = new
ZipArchiveOutputStream(createTempFile())) {
+ assertTrue(stream.isSeekable());
+ }
+ }
+}
diff --git
a/src/test/java/org/apache/commons/compress/archivers/zip/ZipIoUtilTest.java
b/src/test/java/org/apache/commons/compress/archivers/zip/ZipIoUtilTest.java
new file mode 100644
index 000000000..7e1eb3d45
--- /dev/null
+++ b/src/test/java/org/apache/commons/compress/archivers/zip/ZipIoUtilTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.commons.compress.AbstractTempDirTest;
+import org.junit.jupiter.api.Test;
+
+
+public class ZipIoUtilTest extends AbstractTempDirTest {
+
+ @Test
+ public void testWriteFully_whenFullAtOnce_thenSucceed() throws IOException
{
+ SeekableByteChannel channel = mock(SeekableByteChannel.class);
+
+ when(channel.write((ByteBuffer) any()))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(5);
+ return 5;
+ })
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(6);
+ return 6;
+ });
+
+ ZipIoUtil.writeFully(channel,
ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)));
+ ZipIoUtil.writeFully(channel,
ByteBuffer.wrap("world\n".getBytes(StandardCharsets.UTF_8)));
+
+ verify(channel, times(2))
+ .write((ByteBuffer) any());
+ }
+
+ @Test
+ public void testWriteFully_whenFullButPartial_thenSucceed() throws
IOException {
+ SeekableByteChannel channel = mock(SeekableByteChannel.class);
+
+ when(channel.write((ByteBuffer) any()))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(3);
+ return 3;
+ })
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(5);
+ return 2;
+ })
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(6);
+ return 6;
+ });
+
+ ZipIoUtil.writeFully(channel,
ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)));
+ ZipIoUtil.writeFully(channel,
ByteBuffer.wrap("world\n".getBytes(StandardCharsets.UTF_8)));
+
+ verify(channel, times(3))
+ .write((ByteBuffer) any());
+ }
+
+ @Test
+ public void testWriteFully_whenPartial_thenFail() throws IOException {
+ SeekableByteChannel channel = mock(SeekableByteChannel.class);
+
+ when(channel.write((ByteBuffer) any()))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(3);
+ return 3;
+ })
+ .thenAnswer(answer -> {
+ return 0;
+ });
+
+ assertThrows(IOException.class, () ->
+ ZipIoUtil.writeFully(channel,
ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)))
+ );
+
+ verify(channel, times(2))
+ .write((ByteBuffer) any());
+ }
+
+ @Test
+ public void testWriteFullyAt_whenFullAtOnce_thenSucceed() throws
IOException {
+ FileChannel channel = mock(FileChannel.class);
+
+ when(channel.write((ByteBuffer) any(), eq(20L)))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(5);
+ return 5;
+ });
+ when(channel.write((ByteBuffer) any(), eq(30L)))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(6);
+ return 6;
+ });
+
+ ZipIoUtil.writeFullyAt(channel,
ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)), 20);
+ ZipIoUtil.writeFullyAt(channel,
ByteBuffer.wrap("world\n".getBytes(StandardCharsets.UTF_8)), 30);
+
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(20L));
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(30L));
+ }
+
+ @Test
+ public void testWriteFullyAt_whenFullButPartial_thenSucceed() throws
IOException {
+ FileChannel channel = mock(FileChannel.class);
+
+ when(channel.write((ByteBuffer) any(), eq(20L)))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(3);
+ return 3;
+ });
+ when(channel.write((ByteBuffer) any(), eq(23L)))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(5);
+ return 2;
+ });
+ when(channel.write((ByteBuffer) any(), eq(30L)))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(6);
+ return 6;
+ });
+
+ ZipIoUtil.writeFullyAt(channel,
ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)), 20);
+ ZipIoUtil.writeFullyAt(channel,
ByteBuffer.wrap("world\n".getBytes(StandardCharsets.UTF_8)), 30);
+
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(20L));
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(23L));
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(30L));
+ }
+
+ @Test
+ public void testWriteFullyAt_whenPartial_thenFail() throws IOException {
+ FileChannel channel = mock(FileChannel.class);
+
+ when(channel.write((ByteBuffer) any(), eq(20L)))
+ .thenAnswer(answer -> {
+ ((ByteBuffer) answer.getArgument(0)).position(3);
+ return 3;
+ });
+ when(channel.write((ByteBuffer) any(), eq(23L)))
+ .thenAnswer(answer -> {
+ return 0;
+ });
+ assertThrows(IOException.class, () ->
+ ZipIoUtil.writeFullyAt(channel,
ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)), 20));
+
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(20L));
+ verify(channel, times(1))
+ .write((ByteBuffer) any(), eq(23L));
+ verify(channel, times(0))
+ .write((ByteBuffer) any(), eq(25L));
+ }
+}
diff --git
a/src/test/java/org/apache/commons/compress/utils/ZipSplitReadOnlySeekableByteChannelTest.java
b/src/test/java/org/apache/commons/compress/utils/ZipSplitReadOnlySeekableByteChannelTest.java
index cc8b1e4a0..fddc1f26f 100644
---
a/src/test/java/org/apache/commons/compress/utils/ZipSplitReadOnlySeekableByteChannelTest.java
+++
b/src/test/java/org/apache/commons/compress/utils/ZipSplitReadOnlySeekableByteChannelTest.java
@@ -156,7 +156,7 @@ public class ZipSplitReadOnlySeekableByteChannelTest {
@Test
public void testForPathsOfTwoParametersThrowsOnNullArg() {
- assertThrows(NullPointerException.class, () ->
ZipSplitReadOnlySeekableByteChannel.forPaths(null, null));
+ assertThrows(NullPointerException.class, () ->
ZipSplitReadOnlySeekableByteChannel.forPaths((Path) null, null));
}
@Test