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 */ makeUnitTestZipEntry( String path, int level, String contentPrefix, String comment)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 */ makeUnitTestZipEntry( String path, int level, boolean nowrap, String contentPrefix, String comment)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 { makeTestZip()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 */ makeTestZip()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 */ makeTestZip(List<UnitTestZipEntry> entriesInFileOrder)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 */ verifyTestZip(byte[] data)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 */ saveTestZip(File file)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 */ checkEntry(ZipEntry entry, ZipInputStream zipIn)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