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