All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream Maven / Gradle / Ivy

Go to download

Apache Commons Compress defines an API for working with compression and archive formats. These include bzip2, gzip, pack200, LZMA, XZ, Snappy, traditional Unix Compress, DEFLATE, DEFLATE64, LZ4, Brotli, Zstandard and ar, cpio, jar, tar, zip, dump, 7z, arj.

There is a newer version: 1.27.1
Show newest version
/*
 *  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 org.apache.commons.compress.archivers.zip;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
import java.util.zip.ZipException;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveOutputStream;

import static org.apache.commons.compress.archivers.zip.ZipConstants.DATA_DESCRIPTOR_MIN_VERSION;
import static org.apache.commons.compress.archivers.zip.ZipConstants.DWORD;
import static org.apache.commons.compress.archivers.zip.ZipConstants.INITIAL_VERSION;
import static org.apache.commons.compress.archivers.zip.ZipConstants.SHORT;
import static org.apache.commons.compress.archivers.zip.ZipConstants.WORD;
import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC;
import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC_SHORT;
import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MIN_VERSION;

/**
 * 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.io.RandomAccessFile * RandomAccessFile} when you know that the output is going to go to a * file.

* *

If RandomAccessFile 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 * user RandomAccessFile 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; /** indicates if this archive is finished. protected for use in Jar implementation */ protected boolean finished = false; /* * Apparently Deflater.setInput gets slowed down a lot on Sun JVMs * when it gets handed a really big buffer. See * https://issues.apache.org/bugzilla/show_bug.cgi?id=45396 * * Using a buffer size of 8 kB proved to be a good compromise */ private static final int DEFLATER_BLOCK_SIZE = 8192; /** * 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; /** * General purpose flag, which indicates that filenames are * written in UTF-8. * @deprecated use {@link GeneralPurposeBit#UFT8_NAMES_FLAG} instead */ @Deprecated public static final int EFS_FLAG = GeneralPurposeBit.UFT8_NAMES_FLAG; 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(); /** * CRC instance to avoid parsing DEFLATED data twice. */ private final CRC32 crc = new CRC32(); /** * Count the bytes written to out. */ private long written = 0; /** * Start of central directory. */ private long cdOffset = 0; /** * Length of central directory. */ private long cdLength = 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}; /** * Holds the offsets of the LFH starts for each entry. */ private final Map offsets = new HashMap(); /** * The encoding to use for filenames 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 filenames 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 = new Deflater(level, true); /** * This buffer serves as a Deflater. * */ private final byte[] buf = new byte[BUFFER_SIZE]; /** * Optional random access output. */ private final RandomAccessFile raf; private final OutputStream out; /** * whether to use the general purpose bit flag when writing UTF-8 * filenames 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; /** * Creates a new ZIP OutputStream filtering the underlying stream. * @param out the outputstream to zip */ public ZipArchiveOutputStream(OutputStream out) { this.out = out; this.raf = null; } /** * 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(File file) throws IOException { OutputStream o = null; RandomAccessFile _raf = null; try { _raf = new RandomAccessFile(file, "rw"); _raf.setLength(0); } catch (IOException e) { if (_raf != null) { try { _raf.close(); } catch (IOException inner) { // NOPMD // ignore } _raf = null; } o = new FileOutputStream(file); } out = o; raf = _raf; } /** * 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 raf != null; } /** * The encoding to use for filenames 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 filenames 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.

*/ public void setUseLanguageEncodingFlag(boolean b) { useUTF8Flag = b && ZipEncodingHelper.isUTF8(encoding); } /** * Whether to create Unicode Extra Fields. * *

Defaults to NEVER.

*/ public void setCreateUnicodeExtraFields(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.

*/ public void setFallbackToUTF8(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 */ public void setUseZip64(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."); } cdOffset = written; for (ZipArchiveEntry ze : entries) { writeCentralFileHeader(ze); } cdLength = written - cdOffset; writeZip64CentralDirectory(); writeCentralDirectoryEnd(); offsets.clear(); entries.clear(); def.end(); finished = true; } /** * 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 { 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); } flushDeflater(); final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry); long bytesWritten = written - entry.dataStart; long realCrc = crc.getValue(); crc.reset(); final boolean actuallyNeedsZip64 = handleSizesAndCrc(bytesWritten, realCrc, effectiveMode); if (raf != null) { rewriteSizesAndCrc(actuallyNeedsZip64); } writeDataDescriptor(entry.entry); entry = null; } /** * Ensures all bytes sent to the deflater are written to the stream. */ private void flushDeflater() throws IOException { if (entry.entry.getMethod() == DEFLATED) { def.finish(); while (!def.finished()) { deflate(); } } } /** * 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(long bytesWritten, long crc, 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); def.reset(); } else if (raf == 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 RandomAccessFile */ entry.entry.setSize(bytesWritten); entry.entry.setCompressedSize(bytesWritten); entry.entry.setCrc(crc); } final boolean actuallyNeedsZip64 = effectiveMode == Zip64Mode.Always || entry.entry.getSize() >= ZIP64_MAGIC || entry.entry.getCompressedSize() >= ZIP64_MAGIC; if (actuallyNeedsZip64 && effectiveMode == Zip64Mode.Never) { throw new Zip64RequiredException(Zip64RequiredException .getEntryTooBigMessage(entry.entry)); } return actuallyNeedsZip64; } /** * 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(boolean actuallyNeedsZip64) throws IOException { long save = raf.getFilePointer(); raf.seek(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)) { // seek to ZIP64 extra, skip header and size information raf.seek(entry.localDataStart + 3 * WORD + 2 * SHORT + getName(entry.entry).limit() + 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 raf.seek(entry.localDataStart - 5 * SHORT); writeOut(ZipShort.getBytes(INITIAL_VERSION)); // * 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; } } } raf.seek(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(ArchiveEntry archiveEntry) 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)) { Zip64ExtendedInformationExtraField z64 = getZip64Extra(entry.entry); // just a placeholder, real data will be in data // descriptor or inserted later via RandomAccessFile ZipEightByteInteger size = ZipEightByteInteger.ZERO; if (entry.entry.getMethod() == STORED && entry.entry.getSize() != ArchiveEntry.SIZE_UNKNOWN) { // actually, we already know the sizes size = new ZipEightByteInteger(entry.entry.getSize()); } z64.setSize(size); z64.setCompressedSize(size); entry.entry.setExtra(); } if (entry.entry.getMethod() == DEFLATED && hasCompressionLevelChanged) { def.setLevel(level); hasCompressionLevelChanged = false; } writeLocalFileHeader(entry.entry); } /** * Provides default values for compression method and last * modification time. */ private void setDefaults(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(Zip64Mode effectiveMode) throws ZipException { // Size/CRC not required if RandomAccessFile is used if (entry.entry.getMethod() == STORED && raf == 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() == -1) { 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(ZipArchiveEntry entry, Zip64Mode mode) { return mode == Zip64Mode.Always || entry.getSize() >= ZIP64_MAGIC || entry.getCompressedSize() >= ZIP64_MAGIC || (entry.getSize() == ArchiveEntry.SIZE_UNKNOWN && raf != null && mode != Zip64Mode.Never); } /** * Set the file comment. * @param comment the comment */ public void setComment(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(int level) { if (level < Deflater.DEFAULT_COMPRESSION || level > Deflater.BEST_COMPRESSION) { throw new IllegalArgumentException("Invalid compression level: " + level); } hasCompressionLevelChanged = (this.level != level); 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(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(ArchiveEntry ae) { if (ae instanceof ZipArchiveEntry) { return ZipUtil.canHandleEntryData((ZipArchiveEntry) ae); } 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(byte[] b, int offset, int length) throws IOException { ZipUtil.checkRequestedFeatures(entry.entry); entry.hasWritten = true; if (entry.entry.getMethod() == DEFLATED) { writeDeflated(b, offset, length); } else { writeOut(b, offset, length); written += length; } crc.update(b, offset, length); count(length); } /** * write implementation for DEFLATED entries. */ private void writeDeflated(byte[]b, int offset, int length) throws IOException { if (length > 0 && !def.finished()) { entry.bytesRead += length; if (length <= DEFLATER_BLOCK_SIZE) { def.setInput(b, offset, length); deflateUntilInputIsNeeded(); } else { final int fullblocks = length / DEFLATER_BLOCK_SIZE; for (int i = 0; i < fullblocks; i++) { def.setInput(b, offset + i * DEFLATER_BLOCK_SIZE, DEFLATER_BLOCK_SIZE); deflateUntilInputIsNeeded(); } final int done = fullblocks * DEFLATER_BLOCK_SIZE; if (done < length) { def.setInput(b, offset + done, length - done); deflateUntilInputIsNeeded(); } } } } /** * Closes this output stream and releases any system resources * associated with the stream. * * @exception 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 { if (!finished) { finish(); } destroy(); } /** * Flushes this output stream and forces any buffered output bytes * to be written out to the stream. * * @exception IOException if an I/O error occurs. */ @Override public void flush() throws IOException { if (out != null) { out.flush(); } } /* * Various ZIP constants */ /** * local file header signature */ static final byte[] LFH_SIG = ZipLong.LFH_SIG.getBytes(); /** * data descriptor signature */ static final byte[] DD_SIG = ZipLong.DD_SIG.getBytes(); /** * central file header signature */ static final byte[] CFH_SIG = ZipLong.CFH_SIG.getBytes(); /** * end of central dir signature */ static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L); /** * ZIP64 end of central dir signature */ static final byte[] ZIP64_EOCD_SIG = ZipLong.getBytes(0X06064B50L); /** * ZIP64 end of central dir locator signature */ static final byte[] ZIP64_EOCD_LOC_SIG = ZipLong.getBytes(0X07064B50L); /** * Writes next block of compressed data to the output stream. * @throws IOException on error */ protected final void deflate() throws IOException { int len = def.deflate(buf, 0, buf.length); if (len > 0) { writeOut(buf, 0, len); written += len; } } /** * Writes the local file header entry * @param ze the entry to write * @throws IOException on error */ protected void writeLocalFileHeader(ZipArchiveEntry ze) throws IOException { boolean encodable = zipEncoding.canEncode(ze.getName()); ByteBuffer name = getName(ze); if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER) { addUnicodeExtraFields(ze, encodable, name); } offsets.put(ze, Long.valueOf(written)); writeOut(LFH_SIG); written += WORD; //store method in local variable to prevent multiple method calls final int zipMethod = ze.getMethod(); writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod, !encodable && fallbackToUTF8, hasZip64Extra(ze)); written += WORD; // compression method writeOut(ZipShort.getBytes(zipMethod)); written += SHORT; // last mod. time and date writeOut(ZipUtil.toDosTime(ze.getTime())); written += WORD; // CRC // compressed length // uncompressed length entry.localDataStart = written; if (zipMethod == DEFLATED || raf != null) { writeOut(LZERO); if (hasZip64Extra(entry.entry)) { // point to ZIP64 extended information extra field for // sizes, may get rewritten once sizes are known if // stream is seekable writeOut(ZipLong.ZIP64_MAGIC.getBytes()); writeOut(ZipLong.ZIP64_MAGIC.getBytes()); } else { writeOut(LZERO); writeOut(LZERO); } } else { writeOut(ZipLong.getBytes(ze.getCrc())); byte[] size = ZipLong.ZIP64_MAGIC.getBytes(); if (!hasZip64Extra(ze)) { size = ZipLong.getBytes(ze.getSize()); } writeOut(size); writeOut(size); } // CheckStyle:MagicNumber OFF written += 12; // CheckStyle:MagicNumber ON // file name length writeOut(ZipShort.getBytes(name.limit())); written += SHORT; // extra field length byte[] extra = ze.getLocalFileDataExtra(); writeOut(ZipShort.getBytes(extra.length)); written += SHORT; // file name writeOut(name.array(), name.arrayOffset(), name.limit() - name.position()); written += name.limit(); // extra field writeOut(extra); written += extra.length; entry.dataStart = written; } /** * 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(ZipArchiveEntry ze, boolean encodable, ByteBuffer name) throws IOException { if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS || !encodable) { ze.addExtraField(new UnicodePathExtraField(ze.getName(), name.array(), name.arrayOffset(), name.limit() - name.position())); } String comm = ze.getComment(); if (comm != null && !"".equals(comm)) { boolean commentEncodable = zipEncoding.canEncode(comm); if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS || !commentEncodable) { 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(ZipArchiveEntry ze) throws IOException { if (ze.getMethod() != DEFLATED || raf != null) { return; } writeOut(DD_SIG); writeOut(ZipLong.getBytes(ze.getCrc())); int sizeFieldSize = WORD; if (!hasZip64Extra(ze)) { writeOut(ZipLong.getBytes(ze.getCompressedSize())); writeOut(ZipLong.getBytes(ze.getSize())); } else { sizeFieldSize = DWORD; writeOut(ZipEightByteInteger.getBytes(ze.getCompressedSize())); writeOut(ZipEightByteInteger.getBytes(ze.getSize())); } written += 2 * WORD + 2 * sizeFieldSize; } /** * 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(ZipArchiveEntry ze) throws IOException { writeOut(CFH_SIG); written += WORD; final long lfhOffset = offsets.get(ze).longValue(); final boolean needsZip64Extra = hasZip64Extra(ze) || ze.getCompressedSize() >= ZIP64_MAGIC || ze.getSize() >= ZIP64_MAGIC || lfhOffset >= ZIP64_MAGIC; 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, lfhOffset, needsZip64Extra); // version made by // CheckStyle:MagicNumber OFF writeOut(ZipShort.getBytes((ze.getPlatform() << 8) | (!hasUsedZip64 ? DATA_DESCRIPTOR_MIN_VERSION : ZIP64_MIN_VERSION))); written += SHORT; final int zipMethod = ze.getMethod(); final boolean encodable = zipEncoding.canEncode(ze.getName()); writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod, !encodable && fallbackToUTF8, needsZip64Extra); written += WORD; // compression method writeOut(ZipShort.getBytes(zipMethod)); written += SHORT; // last mod. time and date writeOut(ZipUtil.toDosTime(ze.getTime())); written += WORD; // CRC // compressed length // uncompressed length writeOut(ZipLong.getBytes(ze.getCrc())); if (ze.getCompressedSize() >= ZIP64_MAGIC || ze.getSize() >= ZIP64_MAGIC) { writeOut(ZipLong.ZIP64_MAGIC.getBytes()); writeOut(ZipLong.ZIP64_MAGIC.getBytes()); } else { writeOut(ZipLong.getBytes(ze.getCompressedSize())); writeOut(ZipLong.getBytes(ze.getSize())); } // CheckStyle:MagicNumber OFF written += 12; // CheckStyle:MagicNumber ON ByteBuffer name = getName(ze); writeOut(ZipShort.getBytes(name.limit())); written += SHORT; // extra field length byte[] extra = ze.getCentralDirectoryExtra(); writeOut(ZipShort.getBytes(extra.length)); written += SHORT; // file comment length String comm = ze.getComment(); if (comm == null) { comm = ""; } ByteBuffer commentB = getEntryEncoding(ze).encode(comm); writeOut(ZipShort.getBytes(commentB.limit())); written += SHORT; // disk number start writeOut(ZERO); written += SHORT; // internal file attributes writeOut(ZipShort.getBytes(ze.getInternalAttributes())); written += SHORT; // external file attributes writeOut(ZipLong.getBytes(ze.getExternalAttributes())); written += WORD; // relative offset of LFH writeOut(ZipLong.getBytes(Math.min(lfhOffset, ZIP64_MAGIC))); written += WORD; // file name writeOut(name.array(), name.arrayOffset(), name.limit() - name.position()); written += name.limit(); // extra field writeOut(extra); written += extra.length; // file comment writeOut(commentB.array(), commentB.arrayOffset(), commentB.limit() - commentB.position()); written += commentB.limit(); } /** * If the entry needs Zip64 extra information inside the central * directory then configure its data. */ private void handleZip64Extra(ZipArchiveEntry ze, long lfhOffset, boolean needsZip64Extra) { if (needsZip64Extra) { Zip64ExtendedInformationExtraField z64 = getZip64Extra(ze); if (ze.getCompressedSize() >= ZIP64_MAGIC || ze.getSize() >= ZIP64_MAGIC) { 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) { z64.setRelativeHeaderOffset(new ZipEightByteInteger(lfhOffset)); } 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 { writeOut(EOCD_SIG); // disk numbers writeOut(ZERO); writeOut(ZERO); // number of entries int numberOfEntries = entries.size(); if (numberOfEntries > ZIP64_MAGIC_SHORT && zip64Mode == Zip64Mode.Never) { throw new Zip64RequiredException(Zip64RequiredException .TOO_MANY_ENTRIES_MESSAGE); } if (cdOffset > ZIP64_MAGIC && zip64Mode == Zip64Mode.Never) { throw new Zip64RequiredException(Zip64RequiredException .ARCHIVE_TOO_BIG_MESSAGE); } byte[] num = ZipShort.getBytes(Math.min(numberOfEntries, ZIP64_MAGIC_SHORT)); writeOut(num); writeOut(num); // length and location of CD writeOut(ZipLong.getBytes(Math.min(cdLength, ZIP64_MAGIC))); writeOut(ZipLong.getBytes(Math.min(cdOffset, ZIP64_MAGIC))); // ZIP file comment ByteBuffer data = this.zipEncoding.encode(comment); writeOut(ZipShort.getBytes(data.limit())); writeOut(data.array(), data.arrayOffset(), data.limit() - data.position()); } private static final byte[] ONE = ZipLong.getBytes(1L); /** * 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 && (cdOffset >= ZIP64_MAGIC || cdLength >= ZIP64_MAGIC || entries.size() >= ZIP64_MAGIC_SHORT)) { // actually "will use" hasUsedZip64 = true; } if (!hasUsedZip64) { return; } long offset = written; writeOut(ZIP64_EOCD_SIG); // size, 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 */ + DWORD /* offset of CD */ )); // version made by and version needed to extract writeOut(ZipShort.getBytes(ZIP64_MIN_VERSION)); writeOut(ZipShort.getBytes(ZIP64_MIN_VERSION)); // disk numbers - four bytes this time writeOut(LZERO); writeOut(LZERO); // number of entries byte[] num = ZipEightByteInteger.getBytes(entries.size()); writeOut(num); writeOut(num); // length and location of CD writeOut(ZipEightByteInteger.getBytes(cdLength)); writeOut(ZipEightByteInteger.getBytes(cdOffset)); // no "zip64 extensible data sector" for now // and now the "ZIP64 end of central directory locator" writeOut(ZIP64_EOCD_LOC_SIG); // disk number holding the ZIP64 EOCD record writeOut(LZERO); // relative offset of ZIP64 EOCD record writeOut(ZipEightByteInteger.getBytes(offset)); // total number of disks writeOut(ONE); } /** * Write bytes to output or random access file. * @param data the byte array to write * @throws IOException on error */ protected final void writeOut(byte[] data) throws IOException { 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(byte[] data, int offset, int length) throws IOException { if (raf != null) { raf.write(data, offset, length); } else { out.write(data, offset, length); } } private void deflateUntilInputIsNeeded() throws IOException { while (!def.needsInput()) { deflate(); } } private void writeVersionNeededToExtractAndGeneralPurposeBits(final int zipMethod, final boolean utfFallback, final boolean zip64) throws IOException { // CheckStyle:MagicNumber OFF int versionNeededToExtract = INITIAL_VERSION; GeneralPurposeBit b = new GeneralPurposeBit(); b.useUTF8ForNames(useUTF8Flag || utfFallback); if (zipMethod == DEFLATED && raf == null) { // requires version 2 as we are going to store length info // in the data descriptor versionNeededToExtract = DATA_DESCRIPTOR_MIN_VERSION; b.useDataDescriptor(true); } if (zip64) { versionNeededToExtract = ZIP64_MIN_VERSION; } // CheckStyle:MagicNumber ON // version needed to extract writeOut(ZipShort.getBytes(versionNeededToExtract)); // general purpose bit flag writeOut(b.encode()); } /** * 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(File inputFile, 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(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) + ")" + ", raf: " + (raf != 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(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 the * change the default to Never. * * @since 1.3 */ private Zip64Mode getEffectiveZip64Mode(ZipArchiveEntry ze) { if (zip64Mode != Zip64Mode.AsNeeded || raf != null || ze.getMethod() != DEFLATED || ze.getSize() != ArchiveEntry.SIZE_UNKNOWN) { return zip64Mode; } return Zip64Mode.Never; } private ZipEncoding getEntryEncoding(ZipArchiveEntry ze) { boolean encodable = zipEncoding.canEncode(ze.getName()); return !encodable && fallbackToUTF8 ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding; } private ByteBuffer getName(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 { if (raf != null) { raf.close(); } 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 filenames that cannot be * encoded using the specified encoding. */ public static final UnicodeExtraFieldPolicy NOT_ENCODEABLE = new UnicodeExtraFieldPolicy("not encodeable"); private final String name; private UnicodeExtraFieldPolicy(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(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; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy