/** * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.commons.file.archive.zip; import com.mucommander.commons.file.*; import com.mucommander.commons.file.archive.AbstractRWArchiveFile; import com.mucommander.commons.file.archive.ArchiveEntry; import com.mucommander.commons.file.archive.ArchiveEntryIterator; import com.mucommander.commons.file.archive.zip.provider.ZipConstants; import com.mucommander.commons.file.archive.zip.provider.ZipEntry; import com.mucommander.commons.file.archive.zip.provider.ZipFile; import com.mucommander.commons.io.FilteredOutputStream; import java.io.*; import java.util.Iterator; import java.util.zip.ZipInputStream; /** * ZipArchiveFile provides read and write access (under certain conditions) to archives in the Zip format. * <p> * Two different packages that implement the actual Zip compression format are used: the homemade * <code>com.mucommander.commons.file.impl.zip.provider</code> package and Java's <code>java.util.zip</code>. * <code>com.mucommander.commons.file.impl.zip.provider</code> provides additional functionality and improved performance over * <code>java.util.zip</code> but requires the underlying file to supply a <code>RandomAccessInputStream</code> for read * access and a <code>RandomAccessOutputStream</code> for write access. If the underlying file can't provide at least a * <code>RandomAccessInputStream</code>, the lesser <code>java.util.zip</code> package is used. * </p> * * @see com.mucommander.commons.file.archive.zip.ZipFormatProvider * @see com.mucommander.commons.file.archive.zip.provider.ZipFile * @author Maxence Bernard */ public class ZipArchiveFile extends AbstractRWArchiveFile { /** The ZipFile object that actually reads and modifies the entries in the Zip file */ private ZipFile zipFile; /** The date at which the current ZipFile object was created */ private long lastZipFileDate; /** Contents of an empty Zip file, 22 bytes long */ private final static byte EMPTY_ZIP_BYTES[] = { 0x50, 0x4B, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; /** * Creates a new ZipArchiveFile on top of the given file. * * @param file the underlying archive file */ public ZipArchiveFile(AbstractFile file) { super(file); } /** * Checks if the underlying Zip file is up-to-date, i.e. exists and has not changed without this archive file * being aware of it. If one of those 2 conditions are not met, (re)load the ZipFile instance (parse the entries) * and declare the Zip file as up-to-date. * * @throws IOException if an error occurred while reloading */ private void checkZipFile() throws IOException, UnsupportedFileOperationException { long currentDate = file.getDate(); if(zipFile==null || currentDate!=lastZipFileDate) { zipFile = new ZipFile(file); declareZipFileUpToDate(); } } /** * Declare the underlying Zip file as up-to-date. Calling this method after the Zip file has been modified prevents * {@link #checkZipFile()} from being reloaded. */ private void declareZipFileUpToDate() { lastZipFileDate = file.getDate(); } /** * Creates and returns a {@link com.mucommander.commons.file.archive.zip.provider.ZipEntry} instance using the attributes * of the given {@link ArchiveEntry}. * * @param entry the object that serves to initialize the attributes of the returned ZipEntry * @return a ZipEntry whose attributes are fetched from the given ZipEntry */ private ZipEntry createZipEntry(ArchiveEntry entry) { boolean isDirectory = entry.isDirectory(); String path = entry.getPath(); if(isDirectory && !path.endsWith("/")) path += "/"; com.mucommander.commons.file.archive.zip.provider.ZipEntry zipEntry = new com.mucommander.commons.file.archive.zip.provider.ZipEntry(path); zipEntry.setMethod(ZipConstants.DEFLATED); zipEntry.setTime(System.currentTimeMillis()); zipEntry.setUnixMode(SimpleFilePermissions.padPermissions(entry.getPermissions(), isDirectory ? FilePermissions.DEFAULT_DIRECTORY_PERMISSIONS : FilePermissions.DEFAULT_FILE_PERMISSIONS).getIntValue()); return zipEntry; } /** * Creates and return an {@link ArchiveEntry()} whose attributes are fetched from the given {@link com.mucommander.commons.file.archive.zip.provider.ZipEntry}. * It is worth noting that the returned entry has the {@link ArchiveEntry#exists exists} flag set to <code>true</code>. * * @param zipEntry the object that serves to initialize the attributes of the returned ArchiveEntry * @return an ArchiveEntry whose attributes are fetched from the given ZipEntry */ static ArchiveEntry createArchiveEntry(ZipEntry zipEntry) { ArchiveEntry entry = new ArchiveEntry(zipEntry.getName(), zipEntry.isDirectory(), zipEntry.getTime(), zipEntry.getSize(), true); if(zipEntry.hasUnixMode()) entry.setPermissions(new SimpleFilePermissions(zipEntry.getUnixMode())); entry.setEntryObject(zipEntry); return entry; } /** * Adds the given {@link ArchiveEntry} to the entries tree and declares the Zip file and entries tree up-to-date. * * @param entry the entry to add to the entries tree * @throws IOException if an error occurred while adding the entry to the tree * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ private void finishAddEntry(ArchiveEntry entry) throws IOException, UnsupportedFileOperationException { // Declare the zip file and entries tree up-to-date declareZipFileUpToDate(); declareEntriesTreeUpToDate(); // Add the new entry to the entries tree addToEntriesTree(entry); } ////////////////////////////////////////// // AbstractROArchiveFile implementation // ////////////////////////////////////////// @Override public synchronized ArchiveEntryIterator getEntryIterator() throws IOException, UnsupportedFileOperationException { // If the underlying AbstractFile has random read access, use our own ZipFile implementation to read entries if (file.isFileOperationSupported(FileOperation.RANDOM_READ_FILE)) { checkZipFile(); final Iterator<ZipEntry> iterator = zipFile.getEntries(); return new ArchiveEntryIterator() { public ArchiveEntry nextEntry() throws IOException { ZipEntry entry; if(!iterator.hasNext() || (entry = iterator.next())==null) return null; return createArchiveEntry(entry); } public void close() throws IOException { } }; } // If the underlying AbstractFile doesn't have random read access, use java.util.zip.ZipInputStream to // read the entries. This is much slower than the former method as the file cannot be sought through and needs // to be traversed. else { return new JavaUtilZipEntryIterator(new ZipInputStream(file.getInputStream())); } } @Override public synchronized InputStream getEntryInputStream(ArchiveEntry entry, ArchiveEntryIterator entryIterator) throws IOException, UnsupportedFileOperationException { // If the underlying AbstractFile has random read access, use our own ZipFile implementation to read the entry if (file.isFileOperationSupported(FileOperation.RANDOM_READ_FILE)) { checkZipFile(); ZipEntry zipEntry = (com.mucommander.commons.file.archive.zip.provider.ZipEntry)entry.getEntryObject(); if(zipEntry==null) // Should not normally happen throw new IOException(); return zipFile.getInputStream(zipEntry); } // If the underlying AbstractFile doesn't have random read access, use java.util.InputStream to // read the entry. This is much slower than the former method as the file cannot be seeked and needs // to be traversed to locate the entry we're interested in. else { // Optimization: first check if the specified iterator is positionned at the beginning of the entry. // This will typically be the case if an iterator is being used to read all the archive's entries // (unpack operation). In that case, we save the cost of looking for the entry in the archive. if(entryIterator!=null && (entryIterator instanceof JavaUtilZipEntryIterator)) { ArchiveEntry currentEntry = ((JavaUtilZipEntryIterator)entryIterator).getCurrentEntry(); if(currentEntry.getPath().equals(entry.getPath())) { // The entry/zip stream is wrapped in a FilterInputStream where #close is implemented as a no-op: // we don't want the ZipInputStream to be closed when the caller closes the entry's stream. return new FilterInputStream(((JavaUtilZipEntryIterator)entryIterator).getZipInputStream()) { @Override public void close() throws IOException { // No-op } }; } // This is not the one, look for the entry from the beginning of the archive } // Iterate through the archive until we've found the entry java.util.zip.ZipInputStream zin = new java.util.zip.ZipInputStream(file.getInputStream()); java.util.zip.ZipEntry zipEntry; String entryPath = entry.getPath(); // Iterate until we find the entry we're looking for while ((zipEntry=zin.getNextEntry())!=null) if (zipEntry.getName().equals(entryPath)) // That's the one, return it return zin; throw new IOException("Unknown Zip entry: "+entry.getName()); } } ////////////////////////////////////////// // AbstractRWArchiveFile implementation // ////////////////////////////////////////// @Override public synchronized OutputStream addEntry(final ArchiveEntry entry) throws IOException, UnsupportedFileOperationException { checkZipFile(); final ZipEntry zipEntry = createZipEntry(entry); if(zipEntry.isDirectory()) { // Add the new directory entry to the zip file (physically) zipFile.addEntry(zipEntry); // Set the ZipEntry object into the ArchiveEntry entry.setEntryObject(zipEntry); // Declare the zip file and entries tree up-to-date and add the new entry to the entries tree finishAddEntry(entry); return null; } else { // Set the ZipEntry object into the ArchiveEntry entry.setEntryObject(zipEntry); return new FilteredOutputStream(zipFile.addEntry(zipEntry)) { @Override public void close() throws IOException { super.close(); // Declare the zip file and entries tree up-to-date and add the new entry to the entries tree finishAddEntry(entry); } }; } } @Override public synchronized void deleteEntry(ArchiveEntry entry) throws IOException, UnsupportedFileOperationException { ZipEntry zipEntry = (com.mucommander.commons.file.archive.zip.provider.ZipEntry)entry.getEntryObject(); // Most of the time, the ZipEntry will not be null. However, it can be null in some rare cases, when directory // entries have been created in the entries tree but don't exist in the Zip file. // That is the case when a file entry exists in the Zip file but has no directory entry for the parent. if(zipEntry!=null) { // Entry exists physically in the zip file checkZipFile(); // Delete the entry from the zip file (physically) zipFile.deleteEntry(zipEntry); // Remove the ZipEntry object from the AchiveEntry entry.setEntryObject(null); // Declare the zip file and entries tree up-to-date declareZipFileUpToDate(); declareEntriesTreeUpToDate(); } // Else entry doesn't physically exist in the zip file, only in the entries tree // Remove the entry from the entries tree removeFromEntriesTree(entry); } @Override public void updateEntry(ArchiveEntry entry) throws IOException, UnsupportedFileOperationException { ZipEntry zipEntry = (com.mucommander.commons.file.archive.zip.provider.ZipEntry)entry.getEntryObject(); // Most of the time, the ZipEntry will not be null. However, it can be null in some rare cases, when directory // entries have been created in the entries tree but don't exist in the Zip file. // That is the case when a file entry exists in the Zip file but has no directory entry for the parent. if(zipEntry!=null) { // Entry exists physically in the zip file checkZipFile(); zipEntry.setTime(entry.getDate()); zipEntry.setUnixMode(entry.getPermissions().getIntValue()); // Physically update the entry's attributes in the Zip file zipFile.updateEntry(zipEntry); // Declare the zip file and entries tree up-to-date declareZipFileUpToDate(); declareEntriesTreeUpToDate(); } } @Override public synchronized void optimizeArchive() throws IOException, UnsupportedFileOperationException { checkZipFile(); // Defragment the zip file zipFile.defragment(); // Declare the zip file and entries tree up-to-date declareZipFileUpToDate(); declareEntriesTreeUpToDate(); } //////////////////////// // Overridden methods // //////////////////////// /** * Returns <code>true</code> only if the proxied archive file has random read and write access, as reported * by {@link AbstractFile#isFileOperationSupported(FileOperation)}. If that is not the case, this archive has * read-only access and behaves just like a {@link com.mucommander.commons.file.archive.AbstractROArchiveFile}. * * @return true only if the proxied archive file has random read and write access */ @Override public boolean isWritable() { return file.isFileOperationSupported(FileOperation.RANDOM_READ_FILE) && file.isFileOperationSupported(FileOperation.RANDOM_WRITE_FILE); } /** * Creates an empty, valid Zip file. The resulting file is 22 bytes long. */ @Override public void mkfile() throws IOException, UnsupportedFileOperationException { if(exists()) throw new IOException(); copyStream(new ByteArrayInputStream(EMPTY_ZIP_BYTES), false, EMPTY_ZIP_BYTES.length); } }