-/*\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<ZipEntry, NameAndComment>> 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 "End of central dir record", 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 "length of\r
- * filename" 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<ZipEntry, NameAndComment>> 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 "End of central dir record", 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 "length of
+ * filename" 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;
+ }
+ }
+}