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