1 /*
   2  * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  *
  23  */
  24 package test;
  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 copied 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 Manifest 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 it 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 }