ant-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From bode...@apache.org
Subject svn commit: r1346025 [3/3] - in /ant/core/trunk: ./ src/main/org/apache/tools/zip/ src/tests/junit/org/apache/tools/ant/taskdefs/ src/tests/junit/org/apache/tools/zip/
Date Mon, 04 Jun 2012 16:35:07 GMT
Modified: ant/core/trunk/src/main/org/apache/tools/zip/ZipOutputStream.java
URL: http://svn.apache.org/viewvc/ant/core/trunk/src/main/org/apache/tools/zip/ZipOutputStream.java?rev=1346025&r1=1346024&r2=1346025&view=diff
==============================================================================
--- ant/core/trunk/src/main/org/apache/tools/zip/ZipOutputStream.java (original)
+++ ant/core/trunk/src/main/org/apache/tools/zip/ZipOutputStream.java Mon Jun  4 16:35:06 2012
@@ -27,7 +27,6 @@ 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;
@@ -35,6 +34,15 @@ import java.util.zip.CRC32;
 import java.util.zip.Deflater;
 import java.util.zip.ZipException;
 
+import static org.apache.tools.zip.ZipConstants.DATA_DESCRIPTOR_MIN_VERSION;
+import static org.apache.tools.zip.ZipConstants.DWORD;
+import static org.apache.tools.zip.ZipConstants.INITIAL_VERSION;
+import static org.apache.tools.zip.ZipConstants.SHORT;
+import static org.apache.tools.zip.ZipConstants.WORD;
+import static org.apache.tools.zip.ZipConstants.ZIP64_MAGIC;
+import static org.apache.tools.zip.ZipConstants.ZIP64_MAGIC_SHORT;
+import static org.apache.tools.zip.ZipConstants.ZIP64_MIN_VERSION;
+
 /**
  * Reimplementation of {@link java.util.zip.ZipOutputStream
  * java.util.zip.ZipOutputStream} that does handle the extended
@@ -54,13 +62,22 @@ import java.util.zip.ZipException;
  * uncompressed size information is required before {@link
  * #putNextEntry putNextEntry} can be called.</p>
  *
+ * <p>As of Apache Ant 1.9.0 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 ZipEntry of
+ * unknown size then Zip64 extensions will be disabled by default.</p>
  */
 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;
+
+    /**
+     * indicates if this archive is finished.
+     */
+    private boolean finished = false;
+
     /* 
      * Apparently Deflater.setInput gets slowed down a lot on Sun JVMs
      * when it gets handed a really big buffer.  See
@@ -99,22 +116,17 @@ public class ZipOutputStream extends Fil
     /**
      * General purpose flag, which indicates that filenames are
      * written in utf-8.
+     * @deprecated use {@link GeneralPurposeBit#UFT8_NAMES_FLAG} instead
      */
-    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;
+    @Deprecated
+    public static final int EFS_FLAG = GeneralPurposeBit.UFT8_NAMES_FLAG;
 
     /**
      * Current entry.
      *
      * @since 1.1
      */
-    private ZipEntry entry;
+    private CurrentEntry entry;
 
     /**
      * The file comment.
@@ -150,7 +162,7 @@ public class ZipOutputStream extends Fil
      *
      * @since 1.1
      */
-    private final List entries = new LinkedList();
+    private final List<ZipEntry> entries = new LinkedList<ZipEntry>();
 
     /**
      * CRC instance to avoid parsing DEFLATED data twice.
@@ -167,21 +179,6 @@ public class ZipOutputStream extends Fil
     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
@@ -214,7 +211,7 @@ public class ZipOutputStream extends Fil
      *
      * @since 1.1
      */
-    private final Map offsets = new HashMap();
+    private final Map<ZipEntry, Long> offsets = new HashMap<ZipEntry, Long>();
 
     /**
      * The encoding to use for filenames and the file comment.
@@ -241,17 +238,11 @@ public class ZipOutputStream extends Fil
     /**
      * 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);
+    protected final Deflater def = new Deflater(level, true);
 
     /**
-     * This buffer servers as a Deflater.
+     * This buffer serves as a Deflater.
      *
      * <p>This attribute is only protected to provide a level of API
      * backwards compatibility.  This class used to extend {@link
@@ -269,7 +260,7 @@ public class ZipOutputStream extends Fil
      *
      * @since 1.14
      */
-    private RandomAccessFile raf = null;
+    private final RandomAccessFile raf;
 
     /**
      * whether to use the general purpose bit flag when writing UTF-8
@@ -285,8 +276,14 @@ public class ZipOutputStream extends Fil
     /**
      * whether to create UnicodePathExtraField-s for each entry.
      */
