Home | History | Annotate | Download | only in shared
      1 // Copyright 2016 Google Inc. All rights reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //     http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 package com.google.archivepatcher.shared;
     16 
     17 import org.junit.Assert;
     18 
     19 import java.io.ByteArrayInputStream;
     20 import java.io.ByteArrayOutputStream;
     21 import java.io.File;
     22 import java.io.FileOutputStream;
     23 import java.io.IOException;
     24 import java.io.UnsupportedEncodingException;
     25 import java.util.Arrays;
     26 import java.util.Collections;
     27 import java.util.List;
     28 import java.util.zip.CRC32;
     29 import java.util.zip.ZipEntry;
     30 import java.util.zip.ZipInputStream;
     31 import java.util.zip.ZipOutputStream;
     32 
     33 /**
     34  * A testing construct that provides a well-known archive and metadata about it. The archive
     35  * contains four files, one each at compression levels 0 (stored), 1 (fastest), 6 (default), and 9
     36  * (maximum compression). Two of the files have comments in the central directory, two do not. Each
     37  * has unique content with a distinct CRC32. The archive has had its dates normalized, so the date
     38  * and time will be the beginning of the epoch. The goal is to provide a reasonably robust test for
     39  * the logic in MinimalZipParser, but other unit test code also uses this functionality to construct
     40  * contrived data for testing. Exotic stuff like extra data padded at the beginning or in-between
     41  * entries, zip64 support and so on are not present; the goal is not to exhaustively test compliance
     42  * with the zip spec, but rather to ensure that the code works with most common zip files that are
     43  * likely to be encountered in the real world.
     44  */
     45 public class UnitTestZipArchive {
     46   /**
     47    * The data for the first entry in the zip file, compressed at level 1. Has no comment.
     48    */
     49   public static final UnitTestZipEntry entry1 =
     50       makeUnitTestZipEntry(
     51           "file1", // path / filename
     52           1, // compression level
     53           "This is the content of file 1, at level 1. No comment.",
     54           null); // comment
     55 
     56   /**
     57    * The data for the second entry in the zip file, compressed at level 6. Has no comment.
     58    */
     59   public static final UnitTestZipEntry entry2 =
     60       makeUnitTestZipEntry(
     61           "file2", // path / filename
     62           6, // compression level
     63           "Here is some content for file 2, at level 6. No comment.",
     64           null); // comment
     65 
     66   /**
     67    * The data for the third entry in the zip file, compressed at level 9. Has a comment.
     68    */
     69   public static final UnitTestZipEntry entry3 =
     70       makeUnitTestZipEntry(
     71           "file3", // path / filename
     72           9, // compression level
     73           "And some other content for file 3, at level 9. With comment.",
     74           "COMMENT3"); // comment
     75 
     76   /**
     77    * The data for the fourth entry in the zip file, stored (uncompressed / level 0). Has a comment.
     78    */
     79   public static final UnitTestZipEntry entry4 =
     80       makeUnitTestZipEntry(
     81           "file4", // path / filename
     82           0, // compression level
     83           "File 4 data here, this is stored uncompressed. With comment.",
     84           "COMMENT4"); // comment
     85 
     86   /**
     87    * Invokes {@link #makeUnitTestZipEntry(String, int, boolean, String, String)} with nowrap=true.
     88    * @param path the file path
     89    * @param level the level the entry is compressed with
     90    * @param contentPrefix the content prefix - the corpus body will be appended to this value to
     91    * produce the final content for the entry
     92    * @param comment the comment to add to the file in the central directory, if any
     93    * @return the newly created entry
     94    */
     95   public static final UnitTestZipEntry makeUnitTestZipEntry(
     96       String path, int level, String contentPrefix, String comment) {
     97     return makeUnitTestZipEntry(path, level, true, contentPrefix, comment);
     98   }
     99 
    100   /**
    101    * Makes a unit test entry using the specified parameters <em>plus</em> the corpus from
    102    * {@link DefaultDeflateCompatibilityWindow#getCorpus()} to provide enough data for an accurate
    103    * level identification.
    104    * @param path the file path
    105    * @param level the level the entry is compressed with
    106    * @param nowrap the value for the nowrap flag
    107    * @param contentPrefix the content prefix - the corpus body will be appended to this value to
    108    * produce the final content for the entry
    109    * @param comment the comment to add to the file in the central directory, if any
    110    * @return the newly created entry
    111    */
    112   public static final UnitTestZipEntry makeUnitTestZipEntry(
    113       String path, int level, boolean nowrap, String contentPrefix, String comment) {
    114     String corpusText;
    115     try {
    116       corpusText = new String(new DefaultDeflateCompatibilityWindow().getCorpus(), "US-ASCII");
    117     } catch (UnsupportedEncodingException e) {
    118       throw new RuntimeException("System doesn't support US-ASCII", e);
    119     }
    120     return new UnitTestZipEntry(path, level, nowrap, contentPrefix + corpusText, comment);
    121   }
    122 
    123   /**
    124    * All of the entries in the zip file, in the order in which their local entries appear in the
    125    * file.
    126    */
    127   public static final List<UnitTestZipEntry> allEntriesInFileOrder =
    128       Collections.unmodifiableList(
    129           Arrays.asList(new UnitTestZipEntry[] {entry1, entry2, entry3, entry4}));
    130 
    131   // At class load time, ensure that it is safe to use this class for other tests.
    132   static {
    133     try {
    134       verifyTestZip(makeTestZip());
    135     } catch (Exception e) {
    136       throw new RuntimeException("Core sanity test 1 has failed, unit tests are unreliable", e);
    137     }
    138   }
    139 
    140   /**
    141    * Make a test ZIP file in memory and return it as a byte array. The ZIP contains the entries
    142    * described by {@link #entry1}, {@link #entry2}, {@link #entry3}, and {@link #entry4}. In
    143    * general, unit tests should use this data for all testing.
    144    * @return the zip file described above, as a byte array
    145    */
    146   public static byte[] makeTestZip() {
    147     return makeTestZip(allEntriesInFileOrder);
    148   }
    149 
    150   /**
    151    * Make an arbitrary zip archive in memory using the specified entries.
    152    * @param entriesInFileOrder the entries
    153    * @return the zip file described above, as a byte array
    154    */
    155   public static byte[] makeTestZip(List<UnitTestZipEntry> entriesInFileOrder) {
    156     try {
    157       ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    158       ZipOutputStream zipOut = new ZipOutputStream(buffer);
    159       for (UnitTestZipEntry unitTestEntry : entriesInFileOrder) {
    160         ZipEntry zipEntry = new ZipEntry(unitTestEntry.path);
    161         zipOut.setLevel(unitTestEntry.level);
    162         CRC32 crc32 = new CRC32();
    163         byte[] uncompressedContent = unitTestEntry.getUncompressedBinaryContent();
    164         crc32.update(uncompressedContent);
    165         zipEntry.setCrc(crc32.getValue());
    166         zipEntry.setSize(uncompressedContent.length);
    167         if (unitTestEntry.level == 0) {
    168           zipOut.setMethod(ZipOutputStream.STORED);
    169           zipEntry.setCompressedSize(uncompressedContent.length);
    170         } else {
    171           zipOut.setMethod(ZipOutputStream.DEFLATED);
    172         }
    173         // Normalize MSDOS date/time fields to zero for reproducibility.
    174         zipEntry.setTime(0);
    175         if (unitTestEntry.comment != null) {
    176           zipEntry.setComment(unitTestEntry.comment);
    177         }
    178         zipOut.putNextEntry(zipEntry);
    179         zipOut.write(unitTestEntry.getUncompressedBinaryContent());
    180         zipOut.closeEntry();
    181       }
    182       zipOut.close();
    183       return buffer.toByteArray();
    184     } catch (IOException e) {
    185       // Should not happen as this is all in memory
    186       throw new RuntimeException("Unable to generate test zip!", e);
    187     }
    188   }
    189 
    190   /**
    191    * Verifies the test zip file created by {@link #makeTestZip()} or for sanity, so that the rest of
    192    * the tests can safely rely upon them. The outputs may be slightly different from platform to
    193    * platform due to, e.g., filesystem differences that affect the choice of string encoding or
    194    * filesystem attributes that are preserved (eg, NTFS versus POSIX).
    195    * @param data the data to verify
    196    * @throws Exception if verification fails
    197    */
    198   private static void verifyTestZip(byte[] data) throws Exception {
    199     ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(data));
    200     for (int x = 0; x < allEntriesInFileOrder.size(); x++) {
    201       ZipEntry zipEntry = zipIn.getNextEntry();
    202       checkEntry(zipEntry, zipIn);
    203       zipIn.closeEntry();
    204     }
    205     Assert.assertNull(zipIn.getNextEntry());
    206     zipIn.close();
    207   }
    208 
    209   /**
    210    * Save the test archive to a file.
    211    * @param file the file to write to
    212    * @throws IOException if unable to write the file
    213    */
    214   public static void saveTestZip(File file) throws IOException {
    215     FileOutputStream out = new FileOutputStream(file);
    216     out.write(makeTestZip());
    217     out.flush();
    218     out.close();
    219   }
    220 
    221   /**
    222    * Check that the specified entry is one of the test entries and that its content matches the
    223    * expected content. If this is the entry that is uncompressed, also asserts that it is in fact
    224    * uncompressed.
    225    * @param entry the entry to check
    226    * @param zipIn the input stream to read from
    227    * @throws IOException if anything goes wrong
    228    */
    229   private static void checkEntry(ZipEntry entry, ZipInputStream zipIn) throws IOException {
    230     // NB: File comments cannot be verified because the comments are in the central directory, which
    231     // is later in the stream.
    232     for (UnitTestZipEntry testEntry : allEntriesInFileOrder) {
    233       if (testEntry.path.equals(entry.getName())) {
    234         if (testEntry.level == 0) {
    235           // This entry should be uncompressed. So the "compressed" size should be the same as the
    236           // uncompressed size.
    237           Assert.assertEquals(0, entry.getMethod());
    238           Assert.assertEquals(
    239               testEntry.getUncompressedBinaryContent().length, entry.getCompressedSize());
    240         }
    241         ByteArrayOutputStream uncompressedData = new ByteArrayOutputStream();
    242         byte[] buffer = new byte[4096];
    243         int numRead = 0;
    244         while ((numRead = zipIn.read(buffer)) >= 0) {
    245           uncompressedData.write(buffer, 0, numRead);
    246         }
    247         Assert.assertArrayEquals(
    248             testEntry.getUncompressedBinaryContent(), uncompressedData.toByteArray());
    249         return;
    250       }
    251     }
    252     Assert.fail("entry unknown: " + entry.getName());
    253   }
    254 }
    255