1 /* Copyright 2016 Google Inc. All Rights Reserved. 2 3 Distributed under MIT license. 4 See file LICENSE for detail or copy at https://opensource.org/licenses/MIT 5 */ 6 7 package org.brotli.integration; 8 9 import org.brotli.dec.BrotliInputStream; 10 import java.io.FileInputStream; 11 import java.io.FileNotFoundException; 12 import java.io.FilterInputStream; 13 import java.io.IOException; 14 import java.io.InputStream; 15 import java.math.BigInteger; 16 import java.util.concurrent.atomic.AtomicInteger; 17 import java.util.zip.ZipEntry; 18 import java.util.zip.ZipInputStream; 19 20 /** 21 * Decompress files and (optionally) checks their checksums. 22 * 23 * <p> File are read from ZIP archive passed as an array of bytes. Multiple checkers negotiate about 24 * task distribution via shared AtomicInteger counter. 25 * <p> All entries are expected to be valid brotli compressed streams and output CRC64 checksum 26 * is expected to match the checksum hex-encoded in the first part of entry name. 27 */ 28 public class BundleChecker implements Runnable { 29 private final AtomicInteger nextJob; 30 private final InputStream input; 31 private final boolean sanityCheck; 32 33 /** 34 * @param sanityCheck do not calculate checksum and ignore {@link IOException}. 35 */ BundleChecker(InputStream input, AtomicInteger nextJob, boolean sanityCheck)36 public BundleChecker(InputStream input, AtomicInteger nextJob, boolean sanityCheck) { 37 this.input = input; 38 this.nextJob = nextJob; 39 this.sanityCheck = sanityCheck; 40 } 41 42 /** ECMA CRC64 polynomial. */ 43 private static final long CRC_64_POLY = 44 new BigInteger("C96C5795D7870F42", 16).longValue(); 45 46 /** 47 * Rolls CRC64 calculation. 48 * 49 * <p> {@code CRC64(data) = -1 ^ updateCrc64((... updateCrc64(-1, firstBlock), ...), lastBlock);} 50 * <p> This simple and reliable checksum is chosen to make is easy to calculate the same value 51 * across the variety of languages (C++, Java, Go, ...). 52 */ updateCrc64(long crc, byte[] data, int offset, int length)53 private static long updateCrc64(long crc, byte[] data, int offset, int length) { 54 for (int i = offset; i < offset + length; ++i) { 55 long c = (crc ^ (long) (data[i] & 0xFF)) & 0xFF; 56 for (int k = 0; k < 8; k++) { 57 c = ((c & 1) == 1) ? CRC_64_POLY ^ (c >>> 1) : c >>> 1; 58 } 59 crc = c ^ (crc >>> 8); 60 } 61 return crc; 62 } 63 decompressAndCalculateCrc(ZipInputStream input)64 private long decompressAndCalculateCrc(ZipInputStream input) throws IOException { 65 /* Do not allow entry readers to close the whole ZipInputStream. */ 66 FilterInputStream entryStream = new FilterInputStream(input) { 67 @Override 68 public void close() {} 69 }; 70 71 long crc = -1; 72 byte[] buffer = new byte[65536]; 73 BrotliInputStream decompressedStream = new BrotliInputStream(entryStream); 74 while (true) { 75 int len = decompressedStream.read(buffer); 76 if (len <= 0) { 77 break; 78 } 79 crc = updateCrc64(crc, buffer, 0, len); 80 } 81 decompressedStream.close(); 82 return ~crc; 83 } 84 85 @Override run()86 public void run() { 87 String entryName = ""; 88 ZipInputStream zis = new ZipInputStream(input); 89 try { 90 int entryIndex = 0; 91 ZipEntry entry; 92 int jobIndex = nextJob.getAndIncrement(); 93 while ((entry = zis.getNextEntry()) != null) { 94 if (entry.isDirectory()) { 95 continue; 96 } 97 if (entryIndex++ != jobIndex) { 98 zis.closeEntry(); 99 continue; 100 } 101 entryName = entry.getName(); 102 int dotIndex = entryName.indexOf('.'); 103 String entryCrcString = (dotIndex == -1) ? entryName : entryName.substring(0, dotIndex); 104 long entryCrc = new BigInteger(entryCrcString, 16).longValue(); 105 try { 106 if (entryCrc != decompressAndCalculateCrc(zis) && !sanityCheck) { 107 throw new RuntimeException("CRC mismatch"); 108 } 109 } catch (IOException iox) { 110 if (!sanityCheck) { 111 throw new RuntimeException("Decompression failed", iox); 112 } 113 } 114 zis.closeEntry(); 115 entryName = ""; 116 jobIndex = nextJob.getAndIncrement(); 117 } 118 zis.close(); 119 input.close(); 120 } catch (Throwable ex) { 121 throw new RuntimeException(entryName, ex); 122 } 123 } 124 main(String[] args)125 public static void main(String[] args) throws FileNotFoundException { 126 int argsOffset = 0; 127 boolean sanityCheck = false; 128 if (args.length != 0) { 129 if (args[0].equals("-s")) { 130 sanityCheck = true; 131 argsOffset = 1; 132 } 133 } 134 if (args.length == argsOffset) { 135 throw new RuntimeException("Usage: BundleChecker [-s] <fileX.zip> ..."); 136 } 137 for (int i = argsOffset; i < args.length; ++i) { 138 new BundleChecker(new FileInputStream(args[i]), new AtomicInteger(0), sanityCheck).run(); 139 } 140 } 141 } 142