1 /*
  2  * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  *
 23  */
 24 
 25 /*
 26  * @test
 27  * @summary Class-Path: attribute in MANIFEST file
 28  * @requires vm.cds
 29  * @library /test/lib
 30  * @compile test-classes/Hello.java
 31  * @run driver/timeout=240 ClassPathAttr
 32  */
 33 
 34 import jdk.test.lib.Platform;
 35 import jdk.test.lib.cds.CDSTestUtils;
 36 import jdk.test.lib.process.OutputAnalyzer;
 37 import java.io.File;
 38 import java.nio.file.Files;
 39 import java.nio.file.FileAlreadyExistsException;
 40 import java.nio.file.StandardCopyOption;
 41 import java.nio.file.Paths;
 42 
 43 
 44 public class ClassPathAttr {
 45 
 46   public static void main(String[] args) throws Exception {
 47     testNormalOps();
 48     testNonExistentJars();
 49     testClassPathAttrJarOnCP();
 50   }
 51 
 52   static void testNormalOps() throws Exception {
 53     buildCpAttr("cpattr1", "cpattr1.mf", "CpAttr1", "CpAttr1");
 54     buildCpAttr("cpattr1_long", "cpattr1_long.mf", "CpAttr1", "CpAttr1");
 55     buildCpAttr("cpattr2", "cpattr2.mf", "CpAttr2", "CpAttr2");
 56     buildCpAttr("cpattr3", "cpattr3.mf", "CpAttr3", "CpAttr2", "CpAttr3");
 57     buildCpAttr("cpattr4", "cpattr4.mf", "CpAttr4",
 58         "CpAttr2", "CpAttr3", "CpAttr4", "CpAttr5");
 59     buildCpAttr("cpattr5_123456789_223456789_323456789_423456789_523456789_623456789", "cpattr5_extra_long.mf", "CpAttr5", "CpAttr5");
 60 
 61     String[] classlist = { "CpAttr1", "CpAttr2", "CpAttr3", "CpAttr4", "CpAttr5"};
 62     String jar4 = TestCommon.getTestJar("cpattr4.jar");
 63     for (int i=1; i<=2; i++) {
 64       String jar1 = TestCommon.getTestJar("cpattr1.jar");
 65       if (i == 2) {
 66         // Test case #2 -- same as #1, except we use cpattr1_long.jar, which has a super-long
 67         // Class-Path: attribute.
 68         jar1 = TestCommon.getTestJar("cpattr1_long.jar");
 69       }
 70       String cp = jar1 + File.pathSeparator + jar4;
 71 
 72       TestCommon.testDump(cp, classlist);
 73 
 74       TestCommon.run(
 75           "-cp", cp,
 76           "CpAttr1")
 77         .assertNormalExit();
 78 
 79       // Logging test for class+path.
 80       TestCommon.run(
 81           "-Xlog:class+path",
 82           "-cp", cp,
 83           "CpAttr1")
 84         .assertNormalExit(output -> {
 85             output.shouldMatch("checking shared classpath entry: .*cpattr2.jar");
 86             output.shouldMatch("checking shared classpath entry: .*cpattr3.jar");
 87           });
 88 
 89       // Test handling of forward slash ('/') file separator when locating entries
 90       // in the classpath entry on Windows.
 91       // Skip the following test when CDS dynamic dump is enabled due to some
 92       // issue when converting a relative path to real path.
 93       if (Platform.isWindows() && !CDSTestUtils.DYNAMIC_DUMP) {
 94           // Test with relative path
 95           // Find the index to the dir before the jar file.
 96           int idx = jar1.lastIndexOf(File.separator);
 97           idx = jar1.substring(0, idx - 1).lastIndexOf(File.separator);
 98           // Setup jar directory and names.
 99           String jarDir = jar1.substring(0, idx);
100           String jar1Name = jar1.substring(idx + 1);
101           String jar4Name = jar4.substring(idx + 1);
102           String newCp = jar1Name.replace("\\", "/") + File.pathSeparator + jar4Name.replace("\\", "/");
103 
104           OutputAnalyzer out = TestCommon.testDump(jarDir, newCp, classlist, "-Xlog:class+path=info");
105           if (i == 1) {
106               out.shouldMatch("opened:.*cpattr1.jar"); // first jar on -cp
107           } else {
108               // first jar on -cp with long Class-Path: attribute
109               out.shouldMatch("opened:.*cpattr1_long.jar");
110           }
111           // one of the jar in the Class-Path: attribute of cpattr1.jar
112           out.shouldMatch("opened:.*cpattr2.jar");
113 
114           TestCommon.runWithRelativePath(
115               jarDir.replace("\\", "/"),
116               "-Xlog:class+path,class+load",
117               "-cp", newCp,
118               "CpAttr1")
119             .assertNormalExit(output -> {
120                 output.shouldMatch("checking shared classpath entry: .*cpattr2.jar");
121                 output.shouldMatch("checking shared classpath entry: .*cpattr3.jar");
122               });
123 
124           // Go one directory up.
125           int idx2 = jar1.substring(0, idx - 1).lastIndexOf(File.separator);
126           if (idx2 != -1) {
127               // Setup jar directory and names.
128               jarDir = jar1.substring(0, idx2);
129               // Set relative path to jar containing '\' and '/' file separators
130               // e.g. d1\d2/A.jar
131               jar1Name = jar1.substring(idx2 + 1).replace("\\", "/");
132               jar4Name = jar4.substring(idx2 + 1).replace("\\", "/");
133               jar1Name = jar1Name.replaceFirst("/", "\\\\");
134               jar4Name = jar4Name.replaceFirst("/", "\\\\");
135 
136               newCp = jar1Name + File.pathSeparator + jar4Name;
137               out = TestCommon.testDump(jarDir, newCp, classlist, "-Xlog:class+path=info");
138               if (i == 1) {
139                   out.shouldMatch("opened:.*cpattr1.jar"); // first jar on -cp
140               } else {
141                   // first jar on -cp with long Class-Path: attribute
142                   out.shouldMatch("opened:.*cpattr1_long.jar");
143               }
144               // one of the jar in the Class-Path: attribute of cpattr1.jar
145               out.shouldMatch("opened:.*cpattr2.jar");
146 
147               TestCommon.runWithRelativePath(
148                   jarDir.replace("\\", "/"),
149                   "-Xlog:class+path,class+load",
150                   "-cp", newCp,
151                   "CpAttr1")
152                 .assertNormalExit(output -> {
153                     output.shouldMatch("checking shared classpath entry: .*cpattr2.jar");
154                     output.shouldMatch("checking shared classpath entry: .*cpattr3.jar");
155                   });
156           }
157       }
158     }
159 
160     // test duplicate jars in the "Class-path" attribute in the jar manifest
161     buildCpAttr("cpattr_dup", "cpattr_dup.mf", "CpAttr1", "CpAttr1");
162     String cp = TestCommon.getTestJar("cpattr_dup.jar") + File.pathSeparator + jar4;
163     TestCommon.testDump(cp, classlist);
164 
165     TestCommon.run(
166         "-cp", cp,
167         "CpAttr1")
168       .assertNormalExit();
169   }
170 
171   static void testNonExistentJars() throws Exception {
172     buildCpAttr("cpattr6", "cpattr6.mf", "CpAttr6", "CpAttr6");
173 
174     String cp = TestCommon.getTestJar("cpattr6.jar");
175     String nonExistPath = CDSTestUtils.getOutputDir() + File.separator + "cpattrX.jar";
176     (new File(nonExistPath)).delete();
177 
178     TestCommon.testDump(cp, TestCommon.list("CpAttr6"),
179         "-Xlog:class+path");
180 
181     TestCommon.run(
182         "-Xlog:class+path",
183         "-cp", cp,
184         "CpAttr6")
185       .assertNormalExit(output -> {
186           output.shouldMatch("should be non-existent: .*cpattrX.jar");
187         });
188 
189     // Now make nonExistPath exist. CDS still loads, but archived non-system classes will not be used.
190     Files.copy(Paths.get(cp), Paths.get(nonExistPath),
191                StandardCopyOption.REPLACE_EXISTING);
192 
193     CDSTestUtils.Result result = TestCommon.run(
194         "-Xlog:class+path",
195         "-cp", cp,
196         "CpAttr6");
197     if (CDSTestUtils.isAOTClassLinkingEnabled()) {
198         result.assertAbnormalExit(output -> {
199                 output.shouldMatch("CDS archive has aot-linked classes. It cannot be used because the file .*cpattrX.jar exists");
200             });
201 
202     } else {
203         result.assertNormalExit(output -> {
204                 output.shouldMatch("Archived non-system classes are disabled because the file .*cpattrX.jar exists");
205             });
206     }
207   }
208 
209   static void testClassPathAttrJarOnCP() throws Exception {
210     String helloJar = JarBuilder.getOrCreateHelloJar();
211     String jar1 = TestCommon.getTestJar("cpattr1.jar");
212     String cp = jar1 + File.pathSeparator + helloJar;
213 
214     // The cpattr1.jar contains "Class-Path: cpattr2.jar".
215     // The cpattr2.jar contains "Class-Path: cpattr3.jar cpattr5_123456789_223456789_323456789_42345678.jar".
216     // With -cp cpattr1:hello.jar, the following shared paths should be stored in the CDS archive:
217     // cpattr1.jar:cpattr2.jar:cpattr3.jar:cpattr5_123456789_223456789_323456789_42345678.jari:hello.jar
218     TestCommon.testDump(cp, TestCommon.list("Hello"), "-Xlog:class+path");
219 
220     // Run with the same -cp apattr1.jar:hello.jar. The Hello class should be
221     // loaded from the archive.
222     TestCommon.run("-Xlog:class+path,class+load",
223                    "-cp", cp,
224                    "Hello")
225               .assertNormalExit(output -> {
226                   output.shouldContain("Hello source: shared objects file");
227                 });
228 
229     // Run with -cp apattr1.jar:cpattr2.jar:hello.jar. App classpath mismatch should be detected.
230     String jar2 = TestCommon.getTestJar("cpattr2.jar");
231     cp = jar1 + File.pathSeparator + jar2 + File.pathSeparator + helloJar;
232     TestCommon.run("-Xlog:class+path,class+load",
233                    "-cp", cp,
234                    "Hello")
235               .assertAbnormalExit(output -> {
236                   output.shouldMatch(".*APP classpath mismatch, actual: -Djava.class.path=.*cpattr1.jar.*cpattr2.jar.*hello.jar")
237               .shouldContain("Unable to use shared archive.");
238                 });
239 
240     // Run with different -cp cpattr2.jar:hello.jar. App classpath mismatch should be detected.
241     cp = jar2 + File.pathSeparator + helloJar;
242     TestCommon.run("-Xlog:class+path,class+load",
243                    "-cp", cp,
244                    "Hello")
245               .assertAbnormalExit(output -> {
246                   output.shouldMatch(".*APP classpath mismatch, actual: -Djava.class.path=.*cpattr2.jar.*hello.jar")
247               .shouldContain("Unable to use shared archive.");
248                 });
249 
250     // Dumping with -cp cpattr1.jar:cpattr2.jar:hello.jar
251     // The cpattr2.jar is from the Class-Path: attribute of cpattr1.jar.
252     cp = jar1 + File.pathSeparator + jar2 + File.pathSeparator + helloJar;
253     TestCommon.testDump(cp, TestCommon.list("Hello"), "-Xlog:class+path");
254 
255     // Run with the same -cp as dump time. The Hello class should be loaded from the archive.
256     TestCommon.run("-Xlog:class+path,class+load",
257                    "-cp", cp,
258                    "Hello")
259               .assertNormalExit(output -> {
260                   output.shouldContain("Hello source: shared objects file");
261                 });
262 
263   }
264 
265   private static void buildCpAttr(String jarName, String manifest, String enclosingClassName, String ...testClassNames) throws Exception {
266     String jarClassesDir = CDSTestUtils.getOutputDir() + File.separator + jarName + "_classes";
267     try { Files.createDirectory(Paths.get(jarClassesDir)); } catch (FileAlreadyExistsException e) { }
268 
269     JarBuilder.compile(jarClassesDir, System.getProperty("test.src") + File.separator +
270         "test-classes" + File.separator + enclosingClassName + ".java");
271     JarBuilder.buildWithManifest(jarName, manifest, jarClassesDir, testClassNames);
272   }
273 }