com.feilong.lib.compress.archivers.zip.ZipArchiveOutputStream Maven / Gradle / Ivy
Show all versions of feilong Show documentation
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 com.feilong.lib.compress.archivers.zip;
import static com.feilong.lib.compress.archivers.zip.ZipConstants.DATA_DESCRIPTOR_MIN_VERSION;
import static com.feilong.lib.compress.archivers.zip.ZipConstants.DEFLATE_MIN_VERSION;
import static com.feilong.lib.compress.archivers.zip.ZipConstants.DWORD;
import static com.feilong.lib.compress.archivers.zip.ZipConstants.INITIAL_VERSION;
import static com.feilong.lib.compress.archivers.zip.ZipConstants.SHORT;
import static com.feilong.lib.compress.archivers.zip.ZipConstants.WORD;
import static com.feilong.lib.compress.archivers.zip.ZipConstants.ZIP64_MAGIC;
import static com.feilong.lib.compress.archivers.zip.ZipConstants.ZIP64_MAGIC_SHORT;
import static com.feilong.lib.compress.archivers.zip.ZipConstants.ZIP64_MIN_VERSION;
import static com.feilong.lib.compress.archivers.zip.ZipLong.putLong;
import static com.feilong.lib.compress.archivers.zip.ZipShort.putShort;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.Calendar;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.zip.Deflater;
import java.util.zip.ZipException;
import com.feilong.lib.compress.archivers.ArchiveEntry;
import com.feilong.lib.compress.archivers.ArchiveOutputStream;
import com.feilong.lib.compress.utils.IOUtils;
/**
* Reimplementation of {@link java.util.zip.ZipOutputStream
* java.util.zip.ZipOutputStream} that does handle the extended
* functionality of this package, especially internal/external file
* attributes and extra fields with different layouts for local file
* data and central directory entries.
*
*
* This class will try to use {@link
* java.nio.channels.SeekableByteChannel} when it knows that the
* output is going to go to a file and no split archive shall be
* created.
*
*
*
* If SeekableByteChannel cannot be used, this implementation will use
* a Data Descriptor to store size and CRC information for {@link
* #DEFLATED DEFLATED} entries, this means, you don't need to
* calculate them yourself. Unfortunately this is not possible for
* the {@link #STORED STORED} method, here setting the CRC and
* uncompressed size information is required before {@link
* #putArchiveEntry(ArchiveEntry)} can be called.
*
*
*
* As of Apache Commons Compress 1.3 it transparently supports Zip64
* extensions and thus individual entries and archives larger than 4
* GB or with more than 65536 entries in most cases but explicit
* control is provided via {@link #setUseZip64}. If the stream can not
* use SeekableByteChannel and you try to write a ZipArchiveEntry of
* unknown size then Zip64 extensions will be disabled by default.
*
*
* @NotThreadSafe
*/
public class ZipArchiveOutputStream extends ArchiveOutputStream{
static final int BUFFER_SIZE = 512;
private static final int LFH_SIG_OFFSET = 0;
private static final int LFH_VERSION_NEEDED_OFFSET = 4;
private static final int LFH_GPB_OFFSET = 6;
private static final int LFH_METHOD_OFFSET = 8;
private static final int LFH_TIME_OFFSET = 10;
private static final int LFH_CRC_OFFSET = 14;
private static final int LFH_COMPRESSED_SIZE_OFFSET = 18;
private static final int LFH_ORIGINAL_SIZE_OFFSET = 22;
private static final int LFH_FILENAME_LENGTH_OFFSET = 26;
private static final int LFH_EXTRA_LENGTH_OFFSET = 28;
private static final int LFH_FILENAME_OFFSET = 30;
private static final int CFH_SIG_OFFSET = 0;
private static final int CFH_VERSION_MADE_BY_OFFSET = 4;
private static final int CFH_VERSION_NEEDED_OFFSET = 6;
private static final int CFH_GPB_OFFSET = 8;
private static final int CFH_METHOD_OFFSET = 10;
private static final int CFH_TIME_OFFSET = 12;
private static final int CFH_CRC_OFFSET = 16;
private static final int CFH_COMPRESSED_SIZE_OFFSET = 20;
private static final int CFH_ORIGINAL_SIZE_OFFSET = 24;
private static final int CFH_FILENAME_LENGTH_OFFSET = 28;
private static final int CFH_EXTRA_LENGTH_OFFSET = 30;
private static final int CFH_COMMENT_LENGTH_OFFSET = 32;
private static final int CFH_DISK_NUMBER_OFFSET = 34;
private static final int CFH_INTERNAL_ATTRIBUTES_OFFSET = 36;
private static final int CFH_EXTERNAL_ATTRIBUTES_OFFSET = 38;
private static final int CFH_LFH_OFFSET = 42;
private static final int CFH_FILENAME_OFFSET = 46;
/** indicates if this archive is finished. protected for use in Jar implementation */
protected boolean finished = false;
/**
* Compression method for deflated entries.
*/
public static final int DEFLATED = java.util.zip.ZipEntry.DEFLATED;
/**
* Default compression level for deflated entries.
*/
public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION;
/**
* Compression method for stored entries.
*/
public static final int STORED = java.util.zip.ZipEntry.STORED;
/**
* default encoding for file names and comment.
*/
static final String DEFAULT_ENCODING = ZipEncodingHelper.UTF8;
private static final byte[] EMPTY = new byte[0];
/**
* Current entry.
*/
private CurrentEntry entry;
/**
* The file comment.
*/
private String comment = "";
/**
* Compression level for next entry.
*/
private int level = DEFAULT_COMPRESSION;
/**
* Has the compression level changed when compared to the last
* entry?
*/
private boolean hasCompressionLevelChanged = false;
/**
* Default compression method for next entry.
*/
private int method = java.util.zip.ZipEntry.DEFLATED;
/**
* List of ZipArchiveEntries written so far.
*/
private final List entries = new LinkedList<>();
private final StreamCompressor streamCompressor;
/**
* Start of central directory.
*/
private long cdOffset = 0;
/**
* Length of central directory.
*/
private long cdLength = 0;
/**
* Disk number start of central directory.
*/
private long cdDiskNumberStart = 0;
/**
* Length of end of central directory
*/
private long eocdLength = 0;
/**
* Helper, a 0 as ZipShort.
*/
private static final byte[] ZERO = { 0, 0 };
/**
* Helper, a 0 as ZipLong.
*/
private static final byte[] LZERO = { 0, 0, 0, 0 };
private static final byte[] ONE = ZipLong.getBytes(1L);
/**
* Holds some book-keeping data for each entry.
*/
private final Map metaData = new HashMap<>();
/**
* The encoding to use for file names and the file comment.
*
*
* For a list of possible values see http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html.
* Defaults to UTF-8.
*
*/
private String encoding = DEFAULT_ENCODING;
/**
* The zip encoding to use for file names and the file comment.
*
* This field is of internal use and will be set in {@link
* #setEncoding(String)}.
*/
private ZipEncoding zipEncoding = ZipEncodingHelper.getZipEncoding(DEFAULT_ENCODING);
/**
* This Deflater object is used for output.
*
*/
protected final Deflater def;
/**
* Optional random access output.
*/
private final SeekableByteChannel channel;
private final OutputStream out;
/**
* whether to use the general purpose bit flag when writing UTF-8
* file names or not.
*/
private boolean useUTF8Flag = true;
/**
* Whether to encode non-encodable file names as UTF-8.
*/
private boolean fallbackToUTF8 = false;
/**
* whether to create UnicodePathExtraField-s for each entry.
*/
private UnicodeExtraFieldPolicy createUnicodeExtraFields = UnicodeExtraFieldPolicy.NEVER;
/**
* Whether anything inside this archive has used a ZIP64 feature.
*
* @since 1.3
*/
private boolean hasUsedZip64 = false;
private Zip64Mode zip64Mode = Zip64Mode.AsNeeded;
private final byte[] copyBuffer = new byte[32768];
private final Calendar calendarInstance = Calendar.getInstance();
/**
* Whether we are creating a split zip
*/
private final boolean isSplitZip;
/**
* Holds the number of Central Directories on each disk, this is used
* when writing Zip64 End Of Central Directory and End Of Central Directory
*/
private final Map numberOfCDInDiskData = new HashMap<>();
/**
* Creates a new ZIP OutputStream filtering the underlying stream.
*
* @param out
* the outputstream to zip
*/
public ZipArchiveOutputStream(final OutputStream out){
this.out = out;
this.channel = null;
def = new Deflater(level, true);
streamCompressor = StreamCompressor.create(out, def);
isSplitZip = false;
}
/**
* Creates a new ZIP OutputStream writing to a File. Will use
* random access if possible.
*
* @param file
* the file to zip to
* @throws IOException
* on error
*/
public ZipArchiveOutputStream(final File file) throws IOException{
def = new Deflater(level, true);
OutputStream o = null;
SeekableByteChannel _channel = null;
StreamCompressor _streamCompressor = null;
try{
_channel = Files.newByteChannel(
file.toPath(),
EnumSet.of(
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.READ,
StandardOpenOption.TRUNCATE_EXISTING));
// will never get opened properly when an exception is thrown so doesn't need to get closed
_streamCompressor = StreamCompressor.create(_channel, def); //NOSONAR
}catch (final IOException e){ // NOSONAR
IOUtils.closeQuietly(_channel);
_channel = null;
o = new FileOutputStream(file);
_streamCompressor = StreamCompressor.create(o, def);
}
out = o;
channel = _channel;
streamCompressor = _streamCompressor;
isSplitZip = false;
}
/**
* Creates a split ZIP Archive.
*
*
* The files making up the archive will use Z01, Z02,
* ... extensions and the last part of it will be the given {@code
* file}.
*
*
*
* Even though the stream writes to a file this stream will
* behave as if no random access was possible. This means the
* sizes of stored entries need to be known before the actual
* entry data is written.
*
*
* @param file
* the file that will become the last part of the split archive
* @param zipSplitSize
* maximum size of a single part of the split
* archive created by this stream. Must be between 64kB and about
* 4GB.
*
* @throws IOException
* on error
* @throws IllegalArgumentException
* if zipSplitSize is not in the required range
* @since 1.20
*/
public ZipArchiveOutputStream(final File file, final long zipSplitSize) throws IOException{
def = new Deflater(level, true);
this.out = new ZipSplitOutputStream(file, zipSplitSize);
streamCompressor = StreamCompressor.create(this.out, def);
channel = null;
isSplitZip = true;
}
/**
* Creates a new ZIP OutputStream writing to a SeekableByteChannel.
*
*
* {@link
* com.feilong.lib.compress.utils.SeekableInMemoryByteChannel}
* allows you to write to an in-memory archive using random
* access.
*
*
* @param channel
* the channel to zip to
* @throws IOException
* on error
* @since 1.13
*/
public ZipArchiveOutputStream(SeekableByteChannel channel) throws IOException{
this.channel = channel;
def = new Deflater(level, true);
streamCompressor = StreamCompressor.create(channel, def);
out = null;
isSplitZip = false;
}
/**
* This method indicates whether this archive is writing to a
* seekable stream (i.e., to a random access file).
*
*
* For seekable streams, you don't need to calculate the CRC or
* uncompressed size for {@link #STORED} entries before
* invoking {@link #putArchiveEntry(ArchiveEntry)}.
*
* @return true if seekable
*/
public boolean isSeekable(){
return channel != null;
}
/**
* The encoding to use for file names and the file comment.
*
*
* For a list of possible values see http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html.
* Defaults to UTF-8.
*
*
* @param encoding
* the encoding to use for file names, use null
* for the platform's default encoding
*/
public void setEncoding(final String encoding){
this.encoding = encoding;
this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
if (useUTF8Flag && !ZipEncodingHelper.isUTF8(encoding)){
useUTF8Flag = false;
}
}
/**
* The encoding to use for file names and the file comment.
*
* @return null if using the platform's default character encoding.
*/
public String getEncoding(){
return encoding;
}
/**
* Whether to set the language encoding flag if the file name
* encoding is UTF-8.
*
*
* Defaults to true.
*
*
* @param b
* whether to set the language encoding flag if the file
* name encoding is UTF-8
*/
public void setUseLanguageEncodingFlag(final boolean b){
useUTF8Flag = b && ZipEncodingHelper.isUTF8(encoding);
}
/**
* Whether to create Unicode Extra Fields.
*
*
* Defaults to NEVER.
*
*
* @param b
* whether to create Unicode Extra Fields.
*/
public void setCreateUnicodeExtraFields(final UnicodeExtraFieldPolicy b){
createUnicodeExtraFields = b;
}
/**
* Whether to fall back to UTF and the language encoding flag if
* the file name cannot be encoded using the specified encoding.
*
*
* Defaults to false.
*
*
* @param b
* whether to fall back to UTF and the language encoding
* flag if the file name cannot be encoded using the specified
* encoding.
*/
public void setFallbackToUTF8(final boolean b){
fallbackToUTF8 = b;
}
/**
* Whether Zip64 extensions will be used.
*
*
* When setting the mode to {@link Zip64Mode#Never Never},
* {@link #putArchiveEntry}, {@link #closeArchiveEntry}, {@link
* #finish} or {@link #close} may throw a {@link
* Zip64RequiredException} if the entry's size or the total size
* of the archive exceeds 4GB or there are more than 65536 entries
* inside the archive. Any archive created in this mode will be
* readable by implementations that don't support Zip64.
*
*
*
* When setting the mode to {@link Zip64Mode#Always Always},
* Zip64 extensions will be used for all entries. Any archive
* created in this mode may be unreadable by implementations that
* don't support Zip64 even if all its contents would be.
*
*
*
* When setting the mode to {@link Zip64Mode#AsNeeded
* AsNeeded}, Zip64 extensions will transparently be used for
* those entries that require them. This mode can only be used if
* the uncompressed size of the {@link ZipArchiveEntry} is known
* when calling {@link #putArchiveEntry} or the archive is written
* to a seekable output (i.e. you have used the {@link
* #ZipArchiveOutputStream(java.io.File) File-arg constructor}) -
* this mode is not valid when the output stream is not seekable
* and the uncompressed size is unknown when {@link
* #putArchiveEntry} is called.
*
*
*
* If no entry inside the resulting archive requires Zip64
* extensions then {@link Zip64Mode#Never Never} will create the
* smallest archive. {@link Zip64Mode#AsNeeded AsNeeded} will
* create a slightly bigger archive if the uncompressed size of
* any entry has initially been unknown and create an archive
* identical to {@link Zip64Mode#Never Never} otherwise. {@link
* Zip64Mode#Always Always} will create an archive that is at
* least 24 bytes per entry bigger than the one {@link
* Zip64Mode#Never Never} would create.
*
*
*
* Defaults to {@link Zip64Mode#AsNeeded AsNeeded} unless
* {@link #putArchiveEntry} is called with an entry of unknown
* size and data is written to a non-seekable stream - in this
* case the default is {@link Zip64Mode#Never Never}.
*
*
* @since 1.3
* @param mode
* Whether Zip64 extensions will be used.
*/
public void setUseZip64(final Zip64Mode mode){
zip64Mode = mode;
}
/**
* {@inheritDoc}
*
* @throws Zip64RequiredException
* if the archive's size exceeds 4
* GByte or there are more than 65535 entries inside the archive
* and {@link #setUseZip64} is {@link Zip64Mode#Never}.
*/
@Override
public void finish() throws IOException{
if (finished){
throw new IOException("This archive has already been finished");
}
if (entry != null){
throw new IOException("This archive contains unclosed entries.");
}
long cdOverallOffset = streamCompressor.getTotalBytesWritten();
cdOffset = cdOverallOffset;
if (isSplitZip){
// when creating a split zip, the offset should be
// the offset to the corresponding segment disk
ZipSplitOutputStream zipSplitOutputStream = (ZipSplitOutputStream) this.out;
cdOffset = zipSplitOutputStream.getCurrentSplitSegmentBytesWritten();
cdDiskNumberStart = zipSplitOutputStream.getCurrentSplitSegmentIndex();
}
writeCentralDirectoryInChunks();
cdLength = streamCompressor.getTotalBytesWritten() - cdOverallOffset;
// calculate the length of end of central directory, as it may be used in writeZip64CentralDirectory
final ByteBuffer commentData = this.zipEncoding.encode(comment);
final long commentLength = (long) commentData.limit() - commentData.position();
eocdLength = WORD /* length of EOCD_SIG */
+ SHORT /* number of this disk */
+ SHORT /* disk number of start of central directory */
+ SHORT /* total number of entries on this disk */
+ SHORT /* total number of entries */
+ WORD /* size of central directory */
+ WORD /* offset of start of central directory */
+ SHORT /* zip comment length */
+ commentLength /* zip comment */;
writeZip64CentralDirectory();
writeCentralDirectoryEnd();
metaData.clear();
entries.clear();
streamCompressor.close();
if (isSplitZip){
// trigger the ZipSplitOutputStream to write the final split segment
out.close();
}
finished = true;
}
private void writeCentralDirectoryInChunks() throws IOException{
final int NUM_PER_WRITE = 1000;
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(70 * NUM_PER_WRITE);
int count = 0;
for (final ZipArchiveEntry ze : entries){
byteArrayOutputStream.write(createCentralFileHeader(ze));
if (++count > NUM_PER_WRITE){
writeCounted(byteArrayOutputStream.toByteArray());
byteArrayOutputStream.reset();
count = 0;
}
}
writeCounted(byteArrayOutputStream.toByteArray());
}
/**
* Writes all necessary data for this entry.
*
* @throws IOException
* on error
* @throws Zip64RequiredException
* if the entry's uncompressed or
* compressed size exceeds 4 GByte and {@link #setUseZip64}
* is {@link Zip64Mode#Never}.
*/
@Override
public void closeArchiveEntry() throws IOException{
preClose();
flushDeflater();
final long bytesWritten = streamCompressor.getTotalBytesWritten() - entry.dataStart;
final long realCrc = streamCompressor.getCrc32();
entry.bytesRead = streamCompressor.getBytesRead();
final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry);
final boolean actuallyNeedsZip64 = handleSizesAndCrc(bytesWritten, realCrc, effectiveMode);
closeEntry(actuallyNeedsZip64, false);
streamCompressor.reset();
}
/**
* Writes all necessary data for this entry.
*
* @param phased
* This entry is second phase of a 2-phase zip creation, size, compressed size and crc
* are known in ZipArchiveEntry
* @throws IOException
* on error
* @throws Zip64RequiredException
* if the entry's uncompressed or
* compressed size exceeds 4 GByte and {@link #setUseZip64}
* is {@link Zip64Mode#Never}.
*/
private void closeCopiedEntry(final boolean phased) throws IOException{
preClose();
entry.bytesRead = entry.entry.getSize();
final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry);
final boolean actuallyNeedsZip64 = checkIfNeedsZip64(effectiveMode);
closeEntry(actuallyNeedsZip64, phased);
}
private void closeEntry(final boolean actuallyNeedsZip64,final boolean phased) throws IOException{
if (!phased && channel != null){
rewriteSizesAndCrc(actuallyNeedsZip64);
}
if (!phased){
writeDataDescriptor(entry.entry);
}
entry = null;
}
private void preClose() throws IOException{
if (finished){
throw new IOException("Stream has already been finished");
}
if (entry == null){
throw new IOException("No current entry to close");
}
if (!entry.hasWritten){
write(EMPTY, 0, 0);
}
}
/**
* Adds an archive entry with a raw input stream.
*
* If crc, size and compressed size are supplied on the entry, these values will be used as-is.
* Zip64 status is re-established based on the settings in this stream, and the supplied value
* is ignored.
*
* The entry is put and closed immediately.
*
* @param entry
* The archive entry to add
* @param rawStream
* The raw input stream of a different entry. May be compressed/encrypted.
* @throws IOException
* If copying fails
*/
public void addRawArchiveEntry(final ZipArchiveEntry entry,final InputStream rawStream) throws IOException{
final ZipArchiveEntry ae = new ZipArchiveEntry(entry);
if (hasZip64Extra(ae)){
// Will be re-added as required. this may make the file generated with this method
// somewhat smaller than standard mode,
// since standard mode is unable to remove the zip 64 header.
ae.removeExtraField(Zip64ExtendedInformationExtraField.HEADER_ID);
}
final boolean is2PhaseSource = ae.getCrc() != ZipArchiveEntry.CRC_UNKNOWN && ae.getSize() != ArchiveEntry.SIZE_UNKNOWN
&& ae.getCompressedSize() != ArchiveEntry.SIZE_UNKNOWN;
putArchiveEntry(ae, is2PhaseSource);
copyFromZipInputStream(rawStream);
closeCopiedEntry(is2PhaseSource);
}
/**
* Ensures all bytes sent to the deflater are written to the stream.
*/
private void flushDeflater() throws IOException{
if (entry.entry.getMethod() == DEFLATED){
streamCompressor.flushDeflater();
}
}
/**
* Ensures the current entry's size and CRC information is set to
* the values just written, verifies it isn't too big in the
* Zip64Mode.Never case and returns whether the entry would
* require a Zip64 extra field.
*/
private boolean handleSizesAndCrc(final long bytesWritten,final long crc,final Zip64Mode effectiveMode) throws ZipException{
if (entry.entry.getMethod() == DEFLATED){
/*
* It turns out def.getBytesRead() returns wrong values if
* the size exceeds 4 GB on Java < Java7
* entry.entry.setSize(def.getBytesRead());
*/
entry.entry.setSize(entry.bytesRead);
entry.entry.setCompressedSize(bytesWritten);
entry.entry.setCrc(crc);
}else if (channel == null){
if (entry.entry.getCrc() != crc){
throw new ZipException(
"Bad CRC checksum for entry " + entry.entry.getName() + ": " + Long.toHexString(entry.entry.getCrc())
+ " instead of " + Long.toHexString(crc));
}
if (entry.entry.getSize() != bytesWritten){
throw new ZipException(
"Bad size for entry " + entry.entry.getName() + ": " + entry.entry.getSize() + " instead of "
+ bytesWritten);
}
}else{ /* method is STORED and we used SeekableByteChannel */
entry.entry.setSize(bytesWritten);
entry.entry.setCompressedSize(bytesWritten);
entry.entry.setCrc(crc);
}
return checkIfNeedsZip64(effectiveMode);
}
/**
* Verifies the sizes aren't too big in the Zip64Mode.Never case
* and returns whether the entry would require a Zip64 extra
* field.
*/
private boolean checkIfNeedsZip64(final Zip64Mode effectiveMode) throws ZipException{
final boolean actuallyNeedsZip64 = isZip64Required(entry.entry, effectiveMode);
if (actuallyNeedsZip64 && effectiveMode == Zip64Mode.Never){
throw new Zip64RequiredException(Zip64RequiredException.getEntryTooBigMessage(entry.entry));
}
return actuallyNeedsZip64;
}
private boolean isZip64Required(final ZipArchiveEntry entry1,final Zip64Mode requestedMode){
return requestedMode == Zip64Mode.Always || isTooLageForZip32(entry1);
}
private boolean isTooLageForZip32(final ZipArchiveEntry zipArchiveEntry){
return zipArchiveEntry.getSize() >= ZIP64_MAGIC || zipArchiveEntry.getCompressedSize() >= ZIP64_MAGIC;
}
/**
* When using random access output, write the local file header
* and potentiall the ZIP64 extra containing the correct CRC and
* compressed/uncompressed sizes.
*/
private void rewriteSizesAndCrc(final boolean actuallyNeedsZip64) throws IOException{
final long save = channel.position();
channel.position(entry.localDataStart);
writeOut(ZipLong.getBytes(entry.entry.getCrc()));
if (!hasZip64Extra(entry.entry) || !actuallyNeedsZip64){
writeOut(ZipLong.getBytes(entry.entry.getCompressedSize()));
writeOut(ZipLong.getBytes(entry.entry.getSize()));
}else{
writeOut(ZipLong.ZIP64_MAGIC.getBytes());
writeOut(ZipLong.ZIP64_MAGIC.getBytes());
}
if (hasZip64Extra(entry.entry)){
final ByteBuffer name = getName(entry.entry);
final int nameLen = name.limit() - name.position();
// seek to ZIP64 extra, skip header and size information
channel.position(entry.localDataStart + 3 * WORD + 2 * SHORT + nameLen + 2 * SHORT);
// inside the ZIP64 extra uncompressed size comes
// first, unlike the LFH, CD or data descriptor
writeOut(ZipEightByteInteger.getBytes(entry.entry.getSize()));
writeOut(ZipEightByteInteger.getBytes(entry.entry.getCompressedSize()));
if (!actuallyNeedsZip64){
// do some cleanup:
// * rewrite version needed to extract
channel.position(entry.localDataStart - 5 * SHORT);
writeOut(ZipShort.getBytes(versionNeededToExtract(entry.entry.getMethod(), false, false)));
// * remove ZIP64 extra so it doesn't get written
// to the central directory
entry.entry.removeExtraField(Zip64ExtendedInformationExtraField.HEADER_ID);
entry.entry.setExtra();
// * reset hasUsedZip64 if it has been set because
// of this entry
if (entry.causedUseOfZip64){
hasUsedZip64 = false;
}
}
}
channel.position(save);
}
/**
* {@inheritDoc}
*
* @throws ClassCastException
* if entry is not an instance of ZipArchiveEntry
* @throws Zip64RequiredException
* if the entry's uncompressed or
* compressed size is known to exceed 4 GByte and {@link #setUseZip64}
* is {@link Zip64Mode#Never}.
*/
@Override
public void putArchiveEntry(final ArchiveEntry archiveEntry) throws IOException{
putArchiveEntry(archiveEntry, false);
}
/**
* Writes the headers for an archive entry to the output stream.
* The caller must then write the content to the stream and call
* {@link #closeArchiveEntry()} to complete the process.
*
* @param archiveEntry
* The archiveEntry
* @param phased
* If true size, compressedSize and crc required to be known up-front in the archiveEntry
* @throws ClassCastException
* if entry is not an instance of ZipArchiveEntry
* @throws Zip64RequiredException
* if the entry's uncompressed or
* compressed size is known to exceed 4 GByte and {@link #setUseZip64}
* is {@link Zip64Mode#Never}.
*/
private void putArchiveEntry(final ArchiveEntry archiveEntry,final boolean phased) throws IOException{
if (finished){
throw new IOException("Stream has already been finished");
}
if (entry != null){
closeArchiveEntry();
}
entry = new CurrentEntry((ZipArchiveEntry) archiveEntry);
entries.add(entry.entry);
setDefaults(entry.entry);
final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry);
validateSizeInformation(effectiveMode);
if (shouldAddZip64Extra(entry.entry, effectiveMode)){
final Zip64ExtendedInformationExtraField z64 = getZip64Extra(entry.entry);
ZipEightByteInteger size;
ZipEightByteInteger compressedSize;
if (phased){
// sizes are already known
size = new ZipEightByteInteger(entry.entry.getSize());
compressedSize = new ZipEightByteInteger(entry.entry.getCompressedSize());
}else if (entry.entry.getMethod() == STORED && entry.entry.getSize() != ArchiveEntry.SIZE_UNKNOWN){
// actually, we already know the sizes
compressedSize = size = new ZipEightByteInteger(entry.entry.getSize());
}else{
// just a placeholder, real data will be in data
// descriptor or inserted later via SeekableByteChannel
compressedSize = size = ZipEightByteInteger.ZERO;
}
z64.setSize(size);
z64.setCompressedSize(compressedSize);
entry.entry.setExtra();
}
if (entry.entry.getMethod() == DEFLATED && hasCompressionLevelChanged){
def.setLevel(level);
hasCompressionLevelChanged = false;
}
writeLocalFileHeader((ZipArchiveEntry) archiveEntry, phased);
}
/**
* Provides default values for compression method and last
* modification time.
*/
private void setDefaults(final ZipArchiveEntry entry){
if (entry.getMethod() == -1){ // not specified
entry.setMethod(method);
}
if (entry.getTime() == -1){ // not specified
entry.setTime(System.currentTimeMillis());
}
}
/**
* Throws an exception if the size is unknown for a stored entry
* that is written to a non-seekable output or the entry is too
* big to be written without Zip64 extra but the mode has been set
* to Never.
*/
private void validateSizeInformation(final Zip64Mode effectiveMode) throws ZipException{
// Size/CRC not required if SeekableByteChannel is used
if (entry.entry.getMethod() == STORED && channel == null){
if (entry.entry.getSize() == ArchiveEntry.SIZE_UNKNOWN){
throw new ZipException("Uncompressed size is required for" + " STORED method when not writing to a" + " file");
}
if (entry.entry.getCrc() == ZipArchiveEntry.CRC_UNKNOWN){
throw new ZipException("CRC checksum is required for STORED" + " method when not writing to a file");
}
entry.entry.setCompressedSize(entry.entry.getSize());
}
if ((entry.entry.getSize() >= ZIP64_MAGIC || entry.entry.getCompressedSize() >= ZIP64_MAGIC) && effectiveMode == Zip64Mode.Never){
throw new Zip64RequiredException(Zip64RequiredException.getEntryTooBigMessage(entry.entry));
}
}
/**
* Whether to addd a Zip64 extended information extra field to the
* local file header.
*
*
* Returns true if
*
*
*
* - mode is Always
* - or we already know it is going to be needed
* - or the size is unknown and we can ensure it won't hurt
* other implementations if we add it (i.e. we can erase its
* usage
*
*/
private boolean shouldAddZip64Extra(final ZipArchiveEntry entry,final Zip64Mode mode){
return mode == Zip64Mode.Always || entry.getSize() >= ZIP64_MAGIC || entry.getCompressedSize() >= ZIP64_MAGIC
|| (entry.getSize() == ArchiveEntry.SIZE_UNKNOWN && channel != null && mode != Zip64Mode.Never);
}
/**
* Set the file comment.
*
* @param comment
* the comment
*/
public void setComment(final String comment){
this.comment = comment;
}
/**
* Sets the compression level for subsequent entries.
*
*
* Default is Deflater.DEFAULT_COMPRESSION.
*
*
* @param level
* the compression level.
* @throws IllegalArgumentException
* if an invalid compression
* level is specified.
*/
public void setLevel(final int level){
if (level < Deflater.DEFAULT_COMPRESSION || level > Deflater.BEST_COMPRESSION){
throw new IllegalArgumentException("Invalid compression level: " + level);
}
if (this.level == level){
return;
}
hasCompressionLevelChanged = true;
this.level = level;
}
/**
* Sets the default compression method for subsequent entries.
*
*
* Default is DEFLATED.
*
*
* @param method
* an int
from java.util.zip.ZipEntry
*/
public void setMethod(final int method){
this.method = method;
}
/**
* Whether this stream is able to write the given entry.
*
*
* May return false if it is set up to use encryption or a
* compression method that hasn't been implemented yet.
*
*
* @since 1.1
*/
@Override
public boolean canWriteEntryData(final ArchiveEntry ae){
if (ae instanceof ZipArchiveEntry){
final ZipArchiveEntry zae = (ZipArchiveEntry) ae;
return zae.getMethod() != ZipMethod.IMPLODING.getCode() && zae.getMethod() != ZipMethod.UNSHRINKING.getCode()
&& ZipUtil.canHandleEntryData(zae);
}
return false;
}
/**
* Writes bytes to ZIP entry.
*
* @param b
* the byte array to write
* @param offset
* the start position to write from
* @param length
* the number of bytes to write
* @throws IOException
* on error
*/
@Override
public void write(final byte[] b,final int offset,final int length) throws IOException{
if (entry == null){
throw new IllegalStateException("No current entry");
}
ZipUtil.checkRequestedFeatures(entry.entry);
final long writtenThisTime = streamCompressor.write(b, offset, length, entry.entry.getMethod());
count(writtenThisTime);
}
/**
* Write bytes to output or random access file.
*
* @param data
* the byte array to write
* @throws IOException
* on error
*/
private void writeCounted(final byte[] data) throws IOException{
streamCompressor.writeCounted(data);
}
private void copyFromZipInputStream(final InputStream src) throws IOException{
if (entry == null){
throw new IllegalStateException("No current entry");
}
ZipUtil.checkRequestedFeatures(entry.entry);
entry.hasWritten = true;
int length;
while ((length = src.read(copyBuffer)) >= 0){
streamCompressor.writeCounted(copyBuffer, 0, length);
count(length);
}
}
/**
* Closes this output stream and releases any system resources
* associated with the stream.
*
* @throws IOException
* if an I/O error occurs.
* @throws Zip64RequiredException
* if the archive's size exceeds 4
* GByte or there are more than 65535 entries inside the archive
* and {@link #setUseZip64} is {@link Zip64Mode#Never}.
*/
@Override
public void close() throws IOException{
try{
if (!finished){
finish();
}
}finally{
destroy();
}
}
/**
* Flushes this output stream and forces any buffered output bytes
* to be written out to the stream.
*
* @throws IOException
* if an I/O error occurs.
*/
@Override
public void flush() throws IOException{
if (out != null){
out.flush();
}
}
/*
* Various ZIP constants shared between this class, ZipArchiveInputStream and ZipFile
*/
/**
* local file header signature
*/
static final byte[] LFH_SIG = ZipLong.LFH_SIG.getBytes(); //NOSONAR
/**
* data descriptor signature
*/
static final byte[] DD_SIG = ZipLong.DD_SIG.getBytes(); //NOSONAR
/**
* central file header signature
*/
static final byte[] CFH_SIG = ZipLong.CFH_SIG.getBytes(); //NOSONAR
/**
* end of central dir signature
*/
static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L); //NOSONAR
/**
* ZIP64 end of central dir signature
*/
static final byte[] ZIP64_EOCD_SIG = ZipLong.getBytes(0X06064B50L); //NOSONAR
/**
* ZIP64 end of central dir locator signature
*/
static final byte[] ZIP64_EOCD_LOC_SIG = ZipLong.getBytes(0X07064B50L); //NOSONAR
/**
* Writes next block of compressed data to the output stream.
*
* @throws IOException
* on error
*/
protected final void deflate() throws IOException{
streamCompressor.deflate();
}
/**
* Writes the local file header entry
*
* @param ze
* the entry to write
* @throws IOException
* on error
*/
protected void writeLocalFileHeader(final ZipArchiveEntry ze) throws IOException{
writeLocalFileHeader(ze, false);
}
private void writeLocalFileHeader(final ZipArchiveEntry ze,final boolean phased) throws IOException{
final boolean encodable = zipEncoding.canEncode(ze.getName());
final ByteBuffer name = getName(ze);
if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER){
addUnicodeExtraFields(ze, encodable, name);
}
long localHeaderStart = streamCompressor.getTotalBytesWritten();
if (isSplitZip){
// when creating a split zip, the offset should be
// the offset to the corresponding segment disk
ZipSplitOutputStream splitOutputStream = (ZipSplitOutputStream) this.out;
ze.setDiskNumberStart(splitOutputStream.getCurrentSplitSegmentIndex());
localHeaderStart = splitOutputStream.getCurrentSplitSegmentBytesWritten();
}
final byte[] localHeader = createLocalFileHeader(ze, name, encodable, phased, localHeaderStart);
metaData.put(ze, new EntryMetaData(localHeaderStart, usesDataDescriptor(ze.getMethod(), phased)));
entry.localDataStart = localHeaderStart + LFH_CRC_OFFSET; // At crc offset
writeCounted(localHeader);
entry.dataStart = streamCompressor.getTotalBytesWritten();
}
private byte[] createLocalFileHeader(
final ZipArchiveEntry ze,
final ByteBuffer name,
final boolean encodable,
final boolean phased,
long archiveOffset){
ResourceAlignmentExtraField oldAlignmentEx = (ResourceAlignmentExtraField) ze.getExtraField(ResourceAlignmentExtraField.ID);
if (oldAlignmentEx != null){
ze.removeExtraField(ResourceAlignmentExtraField.ID);
}
int alignment = ze.getAlignment();
if (alignment <= 0 && oldAlignmentEx != null){
alignment = oldAlignmentEx.getAlignment();
}
if (alignment > 1 || (oldAlignmentEx != null && !oldAlignmentEx.allowMethodChange())){
int oldLength = LFH_FILENAME_OFFSET + name.limit() - name.position() + ze.getLocalFileDataExtra().length;
int padding = (int) ((-archiveOffset - oldLength - ZipExtraField.EXTRAFIELD_HEADER_SIZE - ResourceAlignmentExtraField.BASE_SIZE)
& (alignment - 1));
ze.addExtraField(
new ResourceAlignmentExtraField(
alignment,
oldAlignmentEx != null && oldAlignmentEx.allowMethodChange(),
padding));
}
final byte[] extra = ze.getLocalFileDataExtra();
final int nameLen = name.limit() - name.position();
final int len = LFH_FILENAME_OFFSET + nameLen + extra.length;
final byte[] buf = new byte[len];
System.arraycopy(LFH_SIG, 0, buf, LFH_SIG_OFFSET, WORD);
//store method in local variable to prevent multiple method calls
final int zipMethod = ze.getMethod();
final boolean dataDescriptor = usesDataDescriptor(zipMethod, phased);
putShort(versionNeededToExtract(zipMethod, hasZip64Extra(ze), dataDescriptor), buf, LFH_VERSION_NEEDED_OFFSET);
final GeneralPurposeBit generalPurposeBit = getGeneralPurposeBits(!encodable && fallbackToUTF8, dataDescriptor);
generalPurposeBit.encode(buf, LFH_GPB_OFFSET);
// compression method
putShort(zipMethod, buf, LFH_METHOD_OFFSET);
ZipUtil.toDosTime(calendarInstance, ze.getTime(), buf, LFH_TIME_OFFSET);
// CRC
if (phased){
putLong(ze.getCrc(), buf, LFH_CRC_OFFSET);
}else if (zipMethod == DEFLATED || channel != null){
System.arraycopy(LZERO, 0, buf, LFH_CRC_OFFSET, WORD);
}else{
putLong(ze.getCrc(), buf, LFH_CRC_OFFSET);
}
// compressed length
// uncompressed length
if (hasZip64Extra(entry.entry)){
// point to ZIP64 extended information extra field for
// sizes, may get rewritten once sizes are known if
// stream is seekable
ZipLong.ZIP64_MAGIC.putLong(buf, LFH_COMPRESSED_SIZE_OFFSET);
ZipLong.ZIP64_MAGIC.putLong(buf, LFH_ORIGINAL_SIZE_OFFSET);
}else if (phased){
putLong(ze.getCompressedSize(), buf, LFH_COMPRESSED_SIZE_OFFSET);
putLong(ze.getSize(), buf, LFH_ORIGINAL_SIZE_OFFSET);
}else if (zipMethod == DEFLATED || channel != null){
System.arraycopy(LZERO, 0, buf, LFH_COMPRESSED_SIZE_OFFSET, WORD);
System.arraycopy(LZERO, 0, buf, LFH_ORIGINAL_SIZE_OFFSET, WORD);
}else{ // Stored
putLong(ze.getSize(), buf, LFH_COMPRESSED_SIZE_OFFSET);
putLong(ze.getSize(), buf, LFH_ORIGINAL_SIZE_OFFSET);
}
// file name length
putShort(nameLen, buf, LFH_FILENAME_LENGTH_OFFSET);
// extra field length
putShort(extra.length, buf, LFH_EXTRA_LENGTH_OFFSET);
// file name
System.arraycopy(name.array(), name.arrayOffset(), buf, LFH_FILENAME_OFFSET, nameLen);
// extra fields
System.arraycopy(extra, 0, buf, LFH_FILENAME_OFFSET + nameLen, extra.length);
return buf;
}
/**
* Adds UnicodeExtra fields for name and file comment if mode is
* ALWAYS or the data cannot be encoded using the configured
* encoding.
*/
private void addUnicodeExtraFields(final ZipArchiveEntry ze,final boolean encodable,final ByteBuffer name) throws IOException{
if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS || !encodable){
ze.addExtraField(new UnicodePathExtraField(ze.getName(), name.array(), name.arrayOffset(), name.limit() - name.position()));
}
final String comm = ze.getComment();
if (comm != null && !"".equals(comm)){
final boolean commentEncodable = zipEncoding.canEncode(comm);
if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS || !commentEncodable){
final ByteBuffer commentB = getEntryEncoding(ze).encode(comm);
ze.addExtraField(
new UnicodeCommentExtraField(
comm,
commentB.array(),
commentB.arrayOffset(),
commentB.limit() - commentB.position()));
}
}
}
/**
* Writes the data descriptor entry.
*
* @param ze
* the entry to write
* @throws IOException
* on error
*/
protected void writeDataDescriptor(final ZipArchiveEntry ze) throws IOException{
if (!usesDataDescriptor(ze.getMethod(), false)){
return;
}
writeCounted(DD_SIG);
writeCounted(ZipLong.getBytes(ze.getCrc()));
if (!hasZip64Extra(ze)){
writeCounted(ZipLong.getBytes(ze.getCompressedSize()));
writeCounted(ZipLong.getBytes(ze.getSize()));
}else{
writeCounted(ZipEightByteInteger.getBytes(ze.getCompressedSize()));
writeCounted(ZipEightByteInteger.getBytes(ze.getSize()));
}
}
/**
* Writes the central file header entry.
*
* @param ze
* the entry to write
* @throws IOException
* on error
* @throws Zip64RequiredException
* if the archive's size exceeds 4
* GByte and {@link Zip64Mode #setUseZip64} is {@link
* Zip64Mode#Never}.
*/
protected void writeCentralFileHeader(final ZipArchiveEntry ze) throws IOException{
final byte[] centralFileHeader = createCentralFileHeader(ze);
writeCounted(centralFileHeader);
}
private byte[] createCentralFileHeader(final ZipArchiveEntry ze) throws IOException{
final EntryMetaData entryMetaData = metaData.get(ze);
final boolean needsZip64Extra = hasZip64Extra(ze) || ze.getCompressedSize() >= ZIP64_MAGIC || ze.getSize() >= ZIP64_MAGIC
|| entryMetaData.offset >= ZIP64_MAGIC || ze.getDiskNumberStart() >= ZIP64_MAGIC_SHORT
|| zip64Mode == Zip64Mode.Always;
if (needsZip64Extra && zip64Mode == Zip64Mode.Never){
// must be the offset that is too big, otherwise an
// exception would have been throw in putArchiveEntry or
// closeArchiveEntry
throw new Zip64RequiredException(Zip64RequiredException.ARCHIVE_TOO_BIG_MESSAGE);
}
handleZip64Extra(ze, entryMetaData.offset, needsZip64Extra);
return createCentralFileHeader(ze, getName(ze), entryMetaData, needsZip64Extra);
}
/**
* Writes the central file header entry.
*
* @param ze
* the entry to write
* @param name
* The encoded name
* @param entryMetaData
* meta data for this file
* @throws IOException
* on error
*/
private byte[] createCentralFileHeader(
final ZipArchiveEntry ze,
final ByteBuffer name,
final EntryMetaData entryMetaData,
final boolean needsZip64Extra) throws IOException{
if (isSplitZip){
// calculate the disk number for every central file header,
// this will be used in writing End Of Central Directory and Zip64 End Of Central Directory
int currentSplitSegment = ((ZipSplitOutputStream) this.out).getCurrentSplitSegmentIndex();
if (numberOfCDInDiskData.get(currentSplitSegment) == null){
numberOfCDInDiskData.put(currentSplitSegment, 1);
}else{
int originalNumberOfCD = numberOfCDInDiskData.get(currentSplitSegment);
numberOfCDInDiskData.put(currentSplitSegment, originalNumberOfCD + 1);
}
}
final byte[] extra = ze.getCentralDirectoryExtra();
// file comment length
String comm = ze.getComment();
if (comm == null){
comm = "";
}
final ByteBuffer commentB = getEntryEncoding(ze).encode(comm);
final int nameLen = name.limit() - name.position();
final int commentLen = commentB.limit() - commentB.position();
final int len = CFH_FILENAME_OFFSET + nameLen + extra.length + commentLen;
final byte[] buf = new byte[len];
System.arraycopy(CFH_SIG, 0, buf, CFH_SIG_OFFSET, WORD);
// version made by
// CheckStyle:MagicNumber OFF
putShort(
(ze.getPlatform() << 8) | (!hasUsedZip64 ? DATA_DESCRIPTOR_MIN_VERSION : ZIP64_MIN_VERSION),
buf,
CFH_VERSION_MADE_BY_OFFSET);
final int zipMethod = ze.getMethod();
final boolean encodable = zipEncoding.canEncode(ze.getName());
putShort(versionNeededToExtract(zipMethod, needsZip64Extra, entryMetaData.usesDataDescriptor), buf, CFH_VERSION_NEEDED_OFFSET);
getGeneralPurposeBits(!encodable && fallbackToUTF8, entryMetaData.usesDataDescriptor).encode(buf, CFH_GPB_OFFSET);
// compression method
putShort(zipMethod, buf, CFH_METHOD_OFFSET);
// last mod. time and date
ZipUtil.toDosTime(calendarInstance, ze.getTime(), buf, CFH_TIME_OFFSET);
// CRC
// compressed length
// uncompressed length
putLong(ze.getCrc(), buf, CFH_CRC_OFFSET);
if (ze.getCompressedSize() >= ZIP64_MAGIC || ze.getSize() >= ZIP64_MAGIC || zip64Mode == Zip64Mode.Always){
ZipLong.ZIP64_MAGIC.putLong(buf, CFH_COMPRESSED_SIZE_OFFSET);
ZipLong.ZIP64_MAGIC.putLong(buf, CFH_ORIGINAL_SIZE_OFFSET);
}else{
putLong(ze.getCompressedSize(), buf, CFH_COMPRESSED_SIZE_OFFSET);
putLong(ze.getSize(), buf, CFH_ORIGINAL_SIZE_OFFSET);
}
putShort(nameLen, buf, CFH_FILENAME_LENGTH_OFFSET);
// extra field length
putShort(extra.length, buf, CFH_EXTRA_LENGTH_OFFSET);
putShort(commentLen, buf, CFH_COMMENT_LENGTH_OFFSET);
// disk number start
if (isSplitZip){
if (ze.getDiskNumberStart() >= ZIP64_MAGIC_SHORT || zip64Mode == Zip64Mode.Always){
putShort(ZIP64_MAGIC_SHORT, buf, CFH_DISK_NUMBER_OFFSET);
}else{
putShort((int) ze.getDiskNumberStart(), buf, CFH_DISK_NUMBER_OFFSET);
}
}else{
System.arraycopy(ZERO, 0, buf, CFH_DISK_NUMBER_OFFSET, SHORT);
}
// internal file attributes
putShort(ze.getInternalAttributes(), buf, CFH_INTERNAL_ATTRIBUTES_OFFSET);
// external file attributes
putLong(ze.getExternalAttributes(), buf, CFH_EXTERNAL_ATTRIBUTES_OFFSET);
// relative offset of LFH
if (entryMetaData.offset >= ZIP64_MAGIC || zip64Mode == Zip64Mode.Always){
putLong(ZIP64_MAGIC, buf, CFH_LFH_OFFSET);
}else{
putLong(Math.min(entryMetaData.offset, ZIP64_MAGIC), buf, CFH_LFH_OFFSET);
}
// file name
System.arraycopy(name.array(), name.arrayOffset(), buf, CFH_FILENAME_OFFSET, nameLen);
final int extraStart = CFH_FILENAME_OFFSET + nameLen;
System.arraycopy(extra, 0, buf, extraStart, extra.length);
final int commentStart = extraStart + extra.length;
// file comment
System.arraycopy(commentB.array(), commentB.arrayOffset(), buf, commentStart, commentLen);
return buf;
}
/**
* If the entry needs Zip64 extra information inside the central
* directory then configure its data.
*/
private void handleZip64Extra(final ZipArchiveEntry ze,final long lfhOffset,final boolean needsZip64Extra){
if (needsZip64Extra){
final Zip64ExtendedInformationExtraField z64 = getZip64Extra(ze);
if (ze.getCompressedSize() >= ZIP64_MAGIC || ze.getSize() >= ZIP64_MAGIC || zip64Mode == Zip64Mode.Always){
z64.setCompressedSize(new ZipEightByteInteger(ze.getCompressedSize()));
z64.setSize(new ZipEightByteInteger(ze.getSize()));
}else{
// reset value that may have been set for LFH
z64.setCompressedSize(null);
z64.setSize(null);
}
if (lfhOffset >= ZIP64_MAGIC || zip64Mode == Zip64Mode.Always){
z64.setRelativeHeaderOffset(new ZipEightByteInteger(lfhOffset));
}
if (ze.getDiskNumberStart() >= ZIP64_MAGIC_SHORT || zip64Mode == Zip64Mode.Always){
z64.setDiskStartNumber(new ZipLong(ze.getDiskNumberStart()));
}
ze.setExtra();
}
}
/**
* Writes the "End of central dir record".
*
* @throws IOException
* on error
* @throws Zip64RequiredException
* if the archive's size exceeds 4
* GByte or there are more than 65535 entries inside the archive
* and {@link Zip64Mode #setUseZip64} is {@link Zip64Mode#Never}.
*/
protected void writeCentralDirectoryEnd() throws IOException{
if (!hasUsedZip64 && isSplitZip){
((ZipSplitOutputStream) this.out).prepareToWriteUnsplittableContent(eocdLength);
}
validateIfZip64IsNeededInEOCD();
writeCounted(EOCD_SIG);
// number of this disk
int numberOfThisDisk = 0;
if (isSplitZip){
numberOfThisDisk = ((ZipSplitOutputStream) this.out).getCurrentSplitSegmentIndex();
}
writeCounted(ZipShort.getBytes(numberOfThisDisk));
// disk number of the start of central directory
writeCounted(ZipShort.getBytes((int) cdDiskNumberStart));
// number of entries
final int numberOfEntries = entries.size();
// total number of entries in the central directory on this disk
int numOfEntriesOnThisDisk = isSplitZip
? (numberOfCDInDiskData.get(numberOfThisDisk) == null ? 0 : numberOfCDInDiskData.get(numberOfThisDisk))
: numberOfEntries;
final byte[] numOfEntriesOnThisDiskData = ZipShort.getBytes(Math.min(numOfEntriesOnThisDisk, ZIP64_MAGIC_SHORT));
writeCounted(numOfEntriesOnThisDiskData);
// number of entries
final byte[] num = ZipShort.getBytes(Math.min(numberOfEntries, ZIP64_MAGIC_SHORT));
writeCounted(num);
// length and location of CD
writeCounted(ZipLong.getBytes(Math.min(cdLength, ZIP64_MAGIC)));
writeCounted(ZipLong.getBytes(Math.min(cdOffset, ZIP64_MAGIC)));
// ZIP file comment
final ByteBuffer data = this.zipEncoding.encode(comment);
final int dataLen = data.limit() - data.position();
writeCounted(ZipShort.getBytes(dataLen));
streamCompressor.writeCounted(data.array(), data.arrayOffset(), dataLen);
}
/**
* If the Zip64 mode is set to never, then all the data in End Of Central Directory
* should not exceed their limits.
*
* @throws Zip64RequiredException
* if Zip64 is actually needed
*/
private void validateIfZip64IsNeededInEOCD() throws Zip64RequiredException{
// exception will only be thrown if the Zip64 mode is never while Zip64 is actually needed
if (zip64Mode != Zip64Mode.Never){
return;
}
int numberOfThisDisk = 0;
if (isSplitZip){
numberOfThisDisk = ((ZipSplitOutputStream) this.out).getCurrentSplitSegmentIndex();
}
if (numberOfThisDisk >= ZIP64_MAGIC_SHORT){
throw new Zip64RequiredException(Zip64RequiredException.NUMBER_OF_THIS_DISK_TOO_BIG_MESSAGE);
}
if (cdDiskNumberStart >= ZIP64_MAGIC_SHORT){
throw new Zip64RequiredException(Zip64RequiredException.NUMBER_OF_THE_DISK_OF_CENTRAL_DIRECTORY_TOO_BIG_MESSAGE);
}
final int numOfEntriesOnThisDisk = numberOfCDInDiskData.get(numberOfThisDisk) == null ? 0
: numberOfCDInDiskData.get(numberOfThisDisk);
if (numOfEntriesOnThisDisk >= ZIP64_MAGIC_SHORT){
throw new Zip64RequiredException(Zip64RequiredException.TOO_MANY_ENTRIES_ON_THIS_DISK_MESSAGE);
}
// number of entries
if (entries.size() >= ZIP64_MAGIC_SHORT){
throw new Zip64RequiredException(Zip64RequiredException.TOO_MANY_ENTRIES_MESSAGE);
}
if (cdLength >= ZIP64_MAGIC){
throw new Zip64RequiredException(Zip64RequiredException.SIZE_OF_CENTRAL_DIRECTORY_TOO_BIG_MESSAGE);
}
if (cdOffset >= ZIP64_MAGIC){
throw new Zip64RequiredException(Zip64RequiredException.ARCHIVE_TOO_BIG_MESSAGE);
}
}
/**
* Writes the "ZIP64 End of central dir record" and
* "ZIP64 End of central dir locator".
*
* @throws IOException
* on error
* @since 1.3
*/
protected void writeZip64CentralDirectory() throws IOException{
if (zip64Mode == Zip64Mode.Never){
return;
}
if (!hasUsedZip64 && shouldUseZip64EOCD()){
// actually "will use"
hasUsedZip64 = true;
}
if (!hasUsedZip64){
return;
}
long offset = streamCompressor.getTotalBytesWritten();
long diskNumberStart = 0L;
if (isSplitZip){
// when creating a split zip, the offset of should be
// the offset to the corresponding segment disk
ZipSplitOutputStream zipSplitOutputStream = (ZipSplitOutputStream) this.out;
offset = zipSplitOutputStream.getCurrentSplitSegmentBytesWritten();
diskNumberStart = zipSplitOutputStream.getCurrentSplitSegmentIndex();
}
writeOut(ZIP64_EOCD_SIG);
// size of zip64 end of central directory, we don't have any variable length
// as we don't support the extensible data sector, yet
writeOut(
ZipEightByteInteger.getBytes(
SHORT /* version made by */
+ SHORT /* version needed to extract */
+ WORD /* disk number */
+ WORD /* disk with central directory */
+ DWORD /* number of entries in CD on this disk */
+ DWORD /* total number of entries */
+ DWORD /* size of CD */
+ (long) DWORD /* offset of CD */
));
// version made by and version needed to extract
writeOut(ZipShort.getBytes(ZIP64_MIN_VERSION));
writeOut(ZipShort.getBytes(ZIP64_MIN_VERSION));
// number of this disk
int numberOfThisDisk = 0;
if (isSplitZip){
numberOfThisDisk = ((ZipSplitOutputStream) this.out).getCurrentSplitSegmentIndex();
}
writeOut(ZipLong.getBytes(numberOfThisDisk));
// disk number of the start of central directory
writeOut(ZipLong.getBytes(cdDiskNumberStart));
// total number of entries in the central directory on this disk
int numOfEntriesOnThisDisk = isSplitZip
? (numberOfCDInDiskData.get(numberOfThisDisk) == null ? 0 : numberOfCDInDiskData.get(numberOfThisDisk))
: entries.size();
final byte[] numOfEntriesOnThisDiskData = ZipEightByteInteger.getBytes(numOfEntriesOnThisDisk);
writeOut(numOfEntriesOnThisDiskData);
// number of entries
final byte[] num = ZipEightByteInteger.getBytes(entries.size());
writeOut(num);
// length and location of CD
writeOut(ZipEightByteInteger.getBytes(cdLength));
writeOut(ZipEightByteInteger.getBytes(cdOffset));
// no "zip64 extensible data sector" for now
if (isSplitZip){
// based on the zip specification, the End Of Central Directory record and
// the Zip64 End Of Central Directory locator record must be on the same segment
final int zip64EOCDLOCLength = WORD /* length of ZIP64_EOCD_LOC_SIG */
+ WORD /* disk number of ZIP64_EOCD_SIG */
+ DWORD /* offset of ZIP64_EOCD_SIG */
+ WORD /* total number of disks */;
final long unsplittableContentSize = zip64EOCDLOCLength + eocdLength;
((ZipSplitOutputStream) this.out).prepareToWriteUnsplittableContent(unsplittableContentSize);
}
// and now the "ZIP64 end of central directory locator"
writeOut(ZIP64_EOCD_LOC_SIG);
// disk number holding the ZIP64 EOCD record
writeOut(ZipLong.getBytes(diskNumberStart));
// relative offset of ZIP64 EOCD record
writeOut(ZipEightByteInteger.getBytes(offset));
// total number of disks
if (isSplitZip){
// the Zip64 End Of Central Directory Locator and the End Of Central Directory must be
// in the same split disk, it means they must be located in the last disk
final int totalNumberOfDisks = ((ZipSplitOutputStream) this.out).getCurrentSplitSegmentIndex() + 1;
writeOut(ZipLong.getBytes(totalNumberOfDisks));
}else{
writeOut(ONE);
}
}
/**
* 4.4.1.4 If one of the fields in the end of central directory
* record is too small to hold required data, the field SHOULD be
* set to -1 (0xFFFF or 0xFFFFFFFF) and the ZIP64 format record
* SHOULD be created.
*
* @return true if zip64 End Of Central Directory is needed
*/
private boolean shouldUseZip64EOCD(){
int numberOfThisDisk = 0;
if (isSplitZip){
numberOfThisDisk = ((ZipSplitOutputStream) this.out).getCurrentSplitSegmentIndex();
}
int numOfEntriesOnThisDisk = numberOfCDInDiskData.get(numberOfThisDisk) == null ? 0 : numberOfCDInDiskData.get(numberOfThisDisk);
return numberOfThisDisk >= ZIP64_MAGIC_SHORT /* number of this disk */
|| cdDiskNumberStart >= ZIP64_MAGIC_SHORT /* number of the disk with the start of the central directory */
|| numOfEntriesOnThisDisk >= ZIP64_MAGIC_SHORT /* total number of entries in the central directory on this disk */
|| entries.size() >= ZIP64_MAGIC_SHORT /* total number of entries in the central directory */
|| cdLength >= ZIP64_MAGIC /* size of the central directory */
|| cdOffset >= ZIP64_MAGIC; /*
* offset of start of central directory with respect to
* the starting disk number
*/
}
/**
* Write bytes to output or random access file.
*
* @param data
* the byte array to write
* @throws IOException
* on error
*/
protected final void writeOut(final byte[] data) throws IOException{
streamCompressor.writeOut(data, 0, data.length);
}
/**
* Write bytes to output or random access file.
*
* @param data
* the byte array to write
* @param offset
* the start position to write from
* @param length
* the number of bytes to write
* @throws IOException
* on error
*/
protected final void writeOut(final byte[] data,final int offset,final int length) throws IOException{
streamCompressor.writeOut(data, offset, length);
}
private GeneralPurposeBit getGeneralPurposeBits(final boolean utfFallback,boolean usesDataDescriptor){
final GeneralPurposeBit b = new GeneralPurposeBit();
b.useUTF8ForNames(useUTF8Flag || utfFallback);
if (usesDataDescriptor){
b.useDataDescriptor(true);
}
return b;
}
private int versionNeededToExtract(final int zipMethod,final boolean zip64,final boolean usedDataDescriptor){
if (zip64){
return ZIP64_MIN_VERSION;
}
if (usedDataDescriptor){
return DATA_DESCRIPTOR_MIN_VERSION;
}
return versionNeededToExtractMethod(zipMethod);
}
private boolean usesDataDescriptor(final int zipMethod,boolean phased){
return !phased && zipMethod == DEFLATED && channel == null;
}
private int versionNeededToExtractMethod(int zipMethod){
return zipMethod == DEFLATED ? DEFLATE_MIN_VERSION : INITIAL_VERSION;
}
/**
* Creates a new zip entry taking some information from the given
* file and using the provided name.
*
*
* The name will be adjusted to end with a forward slash "/" if
* the file is a directory. If the file is not a directory a
* potential trailing forward slash will be stripped from the
* entry name.
*
*
*
* Must not be used if the stream has already been closed.
*
*/
@Override
public ArchiveEntry createArchiveEntry(final File inputFile,final String entryName) throws IOException{
if (finished){
throw new IOException("Stream has already been finished");
}
return new ZipArchiveEntry(inputFile, entryName);
}
/**
* Get the existing ZIP64 extended information extra field or
* create a new one and add it to the entry.
*
* @since 1.3
*/
private Zip64ExtendedInformationExtraField getZip64Extra(final ZipArchiveEntry ze){
if (entry != null){
entry.causedUseOfZip64 = !hasUsedZip64;
}
hasUsedZip64 = true;
Zip64ExtendedInformationExtraField z64 = (Zip64ExtendedInformationExtraField) ze
.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID);
if (z64 == null){
/*
* System.err.println("Adding z64 for " + ze.getName()
* + ", method: " + ze.getMethod()
* + " (" + (ze.getMethod() == STORED) + ")"
* + ", channel: " + (channel != null));
*/
z64 = new Zip64ExtendedInformationExtraField();
}
// even if the field is there already, make sure it is the first one
ze.addAsFirstExtraField(z64);
return z64;
}
/**
* Is there a ZIP64 extended information extra field for the
* entry?
*
* @since 1.3
*/
private boolean hasZip64Extra(final ZipArchiveEntry ze){
return ze.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID) != null;
}
/**
* If the mode is AsNeeded and the entry is a compressed entry of
* unknown size that gets written to a non-seekable stream then
* change the default to Never.
*
* @since 1.3
*/
private Zip64Mode getEffectiveZip64Mode(final ZipArchiveEntry ze){
if (zip64Mode != Zip64Mode.AsNeeded || channel != null || ze.getMethod() != DEFLATED || ze.getSize() != ArchiveEntry.SIZE_UNKNOWN){
return zip64Mode;
}
return Zip64Mode.Never;
}
private ZipEncoding getEntryEncoding(final ZipArchiveEntry ze){
final boolean encodable = zipEncoding.canEncode(ze.getName());
return !encodable && fallbackToUTF8 ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding;
}
private ByteBuffer getName(final ZipArchiveEntry ze) throws IOException{
return getEntryEncoding(ze).encode(ze.getName());
}
/**
* Closes the underlying stream/file without finishing the
* archive, the result will likely be a corrupt archive.
*
*
* This method only exists to support tests that generate
* corrupt archives so they can clean up any temporary files.
*
*/
void destroy() throws IOException{
try{
if (channel != null){
channel.close();
}
}finally{
if (out != null){
out.close();
}
}
}
/**
* enum that represents the possible policies for creating Unicode
* extra fields.
*/
public static final class UnicodeExtraFieldPolicy{
/**
* Always create Unicode extra fields.
*/
public static final UnicodeExtraFieldPolicy ALWAYS = new UnicodeExtraFieldPolicy("always");
/**
* Never create Unicode extra fields.
*/
public static final UnicodeExtraFieldPolicy NEVER = new UnicodeExtraFieldPolicy("never");
/**
* Create Unicode extra fields for file names that cannot be
* encoded using the specified encoding.
*/
public static final UnicodeExtraFieldPolicy NOT_ENCODEABLE = new UnicodeExtraFieldPolicy("not encodeable");
private final String name;
private UnicodeExtraFieldPolicy(final String n){
name = n;
}
@Override
public String toString(){
return name;
}
}
/**
* Structure collecting information for the entry that is
* currently being written.
*/
private static final class CurrentEntry{
private CurrentEntry(final ZipArchiveEntry entry){
this.entry = entry;
}
/**
* Current ZIP entry.
*/
private final ZipArchiveEntry entry;
/**
* Offset for CRC entry in the local file header data for the
* current entry starts here.
*/
private long localDataStart = 0;
/**
* Data for local header data
*/
private long dataStart = 0;
/**
* Number of bytes read for the current entry (can't rely on
* Deflater#getBytesRead) when using DEFLATED.
*/
private long bytesRead = 0;
/**
* Whether current entry was the first one using ZIP64 features.
*/
private boolean causedUseOfZip64 = false;
/**
* Whether write() has been called at all.
*
*
* In order to create a valid archive {@link
* #closeArchiveEntry closeArchiveEntry} will write an empty
* array to get the CRC right if nothing has been written to
* the stream at all.
*
*/
private boolean hasWritten;
}
private static final class EntryMetaData{
private final long offset;
private final boolean usesDataDescriptor;
private EntryMetaData(long offset, boolean usesDataDescriptor){
this.offset = offset;
this.usesDataDescriptor = usesDataDescriptor;
}
}
}