-    private UnicodeExtraFieldPolicy createUnicodeExtraFields =
-        UnicodeExtraFieldPolicy.NEVER;
+    private UnicodeExtraFieldPolicy createUnicodeExtraFields = UnicodeExtraFieldPolicy.NEVER;
+
+    /**
+     * Whether anything inside this archive has used a ZIP64 feature.
+     */
+    private boolean hasUsedZip64 = false;
+
+    private Zip64Mode zip64Mode = Zip64Mode.AsNeeded;
 
     /**
      * Creates a new ZIP OutputStream filtering the underlying stream.
@@ -295,6 +292,7 @@ public class ZipOutputStream extends Fil
      */
     public ZipOutputStream(OutputStream out) {
         super(out);
+        this.raf = null;
     }
 
     /**
@@ -306,21 +304,22 @@ public class ZipOutputStream extends Fil
      */
     public ZipOutputStream(File file) throws IOException {
         super(null);
-
+        RandomAccessFile _raf = null;
         try {
-            raf = new RandomAccessFile(file, "rw");
-            raf.setLength(0);
+            _raf = new RandomAccessFile(file, "rw");
+            _raf.setLength(0);
         } catch (IOException e) {
-            if (raf != null) {
+            if (_raf != null) {
                 try {
-                    raf.close();
-                } catch (IOException inner) {
+                    _raf.close();
+                } catch (IOException inner) { // NOPMD
                     // ignore
                 }
-                raf = null;
+                _raf = null;
             }
             out = new FileOutputStream(file);
         }
+        raf = _raf;
     }
 
     /**
@@ -349,7 +348,9 @@ public class ZipOutputStream extends Fil
     public void setEncoding(final String encoding) {
         this.encoding = encoding;
         this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
-        useUTF8Flag = useUTF8Flag && ZipEncodingHelper.isUTF8(encoding);
+        if (useUTF8Flag && !ZipEncodingHelper.isUTF8(encoding)) {
+            useUTF8Flag = false;
+        }
     }
 
     /**
@@ -393,23 +394,79 @@ public class ZipOutputStream extends Fil
     }
 
     /**
-     * Finishs writing the contents and closes this as well as the
-     * underlying stream.
+     * Whether Zip64 extensions will be used.
      *
-     * @since 1.1
-     * @throws IOException on error
+     * <p>When setting the mode to {@link Zip64Mode#Never Never},
+     * {@link #putNextEntry}, {@link #closeEntry}, {@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.</p>
+     *
+     * <p>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.</p>
+     *
+     * <p>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 ZipEntry} is known
+     * when calling {@link #putNextEntry} or the archive is written
+     * to a seekable output (i.e. you have used the {@link
+     * #ZipOutputStream(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
+     * #putNextEntry} is called.</p>
+     * 
+     * <p>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.</p>
+     *
+     * <p>Defaults to {@link Zip64Mode#AsNeeded AsNeeded} unless
+     * {@link #putNextEntry} 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}.</p>
+     *
+     * @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}.
      */
     public void finish() throws IOException {
-        closeEntry();
+        if (finished) {
+            throw new IOException("This archive has already been finished");
+        }
+
+        if (entry != null) {
+            closeEntry();
+        }
+
         cdOffset = written;
-        for (Iterator i = entries.iterator(); i.hasNext(); ) {
-            writeCentralFileHeader((ZipEntry) i.next());
+        for (ZipEntry ze : entries) {
+            writeCentralFileHeader(ze);
         }
         cdLength = written - cdOffset;
+        writeZip64CentralDirectory();
         writeCentralDirectoryEnd();
         offsets.clear();
         entries.clear();
         def.end();
+        finished = true;
     }
 
     /**
@@ -417,80 +474,206 @@ public class ZipOutputStream extends Fil
      *
      * @since 1.1
      * @throws IOException on error
+     * @throws Zip64RequiredException if the entry's uncompressed or
+     * compressed size exceeds 4 GByte and {@link #setUseZip64} 
+     * is {@link Zip64Mode#Never}.
      */
     public void closeEntry() throws IOException {
+        if (finished) {
+            throw new IOException("Stream has already been finished");
+        }
+
         if (entry == null) {
-            return;
+            throw new IOException("No current entry to close");
+        }
+
+        if (!entry.hasWritten) {
+            write(new byte[0], 0, 0);
         }
 
+        flushDeflater();
+
+        final Zip64Mode effectiveMode = getEffectiveZip64Mode(entry.entry);
+        long bytesWritten = written - entry.dataStart;
         long realCrc = crc.getValue();
         crc.reset();
 
-        if (entry.getMethod() == DEFLATED) {
+        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();
             }
+        }
+    }
 
-            entry.setSize(adjustToLong(def.getTotalIn()));
-            entry.setCompressedSize(adjustToLong(def.getTotalOut()));
-            entry.setCrc(realCrc);
+    /**
+     * 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();
-
-            written += entry.getCompressedSize();
         } else if (raf == null) {
-            if (entry.getCrc() != realCrc) {
+            if (entry.entry.getCrc() != crc) {
                 throw new ZipException("bad CRC checksum for entry "
-                                       + entry.getName() + ": "
-                                       + Long.toHexString(entry.getCrc())
+                                       + entry.entry.getName() + ": "
+                                       + Long.toHexString(entry.entry.getCrc())
                                        + " instead of "
-                                       + Long.toHexString(realCrc));
+                                       + Long.toHexString(crc));
             }
 
-            if (entry.getSize() != written - dataStart) {
+            if (entry.entry.getSize() != bytesWritten) {
                 throw new ZipException("bad size for entry "
-                                       + entry.getName() + ": "
-                                       + entry.getSize()
+                                       + entry.entry.getName() + ": "
+                                       + entry.entry.getSize()
                                        + " instead of "
-                                       + (written - dataStart));
+                                       + bytesWritten);
             }
         } else { /* method is STORED and we used RandomAccessFile */
-            long size = written - dataStart;
+            entry.entry.setSize(bytesWritten);
+            entry.entry.setCompressedSize(bytesWritten);
+            entry.entry.setCrc(crc);
+        }
 
-            entry.setSize(size);
-            entry.setCompressedSize(size);
-            entry.setCrc(realCrc);
+        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;
+    }
 
