Skip to content

Commit

Permalink
#96 Read fully into buffer with retries
Browse files Browse the repository at this point in the history
  • Loading branch information
srikanth-lingala committed Nov 4, 2019
1 parent 04f0264 commit ca5e6f6
Show file tree
Hide file tree
Showing 10 changed files with 355 additions and 10 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.2.3</version>
<version>2.2.4.1-SNAPSHOT</version>

<name>Zip4j</name>
<description>Zip4j - A Java library for zip files and streams</description>
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/net/lingala/zip4j/ZipFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,8 @@ private void readZipInfo() throws ZipException {
HeaderReader headerReader = new HeaderReader();
zipModel = headerReader.readAllHeaders(randomAccessFile, charset);
zipModel.setZipFile(zipFile);
} catch (ZipException e) {
throw e;
} catch (IOException e) {
throw new ZipException(e);
}
Expand Down
15 changes: 9 additions & 6 deletions src/main/java/net/lingala/zip4j/headers/HeaderReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import static net.lingala.zip4j.util.BitUtils.isBitSet;
import static net.lingala.zip4j.util.InternalZipConstants.ENDHDR;
import static net.lingala.zip4j.util.InternalZipConstants.ZIP_64_SIZE_LIMIT;
import static net.lingala.zip4j.util.Zip4jUtil.readFully;

