001/* 002 * SonarQube 003 * Copyright (C) 2009-2018 SonarSource SA 004 * mailto:info AT sonarsource DOT com 005 * 006 * This program is free software; you can redistribute it and/or 007 * modify it under the terms of the GNU Lesser General Public 008 * License as published by the Free Software Foundation; either 009 * version 3 of the License, or (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 * Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with this program; if not, write to the Free Software Foundation, 018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 019 */ 020package org.sonar.api.utils; 021 022import java.io.BufferedInputStream; 023import java.io.File; 024import java.io.FileInputStream; 025import java.io.FileOutputStream; 026import java.io.IOException; 027import java.io.InputStream; 028import java.io.OutputStream; 029import java.nio.file.Path; 030import java.util.Enumeration; 031import java.util.function.Predicate; 032import java.util.zip.ZipEntry; 033import java.util.zip.ZipFile; 034import java.util.zip.ZipInputStream; 035import java.util.zip.ZipOutputStream; 036import org.apache.commons.io.FileUtils; 037import org.apache.commons.io.IOUtils; 038 039/** 040 * Utility to zip directories and unzip files. 041 * 042 * @since 1.10 043 */ 044public final class ZipUtils { 045 046 private static final String ERROR_CREATING_DIRECTORY = "Error creating directory: "; 047 048 private ZipUtils() { 049 // only static methods 050 } 051 052 /** 053 * Unzip a file into a directory. The directory is created if it does not exist. 054 * 055 * @return the target directory 056 */ 057 public static File unzip(File zip, File toDir) throws IOException { 058 return unzip(zip, toDir, (Predicate<ZipEntry>) ze -> true); 059 } 060 061 public static File unzip(InputStream zip, File toDir) throws IOException { 062 return unzip(zip, toDir, (Predicate<ZipEntry>) ze -> true); 063 } 064 065 /** 066 * @deprecated replaced by {@link #unzip(InputStream, File, Predicate)} in 6.2. 067 */ 068 @Deprecated 069 public static File unzip(InputStream stream, File toDir, ZipEntryFilter filter) throws IOException { 070 return unzip(stream, toDir, new ZipEntryFilterDelegate(filter)); 071 } 072 073 /** 074 * Unzip a file to a directory. 075 * 076 * @param stream the zip input file 077 * @param toDir the target directory. It is created if needed. 078 * @param filter filter zip entries so that only a subset of directories/files can be 079 * extracted to target directory. 080 * @return the parameter {@code toDir} 081 * @since 6.2 082 */ 083 public static File unzip(InputStream stream, File toDir, Predicate<ZipEntry> filter) throws IOException { 084 if (!toDir.exists()) { 085 FileUtils.forceMkdir(toDir); 086 } 087 088 Path targetDirNormalizedPath = toDir.toPath().normalize(); 089 ZipInputStream zipStream = new ZipInputStream(stream); 090 try { 091 ZipEntry entry; 092 while ((entry = zipStream.getNextEntry()) != null) { 093 if (filter.test(entry)) { 094 unzipEntry(entry, zipStream, targetDirNormalizedPath); 095 } 096 } 097 return toDir; 098 099 } finally { 100 zipStream.close(); 101 } 102 } 103 104 private static void unzipEntry(ZipEntry entry, ZipInputStream zipStream, Path targetDirNormalized) throws IOException { 105 File to = targetDirNormalized.resolve(entry.getName()).toFile(); 106 verifyInsideTargetDirectory(entry, to.toPath(), targetDirNormalized); 107 108 if (entry.isDirectory()) { 109 throwExceptionIfDirectoryIsNotCreatable(to); 110 } else { 111 File parent = to.getParentFile(); 112 throwExceptionIfDirectoryIsNotCreatable(parent); 113 copy(zipStream, to); 114 } 115 } 116 117 private static void throwExceptionIfDirectoryIsNotCreatable(File to) throws IOException { 118 if (!to.exists() && !to.mkdirs()) { 119 throw new IOException(ERROR_CREATING_DIRECTORY + to); 120 } 121 } 122 123 /** 124 * @deprecated replaced by {@link #unzip(File, File, Predicate)} in 6.2. 125 */ 126 @Deprecated 127 public static File unzip(File zip, File toDir, ZipEntryFilter filter) throws IOException { 128 return unzip(zip, toDir, new ZipEntryFilterDelegate(filter)); 129 } 130 131 /** 132 * Unzip a file to a directory. 133 * 134 * @param zip the zip file. It must exist. 135 * @param toDir the target directory. It is created if needed. 136 * @param filter filter zip entries so that only a subset of directories/files can be 137 * extracted to target directory. 138 * @return the parameter {@code toDir} 139 * @since 6.2 140 */ 141 public static File unzip(File zip, File toDir, Predicate<ZipEntry> filter) throws IOException { 142 if (!toDir.exists()) { 143 FileUtils.forceMkdir(toDir); 144 } 145 146 Path targetDirNormalizedPath = toDir.toPath().normalize(); 147 ZipFile zipFile = new ZipFile(zip); 148 try { 149 Enumeration<? extends ZipEntry> entries = zipFile.entries(); 150 while (entries.hasMoreElements()) { 151 ZipEntry entry = entries.nextElement(); 152 if (filter.test(entry)) { 153 File target = new File(toDir, entry.getName()); 154 155 verifyInsideTargetDirectory(entry, target.toPath(), targetDirNormalizedPath); 156 157 if (entry.isDirectory()) { 158 throwExceptionIfDirectoryIsNotCreatable(target); 159 } else { 160 File parent = target.getParentFile(); 161 throwExceptionIfDirectoryIsNotCreatable(parent); 162 copy(zipFile, entry, target); 163 } 164 } 165 } 166 return toDir; 167 168 } finally { 169 zipFile.close(); 170 } 171 } 172 173 private static void copy(ZipInputStream zipStream, File to) throws IOException { 174 FileOutputStream fos = null; 175 try { 176 fos = new FileOutputStream(to); 177 IOUtils.copy(zipStream, fos); 178 } finally { 179 IOUtils.closeQuietly(fos); 180 } 181 } 182 183 private static void copy(ZipFile zipFile, ZipEntry entry, File to) throws IOException { 184 FileOutputStream fos = new FileOutputStream(to); 185 InputStream input = null; 186 try { 187 input = zipFile.getInputStream(entry); 188 IOUtils.copy(input, fos); 189 } finally { 190 IOUtils.closeQuietly(input); 191 IOUtils.closeQuietly(fos); 192 } 193 } 194 195 public static void zipDir(File dir, File zip) throws IOException { 196 OutputStream out = null; 197 ZipOutputStream zout = null; 198 try { 199 out = FileUtils.openOutputStream(zip); 200 zout = new ZipOutputStream(out); 201 doZipDir(dir, zout); 202 203 } finally { 204 IOUtils.closeQuietly(zout); 205 IOUtils.closeQuietly(out); 206 } 207 } 208 209 private static void doZip(String entryName, InputStream in, ZipOutputStream out) throws IOException { 210 ZipEntry entry = new ZipEntry(entryName); 211 out.putNextEntry(entry); 212 IOUtils.copy(in, out); 213 out.closeEntry(); 214 } 215 216 private static void doZip(String entryName, File file, ZipOutputStream out) throws IOException { 217 if (file.isDirectory()) { 218 entryName += "/"; 219 ZipEntry entry = new ZipEntry(entryName); 220 out.putNextEntry(entry); 221 out.closeEntry(); 222 File[] files = file.listFiles(); 223 // java.io.File#listFiles() returns null if object is a directory (not possible here) or if 224 // an I/O error occurs (weird!) 225 if (files == null) { 226 throw new IllegalStateException("Fail to list files of directory " + file.getAbsolutePath()); 227 } 228 for (File f : files) { 229 doZip(entryName + f.getName(), f, out); 230 } 231 232 } else { 233 try (InputStream in = new BufferedInputStream(new FileInputStream(file))) { 234 doZip(entryName, in, out); 235 } 236 } 237 } 238 239 private static void doZipDir(File dir, ZipOutputStream out) throws IOException { 240 File[] children = dir.listFiles(); 241 if (children == null) { 242 throw new IllegalStateException("Fail to list files of directory " + dir.getAbsolutePath()); 243 } 244 for (File child : children) { 245 doZip(child.getName(), child, out); 246 } 247 } 248 249 private static void verifyInsideTargetDirectory(ZipEntry entry, Path entryPath, Path targetDirNormalizedPath) { 250 if (!entryPath.normalize().startsWith(targetDirNormalizedPath)) { 251 // vulnerability - trying to create a file outside the target directory 252 throw new IllegalStateException("Unzipping an entry outside the target directory is not allowed: " + entry.getName()); 253 } 254 } 255 256 /** 257 * @see #unzip(File, File, Predicate) 258 * @deprecated replaced by {@link Predicate<ZipEntry>} in 6.2. 259 */ 260 @Deprecated 261 @FunctionalInterface 262 public interface ZipEntryFilter { 263 boolean accept(ZipEntry entry); 264 } 265 266 private static class ZipEntryFilterDelegate implements Predicate<ZipEntry> { 267 private final ZipEntryFilter delegate; 268 269 private ZipEntryFilterDelegate(ZipEntryFilter delegate) { 270 this.delegate = delegate; 271 } 272 273 @Override 274 public boolean test(ZipEntry zipEntry) { 275 return delegate.accept(zipEntry); 276 } 277 } 278}