-        // If random access output, write the local file header containing
-        // the correct CRC and compressed/uncompressed sizes
-        if (raf != null) {
-            long save = raf.getFilePointer();
+    /**
+     * 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(localDataStart);
-            writeOut(ZipLong.getBytes(entry.getCrc()));
-            writeOut(ZipLong.getBytes(entry.getCompressedSize()));
-            writeOut(ZipLong.getBytes(entry.getSize()));
-            raf.seek(save);
+        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());
         }
 
-        writeDataDescriptor(entry);
-        entry = null;
+        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);
     }
 
     /**
-     * Begin writing next entry.
-     * @param ze the entry to write
-     * @since 1.1
-     * @throws IOException on error
+     * {@inheritDoc} 
+     * @throws Zip64RequiredException if the entry's uncompressed or
+     * compressed size is known to exceed 4 GByte and {@link #setUseZip64} 
+     * is {@link Zip64Mode#Never}.
      */
-    public void putNextEntry(ZipEntry ze) throws IOException {
-        closeEntry();
+    public void putNextEntry(ZipEntry archiveEntry) throws IOException {
+        if (finished) {
+            throw new IOException("Stream has already been finished");
+        }
+
+        if (entry != null) {
+            closeEntry();
+        }
+
+        entry = new CurrentEntry(archiveEntry);
+        entries.add(entry.entry);
 
-        entry = ze;
-        entries.add(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() != -1) {
+                // 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(ZipEntry entry) {
         if (entry.getMethod() == -1) { // not specified
             entry.setMethod(method);
         }
@@ -498,32 +681,63 @@ public class ZipOutputStream extends Fil
         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.getMethod() == STORED && raf == null) {
-            if (entry.getSize() == -1) {
+        if (entry.entry.getMethod() == STORED && raf == null) {
+            if (entry.entry.getSize() == -1) {
                 throw new ZipException("uncompressed size is required for"
                                        + " STORED method when not writing to a"
                                        + " file");
             }
-            if (entry.getCrc() == -1) {
+            if (entry.entry.getCrc() == -1) {
                 throw new ZipException("crc checksum is required for STORED"
                                        + " method when not writing to a file");
             }
-            entry.setCompressedSize(entry.getSize());
+            entry.entry.setCompressedSize(entry.entry.getSize());
         }
 
-        if (entry.getMethod() == DEFLATED && hasCompressionLevelChanged) {
-            def.setLevel(level);
-            hasCompressionLevelChanged = false;
+        if ((entry.entry.getSize() >= ZIP64_MAGIC
+             || entry.entry.getCompressedSize() >= ZIP64_MAGIC)
+            && effectiveMode == Zip64Mode.Never) {
+            throw new Zip64RequiredException(Zip64RequiredException
+                                             .getEntryTooBigMessage(entry.entry));
         }
-        writeLocalFileHeader(entry);
+    }
+
+    /**
+     * Whether to addd a Zip64 extended information extra field to the
+     * local file header.
+     *
+     * <p>Returns true if</p>
+     *
+     * <ul>
+     * <li>mode is Always</li>
+     * <li>or we already know it is going to be needed</li>
+     * <li>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</li>
+     * </ul>
+     */
+    private boolean shouldAddZip64Extra(ZipEntry entry, Zip64Mode mode) {
+        return mode == Zip64Mode.Always
+            || entry.getSize() >= ZIP64_MAGIC
+            || entry.getCompressedSize() >= ZIP64_MAGIC
+            || (entry.getSize() == -1
+                && raf != null && mode != Zip64Mode.Never);
     }
 
     /**
      * Set the file comment.
      * @param comment the comment
-     * @since 1.1
      */
     public void setComment(String comment) {
         this.comment = comment;
@@ -560,34 +774,28 @@ public class ZipOutputStream extends Fil
     }
 
     /**
+     * Whether this stream is able to write the given entry.
+     *
+     * <p>May return false if it is set up to use encryption or a
+     * compression method that hasn't been implemented yet.</p>
+     */
+    public boolean canWriteEntryData(ZipEntry ae) {
+        return ZipUtil.canHandleEntryData(ae);
+    }
+
+    /**
      * 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 {
-        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();
-                        }
-                    }
-                }
-            }
+        ZipUtil.checkRequestedFeatures(entry.entry);
+        entry.hasWritten = true;
+        if (entry.entry.getMethod() == DEFLATED) {
+            writeDeflated(b, offset, length);
         } else {
             writeOut(b, offset, length);
             written += length;
@@ -596,17 +804,29 @@ public class ZipOutputStream extends Fil
     }
 
     /**
-     * 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
+     * write implementation for DEFLATED entries.
      */
-    public void write(int b) throws IOException {
-        byte[] buff = new byte[1];
-        buff[0] = (byte) (b & BYTE_MASK);
-        write(buff, 0, 1);
+    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();
+                }
+            }
+        }
     }
 
     /**
@@ -614,17 +834,16 @@ public class ZipOutputStream extends Fil
      * associated with the stream.
      *
      * @exception  IOException  if an I/O error occurs.
-     * @since 1.14
+     * @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 {
-        finish();
-
-        if (raf != null) {
-            raf.close();
-        }
-        if (out != null) {
-            out.close();
+        if (!finished) {
+            finish();
         }
+        destroy();
     }
 
     /**
@@ -632,8 +851,8 @@ public class ZipOutputStream extends Fil
      * to be written out to the stream.
      *
      * @exception  IOException  if an I/O error occurs.
-     * @since 1.14
      */
+    @Override
     public void flush() throws IOException {
         if (out != null) {
             out.flush();
@@ -648,25 +867,33 @@ public class ZipOutputStream extends Fil
      *
      * @since 1.1
      */
-    protected static final byte[] LFH_SIG = ZipLong.getBytes(0X04034B50L);
+    protected static final byte[] LFH_SIG = ZipLong.LFH_SIG.getBytes();
     /**
      * data descriptor signature
      *
      * @since 1.1
      */
-    protected static final byte[] DD_SIG = ZipLong.getBytes(0X08074B50L);
+    protected static final byte[] DD_SIG = ZipLong.DD_SIG.getBytes();
     /**
      * central file header signature
      *
      * @since 1.1
      */
-    protected static final byte[] CFH_SIG = ZipLong.getBytes(0X02014B50L);
+    protected static final byte[] CFH_SIG = ZipLong.CFH_SIG.getBytes();
     /**
      * end of central dir signature
      *
      * @since 1.1
      */
     protected 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.
@@ -678,6 +905,7 @@ public class ZipOutputStream extends Fil
         int len = def.deflate(buf, 0, buf.length);
         if (len > 0) {
             writeOut(buf, 0, len);
+            written += len;
         }
     }
 
@@ -691,45 +919,13 @@ public class ZipOutputStream extends Fil
     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());        
+        ByteBuffer name = getName(ze);
 
         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())
-                                     );
-                }
-            }
+            addUnicodeExtraFields(ze, encodable, name);
         }
 
