1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tradefed.util;
17 
18 import com.android.tradefed.log.LogUtil.CLog;
19 
20 import java.io.BufferedInputStream;
21 import java.io.BufferedOutputStream;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.OutputStream;
28 import java.util.Enumeration;
29 import java.util.LinkedList;
30 import java.util.List;
31 import java.util.zip.GZIPOutputStream;
32 import java.util.zip.ZipEntry;
33 import java.util.zip.ZipException;
34 import java.util.zip.ZipFile;
35 import java.util.zip.ZipOutputStream;
36 
37 /**
38  * A helper class for compression-related operations
39  */
40 public class ZipUtil {
41 
42     private static final String DEFAULT_DIRNAME = "dir";
43     private static final String DEFAULT_FILENAME = "files";
44     private static final String ZIP_EXTENSION = ".zip";
45 
46     /**
47      * Utility method to verify that a zip file is not corrupt.
48      *
49      * @param zipFile the {@link File} to check
50      * @param thorough Whether to attempt to fully extract the archive.  If {@code false}, this
51      *        method will fail to detect CRC errors in a well-formed archive.
52      * @throws IOException if the file could not be opened or read
53      * @return {@code false} if the file appears to be corrupt; {@code true} otherwise
54      */
isZipFileValid(File zipFile, boolean thorough)55     public static boolean isZipFileValid(File zipFile, boolean thorough) throws IOException {
56         if (zipFile != null && !zipFile.exists()) {
57             CLog.d("Zip file does not exist: %s", zipFile.getAbsolutePath());
58             return false;
59         }
60 
61         try (ZipFile z = new ZipFile(zipFile)) {
62             if (thorough) {
63                 // Reading the entire file is the only way to detect CRC errors within the archive
64                 final File extractDir = FileUtil.createTempDir("extract-" + zipFile.getName());
65                 try {
66                     extractZip(z, extractDir);
67                 } finally {
68                     FileUtil.recursiveDelete(extractDir);
69                 }
70             }
71         } catch (ZipException e) {
72             // File is likely corrupted
73             CLog.d("Detected corrupt zip file %s:", zipFile.getCanonicalPath());
74             CLog.e(e);
75             return false;
76         }
77 
78         return true;
79     }
80 
81     /**
82      * Utility method to extract entire contents of zip file into given directory
83      *
84      * @param zipFile the {@link ZipFile} to extract
85      * @param destDir the local dir to extract file to
86      * @throws IOException if failed to extract file
87      */
extractZip(ZipFile zipFile, File destDir)88     public static void extractZip(ZipFile zipFile, File destDir) throws IOException {
89         Enumeration<? extends ZipEntry> entries = zipFile.entries();
90         while (entries.hasMoreElements()) {
91 
92             ZipEntry entry = entries.nextElement();
93             File childFile = new File(destDir, entry.getName());
94             childFile.getParentFile().mkdirs();
95             if (entry.isDirectory()) {
96                 continue;
97             } else {
98                 FileUtil.writeToFile(zipFile.getInputStream(entry), childFile);
99             }
100         }
101     }
102 
103     /**
104      * Utility method to extract one specific file from zip file into a tmp file
105      *
106      * @param zipFile the {@link ZipFile} to extract
107      * @param filePath the filePath of to extract
108      * @throws IOException if failed to extract file
109      * @return the {@link File} or null if not found
110      */
extractFileFromZip(ZipFile zipFile, String filePath)111     public static File extractFileFromZip(ZipFile zipFile, String filePath) throws IOException {
112         ZipEntry entry = zipFile.getEntry(filePath);
113         if (entry == null) {
114             return null;
115         }
116         File createdFile = FileUtil.createTempFile("extracted",
117                 FileUtil.getExtension(filePath));
118         FileUtil.writeToFile(zipFile.getInputStream(entry), createdFile);
119         return createdFile;
120     }
121 
122     /**
123      * Utility method to create a temporary zip file containing the given directory and
124      * all its contents.
125      *
126      * @param dir the directory to zip
127      * @return a temporary zip {@link File} containing directory contents
128      * @throws IOException if failed to create zip file
129      */
createZip(File dir)130     public static File createZip(File dir) throws IOException {
131         return createZip(dir, DEFAULT_DIRNAME);
132     }
133 
134     /**
135      * Utility method to create a temporary zip file containing the given directory and
136      * all its contents.
137      *
138      * @param dir the directory to zip
139      * @param name the base name of the zip file created without the extension.
140      * @return a temporary zip {@link File} containing directory contents
141      * @throws IOException if failed to create zip file
142      */
createZip(File dir, String name)143     public static File createZip(File dir, String name) throws IOException {
144         File zipFile = FileUtil.createTempFile(name, ZIP_EXTENSION);
145         createZip(dir, zipFile);
146         return zipFile;
147     }
148 
149     /**
150      * Utility method to create a zip file containing the given directory and
151      * all its contents.
152      *
153      * @param dir the directory to zip
154      * @param zipFile the zip file to create - it should not already exist
155      * @throws IOException if failed to create zip file
156      */
createZip(File dir, File zipFile)157     public static void createZip(File dir, File zipFile) throws IOException {
158         ZipOutputStream out = null;
159         try {
160             FileOutputStream fileStream = new FileOutputStream(zipFile);
161             out = new ZipOutputStream(new BufferedOutputStream(fileStream));
162             addToZip(out, dir, new LinkedList<String>());
163         } catch (IOException e) {
164             zipFile.delete();
165             throw e;
166         } catch (RuntimeException e) {
167             zipFile.delete();
168             throw e;
169         } finally {
170             StreamUtil.close(out);
171         }
172     }
173 
174     /**
175      * Utility method to create a temporary zip file containing the given files
176      *
177      * @param files list of files to zip
178      * @return a temporary zip {@link File} containing directory contents
179      * @throws IOException if failed to create zip file
180      */
createZip(List<File> files)181     public static File createZip(List<File> files) throws IOException {
182         return createZip(files, DEFAULT_FILENAME);
183     }
184 
185     /**
186      * Utility method to create a temporary zip file containing the given files.
187      *
188      * @param files list of files to zip
189      * @param name the base name of the zip file created without the extension.
190      * @return a temporary zip {@link File} containing directory contents
191      * @throws IOException if failed to create zip file
192      */
createZip(List<File> files, String name)193     public static File createZip(List<File> files, String name) throws IOException {
194         File zipFile = FileUtil.createTempFile(name, ZIP_EXTENSION);
195         createZip(files, zipFile);
196         return zipFile;
197     }
198 
199     /**
200      * Utility method to create a zip file containing the given files
201      *
202      * @param files list of files to zip
203      * @param zipFile the zip file to create - it should not already exist
204      * @throws IOException if failed to create zip file
205      */
createZip(List<File> files, File zipFile)206     public static void createZip(List<File> files, File zipFile) throws IOException {
207         ZipOutputStream out = null;
208         try {
209             FileOutputStream fileStream = new FileOutputStream(zipFile);
210             out = new ZipOutputStream(new BufferedOutputStream(fileStream));
211             for (File file : files) {
212                 addToZip(out, file, new LinkedList<String>());
213             }
214         } catch (IOException|RuntimeException e) {
215             zipFile.delete();
216             throw e;
217         } finally {
218             StreamUtil.close(out);
219         }
220     }
221 
222     /**
223      * Recursively adds given file and its contents to ZipOutputStream
224      *
225      * @param out the {@link ZipOutputStream}
226      * @param file the {@link File} to add to the stream
227      * @param relativePathSegs the relative path of file, including separators
228      * @throws IOException if failed to add file to zip
229      */
addToZip(ZipOutputStream out, File file, List<String> relativePathSegs)230     public static void addToZip(ZipOutputStream out, File file, List<String> relativePathSegs)
231             throws IOException {
232         relativePathSegs.add(file.getName());
233         if (file.isDirectory()) {
234             // note: it appears even on windows, ZipEntry expects '/' as a path separator
235             relativePathSegs.add("/");
236         }
237         ZipEntry zipEntry = new ZipEntry(buildPath(relativePathSegs));
238         out.putNextEntry(zipEntry);
239         if (file.isFile()) {
240             writeToStream(file, out);
241         }
242         out.closeEntry();
243         if (file.isDirectory()) {
244             // recursively add contents
245             File[] subFiles = file.listFiles();
246             if (subFiles == null) {
247                 throw new IOException(String.format("Could not read directory %s",
248                         file.getAbsolutePath()));
249             }
250             for (File subFile : subFiles) {
251                 addToZip(out, subFile, relativePathSegs);
252             }
253             // remove the path separator
254             relativePathSegs.remove(relativePathSegs.size()-1);
255         }
256         // remove the last segment, added at beginning of method
257         relativePathSegs.remove(relativePathSegs.size()-1);
258     }
259 
260     /**
261      * Close an open {@link ZipFile}, ignoring any exceptions.
262      *
263      * @param zipFile the file to close
264      */
closeZip(ZipFile zipFile)265     public static void closeZip(ZipFile zipFile) {
266         if (zipFile != null) {
267             try {
268                 zipFile.close();
269             } catch (IOException e) {
270                 // ignore
271             }
272         }
273     }
274 
275     /**
276      * Helper method to create a gzipped version of a single file.
277      *
278      * @param file the original file
279      * @param gzipFile the file to place compressed contents in
280      * @throws IOException
281      */
gzipFile(File file, File gzipFile)282     public static void gzipFile(File file, File gzipFile) throws IOException {
283         GZIPOutputStream out = null;
284         try {
285             FileOutputStream fileStream = new FileOutputStream(gzipFile);
286             out = new GZIPOutputStream(new BufferedOutputStream(fileStream, 64 * 1024));
287             writeToStream(file, out);
288         } catch (IOException e) {
289             gzipFile.delete();
290             throw e;
291         } catch (RuntimeException e) {
292             gzipFile.delete();
293             throw e;
294         } finally {
295             StreamUtil.close(out);
296         }
297     }
298 
299     /**
300      * Helper method to write input file contents to output stream.
301      *
302      * @param file the input {@link File}
303      * @param out the {@link OutputStream}
304      *
305      * @throws IOException
306      */
writeToStream(File file, OutputStream out)307     private static void writeToStream(File file, OutputStream out) throws IOException {
308         InputStream inputStream = null;
309         try {
310             inputStream = new BufferedInputStream(new FileInputStream(file));
311             StreamUtil.copyStreams(inputStream, out);
312         } finally {
313             StreamUtil.close(inputStream);
314         }
315     }
316 
317     /**
318      * Builds a file system path from a stack of relative path segments
319      *
320      * @param relativePathSegs the list of relative paths
321      * @return a {@link String} containing all relativePathSegs
322      */
buildPath(List<String> relativePathSegs)323     private static String buildPath(List<String> relativePathSegs) {
324         StringBuilder pathBuilder = new StringBuilder();
325         for (String segment : relativePathSegs) {
326             pathBuilder.append(segment);
327         }
328         return pathBuilder.toString();
329     }
330 
331     /**
332      * Extract a zip file to a temp directory prepended with a string
333      *
334      * @param zipFile the zip file to extract
335      * @param nameHint a prefix for the temp directory
336      * @return a {@link File} pointing to the temp directory
337      */
extractZipToTemp(File zipFile, String nameHint)338     public static File extractZipToTemp(File zipFile, String nameHint)
339             throws IOException, ZipException {
340         File localRootDir = FileUtil.createTempDir(nameHint);
341         try (ZipFile zip = new ZipFile(zipFile)) {
342             extractZip(zip, localRootDir);
343             return localRootDir;
344         } catch (IOException e) {
345             // clean tmp file since we couldn't extract.
346             FileUtil.recursiveDelete(localRootDir);
347             throw e;
348         }
349     }
350 }
351