OSDN Git Service

リポジトリ内改行コードのLFへの修正
[charactermanaj/CharacterManaJ.git] / src / main / java / org / apache / tools / zip / ZipFile.java
index 2cca68d..e578de9 100644 (file)
-/*\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.IOException;\r
-import java.io.InputStream;\r
-import java.io.RandomAccessFile;\r
-import java.util.Calendar;\r
-import java.util.Collections;\r
-import java.util.Date;\r
-import java.util.Enumeration;\r
-import java.util.HashMap;\r
-import java.util.Map;\r
-import java.util.logging.Level;\r
-import java.util.logging.Logger;\r
-import java.util.zip.CRC32;\r
-import java.util.zip.Inflater;\r
-import java.util.zip.InflaterInputStream;\r
-import java.util.zip.ZipException;\r
-\r
-/**\r
- * Replacement for <code>java.util.ZipFile</code>.\r
- *\r
- * <p>This class adds support for file name encodings other than UTF-8\r
- * (which is required to work on ZIP files created by native zip tools\r
- * and is able to skip a preamble like the one found in self\r
- * extracting archives.  Furthermore it returns instances of\r
- * <code>org.apache.tools.zip.ZipEntry</code> instead of\r
- * <code>java.util.zip.ZipEntry</code>.</p>\r
- *\r
- * <p>It doesn't extend <code>java.util.zip.ZipFile</code> as it would\r
- * have to reimplement all methods anyway.  Like\r
- * <code>java.util.ZipFile</code>, it uses RandomAccessFile under the\r
- * covers and supports compressed and uncompressed entries.</p>\r
- *\r
- * <p>The method signatures mimic the ones of\r
- * <code>java.util.zip.ZipFile</code>, with a couple of exceptions:\r
- *\r
- * <ul>\r
- *   <li>There is no getName method.</li>\r
- *   <li>entries has been renamed to getEntries.</li>\r
- *   <li>getEntries and getEntry return\r
- *   <code>org.apache.tools.zip.ZipEntry</code> instances.</li>\r
- *   <li>close is allowed to throw IOException.</li>\r
- * </ul>\r
- *\r
- */\r
-@SuppressWarnings({"unchecked", "rawtypes"})\r
-public class ZipFile {\r
-       \r
-       private static final Logger logger = Logger.getLogger(ZipFile.class.getName());\r
-\r
-    private static final int HASH_SIZE = 509;\r
-    private static final int SHORT     =   2;\r
-    private static final int WORD      =   4;\r
-    private static final int NIBLET_MASK = 0x0f;\r
-    private static final int BYTE_SHIFT = 8;\r
-    private static final int POS_0 = 0;\r
-    private static final int POS_1 = 1;\r
-    private static final int POS_2 = 2;\r
-    private static final int POS_3 = 3;\r
-\r
-    /**\r
-     * Maps ZipEntrys to Longs, recording the offsets of the local\r
-     * file headers.\r
-     */\r
-    private final Map entries = new HashMap(HASH_SIZE);\r
-\r
-    /**\r
-     * Maps String to ZipEntrys, name -> actual entry.\r
-     */\r
-    private final Map nameMap = new HashMap(HASH_SIZE);\r
-\r
-    private static final class OffsetEntry {\r
-        private long headerOffset = -1;\r
-        private long dataOffset = -1;\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
-     */\r
-    private String encoding = null;\r
-\r
-    /**\r
-     * The zip encoding to use for filenames and the file comment.\r
-     */\r
-    private final ZipEncoding zipEncoding;\r
-\r
-    /**\r
-     * The actual data source.\r
-     */\r
-    private RandomAccessFile archive;\r
-\r
-    /**\r
-     * Whether to look for and use Unicode extra fields.\r
-     */\r
-    private final boolean useUnicodeExtraFields;\r
-\r
-    /**\r
-     * Opens the given file for reading, assuming the platform's\r
-     * native encoding for file names.\r
-     *\r
-     * @param f the archive.\r
-     *\r
-     * @throws IOException if an error occurs while reading the file.\r
-     */\r
-    public ZipFile(File f) throws IOException {\r
-        this(f, null);\r
-    }\r
-\r
-    /**\r
-     * Opens the given file for reading, assuming the platform's\r
-     * native encoding for file names.\r
-     *\r
-     * @param name name of the archive.\r
-     *\r
-     * @throws IOException if an error occurs while reading the file.\r
-     */\r
-    public ZipFile(String name) throws IOException {\r
-        this(new File(name), null);\r
-    }\r
-\r
-    /**\r
-     * Opens the given file for reading, assuming the specified\r
-     * encoding for file names, scanning unicode extra fields.\r
-     *\r
-     * @param name name of the archive.\r
-     * @param encoding the encoding to use for file names\r
-     *\r
-     * @throws IOException if an error occurs while reading the file.\r
-     */\r
-    public ZipFile(String name, String encoding) throws IOException {\r
-        this(new File(name), encoding, true);\r
-    }\r
-\r
-    /**\r
-     * Opens the given file for reading, assuming the specified\r
-     * encoding for file names and scanning for unicode extra fields.\r
-     *\r
-     * @param f the archive.\r
-     * @param encoding the encoding to use for file names, use null\r
-     * for the platform's default encoding\r
-     *\r
-     * @throws IOException if an error occurs while reading the file.\r
-     */\r
-    public ZipFile(File f, String encoding) throws IOException {\r
-        this(f, encoding, true);\r
-    }\r
-\r
-    /**\r
-     * Opens the given file for reading, assuming the specified\r
-     * encoding for file names.\r
-     *\r
-     * @param f the archive.\r
-     * @param encoding the encoding to use for file names, use null\r
-     * for the platform's default encoding\r
-     * @param useUnicodeExtraFields whether to use InfoZIP Unicode\r
-     * Extra Fields (if present) to set the file names.\r
-     *\r
-     * @throws IOException if an error occurs while reading the file.\r
-     */\r
-    public ZipFile(File f, String encoding, boolean useUnicodeExtraFields)\r
-        throws IOException {\r
-        this.encoding = encoding;\r
-        this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);\r
-        this.useUnicodeExtraFields = useUnicodeExtraFields;\r
-        archive = new RandomAccessFile(f, "r");\r
-        boolean success = false;\r
-        try {\r
-            Map entriesWithoutUTF8Flag = populateFromCentralDirectory();\r
-            resolveLocalFileHeaderData(entriesWithoutUTF8Flag);\r
-            success = true;\r
-        } finally {\r
-            if (!success) {\r
-                try {\r
-                    archive.close();\r
-                } catch (IOException e2) {\r
-                    // swallow, throw the original exception instead\r
-                }\r
-            }\r
-        }\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
-    public String getEncoding() {\r
-        return encoding;\r
-    }\r
-\r
-    /**\r
-     * Closes the archive.\r
-     * @throws IOException if an error occurs closing the archive.\r
-     */\r
-    public void close() throws IOException {\r
-        archive.close();\r
-    }\r
-\r
-    /**\r
-     * close a zipfile quietly; throw no io fault, do nothing\r
-     * on a null parameter\r
-     * @param zipfile file to close, can be null\r
-     */\r
-    public static void closeQuietly(ZipFile zipfile) {\r
-        if (zipfile != null) {\r
-            try {\r
-                zipfile.close();\r
-            } catch (IOException e) {\r
-                //ignore\r
-            }\r
-        }\r
-    }\r
-\r
-    /**\r
-     * Returns all entries.\r
-     * @return all entries as {@link ZipEntry} instances\r
-     */\r
-    public Enumeration getEntries() {\r
-        return Collections.enumeration(entries.keySet());\r
-    }\r
-\r
-    /**\r
-     * Returns a named entry - or <code>null</code> if no entry by\r
-     * that name exists.\r
-     * @param name name of the entry.\r
-     * @return the ZipEntry corresponding to the given name - or\r
-     * <code>null</code> if not present.\r
-     */\r
-    public ZipEntry getEntry(String name) {\r
-        return (ZipEntry) nameMap.get(name);\r
-    }\r
-\r
-    /**\r
-     * Returns an InputStream for reading the contents of the given entry.\r
-     * @param ze the entry to get the stream for.\r
-     * @return a stream to read the entry from.\r
-     * @throws IOException if unable to create an input stream from the zipenty\r
-     * @throws ZipException if the zipentry has an unsupported\r
-     * compression method\r
-     */\r
-    public InputStream getInputStream(ZipEntry ze)\r
-        throws IOException, ZipException {\r
-        OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze);\r
-        if (offsetEntry == null) {\r
-            return null;\r
-        }\r
-        long start = offsetEntry.dataOffset;\r
-        BoundedInputStream bis =\r
-            new BoundedInputStream(start, ze.getCompressedSize());\r
-        switch (ze.getMethod()) {\r
-            case ZipEntry.STORED:\r
-                return bis;\r
-            case ZipEntry.DEFLATED:\r
-                bis.addDummy();\r
-                return new InflaterInputStream(bis, new Inflater(true));\r
-            default:\r
-                throw new ZipException("Found unsupported compression method "\r
-                                       + ze.getMethod());\r
-        }\r
-    }\r
-\r
-    private static final int CFH_LEN =\r
-        /* version made by                 */ SHORT\r
-        /* version needed to extract       */ + SHORT\r
-        /* general purpose bit flag        */ + SHORT\r
-        /* compression method              */ + SHORT\r
-        /* last mod file time              */ + SHORT\r
-        /* last mod file date              */ + SHORT\r
-        /* crc-32                          */ + WORD\r
-        /* compressed size                 */ + WORD\r
-        /* uncompressed size               */ + WORD\r
-        /* filename length                 */ + SHORT\r
-        /* extra field length              */ + SHORT\r
-        /* file comment length             */ + SHORT\r
-        /* disk number start               */ + SHORT\r
-        /* internal file attributes        */ + SHORT\r
-        /* external file attributes        */ + WORD\r
-        /* relative offset of local header */ + WORD;\r
-\r
-    /**\r
-     * Reads the central directory of the given archive and populates\r
-     * the internal tables with ZipEntry instances.\r
-     *\r
-     * <p>The ZipEntrys will know all data that can be obtained from\r
-     * the central directory alone, but not the data that requires the\r
-     * local file header or additional data to be read.</p>\r
-     *\r
-     * @return a Map&lt;ZipEntry, NameAndComment>&gt; of\r
-     * zipentries that didn't have the language encoding flag set when\r
-     * read.\r
-     */\r
-    private Map populateFromCentralDirectory()\r
-        throws IOException {\r
-        HashMap noUTF8Flag = new HashMap();\r
-\r
-        positionAtCentralDirectory();\r
-\r
-        byte[] cfh = new byte[CFH_LEN];\r
-\r
-        byte[] signatureBytes = new byte[WORD];\r
-        archive.readFully(signatureBytes);\r
-        long sig = ZipLong.getValue(signatureBytes);\r
-        final long cfhSig = ZipLong.getValue(ZipOutputStream.CFH_SIG);\r
-        if (sig != cfhSig && startsWithLocalFileHeader()) {\r
-            throw new IOException("central directory is empty, can't expand"\r
-                                  + " corrupt archive.");\r
-        }\r
-        while (sig == cfhSig) {\r
-            archive.readFully(cfh);\r
-            int off = 0;\r
-            ZipEntry ze = new ZipEntry();\r
-\r
-            int versionMadeBy = ZipShort.getValue(cfh, off);\r
-            off += SHORT;\r
-            ze.setPlatform((versionMadeBy >> BYTE_SHIFT) & NIBLET_MASK);\r
-\r
-            off += SHORT; // skip version info\r
-\r
-            final int generalPurposeFlag = ZipShort.getValue(cfh, off);\r
-            final boolean hasUTF8Flag = \r
-                (generalPurposeFlag & ZipOutputStream.UFT8_NAMES_FLAG) != 0;\r
-            final ZipEncoding entryEncoding =\r
-                hasUTF8Flag ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding;\r
-\r
-            off += SHORT;\r
-\r
-            ze.setMethod(ZipShort.getValue(cfh, off));\r
-            off += SHORT;\r
-\r
-            // FIXME this is actually not very cpu cycles friendly as we are converting from\r
-            // dos to java while the underlying Sun implementation will convert\r
-            // from java to dos time for internal storage...\r
-            long time = dosToJavaTime(ZipLong.getValue(cfh, off));\r
-            ze.setTime(time);\r
-            off += WORD;\r
-\r
-            ze.setCrc(ZipLong.getValue(cfh, off));\r
-            off += WORD;\r
-\r
-            ze.setCompressedSize(ZipLong.getValue(cfh, off));\r
-            off += WORD;\r
-\r
-            ze.setSize(ZipLong.getValue(cfh, off));\r
-            off += WORD;\r
-\r
-            int fileNameLen = ZipShort.getValue(cfh, off);\r
-            off += SHORT;\r
-\r
-            int extraLen = ZipShort.getValue(cfh, off);\r
-            off += SHORT;\r
-\r
-            int commentLen = ZipShort.getValue(cfh, off);\r
-            off += SHORT;\r
-\r
-            off += SHORT; // disk number\r
-\r
-            ze.setInternalAttributes(ZipShort.getValue(cfh, off));\r
-            off += SHORT;\r
-\r
-            ze.setExternalAttributes(ZipLong.getValue(cfh, off));\r
-            off += WORD;\r
-\r
-            byte[] fileName = new byte[fileNameLen];\r
-            archive.readFully(fileName);\r
-            ze.setName(entryEncoding.decode(fileName));\r
-\r
-            // LFH offset,\r
-            OffsetEntry offset = new OffsetEntry();\r
-            offset.headerOffset = ZipLong.getValue(cfh, off);\r
-            // data offset will be filled later\r
-            entries.put(ze, offset);\r
-\r
-            nameMap.put(ze.getName(), ze);\r
-\r
-            byte[] cdExtraData = new byte[extraLen];\r
-            archive.readFully(cdExtraData);\r
-            ze.setCentralDirectoryExtra(cdExtraData);\r
-\r
-            byte[] comment = new byte[commentLen];\r
-            archive.readFully(comment);\r
-            ze.setComment(entryEncoding.decode(comment));\r
-\r
-            archive.readFully(signatureBytes);\r
-            sig = ZipLong.getValue(signatureBytes);\r
-\r
-            if (!hasUTF8Flag && useUnicodeExtraFields) {\r
-                noUTF8Flag.put(ze, new NameAndComment(fileName, comment));\r
-            }\r
-        }\r
-        return noUTF8Flag;\r
-    }\r
-\r
-    private static final int MIN_EOCD_SIZE =\r
-        /* end of central dir signature    */ WORD\r
-        /* number of this disk             */ + SHORT\r
-        /* number of the disk with the     */\r
-        /* start of the central directory  */ + SHORT\r
-        /* total number of entries in      */\r
-        /* the central dir on this disk    */ + SHORT\r
-        /* total number of entries in      */\r
-        /* the central dir                 */ + SHORT\r
-        /* size of the central directory   */ + WORD\r
-        /* offset of start of central      */\r
-        /* directory with respect to       */\r
-        /* the starting disk number        */ + WORD\r
-        /* zipfile comment length          */ + SHORT;\r
-\r
-    private static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE\r
-        /* maximum length of zipfile comment */ + 0xFFFF;\r
-\r
-    private static final int CFD_LOCATOR_OFFSET =\r
-        /* end of central dir signature    */ WORD\r
-        /* number of this disk             */ + SHORT\r
-        /* number of the disk with the     */\r
-        /* start of the central directory  */ + SHORT\r
-        /* total number of entries in      */\r
-        /* the central dir on this disk    */ + SHORT\r
-        /* total number of entries in      */\r
-        /* the central dir                 */ + SHORT\r
-        /* size of the central directory   */ + WORD;\r
-\r
-    /**\r
-     * Searches for the &quot;End of central dir record&quot;, parses\r
-     * it and positions the stream at the first central directory\r
-     * record.\r
-     */\r
-    private void positionAtCentralDirectory()\r
-        throws IOException {\r
-        boolean found = false;\r
-        long off = archive.length() - MIN_EOCD_SIZE;\r
-        long stopSearching = Math.max(0L, archive.length() - MAX_EOCD_SIZE);\r
-        if (off >= 0) {\r
-            archive.seek(off);\r
-            byte[] sig = ZipOutputStream.EOCD_SIG;\r
-            int curr = archive.read();\r
-            while (off >= stopSearching && curr != -1) {\r
-                if (curr == sig[POS_0]) {\r
-                    curr = archive.read();\r
-                    if (curr == sig[POS_1]) {\r
-                        curr = archive.read();\r
-                        if (curr == sig[POS_2]) {\r
-                            curr = archive.read();\r
-                            if (curr == sig[POS_3]) {\r
-                                found = true;\r
-                                break;\r
-                            }\r
-                        }\r
-                    }\r
-                }\r
-                archive.seek(--off);\r
-                curr = archive.read();\r
-            }\r
-        }\r
-        if (!found) {\r
-            throw new ZipException("archive is not a ZIP archive");\r
-        }\r
-        archive.seek(off + CFD_LOCATOR_OFFSET);\r
-        byte[] cfdOffset = new byte[WORD];\r
-        archive.readFully(cfdOffset);\r
-        archive.seek(ZipLong.getValue(cfdOffset));\r
-    }\r
-\r
-    /**\r
-     * Number of bytes in local file header up to the &quot;length of\r
-     * filename&quot; entry.\r
-     */\r
-    private static final long LFH_OFFSET_FOR_FILENAME_LENGTH =\r
-        /* local file header signature     */ WORD\r
-        /* version needed to extract       */ + SHORT\r
-        /* general purpose bit flag        */ + SHORT\r
-        /* compression method              */ + SHORT\r
-        /* last mod file time              */ + SHORT\r
-        /* last mod file date              */ + SHORT\r
-        /* crc-32                          */ + WORD\r
-        /* compressed size                 */ + WORD\r
-        /* uncompressed size               */ + WORD;\r
-\r
-    /**\r
-     * Walks through all recorded entries and adds the data available\r
-     * from the local file header.\r
-     *\r
-     * <p>Also records the offsets for the data to read from the\r
-     * entries.</p>\r
-     */\r
-    private void resolveLocalFileHeaderData(Map entriesWithoutUTF8Flag)\r
-        throws IOException {\r
-        Enumeration e = getEntries();\r
-        while (e.hasMoreElements()) {\r
-            ZipEntry ze = (ZipEntry) e.nextElement();\r
-            OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze);\r
-            long offset = offsetEntry.headerOffset;\r
-            archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH);\r
-            byte[] b = new byte[SHORT];\r
-            archive.readFully(b);\r
-            int fileNameLen = ZipShort.getValue(b);\r
-            archive.readFully(b);\r
-            int extraFieldLen = ZipShort.getValue(b);\r
-            int lenToSkip = fileNameLen;\r
-            while (lenToSkip > 0) {\r
-                int skipped = archive.skipBytes(lenToSkip);\r
-                if (skipped <= 0) {\r
-                    throw new RuntimeException("failed to skip file name in"\r
-                                               + " local file header");\r
-                }\r
-                lenToSkip -= skipped;\r
-            }            \r
-            byte[] localExtraData = new byte[extraFieldLen];\r
-            archive.readFully(localExtraData);\r
-            ze.setExtra(localExtraData);\r
-            /*dataOffsets.put(ze,\r
-                            new Long(offset + LFH_OFFSET_FOR_FILENAME_LENGTH\r
-                                     + SHORT + SHORT + fileNameLen + extraFieldLen));\r
-            */\r
-            offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH\r
-                + SHORT + SHORT + fileNameLen + extraFieldLen;\r
-\r
-            if (entriesWithoutUTF8Flag.containsKey(ze)) {\r
-                setNameAndCommentFromExtraFields(ze,\r
-                                                 (NameAndComment)\r
-                                                 entriesWithoutUTF8Flag.get(ze));\r
-            }\r
-        }\r
-    }\r
-\r
-    /**\r
-     * Convert a DOS date/time field to a Date object.\r
-     *\r
-     * @param zipDosTime contains the stored DOS time.\r
-     * @return a Date instance corresponding to the given time.\r
-     */\r
-    protected static Date fromDosTime(ZipLong zipDosTime) {\r
-        long dosTime = zipDosTime.getValue();\r
-        return new Date(dosToJavaTime(dosTime));\r
-    }\r
-\r
-    /*\r
-     * Converts DOS time to Java time (number of milliseconds since epoch).\r
-     */\r
-    private static long dosToJavaTime(long dosTime) {\r
-        Calendar cal = Calendar.getInstance();\r
-        // CheckStyle:MagicNumberCheck OFF - no point\r
-        cal.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980);\r
-        cal.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1);\r
-        cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f);\r
-        cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f);\r
-        cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f);\r
-        cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e);\r
-        // CheckStyle:MagicNumberCheck ON\r
-        return cal.getTime().getTime();\r
-    }\r
-\r
-\r
-    /**\r
-     * Retrieve a String from the given bytes using the encoding set\r
-     * for this ZipFile.\r
-     *\r
-     * @param bytes the byte array to transform\r
-     * @return String obtained by using the given encoding\r
-     * @throws ZipException if the encoding cannot be recognized.\r
-     */\r
-    protected String getString(byte[] bytes) throws ZipException {\r
-        try {\r
-            return ZipEncodingHelper.getZipEncoding(encoding).decode(bytes);\r
-        } catch (IOException ex) {\r
-            throw new ZipException("Failed to decode name: " + ex.getMessage());\r
-        }\r
-    }\r
-\r
-    /**\r
-     * Checks whether the archive starts with a LFH.  If it doesn't,\r
-     * it may be an empty archive.\r
-     */\r
-    private boolean startsWithLocalFileHeader() throws IOException {\r
-        archive.seek(0);\r
-        final byte[] start = new byte[WORD];\r
-        archive.readFully(start);\r
-        for (int i = 0; i < start.length; i++) {\r
-            if (start[i] != ZipOutputStream.LFH_SIG[i]) {\r
-                return false;\r
-            }\r
-        }\r
-        return true;\r
-    }\r
-\r
-    /**\r
-     * If the entry has Unicode*ExtraFields and the CRCs of the\r
-     * names/comments match those of the extra fields, transfer the\r
-     * known Unicode values from the extra field.\r
-     */\r
-    private void setNameAndCommentFromExtraFields(ZipEntry ze,\r
-                                                  NameAndComment nc) {\r
-        UnicodePathExtraField name = (UnicodePathExtraField)\r
-            ze.getExtraField(UnicodePathExtraField.UPATH_ID);\r
-        String originalName = ze.getName();\r
-        String newName = getUnicodeStringIfOriginalMatches(name, nc.name);\r
-        if (newName != null && !originalName.equals(newName)) {\r
-            ze.setName(newName);\r
-            nameMap.remove(originalName);\r
-            nameMap.put(newName, ze);\r
-        }\r
-\r
-        if (nc.comment != null && nc.comment.length > 0) {\r
-            UnicodeCommentExtraField cmt = (UnicodeCommentExtraField)\r
-                ze.getExtraField(UnicodeCommentExtraField.UCOM_ID);\r
-            String newComment =\r
-                getUnicodeStringIfOriginalMatches(cmt, nc.comment);\r
-            if (newComment != null) {\r
-                ze.setComment(newComment);\r
-            }\r
-        }\r
-    }\r
-\r
-    /**\r
-     * If the stored CRC matches the one of the given name, return the\r
-     * Unicode name of the given field.\r
-     *\r
-     * <p>If the field is null or the CRCs don't match, return null\r
-     * instead.</p>\r
-     */\r
-    private String getUnicodeStringIfOriginalMatches(AbstractUnicodeExtraField f,\r
-                                                     byte[] orig) {\r
-        if (f != null) {\r
-            CRC32 crc32 = new CRC32();\r
-            crc32.update(orig);\r
-            long origCRC32 = crc32.getValue();\r
-\r
-            if (origCRC32 == f.getNameCRC32()) {\r
-                try {\r
-                    return ZipEncodingHelper\r
-                        .UTF8_ZIP_ENCODING.decode(f.getUnicodeName());\r
-                } catch (IOException ex) {\r
-                    // UTF-8 unsupported?  should be impossible the\r
-                    // Unicode*ExtraField must contain some bad bytes\r
-\r
-                       logger.log(Level.WARNING, "ZipFile: UTF-8 unsupported."\r
-                                       + " should be impossible the Unicode*ExtraField must contain some bad bytes.", ex);\r
-                    return null;\r
-                }\r
-            }\r
-        }\r
-        return null;\r
-    }\r
-\r
-    /**\r
-     * InputStream that delegates requests to the underlying\r
-     * RandomAccessFile, making sure that only bytes from a certain\r
-     * range can be read.\r
-     */\r
-    private class BoundedInputStream extends InputStream {\r
-        private long remaining;\r
-        private long loc;\r
-        private boolean addDummyByte = false;\r
-\r
-        BoundedInputStream(long start, long remaining) {\r
-            this.remaining = remaining;\r
-            loc = start;\r
-        }\r
-\r
-        public int read() throws IOException {\r
-            if (remaining-- <= 0) {\r
-                if (addDummyByte) {\r
-                    addDummyByte = false;\r
-                    return 0;\r
-                }\r
-                return -1;\r
-            }\r
-            synchronized (archive) {\r
-                archive.seek(loc++);\r
-                return archive.read();\r
-            }\r
-        }\r
-\r
-        public int read(byte[] b, int off, int len) throws IOException {\r
-            if (remaining <= 0) {\r
-                if (addDummyByte) {\r
-                    addDummyByte = false;\r
-                    b[off] = 0;\r
-                    return 1;\r
-                }\r
-                return -1;\r
-            }\r
-\r
-            if (len <= 0) {\r
-                return 0;\r
-            }\r
-\r
-            if (len > remaining) {\r
-                len = (int) remaining;\r
-            }\r
-            int ret = -1;\r
-            synchronized (archive) {\r
-                archive.seek(loc);\r
-                ret = archive.read(b, off, len);\r
-            }\r
-            if (ret > 0) {\r
-                loc += ret;\r
-                remaining -= ret;\r
-            }\r
-            return ret;\r
-        }\r
-\r
-        /**\r
-         * Inflater needs an extra dummy byte for nowrap - see\r
-         * Inflater's javadocs.\r
-         */\r
-        void addDummy() {\r
-            addDummyByte = true;\r
-        }\r
-    }\r
-\r
-    private static final class NameAndComment {\r
-        private final byte[] name;\r
-        private final byte[] comment;\r
-        private NameAndComment(byte[] name, byte[] comment) {\r
-            this.name = name;\r
-            this.comment = comment;\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.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.zip.CRC32;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+import java.util.zip.ZipException;
+
+/**
+ * Replacement for <code>java.util.ZipFile</code>.
+ *
+ * <p>This class adds support for file name encodings other than UTF-8
+ * (which is required to work on ZIP files created by native zip tools
+ * and is able to skip a preamble like the one found in self
+ * extracting archives.  Furthermore it returns instances of
+ * <code>org.apache.tools.zip.ZipEntry</code> instead of
+ * <code>java.util.zip.ZipEntry</code>.</p>
+ *
+ * <p>It doesn't extend <code>java.util.zip.ZipFile</code> as it would
+ * have to reimplement all methods anyway.  Like
+ * <code>java.util.ZipFile</code>, it uses RandomAccessFile under the
+ * covers and supports compressed and uncompressed entries.</p>
+ *
+ * <p>The method signatures mimic the ones of
+ * <code>java.util.zip.ZipFile</code>, with a couple of exceptions:
+ *
+ * <ul>
+ *   <li>There is no getName method.</li>
+ *   <li>entries has been renamed to getEntries.</li>
+ *   <li>getEntries and getEntry return
+ *   <code>org.apache.tools.zip.ZipEntry</code> instances.</li>
+ *   <li>close is allowed to throw IOException.</li>
+ * </ul>
+ *
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class ZipFile {
+       
+       private static final Logger logger = Logger.getLogger(ZipFile.class.getName());
+
+    private static final int HASH_SIZE = 509;
+    private static final int SHORT     =   2;
+    private static final int WORD      =   4;
+    private static final int NIBLET_MASK = 0x0f;
+    private static final int BYTE_SHIFT = 8;
+    private static final int POS_0 = 0;
+    private static final int POS_1 = 1;
+    private static final int POS_2 = 2;
+    private static final int POS_3 = 3;
+
+    /**
+     * Maps ZipEntrys to Longs, recording the offsets of the local
+     * file headers.
+     */
+    private final Map entries = new HashMap(HASH_SIZE);
+
+    /**
+     * Maps String to ZipEntrys, name -> actual entry.
+     */
+    private final Map nameMap = new HashMap(HASH_SIZE);
+
+    private static final class OffsetEntry {
+        private long headerOffset = -1;
+        private long dataOffset = -1;
+    }
+
+    /**
+     * 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>
+     */
+    private String encoding = null;
+
+    /**
+     * The zip encoding to use for filenames and the file comment.
+     */
+    private final ZipEncoding zipEncoding;
+
+    /**
+     * The actual data source.
+     */
+    private RandomAccessFile archive;
+
+    /**
+     * Whether to look for and use Unicode extra fields.
+     */
+    private final boolean useUnicodeExtraFields;
+
+    /**
+     * Opens the given file for reading, assuming the platform's
+     * native encoding for file names.
+     *
+     * @param f the archive.
+     *
+     * @throws IOException if an error occurs while reading the file.
+     */
+    public ZipFile(File f) throws IOException {
+        this(f, null);
+    }
+
+    /**
+     * Opens the given file for reading, assuming the platform's
+     * native encoding for file names.
+     *
+     * @param name name of the archive.
+     *
+     * @throws IOException if an error occurs while reading the file.
+     */
+    public ZipFile(String name) throws IOException {
+        this(new File(name), null);
+    }
+
+    /**
+     * Opens the given file for reading, assuming the specified
+     * encoding for file names, scanning unicode extra fields.
+     *
+     * @param name name of the archive.
+     * @param encoding the encoding to use for file names
+     *
+     * @throws IOException if an error occurs while reading the file.
+     */
+    public ZipFile(String name, String encoding) throws IOException {
+        this(new File(name), encoding, true);
+    }
+
+    /**
+     * Opens the given file for reading, assuming the specified
+     * encoding for file names and scanning for unicode extra fields.
+     *
+     * @param f the archive.
+     * @param encoding the encoding to use for file names, use null
+     * for the platform's default encoding
+     *
+     * @throws IOException if an error occurs while reading the file.
+     */
+    public ZipFile(File f, String encoding) throws IOException {
+        this(f, encoding, true);
+    }
+
+    /**
+     * Opens the given file for reading, assuming the specified
+     * encoding for file names.
+     *
+     * @param f the archive.
+     * @param encoding the encoding to use for file names, use null
+     * for the platform's default encoding
+     * @param useUnicodeExtraFields whether to use InfoZIP Unicode
+     * Extra Fields (if present) to set the file names.
+     *
+     * @throws IOException if an error occurs while reading the file.
+     */
+    public ZipFile(File f, String encoding, boolean useUnicodeExtraFields)
+        throws IOException {
+        this.encoding = encoding;
+        this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
+        this.useUnicodeExtraFields = useUnicodeExtraFields;
+        archive = new RandomAccessFile(f, "r");
+        boolean success = false;
+        try {
+            Map entriesWithoutUTF8Flag = populateFromCentralDirectory();
+            resolveLocalFileHeaderData(entriesWithoutUTF8Flag);
+            success = true;
+        } finally {
+            if (!success) {
+                try {
+                    archive.close();
+                } catch (IOException e2) {
+                    // swallow, throw the original exception instead
+                }
+            }
+        }
+    }
+
+    /**
+     * The encoding to use for filenames and the file comment.
+     *
+     * @return null if using the platform's default character encoding.
+     */
+    public String getEncoding() {
+        return encoding;
+    }
+
+    /**
+     * Closes the archive.
+     * @throws IOException if an error occurs closing the archive.
+     */
+    public void close() throws IOException {
+        archive.close();
+    }
+
+    /**
+     * close a zipfile quietly; throw no io fault, do nothing
+     * on a null parameter
+     * @param zipfile file to close, can be null
+     */
+    public static void closeQuietly(ZipFile zipfile) {
+        if (zipfile != null) {
+            try {
+                zipfile.close();
+            } catch (IOException e) {
+                //ignore
+            }
+        }
+    }
+
+    /**
+     * Returns all entries.
+     * @return all entries as {@link ZipEntry} instances
+     */
+    public Enumeration getEntries() {
+        return Collections.enumeration(entries.keySet());
+    }
+
+    /**
+     * Returns a named entry - or <code>null</code> if no entry by
+     * that name exists.
+     * @param name name of the entry.
+     * @return the ZipEntry corresponding to the given name - or
+     * <code>null</code> if not present.
+     */
+    public ZipEntry getEntry(String name) {
+        return (ZipEntry) nameMap.get(name);
+    }
+
+    /**
+     * Returns an InputStream for reading the contents of the given entry.
+     * @param ze the entry to get the stream for.
+     * @return a stream to read the entry from.
+     * @throws IOException if unable to create an input stream from the zipenty
+     * @throws ZipException if the zipentry has an unsupported
+     * compression method
+     */
+    public InputStream getInputStream(ZipEntry ze)
+        throws IOException, ZipException {
+        OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze);
+        if (offsetEntry == null) {
+            return null;
+        }
+        long start = offsetEntry.dataOffset;
+        BoundedInputStream bis =
+            new BoundedInputStream(start, ze.getCompressedSize());
+        switch (ze.getMethod()) {
+            case ZipEntry.STORED:
+                return bis;
+            case ZipEntry.DEFLATED:
+                bis.addDummy();
+                return new InflaterInputStream(bis, new Inflater(true));
+            default:
+                throw new ZipException("Found unsupported compression method "
+                                       + ze.getMethod());
+        }
+    }
+
+    private static final int CFH_LEN =
+        /* version made by                 */ SHORT
+        /* version needed to extract       */ + SHORT
+        /* general purpose bit flag        */ + SHORT
+        /* compression method              */ + SHORT
+        /* last mod file time              */ + SHORT
+        /* last mod file date              */ + SHORT
+        /* crc-32                          */ + WORD
+        /* compressed size                 */ + WORD
+        /* uncompressed size               */ + WORD
+        /* filename length                 */ + SHORT
+        /* extra field length              */ + SHORT
+        /* file comment length             */ + SHORT
+        /* disk number start               */ + SHORT
+        /* internal file attributes        */ + SHORT
+        /* external file attributes        */ + WORD
+        /* relative offset of local header */ + WORD;
+
+    /**
+     * Reads the central directory of the given archive and populates
+     * the internal tables with ZipEntry instances.
+     *
+     * <p>The ZipEntrys will know all data that can be obtained from
+     * the central directory alone, but not the data that requires the
+     * local file header or additional data to be read.</p>
+     *
+     * @return a Map&lt;ZipEntry, NameAndComment>&gt; of
+     * zipentries that didn't have the language encoding flag set when
+     * read.
+     */
+    private Map populateFromCentralDirectory()
+        throws IOException {
+        HashMap noUTF8Flag = new HashMap();
+
+        positionAtCentralDirectory();
+
+        byte[] cfh = new byte[CFH_LEN];
+
+        byte[] signatureBytes = new byte[WORD];
+        archive.readFully(signatureBytes);
+        long sig = ZipLong.getValue(signatureBytes);
+        final long cfhSig = ZipLong.getValue(ZipOutputStream.CFH_SIG);
+        if (sig != cfhSig && startsWithLocalFileHeader()) {
+            throw new IOException("central directory is empty, can't expand"
+                                  + " corrupt archive.");
+        }
+        while (sig == cfhSig) {
+            archive.readFully(cfh);
+            int off = 0;
+            ZipEntry ze = new ZipEntry();
+
+            int versionMadeBy = ZipShort.getValue(cfh, off);
+            off += SHORT;
+            ze.setPlatform((versionMadeBy >> BYTE_SHIFT) & NIBLET_MASK);
+
+            off += SHORT; // skip version info
+
+            final int generalPurposeFlag = ZipShort.getValue(cfh, off);
+            final boolean hasUTF8Flag = 
+                (generalPurposeFlag & ZipOutputStream.UFT8_NAMES_FLAG) != 0;
+            final ZipEncoding entryEncoding =
+                hasUTF8Flag ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding;
+
+            off += SHORT;
+
+            ze.setMethod(ZipShort.getValue(cfh, off));
+            off += SHORT;
+
+            // FIXME this is actually not very cpu cycles friendly as we are converting from
+            // dos to java while the underlying Sun implementation will convert
+            // from java to dos time for internal storage...
+            long time = dosToJavaTime(ZipLong.getValue(cfh, off));
+            ze.setTime(time);
+            off += WORD;
+
+            ze.setCrc(ZipLong.getValue(cfh, off));
+            off += WORD;
+
+            ze.setCompressedSize(ZipLong.getValue(cfh, off));
+            off += WORD;
+
+            ze.setSize(ZipLong.getValue(cfh, off));
+            off += WORD;
+
+            int fileNameLen = ZipShort.getValue(cfh, off);
+            off += SHORT;
+
+            int extraLen = ZipShort.getValue(cfh, off);
+            off += SHORT;
+
+            int commentLen = ZipShort.getValue(cfh, off);
+            off += SHORT;
+
+            off += SHORT; // disk number
+
+            ze.setInternalAttributes(ZipShort.getValue(cfh, off));
+            off += SHORT;
+
+            ze.setExternalAttributes(ZipLong.getValue(cfh, off));
+            off += WORD;
+
+            byte[] fileName = new byte[fileNameLen];
+            archive.readFully(fileName);
+            ze.setName(entryEncoding.decode(fileName));
+
+            // LFH offset,
+            OffsetEntry offset = new OffsetEntry();
+            offset.headerOffset = ZipLong.getValue(cfh, off);
+            // data offset will be filled later
+            entries.put(ze, offset);
+
+            nameMap.put(ze.getName(), ze);
+
+            byte[] cdExtraData = new byte[extraLen];
+            archive.readFully(cdExtraData);
+            ze.setCentralDirectoryExtra(cdExtraData);
+
+            byte[] comment = new byte[commentLen];
+            archive.readFully(comment);
+            ze.setComment(entryEncoding.decode(comment));
+
+            archive.readFully(signatureBytes);
+            sig = ZipLong.getValue(signatureBytes);
+
+            if (!hasUTF8Flag && useUnicodeExtraFields) {
+                noUTF8Flag.put(ze, new NameAndComment(fileName, comment));
+            }
+        }
+        return noUTF8Flag;
+    }
+
+    private static final int MIN_EOCD_SIZE =
+        /* end of central dir signature    */ WORD
+        /* number of this disk             */ + SHORT
+        /* number of the disk with the     */
+        /* start of the central directory  */ + SHORT
+        /* total number of entries in      */
+        /* the central dir on this disk    */ + SHORT
+        /* total number of entries in      */
+        /* the central dir                 */ + SHORT
+        /* size of the central directory   */ + WORD
+        /* offset of start of central      */
+        /* directory with respect to       */
+        /* the starting disk number        */ + WORD
+        /* zipfile comment length          */ + SHORT;
+
+    private static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE
+        /* maximum length of zipfile comment */ + 0xFFFF;
+
+    private static final int CFD_LOCATOR_OFFSET =
+        /* end of central dir signature    */ WORD
+        /* number of this disk             */ + SHORT
+        /* number of the disk with the     */
+        /* start of the central directory  */ + SHORT
+        /* total number of entries in      */
+        /* the central dir on this disk    */ + SHORT
+        /* total number of entries in      */
+        /* the central dir                 */ + SHORT
+        /* size of the central directory   */ + WORD;
+
+    /**
+     * Searches for the &quot;End of central dir record&quot;, parses
+     * it and positions the stream at the first central directory
+     * record.
+     */
+    private void positionAtCentralDirectory()
+        throws IOException {
+        boolean found = false;
+        long off = archive.length() - MIN_EOCD_SIZE;
+        long stopSearching = Math.max(0L, archive.length() - MAX_EOCD_SIZE);
+        if (off >= 0) {
+            archive.seek(off);
+            byte[] sig = ZipOutputStream.EOCD_SIG;
+            int curr = archive.read();
+            while (off >= stopSearching && curr != -1) {
+                if (curr == sig[POS_0]) {
+                    curr = archive.read();
+                    if (curr == sig[POS_1]) {
+                        curr = archive.read();
+                        if (curr == sig[POS_2]) {
+                            curr = archive.read();
+                            if (curr == sig[POS_3]) {
+                                found = true;
+                                break;
+                            }
+                        }
+                    }
+                }
+                archive.seek(--off);
+                curr = archive.read();
+            }
+        }
+        if (!found) {
+            throw new ZipException("archive is not a ZIP archive");
+        }
+        archive.seek(off + CFD_LOCATOR_OFFSET);
+        byte[] cfdOffset = new byte[WORD];
+        archive.readFully(cfdOffset);
+        archive.seek(ZipLong.getValue(cfdOffset));
+    }
+
+    /**
+     * Number of bytes in local file header up to the &quot;length of
+     * filename&quot; entry.
+     */
+    private static final long LFH_OFFSET_FOR_FILENAME_LENGTH =
+        /* local file header signature     */ WORD
+        /* version needed to extract       */ + SHORT
+        /* general purpose bit flag        */ + SHORT
+        /* compression method              */ + SHORT
+        /* last mod file time              */ + SHORT
+        /* last mod file date              */ + SHORT
+        /* crc-32                          */ + WORD
+        /* compressed size                 */ + WORD
+        /* uncompressed size               */ + WORD;
+
+    /**
+     * Walks through all recorded entries and adds the data available
+     * from the local file header.
+     *
+     * <p>Also records the offsets for the data to read from the
+     * entries.</p>
+     */
+    private void resolveLocalFileHeaderData(Map entriesWithoutUTF8Flag)
+        throws IOException {
+        Enumeration e = getEntries();
+        while (e.hasMoreElements()) {
+            ZipEntry ze = (ZipEntry) e.nextElement();
+            OffsetEntry offsetEntry = (OffsetEntry) entries.get(ze);
+            long offset = offsetEntry.headerOffset;
+            archive.seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH);
+            byte[] b = new byte[SHORT];
+            archive.readFully(b);
+            int fileNameLen = ZipShort.getValue(b);
+            archive.readFully(b);
+            int extraFieldLen = ZipShort.getValue(b);
+            int lenToSkip = fileNameLen;
+            while (lenToSkip > 0) {
+                int skipped = archive.skipBytes(lenToSkip);
+                if (skipped <= 0) {
+                    throw new RuntimeException("failed to skip file name in"
+                                               + " local file header");
+                }
+                lenToSkip -= skipped;
+            }            
+            byte[] localExtraData = new byte[extraFieldLen];
+            archive.readFully(localExtraData);
+            ze.setExtra(localExtraData);
+            /*dataOffsets.put(ze,
+                            new Long(offset + LFH_OFFSET_FOR_FILENAME_LENGTH
+                                     + SHORT + SHORT + fileNameLen + extraFieldLen));
+            */
+            offsetEntry.dataOffset = offset + LFH_OFFSET_FOR_FILENAME_LENGTH
+                + SHORT + SHORT + fileNameLen + extraFieldLen;
+
+            if (entriesWithoutUTF8Flag.containsKey(ze)) {
+                setNameAndCommentFromExtraFields(ze,
+                                                 (NameAndComment)
+                                                 entriesWithoutUTF8Flag.get(ze));
+            }
+        }
+    }
+
+    /**
+     * 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.
+     */
+    protected 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).
+     */
+    private 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();
+    }
+
+
+    /**
+     * Retrieve a String from the given bytes using the encoding set
+     * for this ZipFile.
+     *
+     * @param bytes the byte array to transform
+     * @return String obtained by using the given encoding
+     * @throws ZipException if the encoding cannot be recognized.
+     */
+    protected String getString(byte[] bytes) throws ZipException {
+        try {
+            return ZipEncodingHelper.getZipEncoding(encoding).decode(bytes);
+        } catch (IOException ex) {
+            throw new ZipException("Failed to decode name: " + ex.getMessage());
+        }
+    }
+
+    /**
+     * Checks whether the archive starts with a LFH.  If it doesn't,
+     * it may be an empty archive.
+     */
+    private boolean startsWithLocalFileHeader() throws IOException {
+        archive.seek(0);
+        final byte[] start = new byte[WORD];
+        archive.readFully(start);
+        for (int i = 0; i < start.length; i++) {
+            if (start[i] != ZipOutputStream.LFH_SIG[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 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.
+     */
+    private void setNameAndCommentFromExtraFields(ZipEntry ze,
+                                                  NameAndComment nc) {
+        UnicodePathExtraField name = (UnicodePathExtraField)
+            ze.getExtraField(UnicodePathExtraField.UPATH_ID);
+        String originalName = ze.getName();
+        String newName = getUnicodeStringIfOriginalMatches(name, nc.name);
+        if (newName != null && !originalName.equals(newName)) {
+            ze.setName(newName);
+            nameMap.remove(originalName);
+            nameMap.put(newName, ze);
+        }
+
+        if (nc.comment != null && nc.comment.length > 0) {
+            UnicodeCommentExtraField cmt = (UnicodeCommentExtraField)
+                ze.getExtraField(UnicodeCommentExtraField.UCOM_ID);
+            String newComment =
+                getUnicodeStringIfOriginalMatches(cmt, nc.comment);
+            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 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
+
+                       logger.log(Level.WARNING, "ZipFile: UTF-8 unsupported."
+                                       + " should be impossible the Unicode*ExtraField must contain some bad bytes.", ex);
+                    return null;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * InputStream that delegates requests to the underlying
+     * RandomAccessFile, making sure that only bytes from a certain
+     * range can be read.
+     */
+    private class BoundedInputStream extends InputStream {
+        private long remaining;
+        private long loc;
+        private boolean addDummyByte = false;
+
+        BoundedInputStream(long start, long remaining) {
+            this.remaining = remaining;
+            loc = start;
+        }
+
+        public int read() throws IOException {
+            if (remaining-- <= 0) {
+                if (addDummyByte) {
+                    addDummyByte = false;
+                    return 0;
+                }
+                return -1;
+            }
+            synchronized (archive) {
+                archive.seek(loc++);
+                return archive.read();
+            }
+        }
+
+        public int read(byte[] b, int off, int len) throws IOException {
+            if (remaining <= 0) {
+                if (addDummyByte) {
+                    addDummyByte = false;
+                    b[off] = 0;
+                    return 1;
+                }
+                return -1;
+            }
+
+            if (len <= 0) {
+                return 0;
+            }
+
+            if (len > remaining) {
+                len = (int) remaining;
+            }
+            int ret = -1;
+            synchronized (archive) {
+                archive.seek(loc);
+                ret = archive.read(b, off, len);
+            }
+            if (ret > 0) {
+                loc += ret;
+                remaining -= ret;
+            }
+            return ret;
+        }
+
+        /**
+         * Inflater needs an extra dummy byte for nowrap - see
+         * Inflater's javadocs.
+         */
+        void addDummy() {
+            addDummyByte = true;
+        }
+    }
+
+    private static final class NameAndComment {
+        private final byte[] name;
+        private final byte[] comment;
+        private NameAndComment(byte[] name, byte[] comment) {
+            this.name = name;
+            this.comment = comment;
+        }
+    }
+}