/**
* Helper class to read header information for the zip file
Expand All @@ -64,6 +65,8 @@ public ZipModel readAllHeaders(RandomAccessFile zip4jRaf, Charset charset) throw

try {
zipModel.setEndOfCentralDirectoryRecord(readEndOfCentralDirectoryRecord(zip4jRaf, rawIO));
} catch (ZipException e){
throw e;
} catch (IOException e) {
throw new ZipException("Zip headers not found. Probably not a zip file or a corrupted zip file", e);
}
Expand Down Expand Up @@ -302,7 +305,7 @@ private List<ExtraDataRecord> readExtraDataRecords(InputStream inputStream, int
}

byte[] extraFieldBuf = new byte[extraFieldLength];
inputStream.read(extraFieldBuf);
readFully(inputStream, extraFieldBuf);

try {
return parseExtraDataRecords(extraFieldBuf, extraFieldLength);
Expand Down Expand Up @@ -540,7 +543,7 @@ public LocalFileHeader readLocalFileHeader(InputStream inputStream, Charset char
localFileHeader.setVersionNeededToExtract(rawIO.readShortLittleEndian(inputStream));

byte[] generalPurposeFlags = new byte[2];
if (inputStream.read(generalPurposeFlags) != 2) {
if (readFully(inputStream, generalPurposeFlags) != 2) {
throw new ZipException("Could not read enough bytes for generalPurposeFlags");
}
localFileHeader.setEncrypted(isBitSet(generalPurposeFlags[0], 0));
Expand All @@ -552,7 +555,7 @@ public LocalFileHeader readLocalFileHeader(InputStream inputStream, Charset char
rawIO.readShortLittleEndian(inputStream)));
localFileHeader.setLastModifiedTime(rawIO.readIntLittleEndian(inputStream));

inputStream.read(intBuff);
readFully(inputStream, intBuff);
localFileHeader.setCrc(rawIO.readLongLittleEndian(intBuff, 0));
localFileHeader.setCrcRawData(intBuff.clone());

Expand All @@ -566,7 +569,7 @@ public LocalFileHeader readLocalFileHeader(InputStream inputStream, Charset char

if (fileNameLength > 0) {
byte[] fileNameBuf = new byte[fileNameLength];
inputStream.read(fileNameBuf);
readFully(inputStream, fileNameBuf);
// Modified after user reported an issue http://www.lingala.net/zip4j/forum/index.php?topic=2.0
// String fileName = new String(fileNameBuf, "Cp850");
// String fileName = Zip4jUtil.getCp850EncodedString(fileNameBuf);
Expand Down Expand Up @@ -612,15 +615,15 @@ public DataDescriptor readDataDescriptor(InputStream inputStream, boolean isZip6
DataDescriptor dataDescriptor = new DataDescriptor();

byte[] intBuff = new byte[4];
inputStream.read(intBuff);
readFully(inputStream, intBuff);
long sigOrCrc = rawIO.readLongLittleEndian(intBuff, 0);

//According to zip specification, presence of extra data record header signature is optional.
//If this signature is present, read it and read the next 4 bytes for crc
//If signature not present, assign the read 4 bytes for crc
if (sigOrCrc == HeaderSignature.EXTRA_DATA_RECORD.getValue()) {
dataDescriptor.setSignature(HeaderSignature.EXTRA_DATA_RECORD);
inputStream.read(intBuff);
readFully(inputStream, intBuff);
dataDescriptor.setCrc(rawIO.readLongLittleEndian(intBuff, 0));
} else {
dataDescriptor.setCrc(sigOrCrc);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.Arrays;

import static net.lingala.zip4j.util.InternalZipConstants.AES_AUTH_LENGTH;
import static net.lingala.zip4j.util.Zip4jUtil.readFully;

class AesCipherInputStream extends CipherInputStream<AESDecrypter> {

Expand Down Expand Up @@ -142,7 +143,7 @@ private void verifyContent(byte[] storedMac) throws IOException {

protected byte[] readStoredMac(InputStream inputStream) throws IOException {
byte[] storedMac = new byte[AES_AUTH_LENGTH];
int readLen = inputStream.read(storedMac);
int readLen = readFully(inputStream, storedMac);

if (readLen != AES_AUTH_LENGTH) {
throw new ZipException("Invalid AES Mac bytes. Could not read sufficient data");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import java.io.IOException;
import java.io.InputStream;

import static net.lingala.zip4j.util.Zip4jUtil.readFully;

abstract class CipherInputStream<T extends Decrypter> extends InputStream {

private ZipEntryInputStream zipEntryInputStream;
Expand Down Expand Up @@ -44,7 +46,7 @@ public int read(byte[] b) throws IOException {

@Override
public int read(byte[] b, int off, int len) throws IOException {
int readLen = zipEntryInputStream.read(b, off, len);
int readLen = readFully(zipEntryInputStream, b, off, len);

if (readLen > 0) {
cacheRawData(b, readLen);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/net/lingala/zip4j/util/RawIO.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public void writeLongLittleEndian(byte[] array, int pos, long value) {
}

private void readFully(InputStream inputStream, byte[] buff, int readLen) throws IOException {
int actualReadLength = inputStream.read(buff, 0, readLen);
int actualReadLength = Zip4jUtil.readFully(inputStream, buff, 0, readLen);
if (actualReadLength != readLen) {
throw new ZipException("Could not fill buffer");
}
Expand Down
77 changes: 77 additions & 0 deletions src/main/java/net/lingala/zip4j/util/Zip4jUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@
import net.lingala.zip4j.model.enums.CompressionMethod;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Calendar;

public class Zip4jUtil {

private static final int MAX_RAW_READ_FULLY_RETRY_ATTEMPTS = 15;

public static boolean isStringNotNullAndNotEmpty(String str) {
return str != null && str.trim().length() > 0;
}
Expand Down Expand Up @@ -95,4 +99,77 @@ public static CompressionMethod getCompressionMethod(LocalFileHeader localFileHe
return localFileHeader.getAesExtraDataRecord().getCompressionMethod();
}

public static int readFully(InputStream inputStream, byte[] bufferToReadInto) throws IOException {

int readLen = inputStream.read(bufferToReadInto);

if (readLen != bufferToReadInto.length) {
readLen = readUntilBufferIsFull(inputStream, bufferToReadInto, readLen);

if (readLen != bufferToReadInto.length) {
throw new IOException("Cannot read fully into byte buffer");
}
}

return readLen;
}

public static int readFully(InputStream inputStream, byte[] b, int offset, int length) throws IOException {
int numberOfBytesRead = 0;

if (offset < 0) {
throw new IllegalArgumentException("Negative offset");
}

if (length < 0) {
throw new IllegalArgumentException("Negative length");
}

if (length == 0) {
return 0;
}

if (offset + length > b.length) {
throw new IllegalArgumentException("Length greater than buffer size");
}

while (numberOfBytesRead != length) {
int currentReadLength = inputStream.read(b, offset + numberOfBytesRead, length - numberOfBytesRead);
if (currentReadLength == -1) {
if (numberOfBytesRead == 0) {
return -1;
}
return numberOfBytesRead;
}

numberOfBytesRead += currentReadLength;
}

return numberOfBytesRead;
}

private static int readUntilBufferIsFull(InputStream inputStream, byte[] bufferToReadInto, int readLength)
throws IOException {

int remainingLength = bufferToReadInto.length - readLength;
int loopReadLength = 0;
int retryAttempt = 1; // first attempt is already done before this method is called

while (readLength < bufferToReadInto.length
&& loopReadLength != -1
&& retryAttempt < MAX_RAW_READ_FULLY_RETRY_ATTEMPTS) {

loopReadLength = inputStream.read(bufferToReadInto, readLength, remainingLength);

if (loopReadLength > 0) {
readLength += loopReadLength;
remainingLength -= loopReadLength;
}

retryAttempt++;
}

return readLength;
}

}
89 changes: 89 additions & 0 deletions src/test/java/net/lingala/zip4j/ExtractZipFileIT.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.lingala.zip4j;

import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.model.enums.AesKeyStrength;
Expand All @@ -15,6 +16,7 @@

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.List;
Expand Down Expand Up @@ -329,6 +331,93 @@ public void testExtractZipFileWithCheckoutMismatchThrowsExceptionWithType() {
}
}

@Test
public void testExtractNestedZipFileWithNoEncryptionOnInnerAndOuter() throws IOException {
testExtractNestedZipFileWithEncrpytion(EncryptionMethod.NONE, EncryptionMethod.NONE);
}

@Test
public void testExtractNestedZipFileWithNoEncryptionOnInnerAndZipStandardOuter() throws IOException {
testExtractNestedZipFileWithEncrpytion(EncryptionMethod.NONE, EncryptionMethod.ZIP_STANDARD);
}

@Test
public void testExtractNestedZipFileWithNoEncryptionOnInnerAndAesdOuter() throws IOException {
testExtractNestedZipFileWithEncrpytion(EncryptionMethod.NONE, EncryptionMethod.AES);
}

@Test
public void testExtractNestedZipFileWithZipStandardEncryptionOnInnerAndNoneOuter() throws IOException {
testExtractNestedZipFileWithEncrpytion(EncryptionMethod.ZIP_STANDARD, EncryptionMethod.NONE);
}

@Test
public void testExtractNestedZipFileWitAesOnInnerAndNoneOuter() throws IOException {
testExtractNestedZipFileWithEncrpytion(EncryptionMethod.AES, EncryptionMethod.NONE);
}

@Test
public void testExtractNestedZipFileWithZipStandardEncryptionOnInnerAndAesOuter() throws IOException {
testExtractNestedZipFileWithEncrpytion(EncryptionMethod.ZIP_STANDARD, EncryptionMethod.AES);
}

@Test
public void testExtractNestedZipFileWithAesOnInnerAndZipStandardOuter() throws IOException {
testExtractNestedZipFileWithEncrpytion(EncryptionMethod.AES, EncryptionMethod.ZIP_STANDARD);
}

private void testExtractNestedZipFileWithEncrpytion(EncryptionMethod innerZipEncryption,
EncryptionMethod outerZipEncryption) throws IOException {
File innerZipFile = temporaryFolder.newFile("inner.zip");
File outerZipFile = temporaryFolder.newFile("outer.zip");

innerZipFile.delete();
outerZipFile.delete();

createNestedZip(innerZipFile, outerZipFile, innerZipEncryption, outerZipEncryption);

verifyNestedZipFile(outerZipFile, FILES_TO_ADD.size());
}

private void createNestedZip(File innerSourceZipFile, File outerSourceZipFile, EncryptionMethod innerEncryption,
EncryptionMethod outerEncryption) throws ZipException {

ZipFile innerZipFile = new ZipFile(innerSourceZipFile, PASSWORD);
ZipParameters innerZipParameters = createZipParametersForNestedZip(innerEncryption);
innerZipFile.addFiles(FILES_TO_ADD, innerZipParameters);

ZipFile outerZipFile = new ZipFile(outerSourceZipFile, PASSWORD);
ZipParameters outerZipParameters = createZipParametersForNestedZip(outerEncryption);
outerZipFile.addFile(innerSourceZipFile, outerZipParameters);
}

private void verifyNestedZipFile(File outerZipFileToVerify, int numberOfFilesInNestedZip) throws IOException {
ZipFile zipFile = new ZipFile(outerZipFileToVerify, PASSWORD);
FileHeader fileHeader = zipFile.getFileHeader("inner.zip");

assertThat(fileHeader).isNotNull();

int actualNumberOfFilesInNestedZip = 0;
try(InputStream inputStream = zipFile.getInputStream(fileHeader)) {
try(ZipInputStream zipInputStream = new ZipInputStream(inputStream, PASSWORD)) {
while (zipInputStream.getNextEntry() != null) {
actualNumberOfFilesInNestedZip++;
}
}
}

assertThat(actualNumberOfFilesInNestedZip).isEqualTo(numberOfFilesInNestedZip);
}

private ZipParameters createZipParametersForNestedZip(EncryptionMethod encryptionMethod) {
ZipParameters zipParameters = new ZipParameters();
if (encryptionMethod != null && !encryptionMethod.equals(EncryptionMethod.NONE)) {
zipParameters.setEncryptFiles(true);
zipParameters.setEncryptionMethod(encryptionMethod);
}
return zipParameters;
}

private void verifyNumberOfFilesInOutputFolder(File outputFolder, int numberOfExpectedFiles) {
assertThat(outputFolder.listFiles()).hasSize(numberOfExpectedFiles);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package net.lingala.zip4j.testutils;

import java.io.IOException;
import java.io.InputStream;

public class ControlledReadInputStream extends InputStream {

private InputStream inputStream;
private int readLimit;
private byte[] singleByteBuffer = new byte[1];

public ControlledReadInputStream(InputStream inputStream, int maximumNumberOfBytesToReadAtOnce) {
this.inputStream = inputStream;
this.readLimit = maximumNumberOfBytesToReadAtOnce;
}

@Override
public int read() throws IOException {
int readLen = read(singleByteBuffer);

if (readLen == -1) {
return -1;
}

return singleByteBuffer[0];
}

@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
int toRead = len > readLimit ? readLimit : len;
return inputStream.read(b, off, toRead);
}
}
Loading

0 comments on commit ca5e6f6

Please sign in to comment.