This is an automated email from the ASF dual-hosted git repository. bodewig 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 80124dd remove pattern where we first allocate an array and then try to fill it 80124dd is described below commit 80124dd9fe4b0a0b2e203ca19aacac8cd0afc96f Author: Stefan Bodewig <bode...@apache.org> AuthorDate: Fri Jul 2 20:00:47 2021 +0200 remove pattern where we first allocate an array and then try to fill it --- .../archivers/ar/ArArchiveInputStream.java | 16 ++-- .../archivers/arj/ArjArchiveInputStream.java | 29 ++++--- .../archivers/cpio/CpioArchiveInputStream.java | 19 +++-- .../archivers/dump/DumpArchiveInputStream.java | 10 ++- .../compress/archivers/dump/TapeInputStream.java | 11 ++- .../compress/archivers/examples/Expander.java | 2 +- .../compress/archivers/sevenz/SevenZFile.java | 5 +- .../commons/compress/archivers/tar/TarUtils.java | 4 +- .../commons/compress/archivers/zip/BinaryTree.java | 5 +- .../zip/X0017_StrongEncryptionHeader.java | 2 +- .../archivers/zip/ZipArchiveInputStream.java | 27 +++--- .../commons/compress/archivers/zip/ZipFile.java | 24 ++++-- .../org/apache/commons/compress/utils/IOUtils.java | 98 ++++++++++++++++++++++ .../apache/commons/compress/utils/IOUtilsTest.java | 84 +++++++++++++++---- 14 files changed, 258 insertions(+), 78 deletions(-) diff --git a/src/main/java/org/apache/commons/compress/archivers/ar/ArArchiveInputStream.java b/src/main/java/org/apache/commons/compress/archivers/ar/ArArchiveInputStream.java index 36ef33f..f30951d 100644 --- a/src/main/java/org/apache/commons/compress/archivers/ar/ArArchiveInputStream.java +++ b/src/main/java/org/apache/commons/compress/archivers/ar/ArArchiveInputStream.java @@ -101,8 +101,8 @@ public class ArArchiveInputStream extends ArchiveInputStream { if (offset == 0) { final byte[] expected = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER); - final byte[] realized = new byte[expected.length]; - final int read = IOUtils.readFully(input, realized); + final byte[] realized = IOUtils.readRange(input, expected.length); + final int read = realized.length; trackReadBytes(read); if (read != expected.length) { throw new IOException("Failed to read header. Occurred at byte: " + getBytesRead()); @@ -133,8 +133,8 @@ public class ArArchiveInputStream extends ArchiveInputStream { { final byte[] expected = ArchiveUtils.toAsciiBytes(ArArchiveEntry.TRAILER); - final byte[] realized = new byte[expected.length]; - final int read = IOUtils.readFully(input, realized); + final byte[] realized = IOUtils.readRange(input, expected.length); + final int read = realized.length; trackReadBytes(read); if (read != expected.length) { throw new IOException("Failed to read entry trailer. Occurred at byte: " + getBytesRead()); @@ -340,8 +340,8 @@ public class ArArchiveInputStream extends ArchiveInputStream { private String getBSDLongName(final String bsdLongName) throws IOException { final int nameLen = Integer.parseInt(bsdLongName.substring(BSD_LONGNAME_PREFIX_LEN)); - final byte[] name = new byte[nameLen]; - final int read = IOUtils.readFully(input, name); + final byte[] name = IOUtils.readRange(input, nameLen); + final int read = name.length; trackReadBytes(read); if (read != nameLen) { throw new EOFException(); @@ -386,8 +386,8 @@ public class ArArchiveInputStream extends ArchiveInputStream { */ private ArArchiveEntry readGNUStringTable(final byte[] length, final int offset, final int len) throws IOException { final int bufflen = asInt(length, offset, len); // Assume length will fit in an int - namebuffer = new byte[bufflen]; - final int read = IOUtils.readFully(input, namebuffer, 0, bufflen); + namebuffer = IOUtils.readRange(input, bufflen); + final int read = namebuffer.length; trackReadBytes(read); if (read != bufflen){ throw new IOException("Failed to read complete // record: expected=" diff --git a/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java b/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java index b0c16b2..4b20c12 100644 --- a/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java +++ b/src/main/java/org/apache/commons/compress/archivers/arj/ArjArchiveInputStream.java @@ -20,6 +20,7 @@ package org.apache.commons.compress.archivers.arj; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -122,10 +123,14 @@ public class ArjArchiveInputStream extends ArchiveInputStream { } } - private void readFully(final DataInputStream dataIn, final byte[] b) + private byte[] readRange(final InputStream in, final int len) throws IOException { - dataIn.readFully(b); + final byte[] b = IOUtils.readRange(in, len); count(b.length); + if (b.length < len) { + throw new EOFException(); + } + return b; } private byte[] readHeader() throws IOException { @@ -144,8 +149,7 @@ public class ArjArchiveInputStream extends ArchiveInputStream { return null; } if (basicHeaderSize <= 2600) { - basicHeaderBytes = new byte[basicHeaderSize]; - readFully(in, basicHeaderBytes); + basicHeaderBytes = readRange(in, basicHeaderSize); final long basicHeaderCrc32 = read32(in) & 0xFFFFFFFFL; final CRC32 crc32 = new CRC32(); crc32.update(basicHeaderBytes); @@ -166,8 +170,9 @@ public class ArjArchiveInputStream extends ArchiveInputStream { new ByteArrayInputStream(basicHeaderBytes)); final int firstHeaderSize = basicHeader.readUnsignedByte(); - final byte[] firstHeaderBytes = new byte[firstHeaderSize - 1]; - basicHeader.readFully(firstHeaderBytes); + final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1); + pushedBackBytes(firstHeaderBytes.length); + final DataInputStream firstHeader = new DataInputStream( new ByteArrayInputStream(firstHeaderBytes)); @@ -185,7 +190,7 @@ public class ArjArchiveInputStream extends ArchiveInputStream { hdr.securityEnvelopeFilePosition = read32(firstHeader); hdr.fileSpecPosition = read16(firstHeader); hdr.securityEnvelopeLength = read16(firstHeader); - pushedBackBytes(20); // count has already counted them via readFully + pushedBackBytes(20); // count has already counted them via readRange hdr.encryptionVersion = firstHeader.readUnsignedByte(); hdr.lastChapter = firstHeader.readUnsignedByte(); @@ -201,8 +206,7 @@ public class ArjArchiveInputStream extends ArchiveInputStream { final int extendedHeaderSize = read16(in); if (extendedHeaderSize > 0) { - hdr.extendedHeaderBytes = new byte[extendedHeaderSize]; - readFully(in, hdr.extendedHeaderBytes); + hdr.extendedHeaderBytes = readRange(in, extendedHeaderSize); final long extendedHeaderCrc32 = 0xffffFFFFL & read32(in); final CRC32 crc32 = new CRC32(); crc32.update(hdr.extendedHeaderBytes); @@ -222,8 +226,8 @@ public class ArjArchiveInputStream extends ArchiveInputStream { try (final DataInputStream basicHeader = new DataInputStream(new ByteArrayInputStream(basicHeaderBytes))) { final int firstHeaderSize = basicHeader.readUnsignedByte(); - final byte[] firstHeaderBytes = new byte[firstHeaderSize - 1]; - basicHeader.readFully(firstHeaderBytes); + final byte[] firstHeaderBytes = readRange(basicHeader, firstHeaderSize - 1); + pushedBackBytes(firstHeaderBytes.length); try (final DataInputStream firstHeader = new DataInputStream(new ByteArrayInputStream(firstHeaderBytes))) { final LocalFileHeader localFileHeader = new LocalFileHeader(); @@ -252,8 +256,7 @@ public class ArjArchiveInputStream extends ArchiveInputStream { final ArrayList<byte[]> extendedHeaders = new ArrayList<>(); int extendedHeaderSize; while ((extendedHeaderSize = read16(in)) > 0) { - final byte[] extendedHeaderBytes = new byte[extendedHeaderSize]; - readFully(in, extendedHeaderBytes); + final byte[] extendedHeaderBytes = readRange(in, extendedHeaderSize); final long extendedHeaderCrc32 = 0xffffFFFFL & read32(in); final CRC32 crc32 = new CRC32(); crc32.update(extendedHeaderBytes); diff --git a/src/main/java/org/apache/commons/compress/archivers/cpio/CpioArchiveInputStream.java b/src/main/java/org/apache/commons/compress/archivers/cpio/CpioArchiveInputStream.java index 38a8e0c..de57548 100644 --- a/src/main/java/org/apache/commons/compress/archivers/cpio/CpioArchiveInputStream.java +++ b/src/main/java/org/apache/commons/compress/archivers/cpio/CpioArchiveInputStream.java @@ -354,17 +354,25 @@ public class CpioArchiveInputStream extends ArchiveInputStream implements return count; } + private final byte[] readRange(final int len) + throws IOException { + final byte[] b = IOUtils.readRange(in, len); + count(b.length); + if (b.length < len) { + throw new EOFException(); + } + return b; + } + private long readBinaryLong(final int length, final boolean swapHalfWord) throws IOException { - final byte[] tmp = new byte[length]; - readFully(tmp, 0, tmp.length); + final byte[] tmp = readRange(length); return CpioUtil.byteArray2long(tmp, swapHalfWord); } private long readAsciiLong(final int length, final int radix) throws IOException { - final byte[] tmpBuffer = new byte[length]; - readFully(tmpBuffer, 0, tmpBuffer.length); + final byte[] tmpBuffer = readRange(length); return Long.parseLong(ArchiveUtils.toAsciiString(tmpBuffer), radix); } @@ -481,8 +489,7 @@ public class CpioArchiveInputStream extends ArchiveInputStream implements private String readCString(final int length) throws IOException { // don't include trailing NUL in file name to decode - final byte[] tmpBuffer = new byte[length - 1]; - readFully(tmpBuffer, 0, tmpBuffer.length); + final byte[] tmpBuffer = readRange(length - 1); if (this.in.read() == -1) { throw new EOFException(); } diff --git a/src/main/java/org/apache/commons/compress/archivers/dump/DumpArchiveInputStream.java b/src/main/java/org/apache/commons/compress/archivers/dump/DumpArchiveInputStream.java index 9eeae44..59c00b5 100644 --- a/src/main/java/org/apache/commons/compress/archivers/dump/DumpArchiveInputStream.java +++ b/src/main/java/org/apache/commons/compress/archivers/dump/DumpArchiveInputStream.java @@ -22,6 +22,7 @@ import org.apache.commons.compress.archivers.ArchiveException; import org.apache.commons.compress.archivers.ArchiveInputStream; import org.apache.commons.compress.archivers.zip.ZipEncoding; import org.apache.commons.compress.archivers.zip.ZipEncodingHelper; +import org.apache.commons.compress.utils.IOUtils; import java.io.EOFException; import java.io.IOException; @@ -337,10 +338,11 @@ public class DumpArchiveInputStream extends ArchiveInputStream { final int datalen = DumpArchiveConstants.TP_SIZE * entry.getHeaderCount(); if (blockBuffer.length < datalen) { - blockBuffer = new byte[datalen]; - } - - if (raw.read(blockBuffer, 0, datalen) != datalen) { + blockBuffer = IOUtils.readRange(raw, datalen); + if (blockBuffer.length != datalen) { + throw new EOFException(); + } + } else if (raw.read(blockBuffer, 0, datalen) != datalen) { throw new EOFException(); } diff --git a/src/main/java/org/apache/commons/compress/archivers/dump/TapeInputStream.java b/src/main/java/org/apache/commons/compress/archivers/dump/TapeInputStream.java index 25829f2..006953f 100644 --- a/src/main/java/org/apache/commons/compress/archivers/dump/TapeInputStream.java +++ b/src/main/java/org/apache/commons/compress/archivers/dump/TapeInputStream.java @@ -297,8 +297,7 @@ class TapeInputStream extends FilterInputStream { // this block is compressed. final int flags = (h >> 1) & 0x07; int length = (h >> 4) & 0x0FFFFFFF; - final byte[] compBuffer = new byte[length]; - readFully(compBuffer, 0, length); + final byte[] compBuffer = readRange(length); bytesRead += length; if (!decompress) { @@ -355,6 +354,14 @@ class TapeInputStream extends FilterInputStream { } } + private byte[] readRange(final int len) throws IOException { + final byte[] ret = IOUtils.readRange(in, len); + if (ret.length < len) { + throw new ShortFileException(); + } + return ret; + } + /** * Get number of bytes read. */ diff --git a/src/main/java/org/apache/commons/compress/archivers/examples/Expander.java b/src/main/java/org/apache/commons/compress/archivers/examples/Expander.java index bbbae00..d256df9 100644 --- a/src/main/java/org/apache/commons/compress/archivers/examples/Expander.java +++ b/src/main/java/org/apache/commons/compress/archivers/examples/Expander.java @@ -327,7 +327,7 @@ public class Expander { public void expand(final SevenZFile archive, final File targetDirectory) throws IOException, ArchiveException { expand(archive::getNextEntry, (entry, out) -> { - final byte[] buffer = new byte[8024]; + final byte[] buffer = new byte[8192]; int n; while (-1 != (n = archive.read(buffer))) { out.write(buffer, 0, n); diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java index fde329e..867ed18 100644 --- a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java +++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java @@ -646,7 +646,6 @@ public class SevenZFile implements Closeable { int nid = getUnsignedByte(input); while (nid != NID.kEnd) { final long propertySize = readUint64(input); - assertFitsIntoNonNegativeInt("propertySize", propertySize); final byte[] property = new byte[(int)propertySize]; get(input, property); nid = getUnsignedByte(input); @@ -704,8 +703,8 @@ public class SevenZFile implements Closeable { folder.getUnpackSize(), folder.crc); } final int unpackSize = assertFitsIntoNonNegativeInt("unpackSize", folder.getUnpackSize()); - final byte[] nextHeader = new byte[unpackSize]; - if (IOUtils.readFully(inputStreamStack, nextHeader) < unpackSize) { + final byte[] nextHeader = IOUtils.readRange(inputStreamStack, unpackSize); + if (nextHeader.length < unpackSize) { throw new IOException("premature end of stream"); } inputStreamStack.close(); diff --git a/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java b/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java index ec12f17..4dd0f58 100644 --- a/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java +++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarUtils.java @@ -754,8 +754,8 @@ public class TarUtils { throw new IOException("Paxheader value size " + restLen + " exceeds size of header record"); } else { - final byte[] rest = new byte[restLen]; - final int got = IOUtils.readFully(inputStream, rest); + final byte[] rest = IOUtils.readRange(inputStream, restLen); + final int got = rest.length; if (got != restLen) { throw new IOException("Failed to read " + "Paxheader. Expected " diff --git a/src/main/java/org/apache/commons/compress/archivers/zip/BinaryTree.java b/src/main/java/org/apache/commons/compress/archivers/zip/BinaryTree.java index 31153e1..a0ff91d 100644 --- a/src/main/java/org/apache/commons/compress/archivers/zip/BinaryTree.java +++ b/src/main/java/org/apache/commons/compress/archivers/zip/BinaryTree.java @@ -123,9 +123,8 @@ class BinaryTree { 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(inputStream, encodedTree); - if (read != size) { + final byte[] encodedTree = IOUtils.readRange(inputStream, size); + if (encodedTree.length != size) { throw new EOFException(); } diff --git a/src/main/java/org/apache/commons/compress/archivers/zip/X0017_StrongEncryptionHeader.java b/src/main/java/org/apache/commons/compress/archivers/zip/X0017_StrongEncryptionHeader.java index 5bd89f9..d843b89 100644 --- a/src/main/java/org/apache/commons/compress/archivers/zip/X0017_StrongEncryptionHeader.java +++ b/src/main/java/org/apache/commons/compress/archivers/zip/X0017_StrongEncryptionHeader.java @@ -371,11 +371,11 @@ public class X0017_StrongEncryptionHeader extends PKWareExtraHeader { this.hashSize = ZipShort.getValue(data, offset + ivSize + 22 + erdSize); final int resize = ZipShort.getValue(data, offset + ivSize + 24 + erdSize); - this.recipientKeyHash = new byte[this.hashSize]; if (resize < this.hashSize) { throw new ZipException("Invalid X0017_StrongEncryptionHeader: resize " + resize + " is too small to hold hashSize" + this.hashSize); } + this.recipientKeyHash = new byte[this.hashSize]; this.keyBlob = new byte[resize - this.hashSize]; // TODO: this looks suspicious, 26 rather than 24 would be "after" resize assertDynamicLengthFits("resize", resize, ivSize + 24 + erdSize, length); diff --git a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java index af3d45f..ab6994a 100644 --- a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java +++ b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java @@ -278,7 +278,7 @@ public class ZipArchiveInputStream extends ArchiveInputStream implements InputSt // first local file header - look for it and fail with // the appropriate error message if this is a split // archive. - readFirstLocalFileHeader(lfhBuf); + readFirstLocalFileHeader(); } else { readFully(lfhBuf); } @@ -339,15 +339,13 @@ public class ZipArchiveInputStream extends ArchiveInputStream implements InputSt final int extraLen = ZipShort.getValue(lfhBuf, off); off += SHORT; // NOSONAR - assignment as documentation - final byte[] fileName = new byte[fileNameLen]; - readFully(fileName); + final byte[] fileName = readRange(fileNameLen); 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); + final byte[] extraData = readRange(extraLen); try { current.entry.setExtra(extraData); } catch (RuntimeException ex) { @@ -410,9 +408,9 @@ public class ZipArchiveInputStream extends ArchiveInputStream implements InputSt * 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); + private void readFirstLocalFileHeader() throws IOException { + readFully(lfhBuf); + final ZipLong sig = new ZipLong(lfhBuf); if (!skipSplitSig && sig.equals(ZipLong.DD_SIG)) { throw new UnsupportedZipFeatureException(UnsupportedZipFeatureException.Feature.SPLITTING); @@ -423,8 +421,8 @@ public class ZipArchiveInputStream extends ArchiveInputStream implements InputSt // 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); + System.arraycopy(lfhBuf, 4, lfhBuf, 0, LFH_LEN - 4); + System.arraycopy(missedLfhBytes, 0, lfhBuf, LFH_LEN - 4, 4); } } @@ -882,6 +880,15 @@ public class ZipArchiveInputStream extends ArchiveInputStream implements InputSt } } + private byte[] readRange(int len) throws IOException { + final byte[] ret = IOUtils.readRange(in, len); + count(ret.length); + if (ret.length < len) { + throw new EOFException(); + } + return ret; + } + private void readDataDescriptor() throws IOException { readFully(wordBuf); ZipLong val = new ZipLong(wordBuf); 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 0f2bb53..e433c6e 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 @@ -820,8 +820,10 @@ public class ZipFile implements Closeable { ze.setExternalAttributes(ZipLong.getValue(cfhBuf, off)); off += WORD; - final byte[] fileName = new byte[fileNameLen]; - IOUtils.readFully(archive, ByteBuffer.wrap(fileName)); + final byte[] fileName = IOUtils.readRange(archive, fileNameLen); + if (fileName.length < fileNameLen) { + throw new EOFException(); + } ze.setName(entryEncoding.decode(fileName), fileName); // LFH offset, @@ -829,8 +831,10 @@ public class ZipFile implements Closeable { // data offset will be filled later entries.add(ze); - final byte[] cdExtraData = new byte[extraLen]; - IOUtils.readFully(archive, ByteBuffer.wrap(cdExtraData)); + final byte[] cdExtraData = IOUtils.readRange(archive, extraLen); + if (cdExtraData.length < extraLen) { + throw new EOFException(); + } try { ze.setCentralDirectoryExtra(cdExtraData); } catch (RuntimeException ex) { @@ -842,8 +846,10 @@ public class ZipFile implements Closeable { setSizesAndOffsetFromZip64Extra(ze); sanityCheckLFHOffset(ze); - final byte[] comment = new byte[commentLen]; - IOUtils.readFully(archive, ByteBuffer.wrap(comment)); + final byte[] comment = IOUtils.readRange(archive, commentLen); + if (comment.length < commentLen) { + throw new EOFException(); + } ze.setComment(entryEncoding.decode(comment)); if (!hasUTF8Flag && useUnicodeExtraFields) { @@ -1306,8 +1312,10 @@ public class ZipFile implements Closeable { final int fileNameLen = lens[0]; final int extraFieldLen = lens[1]; skipBytes(fileNameLen); - final byte[] localExtraData = new byte[extraFieldLen]; - IOUtils.readFully(archive, ByteBuffer.wrap(localExtraData)); + final byte[] localExtraData = IOUtils.readRange(archive, extraFieldLen); + if (localExtraData.length < extraFieldLen) { + throw new EOFException(); + } try { ze.setExtra(localExtraData); } catch (RuntimeException ex) { diff --git a/src/main/java/org/apache/commons/compress/utils/IOUtils.java b/src/main/java/org/apache/commons/compress/utils/IOUtils.java index 3587ced..3a1f582 100644 --- a/src/main/java/org/apache/commons/compress/utils/IOUtils.java +++ b/src/main/java/org/apache/commons/compress/utils/IOUtils.java @@ -280,4 +280,102 @@ public final class IOUtils { public static void copy(final File sourceFile, final OutputStream outputStream) throws IOException { Files.copy(sourceFile.toPath(), outputStream); } + + /** + * Copies part of the content of a InputStream into an OutputStream. + * Uses a default buffer size of 8024 bytes. + * + * @param input + * the InputStream to copy + * @param output + * the target Stream + * @param len + * maximum amount of bytes to copy + * @return the number of bytes copied + * @throws IOException + * if an error occurs + * @since 1.21 + */ + public static long copyRange(final InputStream input, final long len, final OutputStream output) + throws IOException { + return copyRange(input, len, output, COPY_BUF_SIZE); + } + + /** + * Copies part of the content of a InputStream into an OutputStream + * + * @param input + * the InputStream to copy + * @param len + * maximum amount of bytes to copy + * @param output + * the target Stream + * @param buffersize + * the buffer size to use, must be bigger than 0 + * @return the number of bytes copied + * @throws IOException + * if an error occurs + * @throws IllegalArgumentException + * if buffersize is smaller than or equal to 0 + * @since 1.21 + */ + public static long copyRange(final InputStream input, final long len, final OutputStream output, + final int buffersize) throws IOException { + if (buffersize < 1) { + throw new IllegalArgumentException("buffersize must be bigger than 0"); + } + final byte[] buffer = new byte[(int) Math.min(buffersize, len)]; + int n = 0; + long count = 0; + while (count < len && -1 != (n = input.read(buffer, 0, (int) Math.min(len - count, buffer.length)))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Gets part of the contents of an <code>InputStream</code> as a <code>byte[]</code>. + * + * @param input the <code>InputStream</code> to read from + * @param len + * maximum amount of bytes to copy + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 1.21 + */ + public static byte[] readRange(final InputStream input, final int len) throws IOException { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + copyRange(input, len, output); + return output.toByteArray(); + } + + /** + * Gets part of the contents of an <code>ReadableByteChannel</code> as a <code>byte[]</code>. + * + * @param input the <code>ReadableByteChannel</code> to read from + * @param len + * maximum amount of bytes to copy + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 1.21 + */ + public static byte[] readRange(final ReadableByteChannel input, final int len) throws IOException { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + final ByteBuffer b = ByteBuffer.allocate(Math.min(len, COPY_BUF_SIZE)); + int read = 0; + while (read < len) { + final int readNow = input.read(b); + if (readNow <= 0) { + break; + } + output.write(b.array(), 0, readNow); + b.rewind(); + read += readNow; + } + return output.toByteArray(); + } + } diff --git a/src/test/java/org/apache/commons/compress/utils/IOUtilsTest.java b/src/test/java/org/apache/commons/compress/utils/IOUtilsTest.java index 9b2b314..a692ef0 100644 --- a/src/test/java/org/apache/commons/compress/utils/IOUtilsTest.java +++ b/src/test/java/org/apache/commons/compress/utils/IOUtilsTest.java @@ -26,6 +26,8 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; +import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; + import org.junit.Assert; import org.junit.Test; @@ -92,24 +94,72 @@ public class IOUtilsTest { IOUtils.copy(new ByteArrayInputStream(ByteUtils.EMPTY_BYTE_ARRAY), new ByteArrayOutputStream(), 0); } + @Test(expected = IllegalArgumentException.class) + public void copyRangeThrowsOnZeroBufferSize() throws IOException { + IOUtils.copyRange(new ByteArrayInputStream(ByteUtils.EMPTY_BYTE_ARRAY), 5, new ByteArrayOutputStream(), 0); + } + + @Test + public void copyRangeDoesntCopyMoreThanAskedFor() throws IOException { + try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[] { 1, 2, 3, 4, 5 }); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + Assert.assertEquals(3, IOUtils.copyRange(in, 3, out)); + out.close(); + Assert.assertArrayEquals(new byte[] { 1, 2, 3 }, out.toByteArray()); + } + } + + @Test + public void copyRangeStopsIfThereIsNothingToCopyAnymore() throws IOException { + try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[] { 1, 2, 3, 4, 5 }); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + Assert.assertEquals(5, IOUtils.copyRange(in, 10, out)); + out.close(); + Assert.assertArrayEquals(new byte[] { 1, 2, 3, 4, 5 }, out.toByteArray()); + } + } + + @Test + public void readRangeFromStreamDoesntReadMoreThanAskedFor() throws IOException { + try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[] { 1, 2, 3, 4, 5 })) { + byte[] read = IOUtils.readRange(in, 3); + Assert.assertArrayEquals(new byte[] { 1, 2, 3 }, read); + Assert.assertEquals(4, in.read()); + } + } + + @Test + public void readRangeFromStreamStopsIfThereIsNothingToReadAnymore() throws IOException { + try (ByteArrayInputStream in = new ByteArrayInputStream(new byte[] { 1, 2, 3, 4, 5 })) { + byte[] read = IOUtils.readRange(in, 10); + Assert.assertArrayEquals(new byte[] { 1, 2, 3, 4, 5 }, read); + Assert.assertEquals(-1, in.read()); + } + } + + @Test + public void readRangeFromChannelDoesntReadMoreThanAskedFor() throws IOException { + try (ReadableByteChannel in = new SeekableInMemoryByteChannel(new byte[] { 1, 2, 3, 4, 5 })) { + byte[] read = IOUtils.readRange(in, 3); + Assert.assertArrayEquals(new byte[] { 1, 2, 3 }, read); + final ByteBuffer b = ByteBuffer.allocate(1); + Assert.assertEquals(1, in.read(b)); + Assert.assertArrayEquals(new byte[] { 4 }, b.array()); + } + } + + @Test + public void readRangeFromChannelStopsIfThereIsNothingToReadAnymore() throws IOException { + try (ReadableByteChannel in = new SeekableInMemoryByteChannel(new byte[] { 1, 2, 3, 4, 5 })) { + byte[] read = IOUtils.readRange(in, 10); + Assert.assertArrayEquals(new byte[] { 1, 2, 3, 4, 5 }, read); + final ByteBuffer b = ByteBuffer.allocate(1); + Assert.assertEquals(-1, in.read(b)); + } + } + private static void readFully(final byte[] source, final ByteBuffer b) throws IOException { - IOUtils.readFully(new ReadableByteChannel() { - private int idx; - @Override - public int read(final ByteBuffer buf) { - if (idx >= source.length) { - return -1; - } - buf.put(source[idx++]); - return 1; - } - @Override - public void close() { } - @Override - public boolean isOpen() { - return true; - } - }, b); + IOUtils.readFully(new SeekableInMemoryByteChannel(source), b); } private void skip(final StreamWrapper wrapper) throws Exception {