changeset 48184:85ea7e83af30

8189611: JarFile versioned stream and real name support Reviewed-by: psandoz, alanb, mchung, martin
author sherman
date Wed, 29 Nov 2017 15:01:16 -0800
parents 45c5d7817e9e
children fb0b9913ff7e
files src/java.base/share/classes/java/util/jar/JarEntry.java src/java.base/share/classes/java/util/jar/JarFile.java src/java.base/share/classes/java/util/jar/JarVerifier.java src/java.base/share/classes/java/util/zip/ZipCoder.java src/java.base/share/classes/java/util/zip/ZipFile.java src/java.base/share/classes/jdk/internal/loader/URLClassPath.java src/java.base/share/classes/jdk/internal/misc/JavaUtilZipFileAccess.java src/java.base/share/classes/jdk/internal/module/ModulePath.java src/java.base/share/classes/jdk/internal/module/ModuleReferences.java src/java.base/share/classes/module-info.java src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JarArchive.java test/jdk/java/util/jar/JarFile/mrjar/TestVersionedStream.java test/jdk/jdk/internal/util/jar/TestVersionedStream.java
diffstat 13 files changed, 636 insertions(+), 389 deletions(-) [+]
line wrap: on
line diff
--- a/src/java.base/share/classes/java/util/jar/JarEntry.java	Wed Nov 29 22:23:21 2017 +0100
+++ b/src/java.base/share/classes/java/util/jar/JarEntry.java	Wed Nov 29 15:01:16 2017 -0800
@@ -128,4 +128,25 @@
     public CodeSigner[] getCodeSigners() {
         return signers == null ? null : signers.clone();
     }
+
+    /**
+     * Returns the real name of this {@code JarEntry}.
+     *
+     * If this {@code JarEntry} is an entry of a
+     * <a href="JarFile.html#multirelease">multi-release jar file</a> and the
+     * {@code JarFile} is configured to be processed as such, the name returned
+     * by this method is the path name of the versioned entry that the
+     * {@code JarEntry} represents, rather than the path name of the base entry
+     * that {@link #getName()} returns. If the {@code JarEntry} does not represent
+     * a versioned entry of a multi-release {@code JarFile} or the {@code JarFile}
+     * is not configured for processing a multi-release jar file, this method
+     * returns the same name that {@link #getName()} returns.
+     *
+     * @return the real name of the JarEntry
+     *
+     * @since 10
+     */
+    public String getRealName() {
+        return super.getName();
+    }
 }
--- a/src/java.base/share/classes/java/util/jar/JarFile.java	Wed Nov 29 22:23:21 2017 +0100
+++ b/src/java.base/share/classes/java/util/jar/JarFile.java	Wed Nov 29 15:01:16 2017 -0800
@@ -26,6 +26,7 @@
 package java.util.jar;
 
 import jdk.internal.misc.SharedSecrets;
+import jdk.internal.misc.JavaUtilZipFileAccess;
 import sun.security.action.GetPropertyAction;
 import sun.security.util.ManifestEntryVerifier;
 import sun.security.util.SignatureFileVerifier;
@@ -45,10 +46,12 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Spliterator;
 import java.util.Spliterators;
+import java.util.stream.Collector;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 import java.util.zip.ZipEntry;
@@ -163,9 +166,13 @@
     // true if manifest checked for special attributes
     private volatile boolean hasCheckedSpecialAttributes;
 