-        offsets.put(ze, ZipLong.getBytes(written));
+        offsets.put(ze, Long.valueOf(written));
 
         writeOut(LFH_SIG);
         written += WORD;
@@ -739,7 +935,8 @@ public class ZipOutputStream extends Fil
 
         writeVersionNeededToExtractAndGeneralPurposeBits(zipMethod,
                                                          !encodable
-                                                         && fallbackToUTF8);
+                                                         && fallbackToUTF8,
+                                                         hasZip64Extra(ze));
         written += WORD;
 
         // compression method
@@ -747,21 +944,33 @@ public class ZipOutputStream extends Fil
         written += SHORT;
 
         // last mod. time and date
-        writeOut(toDosTime(ze.getTime()));
+        writeOut(ZipUtil.toDosTime(ze.getTime()));
         written += WORD;
 
         // CRC
         // compressed length
         // uncompressed length
-        localDataStart = written;
+        entry.localDataStart = written;
         if (zipMethod == DEFLATED || raf != null) {
             writeOut(LZERO);
-            writeOut(LZERO);
-            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()));
-            writeOut(ZipLong.getBytes(ze.getSize()));
-            writeOut(ZipLong.getBytes(ze.getSize()));
+            byte[] size = ZipLong.ZIP64_MAGIC.getBytes();
+            if (!hasZip64Extra(ze)) {
+                size = ZipLong.getBytes(ze.getSize());
+            }
+            writeOut(size);
+            writeOut(size);
         }
         // CheckStyle:MagicNumber OFF
         written += 12;
@@ -784,7 +993,40 @@ public class ZipOutputStream extends Fil
         writeOut(extra);
         written += extra.length;
 
-        dataStart = written;
+        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(ZipEntry ze, boolean encodable,
+                                       ByteBuffer name)
+        throws IOException {
+        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 = 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())
+                                 );
+            }
+        }
     }
 
     /**
@@ -799,35 +1041,60 @@ public class ZipOutputStream extends Fil
             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
+        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
-     *
-     * @since 1.1
+     * @throws Zip64RequiredException if the archive's size exceeds 4
+     * GByte and {@link Zip64Mode #setUseZip64} is {@link
+     * Zip64Mode#Never}.
      */
     protected void writeCentralFileHeader(ZipEntry 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 putNextEntry or
+            // closeEntry
+            throw new Zip64RequiredException(Zip64RequiredException
+                                             .ARCHIVE_TOO_BIG_MESSAGE);
+        }
+
+        handleZip64Extra(ze, lfhOffset, needsZip64Extra);
+
         // version made by
         // CheckStyle:MagicNumber OFF
-        writeOut(ZipShort.getBytes((ze.getPlatform() << 8) | 20));
+        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);
+                                                         && fallbackToUTF8,
+                                                         needsZip64Extra);
         written += WORD;
 
         // compression method
@@ -835,29 +1102,26 @@ public class ZipOutputStream extends Fil
         written += SHORT;
 
         // last mod. time and date
-        writeOut(toDosTime(ze.getTime()));
+        writeOut(ZipUtil.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()));
+        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
 
-        // file name length
-        final ZipEncoding entryEncoding;
-        
-        if (!encodable && fallbackToUTF8) {
-            entryEncoding = ZipEncodingHelper.UTF8_ZIP_ENCODING;
-        } else {
-            entryEncoding = zipEncoding;
-        }
-        
-        ByteBuffer name = entryEncoding.encode(ze.getName());        
+        ByteBuffer name = getName(ze);
 
         writeOut(ZipShort.getBytes(name.limit()));
         written += SHORT;
@@ -872,9 +1136,9 @@ public class ZipOutputStream extends Fil
         if (comm == null) {
             comm = "";
         }
-        
-        ByteBuffer commentB = entryEncoding.encode(comm);
-        
+
+        ByteBuffer commentB = getEntryEncoding(ze).encode(comm);
+
         writeOut(ZipShort.getBytes(commentB.limit()));
         written += SHORT;
 
@@ -891,7 +1155,7 @@ public class ZipOutputStream extends Fil
         written += WORD;
 
         // relative offset of LFH
-        writeOut((byte[]) offsets.get(ze));
+        writeOut(ZipLong.getBytes(Math.min(lfhOffset, ZIP64_MAGIC)));
         written += WORD;
 
         // file name
