content: /*
  * 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 
  * 
  * https://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;
  	}
  }
  