-/*\r
- * Licensed to the Apache Software Foundation (ASF) under one or more\r
- * contributor license agreements. See the NOTICE file distributed with\r
- * this work for additional information regarding copyright ownership.\r
- * The ASF licenses this file to You under the Apache License, Version 2.0\r
- * (the "License"); you may not use this file except in compliance with\r
- * the License. You may obtain a copy of the License at\r
- *\r
- * http://www.apache.org/licenses/LICENSE-2.0\r
- *\r
- * Unless required by applicable law or agreed to in writing, software\r
- * distributed under the License is distributed on an "AS IS" BASIS,\r
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- * See the License for the specific language governing permissions and\r
- * limitations under the License.\r
- *\r
- */\r
-\r
-package org.apache.tools.zip;\r
-\r
-import java.io.File;\r
-import java.io.FileOutputStream;\r
-import java.io.FilterOutputStream;\r
-import java.io.IOException;\r
-import java.io.OutputStream;\r
-import java.io.RandomAccessFile;\r
-import java.nio.ByteBuffer;\r
-import java.util.Date;\r
-import java.util.HashMap;\r
-import java.util.Iterator;\r
-import java.util.LinkedList;\r
-import java.util.List;\r
-import java.util.Map;\r
-import java.util.zip.CRC32;\r
-import java.util.zip.Deflater;\r
-import java.util.zip.ZipException;\r
-\r
-/**\r
- * Reimplementation of {@link java.util.zip.ZipOutputStream\r
- * java.util.zip.ZipOutputStream} that does handle the extended\r
- * functionality of this package, especially internal/external file\r
- * attributes and extra fields with different layouts for local file\r
- * data and central directory entries.\r
- *\r
- * <p>This class will try to use {@link java.io.RandomAccessFile\r
- * RandomAccessFile} when you know that the output is going to go to a\r
- * file.</p>\r
- *\r
- * <p>If RandomAccessFile cannot be used, this implementation will use\r
- * a Data Descriptor to store size and CRC information for {@link\r
- * #DEFLATED DEFLATED} entries, this means, you don't need to\r
- * calculate them yourself. Unfortunately this is not possible for\r
- * the {@link #STORED STORED} method, here setting the CRC and\r
- * uncompressed size information is required before {@link\r
- * #putNextEntry putNextEntry} can be called.</p>\r
- *\r
- */\r
-@SuppressWarnings({"unchecked", "rawtypes"})\r
-public class ZipOutputStream extends FilterOutputStream {\r
-\r
- private static final int BYTE_MASK = 0xFF;\r
- private static final int SHORT = 2;\r
- private static final int WORD = 4;\r
- private static final int BUFFER_SIZE = 512;\r
- /* \r
- * Apparently Deflater.setInput gets slowed down a lot on Sun JVMs\r
- * when it gets handed a really big buffer. See\r
- * https://issues.apache.org/bugzilla/show_bug.cgi?id=45396\r
- *\r
- * Using a buffer size of 8 kB proved to be a good compromise\r
- */\r
- private static final int DEFLATER_BLOCK_SIZE = 8192;\r
-\r
- /**\r
- * Compression method for deflated entries.\r
- *\r
- * @since 1.1\r
- */\r
- public static final int DEFLATED = java.util.zip.ZipEntry.DEFLATED;\r
-\r
- /**\r
- * Default compression level for deflated entries.\r
- *\r
- * @since Ant 1.7\r
- */\r
- public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION;\r
-\r
- /**\r
- * Compression method for stored entries.\r
- *\r
- * @since 1.1\r
- */\r
- public static final int STORED = java.util.zip.ZipEntry.STORED;\r
-\r
- /**\r
- * default encoding for file names and comment.\r
- */\r
- static final String DEFAULT_ENCODING = null;\r
-\r
- /**\r
- * General purpose flag, which indicates that filenames are\r
- * written in utf-8.\r
- */\r
- public static final int UFT8_NAMES_FLAG = 1 << 11;\r
-\r
- /**\r
- * General purpose flag, which indicates that filenames are\r
- * written in utf-8.\r
- * @deprecated use {@link #UFT8_NAMES_FLAG} instead\r
- */\r
- public static final int EFS_FLAG = UFT8_NAMES_FLAG;\r
-\r
- /**\r
- * Current entry.\r
- *\r
- * @since 1.1\r
- */\r
- private ZipEntry entry;\r
-\r
- /**\r
- * The file comment.\r
- *\r
- * @since 1.1\r
- */\r
- private String comment = "";\r
-\r
- /**\r
- * Compression level for next entry.\r
- *\r
- * @since 1.1\r
- */\r
- private int level = DEFAULT_COMPRESSION;\r
-\r
- /**\r
- * Has the compression level changed when compared to the last\r
- * entry?\r
- *\r
- * @since 1.5\r
- */\r
- private boolean hasCompressionLevelChanged = false;\r
-\r
- /**\r
- * Default compression method for next entry.\r
- *\r
- * @since 1.1\r
- */\r
- private int method = java.util.zip.ZipEntry.DEFLATED;\r
-\r
- /**\r
- * List of ZipEntries written so far.\r
- *\r
- * @since 1.1\r
- */\r
- private final List entries = new LinkedList();\r
-\r
- /**\r
- * CRC instance to avoid parsing DEFLATED data twice.\r
- *\r
- * @since 1.1\r
- */\r
- private final CRC32 crc = new CRC32();\r
-\r
- /**\r
- * Count the bytes written to out.\r
- *\r
- * @since 1.1\r
- */\r
- private long written = 0;\r
-\r
- /**\r
- * Data for local header data\r
- *\r
- * @since 1.1\r
- */\r
- private long dataStart = 0;\r
-\r
- /**\r
- * Offset for CRC entry in the local file header data for the\r
- * current entry starts here.\r
- *\r
- * @since 1.15\r
- */\r
- private long localDataStart = 0;\r
-\r
- /**\r
- * Start of central directory.\r
- *\r
- * @since 1.1\r
- */\r
- private long cdOffset = 0;\r
-\r
- /**\r
- * Length of central directory.\r
- *\r
- * @since 1.1\r
- */\r
- private long cdLength = 0;\r
-\r
- /**\r
- * Helper, a 0 as ZipShort.\r
- *\r
- * @since 1.1\r
- */\r
- private static final byte[] ZERO = {0, 0};\r
-\r
- /**\r
- * Helper, a 0 as ZipLong.\r
- *\r
- * @since 1.1\r
- */\r
- private static final byte[] LZERO = {0, 0, 0, 0};\r
-\r
- /**\r
- * Holds the offsets of the LFH starts for each entry.\r
- *\r
- * @since 1.1\r
- */\r
- private final Map offsets = new HashMap();\r
-\r
- /**\r
- * The encoding to use for filenames and the file comment.\r
- *\r
- * <p>For a list of possible values see <a\r
- * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.\r
- * Defaults to the platform's default character encoding.</p>\r
- *\r
- * @since 1.3\r
- */\r
- private String encoding = null;\r
-\r
- /**\r
- * The zip encoding to use for filenames and the file comment.\r
- *\r
- * This field is of internal use and will be set in {@link\r
- * #setEncoding(String)}.\r
- */\r
- private ZipEncoding zipEncoding =\r
- ZipEncodingHelper.getZipEncoding(DEFAULT_ENCODING);\r
-\r
- // CheckStyle:VisibilityModifier OFF - bc\r
-\r
- /**\r
- * This Deflater object is used for output.\r
- *\r
- * <p>This attribute is only protected to provide a level of API\r
- * backwards compatibility. This class used to extend {@link\r
- * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to\r
- * Revision 1.13.</p>\r
- *\r
- * @since 1.14\r
- */\r
- protected Deflater def = new Deflater(level, true);\r
-\r
- /**\r
- * This buffer servers as a Deflater.\r
- *\r
- * <p>This attribute is only protected to provide a level of API\r
- * backwards compatibility. This class used to extend {@link\r
- * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to\r
- * Revision 1.13.</p>\r
- *\r
- * @since 1.14\r
- */\r
- protected byte[] buf = new byte[BUFFER_SIZE];\r
-\r
- // CheckStyle:VisibilityModifier ON\r
-\r
- /**\r
- * Optional random access output.\r
- *\r
- * @since 1.14\r
- */\r
- private RandomAccessFile raf = null;\r
-\r
- /**\r
- * whether to use the general purpose bit flag when writing UTF-8\r
- * filenames or not.\r
- */\r
- private boolean useUTF8Flag = true; \r
-\r
- /**\r
- * Whether to encode non-encodable file names as UTF-8.\r
- */\r
- private boolean fallbackToUTF8 = false;\r
-\r
- /**\r
- * whether to create UnicodePathExtraField-s for each entry.\r
- */\r
- private UnicodeExtraFieldPolicy createUnicodeExtraFields =\r
- UnicodeExtraFieldPolicy.NEVER;\r
-\r
- /**\r
- * Creates a new ZIP OutputStream filtering the underlying stream.\r
- * @param out the outputstream to zip\r
- * @since 1.1\r
- */\r
- public ZipOutputStream(OutputStream out) {\r
- super(out);\r
- }\r
-\r
- /**\r
- * Creates a new ZIP OutputStream writing to a File. Will use\r
- * random access if possible.\r
- * @param file the file to zip to\r
- * @since 1.14\r
- * @throws IOException on error\r
- */\r
- public ZipOutputStream(File file) throws IOException {\r
- super(null);\r
-\r
- try {\r
- raf = new RandomAccessFile(file, "rw");\r
- raf.setLength(0);\r
- } catch (IOException e) {\r
- if (raf != null) {\r
- try {\r
- raf.close();\r
- } catch (IOException inner) {\r
- // ignore\r
- }\r
- raf = null;\r
- }\r
- out = new FileOutputStream(file);\r
- }\r
- }\r
-\r
- /**\r
- * This method indicates whether this archive is writing to a\r
- * seekable stream (i.e., to a random access file).\r
- *\r
- * <p>For seekable streams, you don't need to calculate the CRC or\r
- * uncompressed size for {@link #STORED} entries before\r
- * invoking {@link #putNextEntry}.\r
- * @return true if seekable\r
- * @since 1.17\r
- */\r
- public boolean isSeekable() {\r
- return raf != null;\r
- }\r
-\r
- /**\r
- * The encoding to use for filenames and the file comment.\r
- *\r
- * <p>For a list of possible values see <a\r
- * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.\r
- * Defaults to the platform's default character encoding.</p>\r
- * @param encoding the encoding value\r
- * @since 1.3\r
- */\r
- public void setEncoding(final String encoding) {\r
- this.encoding = encoding;\r
- this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);\r
- useUTF8Flag &= ZipEncodingHelper.isUTF8(encoding);\r
- }\r
-\r
- /**\r
- * The encoding to use for filenames and the file comment.\r
- *\r
- * @return null if using the platform's default character encoding.\r
- *\r
- * @since 1.3\r
- */\r
- public String getEncoding() {\r
- return encoding;\r
- }\r
-\r
- /**\r
- * Whether to set the language encoding flag if the file name\r
- * encoding is UTF-8.\r
- *\r
- * <p>Defaults to true.</p>\r
- */\r
- public void setUseLanguageEncodingFlag(boolean b) {\r
- useUTF8Flag = b && ZipEncodingHelper.isUTF8(encoding);\r
- }\r
-\r
- /**\r
- * Whether to create Unicode Extra Fields.\r
- *\r
- * <p>Defaults to NEVER.</p>\r
- */\r
- public void setCreateUnicodeExtraFields(UnicodeExtraFieldPolicy b) {\r
- createUnicodeExtraFields = b;\r
- }\r
-\r
- /**\r
- * Whether to fall back to UTF and the language encoding flag if\r
- * the file name cannot be encoded using the specified encoding.\r
- *\r
- * <p>Defaults to false.</p>\r
- */\r
- public void setFallbackToUTF8(boolean b) {\r
- fallbackToUTF8 = b;\r
- }\r
-\r
- /**\r
- * Finishs writing the contents and closes this as well as the\r
- * underlying stream.\r
- *\r
- * @since 1.1\r
- * @throws IOException on error\r
- */\r
- public void finish() throws IOException {\r
- closeEntry();\r
- cdOffset = written;\r
- for (Iterator i = entries.iterator(); i.hasNext(); ) {\r
- writeCentralFileHeader((ZipEntry) i.next());\r
- }\r
- cdLength = written - cdOffset;\r
- writeCentralDirectoryEnd();\r
- offsets.clear();\r
- entries.clear();\r
- }\r
-\r
- /**\r
- * Writes all necessary data for this entry.\r
- *\r
- * @since 1.1\r
- * @throws IOException on error\r
- */\r
- public void closeEntry() throws IOException {\r
- if (entry == null) {\r
- return;\r
- }\r
-\r
- long realCrc = crc.getValue();\r
- crc.reset();\r
-\r
- if (entry.getMethod() == DEFLATED) {\r
- def.finish();\r
- while (!def.finished()) {\r
- deflate();\r
- }\r
-\r
- entry.setSize(adjustToLong(def.getTotalIn()));\r
- entry.setCompressedSize(adjustToLong(def.getTotalOut()));\r
- entry.setCrc(realCrc);\r
-\r
- def.reset();\r
-\r
- written += entry.getCompressedSize();\r
- } else if (raf == null) {\r
- if (entry.getCrc() != realCrc) {\r
- throw new ZipException("bad CRC checksum for entry "\r
- + entry.getName() + ": "\r
- + Long.toHexString(entry.getCrc())\r
- + " instead of "\r
- + Long.toHexString(realCrc));\r
- }\r
-\r
- if (entry.getSize() != written - dataStart) {\r
- throw new ZipException("bad size for entry "\r
- + entry.getName() + ": "\r
- + entry.getSize()\r
- + " instead of "\r
- + (written - dataStart));\r
- }\r
- } else { /* method is STORED and we used RandomAccessFile */\r
- long size = written - dataStart;\r
-\r
- entry.setSize(size);\r
- entry.setCompressedSize(size);\r
- entry.setCrc(realCrc);\r
- }\r
-\r
- // If random access output, write the local file header containing\r
- // the correct CRC and compressed/uncompressed sizes\r
- if (raf != null) {\r
- long save = raf.getFilePointer();\r
-\r
- raf.seek(localDataStart);\r
- writeOut(ZipLong.getBytes(entry.getCrc()));\r
- writeOut(ZipLong.getBytes(entry.getCompressedSize()));\r
- writeOut(ZipLong.getBytes(entry.getSize()));\r
- raf.seek(save);\r
- }\r
-\r
- writeDataDescriptor(entry);\r
- entry = null;\r
- }\r
-\r
- /**\r
- * Begin writing next entry.\r
- * @param ze the entry to write\r
- * @since 1.1\r
- * @throws IOException on error\r
- */\r
- public void putNextEntry(ZipEntry ze) throws IOException {\r
- closeEntry();\r
-\r
- entry = ze;\r
- entries.add(entry);\r
-\r
- if (entry.getMethod() == -1) { // not specified\r
- entry.setMethod(method);\r
- }\r
-\r
- if (entry.getTime() == -1) { // not specified\r
- entry.setTime(System.currentTimeMillis());\r
- }\r
-\r
- // Size/CRC not required if RandomAccessFile is used\r
- if (entry.getMethod() == STORED && raf == null) {\r
- if (entry.getSize() == -1) {\r
- throw new ZipException("uncompressed size is required for"\r
- + " STORED method when not writing to a"\r
- + " file");\r
- }\r
- if (entry.getCrc() == -1) {\r
- throw new ZipException("crc checksum is required for STORED"\r
- + " method when not writing to a file");\r
- }\r
- entry.setCompressedSize(entry.getSize());\r
- }\r
-\r
- if (entry.getMethod() == DEFLATED && hasCompressionLevelChanged) {\r
- def.setLevel(level);\r
- hasCompressionLevelChanged = false;\r
- }\r
- writeLocalFileHeader(entry);\r
- }\r
-\r
- /**\r
- * Set the file comment.\r
- * @param comment the comment\r
- * @since 1.1\r
- */\r
- public void setComment(String comment) {\r
- this.comment = comment;\r
- }\r
-\r
- /**\r
- * Sets the compression level for subsequent entries.\r
- *\r
- * <p>Default is Deflater.DEFAULT_COMPRESSION.</p>\r
- * @param level the compression level.\r
- * @throws IllegalArgumentException if an invalid compression\r
- * level is specified.\r
- * @since 1.1\r
- */\r
- public void setLevel(int level) {\r
- if (level < Deflater.DEFAULT_COMPRESSION\r
- || level > Deflater.BEST_COMPRESSION) {\r
- throw new IllegalArgumentException("Invalid compression level: "\r
- + level);\r
- }\r
- hasCompressionLevelChanged = (this.level != level);\r
- this.level = level;\r
- }\r
-\r
- /**\r
- * Sets the default compression method for subsequent entries.\r
- *\r
- * <p>Default is DEFLATED.</p>\r
- * @param method an <code>int</code> from java.util.zip.ZipEntry\r
- * @since 1.1\r
- */\r
- public void setMethod(int method) {\r
- this.method = method;\r
- }\r
-\r
- /**\r
- * Writes bytes to ZIP entry.\r
- * @param b the byte array to write\r
- * @param offset the start position to write from\r
- * @param length the number of bytes to write\r
- * @throws IOException on error\r
- */\r
- public void write(byte[] b, int offset, int length) throws IOException {\r
- if (entry.getMethod() == DEFLATED) {\r
- if (length > 0) {\r
- if (!def.finished()) {\r
- if (length <= DEFLATER_BLOCK_SIZE) {\r
- def.setInput(b, offset, length);\r
- deflateUntilInputIsNeeded();\r
- } else {\r
- final int fullblocks = length / DEFLATER_BLOCK_SIZE;\r
- for (int i = 0; i < fullblocks; i++) {\r
- def.setInput(b, offset + i * DEFLATER_BLOCK_SIZE,\r
- DEFLATER_BLOCK_SIZE);\r
- deflateUntilInputIsNeeded();\r
- }\r
- final int done = fullblocks * DEFLATER_BLOCK_SIZE;\r
- if (done < length) {\r
- def.setInput(b, offset + done, length - done);\r
- deflateUntilInputIsNeeded();\r
- }\r
- }\r
- }\r
- }\r
- } else {\r
- writeOut(b, offset, length);\r
- written += length;\r
- }\r
- crc.update(b, offset, length);\r
- }\r
-\r
- /**\r
- * Writes a single byte to ZIP entry.\r
- *\r
- * <p>Delegates to the three arg method.</p>\r
- * @param b the byte to write\r
- * @since 1.14\r
- * @throws IOException on error\r
- */\r
- public void write(int b) throws IOException {\r
- byte[] buff = new byte[1];\r
- buff[0] = (byte) (b & BYTE_MASK);\r
- write(buff, 0, 1);\r
- }\r
-\r
- /**\r
- * Closes this output stream and releases any system resources\r
- * associated with the stream.\r
- *\r
- * @exception IOException if an I/O error occurs.\r
- * @since 1.14\r
- */\r
- public void close() throws IOException {\r
- finish();\r
-\r
- if (raf != null) {\r
- raf.close();\r
- }\r
- if (out != null) {\r
- out.close();\r
- }\r
- }\r
-\r
- /**\r
- * Flushes this output stream and forces any buffered output bytes\r
- * to be written out to the stream.\r
- *\r
- * @exception IOException if an I/O error occurs.\r
- * @since 1.14\r
- */\r
- public void flush() throws IOException {\r
- if (out != null) {\r
- out.flush();\r
- }\r
- }\r
-\r
- /*\r
- * Various ZIP constants\r
- */\r
- /**\r
- * local file header signature\r
- *\r
- * @since 1.1\r
- */\r
- protected static final byte[] LFH_SIG = ZipLong.getBytes(0X04034B50L);\r
- /**\r
- * data descriptor signature\r
- *\r
- * @since 1.1\r
- */\r
- protected static final byte[] DD_SIG = ZipLong.getBytes(0X08074B50L);\r
- /**\r
- * central file header signature\r
- *\r
- * @since 1.1\r
- */\r
- protected static final byte[] CFH_SIG = ZipLong.getBytes(0X02014B50L);\r
- /**\r
- * end of central dir signature\r
- *\r
- * @since 1.1\r
- */\r
- protected static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L);\r
-\r
- /**\r
- * Writes next block of compressed data to the output stream.\r
- * @throws IOException on error\r
- *\r
- * @since 1.14\r
- */\r
- protected final void deflate() throws IOException {\r
- int len = def.deflate(buf, 0, buf.length);\r
- if (len > 0) {\r
- writeOut(buf, 0, len);\r
- }\r
- }\r
-\r
- /**\r
- * Writes the local file header entry\r
- * @param ze the entry to write\r
- * @throws IOException on error\r
- *\r
- * @since 1.1\r
- */\r
- protected void writeLocalFileHeader(ZipEntry ze) throws IOException {\r
-\r
- boolean encodable = zipEncoding.canEncode(ze.getName());\r
- \r
- final ZipEncoding entryEncoding;\r
- \r
- if (!encodable && fallbackToUTF8) {\r
- entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING;\r
- } else {\r
- entryEncoding = zipEncoding;\r
- }\r
- \r
- ByteBuffer name = entryEncoding.encode(ze.getName()); \r
-\r
- if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER) {\r
-\r
- if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS\r
- || !encodable) {\r
- ze.addExtraField(new UnicodePathExtraField(ze.getName(),\r
- name.array(),\r
- name.arrayOffset(),\r
- name.limit()));\r
- }\r
-\r
- String comm = ze.getComment();\r
- if (comm != null && !"".equals(comm)) {\r
-\r
- boolean commentEncodable = this.zipEncoding.canEncode(comm);\r
-\r
- if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS\r
- || !commentEncodable) {\r
- ByteBuffer commentB = entryEncoding.encode(comm);\r
- ze.addExtraField(new UnicodeCommentExtraField(comm,\r
- commentB.array(),\r
- commentB.arrayOffset(),\r
- commentB.limit())\r
- );\r
- }\r
- }\r
- }\r
-\r
- offsets.put(ze, ZipLong.getBytes(written));\r
-\r
- writeOut(LFH_SIG);\r
- written += WORD;\r
-\r
- //store method in local variable to prevent multiple method calls\r
- final int zipMethod = ze.getMethod();\r
-\r
- writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod,\r
- !encodable\r
- && fallbackToUTF8);\r
- written += WORD;\r
-\r
- // compression method\r
- writeOut(ZipShort.getBytes(zipMethod));\r
- written += SHORT;\r
-\r
- // last mod. time and date\r
- writeOut(toDosTime(ze.getTime()));\r
- written += WORD;\r
-\r
- // CRC\r
- // compressed length\r
- // uncompressed length\r
- localDataStart = written;\r
- if (zipMethod == DEFLATED || raf != null) {\r
- writeOut(LZERO);\r
- writeOut(LZERO);\r
- writeOut(LZERO);\r
- } else {\r
- writeOut(ZipLong.getBytes(ze.getCrc()));\r
- writeOut(ZipLong.getBytes(ze.getSize()));\r
- writeOut(ZipLong.getBytes(ze.getSize()));\r
- }\r
- // CheckStyle:MagicNumber OFF\r
- written += 12;\r
- // CheckStyle:MagicNumber ON\r
-\r
- // file name length\r
- writeOut(ZipShort.getBytes(name.limit()));\r
- written += SHORT;\r
-\r
- // extra field length\r
- byte[] extra = ze.getLocalFileDataExtra();\r
- writeOut(ZipShort.getBytes(extra.length));\r
- written += SHORT;\r
-\r
- // file name\r
- writeOut(name.array(), name.arrayOffset(), name.limit());\r
- written += name.limit();\r
-\r
- // extra field\r
- writeOut(extra);\r
- written += extra.length;\r
-\r
- dataStart = written;\r
- }\r
-\r
- /**\r
- * Writes the data descriptor entry.\r
- * @param ze the entry to write\r
- * @throws IOException on error\r
- *\r
- * @since 1.1\r
- */\r
- protected void writeDataDescriptor(ZipEntry ze) throws IOException {\r
- if (ze.getMethod() != DEFLATED || raf != null) {\r
- return;\r
- }\r
- writeOut(DD_SIG);\r
- writeOut(ZipLong.getBytes(entry.getCrc()));\r
- writeOut(ZipLong.getBytes(entry.getCompressedSize()));\r
- writeOut(ZipLong.getBytes(entry.getSize()));\r
- // CheckStyle:MagicNumber OFF\r
- written += 16;\r
- // CheckStyle:MagicNumber ON\r
- }\r
-\r
- /**\r
- * Writes the central file header entry.\r
- * @param ze the entry to write\r
- * @throws IOException on error\r
- *\r
- * @since 1.1\r
- */\r
- protected void writeCentralFileHeader(ZipEntry ze) throws IOException {\r
- writeOut(CFH_SIG);\r
- written += WORD;\r
-\r
- // version made by\r
- // CheckStyle:MagicNumber OFF\r
- writeOut(ZipShort.getBytes((ze.getPlatform() << 8) | 20));\r
- written += SHORT;\r
-\r
- final int zipMethod = ze.getMethod();\r
- final boolean encodable = zipEncoding.canEncode(ze.getName());\r
- writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod,\r
- !encodable\r
- && fallbackToUTF8);\r
- written += WORD;\r
-\r
- // compression method\r
- writeOut(ZipShort.getBytes(zipMethod));\r
- written += SHORT;\r
-\r
- // last mod. time and date\r
- writeOut(toDosTime(ze.getTime()));\r
- written += WORD;\r
-\r
- // CRC\r
- // compressed length\r
- // uncompressed length\r
- writeOut(ZipLong.getBytes(ze.getCrc()));\r
- writeOut(ZipLong.getBytes(ze.getCompressedSize()));\r
- writeOut(ZipLong.getBytes(ze.getSize()));\r
- // CheckStyle:MagicNumber OFF\r
- written += 12;\r
- // CheckStyle:MagicNumber ON\r
-\r
- // file name length\r
- final ZipEncoding entryEncoding;\r
- \r
- if (!encodable && fallbackToUTF8) {\r
- entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING;\r
- } else {\r
- entryEncoding = zipEncoding;\r
- }\r
- \r
- ByteBuffer name = entryEncoding.encode(ze.getName()); \r
-\r
- writeOut(ZipShort.getBytes(name.limit()));\r
- written += SHORT;\r
-\r
- // extra field length\r
- byte[] extra = ze.getCentralDirectoryExtra();\r
- writeOut(ZipShort.getBytes(extra.length));\r
- written += SHORT;\r
-\r
- // file comment length\r
- String comm = ze.getComment();\r
- if (comm == null) {\r
- comm = "";\r
- }\r
- \r
- ByteBuffer commentB = entryEncoding.encode(comm);\r
- \r
- writeOut(ZipShort.getBytes(commentB.limit()));\r
- written += SHORT;\r
-\r
- // disk number start\r
- writeOut(ZERO);\r
- written += SHORT;\r
-\r
- // internal file attributes\r
- writeOut(ZipShort.getBytes(ze.getInternalAttributes()));\r
- written += SHORT;\r
-\r
- // external file attributes\r
- writeOut(ZipLong.getBytes(ze.getExternalAttributes()));\r
- written += WORD;\r
-\r
- // relative offset of LFH\r
- writeOut((byte[]) offsets.get(ze));\r
- written += WORD;\r
-\r
- // file name\r
- writeOut(name.array(), name.arrayOffset(), name.limit());\r
- written += name.limit();\r
-\r
- // extra field\r
- writeOut(extra);\r
- written += extra.length;\r
-\r
- // file comment\r
- writeOut(commentB.array(), commentB.arrayOffset(), commentB.limit());\r
- written += commentB.limit();\r
- }\r
-\r
- /**\r
- * Writes the "End of central dir record".\r
- * @throws IOException on error\r
- *\r
- * @since 1.1\r
- */\r
- protected void writeCentralDirectoryEnd() throws IOException {\r
- writeOut(EOCD_SIG);\r
-\r
- // disk numbers\r
- writeOut(ZERO);\r
- writeOut(ZERO);\r
-\r
- // number of entries\r
- byte[] num = ZipShort.getBytes(entries.size());\r
- writeOut(num);\r
- writeOut(num);\r
-\r
- // length and location of CD\r
- writeOut(ZipLong.getBytes(cdLength));\r
- writeOut(ZipLong.getBytes(cdOffset));\r
-\r
- // ZIP file comment\r
- ByteBuffer data = this.zipEncoding.encode(comment);\r
- writeOut(ZipShort.getBytes(data.limit()));\r
- writeOut(data.array(), data.arrayOffset(), data.limit());\r
- }\r
-\r
- /**\r
- * Smallest date/time ZIP can handle.\r
- *\r
- * @since 1.1\r
- */\r
- private static final byte[] DOS_TIME_MIN = ZipLong.getBytes(0x00002100L);\r
-\r
- /**\r
- * Convert a Date object to a DOS date/time field.\r
- * @param time the <code>Date</code> to convert\r
- * @return the date as a <code>ZipLong</code>\r
- * @since 1.1\r
- */\r
- protected static ZipLong toDosTime(Date time) {\r
- return new ZipLong(toDosTime(time.getTime()));\r
- }\r
-\r
- /**\r
- * Convert a Date object to a DOS date/time field.\r
- *\r
- * <p>Stolen from InfoZip's <code>fileio.c</code></p>\r
- * @param t number of milliseconds since the epoch\r
- * @return the date as a byte array\r
- * @since 1.26\r
- */\r
- @SuppressWarnings("deprecation")\r
- protected static byte[] toDosTime(long t) {\r
- Date time = new Date(t);\r
- // CheckStyle:MagicNumberCheck OFF - I do not think that using constants\r
- // here will improve the readablity\r
- int year = time.getYear() + 1900;\r
- if (year < 1980) {\r
- return DOS_TIME_MIN;\r
- }\r
- int month = time.getMonth() + 1;\r
- long value = ((year - 1980) << 25)\r
- | (month << 21)\r
- | (time.getDate() << 16)\r
- | (time.getHours() << 11)\r
- | (time.getMinutes() << 5)\r
- | (time.getSeconds() >> 1);\r
- return ZipLong.getBytes(value);\r
- // CheckStyle:MagicNumberCheck ON\r
- }\r
-\r
- /**\r
- * Retrieve the bytes for the given String in the encoding set for\r
- * this Stream.\r
- * @param name the string to get bytes from\r
- * @return the bytes as a byte array\r
- * @throws ZipException on error\r
- *\r
- * @since 1.3\r
- */\r
- protected byte[] getBytes(String name) throws ZipException {\r
- try {\r
- ByteBuffer b =\r
- ZipEncodingHelper.getZipEncoding(encoding).encode(name);\r
- byte[] result = new byte[b.limit()];\r
- System.arraycopy(b.array(), b.arrayOffset(), result, 0,\r
- result.length);\r
- return result;\r
- } catch (IOException ex) {\r
- throw new ZipException("Failed to encode name: " + ex.getMessage());\r
- }\r
- }\r
-\r
- /**\r
- * Write bytes to output or random access file.\r
- * @param data the byte array to write\r
- * @throws IOException on error\r
- *\r
- * @since 1.14\r
- */\r
- protected final void writeOut(byte[] data) throws IOException {\r
- writeOut(data, 0, data.length);\r
- }\r
-\r
- /**\r
- * Write bytes to output or random access file.\r
- * @param data the byte array to write\r
- * @param offset the start position to write from\r
- * @param length the number of bytes to write\r
- * @throws IOException on error\r
- *\r
- * @since 1.14\r
- */\r
- protected final void writeOut(byte[] data, int offset, int length)\r
- throws IOException {\r
- if (raf != null) {\r
- raf.write(data, offset, length);\r
- } else {\r
- out.write(data, offset, length);\r
- }\r
- }\r
-\r
- /**\r
- * Assumes a negative integer really is a positive integer that\r
- * has wrapped around and re-creates the original value.\r
- * @param i the value to treat as unsigned int.\r
- * @return the unsigned int as a long.\r
- * @since 1.34\r
- */\r
- protected static long adjustToLong(int i) {\r
- if (i < 0) {\r
- return 2 * ((long) Integer.MAX_VALUE) + 2 + i;\r
- } else {\r
- return i;\r
- }\r
- }\r
-\r
- private void deflateUntilInputIsNeeded() throws IOException {\r
- while (!def.needsInput()) {\r
- deflate();\r
- }\r
- }\r
-\r
- private void writeVersionNeededToExtractAndGeneralPurposeBits(final int\r
- zipMethod,\r
- final boolean\r
- utfFallback)\r
- throws IOException {\r
-\r
- // CheckStyle:MagicNumber OFF\r
- int versionNeededToExtract = 10;\r
- int generalPurposeFlag = (useUTF8Flag || utfFallback) ? UFT8_NAMES_FLAG : 0;\r
- if (zipMethod == DEFLATED && raf == null) {\r
- // requires version 2 as we are going to store length info\r
- // in the data descriptor\r
- versionNeededToExtract = 20;\r
- // bit3 set to signal, we use a data descriptor\r
- generalPurposeFlag |= 8;\r
- }\r
- // CheckStyle:MagicNumber ON\r
-\r
- // version needed to extract\r
- writeOut(ZipShort.getBytes(versionNeededToExtract));\r
- // general purpose bit flag\r
- writeOut(ZipShort.getBytes(generalPurposeFlag));\r
- }\r
-\r
- /**\r
- * enum that represents the possible policies for creating Unicode\r
- * extra fields.\r
- */\r
- public static final class UnicodeExtraFieldPolicy {\r
- /**\r
- * Always create Unicode extra fields.\r
- */\r
- public static final UnicodeExtraFieldPolicy ALWAYS =\r
- new UnicodeExtraFieldPolicy("always");\r
- /**\r
- * Never create Unicode extra fields.\r
- */\r
- public static final UnicodeExtraFieldPolicy NEVER =\r
- new UnicodeExtraFieldPolicy("never");\r
- /**\r
- * Create Unicode extra fields for filenames that cannot be\r
- * encoded using the specified encoding.\r
- */\r
- public static final UnicodeExtraFieldPolicy NOT_ENCODEABLE =\r
- new UnicodeExtraFieldPolicy("not encodeable");\r
-\r
- private final String name;\r
- private UnicodeExtraFieldPolicy(String n) {\r
- name = n;\r
- }\r
- public String toString() {\r
- return name;\r
- }\r
- }\r
-}\r
+/*
+ * 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.tools.zip;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+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;
+
+/**
+ * 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.
+ *
+ * <p>This class will try to use {@link java.io.RandomAccessFile
+ * RandomAccessFile} when you know that the output is going to go to a
+ * file.</p>
+ *
+ * <p>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
+ * #putNextEntry putNextEntry} can be called.</p>
+ *
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class ZipOutputStream extends FilterOutputStream {
+
+ private static final int BYTE_MASK = 0xFF;
+ private static final int SHORT = 2;
+ private static final int WORD = 4;
+ private static final int BUFFER_SIZE = 512;
+ /*
+ * 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.
+ *
+ * @since 1.1
+ */
+ public static final int DEFLATED = java.util.zip.ZipEntry.DEFLATED;
+
+ /**
+ * Default compression level for deflated entries.
+ *
+ * @since Ant 1.7
+ */
+ public static final int DEFAULT_COMPRESSION = Deflater.DEFAULT_COMPRESSION;
+
+ /**
+ * Compression method for stored entries.
+ *
+ * @since 1.1
+ */
+ public static final int STORED = java.util.zip.ZipEntry.STORED;
+
+ /**
+ * default encoding for file names and comment.
+ */
+ static final String DEFAULT_ENCODING = null;
+
+ /**
+ * General purpose flag, which indicates that filenames are
+ * written in utf-8.
+ */
+ public static final int UFT8_NAMES_FLAG = 1 << 11;
+
+ /**
+ * General purpose flag, which indicates that filenames are
+ * written in utf-8.
+ * @deprecated use {@link #UFT8_NAMES_FLAG} instead
+ */
+ public static final int EFS_FLAG = UFT8_NAMES_FLAG;
+
+ /**
+ * Current entry.
+ *
+ * @since 1.1
+ */
+ private ZipEntry entry;
+
+ /**
+ * The file comment.
+ *
+ * @since 1.1
+ */
+ private String comment = "";
+
+ /**
+ * Compression level for next entry.
+ *
+ * @since 1.1
+ */
+ private int level = DEFAULT_COMPRESSION;
+
+ /**
+ * Has the compression level changed when compared to the last
+ * entry?
+ *
+ * @since 1.5
+ */
+ private boolean hasCompressionLevelChanged = false;
+
+ /**
+ * Default compression method for next entry.
+ *
+ * @since 1.1
+ */
+ private int method = java.util.zip.ZipEntry.DEFLATED;
+
+ /**
+ * List of ZipEntries written so far.
+ *
+ * @since 1.1
+ */
+ private final List entries = new LinkedList();
+
+ /**
+ * CRC instance to avoid parsing DEFLATED data twice.
+ *
+ * @since 1.1
+ */
+ private final CRC32 crc = new CRC32();
+
+ /**
+ * Count the bytes written to out.
+ *
+ * @since 1.1
+ */
+ private long written = 0;
+
+ /**
+ * Data for local header data
+ *
+ * @since 1.1
+ */
+ private long dataStart = 0;
+
+ /**
+ * Offset for CRC entry in the local file header data for the
+ * current entry starts here.
+ *
+ * @since 1.15
+ */
+ private long localDataStart = 0;
+
+ /**
+ * Start of central directory.
+ *
+ * @since 1.1
+ */
+ private long cdOffset = 0;
+
+ /**
+ * Length of central directory.
+ *
+ * @since 1.1
+ */
+ private long cdLength = 0;
+
+ /**
+ * Helper, a 0 as ZipShort.
+ *
+ * @since 1.1
+ */
+ private static final byte[] ZERO = {0, 0};
+
+ /**
+ * Helper, a 0 as ZipLong.
+ *
+ * @since 1.1
+ */
+ private static final byte[] LZERO = {0, 0, 0, 0};
+
+ /**
+ * Holds the offsets of the LFH starts for each entry.
+ *
+ * @since 1.1
+ */
+ private final Map offsets = new HashMap();
+
+ /**
+ * The encoding to use for filenames and the file comment.
+ *
+ * <p>For a list of possible values see <a
+ * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.
+ * Defaults to the platform's default character encoding.</p>
+ *
+ * @since 1.3
+ */
+ private String encoding = null;
+
+ /**
+ * 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);
+
+ // CheckStyle:VisibilityModifier OFF - bc
+
+ /**
+ * This Deflater object is used for output.
+ *
+ * <p>This attribute is only protected to provide a level of API
+ * backwards compatibility. This class used to extend {@link
+ * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to
+ * Revision 1.13.</p>
+ *
+ * @since 1.14
+ */
+ protected Deflater def = new Deflater(level, true);
+
+ /**
+ * This buffer servers as a Deflater.
+ *
+ * <p>This attribute is only protected to provide a level of API
+ * backwards compatibility. This class used to extend {@link
+ * java.util.zip.DeflaterOutputStream DeflaterOutputStream} up to
+ * Revision 1.13.</p>
+ *
+ * @since 1.14
+ */
+ protected byte[] buf = new byte[BUFFER_SIZE];
+
+ // CheckStyle:VisibilityModifier ON
+
+ /**
+ * Optional random access output.
+ *
+ * @since 1.14
+ */
+ private RandomAccessFile raf = null;
+
+ /**
+ * 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;
+
+ /**
+ * Creates a new ZIP OutputStream filtering the underlying stream.
+ * @param out the outputstream to zip
+ * @since 1.1
+ */
+ public ZipOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ /**
+ * Creates a new ZIP OutputStream writing to a File. Will use
+ * random access if possible.
+ * @param file the file to zip to
+ * @since 1.14
+ * @throws IOException on error
+ */
+ public ZipOutputStream(File file) throws IOException {
+ super(null);
+
+ try {
+ raf = new RandomAccessFile(file, "rw");
+ raf.setLength(0);
+ } catch (IOException e) {
+ if (raf != null) {
+ try {
+ raf.close();
+ } catch (IOException inner) {
+ // ignore
+ }
+ raf = null;
+ }
+ out = new FileOutputStream(file);
+ }
+ }
+
+ /**
+ * This method indicates whether this archive is writing to a
+ * seekable stream (i.e., to a random access file).
+ *
+ * <p>For seekable streams, you don't need to calculate the CRC or
+ * uncompressed size for {@link #STORED} entries before
+ * invoking {@link #putNextEntry}.
+ * @return true if seekable
+ * @since 1.17
+ */
+ public boolean isSeekable() {
+ return raf != null;
+ }
+
+ /**
+ * The encoding to use for filenames and the file comment.
+ *
+ * <p>For a list of possible values see <a
+ * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>.
+ * Defaults to the platform's default character encoding.</p>
+ * @param encoding the encoding value
+ * @since 1.3
+ */
+ public void setEncoding(final String encoding) {
+ this.encoding = encoding;
+ this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
+ useUTF8Flag &= ZipEncodingHelper.isUTF8(encoding);
+ }
+
+ /**
+ * The encoding to use for filenames and the file comment.
+ *
+ * @return null if using the platform's default character encoding.
+ *
+ * @since 1.3
+ */
+ public String getEncoding() {
+ return encoding;
+ }
+
+ /**
+ * Whether to set the language encoding flag if the file name
+ * encoding is UTF-8.
+ *
+ * <p>Defaults to true.</p>
+ */
+ public void setUseLanguageEncodingFlag(boolean b) {
+ useUTF8Flag = b && ZipEncodingHelper.isUTF8(encoding);
+ }
+
+ /**
+ * Whether to create Unicode Extra Fields.
+ *
+ * <p>Defaults to NEVER.</p>
+ */
+ 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.
+ *
+ * <p>Defaults to false.</p>
+ */
+ public void setFallbackToUTF8(boolean b) {
+ fallbackToUTF8 = b;
+ }
+
+ /**
+ * Finishs writing the contents and closes this as well as the
+ * underlying stream.
+ *
+ * @since 1.1
+ * @throws IOException on error
+ */
+ public void finish() throws IOException {
+ closeEntry();
+ cdOffset = written;
+ for (Iterator i = entries.iterator(); i.hasNext(); ) {
+ writeCentralFileHeader((ZipEntry) i.next());
+ }
+ cdLength = written - cdOffset;
+ writeCentralDirectoryEnd();
+ offsets.clear();
+ entries.clear();
+ }
+
+ /**
+ * Writes all necessary data for this entry.
+ *
+ * @since 1.1
+ * @throws IOException on error
+ */
+ public void closeEntry() throws IOException {
+ if (entry == null) {
+ return;
+ }
+
+ long realCrc = crc.getValue();
+ crc.reset();
+
+ if (entry.getMethod() == DEFLATED) {
+ def.finish();
+ while (!def.finished()) {
+ deflate();
+ }
+
+ entry.setSize(adjustToLong(def.getTotalIn()));
+ entry.setCompressedSize(adjustToLong(def.getTotalOut()));
+ entry.setCrc(realCrc);
+
+ def.reset();
+
+ written += entry.getCompressedSize();
+ } else if (raf == null) {
+ if (entry.getCrc() != realCrc) {
+ throw new ZipException("bad CRC checksum for entry "
+ + entry.getName() + ": "
+ + Long.toHexString(entry.getCrc())
+ + " instead of "
+ + Long.toHexString(realCrc));
+ }
+
+ if (entry.getSize() != written - dataStart) {
+ throw new ZipException("bad size for entry "
+ + entry.getName() + ": "
+ + entry.getSize()
+ + " instead of "
+ + (written - dataStart));
+ }
+ } else { /* method is STORED and we used RandomAccessFile */
+ long size = written - dataStart;
+
+ entry.setSize(size);
+ entry.setCompressedSize(size);
+ entry.setCrc(realCrc);
+ }
+
+ // If random access output, write the local file header containing
+ // the correct CRC and compressed/uncompressed sizes
+ if (raf != null) {
+ long save = raf.getFilePointer();
+
+ raf.seek(localDataStart);
+ writeOut(ZipLong.getBytes(entry.getCrc()));
+ writeOut(ZipLong.getBytes(entry.getCompressedSize()));
+ writeOut(ZipLong.getBytes(entry.getSize()));
+ raf.seek(save);
+ }
+
+ writeDataDescriptor(entry);
+ entry = null;
+ }
+
+ /**
+ * Begin writing next entry.
+ * @param ze the entry to write
+ * @since 1.1
+ * @throws IOException on error
+ */
+ public void putNextEntry(ZipEntry ze) throws IOException {
+ closeEntry();
+
+ entry = ze;
+ entries.add(entry);
+
+ if (entry.getMethod() == -1) { // not specified
+ entry.setMethod(method);
+ }
+
+ if (entry.getTime() == -1) { // not specified
+ entry.setTime(System.currentTimeMillis());
+ }
+
+ // Size/CRC not required if RandomAccessFile is used
+ if (entry.getMethod() == STORED && raf == null) {
+ if (entry.getSize() == -1) {
+ throw new ZipException("uncompressed size is required for"
+ + " STORED method when not writing to a"
+ + " file");
+ }
+ if (entry.getCrc() == -1) {
+ throw new ZipException("crc checksum is required for STORED"
+ + " method when not writing to a file");
+ }
+ entry.setCompressedSize(entry.getSize());
+ }
+
+ if (entry.getMethod() == DEFLATED && hasCompressionLevelChanged) {
+ def.setLevel(level);
+ hasCompressionLevelChanged = false;
+ }
+ writeLocalFileHeader(entry);
+ }
+
+ /**
+ * Set the file comment.
+ * @param comment the comment
+ * @since 1.1
+ */
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+
+ /**
+ * Sets the compression level for subsequent entries.
+ *
+ * <p>Default is Deflater.DEFAULT_COMPRESSION.</p>
+ * @param level the compression level.
+ * @throws IllegalArgumentException if an invalid compression
+ * level is specified.
+ * @since 1.1
+ */
+ 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.
+ *
+ * <p>Default is DEFLATED.</p>
+ * @param method an <code>int</code> from java.util.zip.ZipEntry
+ * @since 1.1
+ */
+ public void setMethod(int method) {
+ this.method = method;
+ }
+
+ /**
+ * 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
+ */
+ public void write(byte[] b, int offset, int length) throws IOException {
+ if (entry.getMethod() == DEFLATED) {
+ if (length > 0) {
+ if (!def.finished()) {
+ 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();
+ }
+ }
+ }
+ }
+ } else {
+ writeOut(b, offset, length);
+ written += length;
+ }
+ crc.update(b, offset, length);
+ }
+
+ /**
+ * Writes a single byte to ZIP entry.
+ *
+ * <p>Delegates to the three arg method.</p>
+ * @param b the byte to write
+ * @since 1.14
+ * @throws IOException on error
+ */
+ public void write(int b) throws IOException {
+ byte[] buff = new byte[1];
+ buff[0] = (byte) (b & BYTE_MASK);
+ write(buff, 0, 1);
+ }
+
+ /**
+ * Closes this output stream and releases any system resources
+ * associated with the stream.
+ *
+ * @exception IOException if an I/O error occurs.
+ * @since 1.14
+ */
+ public void close() throws IOException {
+ finish();
+
+ if (raf != null) {
+ raf.close();
+ }
+ if (out != null) {
+ out.close();
+ }
+ }
+
+ /**
+ * 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.
+ * @since 1.14
+ */
+ public void flush() throws IOException {
+ if (out != null) {
+ out.flush();
+ }
+ }
+
+ /*
+ * Various ZIP constants
+ */
+ /**
+ * local file header signature
+ *
+ * @since 1.1
+ */
+ protected static final byte[] LFH_SIG = ZipLong.getBytes(0X04034B50L);
+ /**
+ * data descriptor signature
+ *
+ * @since 1.1
+ */
+ protected static final byte[] DD_SIG = ZipLong.getBytes(0X08074B50L);
+ /**
+ * central file header signature
+ *
+ * @since 1.1
+ */
+ protected static final byte[] CFH_SIG = ZipLong.getBytes(0X02014B50L);
+ /**
+ * end of central dir signature
+ *
+ * @since 1.1
+ */
+ protected static final byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L);
+
+ /**
+ * Writes next block of compressed data to the output stream.
+ * @throws IOException on error
+ *
+ * @since 1.14
+ */
+ protected final void deflate() throws IOException {
+ int len = def.deflate(buf, 0, buf.length);
+ if (len > 0) {
+ writeOut(buf, 0, len);
+ }
+ }
+
+ /**
+ * Writes the local file header entry
+ * @param ze the entry to write
+ * @throws IOException on error
+ *
+ * @since 1.1
+ */
+ protected void writeLocalFileHeader(ZipEntry ze) throws IOException {
+
+ boolean encodable = zipEncoding.canEncode(ze.getName());
+
+ final ZipEncoding entryEncoding;
+
+ if (!encodable && fallbackToUTF8) {
+ entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING;
+ } else {
+ entryEncoding = zipEncoding;
+ }
+
+ ByteBuffer name = entryEncoding.encode(ze.getName());
+
+ if (createUnicodeExtraFields != UnicodeExtraFieldPolicy.NEVER) {
+
+ if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS
+ || !encodable) {
+ ze.addExtraField(new UnicodePathExtraField(ze.getName(),
+ name.array(),
+ name.arrayOffset(),
+ name.limit()));
+ }
+
+ String comm = ze.getComment();
+ if (comm != null && !"".equals(comm)) {
+
+ boolean commentEncodable = this.zipEncoding.canEncode(comm);
+
+ if (createUnicodeExtraFields == UnicodeExtraFieldPolicy.ALWAYS
+ || !commentEncodable) {
+ ByteBuffer commentB = entryEncoding.encode(comm);
+ ze.addExtraField(new UnicodeCommentExtraField(comm,
+ commentB.array(),
+ commentB.arrayOffset(),
+ commentB.limit())
+ );
+ }
+ }
+ }
+
+ offsets.put(ze, ZipLong.getBytes(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);
+ written += WORD;
+
+ // compression method
+ writeOut(ZipShort.getBytes(zipMethod));
+ written += SHORT;
+
+ // last mod. time and date
+ writeOut(toDosTime(ze.getTime()));
+ written += WORD;
+
+ // CRC
+ // compressed length
+ // uncompressed length
+ localDataStart = written;
+ if (zipMethod == DEFLATED || raf != null) {
+ writeOut(LZERO);
+ writeOut(LZERO);
+ writeOut(LZERO);
+ } else {
+ writeOut(ZipLong.getBytes(ze.getCrc()));
+ writeOut(ZipLong.getBytes(ze.getSize()));
+ writeOut(ZipLong.getBytes(ze.getSize()));
+ }
+ // 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());
+ written += name.limit();
+
+ // extra field
+ writeOut(extra);
+ written += extra.length;
+
+ dataStart = written;
+ }
+
+ /**
+ * Writes the data descriptor entry.
+ * @param ze the entry to write
+ * @throws IOException on error
+ *
+ * @since 1.1
+ */
+ protected void writeDataDescriptor(ZipEntry ze) throws IOException {
+ if (ze.getMethod() != DEFLATED || raf != null) {
+ return;
+ }
+ writeOut(DD_SIG);
+ writeOut(ZipLong.getBytes(entry.getCrc()));
+ writeOut(ZipLong.getBytes(entry.getCompressedSize()));
+ writeOut(ZipLong.getBytes(entry.getSize()));
+ // CheckStyle:MagicNumber OFF
+ written += 16;
+ // CheckStyle:MagicNumber ON
+ }
+
+ /**
+ * Writes the central file header entry.
+ * @param ze the entry to write
+ * @throws IOException on error
+ *
+ * @since 1.1
+ */
+ protected void writeCentralFileHeader(ZipEntry ze) throws IOException {
+ writeOut(CFH_SIG);
+ written += WORD;
+
+ // version made by
+ // CheckStyle:MagicNumber OFF
+ writeOut(ZipShort.getBytes((ze.getPlatform() << 8) | 20));
+ written += SHORT;
+
+ final int zipMethod = ze.getMethod();
+ final boolean encodable = zipEncoding.canEncode(ze.getName());
+ writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod,
+ !encodable
+ && fallbackToUTF8);
+ written += WORD;
+
+ // compression method
+ writeOut(ZipShort.getBytes(zipMethod));
+ written += SHORT;
+
+ // last mod. time and date
+ writeOut(toDosTime(ze.getTime()));
+ written += WORD;
+
+ // CRC
+ // compressed length
+ // uncompressed length
+ writeOut(ZipLong.getBytes(ze.getCrc()));
+ writeOut(ZipLong.getBytes(ze.getCompressedSize()));
+ writeOut(ZipLong.getBytes(ze.getSize()));
+ // CheckStyle:MagicNumber OFF
+ written += 12;
+ // CheckStyle:MagicNumber ON
+
+ // file name length
+ final ZipEncoding entryEncoding;
+
+ if (!encodable && fallbackToUTF8) {
+ entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING;
+ } else {
+ entryEncoding = zipEncoding;
+ }
+
+ ByteBuffer name = entryEncoding.encode(ze.getName());
+
+ 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 = entryEncoding.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((byte[]) offsets.get(ze));
+ written += WORD;
+
+ // file name
+ writeOut(name.array(), name.arrayOffset(), name.limit());
+ written += name.limit();
+
+ // extra field
+ writeOut(extra);
+ written += extra.length;
+
+ // file comment
+ writeOut(commentB.array(), commentB.arrayOffset(), commentB.limit());
+ written += commentB.limit();
+ }
+
+ /**
+ * Writes the "End of central dir record".
+ * @throws IOException on error
+ *
+ * @since 1.1
+ */
+ protected void writeCentralDirectoryEnd() throws IOException {
+ writeOut(EOCD_SIG);
+
+ // disk numbers
+ writeOut(ZERO);
+ writeOut(ZERO);
+
+ // number of entries
+ byte[] num = ZipShort.getBytes(entries.size());
+ writeOut(num);
+ writeOut(num);
+
+ // length and location of CD
+ writeOut(ZipLong.getBytes(cdLength));
+ writeOut(ZipLong.getBytes(cdOffset));
+
+ // ZIP file comment
+ ByteBuffer data = this.zipEncoding.encode(comment);
+ writeOut(ZipShort.getBytes(data.limit()));
+ writeOut(data.array(), data.arrayOffset(), data.limit());
+ }
+
+ /**
+ * Smallest date/time ZIP can handle.
+ *
+ * @since 1.1
+ */
+ private static final byte[] DOS_TIME_MIN = ZipLong.getBytes(0x00002100L);
+
+ /**
+ * Convert a Date object to a DOS date/time field.
+ * @param time the <code>Date</code> to convert
+ * @return the date as a <code>ZipLong</code>
+ * @since 1.1
+ */
+ protected static ZipLong toDosTime(Date time) {
+ return new ZipLong(toDosTime(time.getTime()));
+ }
+
+ /**
+ * Convert a Date object to a DOS date/time field.
+ *
+ * <p>Stolen from InfoZip's <code>fileio.c</code></p>
+ * @param t number of milliseconds since the epoch
+ * @return the date as a byte array
+ * @since 1.26
+ */
+ @SuppressWarnings("deprecation")
+ protected static byte[] toDosTime(long t) {
+ Date time = new Date(t);
+ // CheckStyle:MagicNumberCheck OFF - I do not think that using constants
+ // here will improve the readablity
+ int year = time.getYear() + 1900;
+ if (year < 1980) {
+ return DOS_TIME_MIN;
+ }
+ int month = time.getMonth() + 1;
+ long value = ((year - 1980) << 25)
+ | (month << 21)
+ | (time.getDate() << 16)
+ | (time.getHours() << 11)
+ | (time.getMinutes() << 5)
+ | (time.getSeconds() >> 1);
+ return ZipLong.getBytes(value);
+ // CheckStyle:MagicNumberCheck ON
+ }
+
+ /**
+ * Retrieve the bytes for the given String in the encoding set for
+ * this Stream.
+ * @param name the string to get bytes from
+ * @return the bytes as a byte array
+ * @throws ZipException on error
+ *
+ * @since 1.3
+ */
+ protected byte[] getBytes(String name) throws ZipException {
+ try {
+ ByteBuffer b =
+ ZipEncodingHelper.getZipEncoding(encoding).encode(name);
+ byte[] result = new byte[b.limit()];
+ System.arraycopy(b.array(), b.arrayOffset(), result, 0,
+ result.length);
+ return result;
+ } catch (IOException ex) {
+ throw new ZipException("Failed to encode name: " + ex.getMessage());
+ }
+ }
+
+ /**
+ * Write bytes to output or random access file.
+ * @param data the byte array to write
+ * @throws IOException on error
+ *
+ * @since 1.14
+ */
+ 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
+ *
+ * @since 1.14
+ */
+ 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);
+ }
+ }
+
+ /**
+ * Assumes a negative integer really is a positive integer that
+ * has wrapped around and re-creates the original value.
+ * @param i the value to treat as unsigned int.
+ * @return the unsigned int as a long.
+ * @since 1.34
+ */
+ protected static long adjustToLong(int i) {
+ if (i < 0) {
+ return 2 * ((long) Integer.MAX_VALUE) + 2 + i;
+ } else {
+ return i;
+ }
+ }
+
+ private void deflateUntilInputIsNeeded() throws IOException {
+ while (!def.needsInput()) {
+ deflate();
+ }
+ }
+
+ private void writeVersionNeededToExtractAndGeneralPurposeBits(final int
+ zipMethod,
+ final boolean
+ utfFallback)
+ throws IOException {
+
+ // CheckStyle:MagicNumber OFF
+ int versionNeededToExtract = 10;
+ int generalPurposeFlag = (useUTF8Flag || utfFallback) ? UFT8_NAMES_FLAG : 0;
+ if (zipMethod == DEFLATED && raf == null) {
+ // requires version 2 as we are going to store length info
+ // in the data descriptor
+ versionNeededToExtract = 20;
+ // bit3 set to signal, we use a data descriptor
+ generalPurposeFlag |= 8;
+ }
+ // CheckStyle:MagicNumber ON
+
+ // version needed to extract
+ writeOut(ZipShort.getBytes(versionNeededToExtract));
+ // general purpose bit flag
+ writeOut(ZipShort.getBytes(generalPurposeFlag));
+ }
+
+ /**
+ * 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;
+ }
+ public String toString() {
+ return name;
+ }
+ }
+}