# HG changeset patch # User jpai # Date 1580541136 -19800 # Sat Feb 01 12:42:16 2020 +0530 # Node ID 3f00208f73cadf2ed496aa5a4aeeaed72ecb3800 # Parent 298f812083337f506c06e177df85b41786d2334a 8211917: (zipfs) Creating or updating a JAR file system should put the MANIFEST.MF at the start Reviewed-by: diff --git a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java --- a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java +++ b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java @@ -1729,8 +1729,27 @@ byte[] buf = null; Entry e; + final IndexNode manifestInode = getInode(getBytes("/META-INF/MANIFEST.MF")); + final Iterator inodeIterator = inodes.values().iterator(); + boolean manifestProcessed = false; // write loc - for (IndexNode inode : inodes.values()) { + while(inodeIterator.hasNext()) { + final IndexNode inode; + // write the manifest inode (if any) first in the loc so that the + // java.util.jar.JarInputStream can find it, since it expects it to be + // the first or second entry in the jar + if (manifestInode != null && !manifestProcessed) { + inode = manifestInode; + manifestProcessed = true; + } else { + // the manifest is either absent or we have already processed it, + // so we pick the next inode + inode = inodeIterator.next(); + // don't process the same (manifest) node more than once + if (inode == manifestInode) { + continue; + } + } if (inode instanceof Entry) { // an updated inode e = (Entry)inode; try { diff --git a/test/jdk/jdk/nio/zipfs/ZipFSManifestOrderTest.java b/test/jdk/jdk/nio/zipfs/ZipFSManifestOrderTest.java new file mode 100644 --- /dev/null +++ b/test/jdk/jdk/nio/zipfs/ZipFSManifestOrderTest.java @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + * + */ + +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; + +/** + * @test + * @bug 8211917 + * @summary Test that irrespective of which order the META-INF/MANIFEST.MF gets added using the FileSystem APIs, + * the underlying zipfs implementation always places it at the start of the zip to allow for java.util.jar.JarInputStream + * to be able to locate it. + * @run testng ZipFSManifestOrderTest + */ +public class ZipFSManifestOrderTest { + + private static final String MANIFEST_MAIN_CLASS = "foo.bar.Baz"; + private static final String[] EXPECTED_PATHS = {"hello.txt", "META-INF/bar/world.txt", "META-INF/greeting.txt"}; + + /** + * Use the FileSystem APIs to first add the manifest and then the rest of the files, to a jar. Then verify that the + * JarInputStream finds the manifest and the other expected entries. + * + * @throws Exception + */ + @Test + public void testJarWithManifestAddedFirst() throws Exception { + final Path jarPath = Paths.get("manifest-first.jar"); + Files.deleteIfExists(jarPath); + final Map options = Collections.singletonMap("create", "true"); + try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), options)) { + // first write the manifest + final Path manifestPath = zipFs.getPath("META-INF", "MANIFEST.MF"); + writeManifest(manifestPath); + + // now write some other files + for (final String path : EXPECTED_PATHS) { + writeTxtFile(zipFs.getPath(path)); + } + } + verifyWithJarInputStream(jarPath, true); + + // now repeat the whole test again, on the same jar, but this time update the jar instead of creating it new + try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), Collections.emptyMap())) { + // first write the manifest + final Path manifestPath = zipFs.getPath("META-INF", "MANIFEST.MF"); + writeManifest(manifestPath); + + // now write some other files + for (final String path : EXPECTED_PATHS) { + writeTxtFile(zipFs.getPath(path)); + } + } + verifyWithJarInputStream(jarPath, true); + } + + /** + * Use the FileSystem APIs to add the manifest last, to a jar. Then verify that the JarInputStream finds the + * manifest and the other expected entries. + * + * @throws Exception + */ + @Test + public void testJarWithManifestAddedLast() throws Exception { + final Path jarPath = Paths.get("manifest-last.jar"); + Files.deleteIfExists(jarPath); + final Map options = Collections.singletonMap("create", "true"); + try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), options)) { + // first write other files + for (final String path : EXPECTED_PATHS) { + writeTxtFile(zipFs.getPath(path)); + } + // now write the manifest + final Path manifestPath = zipFs.getPath("META-INF", "MANIFEST.MF"); + writeManifest(manifestPath); + + } + verifyWithJarInputStream(jarPath, true); + + // now repeat the whole test again, on the same jar, but this time update the jar instead of creating it new + try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), Collections.emptyMap())) { + // first write other files + for (final String path : EXPECTED_PATHS) { + writeTxtFile(zipFs.getPath(path)); + } + // now write the manifest + final Path manifestPath = zipFs.getPath("META-INF", "MANIFEST.MF"); + writeManifest(manifestPath); + + } + verifyWithJarInputStream(jarPath, true); + } + + /** + * Use the FileSystem APIs to add the manifest ordered somewhere in between among other files in a jar. Then verify that + * the JarInputStream finds the manifest and the other expected entries. + * + * @throws Exception + */ + @Test + public void testJarWithManifestAddedInBetween() throws Exception { + final Path jarPath = Paths.get("manifest-in-between.jar"); + Files.deleteIfExists(jarPath); + final Map options = Collections.singletonMap("create", "true"); + try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), options)) { + // first write other files + for (int i = 0; i < EXPECTED_PATHS.length; i++) { + writeTxtFile(zipFs.getPath(EXPECTED_PATHS[i])); + if (i == EXPECTED_PATHS.length - 2) { + // write out the manifest + final Path manifestPath = zipFs.getPath("META-INF", "MANIFEST.MF"); + writeManifest(manifestPath); + } + } + } + verifyWithJarInputStream(jarPath, true); + + // now repeat the whole test again, on the same jar, but this time update the jar instead of creating it new + try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), Collections.emptyMap())) { + // first write other files + for (int i = 0; i < EXPECTED_PATHS.length; i++) { + writeTxtFile(zipFs.getPath(EXPECTED_PATHS[i])); + if (i == EXPECTED_PATHS.length - 2) { + // write out the manifest + final Path manifestPath = zipFs.getPath("META-INF", "MANIFEST.MF"); + writeManifest(manifestPath); + } + } + } + verifyWithJarInputStream(jarPath, true); + } + + /** + * Use the FileSystem APIs to create a jar without any manifest. Then verify that the JarInputStream can find the + * expected entries in the jar and also doesn't find any manifest in it. + * + * @throws Exception + */ + @Test + public void testJarWithNoManifest() throws Exception { + final Path jarPath = Paths.get("no-manifest.jar"); + Files.deleteIfExists(jarPath); + final Map options = Collections.singletonMap("create", "true"); + try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), options)) { + for (int i = 0; i < EXPECTED_PATHS.length; i++) { + writeTxtFile(zipFs.getPath(EXPECTED_PATHS[i])); + } + } + verifyWithJarInputStream(jarPath, false); + + // now repeat the whole test again, on the same jar, but this time update the jar instead of creating it new + try (final FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + jarPath.toUri()), Collections.emptyMap())) { + for (int i = 0; i < EXPECTED_PATHS.length; i++) { + writeTxtFile(zipFs.getPath(EXPECTED_PATHS[i])); + } + } + verifyWithJarInputStream(jarPath, false); + } + + private static void writeTxtFile(final Path filePath) throws IOException { + if (filePath.getParent() != null) { + Files.createDirectories(filePath.getParent()); + } + try (final OutputStream os = Files.newOutputStream(filePath)) { + os.write(new byte[]{'a', 'b', 'c'}); + } + } + + private static void writeManifest(final Path manifestPath) throws IOException { + if (manifestPath.getParent() != null) { + Files.createDirectories(manifestPath.getParent()); + } + try (final OutputStream os = Files.newOutputStream(manifestPath)) { + final Manifest manifest = new Manifest(); + final Attributes attributes = manifest.getMainAttributes(); + attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + attributes.put(Attributes.Name.MAIN_CLASS, MANIFEST_MAIN_CLASS); + manifest.write(os); + } + } + + /** + * Verify that the given jar can be opened using JarInputStream and the JarInputStream is + * able to {@link JarInputStream#getManifest() locate the manifest}. Additionally, also + * verify that certain other expected entries in the jar, are found by the JarInputStream + * + * @param jar Path to the jar + * @throws IOException + */ + private static void verifyWithJarInputStream(final Path jar, final boolean expectManifest) throws IOException { + try (final JarInputStream jaris = new JarInputStream(Files.newInputStream(jar))) { + // verify the manifest is present + final Manifest manifest = jaris.getManifest(); + if (expectManifest) { + assertNotNull(manifest, "JarInputStream couldn't locate a manifest in " + jar); + final String mainClass = manifest.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS); + assertEquals(mainClass, MANIFEST_MAIN_CLASS, "Unexpected Main-Class in manifest of " + jar); + } else { + assertNull(manifest, "JarInputStream unexpectedly found a manifest in " + jar); + } + // verify the rest of the expected entries are present + final Set missing = new HashSet<>(Arrays.asList(EXPECTED_PATHS)); + ZipEntry entry = jaris.getNextEntry(); + while (entry != null) { + missing.remove(entry.getName()); + entry = jaris.getNextEntry(); + } + assertEquals(missing.size(), 0, "Missing entries " + missing + " in jar " + jar); + } + } +}