diff --git a/src/net/lingala/zip4j/core/HeaderReader.java b/src/net/lingala/zip4j/core/HeaderReader.java new file mode 100644 index 0000000..1e719d4 --- /dev/null +++ b/src/net/lingala/zip4j/core/HeaderReader.java @@ -0,0 +1,1119 @@ +/* +* 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.core; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.ArrayList; + +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.exception.ZipExceptionConstants; +import net.lingala.zip4j.model.AESExtraDataRecord; +import net.lingala.zip4j.model.CentralDirectory; +import net.lingala.zip4j.model.DigitalSignature; +import net.lingala.zip4j.model.EndCentralDirRecord; +import net.lingala.zip4j.model.ExtraDataRecord; +import net.lingala.zip4j.model.FileHeader; +import net.lingala.zip4j.model.LocalFileHeader; +import net.lingala.zip4j.model.Zip64EndCentralDirLocator; +import net.lingala.zip4j.model.Zip64EndCentralDirRecord; +import net.lingala.zip4j.model.Zip64ExtendedInfo; +import net.lingala.zip4j.model.ZipModel; +import net.lingala.zip4j.util.InternalZipConstants; +import net.lingala.zip4j.util.Raw; +import net.lingala.zip4j.util.Zip4jConstants; +import net.lingala.zip4j.util.Zip4jUtil; + +/** + * Helper class to read header information for the zip file + * + */ +public class HeaderReader { + + private RandomAccessFile zip4jRaf = null; + private ZipModel zipModel; + + /** + * Creates a new HeaderReader object with the given input stream + * @param zip4jRaf + */ + public HeaderReader(RandomAccessFile zip4jRaf) { + this.zip4jRaf = zip4jRaf; + } + + /** + * Reads all the header information for the zip file. + *

Note: This method does not read local file header information + * @return {@link ZipModel} + * @throws ZipException + */ + public ZipModel readAllHeaders() throws ZipException { + return readAllHeaders(null); + } + + /** + * Reads all the header information for the zip file. File names are read with + * input charset name. If this parameter is null, default system charset is used. + *

Note: This method does not read local file header information + * @return {@link ZipModel} + * @throws ZipException + */ + public ZipModel readAllHeaders(String fileNameCharset) throws ZipException { + zipModel = new ZipModel(); + zipModel.setFileNameCharset(fileNameCharset); + zipModel.setEndCentralDirRecord(readEndOfCentralDirectoryRecord()); + + // If file is Zip64 format, then Zip64 headers have to be read before + // reading central directory + zipModel.setZip64EndCentralDirLocator(readZip64EndCentralDirLocator()); + + if (zipModel.isZip64Format()) { + zipModel.setZip64EndCentralDirRecord(readZip64EndCentralDirRec()); + if(zipModel.getZip64EndCentralDirRecord() != null && + zipModel.getZip64EndCentralDirRecord().getNoOfThisDisk() > 0){ + zipModel.setSplitArchive(true); + } else { + zipModel.setSplitArchive(false); + } + } + + zipModel.setCentralDirectory(readCentralDirectory()); + //zipModel.setLocalFileHeaderList(readLocalFileHeaders()); //Donot read local headers now. + return zipModel; + } + + /** + * Reads end of central directory record + * @return {@link EndCentralDirRecord} + * @throws ZipException + */ + private EndCentralDirRecord readEndOfCentralDirectoryRecord() throws ZipException { + + if (zip4jRaf == null) { + throw new ZipException("random access file was null", ZipExceptionConstants.randomAccessFileNull); + } + + try { + byte[] ebs = new byte[4]; + long pos = zip4jRaf.length() - InternalZipConstants.ENDHDR; + + EndCentralDirRecord endCentralDirRecord = new EndCentralDirRecord(); + int counter = 0; + do { + zip4jRaf.seek(pos--); + counter++; + } while ((Raw.readLeInt(zip4jRaf, ebs) != InternalZipConstants.ENDSIG) && counter <= 3000); + + if ((Raw.readIntLittleEndian(ebs, 0) != InternalZipConstants.ENDSIG)) { + throw new ZipException("zip headers not found. probably not a zip file"); + } + byte[] intBuff = new byte[4]; + byte[] shortBuff = new byte[2]; + + //End of central record signature + endCentralDirRecord.setSignature(InternalZipConstants.ENDSIG); + + //number of this disk + readIntoBuff(zip4jRaf, shortBuff); + endCentralDirRecord.setNoOfThisDisk(Raw.readShortLittleEndian(shortBuff, 0)); + + //number of the disk with the start of the central directory + readIntoBuff(zip4jRaf, shortBuff); + endCentralDirRecord.setNoOfThisDiskStartOfCentralDir(Raw.readShortLittleEndian(shortBuff, 0)); + + //total number of entries in the central directory on this disk + readIntoBuff(zip4jRaf, shortBuff); + endCentralDirRecord.setTotNoOfEntriesInCentralDirOnThisDisk(Raw.readShortLittleEndian(shortBuff, 0)); + + //total number of entries in the central directory + readIntoBuff(zip4jRaf, shortBuff); + endCentralDirRecord.setTotNoOfEntriesInCentralDir(Raw.readShortLittleEndian(shortBuff, 0)); + + //size of the central directory + readIntoBuff(zip4jRaf, intBuff); + endCentralDirRecord.setSizeOfCentralDir(Raw.readIntLittleEndian(intBuff, 0)); + + //offset of start of central directory with respect to the starting disk number + readIntoBuff(zip4jRaf, intBuff); + byte[] longBuff = getLongByteFromIntByte(intBuff); + endCentralDirRecord.setOffsetOfStartOfCentralDir(Raw.readLongLittleEndian(longBuff, 0)); + + //.ZIP file comment length + readIntoBuff(zip4jRaf, shortBuff); + int commentLength = Raw.readShortLittleEndian(shortBuff, 0); + endCentralDirRecord.setCommentLength(commentLength); + + //.ZIP file comment + if (commentLength > 0) { + byte[] commentBuf = new byte[commentLength]; + readIntoBuff(zip4jRaf, commentBuf); + endCentralDirRecord.setComment(new String(commentBuf)); + endCentralDirRecord.setCommentBytes(commentBuf); + } else { + endCentralDirRecord.setComment(null); + } + + int diskNumber = endCentralDirRecord.getNoOfThisDisk(); + if (diskNumber > 0) { + zipModel.setSplitArchive(true); + } else { + zipModel.setSplitArchive(false); + } + + return endCentralDirRecord; + } catch (IOException e) { + throw new ZipException("Probably not a zip file or a corrupted zip file", e, ZipExceptionConstants.notZipFile); + } + } + + /** + * Reads central directory information for the zip file + * @return {@link CentralDirectory} + * @throws ZipException + */ + private CentralDirectory readCentralDirectory() throws ZipException { + + if (zip4jRaf == null) { + throw new ZipException("random access file was null", ZipExceptionConstants.randomAccessFileNull); + } + + if (zipModel.getEndCentralDirRecord() == null) { + throw new ZipException("EndCentralRecord was null, maybe a corrupt zip file"); + } + + try { + CentralDirectory centralDirectory = new CentralDirectory(); + ArrayList fileHeaderList = new ArrayList(); + + EndCentralDirRecord endCentralDirRecord = zipModel.getEndCentralDirRecord(); + long offSetStartCentralDir = endCentralDirRecord.getOffsetOfStartOfCentralDir(); + int centralDirEntryCount = endCentralDirRecord.getTotNoOfEntriesInCentralDir(); + + if (zipModel.isZip64Format()) { + offSetStartCentralDir = zipModel.getZip64EndCentralDirRecord().getOffsetStartCenDirWRTStartDiskNo(); + centralDirEntryCount = (int)zipModel.getZip64EndCentralDirRecord().getTotNoOfEntriesInCentralDir(); + } + + zip4jRaf.seek(offSetStartCentralDir); + + byte[] intBuff = new byte[4]; + byte[] shortBuff = new byte[2]; + byte[] longBuff = new byte[8]; + + for (int i = 0; i < centralDirEntryCount; i++) { + FileHeader fileHeader = new FileHeader(); + + //FileHeader Signature + readIntoBuff(zip4jRaf, intBuff); + int signature = Raw.readIntLittleEndian(intBuff, 0); + if (signature != InternalZipConstants.CENSIG) { + throw new ZipException("Expected central directory entry not found (#" + (i + 1) + ")"); + } + fileHeader.setSignature(signature); + + //version made by + readIntoBuff(zip4jRaf, shortBuff); + fileHeader.setVersionMadeBy(Raw.readShortLittleEndian(shortBuff, 0)); + + //version needed to extract + readIntoBuff(zip4jRaf, shortBuff); + fileHeader.setVersionNeededToExtract(Raw.readShortLittleEndian(shortBuff, 0)); + + //general purpose bit flag + readIntoBuff(zip4jRaf, shortBuff); + fileHeader.setFileNameUTF8Encoded((Raw.readShortLittleEndian(shortBuff, 0) & InternalZipConstants.UFT8_NAMES_FLAG) != 0); + int firstByte = shortBuff[0]; + int result = firstByte & 1; + if (result != 0) { + fileHeader.setEncrypted(true); + } + fileHeader.setGeneralPurposeFlag((byte[])shortBuff.clone()); + + //Check if data descriptor exists for local file header + fileHeader.setDataDescriptorExists(firstByte>>3 == 1); + + //compression method + readIntoBuff(zip4jRaf, shortBuff); + fileHeader.setCompressionMethod(Raw.readShortLittleEndian(shortBuff, 0)); + + //last mod file time + readIntoBuff(zip4jRaf, intBuff); + fileHeader.setLastModFileTime(Raw.readIntLittleEndian(intBuff, 0)); + + //crc-32 + readIntoBuff(zip4jRaf, intBuff); + fileHeader.setCrc32(Raw.readIntLittleEndian(intBuff, 0)); + fileHeader.setCrcBuff((byte[])intBuff.clone()); + + //compressed size + readIntoBuff(zip4jRaf, intBuff); + longBuff = getLongByteFromIntByte(intBuff); + fileHeader.setCompressedSize(Raw.readLongLittleEndian(longBuff, 0)); + + //uncompressed size + readIntoBuff(zip4jRaf, intBuff); + longBuff = getLongByteFromIntByte(intBuff); + fileHeader.setUncompressedSize(Raw.readLongLittleEndian(longBuff, 0)); + + //file name length + readIntoBuff(zip4jRaf, shortBuff); + int fileNameLength = Raw.readShortLittleEndian(shortBuff, 0); + fileHeader.setFileNameLength(fileNameLength); + + //extra field length + readIntoBuff(zip4jRaf, shortBuff); + int extraFieldLength = Raw.readShortLittleEndian(shortBuff, 0); + fileHeader.setExtraFieldLength(extraFieldLength); + + //file comment length + readIntoBuff(zip4jRaf, shortBuff); + int fileCommentLength = Raw.readShortLittleEndian(shortBuff, 0); + fileHeader.setFileComment(new String(shortBuff)); + + //disk number start + readIntoBuff(zip4jRaf, shortBuff); + fileHeader.setDiskNumberStart(Raw.readShortLittleEndian(shortBuff, 0)); + + //internal file attributes + readIntoBuff(zip4jRaf, shortBuff); + fileHeader.setInternalFileAttr((byte[])shortBuff.clone()); + + //external file attributes + readIntoBuff(zip4jRaf, intBuff); + fileHeader.setExternalFileAttr((byte[])intBuff.clone()); + + //relative offset of local header + readIntoBuff(zip4jRaf, intBuff); + //Commented on 26.08.2010. Revert back if any issues + //fileHeader.setOffsetLocalHeader((Raw.readIntLittleEndian(intBuff, 0) & 0xFFFFFFFFL) + zip4jRaf.getStart()); + longBuff = getLongByteFromIntByte(intBuff); + fileHeader.setOffsetLocalHeader((Raw.readLongLittleEndian(longBuff, 0) & 0xFFFFFFFFL)); + + if (fileNameLength > 0) { + byte[] fileNameBuf = new byte[fileNameLength]; + readIntoBuff(zip4jRaf, fileNameBuf); + // Modified after user reported an issue http://www.lingala.net/zip4j/forum/index.php?topic=2.0 +// String fileName = new String(fileNameBuf, "Cp850"); + // Modified as per http://www.lingala.net/zip4j/forum/index.php?topic=41.0 +// String fileName = Zip4jUtil.getCp850EncodedString(fileNameBuf); + + String fileName = null; + + if (Zip4jUtil.isStringNotNullAndNotEmpty(zipModel.getFileNameCharset())) { + fileName = new String(fileNameBuf, zipModel.getFileNameCharset()); + } else { + fileName = Zip4jUtil.decodeFileName(fileNameBuf, fileHeader.isFileNameUTF8Encoded()); + } + + if (fileName == null) { + throw new ZipException("fileName is null when reading central directory"); + } + + if (fileName.indexOf(":" + System.getProperty("file.separator")) >= 0) { + fileName = fileName.substring(fileName.indexOf(":" + System.getProperty("file.separator")) + 2); + } + + fileHeader.setFileName(fileName); + fileHeader.setDirectory(fileName.endsWith("/") || fileName.endsWith("\\")); + + } else { + fileHeader.setFileName(null); + } + + //Extra field + readAndSaveExtraDataRecord(fileHeader); + + //Read Zip64 Extra data records if exists + readAndSaveZip64ExtendedInfo(fileHeader); + + //Read AES Extra Data record if exists + readAndSaveAESExtraDataRecord(fileHeader); + +// if (fileHeader.isEncrypted()) { +// +// if (fileHeader.getEncryptionMethod() == ZipConstants.ENC_METHOD_AES) { +// //Do nothing +// } else { +// if ((firstByte & 64) == 64) { +// //hardcoded for now +// fileHeader.setEncryptionMethod(1); +// } else { +// fileHeader.setEncryptionMethod(ZipConstants.ENC_METHOD_STANDARD); +// fileHeader.setCompressedSize(fileHeader.getCompressedSize() +// - ZipConstants.STD_DEC_HDR_SIZE); +// } +// } +// +// } + + if (fileCommentLength > 0) { + byte[] fileCommentBuf = new byte[fileCommentLength]; + readIntoBuff(zip4jRaf, fileCommentBuf); + fileHeader.setFileComment(new String(fileCommentBuf)); + } + + fileHeaderList.add(fileHeader); + } + centralDirectory.setFileHeaders(fileHeaderList); + + //Digital Signature + DigitalSignature digitalSignature = new DigitalSignature(); + readIntoBuff(zip4jRaf, intBuff); + int signature = Raw.readIntLittleEndian(intBuff, 0); + if (signature != InternalZipConstants.DIGSIG) { + return centralDirectory; + } + + digitalSignature.setHeaderSignature(signature); + + //size of data + readIntoBuff(zip4jRaf, shortBuff); + int sizeOfData = Raw.readShortLittleEndian(shortBuff, 0); + digitalSignature.setSizeOfData(sizeOfData); + + if (sizeOfData > 0) { + byte[] sigDataBuf = new byte[sizeOfData]; + readIntoBuff(zip4jRaf, sigDataBuf); + digitalSignature.setSignatureData(new String(sigDataBuf)); + } + + return centralDirectory; + } catch (IOException e) { + throw new ZipException(e); + } + } + + /** + * Reads extra data record and saves it in the {@link FileHeader} + * @param fileHeader + * @throws ZipException + */ + private void readAndSaveExtraDataRecord(FileHeader fileHeader) throws ZipException { + + if (zip4jRaf == null) { + throw new ZipException("invalid file handler when trying to read extra data record"); + } + + if (fileHeader == null) { + throw new ZipException("file header is null"); + } + + int extraFieldLength = fileHeader.getExtraFieldLength(); + if (extraFieldLength <= 0) { + return; + } + + fileHeader.setExtraDataRecords(readExtraDataRecords(extraFieldLength)); + + } + + /** + * Reads extra data record and saves it in the {@link LocalFileHeader} + * @param localFileHeader + * @throws ZipException + */ + private void readAndSaveExtraDataRecord(LocalFileHeader localFileHeader) throws ZipException { + + if (zip4jRaf == null) { + throw new ZipException("invalid file handler when trying to read extra data record"); + } + + if (localFileHeader == null) { + throw new ZipException("file header is null"); + } + + int extraFieldLength = localFileHeader.getExtraFieldLength(); + if (extraFieldLength <= 0) { + return; + } + + localFileHeader.setExtraDataRecords(readExtraDataRecords(extraFieldLength)); + + } + + /** + * Reads extra data records + * @param extraFieldLength + * @return ArrayList of {@link ExtraDataRecord} + * @throws ZipException + */ + private ArrayList readExtraDataRecords(int extraFieldLength) throws ZipException { + + if (extraFieldLength <= 0) { + return null; + } + + try { + byte[] extraFieldBuf = new byte[extraFieldLength]; + zip4jRaf.read(extraFieldBuf); + + int counter = 0; + ArrayList extraDataList = new ArrayList(); + while(counter < extraFieldLength) { + ExtraDataRecord extraDataRecord = new ExtraDataRecord(); + int header = Raw.readShortLittleEndian(extraFieldBuf, counter); + extraDataRecord.setHeader(header); + counter = counter + 2; + int sizeOfRec = Raw.readShortLittleEndian(extraFieldBuf, counter); + + if ((2 + sizeOfRec) > extraFieldLength) { + sizeOfRec = Raw.readShortBigEndian(extraFieldBuf, counter); + if ((2 + sizeOfRec) > extraFieldLength) { + //If this is the case, then extra data record is corrupt + //skip reading any further extra data records + break; + } + } + + extraDataRecord.setSizeOfData(sizeOfRec); + counter = counter + 2; + + if (sizeOfRec > 0) { + byte[] data = new byte[sizeOfRec]; + System.arraycopy(extraFieldBuf, counter, data, 0, sizeOfRec); + extraDataRecord.setData(data); + } + counter = counter + sizeOfRec; + extraDataList.add(extraDataRecord); + } + if (extraDataList.size() > 0) { + return extraDataList; + } else { + return null; + } + } catch (IOException e) { + throw new ZipException(e); + } + } + + /** + * Reads Zip64 End Of Central Directory Locator + * @return {@link Zip64EndCentralDirLocator} + * @throws ZipException + */ + private Zip64EndCentralDirLocator readZip64EndCentralDirLocator() throws ZipException { + + if (zip4jRaf == null) { + throw new ZipException("invalid file handler when trying to read Zip64EndCentralDirLocator"); + } + + try { + Zip64EndCentralDirLocator zip64EndCentralDirLocator = new Zip64EndCentralDirLocator(); + + setFilePointerToReadZip64EndCentralDirLoc(); + + byte[] intBuff = new byte[4]; + byte[] longBuff = new byte[8]; + + readIntoBuff(zip4jRaf, intBuff); + int signature = Raw.readIntLittleEndian(intBuff, 0); + if (signature == InternalZipConstants.ZIP64ENDCENDIRLOC) { + zipModel.setZip64Format(true); + zip64EndCentralDirLocator.setSignature(signature); + } else { + zipModel.setZip64Format(false); + return null; + } + + readIntoBuff(zip4jRaf, intBuff); + zip64EndCentralDirLocator.setNoOfDiskStartOfZip64EndOfCentralDirRec( + Raw.readIntLittleEndian(intBuff, 0)); + + readIntoBuff(zip4jRaf, longBuff); + zip64EndCentralDirLocator.setOffsetZip64EndOfCentralDirRec( + Raw.readLongLittleEndian(longBuff, 0)); + + readIntoBuff(zip4jRaf, intBuff); + zip64EndCentralDirLocator.setTotNumberOfDiscs(Raw.readIntLittleEndian(intBuff, 0)); + + return zip64EndCentralDirLocator; + + } catch (Exception e) { + throw new ZipException(e); + } + + } + + /** + * Reads Zip64 End of Central Directory Record + * @return {@link Zip64EndCentralDirRecord} + * @throws ZipException + */ + private Zip64EndCentralDirRecord readZip64EndCentralDirRec() throws ZipException { + + if (zipModel.getZip64EndCentralDirLocator() == null) { + throw new ZipException("invalid zip64 end of central directory locator"); + } + + long offSetStartOfZip64CentralDir = + zipModel.getZip64EndCentralDirLocator().getOffsetZip64EndOfCentralDirRec(); + + if (offSetStartOfZip64CentralDir < 0) { + throw new ZipException("invalid offset for start of end of central directory record"); + } + + try { + zip4jRaf.seek(offSetStartOfZip64CentralDir); + + Zip64EndCentralDirRecord zip64EndCentralDirRecord = new Zip64EndCentralDirRecord(); + + byte[] shortBuff = new byte[2]; + byte[] intBuff = new byte[4]; + byte[] longBuff = new byte[8]; + + //signature + readIntoBuff(zip4jRaf, intBuff); + int signature = Raw.readIntLittleEndian(intBuff, 0); + if (signature != InternalZipConstants.ZIP64ENDCENDIRREC) { + throw new ZipException("invalid signature for zip64 end of central directory record"); + } + zip64EndCentralDirRecord.setSignature(signature); + + //size of zip64 end of central directory record + readIntoBuff(zip4jRaf, longBuff); + zip64EndCentralDirRecord.setSizeOfZip64EndCentralDirRec( + Raw.readLongLittleEndian(longBuff, 0)); + + //version made by + readIntoBuff(zip4jRaf, shortBuff); + zip64EndCentralDirRecord.setVersionMadeBy(Raw.readShortLittleEndian(shortBuff, 0)); + + //version needed to extract + readIntoBuff(zip4jRaf, shortBuff); + zip64EndCentralDirRecord.setVersionNeededToExtract(Raw.readShortLittleEndian(shortBuff, 0)); + + //number of this disk + readIntoBuff(zip4jRaf, intBuff); + zip64EndCentralDirRecord.setNoOfThisDisk(Raw.readIntLittleEndian(intBuff, 0)); + + //number of the disk with the start of the central directory + readIntoBuff(zip4jRaf, intBuff); + zip64EndCentralDirRecord.setNoOfThisDiskStartOfCentralDir( + Raw.readIntLittleEndian(intBuff, 0)); + + //total number of entries in the central directory on this disk + readIntoBuff(zip4jRaf, longBuff); + zip64EndCentralDirRecord.setTotNoOfEntriesInCentralDirOnThisDisk( + Raw.readLongLittleEndian(longBuff, 0)); + + //total number of entries in the central directory + readIntoBuff(zip4jRaf, longBuff); + zip64EndCentralDirRecord.setTotNoOfEntriesInCentralDir( + Raw.readLongLittleEndian(longBuff, 0)); + + //size of the central directory + readIntoBuff(zip4jRaf, longBuff); + zip64EndCentralDirRecord.setSizeOfCentralDir(Raw.readLongLittleEndian(longBuff, 0)); + + //offset of start of central directory with respect to the starting disk number + readIntoBuff(zip4jRaf, longBuff); + zip64EndCentralDirRecord.setOffsetStartCenDirWRTStartDiskNo( + Raw.readLongLittleEndian(longBuff, 0)); + + //zip64 extensible data sector + //44 is the size of fixed variables in this record + long extDataSecSize = zip64EndCentralDirRecord.getSizeOfZip64EndCentralDirRec() - 44; + if (extDataSecSize > 0) { + byte[] extDataSecRecBuf = new byte[(int)extDataSecSize]; + readIntoBuff(zip4jRaf, extDataSecRecBuf); + zip64EndCentralDirRecord.setExtensibleDataSector(extDataSecRecBuf); + } + + return zip64EndCentralDirRecord; + + } catch (IOException e) { + throw new ZipException(e); + } + + } + + /** + * Reads Zip64 Extended info and saves it in the {@link FileHeader} + * @param fileHeader + * @throws ZipException + */ + private void readAndSaveZip64ExtendedInfo(FileHeader fileHeader) throws ZipException { + if (fileHeader == null) { + throw new ZipException("file header is null in reading Zip64 Extended Info"); + } + + if (fileHeader.getExtraDataRecords() == null || fileHeader.getExtraDataRecords().size() <= 0) { + return; + } + + Zip64ExtendedInfo zip64ExtendedInfo = readZip64ExtendedInfo( + fileHeader.getExtraDataRecords(), + fileHeader.getUncompressedSize(), + fileHeader.getCompressedSize(), + fileHeader.getOffsetLocalHeader(), + fileHeader.getDiskNumberStart()); + + if (zip64ExtendedInfo != null) { + fileHeader.setZip64ExtendedInfo(zip64ExtendedInfo); + if (zip64ExtendedInfo.getUnCompressedSize() != -1) + fileHeader.setUncompressedSize(zip64ExtendedInfo.getUnCompressedSize()); + + if (zip64ExtendedInfo.getCompressedSize() != -1) + fileHeader.setCompressedSize(zip64ExtendedInfo.getCompressedSize()); + + if (zip64ExtendedInfo.getOffsetLocalHeader() != -1) + fileHeader.setOffsetLocalHeader(zip64ExtendedInfo.getOffsetLocalHeader()); + + if (zip64ExtendedInfo.getDiskNumberStart() != -1) + fileHeader.setDiskNumberStart(zip64ExtendedInfo.getDiskNumberStart()); + } + } + + /** + * Reads Zip64 Extended Info and saves it in the {@link LocalFileHeader} + * @param localFileHeader + * @throws ZipException + */ + private void readAndSaveZip64ExtendedInfo(LocalFileHeader localFileHeader) throws ZipException { + if (localFileHeader == null) { + throw new ZipException("file header is null in reading Zip64 Extended Info"); + } + + if (localFileHeader.getExtraDataRecords() == null || localFileHeader.getExtraDataRecords().size() <= 0) { + return; + } + + Zip64ExtendedInfo zip64ExtendedInfo = readZip64ExtendedInfo( + localFileHeader.getExtraDataRecords(), + localFileHeader.getUncompressedSize(), + localFileHeader.getCompressedSize(), + -1, -1); + + if (zip64ExtendedInfo != null) { + localFileHeader.setZip64ExtendedInfo(zip64ExtendedInfo); + + if (zip64ExtendedInfo.getUnCompressedSize() != -1) + localFileHeader.setUncompressedSize(zip64ExtendedInfo.getUnCompressedSize()); + + if (zip64ExtendedInfo.getCompressedSize() != -1) + localFileHeader.setCompressedSize(zip64ExtendedInfo.getCompressedSize()); + } + } + + /** + * Reads Zip64 Extended Info + * @param extraDataRecords + * @param unCompressedSize + * @param compressedSize + * @param offsetLocalHeader + * @param diskNumberStart + * @return {@link Zip64ExtendedInfo} + * @throws ZipException + */ + private Zip64ExtendedInfo readZip64ExtendedInfo( + ArrayList extraDataRecords, + long unCompressedSize, + long compressedSize, + long offsetLocalHeader, + int diskNumberStart) throws ZipException { + + for (int i = 0; i < extraDataRecords.size(); i++) { + ExtraDataRecord extraDataRecord = (ExtraDataRecord)extraDataRecords.get(i); + if (extraDataRecord == null) { + continue; + } + + if (extraDataRecord.getHeader() == 0x0001) { + + Zip64ExtendedInfo zip64ExtendedInfo = new Zip64ExtendedInfo(); + + byte[] byteBuff = extraDataRecord.getData(); + + if (extraDataRecord.getSizeOfData() <= 0) { + break; + } + byte[] longByteBuff = new byte[8]; + byte[] intByteBuff = new byte[4]; + int counter = 0; + boolean valueAdded = false; + + if (((unCompressedSize & 0xFFFF) == 0xFFFF) && counter < extraDataRecord.getSizeOfData()) { + System.arraycopy(byteBuff, counter, longByteBuff, 0, 8); + long val = Raw.readLongLittleEndian(longByteBuff, 0); + zip64ExtendedInfo.setUnCompressedSize(val); + counter += 8; + valueAdded = true; + } + + if (((compressedSize & 0xFFFF) == 0xFFFF) && counter < extraDataRecord.getSizeOfData()) { + System.arraycopy(byteBuff, counter, longByteBuff, 0, 8); + long val = Raw.readLongLittleEndian(longByteBuff, 0); + zip64ExtendedInfo.setCompressedSize(val); + counter += 8; + valueAdded = true; + } + + if (((offsetLocalHeader & 0xFFFF) == 0xFFFF) && counter < extraDataRecord.getSizeOfData()) { + System.arraycopy(byteBuff, counter, longByteBuff, 0, 8); + long val = Raw.readLongLittleEndian(longByteBuff, 0); + zip64ExtendedInfo.setOffsetLocalHeader(val); + counter += 8; + valueAdded = true; + } + + if (((diskNumberStart & 0xFFFF) == 0xFFFF) && counter < extraDataRecord.getSizeOfData()) { + System.arraycopy(byteBuff, counter, intByteBuff, 0, 4); + int val = Raw.readIntLittleEndian(intByteBuff, 0); + zip64ExtendedInfo.setDiskNumberStart(val); + counter += 8; + valueAdded = true; + } + + if (valueAdded) { + return zip64ExtendedInfo; + } + + break; + } + } + return null; + } + + /** + * Sets the current random access file pointer at the start of signature + * of the zip64 end of central directory record + * @throws ZipException + */ + private void setFilePointerToReadZip64EndCentralDirLoc() throws ZipException { + try { + byte[] ebs = new byte[4]; + long pos = zip4jRaf.length() - InternalZipConstants.ENDHDR; + + do { + zip4jRaf.seek(pos--); + } while (Raw.readLeInt(zip4jRaf, ebs) != InternalZipConstants.ENDSIG); + + // Now the file pointer is at the end of signature of Central Dir Rec + // Seek back with the following values + // 4 -> end of central dir signature + // 4 -> total number of disks + // 8 -> relative offset of the zip64 end of central directory record + // 4 -> number of the disk with the start of the zip64 end of central directory + // 4 -> zip64 end of central dir locator signature + // Refer to Appnote for more information + //TODO: Donot harcorde these values. Make use of ZipConstants + zip4jRaf.seek(zip4jRaf.getFilePointer() - 4 - 4 - 8 - 4 - 4); + } catch (IOException e) { + throw new ZipException(e); + } + } + + /** + * Reads local file header for the given file header + * @param fileHeader + * @return {@link LocalFileHeader} + * @throws ZipException + */ + public LocalFileHeader readLocalFileHeader(FileHeader fileHeader) throws ZipException { + if (fileHeader == null || zip4jRaf == null) { + throw new ZipException("invalid read parameters for local header"); + } + + long locHdrOffset = fileHeader.getOffsetLocalHeader(); + + if (fileHeader.getZip64ExtendedInfo() != null) { + Zip64ExtendedInfo zip64ExtendedInfo = fileHeader.getZip64ExtendedInfo(); + if (zip64ExtendedInfo.getOffsetLocalHeader() > 0) { + locHdrOffset = fileHeader.getOffsetLocalHeader(); + } + } + + if (locHdrOffset < 0) { + throw new ZipException("invalid local header offset"); + } + + try { + zip4jRaf.seek(locHdrOffset); + + int length = 0; + LocalFileHeader localFileHeader = new LocalFileHeader(); + + byte[] shortBuff = new byte[2]; + byte[] intBuff = new byte[4]; + byte[] longBuff = new byte[8]; + + //signature + readIntoBuff(zip4jRaf, intBuff); + int sig = Raw.readIntLittleEndian(intBuff, 0); + if (sig != InternalZipConstants.LOCSIG) { + throw new ZipException("invalid local header signature for file: " + fileHeader.getFileName()); + } + localFileHeader.setSignature(sig); + length += 4; + + //version needed to extract + readIntoBuff(zip4jRaf, shortBuff); + localFileHeader.setVersionNeededToExtract(Raw.readShortLittleEndian(shortBuff, 0)); + length += 2; + + //general purpose bit flag + readIntoBuff(zip4jRaf, shortBuff); + localFileHeader.setFileNameUTF8Encoded((Raw.readShortLittleEndian(shortBuff, 0) & InternalZipConstants.UFT8_NAMES_FLAG) != 0); + int firstByte = shortBuff[0]; + int result = firstByte & 1; + if (result != 0) { + localFileHeader.setEncrypted(true); + } + localFileHeader.setGeneralPurposeFlag(shortBuff); + length += 2; + + //Check if data descriptor exists for local file header + String binary = Integer.toBinaryString(firstByte); + if (binary.length() >= 4) + localFileHeader.setDataDescriptorExists(binary.charAt(3) == '1'); + + //compression method + readIntoBuff(zip4jRaf, shortBuff); + localFileHeader.setCompressionMethod(Raw.readShortLittleEndian(shortBuff, 0)); + length += 2; + + //last mod file time + readIntoBuff(zip4jRaf, intBuff); + localFileHeader.setLastModFileTime(Raw.readIntLittleEndian(intBuff, 0)); + length += 4; + + //crc-32 + readIntoBuff(zip4jRaf, intBuff); + localFileHeader.setCrc32(Raw.readIntLittleEndian(intBuff, 0)); + localFileHeader.setCrcBuff((byte[])intBuff.clone()); + length += 4; + + //compressed size + readIntoBuff(zip4jRaf, intBuff); + longBuff = getLongByteFromIntByte(intBuff); + localFileHeader.setCompressedSize(Raw.readLongLittleEndian(longBuff, 0)); + length += 4; + + //uncompressed size + readIntoBuff(zip4jRaf, intBuff); + longBuff = getLongByteFromIntByte(intBuff); + localFileHeader.setUncompressedSize(Raw.readLongLittleEndian(longBuff, 0)); + length += 4; + + //file name length + readIntoBuff(zip4jRaf, shortBuff); + int fileNameLength = Raw.readShortLittleEndian(shortBuff, 0); + localFileHeader.setFileNameLength(fileNameLength); + length += 2; + + //extra field length + readIntoBuff(zip4jRaf, shortBuff); + int extraFieldLength = Raw.readShortLittleEndian(shortBuff, 0); + localFileHeader.setExtraFieldLength(extraFieldLength); + length += 2; + + //file name + if (fileNameLength > 0) { + byte[] fileNameBuf = new byte[fileNameLength]; + readIntoBuff(zip4jRaf, 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); + String fileName = Zip4jUtil.decodeFileName(fileNameBuf, localFileHeader.isFileNameUTF8Encoded()); + + if (fileName == null) { + throw new ZipException("file name is null, cannot assign file name to local file header"); + } + + if (fileName.indexOf(":" + System.getProperty("file.separator")) >= 0) { + fileName = fileName.substring(fileName.indexOf(":" + System.getProperty("file.separator")) + 2); + } + + localFileHeader.setFileName(fileName); + length += fileNameLength; + } else { + localFileHeader.setFileName(null); + } + + //extra field + readAndSaveExtraDataRecord(localFileHeader); + length += extraFieldLength; + + localFileHeader.setOffsetStartOfData(locHdrOffset + length); + + //Copy password from fileHeader to localFileHeader + localFileHeader.setPassword(fileHeader.getPassword()); + + readAndSaveZip64ExtendedInfo(localFileHeader); + + readAndSaveAESExtraDataRecord(localFileHeader); + + if (localFileHeader.isEncrypted()) { + + if (localFileHeader.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) { + //Do nothing + } else { + if ((firstByte & 64) == 64) { + //hardcoded for now + localFileHeader.setEncryptionMethod(1); + } else { + localFileHeader.setEncryptionMethod(Zip4jConstants.ENC_METHOD_STANDARD); +// localFileHeader.setCompressedSize(localFileHeader.getCompressedSize() +// - ZipConstants.STD_DEC_HDR_SIZE); + } + } + + } + + if (localFileHeader.getCrc32() <= 0) { + localFileHeader.setCrc32(fileHeader.getCrc32()); + localFileHeader.setCrcBuff(fileHeader.getCrcBuff()); + } + + if (localFileHeader.getCompressedSize() <= 0) { + localFileHeader.setCompressedSize(fileHeader.getCompressedSize()); + } + + if (localFileHeader.getUncompressedSize() <= 0) { + localFileHeader.setUncompressedSize(fileHeader.getUncompressedSize()); + } + + return localFileHeader; + } catch (IOException e) { + throw new ZipException(e); + } + } + + /** + * Reads AES Extra Data Record and saves it in the {@link FileHeader} + * @param fileHeader + * @throws ZipException + */ + private void readAndSaveAESExtraDataRecord(FileHeader fileHeader) throws ZipException { + if (fileHeader == null) { + throw new ZipException("file header is null in reading Zip64 Extended Info"); + } + + if (fileHeader.getExtraDataRecords() == null || fileHeader.getExtraDataRecords().size() <= 0) { + return; + } + + AESExtraDataRecord aesExtraDataRecord = readAESExtraDataRecord(fileHeader.getExtraDataRecords()); + if (aesExtraDataRecord != null) { + fileHeader.setAesExtraDataRecord(aesExtraDataRecord); + fileHeader.setEncryptionMethod(Zip4jConstants.ENC_METHOD_AES); + } + } + + /** + * Reads AES Extra Data Record and saves it in the {@link LocalFileHeader} + * @param localFileHeader + * @throws ZipException + */ + private void readAndSaveAESExtraDataRecord(LocalFileHeader localFileHeader) throws ZipException { + if (localFileHeader == null) { + throw new ZipException("file header is null in reading Zip64 Extended Info"); + } + + if (localFileHeader.getExtraDataRecords() == null || localFileHeader.getExtraDataRecords().size() <= 0) { + return; + } + + AESExtraDataRecord aesExtraDataRecord = readAESExtraDataRecord(localFileHeader.getExtraDataRecords()); + if (aesExtraDataRecord != null) { + localFileHeader.setAesExtraDataRecord(aesExtraDataRecord); + localFileHeader.setEncryptionMethod(Zip4jConstants.ENC_METHOD_AES); + } + } + + /** + * Reads AES Extra Data Record + * @param extraDataRecords + * @return {@link AESExtraDataRecord} + * @throws ZipException + */ + private AESExtraDataRecord readAESExtraDataRecord(ArrayList extraDataRecords) throws ZipException { + + if (extraDataRecords == null) { + return null; + } + + for (int i = 0; i < extraDataRecords.size(); i++) { + ExtraDataRecord extraDataRecord = (ExtraDataRecord)extraDataRecords.get(i); + if (extraDataRecord == null) { + continue; + } + + if (extraDataRecord.getHeader() == InternalZipConstants.AESSIG) { + + if (extraDataRecord.getData() == null) { + throw new ZipException("corrput AES extra data records"); + } + + AESExtraDataRecord aesExtraDataRecord = new AESExtraDataRecord(); + + aesExtraDataRecord.setSignature(InternalZipConstants.AESSIG); + aesExtraDataRecord.setDataSize(extraDataRecord.getSizeOfData()); + + byte[] aesData = extraDataRecord.getData(); + aesExtraDataRecord.setVersionNumber(Raw.readShortLittleEndian(aesData, 0)); + byte[] vendorIDBytes = new byte[2]; + System.arraycopy(aesData, 2, vendorIDBytes, 0, 2); + aesExtraDataRecord.setVendorID(new String(vendorIDBytes)); + aesExtraDataRecord.setAesStrength((int)(aesData[4] & 0xFF)); + aesExtraDataRecord.setCompressionMethod(Raw.readShortLittleEndian(aesData, 5)); + + return aesExtraDataRecord; + } + } + + return null; + } + + /** + * Reads buf length of bytes from the input stream to buf + * @param zip4jRaf + * @param buf + * @return byte array + * @throws ZipException + */ + private byte[] readIntoBuff(RandomAccessFile zip4jRaf, byte[] buf) throws ZipException { + try { + if (zip4jRaf.read(buf, 0, buf.length) != -1) { + return buf; + } else { + throw new ZipException("unexpected end of file when reading short buff"); + } + } catch (IOException e) { + throw new ZipException("IOException when reading short buff", e); + } + } + + /** + * Returns a long byte from an int byte by appending last 4 bytes as 0's + * @param intByte + * @return byte array + * @throws ZipException + */ + private byte[] getLongByteFromIntByte(byte[] intByte) throws ZipException { + if (intByte == null) { + throw new ZipException("input parameter is null, cannot expand to 8 bytes"); + } + + if (intByte.length != 4) { + throw new ZipException("invalid byte length, cannot expand to 8 bytes"); + } + + byte[] longBuff = {intByte[0], intByte[1], intByte[2], intByte[3], 0, 0, 0, 0}; + return longBuff; + } +} diff --git a/src/net/lingala/zip4j/core/HeaderWriter.java b/src/net/lingala/zip4j/core/HeaderWriter.java new file mode 100644 index 0000000..79b2ab4 --- /dev/null +++ b/src/net/lingala/zip4j/core/HeaderWriter.java @@ -0,0 +1,943 @@ +/* +* 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.core; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.io.SplitOutputStream; +import net.lingala.zip4j.model.AESExtraDataRecord; +import net.lingala.zip4j.model.FileHeader; +import net.lingala.zip4j.model.LocalFileHeader; +import net.lingala.zip4j.model.Zip64EndCentralDirLocator; +import net.lingala.zip4j.model.Zip64EndCentralDirRecord; +import net.lingala.zip4j.model.ZipModel; +import net.lingala.zip4j.util.InternalZipConstants; +import net.lingala.zip4j.util.Raw; +import net.lingala.zip4j.util.Zip4jUtil; + +public class HeaderWriter { + + private final int ZIP64_EXTRA_BUF = 50; + + public int writeLocalFileHeader(ZipModel zipModel, LocalFileHeader localFileHeader, + OutputStream outputStream) throws ZipException { + if (localFileHeader == null) { + throw new ZipException("input parameters are null, cannot write local file header"); + } + + try { + ArrayList byteArrayList = new ArrayList(); + + byte[] shortByte = new byte[2]; + byte[] intByte = new byte[4]; + byte[] longByte = new byte[8]; + byte[] emptyLongByte = {0,0,0,0,0,0,0,0}; + + Raw.writeIntLittleEndian(intByte, 0, localFileHeader.getSignature()); + copyByteArrayToArrayList(intByte, byteArrayList); + Raw.writeShortLittleEndian(shortByte, 0, (short)localFileHeader.getVersionNeededToExtract()); + copyByteArrayToArrayList(shortByte, byteArrayList); + //General Purpose bit flags + copyByteArrayToArrayList(localFileHeader.getGeneralPurposeFlag(), byteArrayList); + //Compression Method + Raw.writeShortLittleEndian(shortByte, 0, (short)localFileHeader.getCompressionMethod()); + copyByteArrayToArrayList(shortByte, byteArrayList); + //File modified time + int dateTime = localFileHeader.getLastModFileTime(); + Raw.writeIntLittleEndian(intByte, 0, (int)dateTime); + copyByteArrayToArrayList(intByte, byteArrayList); + //Skip crc for now - this field will be updated after data is compressed + Raw.writeIntLittleEndian(intByte, 0, (int)localFileHeader.getCrc32()); + copyByteArrayToArrayList(intByte, byteArrayList); + boolean writingZip64Rec = false; + + //compressed & uncompressed size + long uncompressedSize = localFileHeader.getUncompressedSize(); + if (uncompressedSize + ZIP64_EXTRA_BUF >= InternalZipConstants.ZIP_64_LIMIT) { + Raw.writeLongLittleEndian(longByte, 0, InternalZipConstants.ZIP_64_LIMIT); + System.arraycopy(longByte, 0, intByte, 0, 4); + + //Set the uncompressed size to ZipConstants.ZIP_64_LIMIT as + //these values will be stored in Zip64 extra record + copyByteArrayToArrayList(intByte, byteArrayList); + + copyByteArrayToArrayList(intByte, byteArrayList); + zipModel.setZip64Format(true); + writingZip64Rec = true; + localFileHeader.setWriteComprSizeInZip64ExtraRecord(true); + } else { + Raw.writeLongLittleEndian(longByte, 0, localFileHeader.getCompressedSize()); + System.arraycopy(longByte, 0, intByte, 0, 4); + copyByteArrayToArrayList(intByte, byteArrayList); + + Raw.writeLongLittleEndian(longByte, 0, localFileHeader.getUncompressedSize()); + System.arraycopy(longByte, 0, intByte, 0, 4); + //Raw.writeIntLittleEndian(intByte, 0, (int)localFileHeader.getUncompressedSize()); + copyByteArrayToArrayList(intByte, byteArrayList); + + localFileHeader.setWriteComprSizeInZip64ExtraRecord(false); + } + Raw.writeShortLittleEndian(shortByte, 0, (short)localFileHeader.getFileNameLength()); + copyByteArrayToArrayList(shortByte, byteArrayList); + // extra field length + int extraFieldLength = 0; + if (writingZip64Rec) { + extraFieldLength += 20; + } + if (localFileHeader.getAesExtraDataRecord() != null) { + extraFieldLength += 11; + } + Raw.writeShortLittleEndian(shortByte, 0, (short)(extraFieldLength)); + copyByteArrayToArrayList(shortByte, byteArrayList); + if (Zip4jUtil.isStringNotNullAndNotEmpty(zipModel.getFileNameCharset())) { + byte[] fileNameBytes = localFileHeader.getFileName().getBytes(zipModel.getFileNameCharset()); + copyByteArrayToArrayList(fileNameBytes, byteArrayList); + } else { + copyByteArrayToArrayList(Zip4jUtil.convertCharset(localFileHeader.getFileName()), byteArrayList); + } + + //Zip64 should be the first extra data record that should be written + //This is NOT according to any specification but if this is changed + //then take care of updateLocalFileHeader for compressed size + if (writingZip64Rec) { + + + //Zip64 header + Raw.writeShortLittleEndian(shortByte, 0, (short)InternalZipConstants.EXTRAFIELDZIP64LENGTH); + copyByteArrayToArrayList(shortByte, byteArrayList); + //Zip64 extra data record size + //hardcoded it to 16 for local file header as we will just write + //compressed and uncompressed file sizes + Raw.writeShortLittleEndian(shortByte, 0, (short)16); + copyByteArrayToArrayList(shortByte, byteArrayList); + //uncompressed size + Raw.writeLongLittleEndian(longByte, 0, localFileHeader.getUncompressedSize()); + copyByteArrayToArrayList(longByte, byteArrayList); + //set compressed size to 0 for now + copyByteArrayToArrayList(emptyLongByte, byteArrayList); + } + + if (localFileHeader.getAesExtraDataRecord() != null) { + AESExtraDataRecord aesExtraDataRecord = localFileHeader.getAesExtraDataRecord(); + + Raw.writeShortLittleEndian(shortByte, 0, (short)aesExtraDataRecord.getSignature()); + copyByteArrayToArrayList(shortByte, byteArrayList); + + Raw.writeShortLittleEndian(shortByte, 0, (short)aesExtraDataRecord.getDataSize()); + copyByteArrayToArrayList(shortByte, byteArrayList); + + Raw.writeShortLittleEndian(shortByte, 0, (short)aesExtraDataRecord.getVersionNumber()); + copyByteArrayToArrayList(shortByte, byteArrayList); + + copyByteArrayToArrayList(aesExtraDataRecord.getVendorID().getBytes(), byteArrayList); + + byte[] aesStrengthBytes = new byte[1]; + aesStrengthBytes[0] = (byte)aesExtraDataRecord.getAesStrength(); + copyByteArrayToArrayList(aesStrengthBytes, byteArrayList); + + Raw.writeShortLittleEndian(shortByte, 0, (short)aesExtraDataRecord.getCompressionMethod()); + copyByteArrayToArrayList(shortByte, byteArrayList); + } + byte[] lhBytes = byteArrayListToByteArray(byteArrayList); + outputStream.write(lhBytes); + return lhBytes.length; + } catch (ZipException e) { + throw e; + } catch (Exception e) { + throw new ZipException(e); + } + } + + public int writeExtendedLocalHeader(LocalFileHeader localFileHeader, + OutputStream outputStream) throws ZipException, IOException { + if (localFileHeader == null || outputStream == null) { + throw new ZipException("input parameters is null, cannot write extended local header"); + } + + ArrayList byteArrayList = new ArrayList(); + byte[] intByte = new byte[4]; + + //Extended local file header signature + Raw.writeIntLittleEndian(intByte, 0, (int)InternalZipConstants.EXTSIG); + copyByteArrayToArrayList(intByte, byteArrayList); + + //CRC + Raw.writeIntLittleEndian(intByte, 0, (int)localFileHeader.getCrc32()); + copyByteArrayToArrayList(intByte, byteArrayList); + + //compressed size + long compressedSize = localFileHeader.getCompressedSize(); + if (compressedSize >= Integer.MAX_VALUE) { + compressedSize = Integer.MAX_VALUE; + } + Raw.writeIntLittleEndian(intByte, 0, (int)compressedSize); + copyByteArrayToArrayList(intByte, byteArrayList); + + //uncompressed size + long uncompressedSize = localFileHeader.getUncompressedSize(); + if (uncompressedSize >= Integer.MAX_VALUE) { + uncompressedSize = Integer.MAX_VALUE; + } + Raw.writeIntLittleEndian(intByte, 0, (int)uncompressedSize); + copyByteArrayToArrayList(intByte, byteArrayList); + + byte[] extLocHdrBytes = byteArrayListToByteArray(byteArrayList); + outputStream.write(extLocHdrBytes); + return extLocHdrBytes.length; + } + + /** + * Processes zip header data and writes this data to the zip file + * @param zipModel + * @param outputStream + * @throws ZipException + */ + public void finalizeZipFile(ZipModel zipModel, + OutputStream outputStream) throws ZipException { + if (zipModel == null || outputStream == null) { + throw new ZipException("input parameters is null, cannot finalize zip file"); + } + + try { + processHeaderData(zipModel, outputStream); + + long offsetCentralDir = zipModel.getEndCentralDirRecord().getOffsetOfStartOfCentralDir(); + + List headerBytesList = new ArrayList(); + + int sizeOfCentralDir = writeCentralDirectory(zipModel, outputStream, headerBytesList); + + if (zipModel.isZip64Format()) { + if (zipModel.getZip64EndCentralDirRecord() == null) { + zipModel.setZip64EndCentralDirRecord(new Zip64EndCentralDirRecord()); + } + if (zipModel.getZip64EndCentralDirLocator() == null) { + zipModel.setZip64EndCentralDirLocator(new Zip64EndCentralDirLocator()); + } + + zipModel.getZip64EndCentralDirLocator().setOffsetZip64EndOfCentralDirRec(offsetCentralDir + sizeOfCentralDir); + if (outputStream instanceof SplitOutputStream) { + zipModel.getZip64EndCentralDirLocator().setNoOfDiskStartOfZip64EndOfCentralDirRec(((SplitOutputStream)outputStream).getCurrSplitFileCounter()); + zipModel.getZip64EndCentralDirLocator().setTotNumberOfDiscs(((SplitOutputStream)outputStream).getCurrSplitFileCounter() + 1); + } else { + zipModel.getZip64EndCentralDirLocator().setNoOfDiskStartOfZip64EndOfCentralDirRec(0); + zipModel.getZip64EndCentralDirLocator().setTotNumberOfDiscs(1); + } + + writeZip64EndOfCentralDirectoryRecord(zipModel, outputStream, sizeOfCentralDir, offsetCentralDir, headerBytesList); + + writeZip64EndOfCentralDirectoryLocator(zipModel, outputStream, headerBytesList); + } + + writeEndOfCentralDirectoryRecord(zipModel, outputStream, sizeOfCentralDir, offsetCentralDir, headerBytesList); + + writeZipHeaderBytes(zipModel, outputStream, byteArrayListToByteArray(headerBytesList)); + } catch (ZipException e) { + throw e; + } catch (Exception e) { + throw new ZipException(e); + } + } + + /** + * Processes zip header data and writes this data to the zip file without any validations. + * This process is not intended to use for normal operations (adding, deleting, etc) of a zip file. + * This method is used when certain validations need to be skipped (ex: Merging split zip files, + * adding comment to a zip file, etc) + * @param zipModel + * @param outputStream + * @throws ZipException + */ + public void finalizeZipFileWithoutValidations(ZipModel zipModel, OutputStream outputStream) throws ZipException { + if (zipModel == null || outputStream == null) { + throw new ZipException("input parameters is null, cannot finalize zip file without validations"); + } + + try { + + List headerBytesList = new ArrayList(); + + long offsetCentralDir = zipModel.getEndCentralDirRecord().getOffsetOfStartOfCentralDir(); + + int sizeOfCentralDir = writeCentralDirectory(zipModel, outputStream, headerBytesList); + + if (zipModel.isZip64Format()) { + if (zipModel.getZip64EndCentralDirRecord() == null) { + zipModel.setZip64EndCentralDirRecord(new Zip64EndCentralDirRecord()); + } + if (zipModel.getZip64EndCentralDirLocator() == null) { + zipModel.setZip64EndCentralDirLocator(new Zip64EndCentralDirLocator()); + } + + zipModel.getZip64EndCentralDirLocator().setOffsetZip64EndOfCentralDirRec(offsetCentralDir + sizeOfCentralDir); + + writeZip64EndOfCentralDirectoryRecord(zipModel, outputStream, sizeOfCentralDir, offsetCentralDir, headerBytesList); + writeZip64EndOfCentralDirectoryLocator(zipModel, outputStream, headerBytesList); + } + + writeEndOfCentralDirectoryRecord(zipModel, outputStream, sizeOfCentralDir, offsetCentralDir, headerBytesList); + + writeZipHeaderBytes(zipModel, outputStream, byteArrayListToByteArray(headerBytesList)); + } catch(ZipException e) { + throw e; + } catch (Exception e) { + throw new ZipException(e); + } + } + + /** + * Writes the zip header data to the zip file + * @param outputStream + * @param buff + * @throws ZipException + */ + private void writeZipHeaderBytes(ZipModel zipModel, OutputStream outputStream, byte[] buff) throws ZipException { + if (buff == null) { + throw new ZipException("invalid buff to write as zip headers"); + } + + try { + if (outputStream instanceof SplitOutputStream) { + if (((SplitOutputStream)outputStream).checkBuffSizeAndStartNextSplitFile(buff.length)) { + finalizeZipFile(zipModel, outputStream); + return; + } + } + + outputStream.write(buff); + } catch (IOException e) { + throw new ZipException(e); + } + } + + /** + * Fills the header data in the zip model + * @param zipModel + * @param outputStream + * @throws ZipException + */ + private void processHeaderData(ZipModel zipModel, OutputStream outputStream) throws ZipException { + try { + int currSplitFileCounter = 0; + if (outputStream instanceof SplitOutputStream) { + zipModel.getEndCentralDirRecord().setOffsetOfStartOfCentralDir( + ((SplitOutputStream)outputStream).getFilePointer()); + currSplitFileCounter = ((SplitOutputStream)outputStream).getCurrSplitFileCounter(); + + } + + if (zipModel.isZip64Format()) { + if (zipModel.getZip64EndCentralDirRecord() == null) { + zipModel.setZip64EndCentralDirRecord(new Zip64EndCentralDirRecord()); + } + if (zipModel.getZip64EndCentralDirLocator() == null) { + zipModel.setZip64EndCentralDirLocator(new Zip64EndCentralDirLocator()); + } + + zipModel.getZip64EndCentralDirLocator().setNoOfDiskStartOfZip64EndOfCentralDirRec(currSplitFileCounter); + zipModel.getZip64EndCentralDirLocator().setTotNumberOfDiscs(currSplitFileCounter + 1); + } + zipModel.getEndCentralDirRecord().setNoOfThisDisk(currSplitFileCounter); + zipModel.getEndCentralDirRecord().setNoOfThisDiskStartOfCentralDir(currSplitFileCounter); + } catch (IOException e) { + throw new ZipException(e); + } + } + + /** + * Writes central directory header data to an array list + * @param zipModel + * @param outputStream + * @param headerBytesList + * @return size of central directory + * @throws ZipException + */ + private int writeCentralDirectory(ZipModel zipModel, + OutputStream outputStream, List headerBytesList) throws ZipException { + if (zipModel == null || outputStream == null) { + throw new ZipException("input parameters is null, cannot write central directory"); + } + + if (zipModel.getCentralDirectory() == null || + zipModel.getCentralDirectory().getFileHeaders() == null || + zipModel.getCentralDirectory().getFileHeaders().size() <= 0) { + return 0; + } + + int sizeOfCentralDir = 0; + for (int i = 0; i < zipModel.getCentralDirectory().getFileHeaders().size(); i++) { + FileHeader fileHeader = (FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i); + int sizeOfFileHeader = writeFileHeader(zipModel, fileHeader, outputStream, headerBytesList); + sizeOfCentralDir += sizeOfFileHeader; + } + return sizeOfCentralDir; + } + + private int writeFileHeader(ZipModel zipModel, FileHeader fileHeader, + OutputStream outputStream, List headerBytesList) throws ZipException { + + if (fileHeader == null || outputStream == null) { + throw new ZipException("input parameters is null, cannot write local file header"); + } + + try { + int sizeOfFileHeader = 0; + + byte[] shortByte = new byte[2]; + byte[] intByte = new byte[4]; + byte[] longByte = new byte[8]; + final byte[] emptyShortByte = {0,0}; + final byte[] emptyIntByte = {0,0,0,0}; + + boolean writeZip64FileSize = false; + boolean writeZip64OffsetLocalHeader = false; + + Raw.writeIntLittleEndian(intByte, 0, fileHeader.getSignature()); + copyByteArrayToArrayList(intByte, headerBytesList); + sizeOfFileHeader += 4; + + Raw.writeShortLittleEndian(shortByte, 0, (short)fileHeader.getVersionMadeBy()); + copyByteArrayToArrayList(shortByte, headerBytesList); + sizeOfFileHeader += 2; + + Raw.writeShortLittleEndian(shortByte, 0, (short)fileHeader.getVersionNeededToExtract()); + copyByteArrayToArrayList(shortByte, headerBytesList); + sizeOfFileHeader += 2; + + copyByteArrayToArrayList(fileHeader.getGeneralPurposeFlag(), headerBytesList); + sizeOfFileHeader += 2; + + Raw.writeShortLittleEndian(shortByte, 0, (short)fileHeader.getCompressionMethod()); + copyByteArrayToArrayList(shortByte, headerBytesList); + sizeOfFileHeader += 2; + + int dateTime = fileHeader.getLastModFileTime(); + Raw.writeIntLittleEndian(intByte, 0, dateTime); + copyByteArrayToArrayList(intByte, headerBytesList); + sizeOfFileHeader += 4; + + Raw.writeIntLittleEndian(intByte, 0, (int)(fileHeader.getCrc32())); + copyByteArrayToArrayList(intByte, headerBytesList); + sizeOfFileHeader += 4; + + if (fileHeader.getCompressedSize() >= InternalZipConstants.ZIP_64_LIMIT || + fileHeader.getUncompressedSize() + ZIP64_EXTRA_BUF >= InternalZipConstants.ZIP_64_LIMIT) { + Raw.writeLongLittleEndian(longByte, 0, InternalZipConstants.ZIP_64_LIMIT); + System.arraycopy(longByte, 0, intByte, 0, 4); + + copyByteArrayToArrayList(intByte, headerBytesList); + sizeOfFileHeader += 4; + + copyByteArrayToArrayList(intByte, headerBytesList); + sizeOfFileHeader += 4; + + writeZip64FileSize = true; + } else { + Raw.writeLongLittleEndian(longByte, 0, fileHeader.getCompressedSize()); + System.arraycopy(longByte, 0, intByte, 0, 4); +// Raw.writeIntLittleEndian(intByte, 0, (int)fileHeader.getCompressedSize()); + copyByteArrayToArrayList(intByte, headerBytesList); + sizeOfFileHeader += 4; + + Raw.writeLongLittleEndian(longByte, 0, fileHeader.getUncompressedSize()); + System.arraycopy(longByte, 0, intByte, 0, 4); +// Raw.writeIntLittleEndian(intByte, 0, (int)fileHeader.getUncompressedSize()); + copyByteArrayToArrayList(intByte, headerBytesList); + sizeOfFileHeader += 4; + } + + Raw.writeShortLittleEndian(shortByte, 0, (short)fileHeader.getFileNameLength()); + copyByteArrayToArrayList(shortByte, headerBytesList); + sizeOfFileHeader += 2; + + //Compute offset bytes before extra field is written for Zip64 compatibility + //NOTE: this data is not written now, but written at a later point + byte[] offsetLocalHeaderBytes = new byte[4]; + if (fileHeader.getOffsetLocalHeader() > InternalZipConstants.ZIP_64_LIMIT) { + Raw.writeLongLittleEndian(longByte, 0, InternalZipConstants.ZIP_64_LIMIT); + System.arraycopy(longByte, 0, offsetLocalHeaderBytes, 0, 4); + writeZip64OffsetLocalHeader = true; + } else { + Raw.writeLongLittleEndian(longByte, 0, fileHeader.getOffsetLocalHeader()); + System.arraycopy(longByte, 0, offsetLocalHeaderBytes, 0, 4); + } + + // extra field length + int extraFieldLength = 0; + if (writeZip64FileSize || writeZip64OffsetLocalHeader) { + extraFieldLength += 4; + if (writeZip64FileSize) + extraFieldLength += 16; + if (writeZip64OffsetLocalHeader) + extraFieldLength += 8; + } + if (fileHeader.getAesExtraDataRecord() != null) { + extraFieldLength += 11; + } + Raw.writeShortLittleEndian(shortByte, 0, (short)(extraFieldLength)); + copyByteArrayToArrayList(shortByte, headerBytesList); + sizeOfFileHeader += 2; + + //Skip file comment length for now + copyByteArrayToArrayList(emptyShortByte, headerBytesList); + sizeOfFileHeader += 2; + + //Skip disk number start for now + Raw.writeShortLittleEndian(shortByte, 0, (short)(fileHeader.getDiskNumberStart())); + copyByteArrayToArrayList(shortByte, headerBytesList); + sizeOfFileHeader += 2; + + //Skip internal file attributes for now + copyByteArrayToArrayList(emptyShortByte, headerBytesList); + sizeOfFileHeader += 2; + + //External file attributes + if (fileHeader.getExternalFileAttr() != null) { + copyByteArrayToArrayList(fileHeader.getExternalFileAttr(), headerBytesList); + } else { + copyByteArrayToArrayList(emptyIntByte, headerBytesList); + } + sizeOfFileHeader += 4; + + //offset local header + //this data is computed above + copyByteArrayToArrayList(offsetLocalHeaderBytes, headerBytesList); + sizeOfFileHeader += 4; + + if (Zip4jUtil.isStringNotNullAndNotEmpty(zipModel.getFileNameCharset())) { + byte[] fileNameBytes = fileHeader.getFileName().getBytes(zipModel.getFileNameCharset()); + copyByteArrayToArrayList(fileNameBytes, headerBytesList); + sizeOfFileHeader += fileNameBytes.length; + } else { + copyByteArrayToArrayList(Zip4jUtil.convertCharset(fileHeader.getFileName()), headerBytesList); + sizeOfFileHeader += Zip4jUtil.getEncodedStringLength(fileHeader.getFileName()); + } + + if (writeZip64FileSize || writeZip64OffsetLocalHeader) { + zipModel.setZip64Format(true); + + //Zip64 header + Raw.writeShortLittleEndian(shortByte, 0, (short)InternalZipConstants.EXTRAFIELDZIP64LENGTH); + copyByteArrayToArrayList(shortByte, headerBytesList); + sizeOfFileHeader += 2; + + //Zip64 extra data record size + int dataSize = 0; + + if (writeZip64FileSize) { + dataSize += 16; + } + if (writeZip64OffsetLocalHeader) { + dataSize += 8; + } + + Raw.writeShortLittleEndian(shortByte, 0, (short)dataSize); + copyByteArrayToArrayList(shortByte, headerBytesList); + sizeOfFileHeader += 2; + + if (writeZip64FileSize) { + Raw.writeLongLittleEndian(longByte, 0, fileHeader.getUncompressedSize()); + copyByteArrayToArrayList(longByte, headerBytesList); + sizeOfFileHeader += 8; + + Raw.writeLongLittleEndian(longByte, 0, fileHeader.getCompressedSize()); + copyByteArrayToArrayList(longByte, headerBytesList); + sizeOfFileHeader += 8; + } + + if (writeZip64OffsetLocalHeader) { + Raw.writeLongLittleEndian(longByte, 0, fileHeader.getOffsetLocalHeader()); + copyByteArrayToArrayList(longByte, headerBytesList); + sizeOfFileHeader += 8; + } + } + + if (fileHeader.getAesExtraDataRecord() != null) { + AESExtraDataRecord aesExtraDataRecord = fileHeader.getAesExtraDataRecord(); + + Raw.writeShortLittleEndian(shortByte, 0, (short)aesExtraDataRecord.getSignature()); + copyByteArrayToArrayList(shortByte, headerBytesList); + + Raw.writeShortLittleEndian(shortByte, 0, (short)aesExtraDataRecord.getDataSize()); + copyByteArrayToArrayList(shortByte, headerBytesList); + + Raw.writeShortLittleEndian(shortByte, 0, (short)aesExtraDataRecord.getVersionNumber()); + copyByteArrayToArrayList(shortByte, headerBytesList); + + copyByteArrayToArrayList(aesExtraDataRecord.getVendorID().getBytes(), headerBytesList); + + byte[] aesStrengthBytes = new byte[1]; + aesStrengthBytes[0] = (byte)aesExtraDataRecord.getAesStrength(); + copyByteArrayToArrayList(aesStrengthBytes, headerBytesList); + + Raw.writeShortLittleEndian(shortByte, 0, (short)aesExtraDataRecord.getCompressionMethod()); + copyByteArrayToArrayList(shortByte, headerBytesList); + + sizeOfFileHeader += 11; + } + +// outputStream.write(byteArrayListToByteArray(headerBytesList)); + + return sizeOfFileHeader; + } catch (Exception e) { + throw new ZipException(e); + } + } + + private void writeZip64EndOfCentralDirectoryRecord(ZipModel zipModel, + OutputStream outputStream, int sizeOfCentralDir, + long offsetCentralDir, List headerBytesList) throws ZipException { + if (zipModel == null || outputStream == null) { + throw new ZipException("zip model or output stream is null, cannot write zip64 end of central directory record"); + } + + try { + + byte[] shortByte = new byte[2]; + byte[] emptyShortByte = {0,0}; + byte[] intByte = new byte[4]; + byte[] longByte = new byte[8]; + + //zip64 end of central dir signature + Raw.writeIntLittleEndian(intByte, 0, (int)InternalZipConstants.ZIP64ENDCENDIRREC); + copyByteArrayToArrayList(intByte, headerBytesList); + + //size of zip64 end of central directory record + Raw.writeLongLittleEndian(longByte, 0, (long)44); + copyByteArrayToArrayList(longByte, headerBytesList); + + //version made by + //version needed to extract + if (zipModel.getCentralDirectory() != null && + zipModel.getCentralDirectory().getFileHeaders() != null && + zipModel.getCentralDirectory().getFileHeaders().size() > 0) { + Raw.writeShortLittleEndian(shortByte, 0, + (short)((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(0)).getVersionMadeBy()); + copyByteArrayToArrayList(shortByte, headerBytesList); + + Raw.writeShortLittleEndian(shortByte, 0, + (short)((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(0)).getVersionNeededToExtract()); + copyByteArrayToArrayList(shortByte, headerBytesList); + } else { + copyByteArrayToArrayList(emptyShortByte, headerBytesList); + copyByteArrayToArrayList(emptyShortByte, headerBytesList); + } + + //number of this disk + Raw.writeIntLittleEndian(intByte, 0, zipModel.getEndCentralDirRecord().getNoOfThisDisk()); + copyByteArrayToArrayList(intByte, headerBytesList); + + //number of the disk with start of central directory + Raw.writeIntLittleEndian(intByte, 0, zipModel.getEndCentralDirRecord().getNoOfThisDiskStartOfCentralDir()); + copyByteArrayToArrayList(intByte, headerBytesList); + + //total number of entries in the central directory on this disk + int numEntries = 0; + int numEntriesOnThisDisk = 0; + if (zipModel.getCentralDirectory() == null || + zipModel.getCentralDirectory().getFileHeaders() == null) { + throw new ZipException("invalid central directory/file headers, " + + "cannot write end of central directory record"); + } else { + numEntries = zipModel.getCentralDirectory().getFileHeaders().size(); + if (zipModel.isSplitArchive()) { + countNumberOfFileHeaderEntriesOnDisk(zipModel.getCentralDirectory().getFileHeaders(), + zipModel.getEndCentralDirRecord().getNoOfThisDisk()); + } else { + numEntriesOnThisDisk = numEntries; + } + } + Raw.writeLongLittleEndian(longByte, 0, numEntriesOnThisDisk); + copyByteArrayToArrayList(longByte, headerBytesList); + + //Total number of entries in central directory + Raw.writeLongLittleEndian(longByte, 0, numEntries); + copyByteArrayToArrayList(longByte, headerBytesList); + + //Size of central directory + Raw.writeLongLittleEndian(longByte, 0, sizeOfCentralDir); + copyByteArrayToArrayList(longByte, headerBytesList); + + //offset of start of central directory with respect to the starting disk number + Raw.writeLongLittleEndian(longByte, 0, offsetCentralDir); + copyByteArrayToArrayList(longByte, headerBytesList); + + } catch (ZipException zipException) { + throw zipException; + } catch (Exception e) { + throw new ZipException(e); + } + } + + private void writeZip64EndOfCentralDirectoryLocator(ZipModel zipModel, + OutputStream outputStream, List headerBytesList) throws ZipException { + if (zipModel == null || outputStream == null) { + throw new ZipException("zip model or output stream is null, cannot write zip64 end of central directory locator"); + } + + try { + + byte[] intByte = new byte[4]; + byte[] longByte = new byte[8]; + + //zip64 end of central dir locator signature + Raw.writeIntLittleEndian(intByte, 0, (int)InternalZipConstants.ZIP64ENDCENDIRLOC); + copyByteArrayToArrayList(intByte, headerBytesList); + + //number of the disk with the start of the zip64 end of central directory + Raw.writeIntLittleEndian(intByte, 0, zipModel.getZip64EndCentralDirLocator().getNoOfDiskStartOfZip64EndOfCentralDirRec()); + copyByteArrayToArrayList(intByte, headerBytesList); + + //relative offset of the zip64 end of central directory record + Raw.writeLongLittleEndian(longByte, 0, zipModel.getZip64EndCentralDirLocator().getOffsetZip64EndOfCentralDirRec()); + copyByteArrayToArrayList(longByte, headerBytesList); + + //total number of disks + Raw.writeIntLittleEndian(intByte, 0, zipModel.getZip64EndCentralDirLocator().getTotNumberOfDiscs()); + copyByteArrayToArrayList(intByte, headerBytesList); + } catch (ZipException zipException) { + throw zipException; + } catch (Exception e) { + throw new ZipException(e); + } + } + + private void writeEndOfCentralDirectoryRecord(ZipModel zipModel, + OutputStream outputStream, + int sizeOfCentralDir, + long offsetCentralDir, + List headrBytesList) throws ZipException { + if (zipModel == null || outputStream == null) { + throw new ZipException("zip model or output stream is null, cannot write end of central directory record"); + } + + try { + + byte[] shortByte = new byte[2]; + byte[] intByte = new byte[4]; + byte[] longByte = new byte[8]; + + //End of central directory signature + Raw.writeIntLittleEndian(intByte, 0, (int)zipModel.getEndCentralDirRecord().getSignature()); + copyByteArrayToArrayList(intByte, headrBytesList); + + //number of this disk + Raw.writeShortLittleEndian(shortByte, 0, (short)(zipModel.getEndCentralDirRecord().getNoOfThisDisk())); + copyByteArrayToArrayList(shortByte, headrBytesList); + + //number of the disk with start of central directory + Raw.writeShortLittleEndian(shortByte, 0, (short)(zipModel.getEndCentralDirRecord().getNoOfThisDiskStartOfCentralDir())); + copyByteArrayToArrayList(shortByte, headrBytesList); + + //Total number of entries in central directory on this disk + int numEntries = 0; + int numEntriesOnThisDisk = 0; + if (zipModel.getCentralDirectory() == null || + zipModel.getCentralDirectory().getFileHeaders() == null) { + throw new ZipException("invalid central directory/file headers, " + + "cannot write end of central directory record"); + } else { + numEntries = zipModel.getCentralDirectory().getFileHeaders().size(); + if (zipModel.isSplitArchive()) { + numEntriesOnThisDisk = countNumberOfFileHeaderEntriesOnDisk(zipModel.getCentralDirectory().getFileHeaders(), + zipModel.getEndCentralDirRecord().getNoOfThisDisk()); + } else { + numEntriesOnThisDisk = numEntries; + } + } + Raw.writeShortLittleEndian(shortByte, 0, (short)numEntriesOnThisDisk); + copyByteArrayToArrayList(shortByte, headrBytesList); + + //Total number of entries in central directory + Raw.writeShortLittleEndian(shortByte, 0, (short)numEntries); + copyByteArrayToArrayList(shortByte, headrBytesList); + + //Size of central directory + Raw.writeIntLittleEndian(intByte, 0, sizeOfCentralDir); + copyByteArrayToArrayList(intByte, headrBytesList); + + //Offset central directory + if (offsetCentralDir > InternalZipConstants.ZIP_64_LIMIT) { + Raw.writeLongLittleEndian(longByte, 0, InternalZipConstants.ZIP_64_LIMIT); + System.arraycopy(longByte, 0, intByte, 0, 4); + copyByteArrayToArrayList(intByte, headrBytesList); + } else { + Raw.writeLongLittleEndian(longByte, 0, offsetCentralDir); + System.arraycopy(longByte, 0, intByte, 0, 4); +// Raw.writeIntLittleEndian(intByte, 0, (int)offsetCentralDir); + copyByteArrayToArrayList(intByte, headrBytesList); + } + + //Zip File comment length + int commentLength = 0; + if (zipModel.getEndCentralDirRecord().getComment() != null) { + commentLength = zipModel.getEndCentralDirRecord().getCommentLength(); + } + Raw.writeShortLittleEndian(shortByte, 0, (short)commentLength); + copyByteArrayToArrayList(shortByte, headrBytesList); + + //Comment + if (commentLength > 0) { + copyByteArrayToArrayList(zipModel.getEndCentralDirRecord().getCommentBytes(), headrBytesList); + } + + } catch (Exception e) { + throw new ZipException(e); + } + } + + public void updateLocalFileHeader(LocalFileHeader localFileHeader, long offset, + int toUpdate, ZipModel zipModel, byte[] bytesToWrite, int noOfDisk, SplitOutputStream outputStream) throws ZipException { + if (localFileHeader == null || offset < 0 || zipModel == null) { + throw new ZipException("invalid input parameters, cannot update local file header"); + } + + try { + boolean closeFlag = false; + SplitOutputStream currOutputStream = null; + + if (noOfDisk != outputStream.getCurrSplitFileCounter()) { + File zipFile = new File(zipModel.getZipFile()); + String parentFile = zipFile.getParent(); + String fileNameWithoutExt = Zip4jUtil.getZipFileNameWithoutExt(zipFile.getName()); + String fileName = parentFile + System.getProperty("file.separator"); + if (noOfDisk < 9) { + fileName += fileNameWithoutExt + ".z0" + (noOfDisk + 1); + } else { + fileName += fileNameWithoutExt + ".z" + (noOfDisk + 1); + } + currOutputStream = new SplitOutputStream(new File(fileName)); + closeFlag = true; + } else { + currOutputStream = outputStream; + } + + long currOffset = currOutputStream.getFilePointer(); + + switch (toUpdate) { + case InternalZipConstants.UPDATE_LFH_CRC: + currOutputStream.seek(offset + toUpdate); + currOutputStream.write(bytesToWrite); + break; + case InternalZipConstants.UPDATE_LFH_COMP_SIZE: + case InternalZipConstants.UPDATE_LFH_UNCOMP_SIZE: + updateCompressedSizeInLocalFileHeader(currOutputStream, localFileHeader, + offset, toUpdate, bytesToWrite, zipModel.isZip64Format()); + break; + default: + break; + } + if (closeFlag) { + currOutputStream.close(); + } else { + outputStream.seek(currOffset); + } + + } catch (Exception e) { + throw new ZipException(e); + } + } + + private void updateCompressedSizeInLocalFileHeader(SplitOutputStream outputStream, LocalFileHeader localFileHeader, + long offset, long toUpdate, byte[] bytesToWrite, boolean isZip64Format) throws ZipException { + + if (outputStream == null) { + throw new ZipException("invalid output stream, cannot update compressed size for local file header"); + } + + try { + if (localFileHeader.isWriteComprSizeInZip64ExtraRecord()) { + if (bytesToWrite.length != 8) { + throw new ZipException("attempting to write a non 8-byte compressed size block for a zip64 file"); + } + + //4 - compressed size + //4 - uncomprssed size + //2 - file name length + //2 - extra field length + //file name length + //2 - Zip64 signature + //2 - size of zip64 data + //8 - crc + //8 - compressed size + //8 - uncompressed size + long zip64CompressedSizeOffset = offset + toUpdate + 4 + 4 + 2 + 2 + localFileHeader.getFileNameLength() + 2 + 2 + 8; + if (toUpdate == InternalZipConstants.UPDATE_LFH_UNCOMP_SIZE) { + zip64CompressedSizeOffset += 8; + } + outputStream.seek(zip64CompressedSizeOffset); + outputStream.write(bytesToWrite); + } else { + outputStream.seek(offset + toUpdate); + outputStream.write(bytesToWrite); + } + } catch (IOException e) { + throw new ZipException(e); + } + + } + + private void copyByteArrayToArrayList(byte[] byteArray, List arrayList) throws ZipException { + if (arrayList == null || byteArray == null) { + throw new ZipException("one of the input parameters is null, cannot copy byte array to array list"); + } + + for (int i = 0; i < byteArray.length; i++) { + arrayList.add(Byte.toString(byteArray[i])); + } + } + + private byte[] byteArrayListToByteArray(List arrayList) throws ZipException { + if (arrayList == null) { + throw new ZipException("input byte array list is null, cannot conver to byte array"); + } + + if (arrayList.size() <= 0) { + return null; + } + + byte[] retBytes = new byte[arrayList.size()]; + + for (int i = 0; i < arrayList.size(); i++) { + retBytes[i] = Byte.parseByte((String)arrayList.get(i)); + } + + return retBytes; + } + + private int countNumberOfFileHeaderEntriesOnDisk(ArrayList fileHeaders, + int numOfDisk) throws ZipException { + if (fileHeaders == null) { + throw new ZipException("file headers are null, cannot calculate number of entries on this disk"); + } + + int noEntries = 0; + for (int i = 0; i < fileHeaders.size(); i++) { + FileHeader fileHeader = (FileHeader)fileHeaders.get(i); + if (fileHeader.getDiskNumberStart() == numOfDisk) { + noEntries++; + } + } + return noEntries; + } + +} \ No newline at end of file diff --git a/src/net/lingala/zip4j/core/ZipFile.java b/src/net/lingala/zip4j/core/ZipFile.java new file mode 100644 index 0000000..796e8e2 --- /dev/null +++ b/src/net/lingala/zip4j/core/ZipFile.java @@ -0,0 +1,1041 @@ +/* +* 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.core; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.exception.ZipExceptionConstants; +import net.lingala.zip4j.io.ZipInputStream; +import net.lingala.zip4j.model.FileHeader; +import net.lingala.zip4j.model.UnzipParameters; +import net.lingala.zip4j.model.ZipModel; +import net.lingala.zip4j.model.ZipParameters; +import net.lingala.zip4j.progress.ProgressMonitor; +import net.lingala.zip4j.unzip.Unzip; +import net.lingala.zip4j.util.ArchiveMaintainer; +import net.lingala.zip4j.util.InternalZipConstants; +import net.lingala.zip4j.util.Zip4jUtil; +import net.lingala.zip4j.zip.ZipEngine; + +/** + * Base class to handle zip files. Some of the operations supported + * in this class are:
+ * + * + */ + +public class ZipFile { + + private String file; + private int mode; + private ZipModel zipModel; + private boolean isEncrypted; + private ProgressMonitor progressMonitor; + private boolean runInThread; + private String fileNameCharset; + + /** + * Creates a new Zip File Object with the given zip file path. + * If the zip file does not exist, it is not created at this point. + * @param zipFile + * @throws ZipException + */ + public ZipFile(String zipFile) throws ZipException { + this(new File(zipFile)); + } + + /** + * Creates a new Zip File Object with the input file. + * If the zip file does not exist, it is not created at this point. + * @param zipFile + * @throws ZipException + */ + public ZipFile(File zipFile) throws ZipException { + if (zipFile == null) { + throw new ZipException("Input zip file parameter is not null", + ZipExceptionConstants.inputZipParamIsNull); + } + + this.file = zipFile.getPath(); + this.mode = InternalZipConstants.MODE_UNZIP; + this.progressMonitor = new ProgressMonitor(); + this.runInThread = false; + } + + /** + * Creates a zip file and adds the source file to the zip file. If the zip file + * exists then this method throws an exception. Parameters such as compression type, etc + * can be set in the input parameters + * @param sourceFile - File to be added to the zip file + * @param parameters - parameters to create the zip file + * @throws ZipException + */ + public void createZipFile(File sourceFile, ZipParameters parameters) throws ZipException { + ArrayList sourceFileList = new ArrayList(); + sourceFileList.add(sourceFile); + createZipFile(sourceFileList, parameters, false, -1); + } + + /** + * Creates a zip file and adds the source file to the zip file. If the zip file + * exists then this method throws an exception. Parameters such as compression type, etc + * can be set in the input parameters. While the method addFile/addFiles also creates the + * zip file if it does not exist, the main functionality of this method is to create a split + * zip file. To create a split zip file, set the splitArchive parameter to true with a valid + * splitLength. Split Length has to be more than 65536 bytes + * @param sourceFile - File to be added to the zip file + * @param parameters - parameters to create the zip file + * @param splitArchive - if archive has to be split or not + * @param splitLength - if archive has to be split, then length in bytes at which it has to be split + * @throws ZipException + */ + public void createZipFile(File sourceFile, ZipParameters parameters, + boolean splitArchive, long splitLength) throws ZipException { + + ArrayList sourceFileList = new ArrayList(); + sourceFileList.add(sourceFile); + createZipFile(sourceFileList, parameters, splitArchive, splitLength); + } + + /** + * Creates a zip file and adds the list of source file(s) to the zip file. If the zip file + * exists then this method throws an exception. Parameters such as compression type, etc + * can be set in the input parameters + * @param sourceFileList - File to be added to the zip file + * @param parameters - parameters to create the zip file + * @throws ZipException + */ + public void createZipFile(ArrayList sourceFileList, + ZipParameters parameters) throws ZipException { + createZipFile(sourceFileList, parameters, false, -1); + } + + /** + * Creates a zip file and adds the list of source file(s) to the zip file. If the zip file + * exists then this method throws an exception. Parameters such as compression type, etc + * can be set in the input parameters. While the method addFile/addFiles also creates the + * zip file if it does not exist, the main functionality of this method is to create a split + * zip file. To create a split zip file, set the splitArchive parameter to true with a valid + * splitLength. Split Length has to be more than 65536 bytes + * @param sourceFileList - File to be added to the zip file + * @param parameters - zip parameters for this file list + * @param splitArchive - if archive has to be split or not + * @param splitLength - if archive has to be split, then length in bytes at which it has to be split + * @throws ZipException + */ + public void createZipFile(ArrayList sourceFileList, ZipParameters parameters, + boolean splitArchive, long splitLength) throws ZipException { + + if (!Zip4jUtil.isStringNotNullAndNotEmpty(file)) { + throw new ZipException("zip file path is empty"); + } + + if (Zip4jUtil.checkFileExists(file)) { + throw new ZipException("zip file: " + file + " already exists. To add files to existing zip file use addFile method"); + } + + if (sourceFileList == null) { + throw new ZipException("input file ArrayList is null, cannot create zip file"); + } + + if (!Zip4jUtil.checkArrayListTypes(sourceFileList, InternalZipConstants.LIST_TYPE_FILE)) { + throw new ZipException("One or more elements in the input ArrayList is not of type File"); + } + + createNewZipModel(); + this.zipModel.setSplitArchive(splitArchive); + this.zipModel.setSplitLength(splitLength); + addFiles(sourceFileList, parameters); + } + + /** + * Creates a zip file and adds the files/folders from the specified folder to the zip file. + * This method does the same functionality as in addFolder method except that this method + * can also create split zip files when adding a folder. To create a split zip file, set the + * splitArchive parameter to true and specify the splitLength. Split length has to be more than + * or equal to 65536 bytes. Note that this method throws an exception if the zip file already + * exists. + * @param folderToAdd + * @param parameters + * @param splitArchive + * @param splitLength + * @throws ZipException + */ + public void createZipFileFromFolder(String folderToAdd, ZipParameters parameters, + boolean splitArchive, long splitLength) throws ZipException { + + if (!Zip4jUtil.isStringNotNullAndNotEmpty(folderToAdd)) { + throw new ZipException("folderToAdd is empty or null, cannot create Zip File from folder"); + } + + createZipFileFromFolder(new File(folderToAdd), parameters, splitArchive, splitLength); + + } + + /** + * Creates a zip file and adds the files/folders from the specified folder to the zip file. + * This method does the same functionality as in addFolder method except that this method + * can also create split zip files when adding a folder. To create a split zip file, set the + * splitArchive parameter to true and specify the splitLength. Split length has to be more than + * or equal to 65536 bytes. Note that this method throws an exception if the zip file already + * exists. + * @param folderToAdd + * @param parameters + * @param splitArchive + * @param splitLength + * @throws ZipException + */ + public void createZipFileFromFolder(File folderToAdd, ZipParameters parameters, + boolean splitArchive, long splitLength) throws ZipException { + + if (folderToAdd == null) { + throw new ZipException("folderToAdd is null, cannot create zip file from folder"); + } + + if (parameters == null) { + throw new ZipException("input parameters are null, cannot create zip file from folder"); + } + + if (Zip4jUtil.checkFileExists(file)) { + throw new ZipException("zip file: " + file + " already exists. To add files to existing zip file use addFolder method"); + } + + createNewZipModel(); + this.zipModel.setSplitArchive(splitArchive); + if (splitArchive) + this.zipModel.setSplitLength(splitLength); + + addFolder(folderToAdd, parameters, false); + } + + /** + * Adds input source file to the zip file. If zip file does not exist, then + * this method creates a new zip file. Parameters such as compression type, etc + * can be set in the input parameters. + * @param sourceFile - File to tbe added to the zip file + * @param parameters - zip parameters for this file + * @throws ZipException + */ + public void addFile(File sourceFile, ZipParameters parameters) throws ZipException { + ArrayList sourceFileList = new ArrayList(); + sourceFileList.add(sourceFile); + addFiles(sourceFileList, parameters); + } + + /** + * Adds the list of input files to the zip file. If zip file does not exist, then + * this method creates a new zip file. Parameters such as compression type, etc + * can be set in the input parameters. + * @param sourceFileList + * @param parameters + * @throws ZipException + */ + public void addFiles(ArrayList sourceFileList, ZipParameters parameters) throws ZipException { + + checkZipModel(); + + if (this.zipModel == null) { + throw new ZipException("internal error: zip model is null"); + } + + if (sourceFileList == null) { + throw new ZipException("input file ArrayList is null, cannot add files"); + } + + if (!Zip4jUtil.checkArrayListTypes(sourceFileList, InternalZipConstants.LIST_TYPE_FILE)) { + throw new ZipException("One or more elements in the input ArrayList is not of type File"); + } + + if (parameters == null) { + throw new ZipException("input parameters are null, cannot add files to zip"); + } + + if (progressMonitor.getState() == ProgressMonitor.STATE_BUSY) { + throw new ZipException("invalid operation - Zip4j is in busy state"); + } + + if (Zip4jUtil.checkFileExists(file)) { + if (zipModel.isSplitArchive()) { + throw new ZipException("Zip file already exists. Zip file format does not allow updating split/spanned files"); + } + } + + ZipEngine zipEngine = new ZipEngine(zipModel); + zipEngine.addFiles(sourceFileList, parameters, progressMonitor, runInThread); + } + + /** + * Adds the folder in the given path to the zip file. If zip file does not exist, + * then a new zip file is created. If input folder path is invalid then an exception + * is thrown. Zip parameters for the files in the folder to be added can be set in + * the input parameters + * @param path + * @param parameters + * @throws ZipException + */ + public void addFolder(String path, ZipParameters parameters) throws ZipException { + if (!Zip4jUtil.isStringNotNullAndNotEmpty(path)) { + throw new ZipException("input path is null or empty, cannot add folder to zip file"); + } + + addFolder(new File(path), parameters); + } + + /** + * Adds the folder in the given file object to the zip file. If zip file does not exist, + * then a new zip file is created. If input folder is invalid then an exception + * is thrown. Zip parameters for the files in the folder to be added can be set in + * the input parameters + * @param path + * @param parameters + * @throws ZipException + */ + public void addFolder(File path, ZipParameters parameters) throws ZipException { + if (path == null) { + throw new ZipException("input path is null, cannot add folder to zip file"); + } + + if (parameters == null) { + throw new ZipException("input parameters are null, cannot add folder to zip file"); + } + + addFolder(path, parameters, true); + } + + /** + * Internal method to add a folder to the zip file. + * @param path + * @param parameters + * @param checkSplitArchive + * @throws ZipException + */ + private void addFolder(File path, ZipParameters parameters, + boolean checkSplitArchive) throws ZipException { + + checkZipModel(); + + if (this.zipModel == null) { + throw new ZipException("internal error: zip model is null"); + } + + if (checkSplitArchive) { + if (this.zipModel.isSplitArchive()) { + throw new ZipException("This is a split archive. Zip file format does not allow updating split/spanned files"); + } + } + + ZipEngine zipEngine = new ZipEngine(zipModel); + zipEngine.addFolderToZip(path, parameters, progressMonitor, runInThread); + + } + + /** + * Creates a new entry in the zip file and adds the content of the inputstream to the + * zip file. ZipParameters.isSourceExternalStream and ZipParameters.fileNameInZip have to be + * set before in the input parameters. If the file name ends with / or \, this method treats the + * content as a directory. Setting the flag ProgressMonitor.setRunInThread to true will have + * no effect for this method and hence this method cannot be used to add content to zip in + * thread mode + * @param inputStream + * @param parameters + * @throws ZipException + */ + public void addStream(InputStream inputStream, ZipParameters parameters) throws ZipException { + if (inputStream == null) { + throw new ZipException("inputstream is null, cannot add file to zip"); + } + + if (parameters == null) { + throw new ZipException("zip parameters are null"); + } + + this.setRunInThread(false); + + checkZipModel(); + + if (this.zipModel == null) { + throw new ZipException("internal error: zip model is null"); + } + + if (Zip4jUtil.checkFileExists(file)) { + if (zipModel.isSplitArchive()) { + throw new ZipException("Zip file already exists. Zip file format does not allow updating split/spanned files"); + } + } + + ZipEngine zipEngine = new ZipEngine(zipModel); + zipEngine.addStreamToZip(inputStream, parameters); + } + + /** + * Reads the zip header information for this zip file. If the zip file + * does not exist, then this method throws an exception.

+ * Note: This method does not read local file header information + * @throws ZipException + */ + private void readZipInfo() throws ZipException { + + if (!Zip4jUtil.checkFileExists(file)) { + throw new ZipException("zip file does not exist"); + } + + if (!Zip4jUtil.checkFileReadAccess(this.file)) { + throw new ZipException("no read access for the input zip file"); + } + + if (this.mode != InternalZipConstants.MODE_UNZIP) { + throw new ZipException("Invalid mode"); + } + + RandomAccessFile raf = null; + try { + raf = new RandomAccessFile(new File(file), InternalZipConstants.READ_MODE); + + if (zipModel == null) { + + HeaderReader headerReader = new HeaderReader(raf); + zipModel = headerReader.readAllHeaders(this.fileNameCharset); + if (zipModel != null) { + zipModel.setZipFile(file); + } + } + } catch (FileNotFoundException e) { + throw new ZipException(e); + } finally { + if (raf != null) { + try { + raf.close(); + } catch (IOException e) { + //ignore + } + } + } + } + + /** + * Extracts all the files in the given zip file to the input destination path. + * If zip file does not exist or destination path is invalid then an + * exception is thrown. + * @param destPath + * @throws ZipException + */ + public void extractAll(String destPath) throws ZipException { + extractAll(destPath, null); + + } + + /** + * Extracts all the files in the given zip file to the input destination path. + * If zip file does not exist or destination path is invalid then an + * exception is thrown. + * @param destPath + * @param unzipParameters + * @throws ZipException + */ + public void extractAll(String destPath, + UnzipParameters unzipParameters) throws ZipException { + + if (!Zip4jUtil.isStringNotNullAndNotEmpty(destPath)) { + throw new ZipException("output path is null or invalid"); + } + + if (!Zip4jUtil.checkOutputFolder(destPath)) { + throw new ZipException("invalid output path"); + } + + if (zipModel == null) { + readZipInfo(); + } + + // Throw an exception if zipModel is still null + if (zipModel == null) { + throw new ZipException("Internal error occurred when extracting zip file"); + } + + if (progressMonitor.getState() == ProgressMonitor.STATE_BUSY) { + throw new ZipException("invalid operation - Zip4j is in busy state"); + } + + Unzip unzip = new Unzip(zipModel); + unzip.extractAll(unzipParameters, destPath, progressMonitor, runInThread); + + } + + /** + * Extracts a specific file from the zip file to the destination path. + * If destination path is invalid, then this method throws an exception. + * @param fileHeader + * @param destPath + * @throws ZipException + */ + public void extractFile(FileHeader fileHeader, String destPath) throws ZipException { + extractFile(fileHeader, destPath, null); + } + + /** + * Extracts a specific file from the zip file to the destination path. + * If destination path is invalid, then this method throws an exception. + *

+ * If newFileName is not null or empty, newly created file name will be replaced by + * the value in newFileName. If this value is null, then the file name will be the + * value in FileHeader.getFileName + * @param fileHeader + * @param destPath + * @param unzipParameters + * @throws ZipException + */ + public void extractFile(FileHeader fileHeader, + String destPath, UnzipParameters unzipParameters) throws ZipException { + extractFile(fileHeader, destPath, unzipParameters, null); + } + + /** + * Extracts a specific file from the zip file to the destination path. + * If destination path is invalid, then this method throws an exception. + * @param fileHeader + * @param destPath + * @param unzipParameters + * @param newFileName + * @throws ZipException + */ + public void extractFile(FileHeader fileHeader, String destPath, + UnzipParameters unzipParameters, String newFileName) throws ZipException { + + if (fileHeader == null) { + throw new ZipException("input file header is null, cannot extract file"); + } + + if (!Zip4jUtil.isStringNotNullAndNotEmpty(destPath)) { + throw new ZipException("destination path is empty or null, cannot extract file"); + } + + readZipInfo(); + + if (progressMonitor.getState() == ProgressMonitor.STATE_BUSY) { + throw new ZipException("invalid operation - Zip4j is in busy state"); + } + + fileHeader.extractFile(zipModel, destPath, unzipParameters, newFileName, progressMonitor, runInThread); + + } + + /** + * Extracts a specific file from the zip file to the destination path. + * This method first finds the necessary file header from the input file name. + *

+ * File name is relative file name in the zip file. For example if a zip file contains + * a file "a.txt", then to extract this file, input file name has to be "a.txt". Another + * example is if there is a file "b.txt" in a folder "abc" in the zip file, then the + * input file name has to be abc/b.txt + *

+ * Throws an exception if file header could not be found for the given file name or if + * the destination path is invalid + * @param fileName + * @param destPath + * @throws ZipException + */ + public void extractFile(String fileName, String destPath) throws ZipException { + extractFile(fileName, destPath, null); + } + + /** + * Extracts a specific file from the zip file to the destination path. + * This method first finds the necessary file header from the input file name. + *

+ * File name is relative file name in the zip file. For example if a zip file contains + * a file "a.txt", then to extract this file, input file name has to be "a.txt". Another + * example is if there is a file "b.txt" in a folder "abc" in the zip file, then the + * input file name has to be abc/b.txt + *

+ * Throws an exception if file header could not be found for the given file name or if + * the destination path is invalid + * @param fileName + * @param destPath + * @param unzipParameters + * @throws ZipException + */ + public void extractFile(String fileName, + String destPath, UnzipParameters unzipParameters) throws ZipException { + extractFile(fileName, destPath, unzipParameters, null); + } + + /** + * Extracts a specific file from the zip file to the destination path. + * This method first finds the necessary file header from the input file name. + *

+ * File name is relative file name in the zip file. For example if a zip file contains + * a file "a.txt", then to extract this file, input file name has to be "a.txt". Another + * example is if there is a file "b.txt" in a folder "abc" in the zip file, then the + * input file name has to be abc/b.txt + *

+ * If newFileName is not null or empty, newly created file name will be replaced by + * the value in newFileName. If this value is null, then the file name will be the + * value in FileHeader.getFileName + *

+ * Throws an exception if file header could not be found for the given file name or if + * the destination path is invalid + * @param fileName + * @param destPath + * @param unzipParameters + * @param newFileName + * @throws ZipException + */ + public void extractFile(String fileName, String destPath, + UnzipParameters unzipParameters, String newFileName) throws ZipException { + + if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileName)) { + throw new ZipException("file to extract is null or empty, cannot extract file"); + } + + if (!Zip4jUtil.isStringNotNullAndNotEmpty(destPath)) { + throw new ZipException("destination string path is empty or null, cannot extract file"); + } + + readZipInfo(); + + FileHeader fileHeader = Zip4jUtil.getFileHeader(zipModel, fileName); + + if (fileHeader == null) { + throw new ZipException("file header not found for given file name, cannot extract file"); + } + + if (progressMonitor.getState() == ProgressMonitor.STATE_BUSY) { + throw new ZipException("invalid operation - Zip4j is in busy state"); + } + + fileHeader.extractFile(zipModel, destPath, unzipParameters, newFileName, progressMonitor, runInThread); + + } + + /** + * Sets the password for the zip file.
+ * Note: For security reasons, usage of this method is discouraged. Use + * setPassword(char[]) instead. As strings are immutable, they cannot be wiped + * out from memory explicitly after usage. Therefore, usage of Strings to store + * passwords is discouraged. More info here: + * http://docs.oracle.com/javase/1.5.0/docs/guide/security/jce/JCERefGuide.html#PBEEx + * @param password + * @throws ZipException + */ + public void setPassword(String password) throws ZipException { + if (!Zip4jUtil.isStringNotNullAndNotEmpty(password)) { + throw new NullPointerException(); + } + setPassword(password.toCharArray()); + } + + /** + * Sets the password for the zip file + * @param password + * @throws ZipException + */ + public void setPassword(char[] password) throws ZipException { + if (zipModel == null) { + readZipInfo(); + if (zipModel == null) { + throw new ZipException("Zip Model is null"); + } + } + + if (zipModel.getCentralDirectory() == null || zipModel.getCentralDirectory().getFileHeaders() == null) { + throw new ZipException("invalid zip file"); + } + + for (int i = 0; i < zipModel.getCentralDirectory().getFileHeaders().size(); i++) { + if (zipModel.getCentralDirectory().getFileHeaders().get(i) != null) { + if (((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i)).isEncrypted()) { + ((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i)).setPassword(password); + } + } + } + } + + /** + * Returns the list of file headers in the zip file. Throws an exception if the + * zip file does not exist + * @return list of file headers + * @throws ZipException + */ + public List getFileHeaders() throws ZipException { + readZipInfo(); + if (zipModel == null || zipModel.getCentralDirectory() == null) { + return null; + } + return zipModel.getCentralDirectory().getFileHeaders(); + } + + /** + * Returns FileHeader if a file header with the given fileHeader + * string exists in the zip model: If not returns null + * @param fileName + * @return FileHeader + * @throws ZipException + */ + public FileHeader getFileHeader(String fileName) throws ZipException { + if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileName)) { + throw new ZipException("input file name is emtpy or null, cannot get FileHeader"); + } + + readZipInfo(); + if (zipModel == null || zipModel.getCentralDirectory() == null) { + return null; + } + + return Zip4jUtil.getFileHeader(zipModel, fileName); + } + + /** + * Checks to see if the zip file is encrypted + * @return true if encrypted, false if not + * @throws ZipException + */ + public boolean isEncrypted() throws ZipException { + if (zipModel == null) { + readZipInfo(); + if (zipModel == null) { + throw new ZipException("Zip Model is null"); + } + } + + if (zipModel.getCentralDirectory() == null || zipModel.getCentralDirectory().getFileHeaders() == null) { + throw new ZipException("invalid zip file"); + } + + ArrayList fileHeaderList = zipModel.getCentralDirectory().getFileHeaders(); + for (int i = 0; i < fileHeaderList.size(); i++) { + FileHeader fileHeader = (FileHeader)fileHeaderList.get(i); + if (fileHeader != null) { + if (fileHeader.isEncrypted()) { + isEncrypted = true; + break; + } + } + } + + return isEncrypted; + } + + /** + * Checks if the zip file is a split archive + * @return true if split archive, false if not + * @throws ZipException + */ + public boolean isSplitArchive() throws ZipException { + + if (zipModel == null) { + readZipInfo(); + if (zipModel == null) { + throw new ZipException("Zip Model is null"); + } + } + + return zipModel.isSplitArchive(); + + } + + /** + * Removes the file provided in the input paramters from the zip file. + * This method first finds the file header and then removes the file. + * If file does not exist, then this method throws an exception. + * If zip file is a split zip file, then this method throws an exception as + * zip specification does not allow for updating split zip archives. + * @param fileName + * @throws ZipException + */ + public void removeFile(String fileName) throws ZipException { + + if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileName)) { + throw new ZipException("file name is empty or null, cannot remove file"); + } + + if (zipModel == null) { + if (Zip4jUtil.checkFileExists(file)) { + readZipInfo(); + } + } + + if (zipModel.isSplitArchive()) { + throw new ZipException("Zip file format does not allow updating split/spanned files"); + } + + FileHeader fileHeader = Zip4jUtil.getFileHeader(zipModel, fileName); + if (fileHeader == null) { + throw new ZipException("could not find file header for file: " + fileName); + } + + removeFile(fileHeader); + } + + /** + * Removes the file provided in the input file header from the zip file. + * If zip file is a split zip file, then this method throws an exception as + * zip specification does not allow for updating split zip archives. + * @param fileHeader + * @throws ZipException + */ + public void removeFile(FileHeader fileHeader) throws ZipException { + if (fileHeader == null) { + throw new ZipException("file header is null, cannot remove file"); + } + + if (zipModel == null) { + if (Zip4jUtil.checkFileExists(file)) { + readZipInfo(); + } + } + + if (zipModel.isSplitArchive()) { + throw new ZipException("Zip file format does not allow updating split/spanned files"); + } + + ArchiveMaintainer archiveMaintainer = new ArchiveMaintainer(); + archiveMaintainer.initProgressMonitorForRemoveOp(zipModel, fileHeader, progressMonitor); + archiveMaintainer.removeZipFile(zipModel, fileHeader, progressMonitor, runInThread); + } + + /** + * Merges split zip files into a single zip file without the need to extract the + * files in the archive + * @param outputZipFile + * @throws ZipException + */ + public void mergeSplitFiles(File outputZipFile) throws ZipException { + if (outputZipFile == null) { + throw new ZipException("outputZipFile is null, cannot merge split files"); + } + + if (outputZipFile.exists()) { + throw new ZipException("output Zip File already exists"); + } + + checkZipModel(); + + if (this.zipModel == null) { + throw new ZipException("zip model is null, corrupt zip file?"); + } + + ArchiveMaintainer archiveMaintainer = new ArchiveMaintainer(); + archiveMaintainer.initProgressMonitorForMergeOp(zipModel, progressMonitor); + archiveMaintainer.mergeSplitZipFiles(zipModel, outputZipFile, progressMonitor, runInThread); + } + + /** + * Sets comment for the Zip file + * @param comment + * @throws ZipException + */ + public void setComment(String comment) throws ZipException { + if (comment == null) { + throw new ZipException("input comment is null, cannot update zip file"); + } + + if (!Zip4jUtil.checkFileExists(file)) { + throw new ZipException("zip file does not exist, cannot set comment for zip file"); + } + + readZipInfo(); + + if (this.zipModel == null) { + throw new ZipException("zipModel is null, cannot update zip file"); + } + + if (zipModel.getEndCentralDirRecord() == null) { + throw new ZipException("end of central directory is null, cannot set comment"); + } + + ArchiveMaintainer archiveMaintainer = new ArchiveMaintainer(); + archiveMaintainer.setComment(zipModel, comment); + } + + /** + * Returns the comment set for the Zip file + * @return String + * @throws ZipException + */ + public String getComment() throws ZipException { + return getComment(null); + } + + /** + * Returns the comment set for the Zip file in the input encoding + * @param encoding + * @return String + * @throws ZipException + */ + public String getComment(String encoding) throws ZipException { + if (encoding == null) { + if (Zip4jUtil.isSupportedCharset(InternalZipConstants.CHARSET_COMMENTS_DEFAULT)) { + encoding = InternalZipConstants.CHARSET_COMMENTS_DEFAULT; + } else { + encoding = InternalZipConstants.CHARSET_DEFAULT; + } + } + + if (Zip4jUtil.checkFileExists(file)) { + checkZipModel(); + } else { + throw new ZipException("zip file does not exist, cannot read comment"); + } + + if (this.zipModel == null) { + throw new ZipException("zip model is null, cannot read comment"); + } + + if (this.zipModel.getEndCentralDirRecord() == null) { + throw new ZipException("end of central directory record is null, cannot read comment"); + } + + if (this.zipModel.getEndCentralDirRecord().getCommentBytes() == null || + this.zipModel.getEndCentralDirRecord().getCommentBytes().length <= 0) { + return null; + } + + try { + return new String(this.zipModel.getEndCentralDirRecord().getCommentBytes(), encoding); + } catch (UnsupportedEncodingException e) { + throw new ZipException(e); + } + } + + /** + * Loads the zip model if zip model is null and if zip file exists. + * @throws ZipException + */ + private void checkZipModel() throws ZipException { + if (this.zipModel == null) { + if (Zip4jUtil.checkFileExists(file)) { + readZipInfo(); + } else { + createNewZipModel(); + } + } + } + + /** + * Creates a new instance of zip model + * @throws ZipException + */ + private void createNewZipModel() { + zipModel = new ZipModel(); + zipModel.setZipFile(file); + zipModel.setFileNameCharset(fileNameCharset); + } + + /** + * Zip4j will encode all the file names with the input charset. This method throws + * an exception if the Charset is not supported + * @param charsetName + * @throws ZipException + */ + public void setFileNameCharset(String charsetName) throws ZipException { + if (!Zip4jUtil.isStringNotNullAndNotEmpty(charsetName)) { + throw new ZipException("null or empty charset name"); + } + + if (!Zip4jUtil.isSupportedCharset(charsetName)) { + throw new ZipException("unsupported charset: " + charsetName); + } + + this.fileNameCharset = charsetName; + } + + /** + * Returns an input stream for reading the contents of the Zip file corresponding + * to the input FileHeader. Throws an exception if the FileHeader does not exist + * in the ZipFile + * @param fileHeader + * @return ZipInputStream + * @throws ZipException + */ + public ZipInputStream getInputStream(FileHeader fileHeader) throws ZipException { + if (fileHeader == null) { + throw new ZipException("FileHeader is null, cannot get InputStream"); + } + + checkZipModel(); + + if (zipModel == null) { + throw new ZipException("zip model is null, cannot get inputstream"); + } + + Unzip unzip = new Unzip(zipModel); + return unzip.getInputStream(fileHeader); + } + + /** + * Checks to see if the input zip file is a valid zip file. This method + * will try to read zip headers. If headers are read successfully, this + * method returns true else false + * @return boolean + * @since 1.2.3 + */ + public boolean isValidZipFile() { + try { + readZipInfo(); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Returns the full file path+names of all split zip files + * in an ArrayList. For example: If a split zip file(abc.zip) has a 10 split parts + * this method returns an array list with path + "abc.z01", path + "abc.z02", etc. + * Returns null if the zip file does not exist + * @return ArrayList of Strings + * @throws ZipException + */ + public ArrayList getSplitZipFiles() throws ZipException { + checkZipModel(); + return Zip4jUtil.getSplitZipFiles(zipModel); + } + + public ProgressMonitor getProgressMonitor() { + return progressMonitor; + } + + public boolean isRunInThread() { + return runInThread; + } + + public void setRunInThread(boolean runInThread) { + this.runInThread = runInThread; + } + + /** + * Returns the File object of the zip file + * @return File + */ + public File getFile() { + return new File(this.file); + } +} diff --git a/src/net/lingala/zip4j/crypto/AESDecrypter.java b/src/net/lingala/zip4j/crypto/AESDecrypter.java new file mode 100644 index 0000000..f7978a9 --- /dev/null +++ b/src/net/lingala/zip4j/crypto/AESDecrypter.java @@ -0,0 +1,226 @@ +/* +* 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.crypto; + +import java.util.Arrays; + +import net.lingala.zip4j.crypto.PBKDF2.MacBasedPRF; +import net.lingala.zip4j.crypto.PBKDF2.PBKDF2Engine; +import net.lingala.zip4j.crypto.PBKDF2.PBKDF2Parameters; +import net.lingala.zip4j.crypto.engine.AESEngine; +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.exception.ZipExceptionConstants; +import net.lingala.zip4j.model.AESExtraDataRecord; +import net.lingala.zip4j.model.LocalFileHeader; +import net.lingala.zip4j.util.InternalZipConstants; +import net.lingala.zip4j.util.Raw; +import net.lingala.zip4j.util.Zip4jConstants; + +public class AESDecrypter implements IDecrypter { + + private LocalFileHeader localFileHeader; + private AESEngine aesEngine; + private MacBasedPRF mac; + + private final int PASSWORD_VERIFIER_LENGTH = 2; + private int KEY_LENGTH; + private int MAC_LENGTH; + private int SALT_LENGTH; + + private byte[] aesKey; + private byte[] macKey; + private byte[] derivedPasswordVerifier; + private byte[] storedMac; + + private int nonce = 1; + private byte[] iv; + private byte[] counterBlock; + private int loopCount = 0; + + public AESDecrypter(LocalFileHeader localFileHeader, + byte[] salt, byte[] passwordVerifier) throws ZipException { + + if (localFileHeader == null) { + throw new ZipException("one of the input parameters is null in AESDecryptor Constructor"); + } + + this.localFileHeader = localFileHeader; + this.storedMac = null; + iv = new byte[InternalZipConstants.AES_BLOCK_SIZE]; + counterBlock = new byte[InternalZipConstants.AES_BLOCK_SIZE]; + init(salt, passwordVerifier); + } + + private void init(byte[] salt, byte[] passwordVerifier) throws ZipException { + if (localFileHeader == null) { + throw new ZipException("invalid file header in init method of AESDecryptor"); + } + + AESExtraDataRecord aesExtraDataRecord = localFileHeader.getAesExtraDataRecord(); + if (aesExtraDataRecord == null) { + throw new ZipException("invalid aes extra data record - in init method of AESDecryptor"); + } + + switch (aesExtraDataRecord.getAesStrength()) { + case Zip4jConstants.AES_STRENGTH_128: + KEY_LENGTH = 16; + MAC_LENGTH = 16; + SALT_LENGTH = 8; + break; + case Zip4jConstants.AES_STRENGTH_192: + KEY_LENGTH = 24; + MAC_LENGTH = 24; + SALT_LENGTH = 12; + break; + case Zip4jConstants.AES_STRENGTH_256: + KEY_LENGTH = 32; + MAC_LENGTH = 32; + SALT_LENGTH = 16; + break; + default: + throw new ZipException("invalid aes key strength for file: " + localFileHeader.getFileName()); + } + + if (localFileHeader.getPassword() == null || localFileHeader.getPassword().length <= 0) { + throw new ZipException("empty or null password provided for AES Decryptor"); + } + + byte[] derivedKey = deriveKey(salt, localFileHeader.getPassword()); + if (derivedKey == null || + derivedKey.length != (KEY_LENGTH + MAC_LENGTH + PASSWORD_VERIFIER_LENGTH)) { + throw new ZipException("invalid derived key"); + } + + aesKey = new byte[KEY_LENGTH]; + macKey = new byte[MAC_LENGTH]; + derivedPasswordVerifier = new byte[PASSWORD_VERIFIER_LENGTH]; + + System.arraycopy(derivedKey, 0, aesKey, 0, KEY_LENGTH); + System.arraycopy(derivedKey, KEY_LENGTH, macKey, 0, MAC_LENGTH); + System.arraycopy(derivedKey, KEY_LENGTH + MAC_LENGTH, derivedPasswordVerifier, 0, PASSWORD_VERIFIER_LENGTH); + + if (derivedPasswordVerifier == null) { + throw new ZipException("invalid derived password verifier for AES"); + } + + if (!Arrays.equals(passwordVerifier, derivedPasswordVerifier)) { + throw new ZipException("Wrong Password for file: " + localFileHeader.getFileName(), ZipExceptionConstants.WRONG_PASSWORD); + } + + aesEngine = new AESEngine(aesKey); + mac = new MacBasedPRF("HmacSHA1"); + mac.init(macKey); + } + + public int decryptData(byte[] buff, int start, int len) throws ZipException { + + if (aesEngine == null) { + throw new ZipException("AES not initialized properly"); + } + + try { + + for (int j = start; j < (start + len); j += InternalZipConstants.AES_BLOCK_SIZE) { + loopCount = (j + InternalZipConstants.AES_BLOCK_SIZE <= (start + len)) ? + InternalZipConstants.AES_BLOCK_SIZE : ((start + len) - j); + + mac.update(buff, j, loopCount); + Raw.prepareBuffAESIVBytes(iv, nonce, InternalZipConstants.AES_BLOCK_SIZE); + aesEngine.processBlock(iv, counterBlock); + + for (int k = 0; k < loopCount; k++) { + buff[j + k] = (byte)(buff[j + k] ^ counterBlock[k]); + } + + nonce++; + } + + return len; + + } catch (ZipException e) { + throw e; + } catch (Exception e) { + throw new ZipException(e); + } + } + + public int decryptData(byte[] buff) throws ZipException { + return decryptData(buff, 0, buff.length); + } + + private byte[] deriveKey(byte[] salt, char[] password) throws ZipException { + try { + PBKDF2Parameters p = new PBKDF2Parameters("HmacSHA1", "ISO-8859-1", + salt, 1000); + PBKDF2Engine e = new PBKDF2Engine(p); + byte[] derivedKey = e.deriveKey(password, KEY_LENGTH + MAC_LENGTH + PASSWORD_VERIFIER_LENGTH); + return derivedKey; + } catch (Exception e) { + throw new ZipException(e); + } + } + + public int getPasswordVerifierLength() { + return PASSWORD_VERIFIER_LENGTH; + } + + public int getSaltLength() { + return SALT_LENGTH; + } + + public byte[] getCalculatedAuthenticationBytes() { + return mac.doFinal(); + } + + public void setStoredMac(byte[] storedMac) { + this.storedMac = storedMac; + } + + public byte[] getStoredMac() { + return storedMac; + } + +// public byte[] getStoredMac() throws ZipException { +// if (raf == null) { +// throw new ZipException("attempting to read MAC on closed file handle"); +// } +// +// try { +// byte[] storedMacBytes = new byte[InternalZipConstants.AES_AUTH_LENGTH]; +// int bytesRead = raf.read(storedMacBytes); +// if (bytesRead != InternalZipConstants.AES_AUTH_LENGTH) { +// if (zipModel.isSplitArchive()) { +//// unzipEngine.startNextSplitFile(); +// if (bytesRead == -1) bytesRead = 0; +// int newlyRead = raf.read(storedMacBytes, bytesRead, InternalZipConstants.AES_AUTH_LENGTH - bytesRead); +// bytesRead += newlyRead; +// if (bytesRead != InternalZipConstants.AES_AUTH_LENGTH) { +// throw new ZipException("invalid number of bytes read for stored MAC after starting split file"); +// } +// } else { +// throw new ZipException("invalid number of bytes read for stored MAC"); +// } +// } +// return storedMacBytes; +// } catch (IOException e) { +// throw new ZipException(e); +// } catch (Exception e) { +// throw new ZipException(e); +// } +// +// } +} diff --git a/src/net/lingala/zip4j/crypto/AESEncrpyter.java b/src/net/lingala/zip4j/crypto/AESEncrpyter.java new file mode 100644 index 0000000..e781c8e --- /dev/null +++ b/src/net/lingala/zip4j/crypto/AESEncrpyter.java @@ -0,0 +1,214 @@ +/* +* 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.crypto; + +import java.util.Random; + +import net.lingala.zip4j.crypto.PBKDF2.MacBasedPRF; +import net.lingala.zip4j.crypto.PBKDF2.PBKDF2Engine; +import net.lingala.zip4j.crypto.PBKDF2.PBKDF2Parameters; +import net.lingala.zip4j.crypto.engine.AESEngine; +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.util.InternalZipConstants; +import net.lingala.zip4j.util.Raw; +import net.lingala.zip4j.util.Zip4jConstants; + +public class AESEncrpyter implements IEncrypter { + + private char[] password; + private int keyStrength; + private AESEngine aesEngine; + private MacBasedPRF mac; + + private int KEY_LENGTH; + private int MAC_LENGTH; + private int SALT_LENGTH; + private final int PASSWORD_VERIFIER_LENGTH = 2; + + private byte[] aesKey; + private byte[] macKey; + private byte[] derivedPasswordVerifier; + private byte[] saltBytes; + + private boolean finished; + + private int nonce = 1; + private int loopCount = 0; + + private byte[] iv; + private byte[] counterBlock; + + public AESEncrpyter(char[] password, int keyStrength) throws ZipException { + if (password == null || password.length == 0) { + throw new ZipException("input password is empty or null in AES encrypter constructor"); + } + if (keyStrength != Zip4jConstants.AES_STRENGTH_128 && + keyStrength != Zip4jConstants.AES_STRENGTH_256) { + throw new ZipException("Invalid key strength in AES encrypter constructor"); + } + + this.password = password; + this.keyStrength = keyStrength; + this.finished = false; + counterBlock = new byte[InternalZipConstants.AES_BLOCK_SIZE]; + iv = new byte[InternalZipConstants.AES_BLOCK_SIZE]; + init(); + } + + private void init() throws ZipException { + switch (keyStrength) { + case Zip4jConstants.AES_STRENGTH_128: + KEY_LENGTH = 16; + MAC_LENGTH = 16; + SALT_LENGTH = 8; + break; + case Zip4jConstants.AES_STRENGTH_256: + KEY_LENGTH = 32; + MAC_LENGTH = 32; + SALT_LENGTH = 16; + break; + default: + throw new ZipException("invalid aes key strength, cannot determine key sizes"); + } + + saltBytes = generateSalt(SALT_LENGTH); + byte[] keyBytes = deriveKey(saltBytes, password); + + if (keyBytes == null || keyBytes.length != (KEY_LENGTH + MAC_LENGTH + PASSWORD_VERIFIER_LENGTH)) { + throw new ZipException("invalid key generated, cannot decrypt file"); + } + + aesKey = new byte[KEY_LENGTH]; + macKey = new byte[MAC_LENGTH]; + derivedPasswordVerifier = new byte[PASSWORD_VERIFIER_LENGTH]; + + System.arraycopy(keyBytes, 0, aesKey, 0, KEY_LENGTH); + System.arraycopy(keyBytes, KEY_LENGTH, macKey, 0, MAC_LENGTH); + System.arraycopy(keyBytes, KEY_LENGTH + MAC_LENGTH, derivedPasswordVerifier, 0, PASSWORD_VERIFIER_LENGTH); + + aesEngine = new AESEngine(aesKey); + mac = new MacBasedPRF("HmacSHA1"); + mac.init(macKey); + } + + private byte[] deriveKey(byte[] salt, char[] password) throws ZipException { + try { + PBKDF2Parameters p = new PBKDF2Parameters("HmacSHA1", "ISO-8859-1", + salt, 1000); + PBKDF2Engine e = new PBKDF2Engine(p); + byte[] derivedKey = e.deriveKey(password, KEY_LENGTH + MAC_LENGTH + PASSWORD_VERIFIER_LENGTH); + return derivedKey; + } catch (Exception e) { + throw new ZipException(e); + } + } + + public int encryptData(byte[] buff) throws ZipException { + + if (buff == null) { + throw new ZipException("input bytes are null, cannot perform AES encrpytion"); + } + return encryptData(buff, 0, buff.length); + } + + public int encryptData(byte[] buff, int start, int len) throws ZipException { + + if (finished) { + // A non 16 byte block has already been passed to encrypter + // non 16 byte block should be the last block of compressed data in AES encryption + // any more encryption will lead to corruption of data + throw new ZipException("AES Encrypter is in finished state (A non 16 byte block has already been passed to encrypter)"); + } + + if (len%16!=0) { + this.finished = true; + } + + for (int j = start; j < (start + len); j += InternalZipConstants.AES_BLOCK_SIZE) { + loopCount = (j + InternalZipConstants.AES_BLOCK_SIZE <= (start + len)) ? + InternalZipConstants.AES_BLOCK_SIZE : ((start + len) - j); + + Raw.prepareBuffAESIVBytes(iv, nonce, InternalZipConstants.AES_BLOCK_SIZE); + aesEngine.processBlock(iv, counterBlock); + + for (int k = 0; k < loopCount; k++) { + buff[j + k] = (byte)(buff[j + k] ^ counterBlock[k]); + } + + mac.update(buff, j, loopCount); + nonce++; + } + + return len; + } + + private static byte[] generateSalt(int size) throws ZipException { + + if (size != 8 && size != 16) { + throw new ZipException("invalid salt size, cannot generate salt"); + } + + int rounds = 0; + + if (size == 8) + rounds = 2; + if (size == 16) + rounds = 4; + + byte[] salt = new byte[size]; + for( int j = 0; j < rounds; j++ ) { + Random rand = new Random(); + int i = rand.nextInt(); + salt[0+j*4] = (byte)(i>>24); + salt[1+j*4] = (byte)(i>>16); + salt[2+j*4] = (byte)(i>>8); + salt[3+j*4] = (byte)i; + } + return salt; + } + + public byte[] getFinalMac() { + byte[] rawMacBytes = mac.doFinal(); + byte[] macBytes = new byte[10]; + System.arraycopy(rawMacBytes, 0, macBytes, 0, 10); + return macBytes; + } + + public byte[] getDerivedPasswordVerifier() { + return derivedPasswordVerifier; + } + + public void setDerivedPasswordVerifier(byte[] derivedPasswordVerifier) { + this.derivedPasswordVerifier = derivedPasswordVerifier; + } + + public byte[] getSaltBytes() { + return saltBytes; + } + + public void setSaltBytes(byte[] saltBytes) { + this.saltBytes = saltBytes; + } + + public int getSaltLength() { + return SALT_LENGTH; + } + + public int getPasswordVeriifierLength() { + return PASSWORD_VERIFIER_LENGTH; + } +} diff --git a/src/net/lingala/zip4j/crypto/IDecrypter.java b/src/net/lingala/zip4j/crypto/IDecrypter.java new file mode 100644 index 0000000..5df8db3 --- /dev/null +++ b/src/net/lingala/zip4j/crypto/IDecrypter.java @@ -0,0 +1,27 @@ +/* +* 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.crypto; + +import net.lingala.zip4j.exception.ZipException; + +public interface IDecrypter { + + public int decryptData(byte[] buff, int start, int len) throws ZipException; + + public int decryptData(byte[] buff) throws ZipException; + +} diff --git a/src/net/lingala/zip4j/crypto/IEncrypter.java b/src/net/lingala/zip4j/crypto/IEncrypter.java new file mode 100644 index 0000000..23a2377 --- /dev/null +++ b/src/net/lingala/zip4j/crypto/IEncrypter.java @@ -0,0 +1,27 @@ +/* +* 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.crypto; + +import net.lingala.zip4j.exception.ZipException; + +public interface IEncrypter { + + public int encryptData(byte[] buff) throws ZipException; + + public int encryptData(byte[] buff, int start, int len) throws ZipException; + +} diff --git a/src/net/lingala/zip4j/crypto/PBKDF2/BinTools.java b/src/net/lingala/zip4j/crypto/PBKDF2/BinTools.java new file mode 100644 index 0000000..039e2cc --- /dev/null +++ b/src/net/lingala/zip4j/crypto/PBKDF2/BinTools.java @@ -0,0 +1,85 @@ +/* +* 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.crypto.PBKDF2; + +/* + * Source referred from Matthias Gartner's PKCS#5 implementation - + * see http://rtner.de/software/PBKDF2.html + */ + +class BinTools +{ + public static final String hex = "0123456789ABCDEF"; + + public static String bin2hex(final byte[] b) + { + if (b == null) + { + return ""; + } + StringBuffer sb = new StringBuffer(2 * b.length); + for (int i = 0; i < b.length; i++) + { + int v = (256 + b[i]) % 256; + sb.append(hex.charAt((v / 16) & 15)); + sb.append(hex.charAt((v % 16) & 15)); + } + return sb.toString(); + } + + public static byte[] hex2bin(final String s) + { + String m = s; + if (s == null) + { + // Allow empty input string. + m = ""; + } + else if (s.length() % 2 != 0) + { + // Assume leading zero for odd string length + m = "0" + s; + } + byte r[] = new byte[m.length() / 2]; + for (int i = 0, n = 0; i < m.length(); n++) + { + char h = m.charAt(i++); + char l = m.charAt(i++); + r[n] = (byte) (hex2bin(h) * 16 + hex2bin(l)); + } + return r; + } + + public static int hex2bin(char c) + { + if (c >= '0' && c <= '9') + { + return (c - '0'); + } + if (c >= 'A' && c <= 'F') + { + return (c - 'A' + 10); + } + if (c >= 'a' && c <= 'f') + { + return (c - 'a' + 10); + } + throw new IllegalArgumentException( + "Input string may only contain hex digits, but found '" + c + + "'"); + } +} diff --git a/src/net/lingala/zip4j/crypto/PBKDF2/MacBasedPRF.java b/src/net/lingala/zip4j/crypto/PBKDF2/MacBasedPRF.java new file mode 100644 index 0000000..8b67715 --- /dev/null +++ b/src/net/lingala/zip4j/crypto/PBKDF2/MacBasedPRF.java @@ -0,0 +1,116 @@ +/* +* 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.crypto.PBKDF2; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +/* + * Source referred from Matthias Gartner's PKCS#5 implementation - + * see http://rtner.de/software/PBKDF2.html + */ + +public class MacBasedPRF implements PRF +{ + protected Mac mac; + + protected int hLen; + + protected String macAlgorithm; + + public MacBasedPRF(String macAlgorithm) + { + this.macAlgorithm = macAlgorithm; + try + { + mac = Mac.getInstance(macAlgorithm); + hLen = mac.getMacLength(); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(e); + } + } + + public MacBasedPRF(String macAlgorithm, String provider) + { + this.macAlgorithm = macAlgorithm; + try + { + mac = Mac.getInstance(macAlgorithm, provider); + hLen = mac.getMacLength(); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(e); + } + catch (NoSuchProviderException e) + { + throw new RuntimeException(e); + } + } + + public byte[] doFinal(byte[] M) + { + byte[] r = mac.doFinal(M); + return r; + } + + public byte[] doFinal() { + byte[] r = mac.doFinal(); + return r; + } + + public int getHLen() + { + return hLen; + } + + public void init(byte[] P) + { + try + { + mac.init(new SecretKeySpec(P, macAlgorithm)); + } + catch (InvalidKeyException e) + { + throw new RuntimeException(e); + } + } + + public void update(byte[] U) { + + try { + mac.update(U); + } catch (IllegalStateException e) { + throw new RuntimeException(e); + } + + } + + public void update (byte[] U, int start, int len) { + try { + mac.update(U, start, len); + } catch (IllegalStateException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/net/lingala/zip4j/crypto/PBKDF2/PBKDF2Engine.java b/src/net/lingala/zip4j/crypto/PBKDF2/PBKDF2Engine.java new file mode 100644 index 0000000..8db81b5 --- /dev/null +++ b/src/net/lingala/zip4j/crypto/PBKDF2/PBKDF2Engine.java @@ -0,0 +1,198 @@ +/* +* 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.crypto.PBKDF2; + +import net.lingala.zip4j.util.Raw; + +/* + * Source referred from Matthias Gartner's PKCS#5 implementation - + * see http://rtner.de/software/PBKDF2.html + */ + +public class PBKDF2Engine +{ + protected PBKDF2Parameters parameters; + + protected PRF prf; + + public PBKDF2Engine() + { + this.parameters = null; + prf = null; + } + + public PBKDF2Engine(PBKDF2Parameters parameters) + { + this.parameters = parameters; + prf = null; + } + + public PBKDF2Engine(PBKDF2Parameters parameters, PRF prf) + { + this.parameters = parameters; + this.prf = prf; + } + + public byte[] deriveKey(char[] inputPassword) + { + return deriveKey(inputPassword, 0); + } + + public byte[] deriveKey(char[] inputPassword, int dkLen) + { + byte[] r = null; + byte P[] = null; + if (inputPassword == null) + { + throw new NullPointerException(); + } + + P = Raw.convertCharArrayToByteArray(inputPassword); + + assertPRF(P); + if (dkLen == 0) + { + dkLen = prf.getHLen(); + } + r = PBKDF2(prf, parameters.getSalt(), parameters.getIterationCount(), + dkLen); + return r; + } + + public boolean verifyKey(char[] inputPassword) + { + byte[] referenceKey = getParameters().getDerivedKey(); + if (referenceKey == null || referenceKey.length == 0) + { + return false; + } + byte[] inputKey = deriveKey(inputPassword, referenceKey.length); + + if (inputKey == null || inputKey.length != referenceKey.length) + { + return false; + } + for (int i = 0; i < inputKey.length; i++) + { + if (inputKey[i] != referenceKey[i]) + { + return false; + } + } + return true; + } + + protected void assertPRF(byte[] P) + { + if (prf == null) + { + prf = new MacBasedPRF(parameters.getHashAlgorithm()); + } + prf.init(P); + } + + public PRF getPseudoRandomFunction() + { + return prf; + } + + protected byte[] PBKDF2(PRF prf, byte[] S, int c, int dkLen) + { + if (S == null) + { + S = new byte[0]; + } + int hLen = prf.getHLen(); + int l = ceil(dkLen, hLen); + int r = dkLen - (l - 1) * hLen; + byte T[] = new byte[l * hLen]; + int ti_offset = 0; + for (int i = 1; i <= l; i++) + { + _F(T, ti_offset, prf, S, c, i); + ti_offset += hLen; + } + if (r < hLen) + { + // Incomplete last block + byte DK[] = new byte[dkLen]; + System.arraycopy(T, 0, DK, 0, dkLen); + return DK; + } + return T; + } + + protected int ceil(int a, int b) + { + int m = 0; + if (a % b > 0) + { + m = 1; + } + return a / b + m; + } + + protected void _F(byte[] dest, int offset, PRF prf, byte[] S, int c, + int blockIndex) + { + int hLen = prf.getHLen(); + byte U_r[] = new byte[hLen]; + + // U0 = S || INT (i); + byte U_i[] = new byte[S.length + 4]; + System.arraycopy(S, 0, U_i, 0, S.length); + INT(U_i, S.length, blockIndex); + + for (int i = 0; i < c; i++) + { + U_i = prf.doFinal(U_i); + xor(U_r, U_i); + } + System.arraycopy(U_r, 0, dest, offset, hLen); + } + + protected void xor(byte[] dest, byte[] src) + { + for (int i = 0; i < dest.length; i++) + { + dest[i] ^= src[i]; + } + } + + protected void INT(byte[] dest, int offset, int i) + { + dest[offset + 0] = (byte) (i / (256 * 256 * 256)); + dest[offset + 1] = (byte) (i / (256 * 256)); + dest[offset + 2] = (byte) (i / (256)); + dest[offset + 3] = (byte) (i); + } + + public PBKDF2Parameters getParameters() + { + return parameters; + } + + public void setParameters(PBKDF2Parameters parameters) + { + this.parameters = parameters; + } + + public void setPseudoRandomFunction(PRF prf) + { + this.prf = prf; + } +} diff --git a/src/net/lingala/zip4j/crypto/PBKDF2/PBKDF2HexFormatter.java b/src/net/lingala/zip4j/crypto/PBKDF2/PBKDF2HexFormatter.java new file mode 100644 index 0000000..4cea2ed --- /dev/null +++ b/src/net/lingala/zip4j/crypto/PBKDF2/PBKDF2HexFormatter.java @@ -0,0 +1,56 @@ +/* +* 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.crypto.PBKDF2; + +/* + * Source referred from Matthias Gartner's PKCS#5 implementation - + * see http://rtner.de/software/PBKDF2.html + */ + +class PBKDF2HexFormatter +{ + public boolean fromString(PBKDF2Parameters p, String s) + { + if (p == null || s == null) + { + return true; + } + + String[] p123 = s.split(":"); + if (p123 == null || p123.length != 3) + { + return true; + } + + byte salt[] = BinTools.hex2bin(p123[0]); + int iterationCount = Integer.parseInt(p123[1]); + byte bDK[] = BinTools.hex2bin(p123[2]); + + p.setSalt(salt); + p.setIterationCount(iterationCount); + p.setDerivedKey(bDK); + return false; + } + + public String toString(PBKDF2Parameters p) + { + String s = BinTools.bin2hex(p.getSalt()) + ":" + + String.valueOf(p.getIterationCount()) + ":" + + BinTools.bin2hex(p.getDerivedKey()); + return s; + } +} diff --git a/src/net/lingala/zip4j/crypto/PBKDF2/PBKDF2Parameters.java b/src/net/lingala/zip4j/crypto/PBKDF2/PBKDF2Parameters.java new file mode 100644 index 0000000..8462cad --- /dev/null +++ b/src/net/lingala/zip4j/crypto/PBKDF2/PBKDF2Parameters.java @@ -0,0 +1,112 @@ +/* +* 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.crypto.PBKDF2; +/* + * Source referred from Matthias Gartner's PKCS#5 implementation - + * see http://rtner.de/software/PBKDF2.html + */ +public class PBKDF2Parameters +{ + protected byte[] salt; + + protected int iterationCount; + + protected String hashAlgorithm; + + protected String hashCharset; + + protected byte[] derivedKey; + + public PBKDF2Parameters() + { + this.hashAlgorithm = null; + this.hashCharset = "UTF-8"; + this.salt = null; + this.iterationCount = 1000; + this.derivedKey = null; + } + + public PBKDF2Parameters(String hashAlgorithm, String hashCharset, + byte[] salt, int iterationCount) + { + this.hashAlgorithm = hashAlgorithm; + this.hashCharset = hashCharset; + this.salt = salt; + this.iterationCount = iterationCount; + this.derivedKey = null; + } + + public PBKDF2Parameters(String hashAlgorithm, String hashCharset, + byte[] salt, int iterationCount, byte[] derivedKey) + { + this.hashAlgorithm = hashAlgorithm; + this.hashCharset = hashCharset; + this.salt = salt; + this.iterationCount = iterationCount; + this.derivedKey = derivedKey; + } + + public int getIterationCount() + { + return iterationCount; + } + + public void setIterationCount(int iterationCount) + { + this.iterationCount = iterationCount; + } + + public byte[] getSalt() + { + return salt; + } + + public void setSalt(byte[] salt) + { + this.salt = salt; + } + + public byte[] getDerivedKey() + { + return derivedKey; + } + + public void setDerivedKey(byte[] derivedKey) + { + this.derivedKey = derivedKey; + } + + public String getHashAlgorithm() + { + return hashAlgorithm; + } + + public void setHashAlgorithm(String hashAlgorithm) + { + this.hashAlgorithm = hashAlgorithm; + } + + public String getHashCharset() + { + return hashCharset; + } + + public void setHashCharset(String hashCharset) + { + this.hashCharset = hashCharset; + } +} diff --git a/src/net/lingala/zip4j/crypto/PBKDF2/PRF.java b/src/net/lingala/zip4j/crypto/PBKDF2/PRF.java new file mode 100644 index 0000000..c6acc55 --- /dev/null +++ b/src/net/lingala/zip4j/crypto/PBKDF2/PRF.java @@ -0,0 +1,31 @@ +/* +* 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.crypto.PBKDF2; + +/* + * Source referred from Matthias Gartner's PKCS#5 implementation - + * see http://rtner.de/software/PBKDF2.html + */ + +interface PRF +{ + public void init(byte[] P); + + public byte[] doFinal(byte[] M); + + public int getHLen(); +} diff --git a/src/net/lingala/zip4j/crypto/StandardDecrypter.java b/src/net/lingala/zip4j/crypto/StandardDecrypter.java new file mode 100644 index 0000000..f038e6b --- /dev/null +++ b/src/net/lingala/zip4j/crypto/StandardDecrypter.java @@ -0,0 +1,97 @@ +/* +* 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.crypto; + +import net.lingala.zip4j.crypto.engine.ZipCryptoEngine; +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.exception.ZipExceptionConstants; +import net.lingala.zip4j.model.FileHeader; +import net.lingala.zip4j.util.InternalZipConstants; + +public class StandardDecrypter implements IDecrypter { + + private FileHeader fileHeader; + private byte[] crc = new byte[4]; + private ZipCryptoEngine zipCryptoEngine; + + public StandardDecrypter(FileHeader fileHeader, byte[] headerBytes) throws ZipException{ + if (fileHeader == null) { + throw new ZipException("one of more of the input parameters were null in StandardDecryptor"); + } + + this.fileHeader = fileHeader; + this.zipCryptoEngine = new ZipCryptoEngine(); + init(headerBytes); + } + + public int decryptData(byte[] buff) throws ZipException { + return decryptData(buff, 0, buff.length); + } + + public int decryptData(byte[] buff, int start, int len) throws ZipException { + if (start < 0 || len < 0) { + throw new ZipException("one of the input parameters were null in standard decrpyt data"); + } + + try { + for (int i = start; i < start + len; i++) { + int val = buff[i] & 0xff; + val = (val ^ zipCryptoEngine.decryptByte()) & 0xff; + zipCryptoEngine.updateKeys((byte) val); + buff[i] = (byte)val; + } + return len; + } catch (Exception e) { + throw new ZipException(e); + } + } + + public void init(byte[] headerBytes) throws ZipException { + byte[] crcBuff = fileHeader.getCrcBuff(); + crc[3] = (byte) (crcBuff[3] & 0xFF); + crc[2] = (byte) ((crcBuff[3] >> 8) & 0xFF); + crc[1] = (byte) ((crcBuff[3] >> 16) & 0xFF); + crc[0] = (byte) ((crcBuff[3] >> 24) & 0xFF); + + if(crc[2] > 0 || crc[1] > 0 || crc[0] > 0) + throw new IllegalStateException("Invalid CRC in File Header"); + + if (fileHeader.getPassword() == null || fileHeader.getPassword().length <= 0) { + throw new ZipException("Wrong password!", ZipExceptionConstants.WRONG_PASSWORD); + } + + zipCryptoEngine.initKeys(fileHeader.getPassword()); + + try { + int result = headerBytes[0]; + for (int i = 0; i < InternalZipConstants.STD_DEC_HDR_SIZE; i++) { +// Commented this as this check cannot always be trusted +// New functionality: If there is an error in extracting a password protected file, +// "Wrong Password?" text is appended to the exception message +// if(i+1 == InternalZipConstants.STD_DEC_HDR_SIZE && ((byte)(result ^ zipCryptoEngine.decryptByte()) != crc[3]) && !isSplit) +// throw new ZipException("Wrong password!", ZipExceptionConstants.WRONG_PASSWORD); + + zipCryptoEngine.updateKeys((byte) (result ^ zipCryptoEngine.decryptByte())); + if (i+1 != InternalZipConstants.STD_DEC_HDR_SIZE) + result = headerBytes[i+1]; + } + } catch (Exception e) { + throw new ZipException(e); + } + } + +} diff --git a/src/net/lingala/zip4j/crypto/StandardEncrypter.java b/src/net/lingala/zip4j/crypto/StandardEncrypter.java new file mode 100644 index 0000000..6ea3f5f --- /dev/null +++ b/src/net/lingala/zip4j/crypto/StandardEncrypter.java @@ -0,0 +1,133 @@ +/* +* 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.crypto; + +import java.util.Random; + +import net.lingala.zip4j.crypto.engine.ZipCryptoEngine; +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.util.InternalZipConstants; + +public class StandardEncrypter implements IEncrypter { + + private ZipCryptoEngine zipCryptoEngine; + private byte[] headerBytes; + + public StandardEncrypter(char[] password, int crc) throws ZipException { + if (password == null || password.length <= 0) { + throw new ZipException("input password is null or empty in standard encrpyter constructor"); + } + + this.zipCryptoEngine = new ZipCryptoEngine(); + + this.headerBytes = new byte[InternalZipConstants.STD_DEC_HDR_SIZE]; + init(password, crc); + } + + private void init(char[] password, int crc) throws ZipException { + if (password == null || password.length <= 0) { + throw new ZipException("input password is null or empty, cannot initialize standard encrypter"); + } + zipCryptoEngine.initKeys(password); + headerBytes = generateRandomBytes(InternalZipConstants.STD_DEC_HDR_SIZE); + // Initialize again since the generated bytes were encrypted. + zipCryptoEngine.initKeys(password); + + headerBytes[InternalZipConstants.STD_DEC_HDR_SIZE - 1] = (byte)((crc >>> 24)); + headerBytes[InternalZipConstants.STD_DEC_HDR_SIZE - 2] = (byte)((crc >>> 16)); + + if (headerBytes.length < InternalZipConstants.STD_DEC_HDR_SIZE) { + throw new ZipException("invalid header bytes generated, cannot perform standard encryption"); + } + + encryptData(headerBytes); + } + + public int encryptData(byte[] buff) throws ZipException { + if (buff == null) { + throw new NullPointerException(); + } + return encryptData(buff, 0, buff.length); + } + + public int encryptData(byte[] buff, int start, int len) throws ZipException { + + if (len < 0) { + throw new ZipException("invalid length specified to decrpyt data"); + } + + try { + for (int i = start; i < start + len; i++) { + buff[i] = encryptByte(buff[i]); + } + return len; + } catch (Exception e) { + throw new ZipException(e); + } + } + + protected byte encryptByte(byte val) { + byte temp_val = (byte) (val ^ zipCryptoEngine.decryptByte() & 0xff); + zipCryptoEngine.updateKeys(val); + return temp_val; + } + + protected byte[] generateRandomBytes(int size) throws ZipException { + + if (size <= 0) { + throw new ZipException("size is either 0 or less than 0, cannot generate header for standard encryptor"); + } + + byte[] buff = new byte[size]; + + Random rand = new Random(); + + for (int i = 0; i < buff.length; i ++) { + // Encrypted to get less predictability for poorly implemented + // rand functions. + buff[i] = encryptByte((byte) rand.nextInt(256)); + } + +// buff[0] = (byte)87; +// buff[1] = (byte)176; +// buff[2] = (byte)-49; +// buff[3] = (byte)-43; +// buff[4] = (byte)93; +// buff[5] = (byte)-204; +// buff[6] = (byte)-105; +// buff[7] = (byte)213; +// buff[8] = (byte)-80; +// buff[9] = (byte)-8; +// buff[10] = (byte)21; +// buff[11] = (byte)242; + +// for( int j=0; j<2; j++ ) { +// Random rand = new Random(); +// int i = rand.nextInt(); +// buff[0+j*4] = (byte)(i>>24); +// buff[1+j*4] = (byte)(i>>16); +// buff[2+j*4] = (byte)(i>>8); +// buff[3+j*4] = (byte)i; +// } + return buff; + } + + public byte[] getHeaderBytes() { + return headerBytes; + } + +} diff --git a/src/net/lingala/zip4j/crypto/engine/AESEngine.java b/src/net/lingala/zip4j/crypto/engine/AESEngine.java new file mode 100644 index 0000000..dc66d79 --- /dev/null +++ b/src/net/lingala/zip4j/crypto/engine/AESEngine.java @@ -0,0 +1,291 @@ +/* +* 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.crypto.engine; + +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.util.InternalZipConstants; + +/** + * Core Engine for AES Encryption + * @author Srikanth Reddy Lingala + * + */ +public class AESEngine { + + private int rounds; + private int[][] workingKey = null; + private int C0, C1, C2, C3; + + public AESEngine(byte[] key) throws ZipException { + init(key); + } + + public void init(byte[] key) throws ZipException { + workingKey = generateWorkingKey(key); + } + + private int[][] generateWorkingKey(byte[] key) throws ZipException { + int kc = key.length / 4; + int t; + + if (((kc != 4) && (kc != 6) && (kc != 8)) || ((kc * 4) != key.length)) + { + throw new ZipException("invalid key length (not 128/192/256)"); + } + + rounds = kc + 6; + int[][] W = new int[rounds+1][4]; + + t = 0; + int i = 0; + while (i < key.length) + { + W[t >> 2][t & 3] = (key[i]&0xff) | ((key[i+1]&0xff) << 8) | ((key[i+2]&0xff) << 16) | (key[i+3] << 24); + i+=4; + t++; + } + + int k = (rounds + 1) << 2; + for (i = kc; (i < k); i++) + { + int temp = W[(i-1)>>2][(i-1)&3]; + if ((i % kc) == 0) + { + temp = subWord(shift(temp, 8)) ^ rcon[(i / kc)-1]; + } + else if ((kc > 6) && ((i % kc) == 4)) + { + temp = subWord(temp); + } + + W[i>>2][i&3] = W[(i - kc)>>2][(i-kc)&3] ^ temp; + } + return W; + } + + public int processBlock(byte[] in, byte[] out) throws ZipException { + return processBlock(in, 0, out, 0); + } + + public int processBlock(byte[] in, int inOff, byte[] out, int outOff) throws ZipException { + if (workingKey == null) + { + throw new ZipException("AES engine not initialised"); + } + + if ((inOff + (32 / 2)) > in.length) + { + throw new ZipException("input buffer too short"); + } + + if ((outOff + (32 / 2)) > out.length) + { + throw new ZipException("output buffer too short"); + } + + stateIn(in, inOff); + encryptBlock(workingKey); + stateOut(out, outOff); + + return InternalZipConstants.AES_BLOCK_SIZE; + } + + private final void stateIn(byte[] bytes, int off) { + int index = off; + + C0 = (bytes[index++] & 0xff); + C0 |= (bytes[index++] & 0xff) << 8; + C0 |= (bytes[index++] & 0xff) << 16; + C0 |= bytes[index++] << 24; + + C1 = (bytes[index++] & 0xff); + C1 |= (bytes[index++] & 0xff) << 8; + C1 |= (bytes[index++] & 0xff) << 16; + C1 |= bytes[index++] << 24; + + C2 = (bytes[index++] & 0xff); + C2 |= (bytes[index++] & 0xff) << 8; + C2 |= (bytes[index++] & 0xff) << 16; + C2 |= bytes[index++] << 24; + + C3 = (bytes[index++] & 0xff); + C3 |= (bytes[index++] & 0xff) << 8; + C3 |= (bytes[index++] & 0xff) << 16; + C3 |= bytes[index++] << 24; + } + + private final void stateOut(byte[] bytes, int off) { + int index = off; + + bytes[index++] = (byte)C0; + bytes[index++] = (byte)(C0 >> 8); + bytes[index++] = (byte)(C0 >> 16); + bytes[index++] = (byte)(C0 >> 24); + + bytes[index++] = (byte)C1; + bytes[index++] = (byte)(C1 >> 8); + bytes[index++] = (byte)(C1 >> 16); + bytes[index++] = (byte)(C1 >> 24); + + bytes[index++] = (byte)C2; + bytes[index++] = (byte)(C2 >> 8); + bytes[index++] = (byte)(C2 >> 16); + bytes[index++] = (byte)(C2 >> 24); + + bytes[index++] = (byte)C3; + bytes[index++] = (byte)(C3 >> 8); + bytes[index++] = (byte)(C3 >> 16); + bytes[index++] = (byte)(C3 >> 24); + } + + private final void encryptBlock(int[][] KW) { + int r, r0, r1, r2, r3; + + C0 ^= KW[0][0]; + C1 ^= KW[0][1]; + C2 ^= KW[0][2]; + C3 ^= KW[0][3]; + + r = 1; + + while (r < rounds - 1) + { + r0 = T0[C0&255] ^ shift(T0[(C1>>8)&255], 24) ^ shift(T0[(C2>>16)&255],16) ^ shift(T0[(C3>>24)&255],8) ^ KW[r][0]; + r1 = T0[C1&255] ^ shift(T0[(C2>>8)&255], 24) ^ shift(T0[(C3>>16)&255], 16) ^ shift(T0[(C0>>24)&255], 8) ^ KW[r][1]; + r2 = T0[C2&255] ^ shift(T0[(C3>>8)&255], 24) ^ shift(T0[(C0>>16)&255], 16) ^ shift(T0[(C1>>24)&255], 8) ^ KW[r][2]; + r3 = T0[C3&255] ^ shift(T0[(C0>>8)&255], 24) ^ shift(T0[(C1>>16)&255], 16) ^ shift(T0[(C2>>24)&255], 8) ^ KW[r++][3]; + C0 = T0[r0&255] ^ shift(T0[(r1>>8)&255], 24) ^ shift(T0[(r2>>16)&255], 16) ^ shift(T0[(r3>>24)&255], 8) ^ KW[r][0]; + C1 = T0[r1&255] ^ shift(T0[(r2>>8)&255], 24) ^ shift(T0[(r3>>16)&255], 16) ^ shift(T0[(r0>>24)&255], 8) ^ KW[r][1]; + C2 = T0[r2&255] ^ shift(T0[(r3>>8)&255], 24) ^ shift(T0[(r0>>16)&255], 16) ^ shift(T0[(r1>>24)&255], 8) ^ KW[r][2]; + C3 = T0[r3&255] ^ shift(T0[(r0>>8)&255], 24) ^ shift(T0[(r1>>16)&255], 16) ^ shift(T0[(r2>>24)&255], 8) ^ KW[r++][3]; + } + + r0 = T0[C0&255] ^ shift(T0[(C1>>8)&255], 24) ^ shift(T0[(C2>>16)&255], 16) ^ shift(T0[(C3>>24)&255], 8) ^ KW[r][0]; + r1 = T0[C1&255] ^ shift(T0[(C2>>8)&255], 24) ^ shift(T0[(C3>>16)&255], 16) ^ shift(T0[(C0>>24)&255], 8) ^ KW[r][1]; + r2 = T0[C2&255] ^ shift(T0[(C3>>8)&255], 24) ^ shift(T0[(C0>>16)&255], 16) ^ shift(T0[(C1>>24)&255], 8) ^ KW[r][2]; + r3 = T0[C3&255] ^ shift(T0[(C0>>8)&255], 24) ^ shift(T0[(C1>>16)&255], 16) ^ shift(T0[(C2>>24)&255], 8) ^ KW[r++][3]; + + C0 = (S[r0&255]&255) ^ ((S[(r1>>8)&255]&255)<<8) ^ ((S[(r2>>16)&255]&255)<<16) ^ (S[(r3>>24)&255]<<24) ^ KW[r][0]; + C1 = (S[r1&255]&255) ^ ((S[(r2>>8)&255]&255)<<8) ^ ((S[(r3>>16)&255]&255)<<16) ^ (S[(r0>>24)&255]<<24) ^ KW[r][1]; + C2 = (S[r2&255]&255) ^ ((S[(r3>>8)&255]&255)<<8) ^ ((S[(r0>>16)&255]&255)<<16) ^ (S[(r1>>24)&255]<<24) ^ KW[r][2]; + C3 = (S[r3&255]&255) ^ ((S[(r0>>8)&255]&255)<<8) ^ ((S[(r1>>16)&255]&255)<<16) ^ (S[(r2>>24)&255]<<24) ^ KW[r][3]; + + } + + private int shift(int r, int shift) { + return (r >>> shift) | (r << -shift); + } + + private int subWord(int x) { + return (S[x&255]&255 | ((S[(x>>8)&255]&255)<<8) | ((S[(x>>16)&255]&255)<<16) | S[(x>>24)&255]<<24); + } + + private static final byte[] S = { + (byte)99, (byte)124, (byte)119, (byte)123, (byte)242, (byte)107, (byte)111, (byte)197, + (byte)48, (byte)1, (byte)103, (byte)43, (byte)254, (byte)215, (byte)171, (byte)118, + (byte)202, (byte)130, (byte)201, (byte)125, (byte)250, (byte)89, (byte)71, (byte)240, + (byte)173, (byte)212, (byte)162, (byte)175, (byte)156, (byte)164, (byte)114, (byte)192, + (byte)183, (byte)253, (byte)147, (byte)38, (byte)54, (byte)63, (byte)247, (byte)204, + (byte)52, (byte)165, (byte)229, (byte)241, (byte)113, (byte)216, (byte)49, (byte)21, + (byte)4, (byte)199, (byte)35, (byte)195, (byte)24, (byte)150, (byte)5, (byte)154, + (byte)7, (byte)18, (byte)128, (byte)226, (byte)235, (byte)39, (byte)178, (byte)117, + (byte)9, (byte)131, (byte)44, (byte)26, (byte)27, (byte)110, (byte)90, (byte)160, + (byte)82, (byte)59, (byte)214, (byte)179, (byte)41, (byte)227, (byte)47, (byte)132, + (byte)83, (byte)209, (byte)0, (byte)237, (byte)32, (byte)252, (byte)177, (byte)91, + (byte)106, (byte)203, (byte)190, (byte)57, (byte)74, (byte)76, (byte)88, (byte)207, + (byte)208, (byte)239, (byte)170, (byte)251, (byte)67, (byte)77, (byte)51, (byte)133, + (byte)69, (byte)249, (byte)2, (byte)127, (byte)80, (byte)60, (byte)159, (byte)168, + (byte)81, (byte)163, (byte)64, (byte)143, (byte)146, (byte)157, (byte)56, (byte)245, + (byte)188, (byte)182, (byte)218, (byte)33, (byte)16, (byte)255, (byte)243, (byte)210, + (byte)205, (byte)12, (byte)19, (byte)236, (byte)95, (byte)151, (byte)68, (byte)23, + (byte)196, (byte)167, (byte)126, (byte)61, (byte)100, (byte)93, (byte)25, (byte)115, + (byte)96, (byte)129, (byte)79, (byte)220, (byte)34, (byte)42, (byte)144, (byte)136, + (byte)70, (byte)238, (byte)184, (byte)20, (byte)222, (byte)94, (byte)11, (byte)219, + (byte)224, (byte)50, (byte)58, (byte)10, (byte)73, (byte)6, (byte)36, (byte)92, + (byte)194, (byte)211, (byte)172, (byte)98, (byte)145, (byte)149, (byte)228, (byte)121, + (byte)231, (byte)200, (byte)55, (byte)109, (byte)141, (byte)213, (byte)78, (byte)169, + (byte)108, (byte)86, (byte)244, (byte)234, (byte)101, (byte)122, (byte)174, (byte)8, + (byte)186, (byte)120, (byte)37, (byte)46, (byte)28, (byte)166, (byte)180, (byte)198, + (byte)232, (byte)221, (byte)116, (byte)31, (byte)75, (byte)189, (byte)139, (byte)138, + (byte)112, (byte)62, (byte)181, (byte)102, (byte)72, (byte)3, (byte)246, (byte)14, + (byte)97, (byte)53, (byte)87, (byte)185, (byte)134, (byte)193, (byte)29, (byte)158, + (byte)225, (byte)248, (byte)152, (byte)17, (byte)105, (byte)217, (byte)142, (byte)148, + (byte)155, (byte)30, (byte)135, (byte)233, (byte)206, (byte)85, (byte)40, (byte)223, + (byte)140, (byte)161, (byte)137, (byte)13, (byte)191, (byte)230, (byte)66, (byte)104, + (byte)65, (byte)153, (byte)45, (byte)15, (byte)176, (byte)84, (byte)187, (byte)22, + }; + + private static final int[] rcon = { + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, + 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 }; + + private static final int[] T0 = + { + 0xa56363c6, 0x847c7cf8, 0x997777ee, 0x8d7b7bf6, 0x0df2f2ff, + 0xbd6b6bd6, 0xb16f6fde, 0x54c5c591, 0x50303060, 0x03010102, + 0xa96767ce, 0x7d2b2b56, 0x19fefee7, 0x62d7d7b5, 0xe6abab4d, + 0x9a7676ec, 0x45caca8f, 0x9d82821f, 0x40c9c989, 0x877d7dfa, + 0x15fafaef, 0xeb5959b2, 0xc947478e, 0x0bf0f0fb, 0xecadad41, + 0x67d4d4b3, 0xfda2a25f, 0xeaafaf45, 0xbf9c9c23, 0xf7a4a453, + 0x967272e4, 0x5bc0c09b, 0xc2b7b775, 0x1cfdfde1, 0xae93933d, + 0x6a26264c, 0x5a36366c, 0x413f3f7e, 0x02f7f7f5, 0x4fcccc83, + 0x5c343468, 0xf4a5a551, 0x34e5e5d1, 0x08f1f1f9, 0x937171e2, + 0x73d8d8ab, 0x53313162, 0x3f15152a, 0x0c040408, 0x52c7c795, + 0x65232346, 0x5ec3c39d, 0x28181830, 0xa1969637, 0x0f05050a, + 0xb59a9a2f, 0x0907070e, 0x36121224, 0x9b80801b, 0x3de2e2df, + 0x26ebebcd, 0x6927274e, 0xcdb2b27f, 0x9f7575ea, 0x1b090912, + 0x9e83831d, 0x742c2c58, 0x2e1a1a34, 0x2d1b1b36, 0xb26e6edc, + 0xee5a5ab4, 0xfba0a05b, 0xf65252a4, 0x4d3b3b76, 0x61d6d6b7, + 0xceb3b37d, 0x7b292952, 0x3ee3e3dd, 0x712f2f5e, 0x97848413, + 0xf55353a6, 0x68d1d1b9, 0x00000000, 0x2cededc1, 0x60202040, + 0x1ffcfce3, 0xc8b1b179, 0xed5b5bb6, 0xbe6a6ad4, 0x46cbcb8d, + 0xd9bebe67, 0x4b393972, 0xde4a4a94, 0xd44c4c98, 0xe85858b0, + 0x4acfcf85, 0x6bd0d0bb, 0x2aefefc5, 0xe5aaaa4f, 0x16fbfbed, + 0xc5434386, 0xd74d4d9a, 0x55333366, 0x94858511, 0xcf45458a, + 0x10f9f9e9, 0x06020204, 0x817f7ffe, 0xf05050a0, 0x443c3c78, + 0xba9f9f25, 0xe3a8a84b, 0xf35151a2, 0xfea3a35d, 0xc0404080, + 0x8a8f8f05, 0xad92923f, 0xbc9d9d21, 0x48383870, 0x04f5f5f1, + 0xdfbcbc63, 0xc1b6b677, 0x75dadaaf, 0x63212142, 0x30101020, + 0x1affffe5, 0x0ef3f3fd, 0x6dd2d2bf, 0x4ccdcd81, 0x140c0c18, + 0x35131326, 0x2fececc3, 0xe15f5fbe, 0xa2979735, 0xcc444488, + 0x3917172e, 0x57c4c493, 0xf2a7a755, 0x827e7efc, 0x473d3d7a, + 0xac6464c8, 0xe75d5dba, 0x2b191932, 0x957373e6, 0xa06060c0, + 0x98818119, 0xd14f4f9e, 0x7fdcdca3, 0x66222244, 0x7e2a2a54, + 0xab90903b, 0x8388880b, 0xca46468c, 0x29eeeec7, 0xd3b8b86b, + 0x3c141428, 0x79dedea7, 0xe25e5ebc, 0x1d0b0b16, 0x76dbdbad, + 0x3be0e0db, 0x56323264, 0x4e3a3a74, 0x1e0a0a14, 0xdb494992, + 0x0a06060c, 0x6c242448, 0xe45c5cb8, 0x5dc2c29f, 0x6ed3d3bd, + 0xefacac43, 0xa66262c4, 0xa8919139, 0xa4959531, 0x37e4e4d3, + 0x8b7979f2, 0x32e7e7d5, 0x43c8c88b, 0x5937376e, 0xb76d6dda, + 0x8c8d8d01, 0x64d5d5b1, 0xd24e4e9c, 0xe0a9a949, 0xb46c6cd8, + 0xfa5656ac, 0x07f4f4f3, 0x25eaeacf, 0xaf6565ca, 0x8e7a7af4, + 0xe9aeae47, 0x18080810, 0xd5baba6f, 0x887878f0, 0x6f25254a, + 0x722e2e5c, 0x241c1c38, 0xf1a6a657, 0xc7b4b473, 0x51c6c697, + 0x23e8e8cb, 0x7cdddda1, 0x9c7474e8, 0x211f1f3e, 0xdd4b4b96, + 0xdcbdbd61, 0x868b8b0d, 0x858a8a0f, 0x907070e0, 0x423e3e7c, + 0xc4b5b571, 0xaa6666cc, 0xd8484890, 0x05030306, 0x01f6f6f7, + 0x120e0e1c, 0xa36161c2, 0x5f35356a, 0xf95757ae, 0xd0b9b969, + 0x91868617, 0x58c1c199, 0x271d1d3a, 0xb99e9e27, 0x38e1e1d9, + 0x13f8f8eb, 0xb398982b, 0x33111122, 0xbb6969d2, 0x70d9d9a9, + 0x898e8e07, 0xa7949433, 0xb69b9b2d, 0x221e1e3c, 0x92878715, + 0x20e9e9c9, 0x49cece87, 0xff5555aa, 0x78282850, 0x7adfdfa5, + 0x8f8c8c03, 0xf8a1a159, 0x80898909, 0x170d0d1a, 0xdabfbf65, + 0x31e6e6d7, 0xc6424284, 0xb86868d0, 0xc3414182, 0xb0999929, + 0x772d2d5a, 0x110f0f1e, 0xcbb0b07b, 0xfc5454a8, 0xd6bbbb6d, + 0x3a16162c}; + +} diff --git a/src/net/lingala/zip4j/crypto/engine/ZipCryptoEngine.java b/src/net/lingala/zip4j/crypto/engine/ZipCryptoEngine.java new file mode 100644 index 0000000..ac1e27f --- /dev/null +++ b/src/net/lingala/zip4j/crypto/engine/ZipCryptoEngine.java @@ -0,0 +1,65 @@ +/* +* 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.crypto.engine; + +public class ZipCryptoEngine { + + private final int keys[] = new int[3]; + private static final int[] CRC_TABLE = new int[256]; + + static { + for (int i = 0; i < 256; i++) { + int r = i; + for (int j = 0; j < 8; j++) { + if ((r & 1) == 1) { + r = (r >>> 1) ^ 0xedb88320; + } else { + r >>>= 1; + } + } + CRC_TABLE[i] = r; + } + } + + public ZipCryptoEngine() { + } + + public void initKeys(char[] password) { + keys[0] = 305419896; + keys[1] = 591751049; + keys[2] = 878082192; + for (int i = 0; i < password.length; i++) { + updateKeys((byte) (password[i] & 0xff)); + } + } + + public void updateKeys(byte charAt) { + keys[0] = crc32(keys[0], charAt); + keys[1] += keys[0] & 0xff; + keys[1] = keys[1] * 134775813 + 1; + keys[2] = crc32(keys[2], (byte) (keys[1] >> 24)); + } + + private int crc32(int oldCrc, byte charAt) { + return ((oldCrc >>> 8) ^ CRC_TABLE[(oldCrc ^ charAt) & 0xff]); + } + + public byte decryptByte() { + int temp = keys[2] | 2; + return (byte) ((temp * (temp ^ 1)) >>> 8); + } +} diff --git a/src/net/lingala/zip4j/exception/ZipException.java b/src/net/lingala/zip4j/exception/ZipException.java new file mode 100644 index 0000000..1d7521a --- /dev/null +++ b/src/net/lingala/zip4j/exception/ZipException.java @@ -0,0 +1,59 @@ +/* +* 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.exception; + +public class ZipException extends Exception { + + private static final long serialVersionUID = 1L; + + private int code = -1; + + public ZipException() { + } + + public ZipException(String msg) { + super(msg); + } + + public ZipException(String message, Throwable cause) { + super(message, cause); + } + + public ZipException(String msg, int code) { + super(msg); + this.code = code; + } + + public ZipException(String message, Throwable cause, int code) { + super(message, cause); + this.code = code; + } + + public ZipException(Throwable cause) { + super(cause); + } + + public ZipException(Throwable cause, int code) { + super(cause); + this.code = code; + } + + public int getCode() { + return code; + } + +} diff --git a/src/net/lingala/zip4j/exception/ZipExceptionConstants.java b/src/net/lingala/zip4j/exception/ZipExceptionConstants.java new file mode 100644 index 0000000..e6f0cb4 --- /dev/null +++ b/src/net/lingala/zip4j/exception/ZipExceptionConstants.java @@ -0,0 +1,31 @@ +/* +* 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.exception; + +public interface ZipExceptionConstants { + + public static int inputZipParamIsNull = 0001; + + public static int constuctorFileNotFoundException = 0002; + + public static int randomAccessFileNull = 0003; + + public static int notZipFile = 0004; + + public static int WRONG_PASSWORD = 0005; + +} diff --git a/src/net/lingala/zip4j/io/BaseInputStream.java b/src/net/lingala/zip4j/io/BaseInputStream.java new file mode 100644 index 0000000..b0bec3d --- /dev/null +++ b/src/net/lingala/zip4j/io/BaseInputStream.java @@ -0,0 +1,25 @@ +package net.lingala.zip4j.io; + +import java.io.IOException; +import java.io.InputStream; + +import net.lingala.zip4j.unzip.UnzipEngine; + +public abstract class BaseInputStream extends InputStream { + + public int read() throws IOException { + return 0; + } + + public void seek(long pos) throws IOException { + } + + public int available() throws IOException { + return 0; + } + + public UnzipEngine getUnzipEngine() { + return null; + } + +} diff --git a/src/net/lingala/zip4j/io/BaseOutputStream.java b/src/net/lingala/zip4j/io/BaseOutputStream.java new file mode 100644 index 0000000..dc289a4 --- /dev/null +++ b/src/net/lingala/zip4j/io/BaseOutputStream.java @@ -0,0 +1,27 @@ +/* +* 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.io; + +import java.io.IOException; +import java.io.OutputStream; + +public abstract class BaseOutputStream extends OutputStream { + + public void write(int b) throws IOException { + } + +} diff --git a/src/net/lingala/zip4j/io/CipherOutputStream.java b/src/net/lingala/zip4j/io/CipherOutputStream.java new file mode 100644 index 0000000..a90b6a2 --- /dev/null +++ b/src/net/lingala/zip4j/io/CipherOutputStream.java @@ -0,0 +1,570 @@ +/* +* 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.io; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.zip.CRC32; + +import net.lingala.zip4j.core.HeaderWriter; +import net.lingala.zip4j.crypto.AESEncrpyter; +import net.lingala.zip4j.crypto.IEncrypter; +import net.lingala.zip4j.crypto.StandardEncrypter; +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.model.AESExtraDataRecord; +import net.lingala.zip4j.model.CentralDirectory; +import net.lingala.zip4j.model.EndCentralDirRecord; +import net.lingala.zip4j.model.FileHeader; +import net.lingala.zip4j.model.LocalFileHeader; +import net.lingala.zip4j.model.ZipModel; +import net.lingala.zip4j.model.ZipParameters; +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 CipherOutputStream extends BaseOutputStream { + + protected OutputStream outputStream; + private File sourceFile; + protected FileHeader fileHeader; + protected LocalFileHeader localFileHeader; + private IEncrypter encrypter; + protected ZipParameters zipParameters; + protected ZipModel zipModel; + private long totalBytesWritten; + protected CRC32 crc; + private long bytesWrittenForThisFile; + private byte[] pendingBuffer; + private int pendingBufferLength; + private long totalBytesRead; + + public CipherOutputStream(OutputStream outputStream, ZipModel zipModel) { + this.outputStream = outputStream; + initZipModel(zipModel); + crc = new CRC32(); + this.totalBytesWritten = 0; + this.bytesWrittenForThisFile = 0; + this.pendingBuffer = new byte[InternalZipConstants.AES_BLOCK_SIZE]; + this.pendingBufferLength = 0; + this.totalBytesRead = 0; + } + + public void putNextEntry(File file, ZipParameters zipParameters) throws ZipException { + if (!zipParameters.isSourceExternalStream() && file == null) { + throw new ZipException("input file is null"); + } + + if (!zipParameters.isSourceExternalStream() && !Zip4jUtil.checkFileExists(file)) { + throw new ZipException("input file does not exist"); + } + + try { + sourceFile = file; + + this.zipParameters = (ZipParameters)zipParameters.clone(); + + if (!zipParameters.isSourceExternalStream()) { + if (sourceFile.isDirectory()) { + this.zipParameters.setEncryptFiles(false); + this.zipParameters.setEncryptionMethod(-1); + this.zipParameters.setCompressionMethod(Zip4jConstants.COMP_STORE); + } + } else { + if (!Zip4jUtil.isStringNotNullAndNotEmpty(this.zipParameters.getFileNameInZip())) { + throw new ZipException("file name is empty for external stream"); + } + if (this.zipParameters.getFileNameInZip().endsWith("/") || + this.zipParameters.getFileNameInZip().endsWith("\\")) { + this.zipParameters.setEncryptFiles(false); + this.zipParameters.setEncryptionMethod(-1); + this.zipParameters.setCompressionMethod(Zip4jConstants.COMP_STORE); + } + } + + createFileHeader(); + createLocalFileHeader(); + + if (zipModel.isSplitArchive()) { + if (zipModel.getCentralDirectory() == null || + zipModel.getCentralDirectory().getFileHeaders() == null || + zipModel.getCentralDirectory().getFileHeaders().size() == 0) { + byte[] intByte = new byte[4]; + Raw.writeIntLittleEndian(intByte, 0, (int)InternalZipConstants.SPLITSIG); + outputStream.write(intByte); + totalBytesWritten += 4; + } + } + + if (this.outputStream instanceof SplitOutputStream) { + if (totalBytesWritten == 4) { + fileHeader.setOffsetLocalHeader(4); + } else { + fileHeader.setOffsetLocalHeader(((SplitOutputStream)outputStream).getFilePointer()); + } + } else { + if (totalBytesWritten == 4) { + fileHeader.setOffsetLocalHeader(4); + } else { + fileHeader.setOffsetLocalHeader(totalBytesWritten); + } + } + + HeaderWriter headerWriter = new HeaderWriter(); + totalBytesWritten += headerWriter.writeLocalFileHeader(zipModel, localFileHeader, outputStream); + + if (this.zipParameters.isEncryptFiles()) { + initEncrypter(); + if (encrypter != null) { + if (zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) { + byte[] headerBytes = ((StandardEncrypter)encrypter).getHeaderBytes(); + outputStream.write(headerBytes); + totalBytesWritten += headerBytes.length; + bytesWrittenForThisFile += headerBytes.length; + } else if (zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) { + byte[] saltBytes = ((AESEncrpyter)encrypter).getSaltBytes(); + byte[] passwordVerifier = ((AESEncrpyter)encrypter).getDerivedPasswordVerifier(); + outputStream.write(saltBytes); + outputStream.write(passwordVerifier); + totalBytesWritten += saltBytes.length + passwordVerifier.length; + bytesWrittenForThisFile += saltBytes.length + passwordVerifier.length; + } + } + } + + crc.reset(); + } catch (CloneNotSupportedException e) { + throw new ZipException(e); + } catch (ZipException e) { + throw e; + } catch (Exception e) { + throw new ZipException(e); + } + } + + private void initEncrypter() throws ZipException { + if (!zipParameters.isEncryptFiles()) { + encrypter = null; + return; + } + + switch (zipParameters.getEncryptionMethod()) { + case Zip4jConstants.ENC_METHOD_STANDARD: + // Since we do not know the crc here, we use the modification time for encrypting. + encrypter = new StandardEncrypter(zipParameters.getPassword(), (localFileHeader.getLastModFileTime() & 0x0000ffff) << 16); + break; + case Zip4jConstants.ENC_METHOD_AES: + encrypter = new AESEncrpyter(zipParameters.getPassword(), zipParameters.getAesKeyStrength()); + break; + default: + throw new ZipException("invalid encprytion method"); + } + } + + private void initZipModel(ZipModel zipModel) { + if (zipModel == null) { + this.zipModel = new ZipModel(); + } else { + this.zipModel = zipModel; + } + + if (this.zipModel.getEndCentralDirRecord() == null) + this.zipModel.setEndCentralDirRecord(new EndCentralDirRecord()); + + if (this.zipModel.getCentralDirectory() == null) + this.zipModel.setCentralDirectory(new CentralDirectory()); + + if (this.zipModel.getCentralDirectory().getFileHeaders() == null) + this.zipModel.getCentralDirectory().setFileHeaders(new ArrayList()); + + if (this.zipModel.getLocalFileHeaderList() == null) + this.zipModel.setLocalFileHeaderList(new ArrayList()); + + if (this.outputStream instanceof SplitOutputStream) { + if (((SplitOutputStream)outputStream).isSplitZipFile()) { + this.zipModel.setSplitArchive(true); + this.zipModel.setSplitLength(((SplitOutputStream)outputStream).getSplitLength()); + } + } + + this.zipModel.getEndCentralDirRecord().setSignature(InternalZipConstants.ENDSIG); + } + + public void write(int bval) throws IOException { + byte[] b = new byte[1]; + b[0] = (byte) bval; + write(b, 0, 1); + } + + public void write(byte[] b) throws IOException { + if (b == null) + throw new NullPointerException(); + + if (b.length == 0) return; + + write(b, 0, b.length); + } + + public void write(byte[] b, int off, int len) throws IOException { + if (len == 0) return; + + if (zipParameters.isEncryptFiles() && + zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) { + if (pendingBufferLength != 0) { + if (len >= (InternalZipConstants.AES_BLOCK_SIZE - pendingBufferLength)) { + System.arraycopy(b, off, pendingBuffer, pendingBufferLength, + (InternalZipConstants.AES_BLOCK_SIZE - pendingBufferLength)); + encryptAndWrite(pendingBuffer, 0, pendingBuffer.length); + off = (InternalZipConstants.AES_BLOCK_SIZE - pendingBufferLength); + len = len - off; + pendingBufferLength = 0; + } else { + System.arraycopy(b, off, pendingBuffer, pendingBufferLength, + len); + pendingBufferLength += len; + return; + } + } + if (len != 0 && len % 16 != 0) { + System.arraycopy(b, (len + off) - (len % 16), pendingBuffer, 0, len % 16); + pendingBufferLength = len % 16; + len = len - pendingBufferLength; + } + } + if (len != 0) + encryptAndWrite(b, off, len); + } + + private void encryptAndWrite(byte[] b, int off, int len) throws IOException { + if (encrypter != null) { + try { + encrypter.encryptData(b, off, len); + } catch (ZipException e) { + throw new IOException(e.getMessage()); + } + } + outputStream.write(b, off, len); + totalBytesWritten += len; + bytesWrittenForThisFile += len; + } + + public void closeEntry() throws IOException, ZipException { + + if (this.pendingBufferLength != 0) { + encryptAndWrite(pendingBuffer, 0, pendingBufferLength); + pendingBufferLength = 0; + } + + if (this.zipParameters.isEncryptFiles() && + this.zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) { + if (encrypter instanceof AESEncrpyter) { + outputStream.write(((AESEncrpyter)encrypter).getFinalMac()); + bytesWrittenForThisFile += 10; + totalBytesWritten += 10; + } else { + throw new ZipException("invalid encrypter for AES encrypted file"); + } + } + fileHeader.setCompressedSize(bytesWrittenForThisFile); + localFileHeader.setCompressedSize(bytesWrittenForThisFile); + + if (zipParameters.isSourceExternalStream()) { + fileHeader.setUncompressedSize(totalBytesRead); + if (localFileHeader.getUncompressedSize() != totalBytesRead) { + localFileHeader.setUncompressedSize(totalBytesRead); + } + } + + long crc32 = crc.getValue(); + if (fileHeader.isEncrypted()) { + if (fileHeader.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) { + crc32 = 0; + } + } + + if (zipParameters.isEncryptFiles() && + zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) { + fileHeader.setCrc32(0); + localFileHeader.setCrc32(0); + } else { + fileHeader.setCrc32(crc32); + localFileHeader.setCrc32(crc32); + } + + zipModel.getLocalFileHeaderList().add(localFileHeader); + zipModel.getCentralDirectory().getFileHeaders().add(fileHeader); + + HeaderWriter headerWriter = new HeaderWriter(); + totalBytesWritten += headerWriter.writeExtendedLocalHeader(localFileHeader, outputStream); + + crc.reset(); + bytesWrittenForThisFile = 0; + encrypter = null; + totalBytesRead = 0; + } + + public void finish() throws IOException, ZipException { + zipModel.getEndCentralDirRecord().setOffsetOfStartOfCentralDir(totalBytesWritten); + + HeaderWriter headerWriter = new HeaderWriter(); + headerWriter.finalizeZipFile(zipModel, outputStream); + } + + public void close() throws IOException { + if (outputStream != null) + outputStream.close(); + } + + private void createFileHeader() throws ZipException { + this.fileHeader = new FileHeader(); + fileHeader.setSignature((int)InternalZipConstants.CENSIG); + fileHeader.setVersionMadeBy(20); + fileHeader.setVersionNeededToExtract(20); + if (zipParameters.isEncryptFiles() && + zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) { + fileHeader.setCompressionMethod(Zip4jConstants.ENC_METHOD_AES); + fileHeader.setAesExtraDataRecord(generateAESExtraDataRecord(zipParameters)); + } else { + fileHeader.setCompressionMethod(zipParameters.getCompressionMethod()); + } + if (zipParameters.isEncryptFiles()) { + fileHeader.setEncrypted(true); + fileHeader.setEncryptionMethod(zipParameters.getEncryptionMethod()); + } + String fileName = null; + if (zipParameters.isSourceExternalStream()) { + fileHeader.setLastModFileTime((int) Zip4jUtil.javaToDosTime(System.currentTimeMillis())); + if (!Zip4jUtil.isStringNotNullAndNotEmpty(zipParameters.getFileNameInZip())) { + throw new ZipException("fileNameInZip is null or empty"); + } + fileName = zipParameters.getFileNameInZip(); + } else { + fileHeader.setLastModFileTime((int) Zip4jUtil.javaToDosTime((Zip4jUtil.getLastModifiedFileTime( + sourceFile, zipParameters.getTimeZone())))); + fileHeader.setUncompressedSize(sourceFile.length()); + fileName = Zip4jUtil.getRelativeFileName( + sourceFile.getAbsolutePath(), zipParameters.getRootFolderInZip(), zipParameters.getDefaultFolderPath()); + + } + + if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileName)) { + throw new ZipException("fileName is null or empty. unable to create file header"); + } + + fileHeader.setFileName(fileName); + + if (Zip4jUtil.isStringNotNullAndNotEmpty(zipModel.getFileNameCharset())) { + fileHeader.setFileNameLength(Zip4jUtil.getEncodedStringLength(fileName, + zipModel.getFileNameCharset())); + } else { + fileHeader.setFileNameLength(Zip4jUtil.getEncodedStringLength(fileName)); + } + + if (outputStream instanceof SplitOutputStream) { + fileHeader.setDiskNumberStart(((SplitOutputStream)outputStream).getCurrSplitFileCounter()); + } else { + fileHeader.setDiskNumberStart(0); + } + + int fileAttrs = 0; + if (!zipParameters.isSourceExternalStream()) + fileAttrs = getFileAttributes(sourceFile); + byte[] externalFileAttrs = {(byte)fileAttrs, 0, 0, 0}; + fileHeader.setExternalFileAttr(externalFileAttrs); + + if (zipParameters.isSourceExternalStream()) { + fileHeader.setDirectory(fileName.endsWith("/") || fileName.endsWith("\\")); + } else { + fileHeader.setDirectory(this.sourceFile.isDirectory()); + } + if (fileHeader.isDirectory()) { + fileHeader.setCompressedSize(0); + fileHeader.setUncompressedSize(0); + } else { + if (!zipParameters.isSourceExternalStream()) { + long fileSize = Zip4jUtil.getFileLengh(sourceFile); + if (zipParameters.getCompressionMethod() == Zip4jConstants.COMP_STORE) { + if (zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) { + fileHeader.setCompressedSize(fileSize + + InternalZipConstants.STD_DEC_HDR_SIZE); + } else if (zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES) { + int saltLength = 0; + switch (zipParameters.getAesKeyStrength()) { + case Zip4jConstants.AES_STRENGTH_128: + saltLength = 8; + break; + case Zip4jConstants.AES_STRENGTH_256: + saltLength = 16; + break; + default: + throw new ZipException("invalid aes key strength, cannot determine key sizes"); + } + fileHeader.setCompressedSize(fileSize + saltLength + + InternalZipConstants.AES_AUTH_LENGTH + 2); //2 is password verifier + } else { + fileHeader.setCompressedSize(0); + } + } else { + fileHeader.setCompressedSize(0); + } + fileHeader.setUncompressedSize(fileSize); + } + } + if (zipParameters.isEncryptFiles() && + zipParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) { + fileHeader.setCrc32(zipParameters.getSourceFileCRC()); + } + byte[] shortByte = new byte[2]; + shortByte[0] = Raw.bitArrayToByte(generateGeneralPurposeBitArray( + fileHeader.isEncrypted(), zipParameters.getCompressionMethod())); + boolean isFileNameCharsetSet = Zip4jUtil.isStringNotNullAndNotEmpty(zipModel.getFileNameCharset()); + if ((isFileNameCharsetSet && + zipModel.getFileNameCharset().equalsIgnoreCase(InternalZipConstants.CHARSET_UTF8)) || + (!isFileNameCharsetSet && + Zip4jUtil.detectCharSet(fileHeader.getFileName()).equals(InternalZipConstants.CHARSET_UTF8))) { + shortByte[1] = 8; + } else { + shortByte[1] = 0; + } + fileHeader.setGeneralPurposeFlag(shortByte); + } + + private void createLocalFileHeader() throws ZipException { + if (fileHeader == null) { + throw new ZipException("file header is null, cannot create local file header"); + } + this.localFileHeader = new LocalFileHeader(); + localFileHeader.setSignature((int)InternalZipConstants.LOCSIG); + localFileHeader.setVersionNeededToExtract(fileHeader.getVersionNeededToExtract()); + localFileHeader.setCompressionMethod(fileHeader.getCompressionMethod()); + localFileHeader.setLastModFileTime(fileHeader.getLastModFileTime()); + localFileHeader.setUncompressedSize(fileHeader.getUncompressedSize()); + localFileHeader.setFileNameLength(fileHeader.getFileNameLength()); + localFileHeader.setFileName(fileHeader.getFileName()); + localFileHeader.setEncrypted(fileHeader.isEncrypted()); + localFileHeader.setEncryptionMethod(fileHeader.getEncryptionMethod()); + localFileHeader.setAesExtraDataRecord(fileHeader.getAesExtraDataRecord()); + localFileHeader.setCrc32(fileHeader.getCrc32()); + localFileHeader.setCompressedSize(fileHeader.getCompressedSize()); + localFileHeader.setGeneralPurposeFlag((byte[])fileHeader.getGeneralPurposeFlag().clone()); + } + + /** + * Checks the file attributes and returns an integer + * @param file + * @return + * @throws ZipException + */ + private int getFileAttributes(File file) throws ZipException { + if (file == null) { + throw new ZipException("input file is null, cannot get file attributes"); + } + + if (!file.exists()) { + return 0; + } + + if (file.isDirectory()) { + if (file.isHidden()) { + return InternalZipConstants.FOLDER_MODE_HIDDEN; + } else { + return InternalZipConstants.FOLDER_MODE_NONE; + } + } else { + if (!file.canWrite() && file.isHidden()) { + return InternalZipConstants.FILE_MODE_READ_ONLY_HIDDEN; + } else if (!file.canWrite()) { + return InternalZipConstants.FILE_MODE_READ_ONLY; + } else if (file.isHidden()) { + return InternalZipConstants.FILE_MODE_HIDDEN; + } else { + return InternalZipConstants.FILE_MODE_NONE; + } + } + } + + private int[] generateGeneralPurposeBitArray(boolean isEncrpyted, int compressionMethod) { + + int[] generalPurposeBits = new int[8]; + if (isEncrpyted) { + generalPurposeBits[0] = 1; + } else { + generalPurposeBits[0] = 0; + } + + if (compressionMethod == Zip4jConstants.COMP_DEFLATE) { + // Have to set flags for deflate + } else { + generalPurposeBits[1] = 0; + generalPurposeBits[2] = 0; + } + + generalPurposeBits[3] = 1; + + return generalPurposeBits; + } + + private AESExtraDataRecord generateAESExtraDataRecord(ZipParameters parameters) throws ZipException { + + if (parameters == null) { + throw new ZipException("zip parameters are null, cannot generate AES Extra Data record"); + } + + AESExtraDataRecord aesDataRecord = new AESExtraDataRecord(); + aesDataRecord.setSignature(InternalZipConstants.AESSIG); + aesDataRecord.setDataSize(7); + aesDataRecord.setVendorID("AE"); + // Always set the version number to 2 as we do not store CRC for any AES encrypted files + // only MAC is stored and as per the specification, if version number is 2, then MAC is read + // and CRC is ignored + aesDataRecord.setVersionNumber(2); + if (parameters.getAesKeyStrength() == Zip4jConstants.AES_STRENGTH_128) { + aesDataRecord.setAesStrength(Zip4jConstants.AES_STRENGTH_128); + } else if (parameters.getAesKeyStrength() == Zip4jConstants.AES_STRENGTH_256) { + aesDataRecord.setAesStrength(Zip4jConstants.AES_STRENGTH_256); + } else { + throw new ZipException("invalid AES key strength, cannot generate AES Extra data record"); + } + aesDataRecord.setCompressionMethod(parameters.getCompressionMethod()); + + return aesDataRecord; + } + + public void decrementCompressedFileSize(int value) { + if (value <= 0) return; + + if (value <= this.bytesWrittenForThisFile) { + this.bytesWrittenForThisFile -= value; + } + } + + protected void updateTotalBytesRead(int toUpdate) { + if (toUpdate > 0) { + totalBytesRead += toUpdate; + } + } + + public void setSourceFile(File sourceFile) { + this.sourceFile = sourceFile; + } + + public File getSourceFile() { + return sourceFile; + } +} diff --git a/src/net/lingala/zip4j/io/DeflaterOutputStream.java b/src/net/lingala/zip4j/io/DeflaterOutputStream.java new file mode 100644 index 0000000..e8114be --- /dev/null +++ b/src/net/lingala/zip4j/io/DeflaterOutputStream.java @@ -0,0 +1,115 @@ +/* +* 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.io; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.Deflater; + +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.model.ZipModel; +import net.lingala.zip4j.model.ZipParameters; +import net.lingala.zip4j.util.InternalZipConstants; +import net.lingala.zip4j.util.Zip4jConstants; + +public class DeflaterOutputStream extends CipherOutputStream { + + private byte[] buff; + protected Deflater deflater; + private boolean firstBytesRead; + + public DeflaterOutputStream(OutputStream outputStream, ZipModel zipModel) { + super(outputStream, zipModel); + deflater = new Deflater(); + buff = new byte[InternalZipConstants.BUFF_SIZE]; + firstBytesRead = false; + } + + public void putNextEntry(File file, ZipParameters zipParameters) + throws ZipException { + super.putNextEntry(file, zipParameters); + if (zipParameters.getCompressionMethod() == Zip4jConstants.COMP_DEFLATE) { + deflater.reset(); + if ((zipParameters.getCompressionLevel() < 0 || zipParameters + .getCompressionLevel() > 9) + && zipParameters.getCompressionLevel() != -1) { + throw new ZipException( + "invalid compression level for deflater. compression level should be in the range of 0-9"); + } + deflater.setLevel(zipParameters.getCompressionLevel()); + } + } + + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + private void deflate () throws IOException { + int len = deflater.deflate(buff, 0, buff.length); + if (len > 0) { + if (deflater.finished()) { + if (len == 4) return; + if (len < 4) { + decrementCompressedFileSize(4 - len); + return; + } + len -= 4; + } + if (!firstBytesRead) { + super.write(buff, 2, len - 2); + firstBytesRead = true; + } else { + super.write(buff, 0, len); + } + } + } + + public void write(int bval) throws IOException { + byte[] b = new byte[1]; + b[0] = (byte) bval; + write(b, 0, 1); + } + + public void write(byte[] buf, int off, int len) throws IOException { + if (zipParameters.getCompressionMethod() != Zip4jConstants.COMP_DEFLATE) { + super.write(buf, off, len); + } else { + deflater.setInput(buf, off, len); + while (!deflater.needsInput()) { + deflate(); + } + } + } + + public void closeEntry() throws IOException, ZipException { + if (zipParameters.getCompressionMethod() == Zip4jConstants.COMP_DEFLATE) { + if (!deflater.finished()) { + deflater.finish(); + while (!deflater.finished()) { + deflate(); + } + } + firstBytesRead = false; + } + super.closeEntry(); + } + + public void finish() throws IOException, ZipException { + super.finish(); + } +} diff --git a/src/net/lingala/zip4j/io/InflaterInputStream.java b/src/net/lingala/zip4j/io/InflaterInputStream.java new file mode 100644 index 0000000..d7b68e9 --- /dev/null +++ b/src/net/lingala/zip4j/io/InflaterInputStream.java @@ -0,0 +1,174 @@ +/* +* 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.io; + +import java.io.EOFException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +import net.lingala.zip4j.unzip.UnzipEngine; +import net.lingala.zip4j.util.InternalZipConstants; +import net.lingala.zip4j.util.Zip4jConstants; + +public class InflaterInputStream extends PartInputStream { + + private Inflater inflater; + private byte[] buff; + private byte[] oneByteBuff = new byte[1]; + private UnzipEngine unzipEngine; + private long bytesWritten; + private long uncompressedSize; + + public InflaterInputStream(RandomAccessFile raf, long start, long len, UnzipEngine unzipEngine) { + super(raf, start, len, unzipEngine); + this.inflater = new Inflater(true); + this.buff = new byte[InternalZipConstants.BUFF_SIZE]; + this.unzipEngine = unzipEngine; + bytesWritten = 0; + uncompressedSize = unzipEngine.getFileHeader().getUncompressedSize(); + } + + public int read() throws IOException { + return read(oneByteBuff, 0, 1) == -1 ? -1 : oneByteBuff[0] & 0xff; + } + + public int read(byte[] b) throws IOException { + if (b == null) { + throw new NullPointerException("input buffer is null"); + } + + return read(b, 0, b.length); + } + + public int read(byte[] b, int off, int len) throws IOException { + + if (b == null) { + throw new NullPointerException("input buffer is null"); + } else if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + + try { + int n; + if (bytesWritten >= uncompressedSize) { + finishInflating(); + return -1; + } + while ((n = inflater.inflate(b, off, len)) == 0) { + if (inflater.finished() || inflater.needsDictionary()) { + finishInflating(); + return -1; + } + if (inflater.needsInput()) { + fill(); + } + } + bytesWritten += n; + return n; + } catch (DataFormatException e) { + String s = "Invalid ZLIB data format"; + if (e.getMessage() != null) { + s = e.getMessage(); + } + if (unzipEngine != null) { + if (unzipEngine.getLocalFileHeader().isEncrypted() && + unzipEngine.getLocalFileHeader().getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) { + s += " - Wrong Password?"; + } + } + throw new IOException(s); + } + } + + private void finishInflating() throws IOException { + //In some cases, compelte data is not read even though inflater is complete + //make sure to read complete data before returning -1 + byte[] b = new byte[1024]; + while (super.read(b, 0, 1024) != -1) { + //read all data + } + checkAndReadAESMacBytes(); + } + + private void fill() throws IOException { + int len = super.read(buff, 0, buff.length); + if (len == -1) { + throw new EOFException("Unexpected end of ZLIB input stream"); + } + inflater.setInput(buff, 0, len); + } + + /** + * Skips specified number of bytes of uncompressed data. + * @param n the number of bytes to skip + * @return the actual number of bytes skipped. + * @exception IOException if an I/O error has occurred + * @exception IllegalArgumentException if n < 0 + */ + public long skip(long n) throws IOException { + if (n < 0) { + throw new IllegalArgumentException("negative skip length"); + } + int max = (int)Math.min(n, Integer.MAX_VALUE); + int total = 0; + byte[] b = new byte[512]; + while (total < max) { + int len = max - total; + if (len > b.length) { + len = b.length; + } + len = read(b, 0, len); + if (len == -1) { + break; + } + total += len; + } + return total; + } + + + public void seek(long pos) throws IOException { + super.seek(pos); + } + + /** + * Returns 0 after EOF has been reached, otherwise always return 1. + *

+ * Programs should not count on this method to return the actual number + * of bytes that could be read without blocking. + * + * @return 1 before EOF and 0 after EOF. + * @exception IOException if an I/O error occurs. + * + */ + public int available() { + return inflater.finished() ? 0 : 1; + } + + public void close() throws IOException { + inflater.end(); + super.close(); + } + + public UnzipEngine getUnzipEngine() { + return super.getUnzipEngine(); + } +} diff --git a/src/net/lingala/zip4j/io/PartInputStream.java b/src/net/lingala/zip4j/io/PartInputStream.java new file mode 100644 index 0000000..bdfdff9 --- /dev/null +++ b/src/net/lingala/zip4j/io/PartInputStream.java @@ -0,0 +1,172 @@ +/* +* 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.io; + +import java.io.IOException; +import java.io.RandomAccessFile; + +import net.lingala.zip4j.crypto.AESDecrypter; +import net.lingala.zip4j.crypto.IDecrypter; +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.unzip.UnzipEngine; +import net.lingala.zip4j.util.InternalZipConstants; +import net.lingala.zip4j.util.Zip4jConstants; + +public class PartInputStream extends BaseInputStream +{ + private RandomAccessFile raf; + private long bytesRead, length; + private UnzipEngine unzipEngine; + private IDecrypter decrypter; + private byte[] oneByteBuff = new byte[1]; + private byte[] aesBlockByte = new byte[16]; + private int aesBytesReturned = 0; + private boolean isAESEncryptedFile = false; + private int count = -1; + + public PartInputStream(RandomAccessFile raf, long start, long len, UnzipEngine unzipEngine) { + this.raf = raf; + this.unzipEngine = unzipEngine; + this.decrypter = unzipEngine.getDecrypter(); + this.bytesRead = 0; + this.length = len; + this.isAESEncryptedFile = unzipEngine.getFileHeader().isEncrypted() && + unzipEngine.getFileHeader().getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES; + } + + public int available() { + long amount = length - bytesRead; + if (amount > Integer.MAX_VALUE) + return Integer.MAX_VALUE; + return (int) amount; + } + + public int read() throws IOException { + if (bytesRead >= length) + return -1; + + if (isAESEncryptedFile) { + if (aesBytesReturned == 0 || aesBytesReturned == 16) { + if (read(aesBlockByte) == -1) { + return -1; + } + aesBytesReturned = 0; + } + return aesBlockByte[aesBytesReturned++] & 0xff; + } else { + return read(oneByteBuff, 0, 1) == -1 ? -1 : oneByteBuff[0] & 0xff; + } + } + + public int read(byte[] b) throws IOException { + return this.read(b, 0, b.length); + } + + public int read(byte[] b, int off, int len) throws IOException { + if (len > length - bytesRead) { + len = (int) (length - bytesRead); + if (len == 0) { + checkAndReadAESMacBytes(); + return -1; + } + } + + if (unzipEngine.getDecrypter() instanceof AESDecrypter) { + if (bytesRead + len < length) { + if (len % 16 != 0) { + len = len - (len%16); + } + } + } + + synchronized (raf) { + count = raf.read(b, off, len); + if ((count < len) && unzipEngine.getZipModel().isSplitArchive()) { + raf.close(); + raf = unzipEngine.startNextSplitFile(); + if (count < 0) count = 0; + int newlyRead = raf.read(b, count, len-count); + if (newlyRead > 0) + count += newlyRead; + } + } + + if (count > 0) { + if (decrypter != null) { + try { + decrypter.decryptData(b, off, count); + } catch (ZipException e) { + throw new IOException(e.getMessage()); + } + } + bytesRead += count; + } + + if (bytesRead >= length) { + checkAndReadAESMacBytes(); + } + + return count; + } + + protected void checkAndReadAESMacBytes() throws IOException { + if (isAESEncryptedFile) { + if (decrypter != null && decrypter instanceof AESDecrypter) { + if (((AESDecrypter)decrypter).getStoredMac() != null) { + //Stored mac already set + return; + } + byte[] macBytes = new byte[InternalZipConstants.AES_AUTH_LENGTH]; + int readLen = -1; + readLen = raf.read(macBytes); + if (readLen != InternalZipConstants.AES_AUTH_LENGTH) { + if (unzipEngine.getZipModel().isSplitArchive()) { + raf.close(); + raf = unzipEngine.startNextSplitFile(); + int newlyRead = raf.read(macBytes, readLen, InternalZipConstants.AES_AUTH_LENGTH - readLen); + readLen += newlyRead; + } else { + throw new IOException("Error occured while reading stored AES authentication bytes"); + } + } + + ((AESDecrypter)unzipEngine.getDecrypter()).setStoredMac(macBytes); + } + } + } + + public long skip(long amount) throws IOException { + if (amount < 0) + throw new IllegalArgumentException(); + if (amount > length - bytesRead) + amount = length - bytesRead; + bytesRead += amount; + return amount; + } + + public void close() throws IOException { + raf.close(); + } + + public void seek(long pos) throws IOException { + raf.seek(pos); + } + + public UnzipEngine getUnzipEngine() { + return this.unzipEngine; + } +} diff --git a/src/net/lingala/zip4j/io/SplitOutputStream.java b/src/net/lingala/zip4j/io/SplitOutputStream.java new file mode 100644 index 0000000..2f4767d --- /dev/null +++ b/src/net/lingala/zip4j/io/SplitOutputStream.java @@ -0,0 +1,234 @@ +/* +* 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.io; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; + +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.util.InternalZipConstants; +import net.lingala.zip4j.util.Raw; +import net.lingala.zip4j.util.Zip4jUtil; + +public class SplitOutputStream extends OutputStream { + + private RandomAccessFile raf; + private long splitLength; + private File zipFile; + private File outFile; + private int currSplitFileCounter; + private long bytesWrittenForThisPart; + + public SplitOutputStream(String name) throws FileNotFoundException, ZipException { + this(Zip4jUtil.isStringNotNullAndNotEmpty(name) ? + new File(name) : null); + } + + public SplitOutputStream(File file) throws FileNotFoundException, ZipException { + this(file, -1); + } + + public SplitOutputStream(String name, long splitLength) throws FileNotFoundException, ZipException { + this(!Zip4jUtil.isStringNotNullAndNotEmpty(name) ? + new File(name) : null, splitLength); + } + + public SplitOutputStream(File file, long splitLength) throws FileNotFoundException, ZipException { + if (splitLength >= 0 && splitLength < InternalZipConstants.MIN_SPLIT_LENGTH) { + throw new ZipException("split length less than minimum allowed split length of " + InternalZipConstants.MIN_SPLIT_LENGTH +" Bytes"); + } + this.raf = new RandomAccessFile(file, InternalZipConstants.WRITE_MODE); + this.splitLength = splitLength; + this.outFile = file; + this.zipFile = file; + this.currSplitFileCounter = 0; + this.bytesWrittenForThisPart = 0; + } + + public void write(int b) throws IOException { + byte[] buff = new byte[1]; + buff[0] = (byte) b; + write(buff, 0, 1); + } + + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + public void write(byte[] b, int off, int len) throws IOException { + if (len <= 0) return; + + if (splitLength != -1) { + + if (splitLength < InternalZipConstants.MIN_SPLIT_LENGTH) { + throw new IOException("split length less than minimum allowed split length of " + InternalZipConstants.MIN_SPLIT_LENGTH +" Bytes"); + } + + if (bytesWrittenForThisPart >= splitLength) { + startNextSplitFile(); + raf.write(b, off, len); + bytesWrittenForThisPart = len; + } else if (bytesWrittenForThisPart + len > splitLength) { + if (isHeaderData(b)) { + startNextSplitFile(); + raf.write(b, off, len); + bytesWrittenForThisPart = len; + } else { + raf.write(b, off, (int)(splitLength - bytesWrittenForThisPart)); + startNextSplitFile(); + raf.write(b, off + (int)(splitLength - bytesWrittenForThisPart), (int)(len - (splitLength - bytesWrittenForThisPart))); + bytesWrittenForThisPart = len - (splitLength - bytesWrittenForThisPart); + } + } else { + raf.write(b, off, len); + bytesWrittenForThisPart += len; + } + + } else { + raf.write(b, off, len); + bytesWrittenForThisPart += len; + } + + } + + private void startNextSplitFile() throws IOException { + try { + String zipFileWithoutExt = Zip4jUtil.getZipFileNameWithoutExt(outFile.getName()); + File currSplitFile = null; + String zipFileName = zipFile.getAbsolutePath(); + String parentPath = (outFile.getParent() == null)?"":outFile.getParent() + System.getProperty("file.separator"); + + if (currSplitFileCounter < 9) { + currSplitFile = new File(parentPath + zipFileWithoutExt + ".z0" + (currSplitFileCounter + 1)); + } else { + currSplitFile = new File(parentPath + zipFileWithoutExt + ".z" + (currSplitFileCounter + 1)); + } + + raf.close(); + + if (currSplitFile.exists()) { + throw new IOException("split file: " + currSplitFile.getName() + " already exists in the current directory, cannot rename this file"); + } + + if (!zipFile.renameTo(currSplitFile)) { + throw new IOException("cannot rename newly created split file"); + } + + zipFile = new File(zipFileName); + raf = new RandomAccessFile(zipFile, InternalZipConstants.WRITE_MODE); + currSplitFileCounter++; + } catch (ZipException e) { + throw new IOException(e.getMessage()); + } + } + + private boolean isHeaderData(byte[] buff) { + if (buff == null || buff.length < 4) { + return false; + } + + int signature = Raw.readIntLittleEndian(buff, 0); + long[] allHeaderSignatures = Zip4jUtil.getAllHeaderSignatures(); + if (allHeaderSignatures != null && allHeaderSignatures.length > 0) { + for (int i = 0; i < allHeaderSignatures.length; i++) { + //Ignore split signature + if (allHeaderSignatures[i] != InternalZipConstants.SPLITSIG && + allHeaderSignatures[i] == signature) { + return true; + } + } + } + + return false; + } + + /** + * Checks if the buffer size is sufficient for the current split file. If not + * a new split file will be started. + * @param bufferSize + * @return true if a new split file was started else false + * @throws ZipException + */ + public boolean checkBuffSizeAndStartNextSplitFile(int bufferSize) throws ZipException { + if (bufferSize < 0) { + throw new ZipException("negative buffersize for checkBuffSizeAndStartNextSplitFile"); + } + + if (!isBuffSizeFitForCurrSplitFile(bufferSize)) { + try { + startNextSplitFile(); + bytesWrittenForThisPart = 0; + return true; + } catch (IOException e) { + throw new ZipException(e); + } + } + + return false; + } + + /** + * Checks if the given buffer size will be fit in the current split file. + * If this output stream is a non-split file, then this method always returns true + * @param bufferSize + * @return true if the buffer size is fit in the current split file or else false. + * @throws ZipException + */ + public boolean isBuffSizeFitForCurrSplitFile(int bufferSize) throws ZipException { + if (bufferSize < 0) { + throw new ZipException("negative buffersize for isBuffSizeFitForCurrSplitFile"); + } + + if (splitLength >= InternalZipConstants.MIN_SPLIT_LENGTH) { + return (bytesWrittenForThisPart + bufferSize <= splitLength); + } else { + //Non split zip -- return true + return true; + } + } + + public void seek(long pos) throws IOException { + raf.seek(pos); + } + + public void close() throws IOException { + if (raf != null) + raf.close(); + } + + public void flush() throws IOException { + } + + public long getFilePointer() throws IOException { + return raf.getFilePointer(); + } + + public boolean isSplitZipFile() { + return splitLength!=-1; + } + + public long getSplitLength() { + return splitLength; + } + + public int getCurrSplitFileCounter() { + return currSplitFileCounter; + } +} diff --git a/src/net/lingala/zip4j/io/ZipInputStream.java b/src/net/lingala/zip4j/io/ZipInputStream.java new file mode 100644 index 0000000..9257242 --- /dev/null +++ b/src/net/lingala/zip4j/io/ZipInputStream.java @@ -0,0 +1,89 @@ +/* +* 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.io; + +import java.io.IOException; +import java.io.InputStream; + +import net.lingala.zip4j.exception.ZipException; + +public class ZipInputStream extends InputStream { + + private BaseInputStream is; + + public ZipInputStream(BaseInputStream is) { + this.is = is; + } + + public int read() throws IOException { + int readByte = is.read(); + if (readByte != -1) { + is.getUnzipEngine().updateCRC(readByte); + } + return readByte; + } + + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + public int read(byte[] b, int off, int len) throws IOException { + int readLen = is.read(b, off, len); + if (readLen > 0 && is.getUnzipEngine() != null) { + is.getUnzipEngine().updateCRC(b, off, readLen); + } + return readLen; + } + + /** + * Closes the input stream and releases any resources. + * This method also checks for the CRC of the extracted file. + * If CRC check has to be skipped use close(boolean skipCRCCheck) method + * + * @throws IOException + */ + public void close() throws IOException { + close(false); + } + + /** + * Closes the input stream and releases any resources. + * If skipCRCCheck flag is set to true, this method skips CRC Check + * of the extracted file + * + * @throws IOException + */ + public void close(boolean skipCRCCheck) throws IOException { + try { + is.close(); + if (!skipCRCCheck && is.getUnzipEngine() != null) { + is.getUnzipEngine().checkCRC(); + } + } catch (ZipException e) { + throw new IOException(e.getMessage()); + } + } + + public int available() throws IOException { + return is.available(); + } + + public long skip(long n) throws IOException { + return is.skip(n); + } + +} diff --git a/src/net/lingala/zip4j/io/ZipOutputStream.java b/src/net/lingala/zip4j/io/ZipOutputStream.java new file mode 100644 index 0000000..b00150b --- /dev/null +++ b/src/net/lingala/zip4j/io/ZipOutputStream.java @@ -0,0 +1,33 @@ +package net.lingala.zip4j.io; + +import java.io.IOException; +import java.io.OutputStream; + +import net.lingala.zip4j.model.ZipModel; + +public class ZipOutputStream extends DeflaterOutputStream { + + public ZipOutputStream(OutputStream outputStream) { + this(outputStream, null); + } + + public ZipOutputStream(OutputStream outputStream, ZipModel zipModel) { + super(outputStream, zipModel); + } + + public void write(int bval) throws IOException { + byte[] b = new byte[1]; + b[0] = (byte) bval; + write(b, 0, 1); + } + + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + public void write(byte[] b, int off, int len) throws IOException { + crc.update(b, off, len); + updateTotalBytesRead(len); + super.write(b, off, len); + } +} diff --git a/src/net/lingala/zip4j/model/AESExtraDataRecord.java b/src/net/lingala/zip4j/model/AESExtraDataRecord.java new file mode 100644 index 0000000..a7400c5 --- /dev/null +++ b/src/net/lingala/zip4j/model/AESExtraDataRecord.java @@ -0,0 +1,97 @@ +/* +* 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.model; + +public class AESExtraDataRecord { + + private long signature; + private int dataSize; + private int versionNumber; + private String vendorID; + private int aesStrength; + private int compressionMethod; + + public AESExtraDataRecord() { + signature = -1; + dataSize = -1; + versionNumber = -1; + vendorID = null; + aesStrength = -1; + compressionMethod = -1; + } + + + public long getSignature() { + return signature; + } + + + public void setSignature(long signature) { + this.signature = signature; + } + + + public int getDataSize() { + return dataSize; + } + + + public void setDataSize(int dataSize) { + this.dataSize = dataSize; + } + + + public int getVersionNumber() { + return versionNumber; + } + + + public void setVersionNumber(int versionNumber) { + this.versionNumber = versionNumber; + } + + + public String getVendorID() { + return vendorID; + } + + + public void setVendorID(String vendorID) { + this.vendorID = vendorID; + } + + + public int getAesStrength() { + return aesStrength; + } + + + public void setAesStrength(int aesStrength) { + this.aesStrength = aesStrength; + } + + + public int getCompressionMethod() { + return compressionMethod; + } + + + public void setCompressionMethod(int compressionMethod) { + this.compressionMethod = compressionMethod; + } + +} diff --git a/src/net/lingala/zip4j/model/ArchiveExtraDataRecord.java b/src/net/lingala/zip4j/model/ArchiveExtraDataRecord.java new file mode 100644 index 0000000..703863b --- /dev/null +++ b/src/net/lingala/zip4j/model/ArchiveExtraDataRecord.java @@ -0,0 +1,51 @@ +/* +* 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.model; + +public class ArchiveExtraDataRecord { + + private int signature; + + private int extraFieldLength; + + private String extraFieldData; + + public int getSignature() { + return signature; + } + + public void setSignature(int signature) { + this.signature = signature; + } + + public int getExtraFieldLength() { + return extraFieldLength; + } + + public void setExtraFieldLength(int extraFieldLength) { + this.extraFieldLength = extraFieldLength; + } + + public String getExtraFieldData() { + return extraFieldData; + } + + public void setExtraFieldData(String extraFieldData) { + this.extraFieldData = extraFieldData; + } + +} diff --git a/src/net/lingala/zip4j/model/CentralDirectory.java b/src/net/lingala/zip4j/model/CentralDirectory.java new file mode 100644 index 0000000..fe40302 --- /dev/null +++ b/src/net/lingala/zip4j/model/CentralDirectory.java @@ -0,0 +1,44 @@ +/* +* 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.model; + +import java.util.ArrayList; + +public class CentralDirectory { + + private ArrayList fileHeaders; + + private DigitalSignature digitalSignature; + + public ArrayList getFileHeaders() { + return fileHeaders; + } + + public void setFileHeaders(ArrayList fileHeaders) { + this.fileHeaders = fileHeaders; + } + + public DigitalSignature getDigitalSignature() { + return digitalSignature; + } + + public void setDigitalSignature(DigitalSignature digitalSignature) { + this.digitalSignature = digitalSignature; + } + + +} diff --git a/src/net/lingala/zip4j/model/DataDescriptor.java b/src/net/lingala/zip4j/model/DataDescriptor.java new file mode 100644 index 0000000..da4bca9 --- /dev/null +++ b/src/net/lingala/zip4j/model/DataDescriptor.java @@ -0,0 +1,51 @@ +/* +* 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.model; + +public class DataDescriptor { + + private String crc32; + + private int compressedSize; + + private int uncompressedSize; + + public String getCrc32() { + return crc32; + } + + public void setCrc32(String crc32) { + this.crc32 = crc32; + } + + public int getCompressedSize() { + return compressedSize; + } + + public void setCompressedSize(int compressedSize) { + this.compressedSize = compressedSize; + } + + public int getUncompressedSize() { + return uncompressedSize; + } + + public void setUncompressedSize(int uncompressedSize) { + this.uncompressedSize = uncompressedSize; + } + +} diff --git a/src/net/lingala/zip4j/model/DigitalSignature.java b/src/net/lingala/zip4j/model/DigitalSignature.java new file mode 100644 index 0000000..75c6e11 --- /dev/null +++ b/src/net/lingala/zip4j/model/DigitalSignature.java @@ -0,0 +1,51 @@ +/* +* 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.model; + +public class DigitalSignature { + + private int headerSignature; + + private int sizeOfData; + + private String signatureData; + + public int getHeaderSignature() { + return headerSignature; + } + + public void setHeaderSignature(int headerSignature) { + this.headerSignature = headerSignature; + } + + public int getSizeOfData() { + return sizeOfData; + } + + public void setSizeOfData(int sizeOfData) { + this.sizeOfData = sizeOfData; + } + + public String getSignatureData() { + return signatureData; + } + + public void setSignatureData(String signatureData) { + this.signatureData = signatureData; + } + +} diff --git a/src/net/lingala/zip4j/model/EndCentralDirRecord.java b/src/net/lingala/zip4j/model/EndCentralDirRecord.java new file mode 100644 index 0000000..a3fa690 --- /dev/null +++ b/src/net/lingala/zip4j/model/EndCentralDirRecord.java @@ -0,0 +1,122 @@ +/* +* 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.model; + +public class EndCentralDirRecord { + + private long signature; + + private int noOfThisDisk; + + private int noOfThisDiskStartOfCentralDir; + + private int totNoOfEntriesInCentralDirOnThisDisk; + + private int totNoOfEntriesInCentralDir; + + private int sizeOfCentralDir; + + private long offsetOfStartOfCentralDir; + + private int commentLength; + + private String comment; + + private byte[] commentBytes; + + public long getSignature() { + return signature; + } + + public void setSignature(long signature) { + this.signature = signature; + } + + public int getNoOfThisDisk() { + return noOfThisDisk; + } + + public void setNoOfThisDisk(int noOfThisDisk) { + this.noOfThisDisk = noOfThisDisk; + } + + public int getNoOfThisDiskStartOfCentralDir() { + return noOfThisDiskStartOfCentralDir; + } + + public void setNoOfThisDiskStartOfCentralDir(int noOfThisDiskStartOfCentralDir) { + this.noOfThisDiskStartOfCentralDir = noOfThisDiskStartOfCentralDir; + } + + public int getTotNoOfEntriesInCentralDirOnThisDisk() { + return totNoOfEntriesInCentralDirOnThisDisk; + } + + public void setTotNoOfEntriesInCentralDirOnThisDisk( + int totNoOfEntriesInCentralDirOnThisDisk) { + this.totNoOfEntriesInCentralDirOnThisDisk = totNoOfEntriesInCentralDirOnThisDisk; + } + + public int getTotNoOfEntriesInCentralDir() { + return totNoOfEntriesInCentralDir; + } + + public void setTotNoOfEntriesInCentralDir(int totNoOfEntrisInCentralDir) { + this.totNoOfEntriesInCentralDir = totNoOfEntrisInCentralDir; + } + + public int getSizeOfCentralDir() { + return sizeOfCentralDir; + } + + public void setSizeOfCentralDir(int sizeOfCentralDir) { + this.sizeOfCentralDir = sizeOfCentralDir; + } + + public long getOffsetOfStartOfCentralDir() { + return offsetOfStartOfCentralDir; + } + + public void setOffsetOfStartOfCentralDir(long offSetOfStartOfCentralDir) { + this.offsetOfStartOfCentralDir = offSetOfStartOfCentralDir; + } + + public int getCommentLength() { + return commentLength; + } + + public void setCommentLength(int commentLength) { + this.commentLength = commentLength; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public byte[] getCommentBytes() { + return commentBytes; + } + + public void setCommentBytes(byte[] commentBytes) { + this.commentBytes = commentBytes; + } + +} diff --git a/src/net/lingala/zip4j/model/ExtraDataRecord.java b/src/net/lingala/zip4j/model/ExtraDataRecord.java new file mode 100644 index 0000000..d688428 --- /dev/null +++ b/src/net/lingala/zip4j/model/ExtraDataRecord.java @@ -0,0 +1,51 @@ +/* +* 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.model; + +public class ExtraDataRecord { + + private long header; + + private int sizeOfData; + + private byte[] data; + + public long getHeader() { + return header; + } + + public void setHeader(long header) { + this.header = header; + } + + public int getSizeOfData() { + return sizeOfData; + } + + public void setSizeOfData(int sizeOfData) { + this.sizeOfData = sizeOfData; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + +} diff --git a/src/net/lingala/zip4j/model/FileHeader.java b/src/net/lingala/zip4j/model/FileHeader.java new file mode 100644 index 0000000..cd77cdf --- /dev/null +++ b/src/net/lingala/zip4j/model/FileHeader.java @@ -0,0 +1,369 @@ +/* +* 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.model; + +import java.util.ArrayList; + +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.progress.ProgressMonitor; +import net.lingala.zip4j.unzip.Unzip; +import net.lingala.zip4j.util.Zip4jUtil; + +public class FileHeader { + + private int signature; + + private int versionMadeBy; + + private int versionNeededToExtract; + + private byte[] generalPurposeFlag; + + private int compressionMethod; + + private int lastModFileTime; + + private long crc32; + + private byte[] crcBuff; + + private long compressedSize; + + private long uncompressedSize; + + private int fileNameLength; + + private int extraFieldLength; + + private int fileCommentLength; + + private int diskNumberStart; + + private byte[] internalFileAttr; + + private byte[] externalFileAttr; + + private long offsetLocalHeader; + + private String fileName; + + private String fileComment; + + private boolean isDirectory; + + private boolean isEncrypted; + + private int encryptionMethod; + + private char[] password; + + private boolean dataDescriptorExists; + + private Zip64ExtendedInfo zip64ExtendedInfo; + + private AESExtraDataRecord aesExtraDataRecord; + + private ArrayList extraDataRecords; + + private boolean fileNameUTF8Encoded; + + public FileHeader() { + encryptionMethod = -1; + crc32 = 0; + uncompressedSize = 0; + } + + public int getSignature() { + return signature; + } + + public void setSignature(int signature) { + this.signature = signature; + } + + public int getVersionMadeBy() { + return versionMadeBy; + } + + public void setVersionMadeBy(int versionMadeBy) { + this.versionMadeBy = versionMadeBy; + } + + public int getVersionNeededToExtract() { + return versionNeededToExtract; + } + + public void setVersionNeededToExtract(int versionNeededToExtract) { + this.versionNeededToExtract = versionNeededToExtract; + } + + public byte[] getGeneralPurposeFlag() { + return generalPurposeFlag; + } + + public void setGeneralPurposeFlag(byte[] generalPurposeFlag) { + this.generalPurposeFlag = generalPurposeFlag; + } + + public int getCompressionMethod() { + return compressionMethod; + } + + public void setCompressionMethod(int compressionMethod) { + this.compressionMethod = compressionMethod; + } + + public int getLastModFileTime() { + return lastModFileTime; + } + + public void setLastModFileTime(int lastModFileTime) { + this.lastModFileTime = lastModFileTime; + } + + public long getCrc32() { + return crc32 & 0xffffffffL; + } + + public void setCrc32(long crc32) { + this.crc32 = crc32; + } + + public long getCompressedSize() { + return compressedSize; + } + + public void setCompressedSize(long compressedSize) { + this.compressedSize = compressedSize; + } + + public long getUncompressedSize() { + return uncompressedSize; + } + + public void setUncompressedSize(long uncompressedSize) { + this.uncompressedSize = uncompressedSize; + } + + public int getFileNameLength() { + return fileNameLength; + } + + public void setFileNameLength(int fileNameLength) { + this.fileNameLength = fileNameLength; + } + + public int getExtraFieldLength() { + return extraFieldLength; + } + + public void setExtraFieldLength(int extraFieldLength) { + this.extraFieldLength = extraFieldLength; + } + + public int getFileCommentLength() { + return fileCommentLength; + } + + public void setFileCommentLength(int fileCommentLength) { + this.fileCommentLength = fileCommentLength; + } + + public int getDiskNumberStart() { + return diskNumberStart; + } + + public void setDiskNumberStart(int diskNumberStart) { + this.diskNumberStart = diskNumberStart; + } + + public byte[] getInternalFileAttr() { + return internalFileAttr; + } + + public void setInternalFileAttr(byte[] internalFileAttr) { + this.internalFileAttr = internalFileAttr; + } + + public byte[] getExternalFileAttr() { + return externalFileAttr; + } + + public void setExternalFileAttr(byte[] externalFileAttr) { + this.externalFileAttr = externalFileAttr; + } + + public long getOffsetLocalHeader() { + return offsetLocalHeader; + } + + public void setOffsetLocalHeader(long offsetLocalHeader) { + this.offsetLocalHeader = offsetLocalHeader; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getFileComment() { + return fileComment; + } + + public void setFileComment(String fileComment) { + this.fileComment = fileComment; + } + + public boolean isDirectory() { + return isDirectory; + } + + public void setDirectory(boolean isDirectory) { + this.isDirectory = isDirectory; + } + + /** + * Extracts file to the specified directory + * @param zipModel + * @param outPath + * @throws ZipException + */ + public void extractFile(ZipModel zipModel, String outPath, + ProgressMonitor progressMonitor, boolean runInThread) throws ZipException { + extractFile(zipModel, outPath, null, progressMonitor, runInThread); + } + + /** + * Extracts file to the specified directory using any + * user defined parameters in UnzipParameters + * @param zipModel + * @param outPath + * @param unzipParameters + * @throws ZipException + */ + public void extractFile(ZipModel zipModel, String outPath, + UnzipParameters unzipParameters, ProgressMonitor progressMonitor, boolean runInThread) throws ZipException { + extractFile(zipModel, outPath, unzipParameters, null, progressMonitor, runInThread); + } + + /** + * Extracts file to the specified directory using any + * user defined parameters in UnzipParameters. Output file name + * will be overwritten with the value in newFileName. If this + * parameter is null, then file name will be the same as in + * FileHeader.getFileName + * @param zipModel + * @param outPath + * @param unzipParameters + * @throws ZipException + */ + public void extractFile(ZipModel zipModel, String outPath, + UnzipParameters unzipParameters, String newFileName, + ProgressMonitor progressMonitor, boolean runInThread) throws ZipException { + if (zipModel == null) { + throw new ZipException("input zipModel is null"); + } + + if (!Zip4jUtil.checkOutputFolder(outPath)) { + throw new ZipException("Invalid output path"); + } + + if (this == null) { + throw new ZipException("invalid file header"); + } + Unzip unzip = new Unzip(zipModel); + unzip.extractFile(this, outPath, unzipParameters, newFileName, progressMonitor, runInThread); + } + + public boolean isEncrypted() { + return isEncrypted; + } + + public void setEncrypted(boolean isEncrypted) { + this.isEncrypted = isEncrypted; + } + + public int getEncryptionMethod() { + return encryptionMethod; + } + + public void setEncryptionMethod(int encryptionMethod) { + this.encryptionMethod = encryptionMethod; + } + + public char[] getPassword() { + return password; + } + + public void setPassword(char[] password) { + this.password = password; + } + + public byte[] getCrcBuff() { + return crcBuff; + } + + public void setCrcBuff(byte[] crcBuff) { + this.crcBuff = crcBuff; + } + + public ArrayList getExtraDataRecords() { + return extraDataRecords; + } + + public void setExtraDataRecords(ArrayList extraDataRecords) { + this.extraDataRecords = extraDataRecords; + } + + public boolean isDataDescriptorExists() { + return dataDescriptorExists; + } + + public void setDataDescriptorExists(boolean dataDescriptorExists) { + this.dataDescriptorExists = dataDescriptorExists; + } + + public Zip64ExtendedInfo getZip64ExtendedInfo() { + return zip64ExtendedInfo; + } + + public void setZip64ExtendedInfo(Zip64ExtendedInfo zip64ExtendedInfo) { + this.zip64ExtendedInfo = zip64ExtendedInfo; + } + + public AESExtraDataRecord getAesExtraDataRecord() { + return aesExtraDataRecord; + } + + public void setAesExtraDataRecord(AESExtraDataRecord aesExtraDataRecord) { + this.aesExtraDataRecord = aesExtraDataRecord; + } + + public boolean isFileNameUTF8Encoded() { + return fileNameUTF8Encoded; + } + + public void setFileNameUTF8Encoded(boolean fileNameUTF8Encoded) { + this.fileNameUTF8Encoded = fileNameUTF8Encoded; + } + + + +} diff --git a/src/net/lingala/zip4j/model/LocalFileHeader.java b/src/net/lingala/zip4j/model/LocalFileHeader.java new file mode 100644 index 0000000..f5bcd4a --- /dev/null +++ b/src/net/lingala/zip4j/model/LocalFileHeader.java @@ -0,0 +1,261 @@ +/* +* 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.model; + +import java.util.ArrayList; + +public class LocalFileHeader { + + private int signature; + + private int versionNeededToExtract; + + private byte[] generalPurposeFlag; + + private int compressionMethod; + + private int lastModFileTime; + + private long crc32; + + private byte[] crcBuff; + + private long compressedSize; + + private long uncompressedSize; + + private int fileNameLength; + + private int extraFieldLength; + + private String fileName; + + private byte[] extraField; + + private long offsetStartOfData; + + private boolean isEncrypted; + + private int encryptionMethod; + + private char[] password; + + private ArrayList extraDataRecords; + + private Zip64ExtendedInfo zip64ExtendedInfo; + + private AESExtraDataRecord aesExtraDataRecord; + + private boolean dataDescriptorExists; + + private boolean writeComprSizeInZip64ExtraRecord; + + private boolean fileNameUTF8Encoded; + + public LocalFileHeader() { + encryptionMethod = -1; + writeComprSizeInZip64ExtraRecord = false; + crc32 = 0; + uncompressedSize = 0; + } + + public int getSignature() { + return signature; + } + + public void setSignature(int signature) { + this.signature = signature; + } + + public int getVersionNeededToExtract() { + return versionNeededToExtract; + } + + public void setVersionNeededToExtract(int versionNeededToExtract) { + this.versionNeededToExtract = versionNeededToExtract; + } + + public byte[] getGeneralPurposeFlag() { + return generalPurposeFlag; + } + + public void setGeneralPurposeFlag(byte[] generalPurposeFlag) { + this.generalPurposeFlag = generalPurposeFlag; + } + + public int getCompressionMethod() { + return compressionMethod; + } + + public void setCompressionMethod(int compressionMethod) { + this.compressionMethod = compressionMethod; + } + + public int getLastModFileTime() { + return lastModFileTime; + } + + public void setLastModFileTime(int lastModFileTime) { + this.lastModFileTime = lastModFileTime; + } + + public long getCrc32() { + return crc32; + } + + public void setCrc32(long crc32) { + this.crc32 = crc32; + } + + public long getCompressedSize() { + return compressedSize; + } + + public void setCompressedSize(long compressedSize) { + this.compressedSize = compressedSize; + } + + public long getUncompressedSize() { + return uncompressedSize; + } + + public void setUncompressedSize(long uncompressedSize) { + this.uncompressedSize = uncompressedSize; + } + + public int getFileNameLength() { + return fileNameLength; + } + + public void setFileNameLength(int fileNameLength) { + this.fileNameLength = fileNameLength; + } + + public int getExtraFieldLength() { + return extraFieldLength; + } + + public void setExtraFieldLength(int extraFieldLength) { + this.extraFieldLength = extraFieldLength; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public byte[] getExtraField() { + return extraField; + } + + public void setExtraField(byte[] extraField) { + this.extraField = extraField; + } + + public long getOffsetStartOfData() { + return offsetStartOfData; + } + + public void setOffsetStartOfData(long offsetStartOfData) { + this.offsetStartOfData = offsetStartOfData; + } + + public boolean isEncrypted() { + return isEncrypted; + } + + public void setEncrypted(boolean isEncrypted) { + this.isEncrypted = isEncrypted; + } + + public int getEncryptionMethod() { + return encryptionMethod; + } + + public void setEncryptionMethod(int encryptionMethod) { + this.encryptionMethod = encryptionMethod; + } + + public byte[] getCrcBuff() { + return crcBuff; + } + + public void setCrcBuff(byte[] crcBuff) { + this.crcBuff = crcBuff; + } + + public char[] getPassword() { + return password; + } + + public void setPassword(char[] password) { + this.password = password; + } + + public ArrayList getExtraDataRecords() { + return extraDataRecords; + } + + public void setExtraDataRecords(ArrayList extraDataRecords) { + this.extraDataRecords = extraDataRecords; + } + + public boolean isDataDescriptorExists() { + return dataDescriptorExists; + } + + public void setDataDescriptorExists(boolean dataDescriptorExists) { + this.dataDescriptorExists = dataDescriptorExists; + } + + public Zip64ExtendedInfo getZip64ExtendedInfo() { + return zip64ExtendedInfo; + } + + public void setZip64ExtendedInfo(Zip64ExtendedInfo zip64ExtendedInfo) { + this.zip64ExtendedInfo = zip64ExtendedInfo; + } + + public AESExtraDataRecord getAesExtraDataRecord() { + return aesExtraDataRecord; + } + + public void setAesExtraDataRecord(AESExtraDataRecord aesExtraDataRecord) { + this.aesExtraDataRecord = aesExtraDataRecord; + } + + public boolean isWriteComprSizeInZip64ExtraRecord() { + return writeComprSizeInZip64ExtraRecord; + } + + public void setWriteComprSizeInZip64ExtraRecord( + boolean writeComprSizeInZip64ExtraRecord) { + this.writeComprSizeInZip64ExtraRecord = writeComprSizeInZip64ExtraRecord; + } + + public boolean isFileNameUTF8Encoded() { + return fileNameUTF8Encoded; + } + + public void setFileNameUTF8Encoded(boolean fileNameUTF8Encoded) { + this.fileNameUTF8Encoded = fileNameUTF8Encoded; + } + +} diff --git a/src/net/lingala/zip4j/model/UnzipEngineParameters.java b/src/net/lingala/zip4j/model/UnzipEngineParameters.java new file mode 100644 index 0000000..e1f0673 --- /dev/null +++ b/src/net/lingala/zip4j/model/UnzipEngineParameters.java @@ -0,0 +1,88 @@ +/* +* 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.model; + +import java.io.FileOutputStream; + +import net.lingala.zip4j.crypto.IDecrypter; +import net.lingala.zip4j.unzip.UnzipEngine; + +public class UnzipEngineParameters { + + private ZipModel zipModel; + + private FileHeader fileHeader; + + private LocalFileHeader localFileHeader; + + private IDecrypter iDecryptor; + + private FileOutputStream outputStream; + + private UnzipEngine unzipEngine; + + public ZipModel getZipModel() { + return zipModel; + } + + public void setZipModel(ZipModel zipModel) { + this.zipModel = zipModel; + } + + public FileHeader getFileHeader() { + return fileHeader; + } + + public void setFileHeader(FileHeader fileHeader) { + this.fileHeader = fileHeader; + } + + public LocalFileHeader getLocalFileHeader() { + return localFileHeader; + } + + public void setLocalFileHeader(LocalFileHeader localFileHeader) { + this.localFileHeader = localFileHeader; + } + + public IDecrypter getIDecryptor() { + return iDecryptor; + } + + public void setIDecryptor(IDecrypter decrypter) { + iDecryptor = decrypter; + } + + public FileOutputStream getOutputStream() { + return outputStream; + } + + public void setOutputStream(FileOutputStream outputStream) { + this.outputStream = outputStream; + } + + public UnzipEngine getUnzipEngine() { + return unzipEngine; + } + + public void setUnzipEngine(UnzipEngine unzipEngine) { + this.unzipEngine = unzipEngine; + } + + + +} diff --git a/src/net/lingala/zip4j/model/UnzipParameters.java b/src/net/lingala/zip4j/model/UnzipParameters.java new file mode 100644 index 0000000..6f54370 --- /dev/null +++ b/src/net/lingala/zip4j/model/UnzipParameters.java @@ -0,0 +1,77 @@ +/* +* 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.model; + +public class UnzipParameters { + + private boolean ignoreReadOnlyFileAttribute; + private boolean ignoreHiddenFileAttribute; + private boolean ignoreArchiveFileAttribute; + private boolean ignoreSystemFileAttribute; + private boolean ignoreAllFileAttributes; + private boolean ignoreDateTimeAttributes; + + public boolean isIgnoreReadOnlyFileAttribute() { + return ignoreReadOnlyFileAttribute; + } + + public void setIgnoreReadOnlyFileAttribute(boolean ignoreReadOnlyFileAttribute) { + this.ignoreReadOnlyFileAttribute = ignoreReadOnlyFileAttribute; + } + + public boolean isIgnoreHiddenFileAttribute() { + return ignoreHiddenFileAttribute; + } + + public void setIgnoreHiddenFileAttribute(boolean ignoreHiddenFileAttribute) { + this.ignoreHiddenFileAttribute = ignoreHiddenFileAttribute; + } + + public boolean isIgnoreArchiveFileAttribute() { + return ignoreArchiveFileAttribute; + } + + public void setIgnoreArchiveFileAttribute(boolean ignoreArchiveFileAttribute) { + this.ignoreArchiveFileAttribute = ignoreArchiveFileAttribute; + } + + public boolean isIgnoreSystemFileAttribute() { + return ignoreSystemFileAttribute; + } + + public void setIgnoreSystemFileAttribute(boolean ignoreSystemFileAttribute) { + this.ignoreSystemFileAttribute = ignoreSystemFileAttribute; + } + + public boolean isIgnoreAllFileAttributes() { + return ignoreAllFileAttributes; + } + + public void setIgnoreAllFileAttributes(boolean ignoreAllFileAttributes) { + this.ignoreAllFileAttributes = ignoreAllFileAttributes; + } + + public boolean isIgnoreDateTimeAttributes() { + return ignoreDateTimeAttributes; + } + + public void setIgnoreDateTimeAttributes(boolean ignoreDateTimeAttributes) { + this.ignoreDateTimeAttributes = ignoreDateTimeAttributes; + } + + +} diff --git a/src/net/lingala/zip4j/model/Zip64EndCentralDirLocator.java b/src/net/lingala/zip4j/model/Zip64EndCentralDirLocator.java new file mode 100644 index 0000000..78393c4 --- /dev/null +++ b/src/net/lingala/zip4j/model/Zip64EndCentralDirLocator.java @@ -0,0 +1,64 @@ +/* +* 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.model; + +public class Zip64EndCentralDirLocator { + + private long signature; + + private int noOfDiskStartOfZip64EndOfCentralDirRec; + + private long offsetZip64EndOfCentralDirRec; + + private int totNumberOfDiscs; + + public long getSignature() { + return signature; + } + + public void setSignature(long signature) { + this.signature = signature; + } + + public int getNoOfDiskStartOfZip64EndOfCentralDirRec() { + return noOfDiskStartOfZip64EndOfCentralDirRec; + } + + public void setNoOfDiskStartOfZip64EndOfCentralDirRec( + int noOfDiskStartOfZip64EndOfCentralDirRec) { + this.noOfDiskStartOfZip64EndOfCentralDirRec = noOfDiskStartOfZip64EndOfCentralDirRec; + } + + public long getOffsetZip64EndOfCentralDirRec() { + return offsetZip64EndOfCentralDirRec; + } + + public void setOffsetZip64EndOfCentralDirRec(long offsetZip64EndOfCentralDirRec) { + this.offsetZip64EndOfCentralDirRec = offsetZip64EndOfCentralDirRec; + } + + public int getTotNumberOfDiscs() { + return totNumberOfDiscs; + } + + public void setTotNumberOfDiscs(int totNumberOfDiscs) { + this.totNumberOfDiscs = totNumberOfDiscs; + } + + + +} diff --git a/src/net/lingala/zip4j/model/Zip64EndCentralDirRecord.java b/src/net/lingala/zip4j/model/Zip64EndCentralDirRecord.java new file mode 100644 index 0000000..0677fce --- /dev/null +++ b/src/net/lingala/zip4j/model/Zip64EndCentralDirRecord.java @@ -0,0 +1,135 @@ +/* +* 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.model; + +public class Zip64EndCentralDirRecord { + + private long signature; + + private long sizeOfZip64EndCentralDirRec; + + private int versionMadeBy; + + private int versionNeededToExtract; + + private int noOfThisDisk; + + private int noOfThisDiskStartOfCentralDir; + + private long totNoOfEntriesInCentralDirOnThisDisk; + + private long totNoOfEntriesInCentralDir; + + private long sizeOfCentralDir; + + private long offsetStartCenDirWRTStartDiskNo; + + private byte[] extensibleDataSector; + + public long getSignature() { + return signature; + } + + public void setSignature(long signature) { + this.signature = signature; + } + + public long getSizeOfZip64EndCentralDirRec() { + return sizeOfZip64EndCentralDirRec; + } + + public void setSizeOfZip64EndCentralDirRec(long sizeOfZip64EndCentralDirRec) { + this.sizeOfZip64EndCentralDirRec = sizeOfZip64EndCentralDirRec; + } + + public int getVersionMadeBy() { + return versionMadeBy; + } + + public void setVersionMadeBy(int versionMadeBy) { + this.versionMadeBy = versionMadeBy; + } + + public int getVersionNeededToExtract() { + return versionNeededToExtract; + } + + public void setVersionNeededToExtract(int versionNeededToExtract) { + this.versionNeededToExtract = versionNeededToExtract; + } + + public int getNoOfThisDisk() { + return noOfThisDisk; + } + + public void setNoOfThisDisk(int noOfThisDisk) { + this.noOfThisDisk = noOfThisDisk; + } + + public int getNoOfThisDiskStartOfCentralDir() { + return noOfThisDiskStartOfCentralDir; + } + + public void setNoOfThisDiskStartOfCentralDir(int noOfThisDiskStartOfCentralDir) { + this.noOfThisDiskStartOfCentralDir = noOfThisDiskStartOfCentralDir; + } + + public long getTotNoOfEntriesInCentralDirOnThisDisk() { + return totNoOfEntriesInCentralDirOnThisDisk; + } + + public void setTotNoOfEntriesInCentralDirOnThisDisk( + long totNoOfEntriesInCentralDirOnThisDisk) { + this.totNoOfEntriesInCentralDirOnThisDisk = totNoOfEntriesInCentralDirOnThisDisk; + } + + public long getTotNoOfEntriesInCentralDir() { + return totNoOfEntriesInCentralDir; + } + + public void setTotNoOfEntriesInCentralDir(long totNoOfEntriesInCentralDir) { + this.totNoOfEntriesInCentralDir = totNoOfEntriesInCentralDir; + } + + public long getSizeOfCentralDir() { + return sizeOfCentralDir; + } + + public void setSizeOfCentralDir(long sizeOfCentralDir) { + this.sizeOfCentralDir = sizeOfCentralDir; + } + + public long getOffsetStartCenDirWRTStartDiskNo() { + return offsetStartCenDirWRTStartDiskNo; + } + + public void setOffsetStartCenDirWRTStartDiskNo( + long offsetStartCenDirWRTStartDiskNo) { + this.offsetStartCenDirWRTStartDiskNo = offsetStartCenDirWRTStartDiskNo; + } + + public byte[] getExtensibleDataSector() { + return extensibleDataSector; + } + + public void setExtensibleDataSector(byte[] extensibleDataSector) { + this.extensibleDataSector = extensibleDataSector; + } + + + +} diff --git a/src/net/lingala/zip4j/model/Zip64ExtendedInfo.java b/src/net/lingala/zip4j/model/Zip64ExtendedInfo.java new file mode 100644 index 0000000..f9a26f9 --- /dev/null +++ b/src/net/lingala/zip4j/model/Zip64ExtendedInfo.java @@ -0,0 +1,90 @@ +/* +* 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.model; + +public class Zip64ExtendedInfo { + + private int header; + + private int size; + + private long compressedSize; + + private long unCompressedSize; + + private long offsetLocalHeader; + + private int diskNumberStart; + + public Zip64ExtendedInfo() { + compressedSize = -1; + unCompressedSize = -1; + offsetLocalHeader = -1; + diskNumberStart = -1; + } + + public int getHeader() { + return header; + } + + public void setHeader(int header) { + this.header = header; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public long getCompressedSize() { + return compressedSize; + } + + public void setCompressedSize(long compressedSize) { + this.compressedSize = compressedSize; + } + + public long getUnCompressedSize() { + return unCompressedSize; + } + + public void setUnCompressedSize(long unCompressedSize) { + this.unCompressedSize = unCompressedSize; + } + + public long getOffsetLocalHeader() { + return offsetLocalHeader; + } + + public void setOffsetLocalHeader(long offsetLocalHeader) { + this.offsetLocalHeader = offsetLocalHeader; + } + + public int getDiskNumberStart() { + return diskNumberStart; + } + + public void setDiskNumberStart(int diskNumberStart) { + this.diskNumberStart = diskNumberStart; + } + + + +} diff --git a/src/net/lingala/zip4j/model/ZipModel.java b/src/net/lingala/zip4j/model/ZipModel.java new file mode 100644 index 0000000..ea91a33 --- /dev/null +++ b/src/net/lingala/zip4j/model/ZipModel.java @@ -0,0 +1,184 @@ +/* +* 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.model; + +import java.util.List; + +public class ZipModel implements Cloneable { + + private List localFileHeaderList; + + private List dataDescriptorList; + + private ArchiveExtraDataRecord archiveExtraDataRecord; + + private CentralDirectory centralDirectory; + + private EndCentralDirRecord endCentralDirRecord; + + private Zip64EndCentralDirLocator zip64EndCentralDirLocator; + + private Zip64EndCentralDirRecord zip64EndCentralDirRecord; + + private boolean splitArchive; + + private long splitLength; + + private String zipFile; + + private boolean isZip64Format; + + private boolean isNestedZipFile; + + private long start; + + private long end; + + private String fileNameCharset; + + public ZipModel() { + splitLength = -1; + } + + public List getLocalFileHeaderList() { + return localFileHeaderList; + } + + public void setLocalFileHeaderList(List localFileHeaderList) { + this.localFileHeaderList = localFileHeaderList; + } + + public List getDataDescriptorList() { + return dataDescriptorList; + } + + public void setDataDescriptorList(List dataDescriptorList) { + this.dataDescriptorList = dataDescriptorList; + } + + public CentralDirectory getCentralDirectory() { + return centralDirectory; + } + + public void setCentralDirectory(CentralDirectory centralDirectory) { + this.centralDirectory = centralDirectory; + } + + public EndCentralDirRecord getEndCentralDirRecord() { + return endCentralDirRecord; + } + + public void setEndCentralDirRecord(EndCentralDirRecord endCentralDirRecord) { + this.endCentralDirRecord = endCentralDirRecord; + } + + public ArchiveExtraDataRecord getArchiveExtraDataRecord() { + return archiveExtraDataRecord; + } + + public void setArchiveExtraDataRecord( + ArchiveExtraDataRecord archiveExtraDataRecord) { + this.archiveExtraDataRecord = archiveExtraDataRecord; + } + + public boolean isSplitArchive() { + return splitArchive; + } + + public void setSplitArchive(boolean splitArchive) { + this.splitArchive = splitArchive; + } + + public String getZipFile() { + return zipFile; + } + + public void setZipFile(String zipFile) { + this.zipFile = zipFile; + } + + public Zip64EndCentralDirLocator getZip64EndCentralDirLocator() { + return zip64EndCentralDirLocator; + } + + public void setZip64EndCentralDirLocator( + Zip64EndCentralDirLocator zip64EndCentralDirLocator) { + this.zip64EndCentralDirLocator = zip64EndCentralDirLocator; + } + + public Zip64EndCentralDirRecord getZip64EndCentralDirRecord() { + return zip64EndCentralDirRecord; + } + + public void setZip64EndCentralDirRecord( + Zip64EndCentralDirRecord zip64EndCentralDirRecord) { + this.zip64EndCentralDirRecord = zip64EndCentralDirRecord; + } + + public boolean isZip64Format() { + return isZip64Format; + } + + public void setZip64Format(boolean isZip64Format) { + this.isZip64Format = isZip64Format; + } + + public boolean isNestedZipFile() { + return isNestedZipFile; + } + + public void setNestedZipFile(boolean isNestedZipFile) { + this.isNestedZipFile = isNestedZipFile; + } + + public long getStart() { + return start; + } + + public void setStart(long start) { + this.start = start; + } + + public long getEnd() { + return end; + } + + public void setEnd(long end) { + this.end = end; + } + + public long getSplitLength() { + return splitLength; + } + + public void setSplitLength(long splitLength) { + this.splitLength = splitLength; + } + + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + public String getFileNameCharset() { + return fileNameCharset; + } + + public void setFileNameCharset(String fileNameCharset) { + this.fileNameCharset = fileNameCharset; + } + +} diff --git a/src/net/lingala/zip4j/model/ZipParameters.java b/src/net/lingala/zip4j/model/ZipParameters.java new file mode 100644 index 0000000..b39e5f8 --- /dev/null +++ b/src/net/lingala/zip4j/model/ZipParameters.java @@ -0,0 +1,195 @@ +/* +* 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.model; + +import java.util.TimeZone; + +import net.lingala.zip4j.util.InternalZipConstants; +import net.lingala.zip4j.util.Zip4jConstants; +import net.lingala.zip4j.util.Zip4jUtil; + +public class ZipParameters implements Cloneable { + + private int compressionMethod; + private int compressionLevel; + private boolean encryptFiles; + private int encryptionMethod; + private boolean readHiddenFiles; + private char[] password; + private int aesKeyStrength; + private boolean includeRootFolder; + private String rootFolderInZip; + private TimeZone timeZone; + private int sourceFileCRC; + private String defaultFolderPath; + private String fileNameInZip; + private boolean isSourceExternalStream; + + public ZipParameters() { + compressionMethod = Zip4jConstants.COMP_DEFLATE; + encryptFiles = false; + readHiddenFiles = true; + encryptionMethod = Zip4jConstants.ENC_NO_ENCRYPTION; + aesKeyStrength = -1; + includeRootFolder = true; + timeZone = TimeZone.getDefault(); + } + + public int getCompressionMethod() { + return compressionMethod; + } + + public void setCompressionMethod(int compressionMethod) { + this.compressionMethod = compressionMethod; + } + + public boolean isEncryptFiles() { + return encryptFiles; + } + + public void setEncryptFiles(boolean encryptFiles) { + this.encryptFiles = encryptFiles; + } + + public int getEncryptionMethod() { + return encryptionMethod; + } + + public void setEncryptionMethod(int encryptionMethod) { + this.encryptionMethod = encryptionMethod; + } + + public int getCompressionLevel() { + return compressionLevel; + } + + public void setCompressionLevel(int compressionLevel) { + this.compressionLevel = compressionLevel; + } + + public boolean isReadHiddenFiles() { + return readHiddenFiles; + } + + public void setReadHiddenFiles(boolean readHiddenFiles) { + this.readHiddenFiles = readHiddenFiles; + } + + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + public char[] getPassword() { + return password; + } + + /** + * Sets the password for the zip file or the file being added
+ * Note: For security reasons, usage of this method is discouraged. Use + * setPassword(char[]) instead. As strings are immutable, they cannot be wiped + * out from memory explicitly after usage. Therefore, usage of Strings to store + * passwords is discouraged. More info here: + * http://docs.oracle.com/javase/1.5.0/docs/guide/security/jce/JCERefGuide.html#PBEEx + * @param password + */ + public void setPassword(String password) { + if (password == null) return; + setPassword(password.toCharArray()); + } + + public void setPassword(char[] password) { + this.password = password; + } + + public int getAesKeyStrength() { + return aesKeyStrength; + } + + public void setAesKeyStrength(int aesKeyStrength) { + this.aesKeyStrength = aesKeyStrength; + } + + public boolean isIncludeRootFolder() { + return includeRootFolder; + } + + public void setIncludeRootFolder(boolean includeRootFolder) { + this.includeRootFolder = includeRootFolder; + } + + public String getRootFolderInZip() { + return rootFolderInZip; + } + + public void setRootFolderInZip(String rootFolderInZip) { + if (Zip4jUtil.isStringNotNullAndNotEmpty(rootFolderInZip)) { + + if (!rootFolderInZip.endsWith("\\") && !rootFolderInZip.endsWith("/")) { + rootFolderInZip = rootFolderInZip + InternalZipConstants.FILE_SEPARATOR; + } + + rootFolderInZip = rootFolderInZip.replaceAll("\\\\", "/"); + +// if (rootFolderInZip.endsWith("/")) { +// rootFolderInZip = rootFolderInZip.substring(0, rootFolderInZip.length() - 1); +// rootFolderInZip = rootFolderInZip + "\\"; +// } + } + this.rootFolderInZip = rootFolderInZip; + } + + public TimeZone getTimeZone() { + return timeZone; + } + + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + public int getSourceFileCRC() { + return sourceFileCRC; + } + + public void setSourceFileCRC(int sourceFileCRC) { + this.sourceFileCRC = sourceFileCRC; + } + + public String getDefaultFolderPath() { + return defaultFolderPath; + } + + public void setDefaultFolderPath(String defaultFolderPath) { + this.defaultFolderPath = defaultFolderPath; + } + + public String getFileNameInZip() { + return fileNameInZip; + } + + public void setFileNameInZip(String fileNameInZip) { + this.fileNameInZip = fileNameInZip; + } + + public boolean isSourceExternalStream() { + return isSourceExternalStream; + } + + public void setSourceExternalStream(boolean isSourceExternalStream) { + this.isSourceExternalStream = isSourceExternalStream; + } + +} diff --git a/src/net/lingala/zip4j/progress/ProgressMonitor.java b/src/net/lingala/zip4j/progress/ProgressMonitor.java new file mode 100644 index 0000000..314144e --- /dev/null +++ b/src/net/lingala/zip4j/progress/ProgressMonitor.java @@ -0,0 +1,180 @@ +/* +* 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.progress; + +import net.lingala.zip4j.exception.ZipException; + +/** + * If Zip4j is set to run in thread mode, this class helps retrieve current progress + * + */ +public class ProgressMonitor { + + private int state; + private long totalWork; + private long workCompleted; + private int percentDone; + private int currentOperation; + private String fileName; + private int result; + private Throwable exception; + private boolean cancelAllTasks; + private boolean pause; + + //Progress monitor States + public static final int STATE_READY = 0; + public static final int STATE_BUSY = 1; + + //Progress monitor result codes + public static final int RESULT_SUCCESS = 0; + public static final int RESULT_WORKING = 1; + public static final int RESULT_ERROR = 2; + public static final int RESULT_CANCELLED = 3; + + //Operation Types + public static final int OPERATION_NONE = -1; + public static final int OPERATION_ADD = 0; + public static final int OPERATION_EXTRACT = 1; + public static final int OPERATION_REMOVE = 2; + public static final int OPERATION_CALC_CRC = 3; + public static final int OPERATION_MERGE = 4; + + public ProgressMonitor() { + reset(); + percentDone = 0; + } + + public int getState() { + return state; + } + + public void setState(int state) { + this.state = state; + } + + public long getTotalWork() { + return totalWork; + } + + public void setTotalWork(long totalWork) { + this.totalWork = totalWork; + } + + public long getWorkCompleted() { + return workCompleted; + } + + public void updateWorkCompleted(long workCompleted) { + this.workCompleted += workCompleted; + + if (totalWork > 0) { + percentDone = (int)((this.workCompleted*100/totalWork)); + if (percentDone > 100) { + percentDone = 100; + } + } + while (pause) { + try { + Thread.sleep(150); + } catch (InterruptedException e) { + //Do nothing + } + } + } + + public int getPercentDone() { + return percentDone; + } + + public void setPercentDone(int percentDone) { + this.percentDone = percentDone; + } + + public int getResult() { + return result; + } + + public void setResult(int result) { + this.result = result; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public int getCurrentOperation() { + return currentOperation; + } + + public void setCurrentOperation(int currentOperation) { + this.currentOperation = currentOperation; + } + + public Throwable getException() { + return exception; + } + + public void setException(Throwable exception) { + this.exception = exception; + } + + public void endProgressMonitorSuccess() throws ZipException { + reset(); + result = ProgressMonitor.RESULT_SUCCESS; + } + + public void endProgressMonitorError(Throwable e) throws ZipException { + reset(); + result = ProgressMonitor.RESULT_ERROR; + exception = e; + } + + public void reset() { + currentOperation = OPERATION_NONE; + state = STATE_READY; + fileName = null; + totalWork = 0; + workCompleted = 0; + percentDone = 0; + } + + public void fullReset() { + reset(); + exception = null; + result = RESULT_SUCCESS; + } + + public boolean isCancelAllTasks() { + return cancelAllTasks; + } + + public void cancelAllTasks() { + this.cancelAllTasks = true; + } + + public boolean isPause() { + return pause; + } + + public void setPause(boolean pause) { + this.pause = pause; + } +} diff --git a/src/net/lingala/zip4j/unzip/Unzip.java b/src/net/lingala/zip4j/unzip/Unzip.java new file mode 100644 index 0000000..4e9be03 --- /dev/null +++ b/src/net/lingala/zip4j/unzip/Unzip.java @@ -0,0 +1,231 @@ +/* +* 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.util.ArrayList; + +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.io.ZipInputStream; +import net.lingala.zip4j.model.CentralDirectory; +import net.lingala.zip4j.model.FileHeader; +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.Zip4jUtil; + +public class Unzip { + + private ZipModel zipModel; + + public Unzip(ZipModel zipModel) throws ZipException { + + if (zipModel == null) { + throw new ZipException("ZipModel is null"); + } + + this.zipModel = zipModel; + } + + public void extractAll(final UnzipParameters unzipParameters, final String outPath, + final ProgressMonitor progressMonitor, boolean runInThread) throws ZipException { + + CentralDirectory centralDirectory = zipModel.getCentralDirectory(); + + if (centralDirectory == null || + centralDirectory.getFileHeaders() == null) { + throw new ZipException("invalid central directory in zipModel"); + } + + final ArrayList fileHeaders = centralDirectory.getFileHeaders(); + + progressMonitor.setCurrentOperation(ProgressMonitor.OPERATION_EXTRACT); + progressMonitor.setTotalWork(calculateTotalWork(fileHeaders)); + progressMonitor.setState(ProgressMonitor.STATE_BUSY); + + if (runInThread) { + Thread thread = new Thread(InternalZipConstants.THREAD_NAME) { + public void run() { + try { + initExtractAll(fileHeaders, unzipParameters, progressMonitor, outPath); + progressMonitor.endProgressMonitorSuccess(); + } catch (ZipException e) { + } + } + }; + thread.start(); + } else { + initExtractAll(fileHeaders, unzipParameters, progressMonitor, outPath); + } + + } + + private void initExtractAll(ArrayList fileHeaders, UnzipParameters unzipParameters, + ProgressMonitor progressMonitor, String outPath) throws ZipException { + + for (int i = 0; i < fileHeaders.size(); i++) { + FileHeader fileHeader = (FileHeader)fileHeaders.get(i); + initExtractFile(fileHeader, outPath, unzipParameters, null, progressMonitor); + if (progressMonitor.isCancelAllTasks()) { + progressMonitor.setResult(ProgressMonitor.RESULT_CANCELLED); + progressMonitor.setState(ProgressMonitor.STATE_READY); + return; + } + } + } + + public void extractFile(final FileHeader fileHeader, final String outPath, + final UnzipParameters unzipParameters, final String newFileName, + final ProgressMonitor progressMonitor, boolean runInThread) throws ZipException { + if (fileHeader == null) { + throw new ZipException("fileHeader is null"); + } + + progressMonitor.setCurrentOperation(ProgressMonitor.OPERATION_EXTRACT); + progressMonitor.setTotalWork(fileHeader.getCompressedSize()); + progressMonitor.setState(ProgressMonitor.STATE_BUSY); + progressMonitor.setPercentDone(0); + progressMonitor.setFileName(fileHeader.getFileName()); + + if (runInThread) { + Thread thread = new Thread(InternalZipConstants.THREAD_NAME) { + public void run() { + try { + initExtractFile(fileHeader, outPath, unzipParameters, newFileName, progressMonitor); + progressMonitor.endProgressMonitorSuccess(); + } catch (ZipException e) { + } + } + }; + thread.start(); + } else { + initExtractFile(fileHeader, outPath, unzipParameters, newFileName, progressMonitor); + progressMonitor.endProgressMonitorSuccess(); + } + + } + + private void initExtractFile(FileHeader fileHeader, String outPath, + UnzipParameters unzipParameters, String newFileName, ProgressMonitor progressMonitor) throws ZipException { + + if (fileHeader == null) { + throw new ZipException("fileHeader is null"); + } + + try { + progressMonitor.setFileName(fileHeader.getFileName()); + + if (!outPath.endsWith(InternalZipConstants.FILE_SEPARATOR)) { + outPath += InternalZipConstants.FILE_SEPARATOR; + } + + // If file header is a directory, then check if the directory exists + // If not then create a directory and return + if (fileHeader.isDirectory()) { + try { + String fileName = fileHeader.getFileName(); + if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileName)) { + return; + } + String completePath = outPath + fileName; + File file = new File(completePath); + if (!file.exists()) { + file.mkdirs(); + } + } catch (Exception e) { + progressMonitor.endProgressMonitorError(e); + throw new ZipException(e); + } + } else { + //Create Directories + checkOutputDirectoryStructure(fileHeader, outPath, newFileName); + + UnzipEngine unzipEngine = new UnzipEngine(zipModel, fileHeader); + try { + unzipEngine.unzipFile(progressMonitor, outPath, newFileName, unzipParameters); + } catch (Exception e) { + progressMonitor.endProgressMonitorError(e); + throw new ZipException(e); + } + } + } catch (ZipException e) { + progressMonitor.endProgressMonitorError(e); + throw e; + } catch (Exception e) { + progressMonitor.endProgressMonitorError(e); + throw new ZipException(e); + } + } + + public ZipInputStream getInputStream(FileHeader fileHeader) throws ZipException { + UnzipEngine unzipEngine = new UnzipEngine(zipModel, fileHeader); + return unzipEngine.getInputStream(); + } + + private void checkOutputDirectoryStructure(FileHeader fileHeader, String outPath, String newFileName) throws ZipException { + if (fileHeader == null || !Zip4jUtil.isStringNotNullAndNotEmpty(outPath)) { + throw new ZipException("Cannot check output directory structure...one of the parameters was null"); + } + + String fileName = fileHeader.getFileName(); + + if (Zip4jUtil.isStringNotNullAndNotEmpty(newFileName)) { + fileName = newFileName; + } + + if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileName)) { + // Do nothing + return; + } + + String compOutPath = outPath + fileName; + try { + File file = new File(compOutPath); + String parentDir = file.getParent(); + File parentDirFile = new File(parentDir); + if (!parentDirFile.exists()) { + parentDirFile.mkdirs(); + } + } catch (Exception e) { + throw new ZipException(e); + } + } + + private long calculateTotalWork(ArrayList fileHeaders) throws ZipException { + + if (fileHeaders == null) { + throw new ZipException("fileHeaders is null, cannot calculate total work"); + } + + long totalWork = 0; + + for (int i = 0; i < fileHeaders.size(); i++) { + FileHeader fileHeader = (FileHeader)fileHeaders.get(i); + if (fileHeader.getZip64ExtendedInfo() != null && + fileHeader.getZip64ExtendedInfo().getUnCompressedSize() > 0) { + totalWork += fileHeader.getZip64ExtendedInfo().getCompressedSize(); + } else { + totalWork += fileHeader.getCompressedSize(); + } + + } + + return totalWork; + } + +} diff --git a/src/net/lingala/zip4j/unzip/UnzipEngine.java b/src/net/lingala/zip4j/unzip/UnzipEngine.java new file mode 100644 index 0000000..86d3f89 --- /dev/null +++ b/src/net/lingala/zip4j/unzip/UnzipEngine.java @@ -0,0 +1,521 @@ +/* +* 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; + } +} diff --git a/src/net/lingala/zip4j/unzip/UnzipUtil.java b/src/net/lingala/zip4j/unzip/UnzipUtil.java new file mode 100644 index 0000000..4a55c22 --- /dev/null +++ b/src/net/lingala/zip4j/unzip/UnzipUtil.java @@ -0,0 +1,113 @@ +package net.lingala.zip4j.unzip; + +import java.io.File; + +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.model.FileHeader; +import net.lingala.zip4j.model.UnzipParameters; +import net.lingala.zip4j.util.InternalZipConstants; +import net.lingala.zip4j.util.Zip4jUtil; + +public class UnzipUtil { + + public static void applyFileAttributes(FileHeader fileHeader, File file) throws ZipException { + applyFileAttributes(fileHeader, file, null); + } + + public static void applyFileAttributes(FileHeader fileHeader, File file, + UnzipParameters unzipParameters) throws ZipException{ + + if (fileHeader == null) { + throw new ZipException("cannot set file properties: file header is null"); + } + + if (file == null) { + throw new ZipException("cannot set file properties: output file is null"); + } + + if (!Zip4jUtil.checkFileExists(file)) { + throw new ZipException("cannot set file properties: file doesnot exist"); + } + + if (unzipParameters == null || !unzipParameters.isIgnoreDateTimeAttributes()) { + setFileLastModifiedTime(fileHeader, file); + } + + if (unzipParameters == null) { + setFileAttributes(fileHeader, file, true, true, true, true); + } else { + if (unzipParameters.isIgnoreAllFileAttributes()) { + setFileAttributes(fileHeader, file, false, false, false, false); + } else { + setFileAttributes(fileHeader, file, !unzipParameters.isIgnoreReadOnlyFileAttribute(), + !unzipParameters.isIgnoreHiddenFileAttribute(), + !unzipParameters.isIgnoreArchiveFileAttribute(), + !unzipParameters.isIgnoreSystemFileAttribute()); + } + } + } + + private static void setFileAttributes(FileHeader fileHeader, File file, boolean setReadOnly, + boolean setHidden, boolean setArchive, boolean setSystem) throws ZipException { + if (fileHeader == null) { + throw new ZipException("invalid file header. cannot set file attributes"); + } + + byte[] externalAttrbs = fileHeader.getExternalFileAttr(); + if (externalAttrbs == null) { + return; + } + + int atrrib = externalAttrbs[0]; + switch (atrrib) { + case InternalZipConstants.FILE_MODE_READ_ONLY: + if (setReadOnly) Zip4jUtil.setFileReadOnly(file); + break; + case InternalZipConstants.FILE_MODE_HIDDEN: + case InternalZipConstants.FOLDER_MODE_HIDDEN: + if (setHidden) Zip4jUtil.setFileHidden(file); + break; + case InternalZipConstants.FILE_MODE_ARCHIVE: + case InternalZipConstants.FOLDER_MODE_ARCHIVE: + if (setArchive) Zip4jUtil.setFileArchive(file); + break; + case InternalZipConstants.FILE_MODE_READ_ONLY_HIDDEN: + if (setReadOnly) Zip4jUtil.setFileReadOnly(file); + if (setHidden) Zip4jUtil.setFileHidden(file); + break; + case InternalZipConstants.FILE_MODE_READ_ONLY_ARCHIVE: + if (setArchive) Zip4jUtil.setFileArchive(file); + if (setReadOnly) Zip4jUtil.setFileReadOnly(file); + break; + case InternalZipConstants.FILE_MODE_HIDDEN_ARCHIVE: + case InternalZipConstants.FOLDER_MODE_HIDDEN_ARCHIVE: + if (setArchive) Zip4jUtil.setFileArchive(file); + if (setHidden) Zip4jUtil.setFileHidden(file); + break; + case InternalZipConstants.FILE_MODE_READ_ONLY_HIDDEN_ARCHIVE: + if (setArchive) Zip4jUtil.setFileArchive(file); + if (setReadOnly) Zip4jUtil.setFileReadOnly(file); + if (setHidden) Zip4jUtil.setFileHidden(file); + break; + case InternalZipConstants.FILE_MODE_SYSTEM: + if (setReadOnly) Zip4jUtil.setFileReadOnly(file); + if (setHidden) Zip4jUtil.setFileHidden(file); + if (setSystem) Zip4jUtil.setFileSystemMode(file); + break; + default: + //do nothing + break; + } + } + + private static void setFileLastModifiedTime(FileHeader fileHeader, File file) throws ZipException { + if (fileHeader.getLastModFileTime() <= 0) { + return; + } + + if (file.exists()) { + file.setLastModified(Zip4jUtil.dosToJavaTme(fileHeader.getLastModFileTime())); + } + } + +} diff --git a/src/net/lingala/zip4j/util/ArchiveMaintainer.java b/src/net/lingala/zip4j/util/ArchiveMaintainer.java new file mode 100644 index 0000000..a532dd2 --- /dev/null +++ b/src/net/lingala/zip4j/util/ArchiveMaintainer.java @@ -0,0 +1,727 @@ +/* +* 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.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; + +import net.lingala.zip4j.core.HeaderReader; +import net.lingala.zip4j.core.HeaderWriter; +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.io.SplitOutputStream; +import net.lingala.zip4j.model.FileHeader; +import net.lingala.zip4j.model.LocalFileHeader; +import net.lingala.zip4j.model.Zip64EndCentralDirLocator; +import net.lingala.zip4j.model.Zip64EndCentralDirRecord; +import net.lingala.zip4j.model.ZipModel; +import net.lingala.zip4j.progress.ProgressMonitor; + +public class ArchiveMaintainer { + + public ArchiveMaintainer() { + } + + public HashMap removeZipFile(final ZipModel zipModel, + final FileHeader fileHeader, final ProgressMonitor progressMonitor, boolean runInThread) throws ZipException { + + if (runInThread) { + Thread thread = new Thread(InternalZipConstants.THREAD_NAME) { + public void run() { + try { + initRemoveZipFile(zipModel, fileHeader, progressMonitor); + progressMonitor.endProgressMonitorSuccess(); + } catch (ZipException e) { + } + } + }; + thread.start(); + return null; + } else { + HashMap retMap = initRemoveZipFile(zipModel, fileHeader, progressMonitor); + progressMonitor.endProgressMonitorSuccess(); + return retMap; + } + + } + + public HashMap initRemoveZipFile(ZipModel zipModel, + FileHeader fileHeader, ProgressMonitor progressMonitor) throws ZipException { + + if (fileHeader == null || zipModel == null) { + throw new ZipException("input parameters is null in maintain zip file, cannot remove file from archive"); + } + + OutputStream outputStream = null; + File zipFile = null; + RandomAccessFile inputStream = null; + boolean successFlag = false; + String tmpZipFileName = null; + HashMap retMap = new HashMap(); + + try { + int indexOfFileHeader = Zip4jUtil.getIndexOfFileHeader(zipModel, fileHeader); + + if (indexOfFileHeader < 0) { + throw new ZipException("file header not found in zip model, cannot remove file"); + } + + if (zipModel.isSplitArchive()) { + throw new ZipException("This is a split archive. Zip file format does not allow updating split/spanned files"); + } + + long currTime = System.currentTimeMillis(); + tmpZipFileName = zipModel.getZipFile() + currTime%1000; + File tmpFile = new File(tmpZipFileName); + + while (tmpFile.exists()) { + currTime = System.currentTimeMillis(); + tmpZipFileName = zipModel.getZipFile() + currTime%1000; + tmpFile = new File(tmpZipFileName); + } + + try { + outputStream = new SplitOutputStream(new File(tmpZipFileName)); + } catch (FileNotFoundException e1) { + throw new ZipException(e1); + } + + zipFile = new File(zipModel.getZipFile()); + + inputStream = createFileHandler(zipModel, InternalZipConstants.READ_MODE); + + HeaderReader headerReader = new HeaderReader(inputStream); + LocalFileHeader localFileHeader = headerReader.readLocalFileHeader(fileHeader); + if (localFileHeader == null) { + throw new ZipException("invalid local file header, cannot remove file from archive"); + } + + long offsetLocalFileHeader = fileHeader.getOffsetLocalHeader(); + + if (fileHeader.getZip64ExtendedInfo() != null && + fileHeader.getZip64ExtendedInfo().getOffsetLocalHeader() != -1) { + offsetLocalFileHeader = fileHeader.getZip64ExtendedInfo().getOffsetLocalHeader(); + } + + long offsetEndOfCompressedFile = -1; + + long offsetStartCentralDir = zipModel.getEndCentralDirRecord().getOffsetOfStartOfCentralDir(); + if (zipModel.isZip64Format()) { + if (zipModel.getZip64EndCentralDirRecord() != null) { + offsetStartCentralDir = zipModel.getZip64EndCentralDirRecord().getOffsetStartCenDirWRTStartDiskNo(); + } + } + + ArrayList fileHeaderList = zipModel.getCentralDirectory().getFileHeaders(); + + if (indexOfFileHeader == fileHeaderList.size() - 1) { + offsetEndOfCompressedFile = offsetStartCentralDir - 1; + } else { + FileHeader nextFileHeader = (FileHeader)fileHeaderList.get(indexOfFileHeader + 1); + if (nextFileHeader != null) { + offsetEndOfCompressedFile = nextFileHeader.getOffsetLocalHeader() - 1; + if (nextFileHeader.getZip64ExtendedInfo() != null && + nextFileHeader.getZip64ExtendedInfo().getOffsetLocalHeader() != -1) { + offsetEndOfCompressedFile = nextFileHeader.getZip64ExtendedInfo().getOffsetLocalHeader() - 1; + } + } + } + + if (offsetLocalFileHeader < 0 || offsetEndOfCompressedFile < 0) { + throw new ZipException("invalid offset for start and end of local file, cannot remove file"); + } + + if(indexOfFileHeader == 0) { + if (zipModel.getCentralDirectory().getFileHeaders().size() > 1) { + // if this is the only file and it is deleted then no need to do this + copyFile(inputStream, outputStream, offsetEndOfCompressedFile + 1, offsetStartCentralDir, progressMonitor); + } + } else if (indexOfFileHeader == fileHeaderList.size() - 1) { + copyFile(inputStream, outputStream, 0, offsetLocalFileHeader, progressMonitor); + } else { + copyFile(inputStream, outputStream, 0, offsetLocalFileHeader, progressMonitor); + copyFile(inputStream, outputStream, offsetEndOfCompressedFile + 1, offsetStartCentralDir, progressMonitor); + } + + if (progressMonitor.isCancelAllTasks()) { + progressMonitor.setResult(ProgressMonitor.RESULT_CANCELLED); + progressMonitor.setState(ProgressMonitor.STATE_READY); + return null; + } + + zipModel.getEndCentralDirRecord().setOffsetOfStartOfCentralDir(((SplitOutputStream)outputStream).getFilePointer()); + zipModel.getEndCentralDirRecord().setTotNoOfEntriesInCentralDir( + zipModel.getEndCentralDirRecord().getTotNoOfEntriesInCentralDir() - 1); + zipModel.getEndCentralDirRecord().setTotNoOfEntriesInCentralDirOnThisDisk( + zipModel.getEndCentralDirRecord().getTotNoOfEntriesInCentralDirOnThisDisk() - 1); + + zipModel.getCentralDirectory().getFileHeaders().remove(indexOfFileHeader); + + for (int i = indexOfFileHeader; i < zipModel.getCentralDirectory().getFileHeaders().size(); i++) { + long offsetLocalHdr = ((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i)).getOffsetLocalHeader(); + if (((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i)).getZip64ExtendedInfo() != null && + ((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i)).getZip64ExtendedInfo().getOffsetLocalHeader() != -1) { + offsetLocalHdr = ((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i)).getZip64ExtendedInfo().getOffsetLocalHeader(); + } + + ((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i)).setOffsetLocalHeader( + offsetLocalHdr - (offsetEndOfCompressedFile - offsetLocalFileHeader) - 1); + } + + HeaderWriter headerWriter = new HeaderWriter(); + headerWriter.finalizeZipFile(zipModel, outputStream); + + successFlag = true; + + retMap.put(InternalZipConstants.OFFSET_CENTRAL_DIR, + Long.toString(zipModel.getEndCentralDirRecord().getOffsetOfStartOfCentralDir())); + + } catch (ZipException e) { + progressMonitor.endProgressMonitorError(e); + throw e; + } catch (Exception e) { + progressMonitor.endProgressMonitorError(e); + throw new ZipException(e); + } finally { + try { + if (inputStream != null) + inputStream.close(); + if (outputStream != null) + outputStream.close(); + } catch (IOException e) { + throw new ZipException("cannot close input stream or output stream when trying to delete a file from zip file"); + } + + if (successFlag) { + restoreFileName(zipFile, tmpZipFileName); + } else { + File newZipFile = new File(tmpZipFileName); + newZipFile.delete(); + } + } + + return retMap; + } + + private void restoreFileName(File zipFile, String tmpZipFileName) throws ZipException { + if (zipFile.delete()) + { + File newZipFile = new File(tmpZipFileName); + if (!newZipFile.renameTo(zipFile)) { + throw new ZipException("cannot rename modified zip file"); + } + } else { + throw new ZipException("cannot delete old zip file"); + } + } + + private void copyFile(RandomAccessFile inputStream, + OutputStream outputStream, long start, long end, ProgressMonitor progressMonitor) throws ZipException { + + if (inputStream == null || outputStream == null) { + throw new ZipException("input or output stream is null, cannot copy file"); + } + + if (start < 0) { + throw new ZipException("starting offset is negative, cannot copy file"); + } + + if (end < 0) { + throw new ZipException("end offset is negative, cannot copy file"); + } + + if (start > end) { + throw new ZipException("start offset is greater than end offset, cannot copy file"); + } + + if (start == end) { + return; + } + + if (progressMonitor.isCancelAllTasks()) { + progressMonitor.setResult(ProgressMonitor.RESULT_CANCELLED); + progressMonitor.setState(ProgressMonitor.STATE_READY); + return; + } + + try { + inputStream.seek(start); + + int readLen = -2; + byte[] buff; + long bytesRead = 0; + long bytesToRead = end - start; + + if ((end - start) < InternalZipConstants.BUFF_SIZE) { + buff = new byte[(int)(end - start)]; + } else { + buff = new byte[InternalZipConstants.BUFF_SIZE]; + } + + while ((readLen = inputStream.read(buff)) != -1) { + outputStream.write(buff, 0, readLen); + + progressMonitor.updateWorkCompleted(readLen); + if (progressMonitor.isCancelAllTasks()) { + progressMonitor.setResult(ProgressMonitor.RESULT_CANCELLED); + return; + } + + bytesRead += readLen; + + if(bytesRead == bytesToRead) { + break; + } else if (bytesRead + buff.length > bytesToRead) { + buff = new byte[(int)(bytesToRead - bytesRead)]; + } + } + + } catch (IOException e) { + throw new ZipException(e); + } catch (Exception e) { + throw new ZipException(e); + } + } + + private RandomAccessFile createFileHandler(ZipModel zipModel, String mode) throws ZipException { + if (zipModel == null || !Zip4jUtil.isStringNotNullAndNotEmpty(zipModel.getZipFile())) { + throw new ZipException("input parameter is null in getFilePointer, cannot create file handler to remove file"); + } + + try { + return new RandomAccessFile(new File(zipModel.getZipFile()), mode); + } catch (FileNotFoundException e) { + throw new ZipException(e); + } + } + + /** + * Merges split Zip files into a single Zip file + * @param zipModel + * @throws ZipException + */ + public void mergeSplitZipFiles(final ZipModel zipModel, final File outputZipFile, + final ProgressMonitor progressMonitor, boolean runInThread) throws ZipException { + if (runInThread) { + Thread thread = new Thread(InternalZipConstants.THREAD_NAME) { + public void run() { + try { + initMergeSplitZipFile(zipModel, outputZipFile, progressMonitor); + } catch (ZipException e) { + } + } + }; + thread.start(); + } else { + initMergeSplitZipFile(zipModel, outputZipFile, progressMonitor); + } + } + + private void initMergeSplitZipFile(ZipModel zipModel, File outputZipFile, + ProgressMonitor progressMonitor) throws ZipException { + if (zipModel == null) { + ZipException e = new ZipException("one of the input parameters is null, cannot merge split zip file"); + progressMonitor.endProgressMonitorError(e); + throw e; + } + + if (!zipModel.isSplitArchive()) { + ZipException e = new ZipException("archive not a split zip file"); + progressMonitor.endProgressMonitorError(e); + throw e; + } + + OutputStream outputStream = null; + RandomAccessFile inputStream = null; + ArrayList fileSizeList = new ArrayList(); + long totBytesWritten = 0; + boolean splitSigRemoved = false; + try { + + int totNoOfSplitFiles = zipModel.getEndCentralDirRecord().getNoOfThisDisk(); + + if (totNoOfSplitFiles <= 0) { + throw new ZipException("corrupt zip model, archive not a split zip file"); + } + + outputStream = prepareOutputStreamForMerge(outputZipFile); + for (int i = 0; i <= totNoOfSplitFiles; i++) { + inputStream = createSplitZipFileHandler(zipModel, i); + + int start = 0; + Long end = new Long(inputStream.length()); + + if (i == 0) { + if (zipModel.getCentralDirectory() != null && + zipModel.getCentralDirectory().getFileHeaders() != null && + zipModel.getCentralDirectory().getFileHeaders().size() > 0) { + byte[] buff = new byte[4]; + inputStream.seek(0); + inputStream.read(buff); + if (Raw.readIntLittleEndian(buff, 0) == InternalZipConstants.SPLITSIG) { + start = 4; + splitSigRemoved = true; + } + } + } + + if (i == totNoOfSplitFiles) { + end = new Long(zipModel.getEndCentralDirRecord().getOffsetOfStartOfCentralDir()); + } + + copyFile(inputStream, outputStream, start, end.longValue(), progressMonitor); + totBytesWritten += (end.longValue() - start); + if (progressMonitor.isCancelAllTasks()) { + progressMonitor.setResult(ProgressMonitor.RESULT_CANCELLED); + progressMonitor.setState(ProgressMonitor.STATE_READY); + return; + } + + fileSizeList.add(end); + + try { + inputStream.close(); + } catch (IOException e) { + //ignore + } + } + + ZipModel newZipModel = (ZipModel)zipModel.clone(); + newZipModel.getEndCentralDirRecord().setOffsetOfStartOfCentralDir(totBytesWritten); + + updateSplitZipModel(newZipModel, fileSizeList, splitSigRemoved); + + HeaderWriter headerWriter = new HeaderWriter(); + headerWriter.finalizeZipFileWithoutValidations(newZipModel, outputStream); + + progressMonitor.endProgressMonitorSuccess(); + + } catch (IOException e) { + progressMonitor.endProgressMonitorError(e); + throw new ZipException(e); + } catch (Exception e) { + progressMonitor.endProgressMonitorError(e); + throw new ZipException(e); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + //ignore + } + } + + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + /** + * Creates an input stream for the split part of the zip file + * @return Zip4jInputStream + * @throws ZipException + */ + + private RandomAccessFile createSplitZipFileHandler(ZipModel zipModel, int partNumber) throws ZipException { + if (zipModel == null) { + throw new ZipException("zip model is null, cannot create split file handler"); + } + + if (partNumber < 0) { + throw new ZipException("invlaid part number, cannot create split file handler"); + } + + try { + String curZipFile = zipModel.getZipFile(); + String partFile = null; + if (partNumber == zipModel.getEndCentralDirRecord().getNoOfThisDisk()) { + partFile = zipModel.getZipFile(); + } else { + if (partNumber >= 9) { + partFile = curZipFile.substring(0, curZipFile.lastIndexOf(".")) + ".z" + (partNumber+ 1); + } else{ + partFile = curZipFile.substring(0, curZipFile.lastIndexOf(".")) + ".z0" + (partNumber+ 1); + } + } + File tmpFile = new File(partFile); + + if (!Zip4jUtil.checkFileExists(tmpFile)) { + throw new ZipException("split file does not exist: " + partFile); + } + + return new RandomAccessFile(tmpFile, InternalZipConstants.READ_MODE); + } catch (FileNotFoundException e) { + throw new ZipException(e); + } catch (Exception e) { + throw new ZipException(e); + } + + } + + private OutputStream prepareOutputStreamForMerge(File outFile) throws ZipException { + if (outFile == null) { + throw new ZipException("outFile is null, cannot create outputstream"); + } + + try { + return new FileOutputStream(outFile); + } catch (FileNotFoundException e) { + throw new ZipException(e); + } catch (Exception e) { + throw new ZipException(e); + } + } + + private void updateSplitZipModel(ZipModel zipModel, ArrayList fileSizeList, boolean splitSigRemoved) throws ZipException { + if (zipModel == null) { + throw new ZipException("zip model is null, cannot update split zip model"); + } + + zipModel.setSplitArchive(false); + updateSplitFileHeader(zipModel, fileSizeList, splitSigRemoved); + updateSplitEndCentralDirectory(zipModel); + if (zipModel.isZip64Format()) { + updateSplitZip64EndCentralDirLocator(zipModel, fileSizeList); + updateSplitZip64EndCentralDirRec(zipModel, fileSizeList); + } + } + + private void updateSplitFileHeader(ZipModel zipModel, ArrayList fileSizeList, boolean splitSigRemoved) throws ZipException { + try { + + if (zipModel.getCentralDirectory()== null) { + throw new ZipException("corrupt zip model - getCentralDirectory, cannot update split zip model"); + } + + int fileHeaderCount = zipModel.getCentralDirectory().getFileHeaders().size(); + int splitSigOverhead = 0; + if (splitSigRemoved) + splitSigOverhead = 4; + + for (int i = 0; i < fileHeaderCount; i++) { + long offsetLHToAdd = 0; + + for (int j = 0; j < ((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i)).getDiskNumberStart(); j++) { + offsetLHToAdd += ((Long)fileSizeList.get(j)).longValue(); + } + ((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i)).setOffsetLocalHeader( + ((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i)).getOffsetLocalHeader() + + offsetLHToAdd - splitSigOverhead); + ((FileHeader)zipModel.getCentralDirectory().getFileHeaders().get(i)).setDiskNumberStart(0); + } + + } catch (ZipException e) { + throw e; + } catch (Exception e) { + throw new ZipException(e); + } + } + + private void updateSplitEndCentralDirectory(ZipModel zipModel) throws ZipException { + try { + if (zipModel == null) { + throw new ZipException("zip model is null - cannot update end of central directory for split zip model"); + } + + if (zipModel.getCentralDirectory()== null) { + throw new ZipException("corrupt zip model - getCentralDirectory, cannot update split zip model"); + } + + zipModel.getEndCentralDirRecord().setNoOfThisDisk(0); + zipModel.getEndCentralDirRecord().setNoOfThisDiskStartOfCentralDir(0); + zipModel.getEndCentralDirRecord().setTotNoOfEntriesInCentralDir( + zipModel.getCentralDirectory().getFileHeaders().size()); + zipModel.getEndCentralDirRecord().setTotNoOfEntriesInCentralDirOnThisDisk( + zipModel.getCentralDirectory().getFileHeaders().size()); + + } catch (ZipException e) { + throw e; + } catch (Exception e) { + throw new ZipException(e); + } + } + + private void updateSplitZip64EndCentralDirLocator(ZipModel zipModel, ArrayList fileSizeList) throws ZipException { + if (zipModel == null) { + throw new ZipException("zip model is null, cannot update split Zip64 end of central directory locator"); + } + + if (zipModel.getZip64EndCentralDirLocator() == null) { + return; + } + + zipModel.getZip64EndCentralDirLocator().setNoOfDiskStartOfZip64EndOfCentralDirRec(0); + long offsetZip64EndCentralDirRec = 0; + + for (int i = 0; i < fileSizeList.size(); i++) { + offsetZip64EndCentralDirRec += ((Long)fileSizeList.get(i)).longValue(); + } + zipModel.getZip64EndCentralDirLocator().setOffsetZip64EndOfCentralDirRec( + ((Zip64EndCentralDirLocator)zipModel.getZip64EndCentralDirLocator()).getOffsetZip64EndOfCentralDirRec() + + offsetZip64EndCentralDirRec); + zipModel.getZip64EndCentralDirLocator().setTotNumberOfDiscs(1); + } + + private void updateSplitZip64EndCentralDirRec(ZipModel zipModel, ArrayList fileSizeList) throws ZipException { + if (zipModel == null) { + throw new ZipException("zip model is null, cannot update split Zip64 end of central directory record"); + } + + if (zipModel.getZip64EndCentralDirRecord() == null) { + return; + } + + zipModel.getZip64EndCentralDirRecord().setNoOfThisDisk(0); + zipModel.getZip64EndCentralDirRecord().setNoOfThisDiskStartOfCentralDir(0); + zipModel.getZip64EndCentralDirRecord().setTotNoOfEntriesInCentralDirOnThisDisk( + zipModel.getEndCentralDirRecord().getTotNoOfEntriesInCentralDir()); + + long offsetStartCenDirWRTStartDiskNo = 0; + + for (int i = 0; i < fileSizeList.size(); i++) { + offsetStartCenDirWRTStartDiskNo += ((Long)fileSizeList.get(i)).longValue(); + } + + zipModel.getZip64EndCentralDirRecord().setOffsetStartCenDirWRTStartDiskNo( + ((Zip64EndCentralDirRecord)zipModel.getZip64EndCentralDirRecord()).getOffsetStartCenDirWRTStartDiskNo() + + offsetStartCenDirWRTStartDiskNo); + } + + public void setComment(ZipModel zipModel, String comment) throws ZipException { + if (comment == null) { + throw new ZipException("comment is null, cannot update Zip file with comment"); + } + + if (zipModel == null) { + throw new ZipException("zipModel is null, cannot update Zip file with comment"); + } + + String encodedComment = comment; + byte[] commentBytes = comment.getBytes(); + int commentLength = comment.length(); + + if (Zip4jUtil.isSupportedCharset(InternalZipConstants.CHARSET_COMMENTS_DEFAULT)) { + try { + encodedComment = new String(comment.getBytes(InternalZipConstants.CHARSET_COMMENTS_DEFAULT), InternalZipConstants.CHARSET_COMMENTS_DEFAULT); + commentBytes = encodedComment.getBytes(InternalZipConstants.CHARSET_COMMENTS_DEFAULT); + commentLength = encodedComment.length(); + } catch (UnsupportedEncodingException e) { + encodedComment = comment; + commentBytes = comment.getBytes(); + commentLength = comment.length(); + } + } + + if (commentLength > InternalZipConstants.MAX_ALLOWED_ZIP_COMMENT_LENGTH) { + throw new ZipException("comment length exceeds maximum length"); + } + + zipModel.getEndCentralDirRecord().setComment(encodedComment); + zipModel.getEndCentralDirRecord().setCommentBytes(commentBytes); + zipModel.getEndCentralDirRecord().setCommentLength(commentLength); + + SplitOutputStream outputStream = null; + + try { + HeaderWriter headerWriter = new HeaderWriter(); + outputStream = new SplitOutputStream(zipModel.getZipFile()); + + if (zipModel.isZip64Format()) { + outputStream.seek(zipModel.getZip64EndCentralDirRecord().getOffsetStartCenDirWRTStartDiskNo()); + } else { + outputStream.seek(zipModel.getEndCentralDirRecord().getOffsetOfStartOfCentralDir()); + } + + headerWriter.finalizeZipFileWithoutValidations(zipModel, outputStream); + } catch (FileNotFoundException e) { + throw new ZipException(e); + } catch (IOException e) { + throw new ZipException(e); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + //ignore + } + } + } + } + + public void initProgressMonitorForRemoveOp(ZipModel zipModel, + FileHeader fileHeader, ProgressMonitor progressMonitor) throws ZipException { + if (zipModel == null || fileHeader == null || progressMonitor == null) { + throw new ZipException("one of the input parameters is null, cannot calculate total work"); + } + + progressMonitor.setCurrentOperation(ProgressMonitor.OPERATION_REMOVE); + progressMonitor.setFileName(fileHeader.getFileName()); + progressMonitor.setTotalWork(calculateTotalWorkForRemoveOp(zipModel, fileHeader)); + progressMonitor.setState(ProgressMonitor.STATE_BUSY); + } + + private long calculateTotalWorkForRemoveOp(ZipModel zipModel, FileHeader fileHeader) throws ZipException { + return Zip4jUtil.getFileLengh(new File(zipModel.getZipFile())) - fileHeader.getCompressedSize(); + } + + public void initProgressMonitorForMergeOp(ZipModel zipModel, ProgressMonitor progressMonitor) throws ZipException { + if (zipModel == null) { + throw new ZipException("zip model is null, cannot calculate total work for merge op"); + } + + progressMonitor.setCurrentOperation(ProgressMonitor.OPERATION_MERGE); + progressMonitor.setFileName(zipModel.getZipFile()); + progressMonitor.setTotalWork(calculateTotalWorkForMergeOp(zipModel)); + progressMonitor.setState(ProgressMonitor.STATE_BUSY); + } + + private long calculateTotalWorkForMergeOp(ZipModel zipModel) throws ZipException { + long totSize = 0; + if (zipModel.isSplitArchive()) { + int totNoOfSplitFiles = zipModel.getEndCentralDirRecord().getNoOfThisDisk(); + String partFile = null; + String curZipFile = zipModel.getZipFile(); + int partNumber = 0; + for (int i = 0; i <= totNoOfSplitFiles; i++) { + if (partNumber == zipModel.getEndCentralDirRecord().getNoOfThisDisk()) { + partFile = zipModel.getZipFile(); + } else { + if (partNumber >= 9) { + partFile = curZipFile.substring(0, curZipFile.lastIndexOf(".")) + ".z" + (partNumber+ 1); + } else{ + partFile = curZipFile.substring(0, curZipFile.lastIndexOf(".")) + ".z0" + (partNumber+ 1); + } + } + + totSize += Zip4jUtil.getFileLengh(new File(partFile)); + } + + } + return totSize; + } +} diff --git a/src/net/lingala/zip4j/util/CRCUtil.java b/src/net/lingala/zip4j/util/CRCUtil.java new file mode 100644 index 0000000..f030b69 --- /dev/null +++ b/src/net/lingala/zip4j/util/CRCUtil.java @@ -0,0 +1,85 @@ +/* +* 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.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.CRC32; + +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.progress.ProgressMonitor; + +public class CRCUtil { + + private static final int BUF_SIZE = 1 << 14; //16384 + + public static long computeFileCRC(String inputFile) throws ZipException { + return computeFileCRC(inputFile, null); + } + + /** + * Calculates CRC of a file + * @param inputFile - file for which crc has to be calculated + * @return crc of the file + * @throws ZipException + */ + public static long computeFileCRC(String inputFile, ProgressMonitor progressMonitor) throws ZipException { + + if (!Zip4jUtil.isStringNotNullAndNotEmpty(inputFile)) { + throw new ZipException("input file is null or empty, cannot calculate CRC for the file"); + } + InputStream inputStream = null; + try { + Zip4jUtil.checkFileReadAccess(inputFile); + + inputStream = new FileInputStream(new File(inputFile)); + + byte[] buff = new byte[BUF_SIZE]; + int readLen = -2; + CRC32 crc32 = new CRC32(); + + while ((readLen = inputStream.read(buff)) != -1) { + crc32.update(buff, 0, readLen); + if (progressMonitor != null) { + progressMonitor.updateWorkCompleted(readLen); + if (progressMonitor.isCancelAllTasks()) { + progressMonitor.setResult(ProgressMonitor.RESULT_CANCELLED); + progressMonitor.setState(ProgressMonitor.STATE_READY); + return 0; + } + } + } + + return crc32.getValue(); + } catch (IOException e) { + throw new ZipException(e); + } catch (Exception e) { + throw new ZipException(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + throw new ZipException("error while closing the file after calculating crc"); + } + } + } + } + +} diff --git a/src/net/lingala/zip4j/util/InternalZipConstants.java b/src/net/lingala/zip4j/util/InternalZipConstants.java new file mode 100644 index 0000000..8e60330 --- /dev/null +++ b/src/net/lingala/zip4j/util/InternalZipConstants.java @@ -0,0 +1,174 @@ +/* +* 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.util; + +public interface InternalZipConstants { + + /* + * Header signatures + */ + // Whenever a new Signature is added here, make sure to add it + // in Zip4jUtil.getAllHeaderSignatures() + static long LOCSIG = 0x04034b50L; // "PK\003\004" + static long EXTSIG = 0x08074b50L; // "PK\007\008" + static long CENSIG = 0x02014b50L; // "PK\001\002" + static long ENDSIG = 0x06054b50L; // "PK\005\006" + static long DIGSIG = 0x05054b50L; + static long ARCEXTDATREC = 0x08064b50L; + static long SPLITSIG = 0x08074b50L; + static long ZIP64ENDCENDIRLOC = 0x07064b50L; + static long ZIP64ENDCENDIRREC = 0x06064b50; + static int EXTRAFIELDZIP64LENGTH = 0x0001; + static int AESSIG = 0x9901; + + /* + * Header sizes in bytes (including signatures) + */ + static final int LOCHDR = 30; // LOC header size + static final int EXTHDR = 16; // EXT header size + static final int CENHDR = 46; // CEN header size + static final int ENDHDR = 22; // END header size + + /* + * Local file (LOC) header field offsets + */ + static final int LOCVER = 4; // version needed to extract + static final int LOCFLG = 6; // general purpose bit flag + static final int LOCHOW = 8; // compression method + static final int LOCTIM = 10; // modification time + static final int LOCCRC = 14; // uncompressed file crc-32 value + static final int LOCSIZ = 18; // compressed size + static final int LOCLEN = 22; // uncompressed size + static final int LOCNAM = 26; // filename length + static final int LOCEXT = 28; // extra field length + + /* + * Extra local (EXT) header field offsets + */ + static final int EXTCRC = 4; // uncompressed file crc-32 value + static final int EXTSIZ = 8; // compressed size + static final int EXTLEN = 12; // uncompressed size + + /* + * Central directory (CEN) header field offsets + */ + static final int CENVEM = 4; // version made by + static final int CENVER = 6; // version needed to extract + static final int CENFLG = 8; // encrypt, decrypt flags + static final int CENHOW = 10; // compression method + static final int CENTIM = 12; // modification time + static final int CENCRC = 16; // uncompressed file crc-32 value + static final int CENSIZ = 20; // compressed size + static final int CENLEN = 24; // uncompressed size + static final int CENNAM = 28; // filename length + static final int CENEXT = 30; // extra field length + static final int CENCOM = 32; // comment length + static final int CENDSK = 34; // disk number start + static final int CENATT = 36; // internal file attributes + static final int CENATX = 38; // external file attributes + static final int CENOFF = 42; // LOC header offset + + /* + * End of central directory (END) header field offsets + */ + static final int ENDSUB = 8; // number of entries on this disk + static final int ENDTOT = 10; // total number of entries + static final int ENDSIZ = 12; // central directory size in bytes + static final int ENDOFF = 16; // offset of first CEN header + static final int ENDCOM = 20; // zip file comment length + + static final int STD_DEC_HDR_SIZE = 12; + + //AES Constants + static final int AES_AUTH_LENGTH = 10; + static final int AES_BLOCK_SIZE = 16; + + static final int MIN_SPLIT_LENGTH = 65536; + + static final long ZIP_64_LIMIT = 4294967295L; + + public static String OFFSET_CENTRAL_DIR = "offsetCentralDir"; + + public static final String VERSION = "1.3.2"; + + public static final int MODE_ZIP = 1; + + public static final int MODE_UNZIP = 2; + + public static final String WRITE_MODE = "rw"; + + public static final String READ_MODE = "r"; + + public static final int BUFF_SIZE = 1024 * 4; + + public static final int FILE_MODE_NONE = 0; + + public static final int FILE_MODE_READ_ONLY = 1; + + public static final int FILE_MODE_HIDDEN = 2; + + public static final int FILE_MODE_ARCHIVE = 32; + + public static final int FILE_MODE_READ_ONLY_HIDDEN = 3; + + public static final int FILE_MODE_READ_ONLY_ARCHIVE = 33; + + public static final int FILE_MODE_HIDDEN_ARCHIVE = 34; + + public static final int FILE_MODE_READ_ONLY_HIDDEN_ARCHIVE = 35; + + public static final int FILE_MODE_SYSTEM = 38; + + public static final int FOLDER_MODE_NONE = 16; + + public static final int FOLDER_MODE_HIDDEN = 18; + + public static final int FOLDER_MODE_ARCHIVE = 48; + + public static final int FOLDER_MODE_HIDDEN_ARCHIVE = 50; + + // Update local file header constants + // This value holds the number of bytes to skip from + // the offset of start of local header + public static final int UPDATE_LFH_CRC = 14; + + public static final int UPDATE_LFH_COMP_SIZE = 18; + + public static final int UPDATE_LFH_UNCOMP_SIZE = 22; + + public static final int LIST_TYPE_FILE = 1; + + public static final int LIST_TYPE_STRING = 2; + + public static final int UFT8_NAMES_FLAG = 1 << 11; + + public static final String CHARSET_UTF8 = "UTF8"; + + public static final String CHARSET_CP850 = "Cp850"; + + public static final String CHARSET_COMMENTS_DEFAULT = "windows-1254"; + + public static final String CHARSET_DEFAULT = System.getProperty("file.encoding"); + + public static final String FILE_SEPARATOR = System.getProperty("file.separator"); + + public static final String ZIP_FILE_SEPARATOR = "/"; + + public static final String THREAD_NAME = "Zip4j"; + + public static final int MAX_ALLOWED_ZIP_COMMENT_LENGTH = 0xFFFF; +} diff --git a/src/net/lingala/zip4j/util/Raw.java b/src/net/lingala/zip4j/util/Raw.java new file mode 100644 index 0000000..f4c1348 --- /dev/null +++ b/src/net/lingala/zip4j/util/Raw.java @@ -0,0 +1,184 @@ +/* +* 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.util; + +import java.io.DataInput; +import java.io.IOException; + +import net.lingala.zip4j.exception.ZipException; + +public class Raw +{ + public static long readLongLittleEndian(byte[] array,int pos){ + long temp = 0; + temp |= array[pos+7]&0xff; + temp <<=8; + temp |= array[pos+6]&0xff; + temp <<=8; + temp |= array[pos+5]&0xff; + temp <<=8; + temp |= array[pos+4]&0xff; + temp <<=8; + temp |= array[pos+3]&0xff; + temp <<=8; + temp |= array[pos+2]&0xff; + temp <<=8; + temp |= array[pos+1]&0xff; + temp <<=8; + temp |= array[pos]&0xff; + return temp; + } + + public static int readLeInt(DataInput di, byte[] b) throws ZipException{ + try { + di.readFully(b, 0, 4); + } catch (IOException e) { + throw new ZipException(e); + } + return ((b[0] & 0xff) | (b[1] & 0xff) << 8) + | ((b[2] & 0xff) | (b[3] & 0xff) << 8) << 16; + } + + public static int readShortLittleEndian(byte[] b, int off){ + return (b[off] & 0xff) | (b[off+1] & 0xff) << 8; + } + + public static final short readShortBigEndian(byte[] array, int pos) { + short temp = 0; + temp |= array[pos] & 0xff; + temp <<= 8; + temp |= array[pos + 1] & 0xff; + return temp; + } + + public static int readIntLittleEndian(byte[] b, int off){ + return ((b[off] & 0xff) | (b[off+1] & 0xff) << 8) + | ((b[off+2] & 0xff) | (b[off+3] & 0xff) << 8) << 16; + } + + public static byte[] toByteArray(int in,int outSize) { + byte[] out = new byte[outSize]; + byte[] intArray = toByteArray(in); + for( int i=0; i> 8); + out[2] = (byte)(in >> 16); + out[3] = (byte)(in >> 24); + + return out; + } + + public static final void writeShortLittleEndian(byte[] array, int pos, + short value) { + array[pos +1] = (byte) (value >>> 8); + array[pos ] = (byte) (value & 0xFF); + + } + + public static final void writeIntLittleEndian(byte[] array, int pos,int value) { + array[pos+3] = (byte) (value >>>24); + array[pos+2] = (byte) (value >>>16); + array[pos+1] = (byte) (value >>>8); + array[pos] = (byte) (value &0xFF); + + } + + public static void writeLongLittleEndian(byte[] array, int pos, long value){ + array[pos+7] = (byte) (value >>>56); + array[pos+6] = (byte) (value >>>48); + array[pos+5] = (byte) (value >>>40); + array[pos+4] = (byte) (value >>>32); + array[pos+3] = (byte) (value >>>24); + array[pos+2] = (byte) (value >>>16); + array[pos+1] = (byte) (value >>>8); + array[pos] = (byte) (value &0xFF); + } + + public static byte bitArrayToByte(int[] bitArray) throws ZipException { + if (bitArray == null) { + throw new ZipException("bit array is null, cannot calculate byte from bits"); + } + + if (bitArray.length != 8) { + throw new ZipException("invalid bit array length, cannot calculate byte"); + } + + if(!checkBits(bitArray)) { + throw new ZipException("invalid bits provided, bits contain other values than 0 or 1"); + } + + int retNum = 0; + for (int i = 0; i < bitArray.length; i++) { + retNum += Math.pow(2, i) * bitArray[i]; + } + + return (byte)retNum; + } + + private static boolean checkBits(int[] bitArray) { + for (int i = 0; i < bitArray.length; i++) { + if (bitArray[i] != 0 && bitArray[i] != 1) { + return false; + } + } + return true; + } + + public static void prepareBuffAESIVBytes(byte[] buff, int nonce, int length) { + buff[0] = (byte)nonce; + buff[1] = (byte)(nonce >> 8); + buff[2] = (byte)(nonce >> 16); + buff[3] = (byte)(nonce >> 24); + buff[4] = 0; + buff[5] = 0; + buff[6] = 0; + buff[7] = 0; + buff[8] = 0; + buff[9] = 0; + buff[10] = 0; + buff[11] = 0; + buff[12] = 0; + buff[13] = 0; + buff[14] = 0; + buff[15] = 0; + } + + /** + * Converts a char array to byte array + * @param charArray + * @return byte array representation of the input char array + */ + public static byte[] convertCharArrayToByteArray(char[] charArray) { + if (charArray == null) { + throw new NullPointerException(); + } + + byte[] bytes = new byte[charArray.length]; + for(int i=0;i= 0); + } + + public static void setFileReadOnly(File file) throws ZipException { + if (file == null) { + throw new ZipException("input file is null. cannot set read only file attribute"); + } + + if (file.exists()) { + file.setReadOnly(); + } + } + + public static void setFileHidden(File file) throws ZipException { +// if (file == null) { +// throw new ZipException("input file is null. cannot set hidden file attribute"); +// } +// +// if (!isWindows()) { +// return; +// } +// +// if (file.exists()) { +// try { +// Runtime.getRuntime().exec("attrib +H \"" + file.getAbsolutePath() + "\""); +// } catch (IOException e) { +// // do nothing as this is not of a higher priority +// // add log statements here when logging is done +// } +// } + } + + public static void setFileArchive(File file) throws ZipException { +// if (file == null) { +// throw new ZipException("input file is null. cannot set archive file attribute"); +// } +// +// if (!isWindows()) { +// return; +// } +// +// if (file.exists()) { +// try { +// if (file.isDirectory()) { +// Runtime.getRuntime().exec("attrib +A \"" + file.getAbsolutePath() + "\""); +// } else { +// Runtime.getRuntime().exec("attrib +A \"" + file.getAbsolutePath() + "\""); +// } +// +// } catch (IOException e) { +// // do nothing as this is not of a higher priority +// // add log statements here when logging is done +// } +// } + } + + public static void setFileSystemMode(File file) throws ZipException { +// if (file == null) { +// throw new ZipException("input file is null. cannot set archive file attribute"); +// } +// +// if (!isWindows()) { +// return; +// } +// +// if (file.exists()) { +// try { +// Runtime.getRuntime().exec("attrib +S \"" + file.getAbsolutePath() + "\""); +// } catch (IOException e) { +// // do nothing as this is not of a higher priority +// // add log statements here when logging is done +// } +// } + } + + public static long getLastModifiedFileTime(File file, TimeZone timeZone) throws ZipException { + if (file == null) { + throw new ZipException("input file is null, cannot read last modified file time"); + } + + if (!file.exists()) { + throw new ZipException("input file does not exist, cannot read last modified file time"); + } + + return file.lastModified(); + } + + public static String getFileNameFromFilePath(File file) throws ZipException { + if (file == null) { + throw new ZipException("input file is null, cannot get file name"); + } + + if (file.isDirectory()) { + return null; + } + + return file.getName(); + } + + public static long getFileLengh(String file) throws ZipException { + if (!isStringNotNullAndNotEmpty(file)) { + throw new ZipException("invalid file name"); + } + + return getFileLengh(new File(file)); + } + + public static long getFileLengh(File file) throws ZipException { + if (file == null) { + throw new ZipException("input file is null, cannot calculate file length"); + } + + if (file.isDirectory()) { + return -1; + } + + return file.length(); + } + + /** + * Converts input time from Java to DOS format + * @param time + * @return time in DOS format + */ + public static long javaToDosTime(long time) { + + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(time); + + int year = cal.get(Calendar.YEAR); + if (year < 1980) { + return (1 << 21) | (1 << 16); + } + return (year - 1980) << 25 | (cal.get(Calendar.MONTH) + 1) << 21 | + cal.get(Calendar.DATE) << 16 | cal.get(Calendar.HOUR_OF_DAY) << 11 | cal.get(Calendar.MINUTE) << 5 | + cal.get(Calendar.SECOND) >> 1; + } + + /** + * Converts time in dos format to Java format + * @param dosTime + * @return time in java format + */ + public static long dosToJavaTme(int dosTime) { + int sec = 2 * (dosTime & 0x1f); + int min = (dosTime >> 5) & 0x3f; + int hrs = (dosTime >> 11) & 0x1f; + int day = (dosTime >> 16) & 0x1f; + int mon = ((dosTime >> 21) & 0xf) - 1; + int year = ((dosTime >> 25) & 0x7f) + 1980; + + Calendar cal = Calendar.getInstance(); + cal.set(year, mon, day, hrs, min, sec); + cal.set(Calendar.MILLISECOND, 0); + return cal.getTime().getTime(); + } + + public static FileHeader getFileHeader(ZipModel zipModel, String fileName) throws ZipException { + if (zipModel == null) { + throw new ZipException("zip model is null, cannot determine file header for fileName: " + fileName); + } + + if (!isStringNotNullAndNotEmpty(fileName)) { + throw new ZipException("file name is null, cannot determine file header for fileName: " + fileName); + } + + FileHeader fileHeader = null; + fileHeader = getFileHeaderWithExactMatch(zipModel, fileName); + + if (fileHeader == null) { + fileName = fileName.replaceAll("\\\\", "/"); + fileHeader = getFileHeaderWithExactMatch(zipModel, fileName); + + if (fileHeader == null) { + fileName = fileName.replaceAll("/", "\\\\"); + fileHeader = getFileHeaderWithExactMatch(zipModel, fileName); + } + } + + return fileHeader; + } + + public static FileHeader getFileHeaderWithExactMatch(ZipModel zipModel, String fileName) throws ZipException { + if (zipModel == null) { + throw new ZipException("zip model is null, cannot determine file header with exact match for fileName: " + fileName); + } + + if (!isStringNotNullAndNotEmpty(fileName)) { + throw new ZipException("file name is null, cannot determine file header with exact match for fileName: " + fileName); + } + + if (zipModel.getCentralDirectory() == null) { + throw new ZipException("central directory is null, cannot determine file header with exact match for fileName: " + fileName); + } + + if (zipModel.getCentralDirectory().getFileHeaders() == null) { + throw new ZipException("file Headers are null, cannot determine file header with exact match for fileName: " + fileName); + } + + if (zipModel.getCentralDirectory().getFileHeaders().size() <= 0) { + return null; + } + ArrayList fileHeaders = zipModel.getCentralDirectory().getFileHeaders(); + for (int i = 0; i < fileHeaders.size(); i++) { + FileHeader fileHeader = (FileHeader)fileHeaders.get(i); + String fileNameForHdr = fileHeader.getFileName(); + if (!isStringNotNullAndNotEmpty(fileNameForHdr)) { + continue; + } + + if (fileName.equalsIgnoreCase(fileNameForHdr)) { + return fileHeader; + } + } + + return null; + } + + public static int getIndexOfFileHeader(ZipModel zipModel, + FileHeader fileHeader) throws ZipException { + + if (zipModel == null || fileHeader == null) { + throw new ZipException("input parameters is null, cannot determine index of file header"); + } + + if (zipModel.getCentralDirectory() == null) { + throw new ZipException("central directory is null, ccannot determine index of file header"); + } + + if (zipModel.getCentralDirectory().getFileHeaders() == null) { + throw new ZipException("file Headers are null, cannot determine index of file header"); + } + + if (zipModel.getCentralDirectory().getFileHeaders().size() <= 0) { + return -1; + } + String fileName = fileHeader.getFileName(); + + if (!isStringNotNullAndNotEmpty(fileName)) { + throw new ZipException("file name in file header is empty or null, cannot determine index of file header"); + } + + ArrayList fileHeaders = zipModel.getCentralDirectory().getFileHeaders(); + for (int i = 0; i < fileHeaders.size(); i++) { + FileHeader fileHeaderTmp = (FileHeader)fileHeaders.get(i); + String fileNameForHdr = fileHeaderTmp.getFileName(); + if (!isStringNotNullAndNotEmpty(fileNameForHdr)) { + continue; + } + + if (fileName.equalsIgnoreCase(fileNameForHdr)) { + return i; + } + } + return -1; + } + + public static ArrayList getFilesInDirectoryRec(File path, + boolean readHiddenFiles) throws ZipException { + + if (path == null) { + throw new ZipException("input path is null, cannot read files in the directory"); + } + + ArrayList result = new ArrayList(); + File[] filesAndDirs = path.listFiles(); + List filesDirs = Arrays.asList(filesAndDirs); + + if (!path.canRead()) { + return result; + } + + for(int i = 0; i < filesDirs.size(); i++) { + File file = (File)filesDirs.get(i); + if (file.isHidden() && !readHiddenFiles) { + return result; + } + result.add(file); + if (file.isDirectory()) { + List deeperList = getFilesInDirectoryRec(file, readHiddenFiles); + result.addAll(deeperList); + } + } + return result; + } + + public static String getZipFileNameWithoutExt(String zipFile) throws ZipException { + if (!isStringNotNullAndNotEmpty(zipFile)) { + throw new ZipException("zip file name is empty or null, cannot determine zip file name"); + } + String tmpFileName = zipFile; + if (zipFile.indexOf(System.getProperty("file.separator")) >= 0) { + tmpFileName = zipFile.substring(zipFile.lastIndexOf(System.getProperty("file.separator"))); + } + + if (tmpFileName.indexOf(".") > 0) { + tmpFileName = tmpFileName.substring(0, tmpFileName.lastIndexOf(".")); + } + return tmpFileName; + } + + public static byte[] convertCharset(String str) throws ZipException { + try { + byte[] converted = null; + String charSet = detectCharSet(str); + if (charSet.equals(InternalZipConstants.CHARSET_CP850)) { + converted = str.getBytes(InternalZipConstants.CHARSET_CP850); + } else if (charSet.equals(InternalZipConstants.CHARSET_UTF8)) { + converted = str.getBytes(InternalZipConstants.CHARSET_UTF8); + } else { + converted = str.getBytes(); + } + return converted; + } + catch (UnsupportedEncodingException err) { + return str.getBytes(); + } catch (Exception e) { + throw new ZipException(e); + } + } + + /** + * Decodes file name based on encoding. If file name is UTF 8 encoded + * returns an UTF8 encoded string, else return Cp850 encoded String. If + * appropriate charset is not supported, then returns a System default + * charset encoded String + * @param data + * @param isUTF8 + * @return String + */ + public static String decodeFileName(byte[] data, boolean isUTF8) { + if (isUTF8) { + try { + return new String(data, InternalZipConstants.CHARSET_UTF8); + } catch (UnsupportedEncodingException e) { + return new String(data); + } + } else { + return getCp850EncodedString(data); + } + } + + /** + * Returns a string in Cp850 encoding from the input bytes. + * If this encoding is not supported, then String with the default encoding is returned. + * @param data + * @return String + */ + public static String getCp850EncodedString(byte[] data) { + try { + String retString = new String(data, InternalZipConstants.CHARSET_CP850); + return retString; + } catch (UnsupportedEncodingException e) { + return new String(data); + } + } + + /** + * Returns an absoulte path for the given file path + * @param filePath + * @return String + */ + public static String getAbsoluteFilePath(String filePath) throws ZipException { + if (!isStringNotNullAndNotEmpty(filePath)) { + throw new ZipException("filePath is null or empty, cannot get absolute file path"); + } + + File file = new File(filePath); + return file.getAbsolutePath(); + } + + /** + * Checks to see if all the elements in the arraylist match the given type + * @param sourceList - list to be checked + * @param type - type of elements to be present in the list (ex: File, String, etc) + * @return true if all elements match the given type, if not returns false + */ + public static boolean checkArrayListTypes(ArrayList sourceList, int type) throws ZipException { + + if (sourceList == null) { + throw new ZipException("input arraylist is null, cannot check types"); + } + + if (sourceList.size() <= 0) { + return true; + } + + boolean invalidFound = false; + + switch (type) { + case InternalZipConstants.LIST_TYPE_FILE: + for (int i = 0; i < sourceList.size(); i++) { + if (!(sourceList.get(i) instanceof File)) { + invalidFound = true; + break; + } + } + break; + case InternalZipConstants.LIST_TYPE_STRING: + for (int i = 0; i < sourceList.size(); i++) { + if (!(sourceList.get(i) instanceof String)) { + invalidFound = true; + break; + } + } + break; + default: + break; + } + return !invalidFound; + } + + /** + * Detects the encoding charset for the input string + * @param str + * @return String - charset for the String + * @throws ZipException - if input string is null. In case of any other exception + * this method returns default System charset + */ + public static String detectCharSet(String str) throws ZipException { + if (str == null) { + throw new ZipException("input string is null, cannot detect charset"); + } + + try { + byte[] byteString = str.getBytes(InternalZipConstants.CHARSET_CP850); + String tempString = new String(byteString, InternalZipConstants.CHARSET_CP850); + + if (str.equals(tempString)) { + return InternalZipConstants.CHARSET_CP850; + } + + byteString = str.getBytes(InternalZipConstants.CHARSET_UTF8); + tempString = new String(byteString, InternalZipConstants.CHARSET_UTF8); + + if (str.equals(tempString)) { + return InternalZipConstants.CHARSET_UTF8; + } + + return InternalZipConstants.CHARSET_DEFAULT; + } catch (UnsupportedEncodingException e) { + return InternalZipConstants.CHARSET_DEFAULT; + } catch (Exception e) { + return InternalZipConstants.CHARSET_DEFAULT; + } + } + + /** + * returns the length of the string by wrapping it in a byte buffer with + * the appropriate charset of the input string and returns the limit of the + * byte buffer + * @param str + * @return length of the string + * @throws ZipException + */ + public static int getEncodedStringLength(String str) throws ZipException { + if (!isStringNotNullAndNotEmpty(str)) { + throw new ZipException("input string is null, cannot calculate encoded String length"); + } + + String charset = detectCharSet(str); + return getEncodedStringLength(str, charset); + } + + /** + * returns the length of the string in the input encoding + * @param str + * @param charset + * @return int + * @throws ZipException + */ + public static int getEncodedStringLength(String str, String charset) throws ZipException { + if (!isStringNotNullAndNotEmpty(str)) { + throw new ZipException("input string is null, cannot calculate encoded String length"); + } + + if (!isStringNotNullAndNotEmpty(charset)) { + throw new ZipException("encoding is not defined, cannot calculate string length"); + } + + ByteBuffer byteBuffer = null; + + try { + if (charset.equals(InternalZipConstants.CHARSET_CP850)) { + byteBuffer = ByteBuffer.wrap(str.getBytes(InternalZipConstants.CHARSET_CP850)); + } else if (charset.equals(InternalZipConstants.CHARSET_UTF8)) { + byteBuffer = ByteBuffer.wrap(str.getBytes(InternalZipConstants.CHARSET_UTF8)); + } else { + byteBuffer = ByteBuffer.wrap(str.getBytes(charset)); + } + } catch (UnsupportedEncodingException e) { + byteBuffer = ByteBuffer.wrap(str.getBytes()); + } catch (Exception e) { + throw new ZipException(e); + } + + return byteBuffer.limit(); + } + + /** + * Checks if the input charset is supported + * @param charset + * @return boolean + * @throws ZipException + */ + public static boolean isSupportedCharset(String charset) throws ZipException { + if (!isStringNotNullAndNotEmpty(charset)) { + throw new ZipException("charset is null or empty, cannot check if it is supported"); + } + + try { + new String("a".getBytes(), charset); + return true; + } catch (UnsupportedEncodingException e) { + return false; + } catch (Exception e) { + throw new ZipException(e); + } + } + + public static ArrayList getSplitZipFiles(ZipModel zipModel) throws ZipException { + if (zipModel == null) { + throw new ZipException("cannot get split zip files: zipmodel is null"); + } + + if (zipModel.getEndCentralDirRecord() == null) { + return null; + } + + ArrayList retList = new ArrayList(); + String currZipFile = zipModel.getZipFile(); + String zipFileName = (new File(currZipFile)).getName(); + String partFile = null; + + if (!isStringNotNullAndNotEmpty(currZipFile)) { + throw new ZipException("cannot get split zip files: zipfile is null"); + } + + if (!zipModel.isSplitArchive()) { + retList.add(currZipFile); + return retList; + } + + int numberOfThisDisk = zipModel.getEndCentralDirRecord().getNoOfThisDisk(); + + if (numberOfThisDisk == 0) { + retList.add(currZipFile); + return retList; + } else { + for (int i = 0; i <= numberOfThisDisk; i++) { + if (i == numberOfThisDisk) { + retList.add(zipModel.getZipFile()); + } else { + String fileExt = ".z0"; + if (i > 9) { + fileExt = ".z"; + } + partFile = (zipFileName.indexOf(".") >= 0) ? currZipFile.substring(0, currZipFile.lastIndexOf(".")) : currZipFile; + partFile = partFile + fileExt + (i + 1); + retList.add(partFile); + } + } + } + return retList; + } + + public static String getRelativeFileName(String file, String rootFolderInZip, String rootFolderPath) throws ZipException { + if (!Zip4jUtil.isStringNotNullAndNotEmpty(file)) { + throw new ZipException("input file path/name is empty, cannot calculate relative file name"); + } + + String fileName = null; + + if (Zip4jUtil.isStringNotNullAndNotEmpty(rootFolderPath)) { + + File rootFolderFile = new File(rootFolderPath); + + String rootFolderFileRef = rootFolderFile.getPath(); + + if (!rootFolderFileRef.endsWith(InternalZipConstants.FILE_SEPARATOR)) { + rootFolderFileRef += InternalZipConstants.FILE_SEPARATOR; + } + + String tmpFileName = file.substring(rootFolderFileRef.length()); + if (tmpFileName.startsWith(System.getProperty("file.separator"))) { + tmpFileName = tmpFileName.substring(1); + } + + File tmpFile = new File(file); + + if (tmpFile.isDirectory()) { + tmpFileName = tmpFileName.replaceAll("\\\\", "/"); + tmpFileName += InternalZipConstants.ZIP_FILE_SEPARATOR; + } else { + String bkFileName = tmpFileName.substring(0, tmpFileName.lastIndexOf(tmpFile.getName())); + bkFileName = bkFileName.replaceAll("\\\\", "/"); + tmpFileName = bkFileName + tmpFile.getName(); + } + + fileName = tmpFileName; + } else { + File relFile = new File(file); + if (relFile.isDirectory()) { + fileName = relFile.getName() + InternalZipConstants.ZIP_FILE_SEPARATOR; + } else { + fileName = Zip4jUtil.getFileNameFromFilePath(new File(file)); + } + } + + if (Zip4jUtil.isStringNotNullAndNotEmpty(rootFolderInZip)) { + fileName = rootFolderInZip + fileName; + } + + if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileName)) { + throw new ZipException("Error determining file name"); + } + + return fileName; + } + + public static long[] getAllHeaderSignatures() { + long[] allSigs = new long[11]; + + allSigs[0] = InternalZipConstants.LOCSIG; + allSigs[1] = InternalZipConstants.EXTSIG; + allSigs[2] = InternalZipConstants.CENSIG; + allSigs[3] = InternalZipConstants.ENDSIG; + allSigs[4] = InternalZipConstants.DIGSIG; + allSigs[5] = InternalZipConstants.ARCEXTDATREC; + allSigs[6] = InternalZipConstants.SPLITSIG; + allSigs[7] = InternalZipConstants.ZIP64ENDCENDIRLOC; + allSigs[8] = InternalZipConstants.ZIP64ENDCENDIRREC; + allSigs[9] = InternalZipConstants.EXTRAFIELDZIP64LENGTH; + allSigs[10] = InternalZipConstants.AESSIG; + + return allSigs; + } +} diff --git a/src/net/lingala/zip4j/zip/ZipEngine.java b/src/net/lingala/zip4j/zip/ZipEngine.java new file mode 100644 index 0000000..b64fc97 --- /dev/null +++ b/src/net/lingala/zip4j/zip/ZipEngine.java @@ -0,0 +1,485 @@ +/* +* 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.zip; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.HashMap; + +import net.lingala.zip4j.exception.ZipException; +import net.lingala.zip4j.io.SplitOutputStream; +import net.lingala.zip4j.io.ZipOutputStream; +import net.lingala.zip4j.model.EndCentralDirRecord; +import net.lingala.zip4j.model.FileHeader; +import net.lingala.zip4j.model.ZipModel; +import net.lingala.zip4j.model.ZipParameters; +import net.lingala.zip4j.progress.ProgressMonitor; +import net.lingala.zip4j.util.ArchiveMaintainer; +import net.lingala.zip4j.util.CRCUtil; +import net.lingala.zip4j.util.InternalZipConstants; +import net.lingala.zip4j.util.Zip4jConstants; +import net.lingala.zip4j.util.Zip4jUtil; + +public class ZipEngine { + + private ZipModel zipModel; + + public ZipEngine(ZipModel zipModel) throws ZipException { + + if (zipModel == null) { + throw new ZipException("zip model is null in ZipEngine constructor"); + } + + this.zipModel = zipModel; + } + + public void addFiles(final ArrayList fileList, final ZipParameters parameters, + final ProgressMonitor progressMonitor, boolean runInThread) throws ZipException { + + if (fileList == null || parameters == null) { + throw new ZipException("one of the input parameters is null when adding files"); + } + + if(fileList.size() <= 0) { + throw new ZipException("no files to add"); + } + + progressMonitor.setCurrentOperation(ProgressMonitor.OPERATION_ADD); + progressMonitor.setState(ProgressMonitor.STATE_BUSY); + progressMonitor.setResult(ProgressMonitor.RESULT_WORKING); + + if (runInThread) { + progressMonitor.setTotalWork(calculateTotalWork(fileList, parameters)); + progressMonitor.setFileName(((File)fileList.get(0)).getAbsolutePath()); + + Thread thread = new Thread(InternalZipConstants.THREAD_NAME) { + public void run() { + try { + initAddFiles(fileList, parameters, progressMonitor); + } catch (ZipException e) { + } + } + }; + thread.start(); + + } else { + initAddFiles(fileList, parameters, progressMonitor); + } + } + + private void initAddFiles(ArrayList fileList, ZipParameters parameters, + ProgressMonitor progressMonitor) throws ZipException { + + if (fileList == null || parameters == null) { + throw new ZipException("one of the input parameters is null when adding files"); + } + + if(fileList.size() <= 0) { + throw new ZipException("no files to add"); + } + + if (zipModel.getEndCentralDirRecord() == null) { + zipModel.setEndCentralDirRecord(createEndOfCentralDirectoryRecord()); + } + + ZipOutputStream outputStream = null; + InputStream inputStream = null; + try { + checkParameters(parameters); + + removeFilesIfExists(fileList, parameters, progressMonitor); + + boolean isZipFileAlreadExists = Zip4jUtil.checkFileExists(zipModel.getZipFile()); + + SplitOutputStream splitOutputStream = new SplitOutputStream(new File(zipModel.getZipFile()), zipModel.getSplitLength()); + outputStream = new ZipOutputStream(splitOutputStream, this.zipModel); + + if (isZipFileAlreadExists) { + if (zipModel.getEndCentralDirRecord() == null) { + throw new ZipException("invalid end of central directory record"); + } + splitOutputStream.seek(zipModel.getEndCentralDirRecord().getOffsetOfStartOfCentralDir()); + } + byte[] readBuff = new byte[InternalZipConstants.BUFF_SIZE]; + int readLen = -1; + for (int i = 0; i < fileList.size(); i++) { + + if (progressMonitor.isCancelAllTasks()) { + progressMonitor.setResult(ProgressMonitor.RESULT_CANCELLED); + progressMonitor.setState(ProgressMonitor.STATE_READY); + return; + } + + ZipParameters fileParameters = (ZipParameters) parameters.clone(); + + progressMonitor.setFileName(((File)fileList.get(i)).getAbsolutePath()); + + if (!((File)fileList.get(i)).isDirectory()) { + if (fileParameters.isEncryptFiles() && fileParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) { + progressMonitor.setCurrentOperation(ProgressMonitor.OPERATION_CALC_CRC); + fileParameters.setSourceFileCRC((int)CRCUtil.computeFileCRC(((File)fileList.get(i)).getAbsolutePath(), progressMonitor)); + progressMonitor.setCurrentOperation(ProgressMonitor.OPERATION_ADD); + + if (progressMonitor.isCancelAllTasks()) { + progressMonitor.setResult(ProgressMonitor.RESULT_CANCELLED); + progressMonitor.setState(ProgressMonitor.STATE_READY); + return; + } + } + + if (Zip4jUtil.getFileLengh((File)fileList.get(i)) == 0) { + fileParameters.setCompressionMethod(Zip4jConstants.COMP_STORE); + } + } + + outputStream.putNextEntry((File)fileList.get(i), fileParameters); + if (((File)fileList.get(i)).isDirectory()) { + outputStream.closeEntry(); + continue; + } + + inputStream = new FileInputStream((File)fileList.get(i)); + + while ((readLen = inputStream.read(readBuff)) != -1) { + if (progressMonitor.isCancelAllTasks()) { + progressMonitor.setResult(ProgressMonitor.RESULT_CANCELLED); + progressMonitor.setState(ProgressMonitor.STATE_READY); + return; + } + + outputStream.write(readBuff, 0, readLen); + progressMonitor.updateWorkCompleted(readLen); + } + + outputStream.closeEntry(); + + if (inputStream != null) { + inputStream.close(); + } + } + + outputStream.finish(); + progressMonitor.endProgressMonitorSuccess(); + } catch (ZipException e) { + progressMonitor.endProgressMonitorError(e); + throw e; + } catch (Exception e) { + progressMonitor.endProgressMonitorError(e); + throw new ZipException(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + } + } + + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + } + } + } + } + + public void addStreamToZip(InputStream inputStream, ZipParameters parameters) throws ZipException { + if (inputStream == null || parameters == null) { + throw new ZipException("one of the input parameters is null, cannot add stream to zip"); + } + + ZipOutputStream outputStream = null; + + try { + checkParameters(parameters); + + boolean isZipFileAlreadExists = Zip4jUtil.checkFileExists(zipModel.getZipFile()); + + SplitOutputStream splitOutputStream = new SplitOutputStream(new File(zipModel.getZipFile()), zipModel.getSplitLength()); + outputStream = new ZipOutputStream(splitOutputStream, this.zipModel); + + if (isZipFileAlreadExists) { + if (zipModel.getEndCentralDirRecord() == null) { + throw new ZipException("invalid end of central directory record"); + } + splitOutputStream.seek(zipModel.getEndCentralDirRecord().getOffsetOfStartOfCentralDir()); + } + + byte[] readBuff = new byte[InternalZipConstants.BUFF_SIZE]; + int readLen = -1; + + outputStream.putNextEntry(null, parameters); + + if (!parameters.getFileNameInZip().endsWith("/") && + !parameters.getFileNameInZip().endsWith("\\")) { + while ((readLen = inputStream.read(readBuff)) != -1) { + outputStream.write(readBuff, 0, readLen); + } + } + + outputStream.closeEntry(); + outputStream.finish(); + + } catch (ZipException e) { + throw e; + } catch (Exception e) { + throw new ZipException(e); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + //ignore + } + } + } + } + + public void addFolderToZip(File file, ZipParameters parameters, + ProgressMonitor progressMonitor, boolean runInThread) throws ZipException { + if (file == null || parameters == null) { + throw new ZipException("one of the input parameters is null, cannot add folder to zip"); + } + + if (!Zip4jUtil.checkFileExists(file.getAbsolutePath())) { + throw new ZipException("input folder does not exist"); + } + + if (!file.isDirectory()) { + throw new ZipException("input file is not a folder, user addFileToZip method to add files"); + } + + if (!Zip4jUtil.checkFileReadAccess(file.getAbsolutePath())) { + throw new ZipException("cannot read folder: " + file.getAbsolutePath()); + } + + String rootFolderPath = null; + if (parameters.isIncludeRootFolder()) { + if (file.getAbsolutePath() != null) { + rootFolderPath = file.getAbsoluteFile().getParentFile() != null ? file.getAbsoluteFile().getParentFile().getAbsolutePath() : ""; + } else { + rootFolderPath = file.getParentFile() != null ? file.getParentFile().getAbsolutePath() : ""; + } + } else { + rootFolderPath = file.getAbsolutePath(); + } + + parameters.setDefaultFolderPath(rootFolderPath); + + ArrayList fileList = Zip4jUtil.getFilesInDirectoryRec(file, parameters.isReadHiddenFiles()); + + if (parameters.isIncludeRootFolder()) { + if (fileList == null) { + fileList = new ArrayList(); + } + fileList.add(file); + } + + addFiles(fileList, parameters, progressMonitor, runInThread); + } + + + private void checkParameters(ZipParameters parameters) throws ZipException { + + if (parameters == null) { + throw new ZipException("cannot validate zip parameters"); + } + + if ((parameters.getCompressionMethod() != Zip4jConstants.COMP_STORE) && + parameters.getCompressionMethod() != Zip4jConstants.COMP_DEFLATE) { + throw new ZipException("unsupported compression type"); + } + + if (parameters.getCompressionMethod() == Zip4jConstants.COMP_DEFLATE) { + if (parameters.getCompressionLevel() < 0 && parameters.getCompressionLevel() > 9) { + throw new ZipException("invalid compression level. compression level dor deflate should be in the range of 0-9"); + } + } + + if (parameters.isEncryptFiles()) { + if (parameters.getEncryptionMethod() != Zip4jConstants.ENC_METHOD_STANDARD && + parameters.getEncryptionMethod() != Zip4jConstants.ENC_METHOD_AES) { + throw new ZipException("unsupported encryption method"); + } + + if (parameters.getPassword() == null || parameters.getPassword().length <= 0) { + throw new ZipException("input password is empty or null"); + } + } else { + parameters.setAesKeyStrength(-1); + parameters.setEncryptionMethod(-1); + } + + } + + /** + * Before adding a file to a zip file, we check if a file already exists in the zip file + * with the same fileName (including path, if exists). If yes, then we remove this file + * before adding the file

+ * + * Note: Relative path has to be passed as the fileName + * + * @param zipModel + * @param fileName + * @throws ZipException + */ + private void removeFilesIfExists(ArrayList fileList, ZipParameters parameters, ProgressMonitor progressMonitor) throws ZipException { + + if (zipModel == null || zipModel.getCentralDirectory() == null || + zipModel.getCentralDirectory().getFileHeaders() == null || + zipModel.getCentralDirectory().getFileHeaders().size() <= 0) { + //For a new zip file, this condition satisfies, so do nothing + return; + } + RandomAccessFile outputStream = null; + + try { + for (int i = 0; i < fileList.size(); i++) { + File file = (File) fileList.get(i); + + String fileName = Zip4jUtil.getRelativeFileName(file.getAbsolutePath(), + parameters.getRootFolderInZip(), parameters.getDefaultFolderPath()); + + FileHeader fileHeader = Zip4jUtil.getFileHeader(zipModel, fileName); + if (fileHeader != null) { + + if (outputStream != null) { + outputStream.close(); + outputStream = null; + } + + ArchiveMaintainer archiveMaintainer = new ArchiveMaintainer(); + progressMonitor.setCurrentOperation(ProgressMonitor.OPERATION_REMOVE); + HashMap retMap = archiveMaintainer.initRemoveZipFile(zipModel, + fileHeader, progressMonitor); + + if (progressMonitor.isCancelAllTasks()) { + progressMonitor.setResult(ProgressMonitor.RESULT_CANCELLED); + progressMonitor.setState(ProgressMonitor.STATE_READY); + return; + } + + progressMonitor + .setCurrentOperation(ProgressMonitor.OPERATION_ADD); + + if (outputStream == null) { + outputStream = prepareFileOutputStream(); + + if (retMap != null) { + if (retMap.get(InternalZipConstants.OFFSET_CENTRAL_DIR) != null) { + long offsetCentralDir = -1; + try { + offsetCentralDir = Long + .parseLong((String) retMap + .get(InternalZipConstants.OFFSET_CENTRAL_DIR)); + } catch (NumberFormatException e) { + throw new ZipException( + "NumberFormatException while parsing offset central directory. " + + "Cannot update already existing file header"); + } catch (Exception e) { + throw new ZipException( + "Error while parsing offset central directory. " + + "Cannot update already existing file header"); + } + + if (offsetCentralDir >= 0) { + outputStream.seek(offsetCentralDir); + } + } + } + } + } + } + } catch (IOException e) { + throw new ZipException(e); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + //ignore + } + } + } + } + + private RandomAccessFile prepareFileOutputStream() throws ZipException { + String outPath = zipModel.getZipFile(); + if (!Zip4jUtil.isStringNotNullAndNotEmpty(outPath)) { + throw new ZipException("invalid output path"); + } + + try { + File outFile = new File(outPath); + if (!outFile.getParentFile().exists()) { + outFile.getParentFile().mkdirs(); + } + return new RandomAccessFile(outFile, InternalZipConstants.WRITE_MODE); + } catch (FileNotFoundException e) { + throw new ZipException(e); + } + } + + private EndCentralDirRecord createEndOfCentralDirectoryRecord() { + EndCentralDirRecord endCentralDirRecord = new EndCentralDirRecord(); + endCentralDirRecord.setSignature(InternalZipConstants.ENDSIG); + endCentralDirRecord.setNoOfThisDisk(0); + endCentralDirRecord.setTotNoOfEntriesInCentralDir(0); + endCentralDirRecord.setTotNoOfEntriesInCentralDirOnThisDisk(0); + endCentralDirRecord.setOffsetOfStartOfCentralDir(0); + return endCentralDirRecord; + } + + private long calculateTotalWork(ArrayList fileList, ZipParameters parameters) throws ZipException { + if (fileList == null) { + throw new ZipException("file list is null, cannot calculate total work"); + } + + long totalWork = 0; + + for (int i = 0; i < fileList.size(); i++) { + if(fileList.get(i) instanceof File) { + if (((File)fileList.get(i)).exists()) { + if (parameters.isEncryptFiles() && + parameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) { + totalWork += (Zip4jUtil.getFileLengh((File)fileList.get(i)) * 2); + } else { + totalWork += Zip4jUtil.getFileLengh((File)fileList.get(i)); + } + + if (zipModel.getCentralDirectory() != null && + zipModel.getCentralDirectory().getFileHeaders() != null && + zipModel.getCentralDirectory().getFileHeaders().size() > 0) { + String relativeFileName = Zip4jUtil.getRelativeFileName( + ((File)fileList.get(i)).getAbsolutePath(), parameters.getRootFolderInZip(), parameters.getDefaultFolderPath()); + FileHeader fileHeader = Zip4jUtil.getFileHeader(zipModel, relativeFileName); + if (fileHeader != null) { + totalWork += (Zip4jUtil.getFileLengh(new File(zipModel.getZipFile())) - fileHeader.getCompressedSize()); + } + } + } + } + } + + return totalWork; + } +}