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 
  25 import org.testng.annotations.Test;
  26 
  27 import java.io.IOException;
  28 import java.io.OutputStream;
  29 import java.net.URI;
  30 import java.nio.file.FileSystem;
  31 import java.nio.file.FileSystems;
  32 import java.nio.file.Files;
  33 import java.nio.file.Path;
  34 import java.nio.file.Paths;
  35 import java.util.Arrays;
  36 import java.util.Collections;
  37 import java.util.HashSet;
  38 import java.util.Map;
  39 import java.util.Set;
  40 import java.util.jar.Attributes;
  41 import java.util.jar.JarInputStream;
  42 import java.util.jar.Manifest;
  43 import java.util.zip.ZipEntry;
  44 
  45 import static org.testng.Assert.assertEquals;
  46 import static org.testng.Assert.assertNotNull;
  47 import static org.testng.Assert.assertNull;
  48 
  49 /**
  50  * @test
  51  * @bug 8211917
  52  * @summary Test that irrespective of which order the META-INF/MANIFEST.MF gets added using the FileSystem APIs,
  53  * the underlying zipfs implementation always places it at the start of the zip to allow for java.util.jar.JarInputStream
  54  * to be able to locate it.
  55  * @run testng ZipFSManifestOrderTest
  56  */
  57 public class ZipFSManifestOrderTest {
  58 
  59     private static final String MANIFEST_MAIN_CLASS = "foo.bar.Baz";
  60     private static final String[] EXPECTED_PATHS = {"hello.txt", "META-INF/bar/world.txt", "META-INF/greeting.txt"};
  61 
  62     /**
  63      * Use the FileSystem APIs to first add the manifest and then the rest of the files, to a jar. Then verify that the
  64      * JarInputStream finds the manifest and the other expected entries.
  65      *
  66      * @throws Exception
  67      */
  68     @Test
  69     public void testJarWithManifestAddedFirst() throws Exception {
  70         final Path jarPath = Paths.get("manifest-first.jar");
  71         Files.deleteIfExists(jarPath);
  72         final Map<String, String> options = Collections.singletonMap("create", "true");
  73         try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), options)) {
  74             // first write the manifest
  75             final Path manifestPath = zipFs.getPath("META-INF", "MANIFEST.MF");
  76             writeManifest(manifestPath);
  77 
  78             // now write some other files
  79             for (final String path : EXPECTED_PATHS) {
  80                 writeTxtFile(zipFs.getPath(path));
  81             }
  82         }
  83         verifyWithJarInputStream(jarPath, true);
  84 
  85         // now repeat the whole test again, on the same jar, but this time update the jar instead of creating it new
  86         try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), Collections.emptyMap())) {
  87             // first write the manifest
  88             final Path manifestPath = zipFs.getPath("META-INF", "MANIFEST.MF");
  89             writeManifest(manifestPath);
  90 
  91             // now write some other files
  92             for (final String path : EXPECTED_PATHS) {
  93                 writeTxtFile(zipFs.getPath(path));
  94             }
  95         }
  96         verifyWithJarInputStream(jarPath, true);
  97     }
  98 
  99     /**
 100      * Use the FileSystem APIs to add the manifest last, to a jar. Then verify that the JarInputStream finds the
 101      * manifest and the other expected entries.
 102      *
 103      * @throws Exception
 104      */
 105     @Test
 106     public void testJarWithManifestAddedLast() throws Exception {
 107         final Path jarPath = Paths.get("manifest-last.jar");
 108         Files.deleteIfExists(jarPath);
 109         final Map<String, String> options = Collections.singletonMap("create", "true");
 110         try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), options)) {
 111             // first write other files
 112             for (final String path : EXPECTED_PATHS) {
 113                 writeTxtFile(zipFs.getPath(path));
 114             }
 115             // now write the manifest
 116             final Path manifestPath = zipFs.getPath("META-INF", "MANIFEST.MF");
 117             writeManifest(manifestPath);
 118 
 119         }
 120         verifyWithJarInputStream(jarPath, true);
 121 
 122         // now repeat the whole test again, on the same jar, but this time update the jar instead of creating it new
 123         try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), Collections.emptyMap())) {
 124             // first write other files
 125             for (final String path : EXPECTED_PATHS) {
 126                 writeTxtFile(zipFs.getPath(path));
 127             }
 128             // now write the manifest
 129             final Path manifestPath = zipFs.getPath("META-INF", "MANIFEST.MF");
 130             writeManifest(manifestPath);
 131 
 132         }
 133         verifyWithJarInputStream(jarPath, true);
 134     }
 135 
 136     /**
 137      * Use the FileSystem APIs to add the manifest ordered somewhere in between among other files in a jar. Then verify that
 138      * the JarInputStream finds the manifest and the other expected entries.
 139      *
 140      * @throws Exception
 141      */
 142     @Test
 143     public void testJarWithManifestAddedInBetween() throws Exception {
 144         final Path jarPath = Paths.get("manifest-in-between.jar");
 145         Files.deleteIfExists(jarPath);
 146         final Map<String, String> options = Collections.singletonMap("create", "true");
 147         try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), options)) {
 148             // first write other files
 149             for (int i = 0; i < EXPECTED_PATHS.length; i++) {
 150                 writeTxtFile(zipFs.getPath(EXPECTED_PATHS[i]));
 151                 if (i == EXPECTED_PATHS.length - 2) {
 152                     // write out the manifest
 153                     final Path manifestPath = zipFs.getPath("META-INF", "MANIFEST.MF");
 154                     writeManifest(manifestPath);
 155                 }
 156             }
 157         }
 158         verifyWithJarInputStream(jarPath, true);
 159 
 160         // now repeat the whole test again, on the same jar, but this time update the jar instead of creating it new
 161         try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), Collections.emptyMap())) {
 162             // first write other files
 163             for (int i = 0; i < EXPECTED_PATHS.length; i++) {
 164                 writeTxtFile(zipFs.getPath(EXPECTED_PATHS[i]));
 165                 if (i == EXPECTED_PATHS.length - 2) {
 166                     // write out the manifest
 167                     final Path manifestPath = zipFs.getPath("META-INF", "MANIFEST.MF");
 168                     writeManifest(manifestPath);
 169                 }
 170             }
 171         }
 172         verifyWithJarInputStream(jarPath, true);
 173     }
 174 
 175     /**
 176      * Use the FileSystem APIs to create a jar without any manifest. Then verify that the JarInputStream can find the
 177      * expected entries in the jar and also doesn't find any manifest in it.
 178      *
 179      * @throws Exception
 180      */
 181     @Test
 182     public void testJarWithNoManifest() throws Exception {
 183         final Path jarPath = Paths.get("no-manifest.jar");
 184         Files.deleteIfExists(jarPath);
 185         final Map<String, String> options = Collections.singletonMap("create", "true");
 186         try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), options)) {
 187             for (int i = 0; i < EXPECTED_PATHS.length; i++) {
 188                 writeTxtFile(zipFs.getPath(EXPECTED_PATHS[i]));
 189             }
 190         }
 191         verifyWithJarInputStream(jarPath, false);
 192 
 193         // now repeat the whole test again, on the same jar, but this time update the jar instead of creating it new
 194         try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), Collections.emptyMap())) {
 195             for (int i = 0; i < EXPECTED_PATHS.length; i++) {
 196                 writeTxtFile(zipFs.getPath(EXPECTED_PATHS[i]));
 197             }
 198         }
 199         verifyWithJarInputStream(jarPath, false);
 200     }
 201 
 202     private static void writeTxtFile(final Path filePath) throws IOException {
 203         if (filePath.getParent() != null) {
 204             Files.createDirectories(filePath.getParent());
 205         }
 206         try (final OutputStream os = Files.newOutputStream(filePath)) {
 207             os.write(new byte[]{'a', 'b', 'c'});
 208         }
 209     }
 210 
 211     private static void writeManifest(final Path manifestPath) throws IOException {
 212         if (manifestPath.getParent() != null) {
 213             Files.createDirectories(manifestPath.getParent());
 214         }
 215         try (final OutputStream os = Files.newOutputStream(manifestPath)) {
 216             final Manifest manifest = new Manifest();
 217             final Attributes attributes = manifest.getMainAttributes();
 218             attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
 219             attributes.put(Attributes.Name.MAIN_CLASS, MANIFEST_MAIN_CLASS);
 220             manifest.write(os);
 221         }
 222     }
 223 
 224     /**
 225      * Verify that the given jar can be opened using JarInputStream and the JarInputStream is
 226      * able to {@link JarInputStream#getManifest() locate the manifest}. Additionally, also
 227      * verify that certain other expected entries in the jar, are found by the JarInputStream
 228      *
 229      * @param jar Path to the jar
 230      * @throws IOException
 231      */
 232     private static void verifyWithJarInputStream(final Path jar, final boolean expectManifest) throws IOException {
 233         try (final JarInputStream jaris = new JarInputStream(Files.newInputStream(jar))) {
 234             // verify the manifest is present
 235             final Manifest manifest = jaris.getManifest();
 236             if (expectManifest) {
 237                 assertNotNull(manifest, "JarInputStream couldn't locate a manifest in " + jar);
 238                 final String mainClass = manifest.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
 239                 assertEquals(mainClass, MANIFEST_MAIN_CLASS, "Unexpected Main-Class in manifest of " + jar);
 240             } else {
 241                 assertNull(manifest, "JarInputStream unexpectedly found a manifest in " + jar);
 242             }
 243             // verify the rest of the expected entries are present
 244             final Set<String> missing = new HashSet<>(Arrays.asList(EXPECTED_PATHS));
 245             ZipEntry entry = jaris.getNextEntry();
 246             while (entry != null) {
 247                 missing.remove(entry.getName());
 248                 entry = jaris.getNextEntry();
 249             }
 250             assertEquals(missing.size(), 0, "Missing entries " + missing + " in jar " + jar);
 251         }
 252     }
 253 }