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 import com.android.tradefed.util.zip.CentralDirectoryInfo;
20 import com.android.tradefed.util.zip.EndCentralDirectoryInfo;
21 import com.android.tradefed.util.zip.LocalFileHeader;
22 
23 import java.io.BufferedInputStream;
24 import java.io.BufferedOutputStream;
25 import java.io.File;
26 import java.io.FileInputStream;
27 import java.io.FileOutputStream;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.OutputStream;
31 import java.nio.file.Files;
32 import java.util.ArrayList;
33 import java.util.Enumeration;
34 import java.util.LinkedList;
35 import java.util.List;
36 import java.util.zip.DataFormatException;
37 import java.util.zip.GZIPOutputStream;
38 import java.util.zip.Inflater;
39 import java.util.zip.ZipEntry;
40 import java.util.zip.ZipException;
41 import java.util.zip.ZipFile;
42 import java.util.zip.ZipOutputStream;
43 
44 /**
45  * A helper class for compression-related operations
46  */
47 public class ZipUtil {
48 
49     private static final int COMPRESSION_METHOD_STORED = 0;
50     private static final int COMPRESSION_METHOD_DEFLATE = 8;
51     private static final String DEFAULT_DIRNAME = "dir";
52     private static final String DEFAULT_FILENAME = "files";
53     private static final String ZIP_EXTENSION = ".zip";
54     private static final String PARTIAL_ZIP_DATA = "compressed_data";
55 
56     private static final boolean IS_UNIX;
57 
58     static {
59         String OS = System.getProperty("os.name").toLowerCase();
60         IS_UNIX = (OS.contains("nix") || OS.contains("nux") || OS.contains("aix"));
61     }
62 
63     /**
64      * Utility method to verify that a zip file is not corrupt.
65      *
66      * @param zipFile the {@link File} to check
67      * @param thorough Whether to attempt to fully extract the archive.  If {@code false}, this
68      *        method will fail to detect CRC errors in a well-formed archive.
69      * @throws IOException if the file could not be opened or read
70      * @return {@code false} if the file appears to be corrupt; {@code true} otherwise
71      */
isZipFileValid(File zipFile, boolean thorough)72     public static boolean isZipFileValid(File zipFile, boolean thorough) throws IOException {
73         if (zipFile != null && !zipFile.exists()) {
74             CLog.d("Zip file does not exist: %s", zipFile.getAbsolutePath());
75             return false;
76         }
77 
78         try (ZipFile z = new ZipFile(zipFile)) {
79             if (thorough) {
80                 // Reading the entire file is the only way to detect CRC errors within the archive
81                 final File extractDir = FileUtil.createTempDir("extract-" + zipFile.getName());
82                 try {
83                     extractZip(z, extractDir);
84                 } finally {
85                     FileUtil.recursiveDelete(extractDir);
86                 }
87             }
88         } catch (ZipException e) {
89             // File is likely corrupted
90             CLog.d("Detected corrupt zip file %s:", zipFile.getCanonicalPath());
91             CLog.e(e);
92             return false;
93         }
94 
95         return true;
96     }
97 
98     /**
99      * Utility method to extract entire contents of zip file into given directory
100      *
101      * @param zipFile the {@link ZipFile} to extract
102      * @param destDir the local dir to extract file to
103      * @throws IOException if failed to extract file
104      */
extractZip(ZipFile zipFile, File destDir)105     public static void extractZip(ZipFile zipFile, File destDir) throws IOException {
106         Enumeration<? extends ZipEntry> entries = zipFile.entries();
107         while (entries.hasMoreElements()) {
108 
109             ZipEntry entry = entries.nextElement();
110             File childFile = new File(destDir, entry.getName());
111             childFile.getParentFile().mkdirs();
112             if (entry.isDirectory()) {
113                 continue;
114             } else {
115                 FileUtil.writeToFile(zipFile.getInputStream(entry), childFile);
116             }
117         }
118     }
119 
120     /**
121      * Utility method to extract one specific file from zip file into a tmp file
122      *
123      * @param zipFile the {@link ZipFile} to extract
124      * @param filePath the filePath of to extract
125      * @throws IOException if failed to extract file
126      * @return the {@link File} or null if not found
127      */
extractFileFromZip(ZipFile zipFile, String filePath)128     public static File extractFileFromZip(ZipFile zipFile, String filePath) throws IOException {
129         ZipEntry entry = zipFile.getEntry(filePath);
130         if (entry == null) {
131             return null;
132         }
133         File createdFile = FileUtil.createTempFile("extracted",
134                 FileUtil.getExtension(filePath));
135         FileUtil.writeToFile(zipFile.getInputStream(entry), createdFile);
136         return createdFile;
137     }
138 
139     /**
140      * Utility method to create a temporary zip file containing the given directory and
141      * all its contents.
142      *
143      * @param dir the directory to zip
144      * @return a temporary zip {@link File} containing directory contents
145      * @throws IOException if failed to create zip file
146      */
createZip(File dir)147     public static File createZip(File dir) throws IOException {
148         return createZip(dir, DEFAULT_DIRNAME);
149     }
150 
151     /**
152      * Utility method to create a temporary zip file containing the given directory and
153      * all its contents.
154      *
155      * @param dir the directory to zip
156      * @param name the base name of the zip file created without the extension.
157      * @return a temporary zip {@link File} containing directory contents
158      * @throws IOException if failed to create zip file
159      */
createZip(File dir, String name)160     public static File createZip(File dir, String name) throws IOException {
161         File zipFile = FileUtil.createTempFile(name, ZIP_EXTENSION);
162         createZip(dir, zipFile);
163         return zipFile;
164     }
165 
166     /**
167      * Utility method to create a zip file containing the given directory and
168      * all its contents.
169      *
170      * @param dir the directory to zip
171      * @param zipFile the zip file to create - it should not already exist
172      * @throws IOException if failed to create zip file
173      */
createZip(File dir, File zipFile)174     public static void createZip(File dir, File zipFile) throws IOException {
175         ZipOutputStream out = null;
176         try {
177             FileOutputStream fileStream = new FileOutputStream(zipFile);
178             out = new ZipOutputStream(new BufferedOutputStream(fileStream));
179             addToZip(out, dir, new LinkedList<String>());
180         } catch (IOException e) {
181             zipFile.delete();
182             throw e;
183         } catch (RuntimeException e) {
184             zipFile.delete();
185             throw e;
186         } finally {
187             StreamUtil.close(out);
188         }
189     }
190 
191     /**
192      * Utility method to create a temporary zip file containing the given files
193      *
194      * @param files list of files to zip
195      * @return a temporary zip {@link File} containing directory contents
196      * @throws IOException if failed to create zip file
197      */
createZip(List<File> files)198     public static File createZip(List<File> files) throws IOException {
199         return createZip(files, DEFAULT_FILENAME);
200     }
201 
202     /**
203      * Utility method to create a temporary zip file containing the given files.
204      *
205      * @param files list of files to zip
206      * @param name the base name of the zip file created without the extension.
207      * @return a temporary zip {@link File} containing directory contents
208      * @throws IOException if failed to create zip file
209      */
createZip(List<File> files, String name)210     public static File createZip(List<File> files, String name) throws IOException {
211         File zipFile = FileUtil.createTempFile(name, ZIP_EXTENSION);
212         createZip(files, zipFile);
213         return zipFile;
214     }
215 
216     /**
217      * Utility method to create a zip file containing the given files
218      *
219      * @param files list of files to zip
220      * @param zipFile the zip file to create - it should not already exist
221      * @throws IOException if failed to create zip file
222      */
createZip(List<File> files, File zipFile)223     public static void createZip(List<File> files, File zipFile) throws IOException {
224         ZipOutputStream out = null;
225         try {
226             FileOutputStream fileStream = new FileOutputStream(zipFile);
227             out = new ZipOutputStream(new BufferedOutputStream(fileStream));
228             for (File file : files) {
229                 addToZip(out, file, new LinkedList<String>());
230             }
231         } catch (IOException|RuntimeException e) {
232             zipFile.delete();
233             throw e;
234         } finally {
235             StreamUtil.close(out);
236         }
237     }
238 
239     /**
240      * Recursively adds given file and its contents to ZipOutputStream
241      *
242      * @param out the {@link ZipOutputStream}
243      * @param file the {@link File} to add to the stream
244      * @param relativePathSegs the relative path of file, including separators
245      * @throws IOException if failed to add file to zip
246      */
addToZip(ZipOutputStream out, File file, List<String> relativePathSegs)247     public static void addToZip(ZipOutputStream out, File file, List<String> relativePathSegs)
248             throws IOException {
249         relativePathSegs.add(file.getName());
250         if (file.isDirectory()) {
251             // note: it appears even on windows, ZipEntry expects '/' as a path separator
252             relativePathSegs.add("/");
253         }
254         ZipEntry zipEntry = new ZipEntry(buildPath(relativePathSegs));
255         out.putNextEntry(zipEntry);
256         if (file.isFile()) {
257             writeToStream(file, out);
258         }
259         out.closeEntry();
260         if (file.isDirectory()) {
261             // recursively add contents
262             File[] subFiles = file.listFiles();
263             if (subFiles == null) {
264                 throw new IOException(String.format("Could not read directory %s",
265                         file.getAbsolutePath()));
266             }
267             for (File subFile : subFiles) {
268                 addToZip(out, subFile, relativePathSegs);
269             }
270             // remove the path separator
271             relativePathSegs.remove(relativePathSegs.size()-1);
272         }
273         // remove the last segment, added at beginning of method
274         relativePathSegs.remove(relativePathSegs.size()-1);
275     }
276 
277     /**
278      * Close an open {@link ZipFile}, ignoring any exceptions.
279      *
280      * @param zipFile the file to close
281      */
closeZip(ZipFile zipFile)282     public static void closeZip(ZipFile zipFile) {
283         if (zipFile != null) {
284             try {
285                 zipFile.close();
286             } catch (IOException e) {
287                 // ignore
288             }
289         }
290     }
291 
292     /**
293      * Helper method to create a gzipped version of a single file.
294      *
295      * @param file the original file
296      * @param gzipFile the file to place compressed contents in
297      * @throws IOException
298      */
gzipFile(File file, File gzipFile)299     public static void gzipFile(File file, File gzipFile) throws IOException {
300         GZIPOutputStream out = null;
301         try {
302             FileOutputStream fileStream = new FileOutputStream(gzipFile);
303             out = new GZIPOutputStream(new BufferedOutputStream(fileStream, 64 * 1024));
304             writeToStream(file, out);
305         } catch (IOException e) {
306             gzipFile.delete();
307             throw e;
308         } catch (RuntimeException e) {
309             gzipFile.delete();
310             throw e;
311         } finally {
312             StreamUtil.close(out);
313         }
314     }
315 
316     /**
317      * Helper method to write input file contents to output stream.
318      *
319      * @param file the input {@link File}
320      * @param out the {@link OutputStream}
321      *
322      * @throws IOException
323      */
writeToStream(File file, OutputStream out)324     private static void writeToStream(File file, OutputStream out) throws IOException {
325         InputStream inputStream = null;
326         try {
327             inputStream = new BufferedInputStream(new FileInputStream(file));
328             StreamUtil.copyStreams(inputStream, out);
329         } finally {
330             StreamUtil.close(inputStream);
331         }
332     }
333 
334     /**
335      * Builds a file system path from a stack of relative path segments
336      *
337      * @param relativePathSegs the list of relative paths
338      * @return a {@link String} containing all relativePathSegs
339      */
buildPath(List<String> relativePathSegs)340     private static String buildPath(List<String> relativePathSegs) {
341         StringBuilder pathBuilder = new StringBuilder();
342         for (String segment : relativePathSegs) {
343             pathBuilder.append(segment);
344         }
345         return pathBuilder.toString();
346     }
347 
348     /**
349      * Extract a zip file to a temp directory prepended with a string
350      *
351      * @param zipFile the zip file to extract
352      * @param nameHint a prefix for the temp directory
353      * @return a {@link File} pointing to the temp directory
354      */
extractZipToTemp(File zipFile, String nameHint)355     public static File extractZipToTemp(File zipFile, String nameHint)
356             throws IOException, ZipException {
357         File localRootDir = FileUtil.createTempDir(nameHint);
358         try (ZipFile zip = new ZipFile(zipFile)) {
359             extractZip(zip, localRootDir);
360             return localRootDir;
361         } catch (IOException e) {
362             // clean tmp file since we couldn't extract.
363             FileUtil.recursiveDelete(localRootDir);
364             throw e;
365         }
366     }
367 
368     /**
369      * Get a list of {link CentralDirectoryInfo} for files in a zip file.
370      *
371      * @param partialZipFile a {@link File} object of the partial zip file that contains central
372      *     directory entries.
373      * @param endCentralDirInfo a {@link EndCentralDirectoryInfo} object of the zip file.
374      * @param useZip64 a boolean to support zip64 format in partial download.
375      * @return A list of {@link CentralDirectoryInfo} of the zip file
376      * @throws IOException
377      */
getZipCentralDirectoryInfos( File partialZipFile, EndCentralDirectoryInfo endCentralDirInfo, boolean useZip64)378     public static List<CentralDirectoryInfo> getZipCentralDirectoryInfos(
379             File partialZipFile,
380             EndCentralDirectoryInfo endCentralDirInfo,
381             boolean useZip64)
382             throws IOException {
383         return getZipCentralDirectoryInfos(partialZipFile, endCentralDirInfo, 0, useZip64);
384     }
385 
386     /**
387      * Get a list of {link CentralDirectoryInfo} for files in a zip file.
388      *
389      * @param partialZipFile a {@link File} object of the partial zip file that contains central
390      *     directory entries.
391      * @param endCentralDirInfo a {@link EndCentralDirectoryInfo} object of the zip file.
392      * @param offset the offset in the partial zip file where the content of central directory
393      *     entries starts.
394      * @return A list of {@link CentralDirectoryInfo} of the zip file
395      * @throws IOException
396      */
getZipCentralDirectoryInfos( File partialZipFile, EndCentralDirectoryInfo endCentralDirInfo, long offset)397     public static List<CentralDirectoryInfo> getZipCentralDirectoryInfos(
398             File partialZipFile,
399             EndCentralDirectoryInfo endCentralDirInfo,
400             long offset)
401             throws IOException {
402         return getZipCentralDirectoryInfos(partialZipFile, endCentralDirInfo, offset, false);
403     }
404 
405     /**
406      * Get a list of {link CentralDirectoryInfo} for files in a zip file.
407      *
408      * @param partialZipFile a {@link File} object of the partial zip file that contains central
409      *     directory entries.
410      * @param endCentralDirInfo a {@link EndCentralDirectoryInfo} object of the zip file.
411      * @return A list of {@link CentralDirectoryInfo} of the zip file
412      * @throws IOException
413      */
getZipCentralDirectoryInfos( File partialZipFile, EndCentralDirectoryInfo endCentralDirInfo)414     public static List<CentralDirectoryInfo> getZipCentralDirectoryInfos(
415             File partialZipFile,
416             EndCentralDirectoryInfo endCentralDirInfo)
417             throws IOException {
418         return getZipCentralDirectoryInfos(partialZipFile, endCentralDirInfo, 0, false);
419     }
420 
421     /**
422      * Get a list of {link CentralDirectoryInfo} for files in a zip file.
423      *
424      * @param partialZipFile a {@link File} object of the partial zip file that contains central
425      *     directory entries.
426      * @param endCentralDirInfo a {@link EndCentralDirectoryInfo} object of the zip file.
427      * @param offset the offset in the partial zip file where the content of central directory
428      *     entries starts.
429      * @param useZip64 a boolean to support zip64 format in partial download.
430      * @return A list of {@link CentralDirectoryInfo} of the zip file
431      * @throws IOException
432      */
getZipCentralDirectoryInfos( File partialZipFile, EndCentralDirectoryInfo endCentralDirInfo, long offset, boolean useZip64)433     public static List<CentralDirectoryInfo> getZipCentralDirectoryInfos(
434             File partialZipFile,
435             EndCentralDirectoryInfo endCentralDirInfo,
436             long offset,
437             boolean useZip64)
438             throws IOException {
439         List<CentralDirectoryInfo> infos = new ArrayList<>();
440         byte[] data;
441         try (FileInputStream stream = new FileInputStream(partialZipFile)) {
442             // Read in the entire central directory block for a zip file till the end. The block
443             // should be small even for a large zip file.
444             long totalSize = stream.getChannel().size();
445             stream.skip(offset);
446             data = new byte[(int) (totalSize - offset)];
447             stream.read(data);
448         }
449         int startOffset = 0;
450         for (int i = 0; i < endCentralDirInfo.getEntryNumber(); i++) {
451             CentralDirectoryInfo info = new CentralDirectoryInfo(data, startOffset, useZip64);
452             infos.add(info);
453             startOffset += info.getInfoSize();
454         }
455 
456         return infos;
457     }
458 
459     /**
460      * Apply the file permission configured in the central directory entry.
461      *
462      * @param targetFile the {@link File} to set permission to.
463      * @param zipEntry a {@link CentralDirectoryInfo} object that contains the file permissions.
464      * @throws IOException if fail to access the file.
465      */
applyPermission(File targetFile, CentralDirectoryInfo zipEntry)466     public static void applyPermission(File targetFile, CentralDirectoryInfo zipEntry)
467             throws IOException {
468         if (!IS_UNIX) {
469             CLog.w("Permission setting is only supported in Unix/Linux system.");
470             return;
471         }
472 
473         if (zipEntry.getFilePermission() != 0) {
474             Files.setPosixFilePermissions(
475                     targetFile.toPath(), FileUtil.unixModeToPosix(zipEntry.getFilePermission()));
476         }
477     }
478 
479     /**
480      * Extract the requested folder from a partial zip file and apply proper permission.
481      *
482      * @param targetFile the {@link File} to save the extracted file to.
483      * @param zipEntry a {@link CentralDirectoryInfo} object of the file to extract from the partial
484      *     zip file.
485      * @throws IOException
486      */
unzipPartialZipFolder(File targetFile, CentralDirectoryInfo zipEntry)487     public static void unzipPartialZipFolder(File targetFile, CentralDirectoryInfo zipEntry)
488             throws IOException {
489         unzipPartialZipFile(null, targetFile, zipEntry, null, -1);
490     }
491 
492     /**
493      * Extract the requested file from a partial zip file.
494      *
495      * <p>This method assumes all files are on the same disk when compressed. It doesn't support
496      * following features yet:
497      *
498      * <p>Zip file larger than 4GB
499      *
500      * <p>ZIP64(require ZipLocalFileHeader update on compressed size)
501      *
502      * <p>Encrypted zip file
503      *
504      * <p>Symlink
505      *
506      * @param partialZip a {@link File} that's a partial of the zip file.
507      * @param targetFile the {@link File} to save the extracted file to.
508      * @param zipEntry a {@link CentralDirectoryInfo} object of the file to extract from the partial
509      *     zip file.
510      * @param localFileHeader a {@link LocalFileHeader} object of the file to extract from the
511      *     partial zip file.
512      * @param startOffset start offset of the file to extract.
513      * @throws IOException
514      */
unzipPartialZipFile( File partialZip, File targetFile, CentralDirectoryInfo zipEntry, LocalFileHeader localFileHeader, long startOffset)515     public static void unzipPartialZipFile(
516             File partialZip,
517             File targetFile,
518             CentralDirectoryInfo zipEntry,
519             LocalFileHeader localFileHeader,
520             long startOffset)
521             throws IOException {
522         try {
523             if (zipEntry.getFileName().endsWith("/")) {
524                 // Create a folder.
525                 targetFile.mkdir();
526                 return;
527             } else if (zipEntry.getCompressedSize() == 0) {
528                 // The file is empty, just create an empty file.
529                 targetFile.createNewFile();
530                 return;
531             }
532 
533             File zipFile = targetFile;
534             if (zipEntry.getCompressionMethod() != COMPRESSION_METHOD_STORED)
535                 // Create a temp file to store the compressed data, then unzip it.
536                 zipFile = FileUtil.createTempFile(PARTIAL_ZIP_DATA, ZIP_EXTENSION);
537             else {
538                 // The file is not compressed, stream it directly to the target.
539                 zipFile.getParentFile().mkdirs();
540                 zipFile.createNewFile();
541             }
542 
543             // Save compressed data to zipFile
544             try (FileInputStream stream = new FileInputStream(partialZip)) {
545                 FileUtil.writeToFile(
546                         stream,
547                         zipFile,
548                         false,
549                         startOffset + localFileHeader.getHeaderSize(),
550                         zipEntry.getCompressedSize());
551             }
552 
553             if (zipEntry.getCompressionMethod() == COMPRESSION_METHOD_STORED) {
554                 return;
555             } else if (zipEntry.getCompressionMethod() == COMPRESSION_METHOD_DEFLATE) {
556                 boolean success = false;
557                 try {
558                     unzipRawZip(zipFile, targetFile, zipEntry);
559                     success = true;
560                 } catch (DataFormatException e) {
561                     throw new IOException(e);
562                 } finally {
563                     zipFile.delete();
564                     if (!success) {
565                         CLog.e("Failed to unzip %s", zipEntry.getFileName());
566                         targetFile.delete();
567                     }
568                 }
569             } else {
570                 throw new RuntimeException(
571                         String.format(
572                                 "Compression method %d is not supported.",
573                                 localFileHeader.getCompressionMethod()));
574             }
575         } finally {
576             if (targetFile.exists()) {
577                 applyPermission(targetFile, zipEntry);
578             }
579         }
580     }
581 
582     /**
583      * Unzip the raw compressed content without wrapper (local file header).
584      *
585      * @param zipFile the {@link File} that contains the compressed data of the target file.
586      * @param targetFile {@link File} to same the decompressed data to.
587      * @throws DataFormatException if decompression failed due to zip format issue.
588      * @throws IOException if failed to access the compressed data or the decompressed file has
589      *     mismatched CRC.
590      */
unzipRawZip(File zipFile, File targetFile, CentralDirectoryInfo zipEntry)591     private static void unzipRawZip(File zipFile, File targetFile, CentralDirectoryInfo zipEntry)
592             throws IOException, DataFormatException {
593         Inflater decompresser = new Inflater(true);
594 
595         targetFile.getParentFile().mkdirs();
596         targetFile.createNewFile();
597 
598         try (FileInputStream inputStream = new FileInputStream(zipFile);
599                 FileOutputStream outputStream = new FileOutputStream(targetFile)) {
600             byte[] data = new byte[32768];
601             byte[] buffer = new byte[65536];
602             while (inputStream.read(data) > 0) {
603                 decompresser.setInput(data);
604                 while (!decompresser.finished() && !decompresser.needsInput()) {
605                     int size = decompresser.inflate(buffer);
606                     outputStream.write(buffer, 0, size);
607                 }
608             }
609         } finally {
610             decompresser.end();
611         }
612 
613         // Validate CRC
614         if (FileUtil.calculateCrc32(targetFile) != zipEntry.getCrc()) {
615             throw new IOException(String.format("Failed to match CRC for file %s", targetFile));
616         }
617     }
618 }
619