--- /dev/null 2020-02-27 14:52:38.000000000 -0500 +++ new/test/jdk/jdk/nio/zipfs/testng/test/ManifestOrderTest.java 2020-02-27 14:52:37.000000000 -0500 @@ -0,0 +1,527 @@ +package test; +/* + * 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.BeforeSuite; +import org.testng.annotations.Test; +import util.ZipFsBaseTest; + +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.*; +import java.util.jar.Attributes.Name; +import java.util.spi.ToolProvider; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; + +import static org.testng.Assert.*; + +/** + * @test + * @bug 8211917 + * @summary Validate that Zip FS will always add META-INF/MANIFEST.MF to the + * beginning of a Zip file allowing the Manifest be found and processed + * by java.util.jar.JarInputStream. + + */ +public class ManifestOrderTest extends ZipFsBaseTest { + + // Manifest PATH within a JAR + public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; + + // Manifests used by the tests + private static String MANIFEST_ENTRY; + private static String MANIFEST_ENTRY2; + + // Manifest Attributes used by the tests + private static Map MANIFEST_ATTRS; + private static Map MANIFEST_ATTRS2; + + // Used when the test does not expect to find a Manifest + private static final Map NO_ATTRIBUTES = Map.of(); + + // JAR Tool via ToolProvider API + private static final ToolProvider JAR_TOOL = ToolProvider.findFirst("jar") + .orElseThrow(() -> new RuntimeException("jar tool not found") + ); + + /** + * Create the Manifests and Map of attributes included in the Manifests + */ + @BeforeSuite + public void setup() { + String jdkVendor = System.getProperty("java.vendor"); + String jdkVersion = System.getProperty("java.version"); + String attributeKey = "Player"; + String attributeValue = "Rafael Nadal"; + String attributeKey2 = "Country"; + String attributeValue2 = "Spain"; + String jdkVendorVersion = jdkVersion + " (" + jdkVendor + ")"; + MANIFEST_ENTRY = "Manifest-Version: 1.0" + + System.lineSeparator() + + "Created-By: " + jdkVendorVersion + + System.lineSeparator() + + attributeKey + ": " + attributeValue + + System.lineSeparator(); + MANIFEST_ENTRY2 = MANIFEST_ENTRY + + attributeKey2 + ": " + attributeValue2 + + System.lineSeparator(); + + MANIFEST_ATTRS = + Map.of(Name.MANIFEST_VERSION, "1.0", + new Name("Created-By"), jdkVendorVersion, + new Name(attributeKey), attributeValue); + MANIFEST_ATTRS2 = new HashMap<>(); + MANIFEST_ATTRS.forEach(MANIFEST_ATTRS2::put); + MANIFEST_ATTRS2.put(new Name(attributeKey2), attributeValue2); + } + + /** + * Validate that JarInputStream can find META-INF/MANIFEST.MF when its written + * as the first entry within a JAR using Zip FS + * + * @param env Zip FS properties to use when creating the Zip File + * @param compression The compression used when writing the entries + * @throws Exception If an error occurs + */ + @Test(dataProvider = "zipfsMap") + public void testJarWithManifestAddedFirst(final Map env, + final int compression) + throws Exception { + final Path jarPath = generatePath(HERE, "test", ".jar"); + Files.deleteIfExists(jarPath); + // Create the initial JAR writing out the Manifest first + final Entry[] entries = newEntries(compression); + Entry manifest = Entry.of(MANIFEST_NAME, compression, MANIFEST_ENTRY); + zip(jarPath, env, manifest, entries[0], entries[1], entries[2]); + verify(jarPath, MANIFEST_ATTRS, compression, entries); + + // Add an additional entry and re-verify + Entry e00 = Entry.of("Entry-00", compression, "Roger Federer"); + zip(jarPath, Map.of("noCompression", compression == ZipEntry.STORED), + e00); + verify(jarPath, MANIFEST_ATTRS, compression, entries[0], entries[1], + entries[2], e00); + } + + /** + * Validate that JarInputStream can find META-INF/MANIFEST.MF when its written + * as the last entry within a JAR using Zip FS + * + * @param env Zip FS properties to use when creating the Zip File + * @param compression The compression used when writing the entries + * @throws Exception If an error occurs + */ + @Test(dataProvider = "zipfsMap") + public void testJarWithManifestAddedLast(final Map env, + final int compression) throws Exception { + final Path jarPath = generatePath(HERE, "test", ".jar"); + Files.deleteIfExists(jarPath); + // Create the initial JAR writing out the Manifest last + final Entry[] entries = newEntries(compression); + Entry manifest = Entry.of(MANIFEST_NAME, compression, MANIFEST_ENTRY); + zip(jarPath, env, entries[0], entries[1], entries[2], manifest); + verify(jarPath, MANIFEST_ATTRS, compression, entries); + + // Add an additional entry and re-verify + Entry e00 = Entry.of("Entry-00", compression, "Roger Federer"); + zip(jarPath, Map.of("noCompression", compression == ZipEntry.STORED), + e00); + verify(jarPath, MANIFEST_ATTRS, compression, entries[0], entries[1], + entries[2], e00); + } + + /** + * Validate that JarInputStream can find META-INF/MANIFEST.MF when its written + * between other entries within a JAR using Zip FS + * + * @param env Zip FS properties to use when creating the Zip File + * @param compression The compression used when writing the entries + * @throws Exception If an error occurs + */ + @Test(dataProvider = "zipfsMap") + public void testJarWithManifestAddedInBetween(final Map env, + final int compression) + throws Exception { + final Path jarPath = generatePath(HERE, "test", ".jar"); + Files.deleteIfExists(jarPath); + // Create the initial JAR writing out the Manifest in between other entries + final Entry[] entries = newEntries(compression); + Entry manifest = Entry.of(MANIFEST_NAME, compression, MANIFEST_ENTRY); + zip(jarPath, env, entries[0], entries[1], manifest, entries[2]); + verify(jarPath, MANIFEST_ATTRS, compression, entries); + + // Add an additional entry and re-verify + Entry e00 = Entry.of("Entry-00", compression, "Roger Federer"); + zip(jarPath, Map.of("noCompression", compression == ZipEntry.STORED), + e00); + verify(jarPath, MANIFEST_ATTRS, compression, entries[0], entries[1], + entries[2], e00); + } + + /** + * Validate that JarInputStream can read all entries from a JAR created + * using Zip FS without adding a Manifest + * + * @param env Zip FS properties to use when creating the Zip File + * @param compression The compression used when writing the entries + * @throws Exception If an error occurs + */ + @Test(dataProvider = "zipfsMap") + public void testJarWithNoManifest(final Map env, + final int compression) throws Exception { + final Path jarPath = generatePath(HERE, "test", ".jar"); + Files.deleteIfExists(jarPath); + // Create the initial JAR writing without a Manifest + final Entry[] entries = newEntries(compression); + zip(jarPath, env, entries[0], entries[1], entries[2]); + verify(jarPath, NO_ATTRIBUTES, compression, entries); + + // Add an additional entry and re-verify + Entry e00 = Entry.of("Entry-00", compression, "Roger Federer"); + zip(jarPath, Map.of("noCompression", compression == ZipEntry.STORED), + e00); + verify(jarPath, NO_ATTRIBUTES, compression, entries[0], entries[1], + entries[2], e00); + } + + /** + * Validate that JarInputStream can read META-INF/MANIFEST.MF when the + * the Manfiest is copied to the JAR using Files::copy + * + * @param env Zip FS properties to use when creating the Zip File + * @param compression The compression used when writing the entries + * @throws Exception If an error occurs + */ + @Test(dataProvider = "zipfsMap") + public void testManifestCopiedFromOSFile(final Map env, + final int compression) throws Exception { + final Path jarPath = generatePath(HERE, "test", ".jar"); + Files.deleteIfExists(jarPath); + final Path manifest = Paths.get(".", "test-MANIFEST.MF"); + Files.deleteIfExists(manifest); + + // Create initial JAR without a Manifest + Files.writeString(manifest, MANIFEST_ENTRY); + final Entry[] entries = newEntries(compression); + zip(jarPath, env, entries[0], entries[1], entries[2]); + verify(jarPath, NO_ATTRIBUTES, compression, entries); + + // Add the Manifest via Files::copy and verify + try (FileSystem zipfs = + FileSystems.newFileSystem(jarPath, env)) { + Files.copy(manifest, zipfs.getPath("META-INF", "MANIFEST.MF")); + } + verify(jarPath, MANIFEST_ATTRS, compression, entries); + } + + /** + * Validate that JarInputStream can find META-INF/MANIFEST.MF when the + * entries are copyed from one JAR to another JAR using Zip FS and Files::copy + * + * @param env Zip FS properties to use when creating the Zip File + * @param compression The compression used when writing the entries + * @throws Exception If an error occurs + */ + @Test(dataProvider = "zipfsMap") + public void copyJarToJarTest(final Map env, final int compression) + throws Exception { + final Path jarPath = generatePath(HERE, "test", ".jar"); + final Path jarPath2 = generatePath(HERE, "test", ".jar"); + Files.deleteIfExists(jarPath); + Files.deleteIfExists(jarPath2); + + // Create initial JAR with a Manifest + final Entry[] entries = newEntries(compression); + Entry manifest = Entry.of(MANIFEST_NAME, compression, MANIFEST_ENTRY); + zip(jarPath, env, manifest, entries[0], entries[1], entries[2]); + verify(jarPath, MANIFEST_ATTRS, compression, entries); + + // Create the another JAR via Files::copy and verify + try (FileSystem zipfs = FileSystems.newFileSystem(jarPath, env); + FileSystem zipfsTarget = FileSystems.newFileSystem(jarPath2, + Map.of("create", "true", "noCompression", + compression == ZipEntry.STORED))) { + Path mPath = zipfsTarget.getPath(manifest.name); + if (mPath.getParent() != null) { + Files.createDirectories(mPath.getParent()); + } + Files.copy(zipfs.getPath(manifest.name), mPath); + for (Entry e : entries) { + Path target = zipfsTarget.getPath(e.name); + if (target.getParent() != null) { + Files.createDirectories(target.getParent()); + } + Files.copy(zipfs.getPath(e.name), target); + } + } + verify(jarPath2, MANIFEST_ATTRS, compression, entries); + } + + /** + * Validate a JAR created using the jar tool and is updated using Zip FS + * contains the expected entries and Manifest + * + * @param compression The compression used when writing the entries + * @throws Exception If an error occurs + */ + @Test(dataProvider = "compressionMethods") + public void testJarToolGeneratedJarWithManifest(final int compression) + throws Exception { + + final Path jarPath = generatePath(HERE, "test", ".jar"); + Files.deleteIfExists(jarPath); + final Entry[] entries = newEntries(compression); + final Path tmpdir = Paths.get("tmp"); + rmdir(tmpdir); + Files.createDirectory(tmpdir); + // Create a directory to hold the files to bad added to the JAR + for (final Entry entry : entries) { + Path p = Path.of("tmp", entry.name); + if (p.getParent() != null) { + Files.createDirectories(p.getParent()); + } + Files.write(Path.of("tmp", entry.name), entry.bytes); + } + // Create a file containing the Manifest + final Path manifestFile = Paths.get(".", "test-jar-MANIFEST.MF"); + Files.deleteIfExists(manifestFile); + Files.writeString(manifestFile, MANIFEST_ENTRY); + + // Create a JAR via the jar tool and verify + final int exitCode = JAR_TOOL.run(System.out, System.err, "cvfm" + + (compression == ZipEntry.STORED ? "0" : ""), + jarPath.getFileName().toString(), + manifestFile.toAbsolutePath().toString(), + "-C", tmpdir.toAbsolutePath().toString(), "."); + assertEquals(exitCode, 0, "jar tool exited with failure"); + verify(jarPath, MANIFEST_ATTRS, compression, entries); + + // Add an additional entry and re-verify + Entry e00 = Entry.of("Entry-00", compression, "Roger Federer"); + zip(jarPath, Map.of("noCompression", compression == ZipEntry.STORED), + entries[0], entries[1], e00, entries[2]); + verify(jarPath, MANIFEST_ATTRS, compression, entries[0], entries[1], + e00, entries[2]); + } + + /** + * Validate that JarInputStream can read all entries from a JAR created + * using Zip FS with a Manfifest created by java.util.jar.Manifest + * + * @param env Zip FS properties to use when creating the Zip File + * @param compression The compression used when writing the entries + * @throws Exception If an error occurs + */ + @Test(dataProvider = "zipfsMap") + public void createWithManifestTest(final Map env, + final int compression) throws Exception { + Path jarPath = generatePath(HERE, "test", ".jar"); + Files.deleteIfExists(jarPath); + + // Create the JAR and verify + Entry e00 = Entry.of("Entry-00", compression, "Indian Wells"); + try (FileSystem zipfs = FileSystems.newFileSystem(jarPath, env)) { + // Add our Manifest using Manifest::write + Path manifestPath = zipfs.getPath("META-INF", "MANIFEST.MF"); + 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(); + // Populate the Manifest Attributes + MANIFEST_ATTRS.forEach(attributes::put); + manifest.write(os); + } + Files.write(zipfs.getPath(e00.name), e00.bytes); + + } + verify(jarPath, MANIFEST_ATTRS, compression, e00); + } + + /** + * Validate that JarInputStream can find META-INF/MANIFEST.MF when its has + * been updated by Zip FS + * + * @param env Zip FS properties to use when creating the Zip File + * @param compression The compression used when writing the entries + * @throws Exception If an error occurs + */ + @Test(dataProvider = "zipfsMap") + public void updateManifestTest(final Map env, + final int compression) throws Exception { + final Path jarPath = generatePath(HERE, "test", ".jar"); + Files.deleteIfExists(jarPath); + // Create the initial JAR with a Manifest + final Entry[] entries = newEntries(compression); + Entry manifest = Entry.of(MANIFEST_NAME, compression, MANIFEST_ENTRY); + zip(jarPath, env, manifest, entries[0], entries[1], entries[2]); + verify(jarPath, MANIFEST_ATTRS, compression, entries); + + // Add an additional entry, update the Manfiest and re-verify + Entry e00 = Entry.of("Entry-00", compression, "Roger Federer"); + Entry revisedManifest = manifest.content(MANIFEST_ENTRY2); + zip(jarPath, Map.of("noCompression", compression == ZipEntry.STORED), + revisedManifest, e00); + verify(jarPath, MANIFEST_ATTRS2, compression, entries[0], entries[1], + entries[2], e00); + } + + /** + * Create an Entry array used when creating a JAR using Zip FS + * + * @param compressionMethod The compression method used by the test + * @return the Entry array + */ + private static Entry[] newEntries(final int compressionMethod) { + return new Entry[]{new Entry("hello.txt", compressionMethod, "hello"), + new Entry("META-INF/bar/world.txt", compressionMethod, "world"), + new Entry("META-INF/greeting.txt", compressionMethod, "greeting")}; + } + + /** + * Verify the entries including the Manifest in a JAR + * + * @param jar Path to the JAR + * @param attributes Is there a Manifest to check + * @param entries Entries to validate are in the JAR + * @param compression The compression used when writing the entries + * @throws Exception If an error occurs + */ + private static void verify(final Path jar, final Map attributes, + final int compression, Entry... entries) + throws Exception { + // If the compression method is not STORED, then use JarInputStream + // to validate the entries in the JAR. The current JAR/ZipInputStream + // implementation supports PKZIP version 2.04g which only supports + // bit 8 for DEFLATED entries(JDK-8143613). Zip FS will set this bit + // for STORED entries as is now allowed by the PKZIP spec, resulting + // in the error "only DEFLATED entries can have EXT descriptor" + if (ZipEntry.STORED != compression) { + try (final JarInputStream jis = + new JarInputStream(Files.newInputStream(jar))) { + // Verify the Manifest + validateManifest(attributes, jis.getManifest()); + // Verify the rest of the expected entries are present + final Map expected = Arrays.stream(entries) + .collect(Collectors.toMap(entry -> entry.name, entry -> entry)); + JarEntry je = jis.getNextJarEntry(); + assertNotNull(je, "Jar is empty"); + while (je != null) { + if (je.isDirectory()) { + // skip directories + je = jis.getNextJarEntry(); + continue; + } + final Entry e = expected.remove(je.getName()); + assertNotNull(e, "Unexpected entry in jar "); + assertEquals(je.getMethod(), e.method, "Compression method mismatch"); + assertEquals(jis.readAllBytes(), e.bytes); + + je = jis.getNextJarEntry(); + } + assertEquals(expected.size(), 0, "Missing entries in jar!"); + } + } + + // Verify using JarFile + try (final JarFile jf = new JarFile(jar.toFile())) { + // Validate Manifest + validateManifest(attributes, jf.getManifest()); + for (Entry e : entries) { + JarEntry je = jf.getJarEntry(e.name); + assertNotNull(je, "Entry does not exist"); + if (DEBUG) { + System.out.printf("Entry Name: %s, method: %s, Expected Method: %s%n", + e.name, je.getMethod(), e.method); + } + assertEquals(e.method, je.getMethod(), "Compression methods mismatch"); + try (InputStream in = jf.getInputStream(je)) { + byte[] bytes = in.readAllBytes(); + if (DEBUG) { + System.out.printf("bytes= %s, actual=%s%n", + new String(bytes), new String(e.bytes)); + } + assertTrue(Arrays.equals(bytes, e.bytes), "Entries do not match"); + } + } + } + + // Check entries with FileSystem API + try (FileSystem fs = FileSystems.newFileSystem(jar)) { + // Check entry count + Path top = fs.getPath("/"); + long count = Files.find(top, Integer.MAX_VALUE, + (path, attrs) -> attrs.isRegularFile()).count(); + assertEquals(entries.length + (!attributes.isEmpty() ? 1 : 0), count); + Path mf = fs.getPath("META-INF", "MANIFEST.MF"); + Manifest m = null; + + if (!attributes.isEmpty()) { + assertTrue(Files.exists(mf)); + m = new Manifest(Files.newInputStream(mf)); + } + validateManifest(attributes, m); + + // Check content of each entry + for (Entry e : entries) { + Path file = fs.getPath(e.name); + if (DEBUG) { + System.out.printf("Entry name = %s, bytes= %s, actual=%s%n", e.name, + new String(Files.readAllBytes(file)), new String(e.bytes)); + } + assertEquals(Files.readAllBytes(file), e.bytes); + } + } + } + + /** + * Validate whether the Manifest contains the expected attributes + * + * @param attributes A Map containing the attributes expected in the Manifest; + * otherwise empty + * @param m The Manifest to validate + */ + private static void validateManifest(Map attributes, Manifest m) { + if (!attributes.isEmpty()) { + assertNotNull(m, "Manifest is missing!"); + Attributes attrs = m.getMainAttributes(); + attributes.forEach((k, v) -> + { + if (DEBUG) { + System.out.printf("Key: %s, Value: %s%n", k, v); + } + assertTrue(attrs.containsKey(k)); + assertEquals(v, attrs.get(k)); + }); + } else { + assertNull(m, "Manifest was found!"); + } + } +}