From b85500e52479c48b02a96b28fddefa2b25d5d9bd Mon Sep 17 00:00:00 2001
From: Lance Andersen <lancea@openjdk.org>
Date: Fri, 12 Nov 2021 17:12:13 +0000
Subject: [PATCH] 8276123: ZipFile::getEntry will not return a file entry when
 there is a directory entry of the same name within a Zip File

Reviewed-by: redestad, alanb
---
 .../share/classes/java/util/zip/ZipFile.java  |  15 +-
 .../ZipFile/ZipFileDuplicateEntryTest.java    | 581 ++++++++++++++++++
 2 files changed, 591 insertions(+), 5 deletions(-)
 create mode 100644 test/jdk/java/util/zip/ZipFile/ZipFileDuplicateEntryTest.java

diff --git a/src/java.base/share/classes/java/util/zip/ZipFile.java b/src/java.base/share/classes/java/util/zip/ZipFile.java
index 8fc200ed1ae..47edf1ffae1 100644
--- a/src/java.base/share/classes/java/util/zip/ZipFile.java
+++ b/src/java.base/share/classes/java/util/zip/ZipFile.java
@@ -1631,13 +1631,18 @@ public class ZipFile implements ZipConstants, Closeable {
                         // slash
                         int entryLen = entry.length();
                         int nameLen = name.length();
-                        if ((entryLen == nameLen && entry.equals(name)) ||
-                                (addSlash &&
-                                nameLen + 1 == entryLen &&
-                                entry.startsWith(name) &&
-                                entry.charAt(entryLen - 1) == '/')) {
+                        if (entryLen == nameLen && entry.equals(name)) {
+                            // Found our match
                             return pos;
                         }
+                        // If addSlash is true we'll now test for name+/ providing
+                        if (addSlash && nameLen + 1 == entryLen
+                                && entry.startsWith(name) &&
+                                entry.charAt(entryLen - 1) == '/') {
+                            // Found the entry "name+/", now find the CEN entry pos
+                            int exactPos = getEntryPos(name, false);
+                            return exactPos == -1 ? pos : exactPos;
+                        }
                     } catch (IllegalArgumentException iae) {
                         // Ignore
                     }
diff --git a/test/jdk/java/util/zip/ZipFile/ZipFileDuplicateEntryTest.java b/test/jdk/java/util/zip/ZipFile/ZipFileDuplicateEntryTest.java
new file mode 100644
index 00000000000..95a223c8e1f
--- /dev/null
+++ b/test/jdk/java/util/zip/ZipFile/ZipFileDuplicateEntryTest.java
@@ -0,0 +1,581 @@
+/*
+ * Copyright (c) 2021, 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.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Formatter;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 8276123
+ * @summary ZipFile::getEntry will not return a file entry when there is a
+ * directory entry of the same name within a Zip File
+ * @run testng/othervm ZipFileDuplicateEntryTest
+ */
+public class ZipFileDuplicateEntryTest {
+
+    /**
+     * Name to use for creating Zip entries
+     */
+    private static final String ENTRY_NAME = "entry";
+
+    /**
+     * Zip and Jar files to be created
+     */
+    private static final Path ZIP_FILE = Paths.get("fileDirEntry.zip");
+    private static final Path ZIP_FILE2 = Paths.get("OnlyDirEntry.zip");
+    private static final Path DUPLICATE_FILE_ENTRY_FILE = Paths.get("DupFIleEntry.zip");
+    private static final Path TEST_JAR = Paths.get("fileDirEntry.jar");
+
+    /**
+     * Directory entry added to the Zip File.
+     */
+    private static final Entry DIR_ENTRY =
+            Entry.of(ENTRY_NAME + "/", ZipEntry.DEFLATED,
+                    "I am a Directory");
+
+    /**
+     * File entry added to the Zip File.
+     */
+    private static final Entry FILE_ENTRY =
+            Entry.of(ENTRY_NAME, ZipEntry.DEFLATED, "I am a File");
+
+    /**
+     * Duplicate File entry added to the Zip file. This is the 2nd entry added
+     * to the Zip file and is expected to be returned.
+     */
+    private static final Entry DUPLICATE_FILE_ENTRY =
+            Entry.of(ENTRY_NAME, ZipEntry.DEFLATED, "Yet another File");
+    /**
+     * Entries expected to be returned via ZipFile::stream
+     */
+    private static final List<String> EXPECTED_ENTRIES =
+            Arrays.asList(FILE_ENTRY.name, DIR_ENTRY.name);
+
+    /**
+     * Max buffer size for readAllBytes method which can be used when
+     * InputStream::readAllBytes is not available
+     */
+    private static final int MAX_BUFFER_SIZE = 1024;
+
+    /**
+     * Flag to enable test output
+     */
+    private static final boolean DEBUG = false;
+
+    /**
+     * Array representing a Jar File with the entries:
+     * Name: entry, contents: "I am a File"
+     * Name: entry, contents: "Yet another File"
+     * See createByteArray()
+     */
+    private static final byte[] DUPLICATE_ENTRY_JAR_BYTES = {
+            (byte) 0x50, (byte) 0x4b, (byte) 0x3, (byte) 0x4, (byte) 0x14, (byte) 0x0, (byte) 0x0, (byte) 0x8,
+            (byte) 0x8, (byte) 0x0, (byte) 0x60, (byte) 0x59, (byte) 0x55, (byte) 0x53, (byte) 0x8e, (byte) 0x39,
+            (byte) 0x14, (byte) 0x49, (byte) 0xd, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0xb, (byte) 0x0,
+            (byte) 0x0, (byte) 0x0, (byte) 0x5, (byte) 0x0, (byte) 0x14, (byte) 0x0, (byte) 0x65, (byte) 0x6e,
+            (byte) 0x74, (byte) 0x72, (byte) 0x79, (byte) 0x1, (byte) 0x0, (byte) 0x10, (byte) 0x0, (byte) 0xb,
+            (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0xd,
+            (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0xf3,
+            (byte) 0x54, (byte) 0x48, (byte) 0xcc, (byte) 0x55, (byte) 0x48, (byte) 0x54, (byte) 0x70, (byte) 0xcb,
+            (byte) 0xcc, (byte) 0x49, (byte) 0x5, (byte) 0x0, (byte) 0x50, (byte) 0x4b, (byte) 0x3, (byte) 0x4,
+            (byte) 0x14, (byte) 0x0, (byte) 0x0, (byte) 0x8, (byte) 0x8, (byte) 0x0, (byte) 0x60, (byte) 0x59,
+            (byte) 0x55, (byte) 0x53, (byte) 0xe1, (byte) 0x4c, (byte) 0x29, (byte) 0xa4, (byte) 0x12, (byte) 0x0,
+            (byte) 0x0, (byte) 0x0, (byte) 0x10, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x5, (byte) 0x0,
+            (byte) 0x14, (byte) 0x0, (byte) 0x65, (byte) 0x6e, (byte) 0x74, (byte) 0x72, (byte) 0x79, (byte) 0x1,
+            (byte) 0x0, (byte) 0x10, (byte) 0x0, (byte) 0x10, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0,
+            (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x12, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0,
+            (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x8b, (byte) 0x4c, (byte) 0x2d, (byte) 0x51, (byte) 0x48,
+            (byte) 0xcc, (byte) 0xcb, (byte) 0x2f, (byte) 0xc9, (byte) 0x48, (byte) 0x2d, (byte) 0x52, (byte) 0x70,
+            (byte) 0xcb, (byte) 0xcc, (byte) 0x49, (byte) 0x5, (byte) 0x0, (byte) 0x50, (byte) 0x4b, (byte) 0x1,
+            (byte) 0x2, (byte) 0x14, (byte) 0x0, (byte) 0x14, (byte) 0x0, (byte) 0x0, (byte) 0x8, (byte) 0x8,
+            (byte) 0x0, (byte) 0x60, (byte) 0x59, (byte) 0x55, (byte) 0x53, (byte) 0x8e, (byte) 0x39, (byte) 0x14,
+            (byte) 0x49, (byte) 0xd, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0xb, (byte) 0x0, (byte) 0x0,
+            (byte) 0x0, (byte) 0x5, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0,
+            (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0,
+            (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x65, (byte) 0x6e, (byte) 0x74, (byte) 0x72, (byte) 0x79,
+            (byte) 0x50, (byte) 0x4b, (byte) 0x1, (byte) 0x2, (byte) 0x14, (byte) 0x0, (byte) 0x14, (byte) 0x0,
+            (byte) 0x0, (byte) 0x8, (byte) 0x8, (byte) 0x0, (byte) 0x60, (byte) 0x59, (byte) 0x55, (byte) 0x53,
+            (byte) 0xe1, (byte) 0x4c, (byte) 0x29, (byte) 0xa4, (byte) 0x12, (byte) 0x0, (byte) 0x0, (byte) 0x0,
+            (byte) 0x10, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x5, (byte) 0x0, (byte) 0x0, (byte) 0x0,
+            (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0,
+            (byte) 0x0, (byte) 0x0, (byte) 0x44, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x65, (byte) 0x6e,
+            (byte) 0x74, (byte) 0x72, (byte) 0x79, (byte) 0x50, (byte) 0x4b, (byte) 0x5, (byte) 0x6, (byte) 0x0,
+            (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x2, (byte) 0x0, (byte) 0x2, (byte) 0x0, (byte) 0x66,
+            (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x8d, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0,
+            (byte) 0x0,
+    };
+
+    /**
+     * Create Zip files used by the tests.
+     *
+     * @throws IOException If an error occurs
+     */
+    @BeforeTest
+    public static void setup() throws IOException {
+
+        /**
+         *  Zip contains two entries named "entry" and "entry/"
+         */
+        Files.deleteIfExists(ZIP_FILE);
+        try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(ZIP_FILE))) {
+            zos.putNextEntry(new ZipEntry(FILE_ENTRY.name));
+            zos.write(FILE_ENTRY.bytes);
+            zos.closeEntry();
+            zos.putNextEntry(new ZipEntry(DIR_ENTRY.name));
+            zos.write(DIR_ENTRY.bytes);
+            zos.closeEntry();
+        }
+
+        /**
+         *  Jar contains two entries named "entry" and "entry/"
+         */
+        Files.deleteIfExists(TEST_JAR);
+        try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(TEST_JAR))) {
+            jos.putNextEntry(new JarEntry(FILE_ENTRY.name));
+            jos.write(FILE_ENTRY.bytes);
+            jos.closeEntry();
+            jos.putNextEntry(new JarEntry(DIR_ENTRY.name));
+            jos.write(DIR_ENTRY.bytes);
+            jos.closeEntry();
+        }
+
+        /**
+         *  Zip contains the entry "entry/"
+         */
+        Files.deleteIfExists(ZIP_FILE2);
+        try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(ZIP_FILE2))) {
+            zos.putNextEntry(new ZipEntry(DIR_ENTRY.name));
+            zos.write(DIR_ENTRY.bytes);
+            zos.closeEntry();
+        }
+
+        /**
+         *  Create a Jar that contains two entries named "entry"
+         */
+        Files.deleteIfExists(DUPLICATE_FILE_ENTRY_FILE);
+        Files.write(DUPLICATE_FILE_ENTRY_FILE, DUPLICATE_ENTRY_JAR_BYTES);
+    }
+
+    /**
+     * Clean up after the test run
+     *
+     * @throws IOException If an error occurs
+     */
+    @AfterTest
+    public static void cleanup() throws IOException {
+        Files.deleteIfExists(ZIP_FILE);
+        Files.deleteIfExists(ZIP_FILE2);
+        Files.deleteIfExists(DUPLICATE_FILE_ENTRY_FILE);
+        Files.deleteIfExists(TEST_JAR);
+    }
+
+    /**
+     * DataProvider used to specify the Zip entries to use
+     *
+     * @return The Entry to use within the test
+     */
+    @DataProvider
+    public Object[][] entries() {
+        return new Object[][]{
+                {FILE_ENTRY},
+                {DIR_ENTRY}
+        };
+    }
+
+    /**
+     * Test whether ZipFile::getEntry can find a directory entry within a Zip
+     * file specifying "name" vs "name/"
+     *
+     * @throws IOException If an error occurs
+     */
+    @Test
+    public void readDirWithoutSlash() throws IOException {
+        System.out.printf("%n%n**** readDirWithoutSlash ***%n");
+        try (ZipFile zip = new ZipFile(ZIP_FILE2.toString())) {
+            ZipEntry ze = zip.getEntry(ENTRY_NAME);
+            if (DEBUG) {
+                System.out.printf("    Entry:%s, found:%s%n", ENTRY_NAME, ze != null);
+            }
+            assertNotNull(ze);
+            assertTrue(ze.isDirectory());
+            try (InputStream in = zip.getInputStream(ze)) {
+                byte[] bytes = in.readAllBytes();
+                if (DEBUG) {
+                    System.out.printf("name: %s, isDirectory: %s, payload= %s%n",
+                            ze.getName(), ze.isDirectory(), new String(bytes));
+                }
+                assertEquals(bytes, DIR_ENTRY.bytes,
+                        String.format("Expected payload: %s",
+                                new String(DIR_ENTRY.bytes)));
+            }
+        }
+    }
+
+    /**
+     * Validate that ZipFile::getEntry will return the correct entry when a file
+     * and directory have the same name
+     *
+     * @param entry The entry to search for
+     * @throws IOException If an error occurs
+     */
+    @Test(dataProvider = "entries")
+    public void testSameFileDirEntryName(Entry entry) throws IOException {
+        System.out.printf("%n%n**** testSameFileDirEntryName ***%n");
+
+        try (ZipFile zip = new ZipFile(ZIP_FILE.toString())) {
+            ZipEntry ze = zip.getEntry(entry.name);
+            if (DEBUG) {
+                System.out.printf("    Entry:%s, found:%s%n", entry.name, ze != null);
+            }
+            assertNotNull(ze);
+            try (InputStream in = zip.getInputStream(ze)) {
+                byte[] bytes = in.readAllBytes();
+                if (DEBUG) {
+                    System.out.printf("name: %s, isDirectory: %s, payload= %s%n",
+                            ze.getName(), ze.isDirectory(), new String(bytes));
+                }
+                assertEquals(entry.bytes, bytes,
+                        String.format("Expected payload: %s", new String(entry.bytes)));
+            }
+        }
+    }
+
+    /**
+     * Validate that ZipFile::getEntry will return the correct entry, which
+     * is the second entry, when there are duplicate entries within the Zip file.
+     *
+     * @throws IOException If an error occurs
+     */
+    @Test
+    public void DupFileEntryTest() throws IOException {
+        System.out.printf("%n%n**** DupFileEntryTest ***%n");
+        try (ZipFile zip =
+                     new ZipFile(DUPLICATE_FILE_ENTRY_FILE.toString())) {
+            ZipEntry ze = zip.getEntry(ENTRY_NAME);
+            if (DEBUG) {
+                System.out.printf("    Entry:%s, found:%s%n", ENTRY_NAME, ze != null);
+            }
+            assertNotNull(ze);
+            try (InputStream in = zip.getInputStream(ze)) {
+                byte[] bytes = in.readAllBytes();
+                if (DEBUG) {
+                    System.out.printf("name: %s, isDirectory: %s, payload= %s%n",
+                            ze.getName(), ze.isDirectory(), new String(bytes));
+                }
+                assertEquals(bytes, DUPLICATE_FILE_ENTRY.bytes,
+                        String.format("Expected payload: %s", new String(DUPLICATE_FILE_ENTRY.bytes)));
+            }
+        }
+    }
+
+    /**
+     * Verify that ZipInputStream can be used to read all Zip entries including
+     * a file and directory entry with the same name
+     *
+     * @throws IOException If an error occurs
+     */
+    @Test
+    public void ZipInputStreamTest() throws IOException {
+        System.out.printf("%n%n**** ZipInputStreamTest ***%n");
+        try (ZipInputStream zis = new ZipInputStream(
+                new FileInputStream(ZIP_FILE.toFile()))) {
+            ZipEntry zipEntry = zis.getNextEntry();
+            assertNotNull(zipEntry);
+            while (zipEntry != null) {
+                Entry e;
+                if (zipEntry.getName().equals(FILE_ENTRY.name)) {
+                    e = FILE_ENTRY;
+                } else if (zipEntry.getName().equals(DIR_ENTRY.name)) {
+                    e = DIR_ENTRY;
+                } else {
+                    throw new RuntimeException(
+                            String.format("Invalid Zip entry: %s", zipEntry.getName()));
+                }
+                assertEquals(zipEntry.getMethod(), e.method);
+                assertEquals(zis.readAllBytes(), e.bytes,
+                        String.format("Expected payload: %s", new String(e.bytes)));
+                zipEntry = zis.getNextEntry();
+            }
+        }
+    }
+
+    /**
+     * Verify that ZipFile::stream returns all Zip entries including
+     * a file and directory entry with the same name
+     *
+     * @throws IOException If an error occurs
+     */
+    @Test
+    public void ZipFileStreamTest() throws IOException {
+        System.out.printf("%n%n**** ZipFileStreamTest ***%n");
+        try (ZipFile zf = new ZipFile(ZIP_FILE.toFile())) {
+            List<? extends ZipEntry> entries = zf.stream().collect(Collectors.toList());
+            assertEquals(EXPECTED_ENTRIES.size(), entries.size());
+            for (ZipEntry e : entries) {
+                assertTrue(EXPECTED_ENTRIES.contains(e.getName()));
+            }
+        }
+    }
+
+    /**
+     * Verify that JarFile can be used to read all the entries including
+     * a file and directory entry with the same name
+     *
+     * @param entry The entry to validate
+     * @throws IOException If an error occurs
+     */
+    @Test(dataProvider = "entries")
+    public static void JarFileInputStreamTest(Entry entry) throws IOException {
+        System.out.printf("%n%n**** JarFileInputStreamTest ***%n");
+        try (JarFile jarFile = new JarFile(TEST_JAR.toFile())) {
+            JarEntry je = jarFile.getJarEntry(entry.name);
+            assertNotNull(je);
+            if (DEBUG) {
+                System.out.printf("Entry Name: %s, method: %s, Expected Method: %s%n",
+                        entry.name, je.getMethod(), entry.method);
+            }
+            assertEquals(entry.method, je.getMethod());
+            try (InputStream in = jarFile.getInputStream(je)) {
+                byte[] bytes = in.readAllBytes();
+                if (DEBUG) {
+                    System.out.printf("bytes= %s, expected=%s%n",
+                            new String(bytes), new String(entry.bytes));
+                }
+                assertEquals(bytes, entry.bytes,
+                        String.format("Expected payload: %s", new String(entry.bytes)));
+            }
+        }
+    }
+
+    /**
+     * Verify that JarInputStream can be used to read all entries including
+     * a file and directory entry with the same name
+     *
+     * @throws IOException If an error occurs
+     */
+    @Test
+    public void JarInputStreamTest() throws IOException {
+        System.out.printf("%n%n**** JarInputStreamTest ***%n");
+        try (JarInputStream jis = new JarInputStream(
+                new FileInputStream(TEST_JAR.toFile()))) {
+            JarEntry jarEntry = jis.getNextJarEntry();
+            assertNotNull(jarEntry);
+            while (jarEntry != null) {
+                Entry e;
+                if (jarEntry.getName().equals(FILE_ENTRY.name)) {
+                    e = FILE_ENTRY;
+                } else if (jarEntry.getName().equals(DIR_ENTRY.name)) {
+                    e = DIR_ENTRY;
+                } else {
+                    throw new RuntimeException(
+                            String.format("Invalid Jar entry: %s", jarEntry.getName()));
+                }
+                assertEquals(jarEntry.getMethod(), e.method);
+                assertEquals(jis.readAllBytes(), e.bytes,
+                        String.format("Expected payload: %s", new String(e.bytes)));
+                jarEntry = jis.getNextJarEntry();
+            }
+        }
+    }
+
+    /**
+     * Verify that JarURLConnection can be used to access all the entries including
+     * a file and directory entry with the same name within a jar file
+     *
+     * @param entry The entry to validate
+     * @throws IOException If an error occurs
+     */
+    @Test(dataProvider = "entries")
+    public void JarURLConnectionTest(Entry entry) throws Exception {
+        System.out.printf("%n%n**** JarURLConnectionTest ***%n");
+        URL url = new URL("jar:" + TEST_JAR.toUri().toURL() + "!/" + entry.name);
+        if (DEBUG) {
+            System.out.printf("URL=%s%n", url);
+        }
+        JarURLConnection con = (JarURLConnection) url.openConnection();
+        con.connect();
+        JarEntry je = con.getJarEntry();
+        try (JarFile jarFile = con.getJarFile()) {
+            assertNotNull(je);
+            assertNotNull(jarFile);
+            assertNull(con.getAttributes());
+            assertNull(con.getMainAttributes());
+            assertNull(con.getManifest());
+            assertEquals(je.getName(), entry.name);
+            assertEquals(con.getEntryName(), entry.name);
+            assertEquals(je.getMethod(), entry.method);
+            assertEquals(con.getJarFileURL(), TEST_JAR.toUri().toURL());
+            if (DEBUG) {
+                System.out.printf("   getEntryName: %s,  getJarFileURL:%s%n",
+                        con.getEntryName(), con.getJarFileURL());
+                System.out.printf("   Jar Entry= %s, size= %s%n", je.getName(), je.getSize());
+            }
+
+            try (InputStream is = jarFile.getInputStream(je)) {
+                byte[] bytes = is.readAllBytes();
+                if (DEBUG) {
+                    System.out.printf("   Bytes read:%s%n", new String(bytes));
+                }
+                assertEquals(bytes, entry.bytes,
+                        String.format("Expected payload: %s", new String(entry.bytes)));
+            }
+        }
+    }
+
+    /**
+     * Verify that JarFile::stream returns all entries including
+     * a file and directory entry with the same name
+     *
+     * @throws IOException If an error occurs
+     */
+    @Test
+    public void JarFileStreamTest() throws IOException {
+        System.out.printf("%n%n**** JarFileStreamTest ***%n");
+        try (JarFile jf = new JarFile(TEST_JAR.toFile())) {
+            List<? extends JarEntry> entries = jf.stream().collect(Collectors.toList());
+            assertEquals(EXPECTED_ENTRIES.size(), jf.size());
+            for (JarEntry e : entries) {
+                assertTrue(EXPECTED_ENTRIES.contains(e.getName()));
+            }
+        }
+    }
+
+    /**
+     * Method used to read  the bytes from an InputStream.  This method is
+     * here so that the test could be backported to JDK 8 if needed as
+     * InputStream::readAllBytes() does not exist
+     *
+     * @param is The InputStream to read from
+     * @return The byte array representing bytes read from the InputStream
+     * @throws IOException If an error occurs
+     */
+    public static byte[] readAllBytes(InputStream is) throws IOException {
+        byte[] data = new byte[MAX_BUFFER_SIZE];
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        int len;
+        while ((len = is.read(data, 0, data.length)) != -1) {
+            buffer.write(data, 0, len);
+        }
+        buffer.flush();
+        return buffer.toByteArray();
+    }
+
+    /**
+     * Method used to create a byte[] representing a Jar file with
+     * duplicate file entries.  This uses ZipArchiveOutputStream as ZipOutputStream
+     * will fail with a "java.util.zip.ZipException: duplicate entry".
+     */
+//    public static void  createJarWithDuplicateFileEntries() throws IOException {
+//    Files.deleteIfExists(DUPFILE_ENTRY_FILE);
+//    try (ZipArchiveOutputStream zos =
+//                     new ZipArchiveOutputStream(DUPFILE_ENTRY_FILE.toFile())) {
+//            zos.putArchiveEntry(new ZipArchiveEntry(FILE_ENTRY.name));
+//            zos.write(FILE_ENTRY.bytes);
+//            zos.putArchiveEntry(new ZipArchiveEntry(FILE_ENTRY.name));
+//            zos.write("Yet another File".getBytes(StandardCharsets.UTF_8));
+//            zos.closeArchiveEntry();
+//        } catch (IOException e) {
+//            e.printStackTrace();
+//        }
+//        byte[] jarBytes = Files.readAllBytes(DUPFILE_ENTRY_FILE);
+//        String result = createByteArray(jarBytes, "DUPLICATE_ENTRY_JAR_BYTES");
+//        System.out.println(result);
+//    }
+
+    /**
+     * Utility method which takes a byte array and converts to byte array
+     * declaration.  For example:
+     * <pre>
+     *     {@code
+     *        var fooJar = Files.readAllBytes(Path.of("foo.jar"));
+     *        var result = createByteArray(fooJar, "FOOBYTES");
+     *      }
+     * </pre>
+     *
+     * @param bytes A byte array used to create a byte array declaration
+     * @param name  Name to be used in the byte array declaration
+     * @return The formatted byte array declaration
+     */
+    public static String createByteArray(byte[] bytes, String name) {
+        StringBuilder sb = new StringBuilder(bytes.length * 5);
+        Formatter fmt = new Formatter(sb);
+        fmt.format("    public static byte[] %s = {", name);
+        final int linelen = 8;
+        for (int i = 0; i < bytes.length; i++) {
+            if (i % linelen == 0) {
+                fmt.format("%n        ");
+            }
+            fmt.format(" (byte) 0x%x,", bytes[i] & 0xff);
+        }
+        fmt.format("%n    };%n");
+        return sb.toString();
+    }
+
+    /**
+     * Represents an entry in a Zip file. An entry encapsulates a name, a
+     * compression method, and its contents/data.
+     */
+    public static class Entry {
+        public final String name;
+        public final int method;
+        public final byte[] bytes;
+
+        public Entry(String name, int method, String contents) {
+            this.name = name;
+            this.method = method;
+            this.bytes = contents.getBytes(StandardCharsets.UTF_8);
+        }
+
+        public static Entry of(String name, int method, String contents) {
+            return new Entry(name, method, contents);
+        }
+    }
+}