/* * Copyright 2010 Srikanth Reddy Lingala * * Licensed 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 net.lingala.zip4j.unzip; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.util.Arrays; import java.util.zip.CRC32; import net.lingala.zip4j.core.HeaderReader; import net.lingala.zip4j.crypto.AESDecrypter; import net.lingala.zip4j.crypto.IDecrypter; import net.lingala.zip4j.crypto.StandardDecrypter; import net.lingala.zip4j.exception.ZipException; import net.lingala.zip4j.io.InflaterInputStream; import net.lingala.zip4j.io.PartInputStream; import net.lingala.zip4j.io.ZipInputStream; import net.lingala.zip4j.model.AESExtraDataRecord; import net.lingala.zip4j.model.FileHeader; import net.lingala.zip4j.model.LocalFileHeader; import net.lingala.zip4j.model.UnzipParameters; import net.lingala.zip4j.model.ZipModel; import net.lingala.zip4j.progress.ProgressMonitor; import net.lingala.zip4j.util.InternalZipConstants; import net.lingala.zip4j.util.Raw; import net.lingala.zip4j.util.Zip4jConstants; import net.lingala.zip4j.util.Zip4jUtil; public class UnzipEngine { private ZipModel zipModel; private FileHeader fileHeader; private int currSplitFileCounter = 0; private LocalFileHeader localFileHeader; private IDecrypter decrypter; private CRC32 crc; public UnzipEngine(ZipModel zipModel, FileHeader fileHeader) throws ZipException { if (zipModel == null || fileHeader == null) { throw new ZipException("Invalid parameters passed to StoreUnzip. One or more of the parameters were null"); } this.zipModel = zipModel; this.fileHeader = fileHeader; this.crc = new CRC32(); } public void unzipFile(ProgressMonitor progressMonitor, String outPath, String newFileName, UnzipParameters unzipParameters) throws ZipException { if (zipModel == null || fileHeader == null || !Zip4jUtil.isStringNotNullAndNotEmpty(outPath)) { throw new ZipException("Invalid parameters passed during unzipping file. One or more of the parameters were null"); } InputStream is = null; OutputStream os = null; try { byte[] buff = new byte[InternalZipConstants.BUFF_SIZE]; int readLength = -1; is = getInputStream(); os = getOutputStream(outPath, newFileName); while ((readLength = is.read(buff)) != -1) { os.write(buff, 0, readLength); progressMonitor.updateWorkCompleted(readLength); if (progressMonitor.isCancelAllTasks()) { progressMonitor.setResult(ProgressMonitor.RESULT_CANCELLED); progressMonitor.setState(ProgressMonitor.STATE_READY); return; } } closeStreams(is, os); UnzipUtil.applyFileAttributes(fileHeader, new File(getOutputFileNameWithPath(outPath, newFileName)), unzipParameters); } catch (IOException e) { throw new ZipException(e); } catch (Exception e) { throw new ZipException(e); } finally { closeStreams(is, os); } } public ZipInputStream getInputStream() throws ZipException { if (fileHeader == null) { throw new ZipException("file header is null, cannot get inputstream"); } RandomAccessFile raf = null; try { raf = createFileHandler(InternalZipConstants.READ_MODE); String errMsg = "local header and file header do not match"; //checkSplitFile(); if (!checkLocalHeader()) throw new ZipException(errMsg); init(raf); long comprSize = localFileHeader.getCompressedSize(); long offsetStartOfData = localFileHeader.getOffsetStartOfData(); if (localFileHeader.isEncrypted()) { if (localFileHeader.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) { if (decrypter instanceof AESDecrypter) { comprSize -= (((AESDecrypter)decrypter).getSaltLength() + ((AESDecrypter)decrypter).getPasswordVerifierLength() + 10); offsetStartOfData += (((AESDecrypter)decrypter).getSaltLength() + ((AESDecrypter)decrypter).getPasswordVerifierLength()); } else { throw new ZipException("invalid decryptor when trying to calculate " + "compressed size for AES encrypted file: " + fileHeader.getFileName()); } } else if (localFileHeader.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) { comprSize -= InternalZipConstants.STD_DEC_HDR_SIZE; offsetStartOfData += InternalZipConstants.STD_DEC_HDR_SIZE; } } int compressionMethod = fileHeader.getCompressionMethod(); if (fileHeader.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) { if (fileHeader.getAesExtraDataRecord() != null) { compressionMethod = fileHeader.getAesExtraDataRecord().getCompressionMethod(); } else { throw new ZipException("AESExtraDataRecord does not exist for AES encrypted file: " + fileHeader.getFileName()); } } raf.seek(offsetStartOfData); switch (compressionMethod) { case Zip4jConstants.COMP_STORE: return new ZipInputStream(new PartInputStream(raf, offsetStartOfData, comprSize, this)); case Zip4jConstants.COMP_DEFLATE: return new ZipInputStream(new InflaterInputStream(raf, offsetStartOfData, comprSize, this)); default: throw new ZipException("compression type not supported"); } } catch (ZipException e) { if (raf != null) { try { raf.close(); } catch (IOException e1) { //ignore } } throw e; } catch (Exception e) { if (raf != null) { try { raf.close(); } catch (IOException e1) { } } throw new ZipException(e); } } private void init(RandomAccessFile raf) throws ZipException { if (localFileHeader == null) { throw new ZipException("local file header is null, cannot initialize input stream"); } try { initDecrypter(raf); } catch (ZipException e) { throw e; } catch (Exception e) { throw new ZipException(e); } } private void initDecrypter(RandomAccessFile raf) throws ZipException { if (localFileHeader == null) { throw new ZipException("local file header is null, cannot init decrypter"); } if (localFileHeader.isEncrypted()) { if (localFileHeader.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) { decrypter = new StandardDecrypter(fileHeader, getStandardDecrypterHeaderBytes(raf)); } else if (localFileHeader.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) { decrypter = new AESDecrypter(localFileHeader, getAESSalt(raf), getAESPasswordVerifier(raf)); } else { throw new ZipException("unsupported encryption method"); } } } private byte[] getStandardDecrypterHeaderBytes(RandomAccessFile raf) throws ZipException { try { byte[] headerBytes = new byte[InternalZipConstants.STD_DEC_HDR_SIZE]; raf.seek(localFileHeader.getOffsetStartOfData()); raf.read(headerBytes, 0, 12); return headerBytes; } catch (IOException e) { throw new ZipException(e); } catch (Exception e) { throw new ZipException(e); } } private byte[] getAESSalt(RandomAccessFile raf) throws ZipException { if (localFileHeader.getAesExtraDataRecord() == null) return null; try { AESExtraDataRecord aesExtraDataRecord = localFileHeader.getAesExtraDataRecord(); byte[] saltBytes = new byte[calculateAESSaltLength(aesExtraDataRecord)]; raf.seek(localFileHeader.getOffsetStartOfData()); raf.read(saltBytes); return saltBytes; } catch (IOException e) { throw new ZipException(e); } } private byte[] getAESPasswordVerifier(RandomAccessFile raf) throws ZipException { try { byte[] pvBytes = new byte[2]; raf.read(pvBytes); return pvBytes; } catch (IOException e) { throw new ZipException(e); } } private int calculateAESSaltLength(AESExtraDataRecord aesExtraDataRecord) throws ZipException { if (aesExtraDataRecord == null) { throw new ZipException("unable to determine salt length: AESExtraDataRecord is null"); } switch (aesExtraDataRecord.getAesStrength()) { case Zip4jConstants.AES_STRENGTH_128: return 8; case Zip4jConstants.AES_STRENGTH_192: return 12; case Zip4jConstants.AES_STRENGTH_256: return 16; default: throw new ZipException("unable to determine salt length: invalid aes key strength"); } } public void checkCRC() throws ZipException { if (fileHeader != null) { if (fileHeader.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) { if (decrypter != null && decrypter instanceof AESDecrypter) { byte[] tmpMacBytes = ((AESDecrypter)decrypter).getCalculatedAuthenticationBytes(); byte[] storedMac = ((AESDecrypter)decrypter).getStoredMac(); byte[] calculatedMac = new byte[InternalZipConstants.AES_AUTH_LENGTH]; if (calculatedMac == null || storedMac == null) { throw new ZipException("CRC (MAC) check failed for " + fileHeader.getFileName()); } System.arraycopy(tmpMacBytes, 0, calculatedMac, 0, InternalZipConstants.AES_AUTH_LENGTH); if (!Arrays.equals(calculatedMac, storedMac)) { throw new ZipException("invalid CRC (MAC) for file: " + fileHeader.getFileName()); } } } else { long calculatedCRC = crc.getValue() & 0xffffffffL; if (calculatedCRC != fileHeader.getCrc32()) { String errMsg = "invalid CRC for file: " + fileHeader.getFileName(); if (localFileHeader.isEncrypted() && localFileHeader.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) { errMsg += " - Wrong Password?"; } throw new ZipException(errMsg); } } } } // private void checkCRC() throws ZipException { // if (fileHeader != null) { // if (fileHeader.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) { // if (decrypter != null && decrypter instanceof AESDecrypter) { // byte[] tmpMacBytes = ((AESDecrypter)decrypter).getCalculatedAuthenticationBytes(); // byte[] actualMacBytes = ((AESDecrypter)decrypter).getStoredMac(); // if (tmpMacBytes == null || actualMacBytes == null) { // throw new ZipException("null mac value for AES encrypted file: " + fileHeader.getFileName()); // } // byte[] calcMacBytes = new byte[10]; // System.arraycopy(tmpMacBytes, 0, calcMacBytes, 0, 10); // if (!Arrays.equals(calcMacBytes, actualMacBytes)) { // throw new ZipException("invalid CRC(mac) for file: " + fileHeader.getFileName()); // } // } else { // throw new ZipException("invalid decryptor...cannot calculate mac value for file: " // + fileHeader.getFileName()); // } // } else if (unzipEngine != null) { // long calculatedCRC = unzipEngine.getCRC(); // long actualCRC = fileHeader.getCrc32(); // if (calculatedCRC != actualCRC) { // throw new ZipException("invalid CRC for file: " + fileHeader.getFileName()); // } // } // } // } private boolean checkLocalHeader() throws ZipException { RandomAccessFile rafForLH = null; try { rafForLH = checkSplitFile(); if (rafForLH == null) { rafForLH = new RandomAccessFile(new File(this.zipModel.getZipFile()), InternalZipConstants.READ_MODE); } HeaderReader headerReader = new HeaderReader(rafForLH); this.localFileHeader = headerReader.readLocalFileHeader(fileHeader); if (localFileHeader == null) { throw new ZipException("error reading local file header. Is this a valid zip file?"); } //TODO Add more comparision later if (localFileHeader.getCompressionMethod() != fileHeader.getCompressionMethod()) { return false; } return true; } catch (FileNotFoundException e) { throw new ZipException(e); } finally { if (rafForLH != null) { try { rafForLH.close(); } catch (IOException e) { // Ignore this } catch (Exception e) { //Ignore this } } } } private RandomAccessFile checkSplitFile() throws ZipException { if (zipModel.isSplitArchive()) { int diskNumberStartOfFile = fileHeader.getDiskNumberStart(); currSplitFileCounter = diskNumberStartOfFile + 1; String curZipFile = zipModel.getZipFile(); String partFile = null; if (diskNumberStartOfFile == zipModel.getEndCentralDirRecord().getNoOfThisDisk()) { partFile = zipModel.getZipFile(); } else { if (diskNumberStartOfFile >= 9) { partFile = curZipFile.substring(0, curZipFile.lastIndexOf(".")) + ".z" + (diskNumberStartOfFile+ 1); } else{ partFile = curZipFile.substring(0, curZipFile.lastIndexOf(".")) + ".z0" + (diskNumberStartOfFile+ 1); } } try { RandomAccessFile raf = new RandomAccessFile(partFile, InternalZipConstants.READ_MODE); if (currSplitFileCounter == 1) { byte[] splitSig = new byte[4]; raf.read(splitSig); if (Raw.readIntLittleEndian(splitSig, 0) != InternalZipConstants.SPLITSIG) { throw new ZipException("invalid first part split file signature"); } } return raf; } catch (FileNotFoundException e) { throw new ZipException(e); } catch (IOException e) { throw new ZipException(e); } } return null; } private RandomAccessFile createFileHandler(String mode) throws ZipException { if (this.zipModel == null || !Zip4jUtil.isStringNotNullAndNotEmpty(this.zipModel.getZipFile())) { throw new ZipException("input parameter is null in getFilePointer"); } try { RandomAccessFile raf = null; if (zipModel.isSplitArchive()) { raf = checkSplitFile(); } else { raf = new RandomAccessFile(new File(this.zipModel.getZipFile()), mode); } return raf; } catch (FileNotFoundException e) { throw new ZipException(e); } catch (Exception e) { throw new ZipException(e); } } private FileOutputStream getOutputStream(String outPath, String newFileName) throws ZipException { if (!Zip4jUtil.isStringNotNullAndNotEmpty(outPath)) { throw new ZipException("invalid output path"); } try { File file = new File(getOutputFileNameWithPath(outPath, newFileName)); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } if (file.exists()) { file.delete(); } FileOutputStream fileOutputStream = new FileOutputStream(file); return fileOutputStream; } catch (FileNotFoundException e) { throw new ZipException(e); } } private String getOutputFileNameWithPath(String outPath, String newFileName) throws ZipException { String fileName = null; if (Zip4jUtil.isStringNotNullAndNotEmpty(newFileName)) { fileName = newFileName; } else { fileName = fileHeader.getFileName(); } return outPath + System.getProperty("file.separator") + fileName; } public RandomAccessFile startNextSplitFile() throws IOException, FileNotFoundException { String currZipFile = zipModel.getZipFile(); String partFile = null; if (currSplitFileCounter == zipModel.getEndCentralDirRecord().getNoOfThisDisk()) { partFile = zipModel.getZipFile(); } else { if (currSplitFileCounter >= 9) { partFile = currZipFile.substring(0, currZipFile.lastIndexOf(".")) + ".z" + (currSplitFileCounter + 1); } else { partFile = currZipFile.substring(0, currZipFile.lastIndexOf(".")) + ".z0" + (currSplitFileCounter + 1); } } currSplitFileCounter++; try { if(!Zip4jUtil.checkFileExists(partFile)) { throw new IOException("zip split file does not exist: " + partFile); } } catch (ZipException e) { throw new IOException(e.getMessage()); } return new RandomAccessFile(partFile, InternalZipConstants.READ_MODE); } private void closeStreams(InputStream is, OutputStream os) throws ZipException { try { if (is != null) { is.close(); is = null; } } catch (IOException e) { if (e != null && Zip4jUtil.isStringNotNullAndNotEmpty(e.getMessage())) { if (e.getMessage().indexOf(" - Wrong Password?") >= 0) { throw new ZipException(e.getMessage()); } } } finally { try { if (os != null) { os.close(); os = null; } } catch (IOException e) { //do nothing } } } public void updateCRC(int b) { crc.update(b); } public void updateCRC(byte[] buff, int offset, int len) { if (buff != null) { crc.update(buff, offset, len); } } public FileHeader getFileHeader() { return fileHeader; } public IDecrypter getDecrypter() { return decrypter; } public ZipModel getZipModel() { return zipModel; } public LocalFileHeader getLocalFileHeader() { return localFileHeader; } }