1 /* 2 * Copyright 2017 Google Inc. All Rights Reserved. 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 17 package com.google.turbine.zip; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static java.nio.charset.StandardCharsets.UTF_8; 21 import static org.junit.Assert.assertThrows; 22 23 import com.google.common.collect.ImmutableMap; 24 import com.google.common.hash.Hashing; 25 import java.io.IOException; 26 import java.net.URI; 27 import java.nio.ByteBuffer; 28 import java.nio.ByteOrder; 29 import java.nio.file.FileSystem; 30 import java.nio.file.FileSystems; 31 import java.nio.file.Files; 32 import java.nio.file.Path; 33 import java.nio.file.StandardOpenOption; 34 import java.nio.file.attribute.FileTime; 35 import java.util.Enumeration; 36 import java.util.LinkedHashMap; 37 import java.util.Map; 38 import java.util.jar.JarEntry; 39 import java.util.jar.JarFile; 40 import java.util.jar.JarOutputStream; 41 import java.util.zip.ZipException; 42 import java.util.zip.ZipFile; 43 import java.util.zip.ZipOutputStream; 44 import org.junit.Rule; 45 import org.junit.Test; 46 import org.junit.rules.TemporaryFolder; 47 import org.junit.runner.RunWith; 48 import org.junit.runners.JUnit4; 49 50 /** {@link Zip}Test */ 51 @RunWith(JUnit4.class) 52 public class ZipTest { 53 54 @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); 55 56 @Test testEntries()57 public void testEntries() throws IOException { 58 testEntries(1000); 59 } 60 61 @Test zip64_testEntries()62 public void zip64_testEntries() throws IOException { 63 testEntries(70000); 64 } 65 66 @Test compression()67 public void compression() throws IOException { 68 Path path = temporaryFolder.newFile("test.jar").toPath(); 69 try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(path))) { 70 for (int i = 0; i < 2; i++) { 71 String name = "entry" + i; 72 byte[] bytes = name.getBytes(UTF_8); 73 jos.putNextEntry(new JarEntry(name)); 74 jos.write(bytes); 75 } 76 } 77 assertThat(actual(path)).isEqualTo(expected(path)); 78 } 79 testEntries(int entries)80 private void testEntries(int entries) throws IOException { 81 Path path = temporaryFolder.newFile("test.jar").toPath(); 82 try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(path))) { 83 for (int i = 0; i < entries; i++) { 84 String name = "entry" + i; 85 byte[] bytes = name.getBytes(UTF_8); 86 createEntry(jos, name, bytes); 87 } 88 } 89 assertThat(actual(path)).isEqualTo(expected(path)); 90 } 91 createEntry(ZipOutputStream jos, String name, byte[] bytes)92 private static void createEntry(ZipOutputStream jos, String name, byte[] bytes) 93 throws IOException { 94 JarEntry je = new JarEntry(name); 95 je.setMethod(JarEntry.STORED); 96 je.setSize(bytes.length); 97 je.setCrc(Hashing.crc32().hashBytes(bytes).padToLong()); 98 jos.putNextEntry(je); 99 jos.write(bytes); 100 } 101 actual(Path path)102 private static Map<String, Long> actual(Path path) throws IOException { 103 Map<String, Long> result = new LinkedHashMap<>(); 104 for (Zip.Entry e : new Zip.ZipIterable(path)) { 105 result.put(e.name(), Hashing.goodFastHash(128).hashBytes(e.data()).padToLong()); 106 } 107 return result; 108 } 109 expected(Path path)110 private static Map<String, Long> expected(Path path) throws IOException { 111 Map<String, Long> result = new LinkedHashMap<>(); 112 try (JarFile jf = new JarFile(path.toFile())) { 113 Enumeration<JarEntry> entries = jf.entries(); 114 while (entries.hasMoreElements()) { 115 JarEntry je = entries.nextElement(); 116 result.put( 117 je.getName(), 118 Hashing.goodFastHash(128).hashBytes(jf.getInputStream(je).readAllBytes()).padToLong()); 119 } 120 } 121 return result; 122 } 123 124 @Test attributes()125 public void attributes() throws Exception { 126 Path path = temporaryFolder.newFile("test.jar").toPath(); 127 Files.delete(path); 128 try (FileSystem fs = 129 FileSystems.newFileSystem( 130 URI.create("jar:" + path.toUri()), ImmutableMap.of("create", "true"))) { 131 for (int i = 0; i < 3; i++) { 132 String name = "entry" + i; 133 byte[] bytes = name.getBytes(UTF_8); 134 Path entry = fs.getPath(name); 135 Files.write(entry, bytes); 136 Files.setLastModifiedTime(entry, FileTime.fromMillis(0)); 137 } 138 } 139 assertThat(actual(path)).isEqualTo(expected(path)); 140 } 141 142 @Test zipFileCommentsAreSupported()143 public void zipFileCommentsAreSupported() throws Exception { 144 Path path = temporaryFolder.newFile("test.jar").toPath(); 145 Files.delete(path); 146 try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(path))) { 147 createEntry(zos, "hello", "world".getBytes(UTF_8)); 148 zos.setComment("this is a comment"); 149 } 150 assertThat(actual(path)).isEqualTo(expected(path)); 151 } 152 153 @Test malformedComment()154 public void malformedComment() throws Exception { 155 Path path = temporaryFolder.newFile("test.jar").toPath(); 156 Files.delete(path); 157 158 try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(path))) { 159 createEntry(zos, "hello", "world".getBytes(UTF_8)); 160 zos.setComment("this is a comment"); 161 } 162 Files.writeString(path, "trailing garbage", StandardOpenOption.APPEND); 163 164 ZipException e = assertThrows(ZipException.class, () -> actual(path)); 165 assertThat(e).hasMessageThat().isEqualTo("zip file comment length was 33, expected 17"); 166 } 167 168 // Create a zip64 archive with an extensible data sector 169 @Test zip64extension()170 public void zip64extension() throws IOException { 171 172 ByteBuffer buf = ByteBuffer.allocate(1000); 173 buf.order(ByteOrder.LITTLE_ENDIAN); 174 175 // The jar has a single entry named 'hello', with the value 'world' 176 byte[] name = "hello".getBytes(UTF_8); 177 byte[] value = "world".getBytes(UTF_8); 178 int crc = Hashing.crc32().hashBytes(value).asInt(); 179 180 int localHeaderPosition = buf.position(); 181 182 // local file header signature 4 bytes (0x04034b50) 183 buf.putInt((int) ZipFile.LOCSIG); 184 // version needed to extract 2 bytes 185 buf.putShort((short) 0); 186 // general purpose bit flag 2 bytes 187 buf.putShort((short) 0); 188 // compression method 2 bytes 189 buf.putShort((short) 0); 190 // last mod file time 2 bytes 191 buf.putShort((short) 0); 192 // last mod file date 2 bytes 193 buf.putShort((short) 0); 194 // crc-32 4 bytes 195 buf.putInt(crc); 196 // compressed size 4 bytes 197 buf.putInt(value.length); 198 // uncompressed size 4 bytes 199 buf.putInt(value.length); 200 // file name length 2 bytes 201 buf.putShort((short) name.length); 202 // extra field length 2 bytes 203 buf.putShort((short) 0); 204 // file name (variable size) 205 buf.put(name); 206 // extra field (variable size) 207 // file data 208 buf.put(value); 209 210 int centralDirectoryPosition = buf.position(); 211 212 // central file header signature 4 bytes (0x02014b50) 213 buf.putInt((int) ZipFile.CENSIG); 214 // version made by 2 bytes 215 buf.putShort((short) 0); 216 // version needed to extract 2 bytes 217 buf.putShort((short) 0); 218 // general purpose bit flag 2 bytes 219 buf.putShort((short) 0); 220 // compression method 2 bytes 221 buf.putShort((short) 0); 222 // last mod file time 2 bytes 223 buf.putShort((short) 0); 224 // last mod file date 2 bytes 225 buf.putShort((short) 0); 226 // crc-32 4 bytes 227 buf.putInt(crc); 228 // compressed size 4 bytes 229 buf.putInt(value.length); 230 // uncompressed size 4 bytes 231 buf.putInt(value.length); 232 // file name length 2 bytes 233 buf.putShort((short) name.length); 234 // extra field length 2 bytes 235 buf.putShort((short) 0); 236 // file comment length 2 bytes 237 buf.putShort((short) 0); 238 // disk number start 2 bytes 239 buf.putShort((short) 0); 240 // internal file attributes 2 bytes 241 buf.putShort((short) 0); 242 // external file attributes 4 bytes 243 buf.putInt(0); 244 // relative offset of local header 4 bytes 245 buf.putInt(localHeaderPosition); 246 // file name (variable size) 247 buf.put(name); 248 249 int centralDirectorySize = buf.position() - centralDirectoryPosition; 250 int zip64eocdPosition = buf.position(); 251 252 // zip64 end of central dir 253 // signature 4 bytes (0x06064b50) 254 buf.putInt(Zip.ZIP64_ENDSIG); 255 // size of zip64 end of central 256 // directory record 8 bytes 257 buf.putLong(Zip.ZIP64_ENDSIZ + 5); 258 // version made by 2 bytes 259 buf.putShort((short) 0); 260 // version needed to extract 2 bytes 261 buf.putShort((short) 0); 262 // number of this disk 4 bytes 263 buf.putInt(0); 264 // number of the disk with the 265 // start of the central directory 4 bytes 266 buf.putInt(0); 267 // total number of entries in the 268 // central directory on this disk 8 bytes 269 buf.putLong(1); 270 // total number of entries in the 271 // central directory 8 bytes 272 buf.putLong(1); 273 // size of the central directory 8 bytes 274 buf.putLong(centralDirectorySize); 275 // offset of start of central 276 // directory with respect to 277 // offset of start of central 278 // the starting disk number 8 bytes 279 buf.putLong(centralDirectoryPosition); 280 // zip64 extensible data sector (variable size) 281 buf.put((byte) 3); 282 buf.putInt(42); 283 284 // zip64 end of central dir locator 285 // signature 4 bytes (0x07064b50) 286 buf.putInt(Zip.ZIP64_LOCSIG); 287 // number of the disk with the 288 // start of the zip64 end of 289 // central directory 4 bytes 290 buf.putInt(0); 291 // relative offset of the zip64 292 // end of central directory record 8 bytes 293 buf.putLong(zip64eocdPosition); 294 // total number of disks 4 bytes 295 buf.putInt(0); 296 297 // end of central dir signature 4 bytes (0x06054b50) 298 buf.putInt((int) ZipFile.ENDSIG); 299 // number of this disk 2 bytes 300 buf.putShort((short) 0); 301 // number of the disk with the 302 // start of the central directory 2 bytes 303 buf.putShort((short) 0); 304 // total number of entries in the 305 // central directory on this disk 2 bytes 306 buf.putShort((short) 1); 307 // total number of entries in 308 // the central directory 2 bytes 309 buf.putShort((short) Zip.ZIP64_MAGICCOUNT); 310 // size of the central directory 4 bytes 311 buf.putInt(centralDirectorySize); 312 // offset of start of central 313 // directory with respect to 314 // the starting disk number 4 bytes 315 buf.putInt(centralDirectoryPosition); 316 // .ZIP file comment length 2 bytes 317 buf.putShort((short) 0); 318 // .ZIP file comment (variable size) 319 320 byte[] bytes = new byte[buf.position()]; 321 buf.rewind(); 322 buf.get(bytes); 323 Path path = temporaryFolder.newFile("test.jar").toPath(); 324 Files.write(path, bytes); 325 assertThat(actual(path)).isEqualTo(expected(path)); 326 } 327 } 328