@@ -908,10 +1172,35 @@ public class ZipOutputStream extends Fil
     }
 
     /**
+     * If the entry needs Zip64 extra information inside the central
+     * directory then configure its data.
+     */
+    private void handleZip64Extra(ZipEntry 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 &quot;End of central dir record&quot;.
      * @throws IOException on error
-     *
-     * @since 1.1
+     * @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);
@@ -921,13 +1210,25 @@ public class ZipOutputStream extends Fil
         writeOut(ZERO);
 
         // number of entries
-        byte[] num = ZipShort.getBytes(entries.size());
+        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(cdLength));
-        writeOut(ZipLong.getBytes(cdOffset));
+        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);
@@ -936,20 +1237,14 @@ public class ZipOutputStream extends Fil
     }
 
     /**
-     * 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
+     * @deprecated use ZipUtil#toDosTime
      */
     protected static ZipLong toDosTime(Date time) {
-        return new ZipLong(toDosTime(time.getTime()));
+        return ZipUtil.toDosTime(time);
     }
 
     /**
@@ -959,24 +1254,10 @@ public class ZipOutputStream extends Fil
      * @param t number of milliseconds since the epoch
      * @return the date as a byte array
      * @since 1.26
+     * @deprecated use ZipUtil#toDosTime
      */
     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
+        return ZipUtil.toDosTime(t);
     }
 
     /**
@@ -1001,6 +1282,76 @@ public class ZipOutputStream extends Fil
         }
     }
 
+    private static final byte[] ONE = ZipLong.getBytes(1L);
+
+    /**
+     * Writes the &quot;ZIP64 End of central dir record&quot; and
+     * &quot;ZIP64 End of central dir locator&quot;.
+     * @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
@@ -1036,13 +1387,10 @@ public class ZipOutputStream extends Fil
      * @param i the value to treat as unsigned int.
      * @return the unsigned int as a long.
      * @since 1.34
+     * @deprecated use ZipUtil#adjustToLong
      */
     protected static long adjustToLong(int i) {
-        if (i < 0) {
-            return 2 * ((long) Integer.MAX_VALUE) + 2 + i;
-        } else {
-            return i;
-        }
+        return ZipUtil.adjustToLong(i);
     }
 
     private void deflateUntilInputIsNeeded() throws IOException {
@@ -1054,25 +1402,116 @@ public class ZipOutputStream extends Fil
     private void writeVersionNeededToExtractAndGeneralPurposeBits(final int
                                                                   zipMethod,
                                                                   final boolean
-                                                                  utfFallback)
+                                                                  utfFallback,
+                                                                  final boolean
+                                                                  zip64)
         throws IOException {
 
         // CheckStyle:MagicNumber OFF
-        int versionNeededToExtract = 10;
-        int generalPurposeFlag = (useUTF8Flag || utfFallback) ? UFT8_NAMES_FLAG : 0;
+        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 =  20;
-            // bit3 set to signal, we use a data descriptor
-            generalPurposeFlag |= 8;
+            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(ZipShort.getBytes(generalPurposeFlag));
+        writeOut(b.encode());
+    }
+
+    /**
+     * 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(ZipEntry 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(ZipEntry 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(ZipEntry ze) {
+        if (zip64Mode != Zip64Mode.AsNeeded
+            || raf != null
+            || ze.getMethod() != DEFLATED
+            || ze.getSize() != -1) {
+            return zip64Mode;
+        }
+        return Zip64Mode.Never;
+    }
+
+    private ZipEncoding getEntryEncoding(ZipEntry ze) {
+        boolean encodable = zipEncoding.canEncode(ze.getName());
+        return !encodable && fallbackToUTF8
+            ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding;
+    }
+
+    private ByteBuffer getName(ZipEntry 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.
+     *
+     * <p>This method only exists to support tests that generate
+     * corrupt archives so they can clean up any temporary files.</p>
+     */
+    void destroy() throws IOException {
+        if (raf != null) {
+            raf.close();
+        }
+        if (out != null) {
+            out.close();
+        }
     }
 
     /**
@@ -1101,8 +1540,51 @@ public class ZipOutputStream extends Fil
         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(ZipEntry entry) {
+            this.entry = entry;
+        }
+        /**
+         * Current ZIP entry.
+         */
+        private final ZipEntry 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.
+         *
+         * <p>In order to create a valid archive {@link
+         * #closeEntry closeEntry} will write an empty
+         * array to get the CRC right if nothing has been written to
+         * the stream at all.</p>
+         */
+        private boolean hasWritten;
+    }
+
 }

Modified: ant/core/trunk/src/main/org/apache/tools/zip/ZipShort.java
URL: http://svn.apache.org/viewvc/ant/core/trunk/src/main/org/apache/tools/zip/ZipShort.java?rev=1346025&r1=1346024&r2=1346025&view=diff
==============================================================================
--- ant/core/trunk/src/main/org/apache/tools/zip/ZipShort.java (original)
+++ ant/core/trunk/src/main/org/apache/tools/zip/ZipShort.java Mon Jun  4 16:35:06 2012
@@ -18,17 +18,18 @@
 
 package org.apache.tools.zip;
 
