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