1 package test; 2 /* 3 * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. 4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 * 6 * This code is free software; you can redistribute it and/or modify it 7 * under the terms of the GNU General Public License version 2 only, as 8 * published by the Free Software Foundation. 9 * 10 * This code is distributed in the hope that it will be useful, but WITHOUT 11 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 13 * version 2 for more details (a copy is included in the LICENSE file that 14 * accompanied this code). 15 * 16 * You should have received a copy of the GNU General Public License version 17 * 2 along with this work; if not, write to the Free Software Foundation, 18 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 19 * 20 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 21 * or visit www.oracle.com if you need additional information or have any 22 * questions. 23 * 24 */ 25 26 import org.testng.annotations.BeforeSuite; 27 import org.testng.annotations.Test; 28 import util.ZipFsBaseTest; 29 30 import java.io.InputStream; 31 import java.io.OutputStream; 32 import java.nio.file.*; 33 import java.util.Arrays; 34 import java.util.HashMap; 35 import java.util.Map; 36 import java.util.jar.*; 37 import java.util.jar.Attributes.Name; 38 import java.util.spi.ToolProvider; 39 import java.util.stream.Collectors; 40 import java.util.zip.ZipEntry; 41 42 import static org.testng.Assert.*; 43 44 /** 45 * @test 46 * @bug 8211917 47 * @summary Validate that Zip FS will always add META-INF/MANIFEST.MF to the 48 * beginning of a Zip file allowing the Manifest be found and processed 49 * by java.util.jar.JarInputStream. 50 51 */ 52 public class ManifestOrderTest extends ZipFsBaseTest { 53 54 // Manifest PATH within a JAR 55 public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; 56 57 // Manifests used by the tests 58 private static String MANIFEST_ENTRY; 59 private static String MANIFEST_ENTRY2; 60 61 // Manifest Attributes used by the tests 62 private static Map<Name, String> MANIFEST_ATTRS; 63 private static Map<Name, String> MANIFEST_ATTRS2; 64 65 // Used when the test does not expect to find a Manifest 66 private static final Map<Name, String> NO_ATTRIBUTES = Map.of(); 67 68 // JAR Tool via ToolProvider API 69 private static final ToolProvider JAR_TOOL = ToolProvider.findFirst("jar") 70 .orElseThrow(() -> new RuntimeException("jar tool not found") 71 ); 72 73 /** 74 * Create the Manifests and Map of attributes included in the Manifests 75 */ 76 @BeforeSuite 77 public void setup() { 78 String jdkVendor = System.getProperty("java.vendor"); 79 String jdkVersion = System.getProperty("java.version"); 80 String attributeKey = "Player"; 81 String attributeValue = "Rafael Nadal"; 82 String attributeKey2 = "Country"; 83 String attributeValue2 = "Spain"; 84 String jdkVendorVersion = jdkVersion + " (" + jdkVendor + ")"; 85 MANIFEST_ENTRY = "Manifest-Version: 1.0" 86 + System.lineSeparator() 87 + "Created-By: " + jdkVendorVersion 88 + System.lineSeparator() 89 + attributeKey + ": " + attributeValue 90 + System.lineSeparator(); 91 MANIFEST_ENTRY2 = MANIFEST_ENTRY 92 + attributeKey2 + ": " + attributeValue2 93 + System.lineSeparator(); 94 95 MANIFEST_ATTRS = 96 Map.of(Name.MANIFEST_VERSION, "1.0", 97 new Name("Created-By"), jdkVendorVersion, 98 new Name(attributeKey), attributeValue); 99 MANIFEST_ATTRS2 = new HashMap<>(); 100 MANIFEST_ATTRS.forEach(MANIFEST_ATTRS2::put); 101 MANIFEST_ATTRS2.put(new Name(attributeKey2), attributeValue2); 102 } 103 104 /** 105 * Validate that JarInputStream can find META-INF/MANIFEST.MF when its written 106 * as the first entry within a JAR using Zip FS 107 * 108 * @param env Zip FS properties to use when creating the Zip File 109 * @param compression The compression used when writing the entries 110 * @throws Exception If an error occurs 111 */ 112 @Test(dataProvider = "zipfsMap") 113 public void testJarWithManifestAddedFirst(final Map<String, String> env, 114 final int compression) 115 throws Exception { 116 final Path jarPath = generatePath(HERE, "test", ".jar"); 117 Files.deleteIfExists(jarPath); 118 // Create the initial JAR writing out the Manifest first 119 final Entry[] entries = newEntries(compression); 120 Entry manifest = Entry.of(MANIFEST_NAME, compression, MANIFEST_ENTRY); 121 zip(jarPath, env, manifest, entries[0], entries[1], entries[2]); 122 verify(jarPath, MANIFEST_ATTRS, compression, entries); 123 124 // Add an additional entry and re-verify 125 Entry e00 = Entry.of("Entry-00", compression, "Roger Federer"); 126 zip(jarPath, Map.of("noCompression", compression == ZipEntry.STORED), 127 e00); 128 verify(jarPath, MANIFEST_ATTRS, compression, entries[0], entries[1], 129 entries[2], e00); 130 } 131 132 /** 133 * Validate that JarInputStream can find META-INF/MANIFEST.MF when its written 134 * as the last entry within a JAR using Zip FS 135 * 136 * @param env Zip FS properties to use when creating the Zip File 137 * @param compression The compression used when writing the entries 138 * @throws Exception If an error occurs 139 */ 140 @Test(dataProvider = "zipfsMap") 141 public void testJarWithManifestAddedLast(final Map<String, String> env, 142 final int compression) throws Exception { 143 final Path jarPath = generatePath(HERE, "test", ".jar"); 144 Files.deleteIfExists(jarPath); 145 // Create the initial JAR writing out the Manifest last 146 final Entry[] entries = newEntries(compression); 147 Entry manifest = Entry.of(MANIFEST_NAME, compression, MANIFEST_ENTRY); 148 zip(jarPath, env, entries[0], entries[1], entries[2], manifest); 149 verify(jarPath, MANIFEST_ATTRS, compression, entries); 150 151 // Add an additional entry and re-verify 152 Entry e00 = Entry.of("Entry-00", compression, "Roger Federer"); 153 zip(jarPath, Map.of("noCompression", compression == ZipEntry.STORED), 154 e00); 155 verify(jarPath, MANIFEST_ATTRS, compression, entries[0], entries[1], 156 entries[2], e00); 157 } 158 159 /** 160 * Validate that JarInputStream can find META-INF/MANIFEST.MF when its written 161 * between other entries within a JAR using Zip FS 162 * 163 * @param env Zip FS properties to use when creating the Zip File 164 * @param compression The compression used when writing the entries 165 * @throws Exception If an error occurs 166 */ 167 @Test(dataProvider = "zipfsMap") 168 public void testJarWithManifestAddedInBetween(final Map<String, String> env, 169 final int compression) 170 throws Exception { 171 final Path jarPath = generatePath(HERE, "test", ".jar"); 172 Files.deleteIfExists(jarPath); 173 // Create the initial JAR writing out the Manifest in between other entries 174 final Entry[] entries = newEntries(compression); 175 Entry manifest = Entry.of(MANIFEST_NAME, compression, MANIFEST_ENTRY); 176 zip(jarPath, env, entries[0], entries[1], manifest, entries[2]); 177 verify(jarPath, MANIFEST_ATTRS, compression, entries); 178 179 // Add an additional entry and re-verify 180 Entry e00 = Entry.of("Entry-00", compression, "Roger Federer"); 181 zip(jarPath, Map.of("noCompression", compression == ZipEntry.STORED), 182 e00); 183 verify(jarPath, MANIFEST_ATTRS, compression, entries[0], entries[1], 184 entries[2], e00); 185 } 186 187 /** 188 * Validate that JarInputStream can read all entries from a JAR created 189 * using Zip FS without adding a Manifest 190 * 191 * @param env Zip FS properties to use when creating the Zip File 192 * @param compression The compression used when writing the entries 193 * @throws Exception If an error occurs 194 */ 195 @Test(dataProvider = "zipfsMap") 196 public void testJarWithNoManifest(final Map<String, String> env, 197 final int compression) throws Exception { 198 final Path jarPath = generatePath(HERE, "test", ".jar"); 199 Files.deleteIfExists(jarPath); 200 // Create the initial JAR writing without a Manifest 201 final Entry[] entries = newEntries(compression); 202 zip(jarPath, env, entries[0], entries[1], entries[2]); 203 verify(jarPath, NO_ATTRIBUTES, compression, entries); 204 205 // Add an additional entry and re-verify 206 Entry e00 = Entry.of("Entry-00", compression, "Roger Federer"); 207 zip(jarPath, Map.of("noCompression", compression == ZipEntry.STORED), 208 e00); 209 verify(jarPath, NO_ATTRIBUTES, compression, entries[0], entries[1], 210 entries[2], e00); 211 } 212 213 /** 214 * Validate that JarInputStream can read META-INF/MANIFEST.MF when the 215 * the Manfiest is copied to the JAR using Files::copy 216 * 217 * @param env Zip FS properties to use when creating the Zip File 218 * @param compression The compression used when writing the entries 219 * @throws Exception If an error occurs 220 */ 221 @Test(dataProvider = "zipfsMap") 222 public void testManifestCopiedFromOSFile(final Map<String, String> env, 223 final int compression) throws Exception { 224 final Path jarPath = generatePath(HERE, "test", ".jar"); 225 Files.deleteIfExists(jarPath); 226 final Path manifest = Paths.get(".", "test-MANIFEST.MF"); 227 Files.deleteIfExists(manifest); 228 229 // Create initial JAR without a Manifest 230 Files.writeString(manifest, MANIFEST_ENTRY); 231 final Entry[] entries = newEntries(compression); 232 zip(jarPath, env, entries[0], entries[1], entries[2]); 233 verify(jarPath, NO_ATTRIBUTES, compression, entries); 234 235 // Add the Manifest via Files::copy and verify 236 try (FileSystem zipfs = 237 FileSystems.newFileSystem(jarPath, env)) { 238 Files.copy(manifest, zipfs.getPath("META-INF", "MANIFEST.MF")); 239 } 240 verify(jarPath, MANIFEST_ATTRS, compression, entries); 241 } 242 243 /** 244 * Validate that JarInputStream can find META-INF/MANIFEST.MF when the 245 * entries are copyed from one JAR to another JAR using Zip FS and Files::copy 246 * 247 * @param env Zip FS properties to use when creating the Zip File 248 * @param compression The compression used when writing the entries 249 * @throws Exception If an error occurs 250 */ 251 @Test(dataProvider = "zipfsMap") 252 public void copyJarToJarTest(final Map<String, String> env, final int compression) 253 throws Exception { 254 final Path jarPath = generatePath(HERE, "test", ".jar"); 255 final Path jarPath2 = generatePath(HERE, "test", ".jar"); 256 Files.deleteIfExists(jarPath); 257 Files.deleteIfExists(jarPath2); 258 259 // Create initial JAR with a Manifest 260 final Entry[] entries = newEntries(compression); 261 Entry manifest = Entry.of(MANIFEST_NAME, compression, MANIFEST_ENTRY); 262 zip(jarPath, env, manifest, entries[0], entries[1], entries[2]); 263 verify(jarPath, MANIFEST_ATTRS, compression, entries); 264 265 // Create the another JAR via Files::copy and verify 266 try (FileSystem zipfs = FileSystems.newFileSystem(jarPath, env); 267 FileSystem zipfsTarget = FileSystems.newFileSystem(jarPath2, 268 Map.of("create", "true", "noCompression", 269 compression == ZipEntry.STORED))) { 270 Path mPath = zipfsTarget.getPath(manifest.name); 271 if (mPath.getParent() != null) { 272 Files.createDirectories(mPath.getParent()); 273 } 274 Files.copy(zipfs.getPath(manifest.name), mPath); 275 for (Entry e : entries) { 276 Path target = zipfsTarget.getPath(e.name); 277 if (target.getParent() != null) { 278 Files.createDirectories(target.getParent()); 279 } 280 Files.copy(zipfs.getPath(e.name), target); 281 } 282 } 283 verify(jarPath2, MANIFEST_ATTRS, compression, entries); 284 } 285 286 /** 287 * Validate a JAR created using the jar tool and is updated using Zip FS 288 * contains the expected entries and Manifest 289 * 290 * @param compression The compression used when writing the entries 291 * @throws Exception If an error occurs 292 */ 293 @Test(dataProvider = "compressionMethods") 294 public void testJarToolGeneratedJarWithManifest(final int compression) 295 throws Exception { 296 297 final Path jarPath = generatePath(HERE, "test", ".jar"); 298 Files.deleteIfExists(jarPath); 299 final Entry[] entries = newEntries(compression); 300 final Path tmpdir = Paths.get("tmp"); 301 rmdir(tmpdir); 302 Files.createDirectory(tmpdir); 303 // Create a directory to hold the files to bad added to the JAR 304 for (final Entry entry : entries) { 305 Path p = Path.of("tmp", entry.name); 306 if (p.getParent() != null) { 307 Files.createDirectories(p.getParent()); 308 } 309 Files.write(Path.of("tmp", entry.name), entry.bytes); 310 } 311 // Create a file containing the Manifest 312 final Path manifestFile = Paths.get(".", "test-jar-MANIFEST.MF"); 313 Files.deleteIfExists(manifestFile); 314 Files.writeString(manifestFile, MANIFEST_ENTRY); 315 316 // Create a JAR via the jar tool and verify 317 final int exitCode = JAR_TOOL.run(System.out, System.err, "cvfm" 318 + (compression == ZipEntry.STORED ? "0" : ""), 319 jarPath.getFileName().toString(), 320 manifestFile.toAbsolutePath().toString(), 321 "-C", tmpdir.toAbsolutePath().toString(), "."); 322 assertEquals(exitCode, 0, "jar tool exited with failure"); 323 verify(jarPath, MANIFEST_ATTRS, compression, entries); 324 325 // Add an additional entry and re-verify 326 Entry e00 = Entry.of("Entry-00", compression, "Roger Federer"); 327 zip(jarPath, Map.of("noCompression", compression == ZipEntry.STORED), 328 entries[0], entries[1], e00, entries[2]); 329 verify(jarPath, MANIFEST_ATTRS, compression, entries[0], entries[1], 330 e00, entries[2]); 331 } 332 333 /** 334 * Validate that JarInputStream can read all entries from a JAR created 335 * using Zip FS with a Manfifest created by java.util.jar.Manifest 336 * 337 * @param env Zip FS properties to use when creating the Zip File 338 * @param compression The compression used when writing the entries 339 * @throws Exception If an error occurs 340 */ 341 @Test(dataProvider = "zipfsMap") 342 public void createWithManifestTest(final Map<String, String> env, 343 final int compression) throws Exception { 344 Path jarPath = generatePath(HERE, "test", ".jar"); 345 Files.deleteIfExists(jarPath); 346 347 // Create the JAR and verify 348 Entry e00 = Entry.of("Entry-00", compression, "Indian Wells"); 349 try (FileSystem zipfs = FileSystems.newFileSystem(jarPath, env)) { 350 // Add our Manifest using Manifest::write 351 Path manifestPath = zipfs.getPath("META-INF", "MANIFEST.MF"); 352 if (manifestPath.getParent() != null) { 353 Files.createDirectories(manifestPath.getParent()); 354 } 355 try (final OutputStream os = Files.newOutputStream(manifestPath)) { 356 final Manifest manifest = new Manifest(); 357 final Attributes attributes = manifest.getMainAttributes(); 358 // Populate the Manifest Attributes 359 MANIFEST_ATTRS.forEach(attributes::put); 360 manifest.write(os); 361 } 362 Files.write(zipfs.getPath(e00.name), e00.bytes); 363 364 } 365 verify(jarPath, MANIFEST_ATTRS, compression, e00); 366 } 367 368 /** 369 * Validate that JarInputStream can find META-INF/MANIFEST.MF when its has 370 * been updated by Zip FS 371 * 372 * @param env Zip FS properties to use when creating the Zip File 373 * @param compression The compression used when writing the entries 374 * @throws Exception If an error occurs 375 */ 376 @Test(dataProvider = "zipfsMap") 377 public void updateManifestTest(final Map<String, String> env, 378 final int compression) throws Exception { 379 final Path jarPath = generatePath(HERE, "test", ".jar"); 380 Files.deleteIfExists(jarPath); 381 // Create the initial JAR with a Manifest 382 final Entry[] entries = newEntries(compression); 383 Entry manifest = Entry.of(MANIFEST_NAME, compression, MANIFEST_ENTRY); 384 zip(jarPath, env, manifest, entries[0], entries[1], entries[2]); 385 verify(jarPath, MANIFEST_ATTRS, compression, entries); 386 387 // Add an additional entry, update the Manfiest and re-verify 388 Entry e00 = Entry.of("Entry-00", compression, "Roger Federer"); 389 Entry revisedManifest = manifest.content(MANIFEST_ENTRY2); 390 zip(jarPath, Map.of("noCompression", compression == ZipEntry.STORED), 391 revisedManifest, e00); 392 verify(jarPath, MANIFEST_ATTRS2, compression, entries[0], entries[1], 393 entries[2], e00); 394 } 395 396 /** 397 * Create an Entry array used when creating a JAR using Zip FS 398 * 399 * @param compressionMethod The compression method used by the test 400 * @return the Entry array 401 */ 402 private static Entry[] newEntries(final int compressionMethod) { 403 return new Entry[]{new Entry("hello.txt", compressionMethod, "hello"), 404 new Entry("META-INF/bar/world.txt", compressionMethod, "world"), 405 new Entry("META-INF/greeting.txt", compressionMethod, "greeting")}; 406 } 407 408 /** 409 * Verify the entries including the Manifest in a JAR 410 * 411 * @param jar Path to the JAR 412 * @param attributes Is there a Manifest to check 413 * @param entries Entries to validate are in the JAR 414 * @param compression The compression used when writing the entries 415 * @throws Exception If an error occurs 416 */ 417 private static void verify(final Path jar, final Map<?, ?> attributes, 418 final int compression, Entry... entries) 419 throws Exception { 420 // If the compression method is not STORED, then use JarInputStream 421 // to validate the entries in the JAR. The current JAR/ZipInputStream 422 // implementation supports PKZIP version 2.04g which only supports 423 // bit 8 for DEFLATED entries(JDK-8143613). Zip FS will set this bit 424 // for STORED entries as is now allowed by the PKZIP spec, resulting 425 // in the error "only DEFLATED entries can have EXT descriptor" 426 if (ZipEntry.STORED != compression) { 427 try (final JarInputStream jis = 428 new JarInputStream(Files.newInputStream(jar))) { 429 // Verify the Manifest 430 validateManifest(attributes, jis.getManifest()); 431 // Verify the rest of the expected entries are present 432 final Map<String, Entry> expected = Arrays.stream(entries) 433 .collect(Collectors.toMap(entry -> entry.name, entry -> entry)); 434 JarEntry je = jis.getNextJarEntry(); 435 assertNotNull(je, "Jar is empty"); 436 while (je != null) { 437 if (je.isDirectory()) { 438 // skip directories 439 je = jis.getNextJarEntry(); 440 continue; 441 } 442 final Entry e = expected.remove(je.getName()); 443 assertNotNull(e, "Unexpected entry in jar "); 444 assertEquals(je.getMethod(), e.method, "Compression method mismatch"); 445 assertEquals(jis.readAllBytes(), e.bytes); 446 447 je = jis.getNextJarEntry(); 448 } 449 assertEquals(expected.size(), 0, "Missing entries in jar!"); 450 } 451 } 452 453 // Verify using JarFile 454 try (final JarFile jf = new JarFile(jar.toFile())) { 455 // Validate Manifest 456 validateManifest(attributes, jf.getManifest()); 457 for (Entry e : entries) { 458 JarEntry je = jf.getJarEntry(e.name); 459 assertNotNull(je, "Entry does not exist"); 460 if (DEBUG) { 461 System.out.printf("Entry Name: %s, method: %s, Expected Method: %s%n", 462 e.name, je.getMethod(), e.method); 463 } 464 assertEquals(e.method, je.getMethod(), "Compression methods mismatch"); 465 try (InputStream in = jf.getInputStream(je)) { 466 byte[] bytes = in.readAllBytes(); 467 if (DEBUG) { 468 System.out.printf("bytes= %s, actual=%s%n", 469 new String(bytes), new String(e.bytes)); 470 } 471 assertTrue(Arrays.equals(bytes, e.bytes), "Entries do not match"); 472 } 473 } 474 } 475 476 // Check entries with FileSystem API 477 try (FileSystem fs = FileSystems.newFileSystem(jar)) { 478 // Check entry count 479 Path top = fs.getPath("/"); 480 long count = Files.find(top, Integer.MAX_VALUE, 481 (path, attrs) -> attrs.isRegularFile()).count(); 482 assertEquals(entries.length + (!attributes.isEmpty() ? 1 : 0), count); 483 Path mf = fs.getPath("META-INF", "MANIFEST.MF"); 484 Manifest m = null; 485 486 if (!attributes.isEmpty()) { 487 assertTrue(Files.exists(mf)); 488 m = new Manifest(Files.newInputStream(mf)); 489 } 490 validateManifest(attributes, m); 491 492 // Check content of each entry 493 for (Entry e : entries) { 494 Path file = fs.getPath(e.name); 495 if (DEBUG) { 496 System.out.printf("Entry name = %s, bytes= %s, actual=%s%n", e.name, 497 new String(Files.readAllBytes(file)), new String(e.bytes)); 498 } 499 assertEquals(Files.readAllBytes(file), e.bytes); 500 } 501 } 502 } 503 504 /** 505 * Validate whether the Manifest contains the expected attributes 506 * 507 * @param attributes A Map containing the attributes expected in the Manifest; 508 * otherwise empty 509 * @param m The Manifest to validate 510 */ 511 private static void validateManifest(Map<?, ?> attributes, Manifest m) { 512 if (!attributes.isEmpty()) { 513 assertNotNull(m, "Manifest is missing!"); 514 Attributes attrs = m.getMainAttributes(); 515 attributes.forEach((k, v) -> 516 { 517 if (DEBUG) { 518 System.out.printf("Key: %s, Value: %s%n", k, v); 519 } 520 assertTrue(attrs.containsKey(k)); 521 assertEquals(v, attrs.get(k)); 522 }); 523 } else { 524 assertNull(m, "Manifest was found!"); 525 } 526 } 527 }