+    private static final JavaUtilZipFileAccess JUZFA;
+
     static {
         // Set up JavaUtilJarAccess in SharedSecrets
         SharedSecrets.setJavaUtilJarAccess(new JavaUtilJarAccessImpl());
+        // Get JavaUtilZipFileAccess from SharedSecrets
+        JUZFA = jdk.internal.misc.SharedSecrets.getJavaUtilZipFileAccess();
         // multi-release jar file versions >= 9
         BASE_VERSION = Runtime.Version.parse(Integer.toString(8));
         BASE_VERSION_MAJOR = BASE_VERSION.major();
@@ -424,8 +431,7 @@
     }
 
     private String[] getMetaInfEntryNames() {
-        return jdk.internal.misc.SharedSecrets.getJavaUtilZipFileAccess()
-                                              .getMetaInfEntryNames((ZipFile)this);
+        return JUZFA.getMetaInfEntryNames((ZipFile)this);
     }
 
     /**
@@ -497,47 +503,11 @@
      * </div>
      */
     public ZipEntry getEntry(String name) {
-        ZipEntry ze = super.getEntry(name);
-        if (ze != null) {
-            return new JarFileEntry(ze);
+        JarFileEntry je = getEntry0(name);
+        if (isMultiRelease()) {
+            return getVersionedEntry(name, je);
         }
-        // no matching base entry, but maybe there is a versioned entry,
-        // like a new private class
-        if (isMultiRelease()) {
-            ze = new ZipEntry(name);
-            ZipEntry vze = getVersionedEntry(ze);
-            if (ze != vze) {
-                return new JarFileEntry(name, vze);
-            }
-        }
-        return null;
-    }
-
-    private class JarEntryIterator implements Enumeration<JarEntry>,
-            Iterator<JarEntry>
-    {
-        final Enumeration<? extends ZipEntry> e = JarFile.super.entries();
-
-        public boolean hasNext() {
-            return e.hasMoreElements();
-        }
-
-        public JarEntry next() {
-            ZipEntry ze = e.nextElement();
-            return new JarFileEntry(ze.getName(), ze);
-        }
-
-        public boolean hasMoreElements() {
-            return hasNext();
-        }
-
-        public JarEntry nextElement() {
-            return next();
-        }
-
-        public Iterator<JarEntry> asIterator() {
-            return this;
-        }
+        return je;
     }
 
     /**
@@ -548,7 +518,7 @@
      *         may be thrown if the jar file has been closed
      */
     public Enumeration<JarEntry> entries() {
-        return new JarEntryIterator();
+        return JUZFA.entries(this, JarFileEntry::new);
     }
 
     /**
@@ -561,68 +531,100 @@
      * @since 1.8
      */
     public Stream<JarEntry> stream() {
-        return StreamSupport.stream(Spliterators.spliterator(
-                new JarEntryIterator(), size(),
-                Spliterator.ORDERED | Spliterator.DISTINCT |
-                        Spliterator.IMMUTABLE | Spliterator.NONNULL), false);
-    }
-
-    private ZipEntry searchForVersionedEntry(final int version, String name) {
-        ZipEntry vze = null;
-        String sname = "/" + name;
-        int i = version;
-        while (i > BASE_VERSION_MAJOR) {
-            vze = super.getEntry(META_INF_VERSIONS + i + sname);
-            if (vze != null) break;
-            i--;
-        }
-        return vze;
-    }
-
-    private ZipEntry getVersionedEntry(ZipEntry ze) {
-        ZipEntry vze = null;
-        if (BASE_VERSION_MAJOR < versionMajor) {
-            String name = ze.getName();
-            if (!name.startsWith(META_INF)) {
-                vze = searchForVersionedEntry(versionMajor, name);
-            }
-        }
-        return vze == null ? ze : vze;
+        return JUZFA.stream(this, JarFileEntry::new);
     }
 
     /**
-     * Returns the real name of a {@code JarEntry}.  If this {@code JarFile} is
-     * a multi-release jar file and is configured to be processed as such, the
-     * name returned by this method is the path name of the versioned entry
-     * that the {@code JarEntry} represents, rather than the path name of the
-     * base entry that {@link JarEntry#getName()} returns.  If the
-     * {@code JarEntry} does not represent a versioned entry, or the
-     * jar file is not a multi-release jar file or {@code JarFile} is not
-     * configured for processing a multi-release jar file, this method returns
-     * the same name that {@link JarEntry#getName()} returns.
+     * Returns a {@code Stream} of the versioned jar file entries.
      *
-     * @param entry the JarEntry
-     * @return the real name of the JarEntry
-     * @since 9
+     * <p>If this {@code JarFile} is a multi-release jar file and is configured to
+     * be processed as such, then an entry in the stream is the latest versioned entry
+     * associated with the corresponding base entry name. The maximum version of the
+     * latest versioned entry is the version returned by {@link #getVersion()}.
+     * The returned stream may include an entry that only exists as a versioned entry.
+     *
+     * If the jar file is not a multi-release jar file or the {@code JarFile} is not
+     * configured for processing a multi-release jar file, this method returns the
+     * same stream that {@link #stream()} returns.
+     *
+     * @return stream of versioned entries
+     * @since 10
      */
+    public Stream<JarEntry> versionedStream() {
+
+        if (isMultiRelease()) {
+            return JUZFA.entryNameStream(this).map(this::getBasename)
+                                              .filter(Objects::nonNull)
+                                              .distinct()
+                                              .map(this::getJarEntry);
+        }
+        return stream();
+    }
+
+    /*
+     * Invokes {@ZipFile}'s getEntry to Return a {@code JarFileEntry} for the
+     * given entry name or {@code null} if not found.
+     */
+    private JarFileEntry getEntry0(String name) {
+        return (JarFileEntry)JUZFA.getEntry(this, name, JarFileEntry::new);
+    }
+
+    private String getBasename(String name) {
+        if (name.startsWith(META_INF_VERSIONS)) {
+            int off = META_INF_VERSIONS.length();
+            int index = name.indexOf('/', off);
+            try {
+                // filter out dir META-INF/versions/ and META-INF/versions/*/
+                // and any entry with version > 'version'
+                if (index == -1 || index == (name.length() - 1) ||
+                    Integer.parseInt(name, off, index, 10) > versionMajor) {
+                    return null;
+                }
+            } catch (NumberFormatException x) {
+                return null; // remove malformed entries silently
+            }
+            // map to its base name
+            return name.substring(index + 1);
+        }
+        return name;
+    }
+
+    private JarEntry getVersionedEntry(String name, JarEntry je) {
+        if (BASE_VERSION_MAJOR < versionMajor) {
+            if (!name.startsWith(META_INF)) {
+                // search for versioned entry
+                int v = versionMajor;
+                while (v > BASE_VERSION_MAJOR) {
+                    JarFileEntry vje = getEntry0(META_INF_VERSIONS + v + "/" + name);
+                    if (vje != null) {
+                        return vje.withBasename(name);
+                    }
+                    v--;
+                }
+            }
+        }
+        return je;
+    }
+
+    // placeholder for now
     String getRealName(JarEntry entry) {
-        if (entry instanceof JarFileEntry) {
-            return ((JarFileEntry)entry).realName();
-        }
-        return entry.getName();
+        return entry.getRealName();
     }
 
     private class JarFileEntry extends JarEntry {
-        final private String name;
+        private String basename;
 
-        JarFileEntry(ZipEntry ze) {
-            super(isMultiRelease() ? getVersionedEntry(ze) : ze);
-            this.name = ze.getName();
+        JarFileEntry(String name) {
+            super(name);
+            this.basename = name;
         }
+
         JarFileEntry(String name, ZipEntry vze) {
             super(vze);
-            this.name = name;
+            this.basename = name;
         }
+
+        @Override
         public Attributes getAttributes() throws IOException {
             Manifest man = JarFile.this.getManifest();
             if (man != null) {
@@ -631,6 +633,8 @@
                 return null;
             }
         }
+
+        @Override
         public Certificate[] getCertificates() {
             try {
                 maybeInstantiateVerifier();
@@ -642,6 +646,8 @@
             }
             return certs == null ? null : certs.clone();
         }
+
+        @Override
         public CodeSigner[] getCodeSigners() {
             try {
                 maybeInstantiateVerifier();
@@ -653,20 +659,30 @@
             }
             return signers == null ? null : signers.clone();
         }
-        JarFileEntry realEntry() {
-            if (isMultiRelease() && versionMajor != BASE_VERSION_MAJOR) {
-                String entryName = super.getName();
-                return entryName.equals(this.name) ? this : new JarFileEntry(entryName, this);
-            }
-            return this;
-        }
-        String realName() {
+
+        @Override
+        public String getRealName() {
             return super.getName();
         }
 
         @Override
         public String getName() {
-            return name;
+            return basename;
+        }
+
+        JarFileEntry realEntry() {
+            if (isMultiRelease() && versionMajor != BASE_VERSION_MAJOR) {
+                String entryName = super.getName();
+                return entryName == basename || entryName.equals(basename) ?
+                        this : new JarFileEntry(entryName, this);
+            }
+            return this;
+        }
+
+        // changes the basename, returns "this"
+        JarFileEntry withBasename(String name) {
+            basename = name;
+            return this;
         }
     }
 
@@ -704,7 +720,6 @@
         }
     }
 
-
     /*
      * Initializes the verifier object by reading all the manifest
      * entries and passing them to the verifier.
@@ -904,7 +919,7 @@
     private JarEntry getManEntry() {
         if (manEntry == null) {
             // First look up manifest entry using standard name
-            ZipEntry manEntry = super.getEntry(MANIFEST_NAME);
+            JarEntry manEntry = getEntry0(MANIFEST_NAME);
             if (manEntry == null) {
                 // If not found, then iterate through all the "META-INF/"
                 // entries to find a match.
@@ -912,15 +927,13 @@
                 if (names != null) {
                     for (String name : names) {
                         if (MANIFEST_NAME.equals(name.toUpperCase(Locale.ENGLISH))) {
-                            manEntry = super.getEntry(name);
+                            manEntry = getEntry0(name);
                             break;
                         }
                     }
                 }
             }
-            this.manEntry = (manEntry == null)
-                    ? null
-                    : new JarFileEntry(manEntry.getName(), manEntry);
+            this.manEntry = manEntry;
         }
         return manEntry;
     }
@@ -1032,8 +1045,32 @@
         }
     }
 
-    JarEntry newEntry(ZipEntry ze) {
-        return new JarFileEntry(ze);
+    /*
+     * Returns a versioned {@code JarFileEntry} for the given entry,
+     * if there is one. Otherwise returns the original entry. This
+     * is invoked by the {@code entries2} for verifier.
+     */
+    JarEntry newEntry(JarEntry je) {
+        if (isMultiRelease()) {
+            return getVersionedEntry(je.getName(), je);
+        }
+        return je;
+    }
+
+    /*
+     * Returns a versioned {@code JarFileEntry} for the given entry
+     * name, if there is one. Otherwise returns a {@code JarFileEntry}
+     * with the given name. It is invoked from JarVerifier's entries2
+     * for {@code singers}.
+     */
+    JarEntry newEntry(String name) {
+        if (isMultiRelease()) {
+            JarEntry vje = getVersionedEntry(name, (JarEntry)null);
+            if (vje != null) {
+                return vje;
+            }
+        }
+        return new JarFileEntry(name);
     }
 
     Enumeration<String> entryNames(CodeSource[] cs) {
@@ -1077,35 +1114,37 @@
     Enumeration<JarEntry> entries2() {
         ensureInitialization();
         if (jv != null) {
-            return jv.entries2(this, super.entries());
+            return jv.entries2(this, JUZFA.entries(JarFile.this,
+                                                   JarFileEntry::new));
         }
 
         // screen out entries which are never signed
-        final Enumeration<? extends ZipEntry> enum_ = super.entries();
+        final var unfilteredEntries = JUZFA.entries(JarFile.this, JarFileEntry::new);
+
         return new Enumeration<>() {
 
-            ZipEntry entry;
+            JarEntry entry;
 
             public boolean hasMoreElements() {
                 if (entry != null) {
                     return true;
                 }
-                while (enum_.hasMoreElements()) {
-                    ZipEntry ze = enum_.nextElement();
-                    if (JarVerifier.isSigningRelated(ze.getName())) {
+                while (unfilteredEntries.hasMoreElements()) {
+                    JarEntry je = unfilteredEntries.nextElement();
+                    if (JarVerifier.isSigningRelated(je.getName())) {
                         continue;
                     }
-                    entry = ze;
+                    entry = je;
                     return true;
                 }
                 return false;
             }
 
-            public JarFileEntry nextElement() {
+            public JarEntry nextElement() {
                 if (hasMoreElements()) {
-                    ZipEntry ze = entry;
+                    JarEntry je = entry;
                     entry = null;
-                    return new JarFileEntry(ze);
+                    return newEntry(je);
                 }
                 throw new NoSuchElementException();
             }
--- a/src/java.base/share/classes/java/util/jar/JarVerifier.java	Wed Nov 29 22:23:21 2017 +0100
+++ b/src/java.base/share/classes/java/util/jar/JarVerifier.java	Wed Nov 29 15:01:16 2017 -0800
@@ -724,10 +724,10 @@
      * Like entries() but screens out internal JAR mechanism entries
      * and includes signed entries with no ZIP data.
      */
-    public Enumeration<JarEntry> entries2(final JarFile jar, Enumeration<? extends ZipEntry> e) {
+    public Enumeration<JarEntry> entries2(final JarFile jar, Enumeration<JarEntry> e) {
         final Map<String, CodeSigner[]> map = new HashMap<>();
         map.putAll(signerMap());
-        final Enumeration<? extends ZipEntry> enum_ = e;
+        final Enumeration<JarEntry> enum_ = e;
         return new Enumeration<>() {
 
             Enumeration<String> signers = null;
@@ -738,11 +738,11 @@
                     return true;
                 }
                 while (enum_.hasMoreElements()) {
-                    ZipEntry ze = enum_.nextElement();
-                    if (JarVerifier.isSigningRelated(ze.getName())) {
+                    JarEntry je = enum_.nextElement();
+                    if (JarVerifier.isSigningRelated(je.getName())) {
                         continue;
                     }
-                    entry = jar.newEntry(ze);
+                    entry = jar.newEntry(je);
                     return true;
                 }
                 if (signers == null) {
@@ -750,7 +750,7 @@
                 }
                 while (signers.hasMoreElements()) {
                     String name = signers.nextElement();
-                    entry = jar.newEntry(new ZipEntry(name));
+                    entry = jar.newEntry(name);
                     return true;
                 }
 
--- a/src/java.base/share/classes/java/util/zip/ZipCoder.java	Wed Nov 29 22:23:21 2017 +0100
+++ b/src/java.base/share/classes/java/util/zip/ZipCoder.java	Wed Nov 29 15:01:16 2017 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009, 2011, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2009, 2017, 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
@@ -43,7 +43,34 @@
 
 final class ZipCoder {
 
+    private static boolean isASCII(byte[] ba, int off, int len) {
+        for (int i = off; i < off + len; i++) {
+            if (ba[i] < 0)
+                return false;
+        }
+        return true;
+    }
+
+    private static boolean hasReplaceChar(byte[] ba) {
+        for (int i = 0; i < ba.length; i++) {
+            if (ba[i] == (byte)'?')
+                return true;
+        }
+        return false;
+    }
+
     String toString(byte[] ba, int off, int length) {
+
+        // fastpath for UTF-8 cs and ascii only name, leverage the
+        // compact string impl to avoid the unnecessary char[] copy/
+        // paste. A temporary workaround before we have better approach,
+        // such as a String constructor that throws exception for
+        // malformed and/or unmappable characters, instead of silently
+        // replacing with repl char
+        if (isUTF8 && isASCII(ba, off, length)) {
+            return new String(ba, off, length, cs);
+        }
+
         CharsetDecoder cd = decoder().reset();
         int len = (int)(length * cd.maxCharsPerByte());
         char[] ca = new char[len];
@@ -78,6 +105,15 @@
     }
 
     byte[] getBytes(String s) {
+        if (isUTF8) {
+            // fastpath for UTF8. should only occur when the string
+            // has malformed surrogates. A postscan should still be
+            // faster and use less memory.
+            byte[] ba = s.getBytes(cs);
+            if (!hasReplaceChar(ba)) {
+                return ba;
+            }
+        }
         CharsetEncoder ce = encoder().reset();
         char[] ca = s.toCharArray();
         int len = (int)(ca.length * ce.maxBytesPerChar());
--- a/src/java.base/share/classes/java/util/zip/ZipFile.java	Wed Nov 29 22:23:21 2017 +0100
+++ b/src/java.base/share/classes/java/util/zip/ZipFile.java	Wed Nov 29 15:01:16 2017 -0800
@@ -50,11 +50,15 @@
 import java.util.Spliterator;
 import java.util.Spliterators;
 import java.util.WeakHashMap;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.IntFunction;
+import java.util.jar.JarEntry;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 import jdk.internal.misc.JavaUtilZipFileAccess;
 import jdk.internal.misc.SharedSecrets;
-import jdk.internal.misc.JavaIORandomAccessFileAccess;
 import jdk.internal.misc.VM;
 import jdk.internal.perf.PerfCounter;
 
@@ -296,13 +300,27 @@
      * @throws IllegalStateException if the zip file has been closed
      */
     public ZipEntry getEntry(String name) {
+        return getEntry(name, ZipEntry::new);
+    }
+
+    /*
+     * Returns the zip file entry for the specified name, or null
+     * if not found.
+     *
+     * @param name the name of the entry
+     * @param func the function that creates the returned entry
+     *
+     * @return the zip file entry, or null if not found
+     * @throws IllegalStateException if the zip file has been closed
+     */
+    private ZipEntry getEntry(String name, Function<String, ? extends ZipEntry> func) {
         Objects.requireNonNull(name, "name");
         synchronized (this) {
             ensureOpen();
             byte[] bname = zc.getBytes(name);
             int pos = zsrc.getEntryPos(bname, true);
             if (pos != -1) {
-                return getZipEntry(name, bname, pos);
+                return getZipEntry(name, bname, pos, func);
             }
         }
         return null;
@@ -374,12 +392,10 @@
     private class ZipFileInflaterInputStream extends InflaterInputStream {
         private volatile boolean closeRequested;
         private boolean eof = false;
-        private final ZipFileInputStream zfin;
 
         ZipFileInflaterInputStream(ZipFileInputStream zfin, Inflater inf,
                 int size) {
             super(zfin, inf, size);
-            this.zfin = zfin;
         }
 
         public void close() throws IOException {
@@ -416,7 +432,7 @@
         public int available() throws IOException {
             if (closeRequested)
                 return 0;
-            long avail = zfin.size() - inf.getBytesWritten();
+            long avail = ((ZipFileInputStream)in).size() - inf.getBytesWritten();
             return (avail > (long) Integer.MAX_VALUE ?
                     Integer.MAX_VALUE : (int) avail);
         }
@@ -466,41 +482,48 @@
         return name;
     }
 
-    private class ZipEntryIterator implements Enumeration<ZipEntry>, Iterator<ZipEntry> {
+    private class ZipEntryIterator<T extends ZipEntry>
+            implements Enumeration<T>, Iterator<T> {
+
         private int i = 0;
         private final int entryCount;
+        private final Function<String, T> gen;
 
-        public ZipEntryIterator() {
-            synchronized (ZipFile.this) {
-                ensureOpen();
-                this.entryCount = zsrc.total;
-            }
+        public ZipEntryIterator(int entryCount, Function<String, T> gen) {
+            this.entryCount = entryCount;
+            this.gen = gen;
         }
 
+        @Override
         public boolean hasMoreElements() {
             return hasNext();
         }
 
+        @Override
         public boolean hasNext() {
             return i < entryCount;
         }
 
-        public ZipEntry nextElement() {
+        @Override
+        public T nextElement() {
             return next();
         }
 
-        public ZipEntry next() {
+        @Override
+        @SuppressWarnings("unchecked")
+        public T  next() {
             synchronized (ZipFile.this) {
                 ensureOpen();
                 if (!hasNext()) {
                     throw new NoSuchElementException();
                 }
                 // each "entry" has 3 ints in table entries
-                return getZipEntry(null, null, zsrc.getEntryPos(i++ * 3));
+                return (T)getZipEntry(null, null, zsrc.getEntryPos(i++ * 3), gen);
             }
         }
 
-        public Iterator<ZipEntry> asIterator() {
+        @Override
+        public Iterator<T> asIterator() {
             return this;
         }
     }
@@ -511,11 +534,51 @@
      * @throws IllegalStateException if the zip file has been closed
      */
     public Enumeration<? extends ZipEntry> entries() {
-        return new ZipEntryIterator();
+        synchronized (this) {
+            ensureOpen();
+            return new ZipEntryIterator<ZipEntry>(zsrc.total, ZipEntry::new);
+        }
+    }
+
+    private Enumeration<JarEntry> entries(Function<String, JarEntry> func) {
+        synchronized (this) {
+            ensureOpen();
+            return new ZipEntryIterator<JarEntry>(zsrc.total, func);
+        }
+    }
+
+    private class EntrySpliterator<T> extends Spliterators.AbstractSpliterator<T> {
+        private int index;
+        private final int fence;
+        private final IntFunction<T> gen;
+
+        EntrySpliterator(int index, int fence, IntFunction<T> gen) {
+            super((long)fence,
+                  Spliterator.ORDERED | Spliterator.DISTINCT | Spliterator.IMMUTABLE |
+                  Spliterator.NONNULL);
+            this.index = index;
+            this.fence = fence;
+            this.gen = gen;
+        }
+
+        @Override
+        public boolean tryAdvance(Consumer<? super T> action) {
+            if (action == null)
+                throw new NullPointerException();
+            if (index >= 0 && index < fence) {
+                synchronized (ZipFile.this) {
+                    ensureOpen();
+                    action.accept(gen.apply(zsrc.getEntryPos(index++ * 3)));
+                }
+                return true;
+            }
+            return false;
+        }
     }
 
     /**
      * Returns an ordered {@code Stream} over the ZIP file entries.
+     *
      * Entries appear in the {@code Stream} in the order they appear in
      * the central directory of the ZIP file.
      *
@@ -524,17 +587,68 @@
      * @since 1.8
      */
     public Stream<? extends ZipEntry> stream() {
-        return StreamSupport.stream(Spliterators.spliterator(
-                new ZipEntryIterator(), size(),
-                Spliterator.ORDERED | Spliterator.DISTINCT |
-                        Spliterator.IMMUTABLE | Spliterator.NONNULL), false);
+        synchronized (this) {
+            ensureOpen();
+            return StreamSupport.stream(new EntrySpliterator<>(0, zsrc.total,
+                pos -> getZipEntry(null, null, pos, ZipEntry::new)), false);
+       }
+    }
+
+    private String getEntryName(int pos) {
+        byte[] cen = zsrc.cen;
+        int nlen = CENNAM(cen, pos);
+        int clen = CENCOM(cen, pos);
+        int flag = CENFLG(cen, pos);
+        if (!zc.isUTF8() && (flag & EFS) != 0) {
+            return zc.toStringUTF8(cen, pos + CENHDR, nlen);
+        } else {
+            return zc.toString(cen, pos + CENHDR, nlen);
+        }
+    }
+
+    /*
+     * Returns an ordered {@code Stream} over the zip file entry names.
+     *
+     * Entry names appear in the {@code Stream} in the order they appear in
+     * the central directory of the ZIP file.
+     *
+     * @return an ordered {@code Stream} of entry names in this zip file
+     * @throws IllegalStateException if the zip file has been closed
+     * @since 10
+     */
+    private Stream<String> entryNameStream() {
+        synchronized (this) {
+            ensureOpen();
+            return StreamSupport.stream(
+                new EntrySpliterator<>(0, zsrc.total, this::getEntryName), false);
+        }
+    }
+
+    /*
+     * Returns an ordered {@code Stream} over the zip file entries.
+     *
+     * Entries appear in the {@code Stream} in the order they appear in
+     * the central directory of the jar file.
+     *
+     * @param func the function that creates the returned entry
+     * @return an ordered {@code Stream} of entries in this zip file
+     * @throws IllegalStateException if the zip file has been closed
+     * @since 10
+     */
+    private Stream<JarEntry> stream(Function<String, JarEntry> func) {
+        synchronized (this) {
+            ensureOpen();
+            return StreamSupport.stream(new EntrySpliterator<>(0, zsrc.total,
+                pos -> (JarEntry)getZipEntry(null, null, pos, func)), false);
+        }
     }
 
     private String lastEntryName;
     private int lastEntryPos;
 
     /* Checks ensureOpen() before invoke this method */
-    private ZipEntry getZipEntry(String name, byte[] bname, int pos) {
+    private ZipEntry getZipEntry(String name, byte[] bname, int pos,
+                                 Function<String, ? extends ZipEntry> func) {
         byte[] cen = zsrc.cen;
         int nlen = CENNAM(cen, pos);
         int elen = CENEXT(cen, pos);
@@ -551,7 +665,7 @@
                 name = zc.toString(cen, pos + CENHDR, nlen);
             }
         }
-        ZipEntry e = new ZipEntry(name);
+        ZipEntry e = func.apply(name);    //ZipEntry e = new ZipEntry(name);
         e.flag = flag;
         e.xdostime = CENTIM(cen, pos);
         e.crc = CENCRC(cen, pos);
@@ -791,7 +905,6 @@
 
         public long skip(long n) throws IOException {
             synchronized (ZipFile.this) {
-                ensureOpenOrZipException();
                 initDataOffset();
                 if (n > rem) {
                     n = rem;
@@ -857,12 +970,33 @@
     static {
         SharedSecrets.setJavaUtilZipFileAccess(
             new JavaUtilZipFileAccess() {
+                @Override
                 public boolean startsWithLocHeader(ZipFile zip) {
                     return zip.zsrc.startsWithLoc;
                 }
+                @Override
                 public String[] getMetaInfEntryNames(ZipFile zip) {
                     return zip.getMetaInfEntryNames();
                 }
+                @Override
+                public JarEntry getEntry(ZipFile zip, String name,
+                    Function<String, JarEntry> func) {
+                    return (JarEntry)zip.getEntry(name, func);
+                }
+                @Override
+                public Enumeration<JarEntry> entries(ZipFile zip,
+                    Function<String, JarEntry> func) {
+                    return zip.entries(func);
+                }
+                @Override
+                public Stream<JarEntry> stream(ZipFile zip,
+                    Function<String, JarEntry> func) {
+                    return zip.stream(func);
+                }
+                @Override
+                public Stream<String> entryNameStream(ZipFile zip) {
+                    return zip.entryNameStream();
+                }
              }
         );
         isWindows = VM.getSavedProperty("os.name").contains("Windows");
--- a/src/java.base/share/classes/jdk/internal/loader/URLClassPath.java	Wed Nov 29 22:23:21 2017 +0100
+++ b/src/java.base/share/classes/jdk/internal/loader/URLClassPath.java	Wed Nov 29 15:01:16 2017 -0800
@@ -834,7 +834,7 @@
             try {
                 String nm;
                 if (jar.isMultiRelease()) {
-                    nm = SharedSecrets.javaUtilJarAccess().getRealName(jar, entry);
+                    nm = entry.getRealName();
                 } else {
                     nm = name;
                 }
--- a/src/java.base/share/classes/jdk/internal/misc/JavaUtilZipFileAccess.java	Wed Nov 29 22:23:21 2017 +0100
+++ b/src/java.base/share/classes/jdk/internal/misc/JavaUtilZipFileAccess.java	Wed Nov 29 15:01:16 2017 -0800
@@ -25,10 +25,19 @@
 
 package jdk.internal.misc;
 
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.function.Function;
+import java.util.jar.JarEntry;
+import java.util.stream.Stream;
 import java.util.zip.ZipFile;
 
 public interface JavaUtilZipFileAccess {
     public boolean startsWithLocHeader(ZipFile zip);
     public String[] getMetaInfEntryNames(ZipFile zip);
+    public JarEntry getEntry(ZipFile zip, String name, Function<String, JarEntry> func);
+    public Enumeration<JarEntry> entries(ZipFile zip, Function<String, JarEntry> func);
+    public Stream<JarEntry> stream(ZipFile zip, Function<String, JarEntry> func);
+    public Stream<String> entryNameStream(ZipFile zip);
 }
 
--- a/src/java.base/share/classes/jdk/internal/module/ModulePath.java	Wed Nov 29 22:23:21 2017 +0100
+++ b/src/java.base/share/classes/jdk/internal/module/ModulePath.java	Wed Nov 29 15:01:16 2017 -0800
@@ -66,8 +66,6 @@
 import jdk.internal.jmod.JmodFile;
 import jdk.internal.jmod.JmodFile.Section;
 import jdk.internal.perf.PerfCounter;
-import jdk.internal.util.jar.VersionedStream;
-
 
 /**
  * A {@code ModuleFinder} that locates modules on the file system by searching
@@ -515,7 +513,7 @@
             builder.version(vs);
 
         // scan the names of the entries in the JAR file
-        Map<Boolean, Set<String>> map = VersionedStream.stream(jf)
+        Map<Boolean, Set<String>> map = jf.versionedStream()
                 .filter(e -> !e.isDirectory())
                 .map(JarEntry::getName)
                 .filter(e -> (e.endsWith(".class") ^ e.startsWith(SERVICES_PREFIX)))
@@ -615,7 +613,7 @@
     }
 
     private Set<String> jarPackages(JarFile jf) {
-        return VersionedStream.stream(jf)
+        return jf.versionedStream()
                 .filter(e -> !e.isDirectory())
                 .map(JarEntry::getName)
                 .map(this::toPackageName)
--- a/src/java.base/share/classes/jdk/internal/module/ModuleReferences.java	Wed Nov 29 22:23:21 2017 +0100
+++ b/src/java.base/share/classes/jdk/internal/module/ModuleReferences.java	Wed Nov 29 15:01:16 2017 -0800
@@ -50,9 +50,7 @@
 import java.util.zip.ZipFile;
 
 import jdk.internal.jmod.JmodFile;
-import jdk.internal.misc.SharedSecrets;
 import jdk.internal.module.ModuleHashes.HashSupplier;
-import jdk.internal.util.jar.VersionedStream;
 import sun.net.www.ParseUtil;
 
 
@@ -250,7 +248,7 @@
             JarEntry je = getEntry(name);
             if (je != null) {
                 if (jf.isMultiRelease())
-                    name = SharedSecrets.javaUtilJarAccess().getRealName(jf, je);
+                    name = je.getRealName();
                 if (je.isDirectory() && !name.endsWith("/"))
                     name += "/";
                 String encodedPath = ParseUtil.encodePath(name, false);
@@ -274,7 +272,7 @@
         @Override
         Stream<String> implList() throws IOException {
             // take snapshot to avoid async close
-            List<String> names = VersionedStream.stream(jf)
+            List<String> names = jf.versionedStream()
                     .map(JarEntry::getName)
                     .collect(Collectors.toList());
             return names.stream();
--- a/src/java.base/share/classes/module-info.java	Wed Nov 29 22:23:21 2017 +0100
+++ b/src/java.base/share/classes/module-info.java	Wed Nov 29 15:01:16 2017 -0800
@@ -211,8 +211,7 @@
         jdk.incubator.httpclient;
     exports jdk.internal.util.jar to
         jdk.jartool,
-        jdk.jdeps,
-        jdk.jlink;
+        jdk.jdeps;
     exports sun.net to
         jdk.incubator.httpclient;
     exports sun.net.ext to
--- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JarArchive.java	Wed Nov 29 22:23:21 2017 +0100
+++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JarArchive.java	Wed Nov 29 15:01:16 2017 -0800
@@ -34,7 +34,6 @@
 import java.util.stream.Stream;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
-import jdk.internal.util.jar.VersionedStream;
 import jdk.tools.jlink.internal.Archive.Entry.EntryType;
 
 /**
@@ -105,7 +104,7 @@
         } catch (IOException ioe) {
             throw new UncheckedIOException(ioe);
         }
-        return VersionedStream.stream(jarFile)
+        return jarFile.versionedStream()
                 .filter(je -> !je.isDirectory())
                 .map(this::toEntry);
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/util/jar/JarFile/mrjar/TestVersionedStream.java	Wed Nov 29 15:01:16 2017 -0800
@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) 2016, 2017, 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.
+ */
+
+/*
+ * @test
+ * @bug 8163798 8189611
+ * @summary basic tests for multi-release jar versioned streams
+ * @library /test/lib
+ * @modules jdk.jartool/sun.tools.jar java.base/jdk.internal.util.jar
+ * @build jdk.test.lib.Platform
+ *        jdk.test.lib.util.FileUtils
+ * @run testng TestVersionedStream
+ */
+
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.zip.ZipFile;
+
+import jdk.test.lib.util.FileUtils;
+
+public class TestVersionedStream {
+    private final Path userdir;
+    private final Set<String> unversionedEntryNames;
+
+    public TestVersionedStream() throws IOException {
+        userdir = Paths.get(System.getProperty("user.dir", "."));
+
+        // These are not real class files even though they end with .class.
+        // They are resource files so jar tool validation won't reject them.
+        // But they are what we want to test, especially q/Bar.class that
+        // could be in a concealed package if this was a modular multi-release
+        // jar.
+        createFiles(
+                "base/p/Bar.class",
+                "base/p/Foo.class",
+                "base/p/Main.class",
+                "v9/p/Foo.class",
+                "v10/p/Foo.class",
+                "v10/q/Bar.class",
+                "v11/p/Bar.class",
+                "v11/p/Foo.class"
+        );
+
+        jar("cf mmr.jar -C base . --release 9 -C v9 . " +
+                "--release 10 -C v10 . --release 11 -C v11 .");
+
+        System.out.println("Contents of mmr.jar\n=======");
+
+        try(JarFile jf = new JarFile("mmr.jar")) {
+            unversionedEntryNames = jf.stream()
+                    .map(je -> je.getName())
+                    .peek(System.out::println)
+                    .map(nm -> nm.startsWith("META-INF/versions/")
+                            ? nm.replaceFirst("META-INF/versions/\\d+/", "")
+                            : nm)
+                    .collect(Collectors.toCollection(LinkedHashSet::new));
+        }
+
+        System.out.println("=======");
+    }
+
+    @AfterClass
+    public void close() throws IOException {
+        Files.walk(userdir, 1)
+                .filter(p -> !p.equals(userdir))
+                .forEach(p -> {
+                    try {
+                        if (Files.isDirectory(p)) {
+                            FileUtils.deleteFileTreeWithRetry(p);
+                        } else {
+                            FileUtils.deleteFileIfExistsWithRetry(p);
+                        }
+                    } catch (IOException x) {
+                        throw new UncheckedIOException(x);
+                    }
+                });
+    }
+
+    @DataProvider
+    public Object[][] data() {
+        return new Object[][] {
+            {Runtime.Version.parse("8")},
+            {Runtime.Version.parse("9")},
+            {Runtime.Version.parse("10")},
+            {Runtime.Version.parse("11")},
+            {JarFile.baseVersion()},
+            {JarFile.runtimeVersion()}
+        };
+    }
+
+    @Test(dataProvider="data")
+    public void test(Runtime.Version version) throws Exception {
+        try (JarFile jf = new JarFile(new File("mmr.jar"), false, ZipFile.OPEN_READ, version);
+             Stream<JarEntry> jes = jf.versionedStream())
+        {
+            Assert.assertNotNull(jes);
+
+            // put versioned entries in list so we can reuse them
+            List<JarEntry> versionedEntries = jes.collect(Collectors.toList());
+
+            Assert.assertTrue(versionedEntries.size() > 0);
+
+            // also keep the names
+            List<String> versionedNames = new ArrayList<>(versionedEntries.size());
+
+            // verify the correct order while building enames
+            Iterator<String> allIt = unversionedEntryNames.iterator();
+            Iterator<JarEntry> verIt = versionedEntries.iterator();
+            boolean match = false;
+
+            while (verIt.hasNext()) {
+                match = false;
+                if (!allIt.hasNext()) break;
+                String name = verIt.next().getName();
+                versionedNames.add(name);
+                while (allIt.hasNext()) {
+                    if (name.equals(allIt.next())) {
+                        match = true;
+                        break;
+                    }
+                }
+            }
+            if (!match) {
+                Assert.fail("versioned entries not in same order as unversioned entries");
+            }
+
+            // verify the contents:
+            // value.[0] end of the path
+            // value.[1] versioned path/real name
+            Map<String,String[]> expected = new HashMap<>();
+
+            expected.put("p/Bar.class", new String[] { "base/p/Bar.class", "p/Bar.class" });
+            expected.put("p/Main.class", new String[] { "base/p/Main.class", "p/Main.class" });
+            switch (version.major()) {
+                case 8:
+                    expected.put("p/Foo.class", new String[]
+                        { "base/p/Foo.class", "p/Foo.class" });
+                    break;
+                case 9:
+                    expected.put("p/Foo.class", new String[]
+                        { "v9/p/Foo.class", "META-INF/versions/9/p/Foo.class" });
+                    break;
+                case 10:
+                    expected.put("p/Foo.class", new String[]
+                        { "v10/p/Foo.class", "META-INF/versions/10/p/Foo.class" });
+
+                    expected.put("q/Bar.class", new String[]
+                        { "v10/q/Bar.class", "META-INF/versions/10/q/Bar.class" });
+                    break;
+                case 11:
+                    expected.put("p/Bar.class", new String[]
+                        { "v11/p/Bar.class", "META-INF/versions/11/p/Bar.class"});
+                    expected.put("p/Foo.class", new String[]
+                        { "v11/p/Foo.class", "META-INF/versions/11/p/Foo.class"});
+                    expected.put("q/Bar.class", new String[]
+                        { "q/Bar.class", "META-INF/versions/10/q/Bar.class"});
+                    break;
+                default:
+                    Assert.fail("Test out of date, please add more cases");
+            }
+
+            expected.entrySet().stream().forEach(e -> {
+                String name = e.getKey();
+                int i = versionedNames.indexOf(name);
+                Assert.assertTrue(i != -1, name + " not in enames");
+                JarEntry je = versionedEntries.get(i);
+                try (InputStream is = jf.getInputStream(je)) {
+                    String s = new String(is.readAllBytes()).replaceAll(System.lineSeparator(), "");
+                    // end of the path
+                    Assert.assertTrue(s.endsWith(e.getValue()[0]), s);
+                    // getRealName()
+                    Assert.assertTrue(je.getRealName().equals(e.getValue()[1]));
+                } catch (IOException x) {
+                    throw new UncheckedIOException(x);
+                }
+            });
+        }
+    }
+
+    private void createFiles(String... files) {
+        ArrayList<String> list = new ArrayList();
+        Arrays.stream(files)
+                .map(f -> Paths.get(userdir.toAbsolutePath().toString(), f))
+                .forEach(p -> {
+                    try {
+                        Files.createDirectories(p.getParent());
+                        Files.createFile(p);
+                        list.clear();
+                        list.add(p.toString().replace(File.separatorChar, '/'));
+                        Files.write(p, list);
+                    } catch (IOException x) {
+                        throw new UncheckedIOException(x);
+                    }});
+    }
+
+    private void jar(String args) {
+        new sun.tools.jar.Main(System.out, System.err, "jar")
+                .run(args.split(" +"));
+    }
+}
--- a/test/jdk/jdk/internal/util/jar/TestVersionedStream.java	Wed Nov 29 22:23:21 2017 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,228 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 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.
- */
-
-/*
- * @test
- * @bug 8163798
- * @summary basic tests for multi-release jar versioned streams
- * @library /test/lib
- * @modules jdk.jartool/sun.tools.jar java.base/jdk.internal.util.jar
- * @build jdk.test.lib.Platform
- *        jdk.test.lib.util.FileUtils
- * @run testng TestVersionedStream
- */
-
-import org.testng.Assert;
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.jar.JarEntry;
-import java.util.jar.JarFile;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import java.util.zip.ZipFile;
-
-import jdk.test.lib.util.FileUtils;
-
-public class TestVersionedStream {
-    private final Path userdir;
-    private final Set<String> unversionedEntryNames;
-
-    public TestVersionedStream() throws IOException {
-        userdir = Paths.get(System.getProperty("user.dir", "."));
-
-        // These are not real class files even though they end with .class.
-        // They are resource files so jar tool validation won't reject them.
-        // But they are what we want to test, especially q/Bar.class that
-        // could be in a concealed package if this was a modular multi-release
-        // jar.
-        createFiles(
-                "base/p/Bar.class",
-                "base/p/Foo.class",
-                "base/p/Main.class",
-                "v9/p/Foo.class",
-                "v10/p/Foo.class",
-                "v10/q/Bar.class",
-                "v11/p/Bar.class",
-                "v11/p/Foo.class"
-        );
-
-        jar("cf mmr.jar -C base . --release 9 -C v9 . " +
-                "--release 10 -C v10 . --release 11 -C v11 .");
-
-        System.out.println("Contents of mmr.jar\n=======");
-
-        try(JarFile jf = new JarFile("mmr.jar")) {
-            unversionedEntryNames = jf.stream()
-                    .map(je -> je.getName())
-                    .peek(System.out::println)
-                    .map(nm -> nm.startsWith("META-INF/versions/")
-                            ? nm.replaceFirst("META-INF/versions/\\d+/", "")
-                            : nm)
-                    .collect(Collectors.toCollection(LinkedHashSet::new));
-        }
-
-        System.out.println("=======");
-    }
-
-    @AfterClass
-    public void close() throws IOException {
-        Files.walk(userdir, 1)
-                .filter(p -> !p.equals(userdir))
-                .forEach(p -> {
-                    try {
-                        if (Files.isDirectory(p)) {
-                            FileUtils.deleteFileTreeWithRetry(p);
-                        } else {
-                            FileUtils.deleteFileIfExistsWithRetry(p);
-                        }
-                    } catch (IOException x) {
-                        throw new UncheckedIOException(x);
-                    }
-                });
-    }
-
-    @DataProvider
-    public Object[][] data() {
-        return new Object[][] {
-            {Runtime.Version.parse("8")},
-            {Runtime.Version.parse("9")},
-            {Runtime.Version.parse("10")},
-            {Runtime.Version.parse("11")},
-            {JarFile.baseVersion()},
-            {JarFile.runtimeVersion()}
-        };
-    }
-
-    @Test(dataProvider="data")
-    public void test(Runtime.Version version) throws Exception {
-        try (JarFile jf = new JarFile(new File("mmr.jar"), false, ZipFile.OPEN_READ, version);
-             Stream<JarEntry> jes = jdk.internal.util.jar.VersionedStream.stream(jf))
-        {
-            Assert.assertNotNull(jes);
-
-            // put versioned entries in list so we can reuse them
-            List<JarEntry> versionedEntries = jes.collect(Collectors.toList());
-
-            Assert.assertTrue(versionedEntries.size() > 0);
-
-            // also keep the names
-            List<String> versionedNames = new ArrayList<>(versionedEntries.size());
-
-            // verify the correct order while building enames
-            Iterator<String> allIt = unversionedEntryNames.iterator();
-            Iterator<JarEntry> verIt = versionedEntries.iterator();
-            boolean match = false;
-
-            while (verIt.hasNext()) {
-                match = false;
-                if (!allIt.hasNext()) break;
-                String name = verIt.next().getName();
-                versionedNames.add(name);
-                while (allIt.hasNext()) {
-                    if (name.equals(allIt.next())) {
-                        match = true;
-                        break;
-                    }
-                }
-            }
-            if (!match) {
-                Assert.fail("versioned entries not in same order as unversioned entries");
-            }
-
-            // verify the contents
-            Map<String,String> contents = new HashMap<>();
-            contents.put("p/Bar.class", "base/p/Bar.class");
-            contents.put("p/Main.class", "base/p/Main.class");
-            switch (version.major()) {
-                case 8:
-                    contents.put("p/Foo.class", "base/p/Foo.class");
-                    break;
-                case 9:
-                    contents.put("p/Foo.class", "v9/p/Foo.class");
-                    break;
-                case 10:
-                    contents.put("p/Foo.class", "v10/p/Foo.class");
-                    contents.put("q/Bar.class", "v10/q/Bar.class");
-                    break;
-                case 11:
-                    contents.put("p/Bar.class", "v11/p/Bar.class");
-                    contents.put("p/Foo.class", "v11/p/Foo.class");
-                    contents.put("q/Bar.class", "v10/q/Bar.class");
-                    break;
-                default:
-                    Assert.fail("Test out of date, please add more cases");
-            }
-
-            contents.entrySet().stream().forEach(e -> {
-                String name = e.getKey();
-                int i = versionedNames.indexOf(name);
-                Assert.assertTrue(i != -1, name + " not in enames");
-                JarEntry je = versionedEntries.get(i);
-                try (InputStream is = jf.getInputStream(je)) {
-                    String s = new String(is.readAllBytes()).replaceAll(System.lineSeparator(), "");
-                    Assert.assertTrue(s.endsWith(e.getValue()), s);
-                } catch (IOException x) {
-                    throw new UncheckedIOException(x);
-                }
-            });
-        }
-    }
-
-    private void createFiles(String... files) {
-        ArrayList<String> list = new ArrayList();
-        Arrays.stream(files)
-                .map(f -> Paths.get(userdir.toAbsolutePath().toString(), f))
-                .forEach(p -> {
-                    try {
-                        Files.createDirectories(p.getParent());
-                        Files.createFile(p);
-                        list.clear();
-                        list.add(p.toString().replace(File.separatorChar, '/'));
-                        Files.write(p, list);
-                    } catch (IOException x) {
-                        throw new UncheckedIOException(x);
-                    }});
-    }
-
-    private void jar(String args) {
-        new sun.tools.jar.Main(System.out, System.err, "jar")
-                .run(args.split(" +"));
-    }
-}