+import static org.apache.tools.zip.ZipConstants.BYTE_MASK;
+
 /**
  * Utility class that represents a two byte integer with conversion
  * rules for the big endian byte order of ZIP files.
  *
  */
 public final class ZipShort implements Cloneable {
-    private static final int BYTE_MASK = 0xFF;
     private static final int BYTE_1_MASK = 0xFF00;
     private static final int BYTE_1_SHIFT = 8;
 
-    private int value;
+    private final int value;
 
     /**
      * Create instance from a number.
@@ -95,7 +96,7 @@ public final class ZipShort implements C
      * Helper method to get the value as a java int from two bytes starting at given array offset
      * @param bytes the array of bytes
      * @param offset the offset to start
-     * @return the correspondanding java int value
+     * @return the corresponding java int value
      */
     public static int getValue(byte[] bytes, int offset) {
         int value = (bytes[offset + 1] << BYTE_1_SHIFT) & BYTE_1_MASK;
@@ -106,7 +107,7 @@ public final class ZipShort implements C
     /**
      * Helper method to get the value as a java int from a two-byte array
      * @param bytes the array of bytes
-     * @return the correspondanding java int value
+     * @return the corresponding java int value
      */
     public static int getValue(byte[] bytes) {
         return getValue(bytes, 0);
@@ -118,6 +119,7 @@ public final class ZipShort implements C
      * @return true if the objects are equal
      * @since 1.1
      */
+    @Override
     public boolean equals(Object o) {
         if (o == null || !(o instanceof ZipShort)) {
             return false;
@@ -130,10 +132,12 @@ public final class ZipShort implements C
      * @return the value stored in the ZipShort
      * @since 1.1
      */
+    @Override
     public int hashCode() {
         return value;
     }
 
+    @Override
     public Object clone() {
         try {
             return super.clone();
@@ -142,4 +146,9 @@ public final class ZipShort implements C
             throw new RuntimeException(cnfe);
         }
     }
+
+    @Override
+    public String toString() {
+        return "ZipShort value: " + value;
+    }
 }

Modified: ant/core/trunk/src/main/org/apache/tools/zip/ZipUtil.java
URL: http://svn.apache.org/viewvc/ant/core/trunk/src/main/org/apache/tools/zip/ZipUtil.java?rev=1346025&r1=1346024&r2=1346025&view=diff
==============================================================================
--- ant/core/trunk/src/main/org/apache/tools/zip/ZipUtil.java (original)
+++ ant/core/trunk/src/main/org/apache/tools/zip/ZipUtil.java Mon Jun  4 16:35:06 2012
@@ -17,12 +17,160 @@
  */
 package org.apache.tools.zip;
 
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.zip.CRC32;
+
 /**
  * Utility class for handling DOS and Java time conversions.
  * @since Ant 1.8.1
  */
 public abstract class ZipUtil {
     /**
+     * Smallest date/time ZIP can handle.
+     */
+    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>
+     */
+    public 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
+     */
+    public static byte[] toDosTime(long t) {
+        Calendar c = Calendar.getInstance();
+        c.setTimeInMillis(t);
+
+        int year = c.get(Calendar.YEAR);
+        if (year < 1980) {
+            return copy(DOS_TIME_MIN); // stop callers from changing the array
+        }
+        int month = c.get(Calendar.MONTH) + 1;
+        long value =  ((year - 1980) << 25)
+            |         (month << 21)
+            |         (c.get(Calendar.DAY_OF_MONTH) << 16)
+            |         (c.get(Calendar.HOUR_OF_DAY) << 11)
+            |         (c.get(Calendar.MINUTE) << 5)
+            |         (c.get(Calendar.SECOND) >> 1);
+        return ZipLong.getBytes(value);
+    }
+
+    /**
+     * Assumes a negative integer really is a positive integer that
+     * has wrapped around and re-creates the original value.
+     *
+     * <p>This methods is no longer used as of Apache Ant 1.9.0</p>
+     *
+     * @param i the value to treat as unsigned int.
+     * @return the unsigned int as a long.
+     */
+    public static long adjustToLong(int i) {
+        if (i < 0) {
+            return 2 * ((long) Integer.MAX_VALUE) + 2 + i;
+        } else {
+            return i;
+        }
+    }
+
+    /**
+     * Convert a DOS date/time field to a Date object.
+     *
+     * @param zipDosTime contains the stored DOS time.
+     * @return a Date instance corresponding to the given time.
+     */
+    public static Date fromDosTime(ZipLong zipDosTime) {
+        long dosTime = zipDosTime.getValue();
+        return new Date(dosToJavaTime(dosTime));
+    }
+
+    /**
+     * Converts DOS time to Java time (number of milliseconds since
+     * epoch).
+     */
+    public static long dosToJavaTime(long dosTime) {
+        Calendar cal = Calendar.getInstance();
+        // CheckStyle:MagicNumberCheck OFF - no point
+        cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980);
+        cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1);
+        cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f);
+        cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f);
+        cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f);
+        cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e);
+        // CheckStyle:MagicNumberCheck ON
+        return cal.getTime().getTime();
+    }
+
+    /**
+     * If the entry has Unicode*ExtraFields and the CRCs of the
+     * names/comments match those of the extra fields, transfer the
+     * known Unicode values from the extra field.
+     */
+    static void setNameAndCommentFromExtraFields(ZipEntry ze,
+                                                 byte[] originalNameBytes,
+                                                 byte[] commentBytes) {
+        UnicodePathExtraField name = (UnicodePathExtraField)
+            ze.getExtraField(UnicodePathExtraField.UPATH_ID);
+        String originalName = ze.getName();
+        String newName = getUnicodeStringIfOriginalMatches(name,
+                                                           originalNameBytes);
+        if (newName != null && !originalName.equals(newName)) {
+            ze.setName(newName);
+        }
+
+        if (commentBytes != null && commentBytes.length > 0) {
+            UnicodeCommentExtraField cmt = (UnicodeCommentExtraField)
+                ze.getExtraField(UnicodeCommentExtraField.UCOM_ID);
+            String newComment =
+                getUnicodeStringIfOriginalMatches(cmt, commentBytes);
+            if (newComment != null) {
+                ze.setComment(newComment);
+            }
+        }
+    }
+
+    /**
+     * If the stored CRC matches the one of the given name, return the
+     * Unicode name of the given field.
+     *
+     * <p>If the field is null or the CRCs don't match, return null
+     * instead.</p>
+     */
+    private static 
+        String getUnicodeStringIfOriginalMatches(AbstractUnicodeExtraField f,
+                                                 byte[] orig) {
+        if (f != null) {
+            CRC32 crc32 = new CRC32();
+            crc32.update(orig);
+            long origCRC32 = crc32.getValue();
+
+            if (origCRC32 == f.getNameCRC32()) {
+                try {
+                    return ZipEncodingHelper
+                        .UTF8_ZIP_ENCODING.decode(f.getUnicodeName());
+                } catch (IOException ex) {
+                    // UTF-8 unsupported?  should be impossible the
+                    // Unicode*ExtraField must contain some bad bytes
+
+                    // TODO log this anywhere?
+                    return null;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
      * Create a copy of the given array - or return null if the
      * argument is null.
      */
@@ -35,4 +183,49 @@ public abstract class ZipUtil {
         return null;
     }
 
+    /**
+     * Whether this library is able to read or write the given entry.
+     */
+    static boolean canHandleEntryData(ZipEntry entry) {
+        return supportsEncryptionOf(entry) && supportsMethodOf(entry);
+    }
+
+    /**
+     * Whether this library supports the encryption used by the given
+     * entry.
+     *
+     * @return true if the entry isn't encrypted at all
+     */
+    private static boolean supportsEncryptionOf(ZipEntry entry) {
+        return !entry.getGeneralPurposeBit().usesEncryption();
+    }
+
+    /**
+     * Whether this library supports the compression method used by
+     * the given entry.
+     *
+     * @return true if the compression method is STORED or DEFLATED
+     */
+    private static boolean supportsMethodOf(ZipEntry entry) {
+        return entry.getMethod() == ZipEntry.STORED
+            || entry.getMethod() == ZipEntry.DEFLATED;
+    }
+
+    /**
+     * Checks whether the entry requires features not (yet) supported
+     * by the library and throws an exception if it does.
+     */
+    static void checkRequestedFeatures(ZipEntry ze)
+        throws UnsupportedZipFeatureException {
+        if (!supportsEncryptionOf(ze)) {
+            throw
+                new UnsupportedZipFeatureException(UnsupportedZipFeatureException
+                                                   .Feature.ENCRYPTION, ze);
+        }
+        if (!supportsMethodOf(ze)) {
+            throw
+                new UnsupportedZipFeatureException(UnsupportedZipFeatureException
+                                                   .Feature.METHOD, ze);
+        }
+    }
 }
\ No newline at end of file

Modified: ant/core/trunk/src/tests/junit/org/apache/tools/ant/taskdefs/ZipExtraFieldTest.java
URL: http://svn.apache.org/viewvc/ant/core/trunk/src/tests/junit/org/apache/tools/ant/taskdefs/ZipExtraFieldTest.java?rev=1346025&r1=1346024&r2=1346025&view=diff
==============================================================================
--- ant/core/trunk/src/tests/junit/org/apache/tools/ant/taskdefs/ZipExtraFieldTest.java (original)
+++ ant/core/trunk/src/tests/junit/org/apache/tools/ant/taskdefs/ZipExtraFieldTest.java Mon Jun  4 16:35:06 2012
@@ -32,6 +32,7 @@ import org.apache.tools.ant.types.Resour
 import org.apache.tools.ant.types.ResourceCollection;
 import org.apache.tools.ant.types.resources.ZipResource;
 import org.apache.tools.zip.JarMarker;
+import org.apache.tools.zip.Zip64ExtendedInformationExtraField;
 import org.apache.tools.zip.ZipEntry;
 import org.apache.tools.zip.ZipExtraField;
 import org.apache.tools.zip.ZipFile;
@@ -79,8 +80,10 @@ public class ZipExtraFieldTest extends T
             zf = new ZipFile(f);
             ZipEntry ze = zf.getEntry("x");
             assertNotNull(ze);
-            assertEquals(1, ze.getExtraFields().length);
+            assertEquals(2, ze.getExtraFields().length);
             assertTrue(ze.getExtraFields()[0] instanceof JarMarker);
+            assertTrue(ze.getExtraFields()[1]
+                       instanceof Zip64ExtendedInformationExtraField);
         } finally {
             ZipFile.closeQuietly(zf);
             if (f.exists()) {

Modified: ant/core/trunk/src/tests/junit/org/apache/tools/zip/ExtraFieldUtilsTest.java
URL: http://svn.apache.org/viewvc/ant/core/trunk/src/tests/junit/org/apache/tools/zip/ExtraFieldUtilsTest.java?rev=1346025&r1=1346024&r2=1346025&view=diff
==============================================================================
--- ant/core/trunk/src/tests/junit/org/apache/tools/zip/ExtraFieldUtilsTest.java (original)
+++ ant/core/trunk/src/tests/junit/org/apache/tools/zip/ExtraFieldUtilsTest.java Mon Jun  4 16:35:06 2012
@@ -29,17 +29,26 @@ public class ExtraFieldUtilsTest extends
         super(name);
     }
 
+    /**
+     * Header-ID of a ZipExtraField not supported by Ant.
+     *
+     * <p>Used to be ZipShort(1) but this is the ID of the Zip64 extra
+     * field.</p>
+     */
+    static final ZipShort UNRECOGNIZED_HEADER = new ZipShort(0x5555);
+
     private AsiExtraField a;
     private UnrecognizedExtraField dummy;
     private byte[] data;
     private byte[] aLocal;
 
+    @Override
     public void setUp() {
         a = new AsiExtraField();
         a.setMode(0755);
         a.setDirectory(true);
         dummy = new UnrecognizedExtraField();
-        dummy.setHeaderId(new ZipShort(1));
+        dummy.setHeaderId(UNRECOGNIZED_HEADER);
         dummy.setLocalFileDataData(new byte[] {0});
         dummy.setCentralDirectoryData(new byte[] {0});
 
@@ -167,7 +176,8 @@ public class ExtraFieldUtilsTest extends
 
     public void testMergeWithUnparseableData() throws Exception {
         ZipExtraField d = new UnparseableExtraFieldData();
-        d.parseFromLocalFileData(new byte[] {1, 0, 1, 0}, 0, 4);
+        byte[] b = UNRECOGNIZED_HEADER.getBytes();
+        d.parseFromLocalFileData(new byte[] {b[0], b[1], 1, 0}, 0, 4);
         byte[] local =
             ExtraFieldUtils.mergeLocalFileDataData(new ZipExtraField[] {a, d});
         assertEquals("local length", data.length - 1, local.length);

Modified: ant/core/trunk/src/tests/junit/org/apache/tools/zip/ZipEntryTest.java
URL: http://svn.apache.org/viewvc/ant/core/trunk/src/tests/junit/org/apache/tools/zip/ZipEntryTest.java?rev=1346025&r1=1346024&r2=1346025&view=diff
==============================================================================
--- ant/core/trunk/src/tests/junit/org/apache/tools/zip/ZipEntryTest.java (original)
+++ ant/core/trunk/src/tests/junit/org/apache/tools/zip/ZipEntryTest.java Mon Jun  4 16:35:06 2012
@@ -38,7 +38,7 @@ public class ZipEntryTest extends TestCa
         a.setDirectory(true);
         a.setMode(0755);
         UnrecognizedExtraField u = new UnrecognizedExtraField();
-        u.setHeaderId(new ZipShort(1));
+        u.setHeaderId(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER);
         u.setLocalFileDataData(new byte[0]);
 
         ZipEntry ze = new ZipEntry("test/");
@@ -50,7 +50,7 @@ public class ZipEntryTest extends TestCa
         assertSame(u, result[1]);
 
         UnrecognizedExtraField u2 = new UnrecognizedExtraField();
-        u2.setHeaderId(new ZipShort(1));
+        u2.setHeaderId(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER);
         u2.setLocalFileDataData(new byte[] {1});
 
         ze.addExtraField(u2);
@@ -68,7 +68,7 @@ public class ZipEntryTest extends TestCa
         result = ze.getExtraFields();
         assertEquals("third pass", 3, result.length);
 
-        ze.removeExtraField(new ZipShort(1));
+        ze.removeExtraField(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER);
         byte[] data3 = ze.getExtra();
         result = ze.getExtraFields();
         assertEquals("fourth pass", 2, result.length);
@@ -77,7 +77,7 @@ public class ZipEntryTest extends TestCa
         assertEquals("length fourth pass", data2.length, data3.length);
 
         try {
-            ze.removeExtraField(new ZipShort(1));
+            ze.removeExtraField(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER);
             fail("should be no such element");
         } catch (java.util.NoSuchElementException nse) {
         }
@@ -91,7 +91,7 @@ public class ZipEntryTest extends TestCa
         a.setDirectory(true);
         a.setMode(0755);
         UnrecognizedExtraField u = new UnrecognizedExtraField();
-        u.setHeaderId(new ZipShort(1));
+        u.setHeaderId(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER);
         u.setLocalFileDataData(new byte[0]);
 
         ZipEntry ze = new ZipEntry("test/");
@@ -99,12 +99,14 @@ public class ZipEntryTest extends TestCa
 
         // merge
         // Header-ID 1 + length 1 + one byte of data
-        ze.setCentralDirectoryExtra(new byte[] {1, 0, 1, 0, 127});
+        byte[] b = ExtraFieldUtilsTest.UNRECOGNIZED_HEADER.getBytes();
+        ze.setCentralDirectoryExtra(new byte[] {b[0], b[1], 1, 0, 127});
 
         ZipExtraField[] result = ze.getExtraFields();
         assertEquals("first pass", 2, result.length);
         assertSame(a, result[0]);
-        assertEquals(new ZipShort(1), result[1].getHeaderId());
+        assertEquals(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER,
+                     result[1].getHeaderId());
         assertEquals(new ZipShort(0), result[1].getLocalFileDataLength());
         assertEquals(new ZipShort(1), result[1].getCentralDirectoryLength());
 
@@ -135,7 +137,7 @@ public class ZipEntryTest extends TestCa
         a.setDirectory(true);
         a.setMode(0755);
         UnrecognizedExtraField u = new UnrecognizedExtraField();
-        u.setHeaderId(new ZipShort(1));
+        u.setHeaderId(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER);
         u.setLocalFileDataData(new byte[0]);
 
         ZipEntry ze = new ZipEntry("test/");
@@ -143,7 +145,7 @@ public class ZipEntryTest extends TestCa
         byte[] data1 = ze.getExtra();
 
         UnrecognizedExtraField u2 = new UnrecognizedExtraField();
-        u2.setHeaderId(new ZipShort(1));
+        u2.setHeaderId(ExtraFieldUtilsTest.UNRECOGNIZED_HEADER);
         u2.setLocalFileDataData(new byte[] {1});
 
         ze.addAsFirstExtraField(u2);



Mime
View raw message