<NME> ZipUtil.java <BEF> /** * Copyright (C) 2012 ZeroTurnaround LLC <support@zeroturnaround.com> * * Licensed 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.zeroturnaround.zip; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.zeroturnaround.zip.commons.FileUtils; import org.zeroturnaround.zip.commons.FilenameUtils; import org.zeroturnaround.zip.commons.IOUtils; import org.zeroturnaround.zip.transform.ZipEntryTransformer; import org.zeroturnaround.zip.transform.ZipEntryTransformerEntry; /** * ZIP file manipulation utilities. * * @author Rein Raudjärv * @author Innokenty Shuvalov * * @see #containsEntry(File, String) * @see #unpackEntry(File, String) * @see #unpack(File, File) * @see #pack(File, File) */ public final class ZipUtil { private static final String PATH_SEPARATOR = "/"; /** Default compression level */ public static final int DEFAULT_COMPRESSION_LEVEL = Deflater.DEFAULT_COMPRESSION; // Use / instead of . to work around an issue with Maven Shade Plugin private static final Logger log = LoggerFactory.getLogger("org/zeroturnaround/zip/ZipUtil".replace('/', '.')); // NOSONAR private ZipUtil() { } /* Extracting single entries from ZIP files. */ /** * Checks if the ZIP file contains the given entry. * * @param zip * ZIP file. * @param name * entry name. * @return <code>true</code> if the ZIP file contains the given entry. */ public static boolean containsEntry(File zip, String name) { ZipFile zf = null; try { zf = new ZipFile(zip); return zf.getEntry(name) != null; } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Returns the compression method of a given entry of the ZIP file. * * @param zip * ZIP file. * @param name * entry name. * @return Returns <code>ZipEntry.STORED</code>, <code>ZipEntry.DEFLATED</code> or -1 if * the ZIP file does not contain the given entry. * @deprecated The compression level cannot be retrieved. This method exists only to ensure backwards compatibility with ZipUtil version 1.9, which returned the compression * method, not the level. */ @Deprecated public static int getCompressionLevelOfEntry(File zip, String name) { return getCompressionMethodOfEntry(zip, name); } /** * Returns the compression method of a given entry of the ZIP file. * * @param zip * ZIP file. * @param name * entry name. * @return Returns <code>ZipEntry.STORED</code>, <code>ZipEntry.DEFLATED</code> or -1 if * the ZIP file does not contain the given entry. */ public static int getCompressionMethodOfEntry(File zip, String name) { ZipFile zf = null; try { zf = new ZipFile(zip); ZipEntry zipEntry = zf.getEntry(name); if (zipEntry == null) { return -1; } return zipEntry.getMethod(); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Checks if the ZIP file contains any of the given entries. * * @param zip * ZIP file. * @param names * entry names. * @return <code>true</code> if the ZIP file contains any of the given * entries. */ public static boolean containsAnyEntry(File zip, String[] names) { ZipFile zf = null; try { zf = new ZipFile(zip); for (int i = 0; i < names.length; i++) { if (zf.getEntry(names[i]) != null) { return true; } } return false; } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Unpacks a single entry from a ZIP file. * * @param zip * ZIP file. * @param name * entry name. * @return contents of the entry or <code>null</code> if it was not found. */ public static byte[] unpackEntry(File zip, String name) { ZipFile zf = null; try { zf = new ZipFile(zip); return doUnpackEntry(zf, name); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Unpacks a single entry from a ZIP file. * * @param zip * ZIP file. * @param name * entry name. * * @param charset * charset to be used to process the zip * * @return contents of the entry or <code>null</code> if it was not found. */ public static byte[] unpackEntry(File zip, String name, Charset charset) { ZipFile zf = null; try { if (charset != null) { zf = new ZipFile(zip, charset); } else { zf = new ZipFile(zip); } return doUnpackEntry(zf, name); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Unpacks a single entry from a ZIP file. * * @param zf * ZIP file. * @param name * entry name. * @return contents of the entry or <code>null</code> if it was not found. */ public static byte[] unpackEntry(ZipFile zf, String name) { try { return doUnpackEntry(zf, name); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /** * Unpacks a single entry from a ZIP file. * * @param zf * ZIP file. * @param name * entry name. * @return contents of the entry or <code>null</code> if it was not found. */ private static byte[] doUnpackEntry(ZipFile zf, String name) throws IOException { ZipEntry ze = zf.getEntry(name); if (ze == null) { return null; // entry not found } InputStream is = zf.getInputStream(ze); try { return IOUtils.toByteArray(is); } finally { IOUtils.closeQuietly(is); } } /** * Unpacks a single entry from a ZIP stream. * * @param is * ZIP stream. * @param name * entry name. * @return contents of the entry or <code>null</code> if it was not found. */ public static byte[] unpackEntry(InputStream is, String name) { ByteArrayUnpacker action = new ByteArrayUnpacker(); if (!handle(is, name, action)) return null; // entry not found return action.getBytes(); } /** * Copies an entry into a byte array. * * @author Rein Raudjärv */ private static class ByteArrayUnpacker implements ZipEntryCallback { private byte[] bytes; public void process(InputStream in, ZipEntry zipEntry) throws IOException { bytes = IOUtils.toByteArray(in); } public byte[] getBytes() { return bytes; } } /** * Unpacks a single file from a ZIP archive to a file. * * @param zip * ZIP file. * @param name * entry name. * @param file * target file to be created or overwritten. * @return <code>true</code> if the entry was found and unpacked, * <code>false</code> if the entry was not found. */ public static boolean unpackEntry(File zip, String name, File file) { return unpackEntry(zip, name, file, null); } /** * Unpacks a single file from a ZIP archive to a file. * * @param zip * ZIP file. * @param name * entry name. * @param file * target file to be created or overwritten. * @param charset * charset to be used processing the zip * * @return <code>true</code> if the entry was found and unpacked, * <code>false</code> if the entry was not found. */ public static boolean unpackEntry(File zip, String name, File file, Charset charset) { ZipFile zf = null; try { if (charset != null) { zf = new ZipFile(zip, charset); } else { zf = new ZipFile(zip); } return doUnpackEntry(zf, name, file); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Unpacks a single file from a ZIP archive to a file. * * @param zf * ZIP file. * @param name * entry name. * @param file * target file to be created or overwritten. * @return <code>true</code> if the entry was found and unpacked, * <code>false</code> if the entry was not found. */ public static boolean unpackEntry(ZipFile zf, String name, File file) { try { return doUnpackEntry(zf, name, file); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /** * Unpacks a single file from a ZIP archive to a file. * * @param zf * ZIP file. * @param name * entry name. * @param file * target file to be created or overwritten. * @return <code>true</code> if the entry was found and unpacked, * <code>false</code> if the entry was not found. */ private static boolean doUnpackEntry(ZipFile zf, String name, File file) throws IOException { if (log.isTraceEnabled()) { log.trace("Extracting '" + zf.getName() + "' entry '" + name + "' into '" + file + "'."); } ZipEntry ze = zf.getEntry(name); if (ze == null) { return false; // entry not found } if (ze.isDirectory() || zf.getInputStream(ze) == null) { if (file.isDirectory()) { return true; } if (file.exists()) { FileUtils.forceDelete(file); } return file.mkdirs(); } InputStream in = new BufferedInputStream(zf.getInputStream(ze)); try { FileUtils.copy(in, file); } finally { IOUtils.closeQuietly(in); } return true; } /** * Unpacks a single file from a ZIP stream to a file. * * @param is * ZIP stream. * @param name * entry name. * @param file * target file to be created or overwritten. * @return <code>true</code> if the entry was found and unpacked, * <code>false</code> if the entry was not found. * @throws java.io.IOException if file is not found or writing to it fails */ public static boolean unpackEntry(InputStream is, String name, File file) throws IOException { return handle(is, name, new FileUnpacker(file)); } /** * Copies an entry into a File. * * @author Rein Raudjärv */ private static class FileUnpacker implements ZipEntryCallback { private final File file; public FileUnpacker(File file) { this.file = file; } public void process(InputStream in, ZipEntry zipEntry) throws IOException { FileUtils.copy(in, file); } } /* Traversing ZIP files */ /** * Reads the given ZIP file and executes the given action for each entry. * <p> * For each entry the corresponding input stream is also passed to the action. If you want to stop the loop * then throw a ZipBreakException. * * @param zip * input ZIP file. * @param action * action to be called for each entry. * * @see ZipEntryCallback * @see #iterate(File, ZipInfoCallback) */ public static void iterate(File zip, ZipEntryCallback action) { iterate(zip, action, null); } /** * Reads the given ZIP file and executes the given action for each entry. * <p> * For each entry the corresponding input stream is also passed to the action. If you want to stop the loop * then throw a ZipBreakException. * * @param zip * input ZIP file. * @param action * action to be called for each entry. * * @param charset * Charset used to processed the ZipFile with * * @see ZipEntryCallback * @see #iterate(File, ZipInfoCallback) */ public static void iterate(File zip, ZipEntryCallback action, Charset charset) { ZipFile zf = null; try { if (charset == null) { zf = new ZipFile(zip); } else { zf = new ZipFile(zip, charset); } Enumeration<? extends ZipEntry> en = zf.entries(); while (en.hasMoreElements()) { ZipEntry e = (ZipEntry) en.nextElement(); InputStream is = zf.getInputStream(e); try { action.process(is, e); } catch (IOException ze) { throw new ZipException("Failed to process zip entry '" + e.getName() + "' with action " + action, ze); } catch (ZipBreakException ex) { break; } finally { IOUtils.closeQuietly(is); } } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Reads the given ZIP file and executes the given action for each given entry. * <p> * For each given entry the corresponding input stream is also passed to the action. If you want to stop the loop then throw a ZipBreakException. * * @param zip * input ZIP file. * @param entryNames * names of entries to iterate * @param action * action to be called for each entry. * * @see ZipEntryCallback * @see #iterate(File, String[], ZipInfoCallback) */ public static void iterate(File zip, String[] entryNames, ZipEntryCallback action) { iterate(zip, entryNames, action, null); } /** * Reads the given ZIP file and executes the given action for each given entry. * <p> * For each given entry the corresponding input stream is also passed to the action. If you want to stop the loop then throw a ZipBreakException. * * @param zip * input ZIP file. * @param entryNames * names of entries to iterate * @param action * action to be called for each entry. * @param charset * charset used to process the zip file * * @see ZipEntryCallback * @see #iterate(File, String[], ZipInfoCallback) */ public static void iterate(File zip, String[] entryNames, ZipEntryCallback action, Charset charset) { ZipFile zf = null; try { if (charset == null) { zf = new ZipFile(zip); } else { zf = new ZipFile(zip, charset); } for (int i = 0; i < entryNames.length; i++) { ZipEntry e = zf.getEntry(entryNames[i]); if (e == null) { continue; } InputStream is = zf.getInputStream(e); try { action.process(is, e); } catch (IOException ze) { throw new ZipException("Failed to process zip entry '" + e.getName() + " with action " + action, ze); } catch (ZipBreakException ex) { break; } finally { IOUtils.closeQuietly(is); } } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Scans the given ZIP file and executes the given action for each entry. * <p> * Only the meta-data without the actual data is read. If you want to stop the loop * then throw a ZipBreakException. * * @param zip * input ZIP file. * @param action * action to be called for each entry. * * @see ZipInfoCallback * @see #iterate(File, ZipEntryCallback) */ public static void iterate(File zip, ZipInfoCallback action) { ZipFile zf = null; try { zf = new ZipFile(zip); Enumeration<? extends ZipEntry> en = zf.entries(); while (en.hasMoreElements()) { ZipEntry e = (ZipEntry) en.nextElement(); try { action.process(e); } catch (IOException ze) { throw new ZipException("Failed to process zip entry '" + e.getName() + " with action " + action, ze); } catch (ZipBreakException ex) { break; } } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Scans the given ZIP file and executes the given action for each given entry. * <p> * Only the meta-data without the actual data is read. If you want to stop the loop then throw a ZipBreakException. * * @param zip * input ZIP file. * @param entryNames * names of entries to iterate * @param action * action to be called for each entry. * * @see ZipInfoCallback * @see #iterate(File, String[], ZipEntryCallback) */ public static void iterate(File zip, String[] entryNames, ZipInfoCallback action) { ZipFile zf = null; try { zf = new ZipFile(zip); for (int i = 0; i < entryNames.length; i++) { ZipEntry e = zf.getEntry(entryNames[i]); if (e == null) { continue; } try { action.process(e); } catch (IOException ze) { throw new ZipException("Failed to process zip entry '" + e.getName() + " with action " + action, ze); } catch (ZipBreakException ex) { break; } } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Reads the given ZIP stream and executes the given action for each entry. * <p> * For each entry the corresponding input stream is also passed to the action. If you want to stop the loop * then throw a ZipBreakException. * * @param is * input ZIP stream (it will not be closed automatically). * @param action * action to be called for each entry. * @param charset * charset to process entries in * * @see ZipEntryCallback * @see #iterate(File, ZipEntryCallback) */ public static void iterate(InputStream is, ZipEntryCallback action, Charset charset) { try { ZipInputStream in = null; try { in = newCloseShieldZipInputStream(is, charset); ZipEntry entry; while ((entry = in.getNextEntry()) != null) { try { action.process(in, entry); } catch (IOException ze) { throw new ZipException("Failed to process zip entry '" + entry.getName() + " with action " + action, ze); } catch (ZipBreakException ex) { break; } } } finally { if (in != null) { in.close(); } } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /** * See {@link #iterate(InputStream, ZipEntryCallback, Charset)}. This method * is a shorthand for a version where no Charset is specified. * * @param is * input ZIP stream (it will not be closed automatically). * @param action * action to be called for each entry. * * @see ZipEntryCallback * @see #iterate(File, ZipEntryCallback) */ public static void iterate(InputStream is, ZipEntryCallback action) { iterate(is, action, null); } /** * Reads the given ZIP stream and executes the given action for each given entry. * <p> * For each given entry the corresponding input stream is also passed to the action. If you want to stop the loop then throw a ZipBreakException. * * @param is * input ZIP stream (it will not be closed automatically). * @param entryNames * names of entries to iterate * @param action * action to be called for each entry. * @param charset * charset to process entries in * * @see ZipEntryCallback * @see #iterate(File, String[], ZipEntryCallback) */ public static void iterate(InputStream is, String[] entryNames, ZipEntryCallback action, Charset charset) { Set<String> namesSet = new HashSet<String>(); for (int i = 0; i < entryNames.length; i++) { namesSet.add(entryNames[i]); } try { ZipInputStream in = null; try { in = newCloseShieldZipInputStream(is, charset); ZipEntry entry; while ((entry = in.getNextEntry()) != null) { if (!namesSet.contains(entry.getName())) { // skip the unnecessary entry continue; } try { action.process(in, entry); } catch (IOException ze) { throw new ZipException("Failed to process zip entry '" + entry.getName() + " with action " + action, ze); } catch (ZipBreakException ex) { break; } } } finally { if (in != null) { in.close(); } } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /** * See @link{ {@link #iterate(InputStream, ZipEntryCallback, Charset)}. It is a * shorthand where no Charset is specified. * * @param is * input ZIP stream (it will not be closed automatically). * @param entryNames * names of entries to iterate * @param action * action to be called for each entry. * * @see ZipEntryCallback * @see #iterate(File, String[], ZipEntryCallback) */ public static void iterate(InputStream is, String[] entryNames, ZipEntryCallback action) { iterate(is, entryNames, action, null); } /** * Creates a new {@link ZipInputStream} based on the given {@link InputStream}. It will be buffered and close-shielded. * Closing the result stream flushes the buffers and frees up resources of the {@link ZipInputStream}. However the source stream itself remains open. */ private static ZipInputStream newCloseShieldZipInputStream(final InputStream is, Charset charset) { InputStream in = new BufferedInputStream(new CloseShieldInputStream(is)); if (charset == null) { return new ZipInputStream(in); } return ZipFileUtil.createZipInputStream(in, charset); } /** * Reads the given ZIP file and executes the given action for a single entry. * * @param zip * input ZIP file. * @param name * entry name. * @param action * action to be called for this entry. * @return <code>true</code> if the entry was found, <code>false</code> if the * entry was not found. * * @see ZipEntryCallback */ public static boolean handle(File zip, String name, ZipEntryCallback action) { ZipFile zf = null; try { zf = new ZipFile(zip); ZipEntry ze = zf.getEntry(name); if (ze == null) { return false; // entry not found } InputStream in = new BufferedInputStream(zf.getInputStream(ze)); try { action.process(in, ze); } finally { IOUtils.closeQuietly(in); } return true; } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { closeQuietly(zf); } } /** * Reads the given ZIP stream and executes the given action for a single * entry. * * @param is * input ZIP stream (it will not be closed automatically). * @param name * entry name. * @param action * action to be called for this entry. * @return <code>true</code> if the entry was found, <code>false</code> if the * entry was not found. * * @see ZipEntryCallback */ public static boolean handle(InputStream is, String name, ZipEntryCallback action) { SingleZipEntryCallback helper = new SingleZipEntryCallback(name, action); iterate(is, helper); return helper.found(); } /** * ZipEntryCallback which is only applied to single entry. * * @author Rein Raudjärv */ private static class SingleZipEntryCallback implements ZipEntryCallback { private final String name; private final ZipEntryCallback action; private boolean found; public SingleZipEntryCallback(String name, ZipEntryCallback action) { this.name = name; this.action = action; } public void process(InputStream in, ZipEntry zipEntry) throws IOException { if (name.equals(zipEntry.getName())) { found = true; action.process(in, zipEntry); } } public boolean found() { return found; } } /* Extracting whole ZIP files. */ /** * Unpacks a ZIP file to the given directory. * <p> * The output directory must not be a file. * * @param zip * input ZIP file. * @param outputDir * output directory (created automatically if not found). */ public static void unpack(File zip, final File outputDir) { unpack(zip, outputDir, IdentityNameMapper.INSTANCE); } /** * Unpacks a ZIP file to the given directory using a specific Charset * for the input file. * * <p> * The output directory must not be a file. * * @param zip * input ZIP file. * @param outputDir * output directory (created automatically if not found). * * @param charset * charset used to unpack the zip file */ public static void unpack(File zip, final File outputDir, Charset charset) { unpack(zip, outputDir, IdentityNameMapper.INSTANCE, charset); } /** * Unpacks a ZIP file to the given directory. * <p> * The output directory must not be a file. * * @param zip * input ZIP file. * @param outputDir * output directory (created automatically if not found). * @param mapper * call-back for renaming the entries. * @param charset * charset used to process the zip file */ public static void unpack(File zip, File outputDir, NameMapper mapper, Charset charset) { log.debug("Extracting '{}' into '{}'.", zip, outputDir); iterate(zip, new Unpacker(outputDir, mapper), charset); } /** * Unpacks a ZIP file to the given directory using a specific Charset * for the input file. * * <p> * The output directory must not be a file. * * @param zip * input ZIP file. * @param outputDir * output directory (created automatically if not found). * @param mapper * call-back for renaming the entries. */ public static void unpack(File zip, File outputDir, NameMapper mapper) { log.debug("Extracting '{}' into '{}'.", zip, outputDir); iterate(zip, new Unpacker(outputDir, mapper)); } /** * Unwraps a ZIP file to the given directory shaving of root dir. * If there are multiple root dirs or entries in the root of zip, * ZipException is thrown. * <p> * The output directory must not be a file. * * @param zip * input ZIP file. * @param outputDir * output directory (created automatically if not found). */ public static void unwrap(File zip, final File outputDir) { unwrap(zip, outputDir, IdentityNameMapper.INSTANCE); } /** * Unwraps a ZIP file to the given directory shaving of root dir. * If there are multiple root dirs or entries in the root of zip, * ZipException is thrown. * <p> * The output directory must not be a file. * * @param zip * input ZIP file. * @param outputDir * output directory (created automatically if not found). * @param mapper * call-back for renaming the entries. */ public static void unwrap(File zip, File outputDir, NameMapper mapper) { log.debug("Unwrapping '{}' into '{}'.", zip, outputDir); iterate(zip, new Unwrapper(outputDir, mapper)); } /** * Unpacks a ZIP stream to the given directory. * <p> * The output directory must not be a file. * * @param is * inputstream for ZIP file. * @param outputDir * output directory (created automatically if not found). */ public static void unpack(InputStream is, File outputDir) { unpack(is, outputDir, IdentityNameMapper.INSTANCE, null); } /** * Unpacks a ZIP stream to the given directory. * <p> * The output directory must not be a file. * * @param is * inputstream for ZIP file. * @param outputDir * output directory (created automatically if not found). * @param charset * charset used to process the zip stream */ public static void unpack(InputStream is, File outputDir, Charset charset) { unpack(is, outputDir, IdentityNameMapper.INSTANCE, charset); } /** * Unpacks a ZIP stream to the given directory. * <p> * The output directory must not be a file. * * @param is * inputstream for ZIP file. * @param outputDir * output directory (created automatically if not found). * @param mapper * call-back for renaming the entries. */ public static void unpack(InputStream is, File outputDir, NameMapper mapper) { unpack(is, outputDir, mapper, null); } /** * Unpacks a ZIP stream to the given directory. * <p> * The output directory must not be a file. * * @param is * inputstream for ZIP file. * @param outputDir * output directory (created automatically if not found). * @param mapper * call-back for renaming the entries. * @param charset * charset to use when unpacking the stream */ public static void unpack(InputStream is, File outputDir, NameMapper mapper, Charset charset) { log.debug("Extracting {} into '{}'.", is, outputDir); iterate(is, new Unpacker(outputDir, mapper), charset); } /** * Unwraps a ZIP file to the given directory shaving of root dir. * If there are multiple root dirs or entries in the root of zip, * ZipException is thrown. * <p> * The output directory must not be a file. * * @param is * inputstream for ZIP file. * @param outputDir * output directory (created automatically if not found). */ public static void unwrap(InputStream is, File outputDir) { unwrap(is, outputDir, IdentityNameMapper.INSTANCE); } /** * Unwraps a ZIP file to the given directory shaving of root dir. * If there are multiple root dirs or entries in the root of zip, * ZipException is thrown. * <p> * The output directory must not be a file. * * @param is * inputstream for ZIP file. * @param outputDir * output directory (created automatically if not found). * @param mapper * call-back for renaming the entries. */ public static void unwrap(InputStream is, File outputDir, NameMapper mapper) { log.debug("Unwrapping {} into '{}'.", is, outputDir); iterate(is, new Unwrapper(outputDir, mapper)); } private static File makeDestinationFile(File outputDir, String name) throws IOException { return checkDestinationFileForTraversal(outputDir, name, new File(outputDir, name)); } private static File checkDestinationFileForTraversal(File outputDir, String name, File destFile) throws IOException { /* If we see the relative traversal string of ".." we need to make sure * that the outputdir + name doesn't leave the outputdir. See * DirectoryTraversalMaliciousTest for details. */ if (name.indexOf("..") != -1 && !destFile.getCanonicalPath().startsWith(outputDir.getCanonicalPath())) { throw new MaliciousZipException(outputDir, name); } return destFile; } /** * Unpacks each ZIP entry. * * @author Rein Raudjärv */ private static class Unpacker implements ZipEntryCallback { private final File outputDir; private final NameMapper mapper; public Unpacker(File outputDir, NameMapper mapper) { this.outputDir = outputDir; this.mapper = mapper; } public void process(InputStream in, ZipEntry zipEntry) throws IOException { String name = mapper.map(zipEntry.getName()); if (name != null) { File file = makeDestinationFile(outputDir, name); if (zipEntry.isDirectory()) { FileUtils.forceMkdir(file); } else { FileUtils.forceMkdir(file.getParentFile()); if (log.isDebugEnabled() && file.exists()) { log.debug("Overwriting file '{}'.", zipEntry.getName()); } FileUtils.copy(in, file); } try { ZTFilePermissions permissions = ZipEntryUtil.getZTFilePermissions(zipEntry); if (permissions != null) { ZTFilePermissionsUtil.getDefaultStategy().setPermissions(file, permissions); } } catch (ZipException e) { /* * We are not bubbling this exception because there is a case where yes * the file has problems with permissions and there is a block length that * does not match but the other tooling like zip, jar and other just unpacks * the files and does not use the permission information. * */ } } } } /** * Unpacks each ZIP entries. Presumes they are packed with the backslash separator. * Some archives can have this problem if they are created with some software * that is not following the ZIP specification. * * @since zt-zip 1.9 */ public static class BackslashUnpacker implements ZipEntryCallback { private final File outputDir; private final NameMapper mapper; public BackslashUnpacker(File outputDir, NameMapper mapper) { this.outputDir = outputDir; this.mapper = mapper; } public BackslashUnpacker(File outputDir) { this(outputDir, IdentityNameMapper.INSTANCE); } public void process(InputStream in, ZipEntry zipEntry) throws IOException { String name = mapper.map(zipEntry.getName()); if (name != null) { /** * We assume that EVERY backslash will denote a directory * separator. Also such broken archives don't have entries that * are just directories. Everything is a file. See the example * * Archive: backSlashTest.zip * testing: testDirectory\testfileInTestDirectory.txt OK * testing: testDirectory\testSubdirectory\testFileInTestSubdirectory.txt OK * No errors detected in compressed data of backSlashTest.zip. */ if (name.indexOf('\\') != -1) { File parentDirectory = outputDir; String[] dirs = name.split("\\\\"); // lets create all the directories and the last entry is the file as EVERY entry is a file for (int i = 0; i < dirs.length - 1; i++) { File file = new File(parentDirectory, dirs[i]); if (!file.exists()) { FileUtils.forceMkdir(file); } parentDirectory = file; } File destFile = checkDestinationFileForTraversal(outputDir, name, new File(parentDirectory, dirs[dirs.length - 1])); FileUtils.copy(in, destFile); } // it could be that there are just top level files that the unpacker is used for else { File destFile = makeDestinationFile(outputDir, name); FileUtils.copy(in, destFile); } } } } /** * Unwraps entries excluding a single parent dir. If there are multiple roots * ZipException is thrown. * * @author Oleg Shelajev */ private static class Unwrapper implements ZipEntryCallback { private final File outputDir; private final NameMapper mapper; private String rootDir; public Unwrapper(File outputDir, NameMapper mapper) { this.outputDir = outputDir; this.mapper = mapper; } public void process(InputStream in, ZipEntry zipEntry) throws IOException { String root = getRootName(zipEntry.getName()); if (rootDir == null) { rootDir = root; } else if (!rootDir.equals(root)) { throw new ZipException("Unwrapping with multiple roots is not supported, roots: " + rootDir + ", " + root); } String name = mapper.map(getUnrootedName(root, zipEntry.getName())); if (name != null) { File file = makeDestinationFile(outputDir, name); if (zipEntry.isDirectory()) { FileUtils.forceMkdir(file); } else { FileUtils.forceMkdir(file.getParentFile()); if (log.isDebugEnabled() && file.exists()) { log.debug("Overwriting file '{}'.", zipEntry.getName()); } FileUtils.copy(in, file); } } } private String getUnrootedName(String root, String name) { return name.substring(root.length()); } private String getRootName(final String name) { String newName = name.substring(FilenameUtils.getPrefixLength(name)); int idx = newName.indexOf(PATH_SEPARATOR); if (idx < 0) { throw new ZipException("Entry " + newName + " from the root of the zip is not supported"); } return newName.substring(0, newName.indexOf(PATH_SEPARATOR)); } } /** * Unpacks a ZIP file to its own location. * <p> * The ZIP file will be first renamed (using a temporary name). After the * extraction it will be deleted. * * @param zip * input ZIP file as well as the target directory. * * @see #unpack(File, File) */ public static void explode(File zip) { try { // Find a new unique name is the same directory File tempFile = FileUtils.getTempFileFor(zip); // Rename the archive FileUtils.moveFile(zip, tempFile); // Unpack it unpack(tempFile, zip); // Delete the archive if (!tempFile.delete()) { throw new IOException("Unable to delete file: " + tempFile); } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /* Compressing single entries to ZIP files. */ /** * Compresses the given file into a ZIP file with single entry. * * @param file file to be compressed. * @return ZIP file created. */ public static byte[] packEntry(File file) { log.trace("Compressing '{}' into a ZIP file with single entry.", file); ByteArrayOutputStream result = new ByteArrayOutputStream(); try { ZipOutputStream out = new ZipOutputStream(result); ZipEntry entry = ZipEntryUtil.fromFile(file.getName(), file); InputStream in = new BufferedInputStream(new FileInputStream(file)); try { ZipEntryUtil.addEntry(entry, in, out); } finally { IOUtils.closeQuietly(in); } out.close(); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } return result.toByteArray(); } /* Compressing ZIP files. */ /** * Compresses the given directory and all its sub-directories into a ZIP file. * <p> * The ZIP file must not be a directory and its parent directory must exist. * Will not include the root directory name in the archive. * * @param rootDir * root directory. * @param zip * ZIP file that will be created or overwritten. */ public static void pack(File rootDir, File zip) { pack(rootDir, zip, DEFAULT_COMPRESSION_LEVEL); } /** * Compresses the given directory and all its sub-directories into a ZIP file. * <p> * The ZIP file must not be a directory and its parent directory must exist. * Will not include the root directory name in the archive. * * @param rootDir * root directory. * @param zip * ZIP file that will be created or overwritten. * @param compressionLevel * compression level */ public static void pack(File rootDir, File zip, int compressionLevel) { pack(rootDir, zip, IdentityNameMapper.INSTANCE, compressionLevel); } /** * Compresses the given directory and all its sub-directories into a ZIP file. * <p> * The ZIP file must not be a directory and its parent directory must exist. * Will not include the root directory name in the archive. * * @param sourceDir * root directory. * @param targetZipFile * ZIP file that will be created or overwritten. * @param preserveRoot * true if the resulted archive should have the top directory entry */ public static void pack(final File sourceDir, final File targetZipFile, final boolean preserveRoot) { if (preserveRoot) { final String parentName = sourceDir.getName(); pack(sourceDir, targetZipFile, new NameMapper() { public String map(String name) { return parentName + PATH_SEPARATOR + name; } }); } else { pack(sourceDir, targetZipFile); } } /** * Compresses the given file into a ZIP file. * <p> * The ZIP file must not be a directory and its parent directory must exist. * * @param fileToPack * file that needs to be zipped. * @param destZipFile * ZIP file that will be created or overwritten. */ public static void packEntry(File fileToPack, File destZipFile) { packEntry(fileToPack, destZipFile, IdentityNameMapper.INSTANCE); } /** * Compresses the given file into a ZIP file. * <p> * The ZIP file must not be a directory and its parent directory must exist. * * @param fileToPack * file that needs to be zipped. * @param destZipFile * ZIP file that will be created or overwritten. * @param fileName * the name for the file inside the archive */ public static void packEntry(File fileToPack, File destZipFile, final String fileName) { packEntry(fileToPack, destZipFile, new NameMapper() { public String map(String name) { return fileName; } }); } /** * Compresses the given file into a ZIP file. * <p> * The ZIP file must not be a directory and its parent directory must exist. * * @param fileToPack * file that needs to be zipped. * @param destZipFile * ZIP file that will be created or overwritten. * @param mapper * call-back for renaming the entries. */ public static void packEntry(File fileToPack, File destZipFile, NameMapper mapper) { packEntries(new File[] { fileToPack }, destZipFile, mapper); } /** * Compresses the given files into a ZIP file. * <p> * The ZIP file must not be a directory and its parent directory must exist. * * @param filesToPack * files that needs to be zipped. * @param destZipFile * ZIP file that will be created or overwritten. */ public static void packEntries(File[] filesToPack, File destZipFile) { packEntries(filesToPack, destZipFile, IdentityNameMapper.INSTANCE); } /** * Compresses the given files into a ZIP file. * <p> * The ZIP file must not be a directory and its parent directory must exist. * * @param filesToPack * files that needs to be zipped. * @param destZipFile * ZIP file that will be created or overwritten. * @param mapper * call-back for renaming the entries. */ public static void packEntries(File[] filesToPack, File destZipFile, NameMapper mapper) { packEntries(filesToPack, destZipFile, mapper, DEFAULT_COMPRESSION_LEVEL); } /** * Compresses the given files into a ZIP file. * <p> * The ZIP file must not be a directory and its parent directory must exist. * * @param filesToPack * files that needs to be zipped. * @param destZipFile * ZIP file that will be created or overwritten. * @param compressionLevel * ZIP file compression level (speed versus filesize), e.g. <code>Deflater.NO_COMPRESSION</code>, <code>Deflater.BEST_SPEED</code>, or * <code>Deflater.BEST_COMPRESSION</code> */ public static void packEntries(File[] filesToPack, File destZipFile, int compressionLevel) { packEntries(filesToPack, destZipFile, IdentityNameMapper.INSTANCE, compressionLevel); } /** * Compresses the given files into a ZIP file. * <p> * The ZIP file must not be a directory and its parent directory must exist. * * @param filesToPack * files that needs to be zipped. * @param destZipFile * ZIP file that will be created or overwritten. * @param mapper * call-back for renaming the entries. * @param compressionLevel * ZIP file compression level (speed versus filesize), e.g. <code>Deflater.NO_COMPRESSION</code>, <code>Deflater.BEST_SPEED</code>, or * <code>Deflater.BEST_COMPRESSION</code> */ public static void packEntries(File[] filesToPack, File destZipFile, NameMapper mapper, int compressionLevel) { log.debug("Compressing '{}' into '{}'.", filesToPack, destZipFile); ZipOutputStream out = null; FileOutputStream fos = null; try { fos = new FileOutputStream(destZipFile); out = new ZipOutputStream(new BufferedOutputStream(fos)); out.setLevel(compressionLevel); for (int i = 0; i < filesToPack.length; i++) { File fileToPack = filesToPack[i]; ZipEntry zipEntry = ZipEntryUtil.fromFile(mapper.map(fileToPack.getName()), fileToPack); out.putNextEntry(zipEntry); FileUtils.copy(fileToPack, out); out.closeEntry(); } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { IOUtils.closeQuietly(out); IOUtils.closeQuietly(fos); } } /** * Compresses the given directory and all its sub-directories into a ZIP file. * <p> * The ZIP file must not be a directory and its parent directory must exist. * * @param sourceDir * root directory. * @param targetZip * ZIP file that will be created or overwritten. * @param mapper * call-back for renaming the entries. */ public static void pack(File sourceDir, File targetZip, NameMapper mapper) { pack(sourceDir, targetZip, mapper, DEFAULT_COMPRESSION_LEVEL); } /** * Compresses the given directory and all its sub-directories into a ZIP file. * <p> * The ZIP file must not be a directory and its parent directory must exist. * * @param sourceDir * root directory. * @param targetZip * ZIP file that will be created or overwritten. * @param mapper * call-back for renaming the entries. * @param compressionLevel * compression level */ public static void pack(File sourceDir, File targetZip, NameMapper mapper, int compressionLevel) { log.debug("Compressing '{}' into '{}'.", sourceDir, targetZip); if (!sourceDir.exists()) { throw new ZipException("Given file '" + sourceDir + "' doesn't exist!"); } ZipOutputStream out = null; try { out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(targetZip))); out.setLevel(compressionLevel); pack(sourceDir, out, mapper, "", true); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { IOUtils.closeQuietly(out); } } /** * Compresses the given directory and all of its sub-directories into the passed in * stream. It is the responsibility of the caller to close the passed in * stream properly. * * @param sourceDir * root directory. * @param os * output stream (will be buffered in this method). * * @since 1.10 */ public static void pack(File sourceDir, OutputStream os) { pack(sourceDir, os, IdentityNameMapper.INSTANCE, DEFAULT_COMPRESSION_LEVEL); } /** * Compresses the given directory and all of its sub-directories into the passed in * stream. It is the responsibility of the caller to close the passed in for (int i = 0; i < entries.length; i++) { addEntry(entries[i], out); } } catch (IOException e) { ZipExceptionUtil.rethrow(e); * compression level * * @since 1.10 */ public static void pack(File sourceDir, OutputStream os, int compressionLevel) { pack(sourceDir, os, IdentityNameMapper.INSTANCE, compressionLevel); } /** * Compresses the given directory and all of its sub-directories into the passed in * stream. It is the responsibility of the caller to close the passed in * stream properly. * * @param sourceDir * root directory. * @param os * output stream (will be buffered in this method). * @param mapper * call-back for renaming the entries. * * @since 1.10 */ public static void pack(File sourceDir, OutputStream os, NameMapper mapper) { pack(sourceDir, os, mapper, DEFAULT_COMPRESSION_LEVEL); } /** * Compresses the given directory and all of its sub-directories into the passed in * stream. It is the responsibility of the caller to close the passed in * stream properly. * * @param sourceDir * root directory. * @param os * output stream (will be buffered in this method). * @param mapper * call-back for renaming the entries. * @param compressionLevel * compression level * * @since 1.10 */ public static void pack(File sourceDir, OutputStream os, NameMapper mapper, int compressionLevel) { log.debug("Compressing '{}' into a stream.", sourceDir); if (!sourceDir.exists()) { throw new ZipException("Given file '" + sourceDir + "' doesn't exist!"); } ZipOutputStream out = null; IOException error = null; try { out = new ZipOutputStream(new BufferedOutputStream(os)); out.setLevel(compressionLevel); pack(sourceDir, out, mapper, "", true); } catch (IOException e) { error = e; } finally { if (out != null && error == null) { try { out.finish(); out.flush(); } catch (IOException e) { error = e; } } } if (error != null) { throw ZipExceptionUtil.rethrow(error); } } /** * Compresses the given directory and all its sub-directories into a ZIP file. * * @param dir * root directory. * @param out * ZIP output stream. * @param mapper * call-back for renaming the entries. * @param pathPrefix * prefix to be used for the entries. * @param mustHaveChildren * if true, but directory to pack doesn't have any files, throw an exception. */ private static void pack(File dir, ZipOutputStream out, NameMapper mapper, String pathPrefix, boolean mustHaveChildren) throws IOException { String[] filenames = dir.list(); if (filenames == null) { if (!dir.exists()) { throw new ZipException("Given file '" + dir + "' doesn't exist!"); } throw new IOException("Given file is not a directory '" + dir + "'"); } if (mustHaveChildren && filenames.length == 0) { throw new ZipException("Given directory '" + dir + "' doesn't contain any files!"); } for (int i = 0; i < filenames.length; i++) { String filename = filenames[i]; File file = new File(dir, filename); boolean isDir = file.isDirectory(); String path = pathPrefix + file.getName(); // NOSONAR if (isDir) { path += PATH_SEPARATOR; // NOSONAR } // Create a ZIP entry String name = mapper.map(path); if (name != null) { ZipEntry zipEntry = ZipEntryUtil.fromFile(name, file); out.putNextEntry(zipEntry); // Copy the file content if (!isDir) { FileUtils.copy(file, out); } out.closeEntry(); } // Traverse the directory if (isDir) { pack(file, out, mapper, path, false); } } } /** * Repacks a provided ZIP file into a new ZIP with a given compression level. * <p> * * @param srcZip * source ZIP file. * @param dstZip * destination ZIP file. * @param compressionLevel * compression level. */ public static void repack(File srcZip, File dstZip, int compressionLevel) { log.debug("Repacking '{}' into '{}'.", srcZip, dstZip); RepackZipEntryCallback callback = new RepackZipEntryCallback(dstZip, compressionLevel); try { iterate(srcZip, callback); } finally { callback.closeStream(); } } /** * Repacks a provided ZIP input stream into a ZIP file with a given compression level. * <p> * * @param is * ZIP input stream. * @param dstZip * destination ZIP file. * @param compressionLevel * compression level. */ public static void repack(InputStream is, File dstZip, int compressionLevel) { log.debug("Repacking from input stream into '{}'.", dstZip); RepackZipEntryCallback callback = new RepackZipEntryCallback(dstZip, compressionLevel); try { iterate(is, callback); } finally { callback.closeStream(); } } /** * Repacks a provided ZIP file and replaces old file with the new one. * <p> * * @param zip * source ZIP file to be repacked and replaced. * @param compressionLevel * compression level. */ public static void repack(File zip, int compressionLevel) { try { File tmpZip = FileUtils.getTempFileFor(zip); repack(zip, tmpZip, compressionLevel); // Delete original zip if (!zip.delete()) { throw new IOException("Unable to delete the file: " + zip); } // Rename the archive FileUtils.moveFile(tmpZip, zip); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /** * Creates an empty ZIP archive at the location of the provided file. * * @param file the file to become an empty ZIP archive */ public static void createEmpty(File file) { // https://en.wikipedia.org/wiki/Zip_(file_format)#Limits FileOutputStream fos = null; try { fos = new FileOutputStream(file); fos.write(new byte[] {0x50,0x4B,0x05,0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { IOUtils.closeQuietly(fos); } } /** * RepackZipEntryCallback used in repacking methods. * * @author Pavel Grigorenko */ private static final class RepackZipEntryCallback implements ZipEntryCallback { private ZipOutputStream out; private RepackZipEntryCallback(File dstZip, int compressionLevel) { try { this.out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(dstZip))); this.out.setLevel(compressionLevel); } catch (IOException e) { ZipExceptionUtil.rethrow(e); } } public void process(InputStream in, ZipEntry zipEntry) throws IOException { ZipEntryUtil.copyEntry(zipEntry, in, out); } private void closeStream() { IOUtils.closeQuietly(out); } } /** * Compresses a given directory in its own location. * <p> * A ZIP file will be first created with a temporary name. After the * compressing the directory will be deleted and the ZIP file will be renamed * as the original directory. * * @param dir * input directory as well as the target ZIP file. * * @see #pack(File, File) */ public static void unexplode(File dir) { unexplode(dir, DEFAULT_COMPRESSION_LEVEL); } /** * Compresses a given directory in its own location. * <p> * A ZIP file will be first created with a temporary name. After the * compressing the directory will be deleted and the ZIP file will be renamed * as the original directory. * * @param dir * input directory as well as the target ZIP file. * @param compressionLevel * compression level * * @see #pack(File, File) */ public static void unexplode(File dir, int compressionLevel) { try { // Find a new unique name is the same directory File zip = FileUtils.getTempFileFor(dir); // Pack it pack(dir, zip, compressionLevel); // Delete the directory FileUtils.deleteDirectory(dir); // Rename the archive FileUtils.moveFile(zip, dir); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /** * Compresses the given entries into an output stream. * * @param entries * ZIP entries added. * @param os * output stream for the new ZIP (does not have to be buffered) * * @since 1.9 */ public static void pack(ZipEntrySource[] entries, OutputStream os) { if (log.isDebugEnabled()) { log.debug("Creating stream from {}.", Arrays.asList(entries)); } pack(entries, os, false); } private static void pack(ZipEntrySource[] entries, OutputStream os, boolean closeStream) { try { ZipOutputStream out = new ZipOutputStream(os); for (int i = 0; i < entries.length; i++) { addEntry(entries[i], out); } out.flush(); out.finish(); if (closeStream) { out.close(); } } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } } /** * Compresses the given entries into a new ZIP file. * * @param entries * ZIP entries added. * @param zip * new ZIP file created. */ public static void pack(ZipEntrySource[] entries, File zip) { if (log.isDebugEnabled()) { log.debug("Creating '{}' from {}.", zip, Arrays.asList(entries)); } OutputStream out = null; try { out = new BufferedOutputStream(new FileOutputStream(zip)); pack(entries, out, true); } catch (IOException e) { throw ZipExceptionUtil.rethrow(e); } finally { IOUtils.closeQuietly(out); } } /** * Copies an existing ZIP file and appends it with one new entry. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param file * new entry to be added. * @param destZip * new ZIP file created. */ public static void addEntry(File zip, String path, File file, File destZip) { addEntry(zip, new FileSource(path, file), destZip); } /** * Changes a zip file, adds one new entry in-place. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param file * new entry to be added. */ public static void addEntry(final File zip, final String path, final File file) { operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { addEntry(zip, path, file, tmpFile); return true; } }); } /** * Copies an existing ZIP file and appends it with one new entry. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param bytes * new entry bytes (or <code>null</code> if directory). * @param destZip * new ZIP file created. */ public static void addEntry(File zip, String path, byte[] bytes, File destZip) { addEntry(zip, new ByteSource(path, bytes), destZip); } /** * Copies an existing ZIP file and appends it with one new entry. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param bytes * new entry bytes (or <code>null</code> if directory). * @param destZip * new ZIP file created. * @param compressionMethod * the new compression method (<code>ZipEntry.STORED</code> or <code>ZipEntry.DEFLATED</code>). */ public static void addEntry(File zip, String path, byte[] bytes, File destZip, final int compressionMethod) { addEntry(zip, new ByteSource(path, bytes, compressionMethod), destZip); } /** * Changes a zip file, adds one new entry in-place. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param bytes * new entry bytes (or <code>null</code> if directory). */ public static void addEntry(final File zip, final String path, final byte[] bytes) { operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { addEntry(zip, path, bytes, tmpFile); return true; } }); } /** * Changes a zip file, adds one new entry in-place. * * @param zip * an existing ZIP file (only read). * @param path * new ZIP entry path. * @param bytes * new entry bytes (or <code>null</code> if directory). * @param compressionMethod * the new compression method (<code>ZipEntry.STORED</code> or <code>ZipEntry.DEFLATED</code>). */ public static void addEntry(final File zip, final String path, final byte[] bytes, final int compressionMethod) { operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { addEntry(zip, path, bytes, tmpFile, compressionMethod); return true; } }); } /** * Copies an existing ZIP file and appends it with one new entry. * * @param zip * an existing ZIP file (only read). * @param entry * new ZIP entry appended. * @param destZip * new ZIP file created. */ public static void addEntry(File zip, ZipEntrySource entry, File destZip) { addEntries(zip, new ZipEntrySource[] { entry }, destZip); } /** * Changes a zip file, adds one new entry in-place. * * @param zip * an existing ZIP file (only read). * @param entry * new ZIP entry appended. */ public static void addEntry(final File zip, final ZipEntrySource entry) { operateInPlace(zip, new InPlaceAction() { public boolean act(File tmpFile) { addEntry(zip, entry, tmpFile); return true; } }); } /** * Copies an existing ZIP file and appends it with new entries.
{'addition_count': 1, 'commit_subject': 'finish writing to buffer, but leave stream open', 'deletion_count': 0, 'file_extension': '.java', 'lang': 'java', 'license': 'apache-2.0', 'repo_name': 'zeroturnaround/zt-zip'}