changeset 59761:1d45272feec9

Merge
author jwilhelm
date Fri, 12 Jun 2020 05:12:32 +0200
parents 4a485c89d5a0 a5da4ad3129e
children 6bc52804b930
files .hgtags
diffstat 7 files changed, 373 insertions(+), 158 deletions(-) [+]
line wrap: on
line diff
--- a/.hgtags	Thu Jun 11 16:35:29 2020 -0700
+++ b/.hgtags	Fri Jun 12 05:12:32 2020 +0200
@@ -635,9 +635,7 @@
 7223c6d610343fd8323af9d07d501e01fa1a7696 jdk-15+22
 f143729ca00ec14a98ea5c7f73acba88da97746e jdk-15+23
 497fd9f9129c4928fd5a876dd55e0daf6298b511 jdk-15+24
-58833044988772ca06c97ab2f142474a8627af80 jdk-15+25
-58833044988772ca06c97ab2f142474a8627af80 jdk-15+25
 90b266a84c06f1b3dc0ed8767856793e8c1c357e jdk-15+25
 0a32396f7a690015d22ca3328ac441a358295d90 jdk-15+26
-506abc554caeb275928c02bf3a16e95d1978749f jdk-15+27
 93813843680bbe1b7efbca56c03fd137f20a2c31 jdk-16+0
+93813843680bbe1b7efbca56c03fd137f20a2c31 jdk-15+27
--- a/src/java.base/share/classes/java/security/Provider.java	Thu Jun 11 16:35:29 2020 -0700
+++ b/src/java.base/share/classes/java/security/Provider.java	Fri Jun 12 05:12:32 2020 +0200
@@ -858,10 +858,18 @@
     // serviceMap changed since last call to getServices()
     private volatile transient boolean servicesChanged;
 
+    // Map<String,String> used to keep track of legacy registration
+    private transient Map<String,String> legacyStrings;
+
     // Map<ServiceKey,Service>
     // used for services added via putService(), initialized on demand
     private transient Map<ServiceKey,Service> serviceMap;
 
+    // For backward compatibility, the registration ordering of
+    // SecureRandom (RNG) algorithms needs to be preserved for
+    // "new SecureRandom()" calls when this provider is used
+    private transient Set<Service> prngServices;
+
     // Map<ServiceKey,Service>
     // used for services added via legacy methods, init on demand
     private transient Map<ServiceKey,Service> legacyMap;
@@ -913,12 +921,18 @@
         putAll(copy);
     }
 
