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
7package org.brotli.integration;
8
9import org.brotli.dec.BrotliInputStream;
10import java.io.FileInputStream;
11import java.io.FileNotFoundException;
12import java.io.FilterInputStream;
13import java.io.IOException;
14import java.io.InputStream;
15import java.math.BigInteger;
16import java.util.concurrent.atomic.AtomicInteger;
17import java.util.zip.ZipEntry;
18import 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 */
28public 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   */
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   */
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
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
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
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