Skip to content

Commit a0624ee

Browse files
committed
Rework service lookup to allow absolute paths. Fixes #2336.
1 parent 91444d1 commit a0624ee

File tree

1 file changed

+112
-28
lines changed

1 file changed

+112
-28
lines changed

core/src/main/java/org/jruby/runtime/load/ClassExtensionLibrary.java

Lines changed: 112 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@
2727
***** END LICENSE BLOCK *****/
2828
package org.jruby.runtime.load;
2929

30+
import jnr.posix.JavaSecuredFile;
3031
import org.jruby.Ruby;
3132

33+
import java.net.URL;
34+
3235
/**
3336
* The ClassExtensionLibrary wraps a class which implements BasicLibraryService,
3437
* and when asked to load the service, does a basicLoad of the BasicLibraryService.
@@ -41,40 +44,121 @@ public class ClassExtensionLibrary implements Library {
4144
private final Class theClass;
4245
private final String name;
4346

47+
/**
48+
* Try to locate an extension service in the current classloader resources. This happens
49+
* after the jar has been added to JRuby's URLClassLoader (JRubyClassLoader) and is how
50+
* extensions can magically load.
51+
*
52+
* The basic logic is to use the require name (@param searchName) to build the name of
53+
* a class ending in "Service", and then invoke that class to boot the extension.
54+
*
55+
* The looping logic here was in response to a RubyGems change that started absolutizing
56+
* the path to the extension jar under some circumstances, leading to an incorrect
57+
* package/class name for the service contained therein. The new logic will try
58+
* successively more trailing elements until one of them is not a valid package
59+
* name or all elements have been exhausted.
60+
*
61+
* @param runtime the current JRuby runtime
62+
* @param searchName the name passed to `require`
63+
* @return a ClassExtensionLibrary that will boot the ext, or null if none was found
64+
*/
4465
static ClassExtensionLibrary tryFind(Ruby runtime, String searchName) {
45-
// Create package name, by splitting on / and joining all but the last elements with a ".", and downcasing them.
46-
String[] all = searchName.split("/");
47-
48-
StringBuilder finName = new StringBuilder();
49-
for(int i=0, j=(all.length-1); i<j; i++) {
50-
finName.append(all[i].toLowerCase()).append(".");
51-
}
52-
53-
try {
54-
// Make the class name look nice, by splitting on _ and capitalize each segment, then joining
55-
// the, together without anything separating them, and last put on "Service" at the end.
56-
String[] last = all[all.length-1].split("_");
57-
for(int i=0, j=last.length; i<j; i++) {
58-
if ("".equals(last[i])) break;
59-
finName.append(Character.toUpperCase(last[i].charAt(0))).append(last[i].substring(1));
66+
// Create package name, by splitting on / and successively accumulating elements to form a class name
67+
String[] elts = searchName.split("/");
68+
69+
boolean isAbsolute = new JavaSecuredFile(searchName).isAbsolute();
70+
71+
String simpleName = buildSimpleName(elts[elts.length - 1]);
72+
73+
int firstElement = isAbsolute ? elts.length - 1 : 0;
74+
for (; firstElement >= 0; firstElement--) {
75+
String className = buildClassName(elts, firstElement, elts.length - 1, simpleName);
76+
ClassExtensionLibrary library = tryServiceLoad(runtime, className);
77+
78+
if (library != null) return library;
79+
}
80+
81+
return null;
82+
}
83+
84+
/**
85+
* Build the "simple" part of a service class name from the given require path element
86+
* by splitting on "_" and concatenating as CamelCase, plus "Service" suffix.
87+
*
88+
* @param element the element from which to build a simple service class name
89+
* @return the resulting simple service class name
90+
*/
91+
private static String buildSimpleName(String element) {
92+
StringBuilder nameBuilder = new StringBuilder(element.length() + "Service".length());
93+
94+
String[] last = element.split("_");
95+
for (String part : last) {
96+
if (part.isEmpty()) break;
97+
98+
nameBuilder
99+
.append(Character.toUpperCase(part.charAt(0)))
100+
.append(part, 1, part.length());
101+
}
102+
nameBuilder.append("Service");
103+
104+
return nameBuilder.toString();
105+
}
106+
107+
/**
108+
* Build the full class name for a service by joining the specified package elements
109+
* with '.' and appending the given simple class name.
110+
*
111+
* @param elts the array from which to retrieve the package elements
112+
* @param firstElement the first element to use
113+
* @param end the end index at which to stop building the package name
114+
* @param simpleName the simple class name for the service
115+
* @return the full class name for the service
116+
*/
117+
private static String buildClassName(String[] elts, int firstElement, int end, String simpleName) {
118+
StringBuilder nameBuilder = new StringBuilder();
119+
for (int offset = firstElement; offset < end; offset++) {
120+
// avoid blank elements from leading or double slashes
121+
if (elts[offset].isEmpty()) continue;
122+
123+
nameBuilder
124+
.append(elts[offset].toLowerCase())
125+
.append('.');
60126
}
61-
finName.append("Service");
62127

63-
// We don't want a package name beginning with dots, so we remove them
64-
String className = finName.toString().replaceAll("^\\.*","");
128+
nameBuilder.append(simpleName);
65129

66-
// quietly try to load the class
67-
Class theClass = runtime.getJavaSupport().loadJavaClass(className);
68-
return new ClassExtensionLibrary(className + ".java", theClass);
69-
} catch (ClassNotFoundException cnfe) {
70-
if (runtime.isDebug()) cnfe.printStackTrace();
130+
return nameBuilder.toString();
131+
}
132+
133+
/**
134+
* Try loading the given service class. Rather than raise ClassNotFoundException for
135+
* jars that do not contain any service class, we require that the service class be a
136+
* "normal" .class file accessible as a classloader resource. If it can be found
137+
* using ClassLoader.getResource, we proceed to attempt to load it as a class.
138+
*
139+
* @param runtime the Ruby runtime into which the extension service will load
140+
* @param className the class name of the service class
141+
* @return a ClassExtensionLibrary if the service class was found; null otherwise.
142+
*/
143+
private static ClassExtensionLibrary tryServiceLoad(Ruby runtime, String className) {
144+
String classFile = className.replaceAll("\\.", "/") + ".class";
145+
146+
try {
147+
// quietly try to load the class, which must be reachable as a .class resource
148+
URL resource = runtime.getJRubyClassLoader().getResource(classFile);
149+
if (resource != null) {
150+
Class theClass = runtime.getJavaSupport().loadJavaClass(className);
151+
return new ClassExtensionLibrary(className + ".java", theClass);
152+
}
153+
} catch (ClassNotFoundException cnfe) {
154+
if (runtime.isDebug()) cnfe.printStackTrace();
155+
} catch (UnsupportedClassVersionError ucve) {
156+
if (runtime.isDebug()) ucve.printStackTrace();
157+
throw runtime.newLoadError("JRuby ext built for wrong Java version in `" + className + "': " + ucve, className);
158+
}
71159

72-
// So apparently the class doesn't exist
160+
// The class doesn't exist
73161
return null;
74-
} catch (UnsupportedClassVersionError ucve) {
75-
if (runtime.isDebug()) ucve.printStackTrace();
76-
throw runtime.newLoadError("JRuby ext built for wrong Java version in `" + finName + "': " + ucve, finName.toString());
77-
}
78162
}
79163

80164
public ClassExtensionLibrary(String name, Class extension) {

0 commit comments

Comments
 (0)