-    private static boolean isProviderInfo(Object key) {
+    // check whether to update 'legacyString' with the specified key
+    private boolean checkLegacy(Object key) {
         String keyString = (String)key;
         if (keyString.startsWith("Provider.")) {
-            return true;
+            return false;
         }
-        return false;
+
+        legacyChanged = true;
+        if (legacyStrings == null) {
+            legacyStrings = new LinkedHashMap<>();
+        }
+        return true;
     }
 
     /**
@@ -934,20 +948,20 @@
 
     private Object implRemove(Object key) {
         if (key instanceof String) {
-            if (isProviderInfo(key)) {
+            if (!checkLegacy(key)) {
                 return null;
             }
-            legacyChanged = true;
+            legacyStrings.remove((String)key);
         }
         return super.remove(key);
     }
 
     private boolean implRemove(Object key, Object value) {
         if (key instanceof String && value instanceof String) {
-            if (isProviderInfo(key)) {
+            if (!checkLegacy(key)) {
                 return false;
             }
-            legacyChanged = true;
+            legacyStrings.remove((String)key, (String)value);
         }
         return super.remove(key, value);
     }
@@ -955,20 +969,21 @@
     private boolean implReplace(Object key, Object oldValue, Object newValue) {
         if ((key instanceof String) && (oldValue instanceof String) &&
                 (newValue instanceof String)) {
-            if (isProviderInfo(key)) {
+            if (!checkLegacy(key)) {
                 return false;
             }
-            legacyChanged = true;
+            legacyStrings.replace((String)key, (String)oldValue,
+                    (String)newValue);
         }
         return super.replace(key, oldValue, newValue);
     }
 
     private Object implReplace(Object key, Object value) {
         if ((key instanceof String) && (value instanceof String)) {
-            if (isProviderInfo(key)) {
+            if (!checkLegacy(key)) {
                 return null;
             }
-            legacyChanged = true;
+            legacyStrings.replace((String)key, (String)value);
         }
         return super.replace(key, value);
     }
@@ -977,17 +992,26 @@
     private void implReplaceAll(BiFunction<? super Object, ? super Object,
             ? extends Object> function) {
         legacyChanged = true;
+        if (legacyStrings == null) {
+            legacyStrings = new LinkedHashMap<>();
+        } else {
+            legacyStrings.replaceAll((BiFunction<? super String, ? super String,
+                    ? extends String>) function);
+        }
         super.replaceAll(function);
     }
 
     @SuppressWarnings("unchecked") // Function must actually operate over strings
-    private Object implMerge(Object key, Object value, BiFunction<? super Object,
-            ? super Object, ? extends Object> remappingFunction) {
+    private Object implMerge(Object key, Object value,
+            BiFunction<? super Object, ? super Object, ? extends Object>
+            remappingFunction) {
         if ((key instanceof String) && (value instanceof String)) {
-            if (isProviderInfo(key)) {
+            if (!checkLegacy(key)) {
                 return null;
             }
-            legacyChanged = true;
+            legacyStrings.merge((String)key, (String)value,
+                    (BiFunction<? super String, ? super String,
+                    ? extends String>) remappingFunction);
         }
         return super.merge(key, value, remappingFunction);
     }
@@ -996,10 +1020,12 @@
     private Object implCompute(Object key, BiFunction<? super Object,
             ? super Object, ? extends Object> remappingFunction) {
         if (key instanceof String) {
-            if (isProviderInfo(key)) {
+            if (!checkLegacy(key)) {
                 return null;
             }
-            legacyChanged = true;
+            legacyStrings.compute((String) key,
+                    (BiFunction<? super String,? super String,
+                    ? extends String>) remappingFunction);
         }
         return super.compute(key, remappingFunction);
     }
@@ -1008,10 +1034,12 @@
     private Object implComputeIfAbsent(Object key, Function<? super Object,
             ? extends Object> mappingFunction) {
         if (key instanceof String) {
-            if (isProviderInfo(key)) {
+            if (!checkLegacy(key)) {
                 return null;
             }
-            legacyChanged = true;
+            legacyStrings.computeIfAbsent((String) key,
+                    (Function<? super String, ? extends String>)
+                    mappingFunction);
         }
         return super.computeIfAbsent(key, mappingFunction);
     }
@@ -1020,35 +1048,40 @@
     private Object implComputeIfPresent(Object key, BiFunction<? super Object,
             ? super Object, ? extends Object> remappingFunction) {
         if (key instanceof String) {
-            if (isProviderInfo(key)) {
+            if (!checkLegacy(key)) {
                 return null;
             }
-            legacyChanged = true;
+            legacyStrings.computeIfPresent((String) key,
+                    (BiFunction<? super String, ? super String,
+                    ? extends String>) remappingFunction);
         }
         return super.computeIfPresent(key, remappingFunction);
     }
 
     private Object implPut(Object key, Object value) {
         if ((key instanceof String) && (value instanceof String)) {
-            if (isProviderInfo(key)) {
+            if (!checkLegacy(key)) {
                 return null;
             }
-            legacyChanged = true;
+            legacyStrings.put((String)key, (String)value);
         }
         return super.put(key, value);
     }
 
     private Object implPutIfAbsent(Object key, Object value) {
         if ((key instanceof String) && (value instanceof String)) {
-            if (isProviderInfo(key)) {
+            if (!checkLegacy(key)) {
                 return null;
             }
-            legacyChanged = true;
+            legacyStrings.putIfAbsent((String)key, (String)value);
         }
         return super.putIfAbsent(key, value);
     }
 
     private void implClear() {
+        if (legacyStrings != null) {
+            legacyStrings.clear();
+        }
         if (legacyMap != null) {
             legacyMap.clear();
         }
@@ -1056,6 +1089,7 @@
         legacyChanged = false;
         servicesChanged = false;
         serviceSet = null;
+        prngServices = null;
         super.clear();
         putId();
     }
@@ -1095,7 +1129,7 @@
      * service objects.
      */
     private void ensureLegacyParsed() {
-        if (legacyChanged == false) {
+        if (legacyChanged == false || (legacyStrings == null)) {
             return;
         }
         serviceSet = null;
@@ -1104,7 +1138,7 @@
         } else {
             legacyMap.clear();
         }
-        for (Map.Entry<?,?> entry : super.entrySet()) {
+        for (Map.Entry<String,String> entry : legacyStrings.entrySet()) {
             parseLegacyPut(entry.getKey(), entry.getValue());
         }
         removeInvalidServices(legacyMap);
@@ -1125,12 +1159,12 @@
         }
     }
 
-    private String[] getTypeAndAlgorithm(String key) {
+    private static String[] getTypeAndAlgorithm(String key) {
         int i = key.indexOf('.');
         if (i < 1) {
             if (debug != null) {
-                debug.println("Ignoring invalid entry in provider "
-                        + name + ":" + key);
+                debug.println("Ignoring invalid entry in provider: "
+                        + key);
             }
             return null;
         }
@@ -1143,15 +1177,7 @@
     private static final String ALIAS_PREFIX_LOWER = "alg.alias.";
     private static final int ALIAS_LENGTH = ALIAS_PREFIX.length();
 
-    private void parseLegacyPut(Object k, Object v) {
-        if (!(k instanceof String) || !(v instanceof String)) {
-            return;
-        }
-        String name = (String) k;
-        String value = (String) v;
-        if (isProviderInfo(name)) {
-            return;
-        }
+    private void parseLegacyPut(String name, String value) {
         if (name.toLowerCase(ENGLISH).startsWith(ALIAS_PREFIX_LOWER)) {
             // e.g. put("Alg.Alias.MessageDigest.SHA", "SHA-1");
             // aliasKey ~ MessageDigest.SHA
@@ -1193,6 +1219,10 @@
                     legacyMap.put(key, s);
                 }
                 s.className = className;
+
+                if (type.equals("SecureRandom")) {
+                    updateSecureRandomEntries(true, s);
+                }
             } else { // attribute
                 // e.g. put("MessageDigest.SHA-1 ImplementedIn", "Software");
                 String attributeValue = value;
@@ -1352,9 +1382,46 @@
         servicesChanged = true;
         synchronized (this) {
             putPropertyStrings(s);
+            if (type.equals("SecureRandom")) {
+                updateSecureRandomEntries(true, s);
+            }
         }
     }
 
+    private void updateSecureRandomEntries(boolean doAdd, Service s) {
+        Objects.requireNonNull(s);
+        if (doAdd) {
+            if (prngServices == null) {
+                prngServices = new LinkedHashSet<Service>();
+            }
+            prngServices.add(s);
+        } else {
+            prngServices.remove(s);
+        }
+
+        if (debug != null) {
+            debug.println((doAdd? "Add":"Remove") + " SecureRandom algo " +
+                s.getAlgorithm());
+        }
+    }
+
+    // used by new SecureRandom() to find out the default SecureRandom
+    // service for this provider
+    synchronized Service getDefaultSecureRandomService() {
+        checkInitialized();
+
+        if (legacyChanged) {
+            prngServices = null;
+            ensureLegacyParsed();
+        }
+
+        if (prngServices != null && !prngServices.isEmpty()) {
+            return prngServices.iterator().next();
+        }
+
+        return null;
+    }
+
     /**
      * Put the string properties for this Service in this Provider's
      * Hashtable.
@@ -1448,6 +1515,9 @@
         }
         synchronized (this) {
             removePropertyStrings(s);
+            if (type.equals("SecureRandom")) {
+                updateSecureRandomEntries(false, s);
+            }
         }
     }
 
--- a/src/java.base/share/classes/java/security/SecureRandom.java	Thu Jun 11 16:35:29 2020 -0700
+++ b/src/java.base/share/classes/java/security/SecureRandom.java	Fri Jun 12 05:12:32 2020 +0200
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1996, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1996, 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
@@ -259,35 +259,51 @@
     }
 
     private void getDefaultPRNG(boolean setSeed, byte[] seed) {
-        String prng = getPrngAlgorithm();
-        if (prng == null) {
-            // bummer, get the SUN implementation
-            prng = "SHA1PRNG";
+        Service prngService = null;
+        String prngAlgorithm = null;
+        for (Provider p : Providers.getProviderList().providers()) {
+            // SUN provider uses the SunEntries.DEF_SECURE_RANDOM_ALGO
+            // as the default SecureRandom algorithm; for other providers,
+            // Provider.getDefaultSecureRandom() will use the 1st
+            // registered SecureRandom algorithm
+            if (p.getName().equals("SUN")) {
+                prngAlgorithm = SunEntries.DEF_SECURE_RANDOM_ALGO;
+                prngService = p.getService("SecureRandom", prngAlgorithm);
+                break;
+            } else {
+                prngService = p.getDefaultSecureRandomService();
+                if (prngService != null) {
+                    prngAlgorithm = prngService.getAlgorithm();
+                    break;
+                }
+            }
+        }
+        // per javadoc, if none of the Providers support a RNG algorithm,
+        // then an implementation-specific default is returned.
+        if (prngService == null) {
+            prngAlgorithm = "SHA1PRNG";
             this.secureRandomSpi = new sun.security.provider.SecureRandom();
             this.provider = Providers.getSunProvider();
-            if (setSeed) {
-                this.secureRandomSpi.engineSetSeed(seed);
-            }
         } else {
             try {
-                SecureRandom random = SecureRandom.getInstance(prng);
-                this.secureRandomSpi = random.getSecureRandomSpi();
-                this.provider = random.getProvider();
-                if (setSeed) {
-                    this.secureRandomSpi.engineSetSeed(seed);
-                }
+                this.secureRandomSpi = (SecureRandomSpi)
+                    prngService.newInstance(null);
+                this.provider = prngService.getProvider();
             } catch (NoSuchAlgorithmException nsae) {
-                // never happens, because we made sure the algorithm exists
+                // should not happen
                 throw new RuntimeException(nsae);
             }
         }
+        if (setSeed) {
+            this.secureRandomSpi.engineSetSeed(seed);
+        }
         // JDK 1.1 based implementations subclass SecureRandom instead of
         // SecureRandomSpi. They will also go through this code path because
         // they must call a SecureRandom constructor as it is their superclass.
         // If we are dealing with such an implementation, do not set the
         // algorithm value as it would be inaccurate.
         if (getClass() == SecureRandom.class) {
-            this.algorithm = prng;
+            this.algorithm = prngAlgorithm;
         }
     }
 
@@ -621,13 +637,6 @@
     }
 
     /**
-     * Returns the {@code SecureRandomSpi} of this {@code SecureRandom} object.
-     */
-    SecureRandomSpi getSecureRandomSpi() {
-        return secureRandomSpi;
-    }
-
-    /**
      * Returns the provider of this {@code SecureRandom} object.
      *
      * @return the provider of this {@code SecureRandom} object.
@@ -868,30 +877,6 @@
         return retVal;
     }
 
-    /**
-     * Gets a default PRNG algorithm by looking through all registered
-     * providers. Returns the first PRNG algorithm of the first provider that
-     * has registered a {@code SecureRandom} implementation, or null if none of
-     * the registered providers supplies a {@code SecureRandom} implementation.
-     */
-    private static String getPrngAlgorithm() {
-        for (Provider p : Providers.getProviderList().providers()) {
-            // For SUN provider, we use SunEntries.DEFF_SECURE_RANDOM_ALGO
-            // as the default SecureRandom algorithm; for other providers,
-            // we continue to iterate through to the 1st SecureRandom
-            // service
-            if (p.getName().equals("SUN")) {
-                return SunEntries.DEF_SECURE_RANDOM_ALGO;
-            }
-            for (Service s : p.getServices()) {
-                if (s.getType().equals("SecureRandom")) {
-                    return s.getAlgorithm();
-                }
-            }
-        }
-        return null;
-    }
-
     /*
      * Lazily initialize since Pattern.compile() is heavy.
      * Effective Java (2nd Edition), Item 71.
--- a/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/StandardBundlerParam.java	Thu Jun 11 16:35:29 2020 -0700
+++ b/src/jdk.incubator.jpackage/share/classes/jdk/incubator/jpackage/internal/StandardBundlerParam.java	Fri Jun 12 05:12:32 2020 +0200
@@ -484,6 +484,16 @@
                     PREDEFINED_RUNTIME_IMAGE.getID()));
         }
 
+        if (Platform.isMac()) {
+            // On Mac topImage can be runtime root or runtime home.
+            Path runtimeHome = topImage.toPath().resolve("Contents/Home");
+            if (Files.isDirectory(runtimeHome)) {
+                // topImage references runtime root, adjust it to pick data from
+                // runtime home
+                topImage = runtimeHome.toFile();
+            }
+        }
+
         // copy whole runtime, need to skip jmods and src.zip
         final List<String> excludes = Arrays.asList("jmods", "src.zip");
         IOUtils.copyRecursive(topImage.toPath(),
--- a/test/jdk/java/security/SecureRandom/DefaultAlgo.java	Thu Jun 11 16:35:29 2020 -0700
+++ b/test/jdk/java/security/SecureRandom/DefaultAlgo.java	Fri Jun 12 05:12:32 2020 +0200
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 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
@@ -22,33 +22,96 @@
  */
 
 import static java.lang.System.out;
+import java.security.Provider;
+import java.security.Security;
 import java.security.SecureRandom;
+import java.security.Provider.Service;
+import java.util.Objects;
+import java.util.Arrays;
 import sun.security.provider.SunEntries;
 
 /**
  * @test
- * @bug 8228613
- * @summary Ensure that the default SecureRandom algo matches
- *     SunEntries.DEF_SECURE_RANDOM_ALGO when SUN provider is used
+ * @bug 8228613 8246613
+ * @summary Ensure that the default SecureRandom algo used is based
+ *     on the registration ordering, and falls to next provider
+ *     if none are found
  * @modules java.base/sun.security.provider
  */
 public class DefaultAlgo {
 
     public static void main(String[] args) throws Exception {
-        SecureRandom sr = new SecureRandom();
-        String actualAlg = sr.getAlgorithm();
-        out.println("Default SecureRandom algo: " + actualAlg);
-        if (sr.getProvider().getName().equals("SUN")) {
-            // when using Sun provider, compare and check if the algorithm
-            // matches SunEntries.DEF_SECURE_RANDOM_ALGO
-            if (actualAlg.equals(SunEntries.DEF_SECURE_RANDOM_ALGO)) {
-                out.println("Test Passed");
+        String[] algos = { "A", "B", "C" };
+        test3rdParty(algos);
+        // reverse the order and re-check
+        String[] algosReversed = { "C", "B", "A" };
+        test3rdParty(algosReversed);
+    }
+
+    private static void test3rdParty(String[] algos) {
+        Provider[] provs = {
+            new SampleLegacyProvider(algos),
+            new SampleServiceProvider(algos)
+        };
+        for (Provider p : provs) {
+            checkDefault(p, algos);
+        }
+    }
+
+    // validate the specified SecureRandom obj to be from the specified
+    // provider and matches the specified algorithm
+    private static void validate(SecureRandom sr, String pName, String algo) {
+        if (!sr.getProvider().getName().equals(pName)) {
+            throw new RuntimeException("Failed provider check, exp: " +
+                    pName + ", got " + sr.getProvider().getName());
+        }
+        if (!sr.getAlgorithm().equals(algo)) {
+            throw new RuntimeException("Failed algo check, exp: " +
+                    algo + ", got " + sr.getAlgorithm());
+        }
+    }
+
+    private static void checkDefault(Provider p, String ... algos) {
+        out.println(p.getName() + " with " + Arrays.toString(algos));
+        int pos = Security.insertProviderAt(p, 1);
+        String pName = p.getName();
+        boolean isLegacy = pName.equals("SampleLegacy");
+        try {
+            if (isLegacy) {
+                for (String s : algos) {
+                    validate(new SecureRandom(), pName, s);
+                    p.remove("SecureRandom." + s);
+                    out.println("removed "  + s);
+                }
+                validate(new SecureRandom(), "SUN",
+                        SunEntries.DEF_SECURE_RANDOM_ALGO);
             } else {
-                throw new RuntimeException("Failed: Expected " +
-                        SunEntries.DEF_SECURE_RANDOM_ALGO);
+                validate(new SecureRandom(), pName, algos[0]);
+            }
+            out.println("=> Test Passed");
+        } finally {
+            if (pos != -1) {
+                Security.removeProvider(p.getName());
             }
-        } else {
-            out.println("Skip test for non-Sun provider: " + sr.getProvider());
+        }
+    }
+
+    private static class SampleLegacyProvider extends Provider {
+        SampleLegacyProvider(String[] listOfSupportedRNGs) {
+            super("SampleLegacy", "1.0", "test provider using legacy put");
+            for (String s : listOfSupportedRNGs) {
+                put("SecureRandom." + s, "sun.security.provider.SecureRandom");
+            }
+        }
+    }
+
+    private static class SampleServiceProvider extends Provider {
+        SampleServiceProvider(String[] listOfSupportedRNGs) {
+            super("SampleService", "1.0", "test provider using putService");
+            for (String s : listOfSupportedRNGs) {
+                putService(new Provider.Service(this, "SecureRandom", s,
+                        "sun.security.provider.SecureRandom", null, null));
+            }
         }
     }
 }
--- a/test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java	Thu Jun 11 16:35:29 2020 -0700
+++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/BasicTest.java	Fri Jun 12 05:12:32 2020 +0200
@@ -303,59 +303,6 @@
         HelloApp.executeLauncherAndVerifyOutput(cmd);
     }
 
-    @Parameter("Hello")
-    @Parameter("com.foo/com.foo.main.Aloha")
-    @Test
-    public void testJLinkRuntime(String javaAppDesc) throws IOException {
-        JavaAppDesc appDesc = JavaAppDesc.parse(javaAppDesc);
-
-        JPackageCommand cmd = JPackageCommand.helloAppImage(appDesc);
-
-        final String moduleName = appDesc.moduleName();
-
-        if (moduleName != null) {
-            // Build module jar.
-            cmd.executePrerequisiteActions();
-        }
-
-        final Path runtimeDir = TKit.createTempDirectory("runtime").resolve("data");
-
-        // List of modules required for test app.
-        final var modules = new String[] {
-            "java.base",
-            "java.desktop"
-        };
-
-        Executor jlink = getToolProvider(JavaTool.JLINK)
-        .saveOutput(false)
-        .addArguments(
-                "--add-modules", String.join(",", modules),
-                "--output", runtimeDir.toString(),
-                "--strip-debug",
-                "--no-header-files",
-                "--no-man-pages");
-
-        TKit.trace("jlink output BEGIN");
-        try (Stream<Path> paths = Files.walk(runtimeDir)) {
-            paths.filter(Files::isRegularFile)
-                    .map(runtimeDir::relativize)
-                    .map(Path::toString)
-                    .forEach(TKit::trace);
-        }
-        TKit.trace("jlink output END");
-
-        if (moduleName != null) {
-            jlink.addArguments("--add-modules", moduleName, "--module-path",
-                    Path.of(cmd.getArgumentValue("--module-path")).resolve(
-                            "hello.jar").toString());
-        }
-
-        jlink.execute();
-
-        cmd.addArguments("--runtime-image", runtimeDir);
-        cmd.executeAndAssertHelloAppImageCreated();
-    }
-
     private static Executor getJPackageToolProvider() {
         return getToolProvider(JavaTool.JPACKAGE);
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/jpackage/share/jdk/jpackage/tests/CookedRuntimeTest.java	Fri Jun 12 05:12:32 2020 +0200
@@ -0,0 +1,142 @@
+/*
+ * 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.
+ */
+
+package jdk.jpackage.tests;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collection;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+import java.nio.file.Path;
+import jdk.jpackage.test.Annotations.Parameters;
+import jdk.jpackage.test.Annotations.Test;
+import jdk.jpackage.test.Executor;
+import jdk.jpackage.test.JPackageCommand;
+import jdk.jpackage.test.JavaAppDesc;
+import jdk.jpackage.test.JavaTool;
+import jdk.jpackage.test.TKit;
+
+
+/*
+ * @test
+ * @summary test '--runtime-image' option of jpackage
+ * @library ../../../../helpers
+ * @build jdk.jpackage.test.*
+ * @modules jdk.incubator.jpackage/jdk.incubator.jpackage.internal
+ * @compile CookedRuntimeTest.java
+ * @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.Main
+ *  --jpt-run=jdk.jpackage.tests.CookedRuntimeTest
+ */
+
+public final class CookedRuntimeTest {
+
+    public CookedRuntimeTest(String javaAppDesc, String jlinkOutputSubdir,
+            String runtimeSubdir) {
+        this.javaAppDesc = javaAppDesc;
+        this.jlinkOutputSubdir = Path.of(jlinkOutputSubdir);
+        this.runtimeSubdir = Path.of(runtimeSubdir);
+    }
+
+    @Test
+    public void test() throws IOException {
+        JavaAppDesc appDesc = JavaAppDesc.parse(javaAppDesc);
+
+        JPackageCommand cmd = JPackageCommand.helloAppImage(appDesc);
+
+        final String moduleName = appDesc.moduleName();
+
+        if (moduleName != null) {
+            // Build module jar.
+            cmd.executePrerequisiteActions();
+        }
+
+        final Path workDir = TKit.createTempDirectory("runtime").resolve("data");
+        final Path jlinkOutputDir = workDir.resolve(jlinkOutputSubdir);
+        Files.createDirectories(jlinkOutputDir.getParent());
+
+        // List of modules required for test app.
+        final var modules = new String[] {
+            "java.base",
+            "java.desktop"
+        };
+
+        Executor jlink = new Executor()
+        .setToolProvider(JavaTool.JLINK)
+        .dumpOutput()
+        .addArguments(
+                "--add-modules", String.join(",", modules),
+                "--output", jlinkOutputDir.toString(),
+                "--strip-debug",
+                "--no-header-files",
+                "--no-man-pages");
+
+        if (moduleName != null) {
+            jlink.addArguments("--add-modules", moduleName, "--module-path",
+                    Path.of(cmd.getArgumentValue("--module-path")).resolve(
+                            "hello.jar").toString());
+        }
+
+        jlink.execute();
+
+        TKit.trace("jlink output BEGIN");
+        try (Stream<Path> paths = Files.walk(jlinkOutputDir)) {
+            paths.filter(Files::isRegularFile)
+                    .map(jlinkOutputDir::relativize)
+                    .map(Path::toString)
+                    .forEach(TKit::trace);
+        }
+        TKit.trace("jlink output END");
+
+        cmd.setArgumentValue("--runtime-image", workDir.resolve(runtimeSubdir));
+        cmd.executeAndAssertHelloAppImageCreated();
+    }
+
+    @Parameters
+    public static Collection data() {
+        final List<String> javaAppDescs = List.of("Hello",
+                "com.foo/com.foo.main.Aloha");
+
+        final List<String[]> paths = new ArrayList<>();
+        paths.add(new String[] { "", "" });
+        if (TKit.isOSX()) {
+            // On OSX jpackage should accept both runtime root and runtime home
+            // directories.
+            paths.add(new String[] { "Contents/Home", "" });
+        }
+
+        List<Object[]> data = new ArrayList<>();
+        for (var javaAppDesc : javaAppDescs) {
+            for (var pathCfg : paths) {
+                data.add(new Object[] { javaAppDesc, pathCfg[0], pathCfg[1] });
+            }
+        }
+
+        return data;
+    }
+
+    private final String javaAppDesc;
+    private final Path jlinkOutputSubdir;
+    private final Path runtimeSubdir;
+}