summaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/bscv/asm/BoSCoVicinoLoader.java266
-rw-r--r--src/main/java/bscv/asm/api/AnnotationChecker.java37
-rw-r--r--src/main/java/bscv/asm/api/IInjector.java56
-rw-r--r--src/main/java/bscv/asm/api/annotations/Inject.java18
-rw-r--r--src/main/java/bscv/asm/api/annotations/Patch.java12
-rw-r--r--src/main/java/bscv/asm/patches/TestPatch.java53
-rw-r--r--src/main/java/bscv/asm/util/ClassPath22.java670
7 files changed, 157 insertions, 955 deletions
diff --git a/src/main/java/bscv/asm/BoSCoVicinoLoader.java b/src/main/java/bscv/asm/BoSCoVicinoLoader.java
index 15a2bbf..4adc22b 100644
--- a/src/main/java/bscv/asm/BoSCoVicinoLoader.java
+++ b/src/main/java/bscv/asm/BoSCoVicinoLoader.java
@@ -1,43 +1,32 @@
package bscv.asm;
-import bscv.asm.api.AnnotationChecker;
-import bscv.asm.util.ClassPath22;
-import bscv.asm.api.annotations.Inject;
-import bscv.asm.api.annotations.Patch;
-import com.google.common.collect.HashMultimap;
-import cpw.mods.modlauncher.api.INameMappingService;
+import bscv.asm.api.IInjector;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
-import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
-import javax.annotation.Nullable;
-import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.Arrays;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.EnumSet;
-import java.util.Optional;
-import java.util.Set;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
import java.util.stream.Collectors;
public class BoSCoVicinoLoader implements ILaunchPluginService {
-
+ public static final Logger LOGGER = LogManager.getLogger("BoSCoVicino-ASM");
public static final String NAME = "boscovicino_asm"; //TODO: temp name
- public static final Logger LOGGER = LogManager.getLogger("BoSCoVicinoASM");
- private static final EnumSet<Phase> YAY = EnumSet.of(Phase.BEFORE);
- private static final EnumSet<Phase> NAY = EnumSet.noneOf(Phase.class);
-
- private static final HashMultimap<String, Class<?>> patchClasses = HashMultimap.create();
+ private List<IInjector> injectors = new ArrayList<>();
public BoSCoVicinoLoader() {
- loadPatchesPozzed();
- LOGGER.info("BoSCoVicinoLoader instantiated successfully!");
+ LOGGER.info("BoSCoVicino ASM Patcher instantiated");
}
@Override
@@ -45,205 +34,78 @@ public class BoSCoVicinoLoader implements ILaunchPluginService {
return NAME;
}
+
+ // Load mods requesting patches from resources
+
@Override
- public EnumSet<Phase> handlesClass(Type classType, final boolean isEmpty) {
- throw new IllegalStateException("Outdated ModLauncher"); //mixin does it
+ public void offerResource(Path resource, String name) {
+ LOGGER.warn(String.format("Resource offered to us: %s @ '%s'", name, resource.toString()));
}
@Override
- public EnumSet<Phase> handlesClass(Type classType, final boolean isEmpty, final String reason) {
- if(isEmpty) return NAY;
- String name = classType.getClassName();
- if(name.startsWith("net.minecraft.") || name.indexOf('.') == -1) {
- LOGGER.debug("Marked {} as to-be-handled", classType.getClassName());
- return YAY;
- } else return NAY;
- /* TODO: either bring back or delete this (probably the latter)
- else {
+ public void addResources(List<Map.Entry<String, Path>> resources) {
+ LOGGER.info("Resources being added:");
+ for (Map.Entry<String, Path> row : resources) {
+ LOGGER.info(String.format("> %s @ '%s'", row.getKey(), row.getValue().toString()));
try {
- if(hasAnnotation(name)) {
- Class<?> patch = Class.forName(name);
- //patchClasses.put(patch.getAnnotation(Patch.class).value(), patch);
- //LOGGER.info("Found patch class {}", patch.getName());
+ URL jarUrl = new URL("file:" + row.getValue().toString());
+ URLClassLoader loader = new URLClassLoader(new URL[] { jarUrl });
+ for (IInjector inj : ServiceLoader.load(IInjector.class, loader)) {
+ LOGGER.info(String.format("Registering injector %s", inj.name()));
+ this.injectors.add(inj);
}
- } catch(IOException | ClassNotFoundException e) {
- LOGGER.debug("Could not load {}", name);
+ } catch (MalformedURLException e) {
+ LOGGER.error(String.format("Malformed URL for resource %s - 'file:%s'", row.getKey(), row.getValue().toString()));
}
- return NAY;
- } */
+ }
}
- @Override
- public int processClassWithFlags(Phase phase, ClassNode classNode, Type classType, String reason) {
- LOGGER.debug("Processing class {} in phase {} of {}", classType.getClassName(), phase.name(), reason);
- if(patchClasses.containsKey(classType.getClassName())) {
- LOGGER.info("Found class with descriptor {} among the patches", classType.getClassName());
- //can have multiple patches working on same class
- //each of these classes may attempt to patch a method one or more times
- Set<String> classMethodNames = classNode.methods
- .stream()
- .map(m -> deobfuscator(m.name))
- .collect(Collectors.toSet());
- Set<String> classMethodDescriptors = classNode.methods
- .stream()
- .map(m -> m.desc)
- .collect(Collectors.toSet());
- HashMultimap<Object, Method> patches = HashMultimap.create();
- patchClasses
- .get(classType.getClassName())
- .forEach(p -> {
- Set<Method> injectors =
- getInjectorsInPatch(p)
- .stream()
- .filter(m -> {
- Inject a = m.getAnnotation(Inject.class);
- LOGGER.info("Found {} injector for method {}{}", m.getName(), a.methodName(), descriptorBuilder(a.returnType(), a.parameters()));
- return classMethodNames.contains(a.methodName())
- && classMethodDescriptors.contains(descriptorBuilder(a.returnType(), a.parameters()));
- })
- .collect(Collectors.toSet());
- if(!injectors.isEmpty()) {
- try {
- patches.putAll(p.newInstance(), injectors);
- StringBuilder sb = new StringBuilder();
- for(Method i : injectors)
- sb.append(i.getName()).append(",");
- LOGGER.info("Stored patch {} with injectors {}", p, sb.toString());
- } catch(InstantiationException | IllegalAccessException e) {
- LOGGER.error("Something went wrong while instantiating patch {}", p);
- throw new RuntimeException(e); //todo: better error handling
- }
- }
- });
- if(patches.isEmpty()) {
- LOGGER.info("No valid patches found for {}!", classNode.name.replace('/', '.'));
- return ComputeFlags.NO_REWRITE;
- } else {
- boolean success = false;
- for(Object p : patches.keys())
- for(Method m : patches.get(p))
- success = processInjector(classNode, p, m) || success;
- LOGGER.info("Altered class {}", classNode.name);
- return success ? ComputeFlags.COMPUTE_FRAMES : ComputeFlags.NO_REWRITE; //todo: is this the right flag?
- }
- } else return ComputeFlags.NO_REWRITE;
- }
- /* TODO: Get fucking rid of this!
- * This method is pure evil. It scans the JAR and forcefully loads the patches.
- * Sure, it works, but it makes it impossible to split Patch Framework and client. It's
- * also very slow.
- * Cherry on top: ClassPath in Guava 21 is broken. My solution was to shamelessly paste
- * the version from Guava 22 as ClassPath22. License-wise there shouldn't be any problem,
- * but I have no intention of keeping it. It's only used here, and this method needs to
- * go as soon as humanly possible. Don't worry, I'm not bloating your precious codebase.
- * P.S.: Yes, that's why your shot at dynloading Modules didn't work.
- **/
- private static void loadPatchesPozzed() {
- try {
- ClassPath22.from(BoSCoVicinoLoader.class.getClassLoader())
- .getAllClasses()
- .stream()
- .filter(ci -> {
- try {
- return hasAnnotation(ci.getName());
- } catch(IOException e) {
- throw new RuntimeException(e);
- }
- })
- .map(ClassPath22.ClassInfo::load)
- .forEach(patch -> {
- patchClasses.put(patch.getAnnotation(Patch.class).value(), patch);
- LOGGER.info("Found patch class {}", patch.getName());
- });
- } catch(IOException e) {
- e.printStackTrace();
- }
- }
+ // Filter only classes we need to patch
- private static boolean hasAnnotation(String name) throws IOException {
- ClassReader cr = new ClassReader(name);
- AnnotationChecker ac = new AnnotationChecker(Patch.class);
- cr.accept(ac, 0);
- return ac.isAnnotationPresent();
+ @Override
+ public EnumSet<Phase> handlesClass(Type classType, final boolean isEmpty) {
+ throw new IllegalStateException("Outdated ModLauncher"); //mixin does it
}
- private static Set<Method> getInjectorsInPatch(Class<?> patch) {
- return Arrays.stream(patch.getDeclaredMethods())
- .filter(m -> m.isAnnotationPresent(Inject.class))
- .collect(Collectors.toSet());
- }
+ private static final EnumSet<Phase> YAY = EnumSet.of(Phase.BEFORE);
+ private static final EnumSet<Phase> NAY = EnumSet.noneOf(Phase.class);
- public static String descriptorBuilder(@Nullable Class<?> returnType, Class<?> ... parameters) {
- StringBuilder desc = new StringBuilder("(");
- for(Class<?> p : parameters)
- desc.append(Type.getDescriptor(p));
- desc.append(")");
- if(returnType != null)
- desc.append(Type.getDescriptor(returnType));
- return desc.toString();
+ @Override
+ public EnumSet<Phase> handlesClass(Type classType, final boolean isEmpty, final String reason) {
+ if (isEmpty) return NAY;
+ // TODO can I make a set of target classes to make this faster
+ for (IInjector inj : this.injectors) {
+ if (inj.targetClass().equals(classType.getClassName()))
+ return YAY;
+ }
+ return NAY;
}
- public static String descriptorBuilder(Class<?>[] parameters) {
- return descriptorBuilder(null, parameters);
- }
- private static String deobfuscator(String srg) {
- /* FIXME: this will only work in client env (runClient task)
- Find a different way to deobf searge names or try to circumvent
- this step altogether (serve already deobfed names?) */
- return ObfuscationReflectionHelper.remapName(INameMappingService.Domain.METHOD, srg);
- }
+ // Process classes and inject methods
- private static boolean processInjector(ClassNode target, Object patch, Method injector) {
- //get relevant method from target
- Optional<MethodNode> targetMethod =
- target.methods
- .stream()
- .filter(m -> {
- Inject a = injector.getAnnotation(Inject.class);
- return m.name.equals(a.methodName()) && m.desc.startsWith(descriptorBuilder(a.parameters()));
- })
- .findAny(); //there can literally only be one so this is safe and more efficient than findFirst
-
- try {
- if(!targetMethod.isPresent())
- throw new NoSuchMethodException();
- injector.invoke(patch, targetMethod.get());
-
- LOGGER.info(
- "Completed transformation task {} for {}::{}",
- injector.getAnnotation(Inject.class).description(),
- target.name,
- targetMethod.get().name
- );
-
- return true;
-
- } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
- if (e instanceof NoSuchMethodException) {
- LOGGER.error(
- "{} while attempting to find method {}::{} in task {}. This should never happen.",
- e,
- target.name,
- injector.getAnnotation(Inject.class).methodName(),
- injector.getAnnotation(Inject.class).description()
- );
- } else {
- Throwable cause;
- if(e instanceof InvocationTargetException)
- cause = e.getCause();
- else cause = e;
- LOGGER.error(
- "{} thrown from {}::{} for task with description {}",
- cause,
- target.name,
- targetMethod.get().name,
- injector.getAnnotation(Inject.class).description()
- );
+ @Override
+ public int processClassWithFlags(Phase phase, ClassNode classNode, Type classType, String reason) {
+ LOGGER.debug("Processing class {} in phase {} of {}", classType.getClassName(), phase.name(), reason);
+ List<IInjector> relevantInjectors = this.injectors.stream()
+ .filter(i -> i.targetClass().equals(classType.getClassName()))
+ .collect(Collectors.toList());
+ boolean modified = false;
+ for (MethodNode method : classNode.methods) {
+ for (IInjector inj : relevantInjectors) {
+ if (
+ inj.methodName().equals(method.name) &&
+ inj.methodDesc().equals(method.desc)
+ ) {
+ LOGGER.info(String.format("Patching %s.%s with %s", classType.getClassName(), method.name, inj.name()));
+ inj.inject(classNode, method); // TODO catch patching exceptions
+ modified = true;
+ }
}
- return false;
}
+
+ return modified ? ComputeFlags.COMPUTE_FRAMES : ComputeFlags.NO_REWRITE;
}
}
diff --git a/src/main/java/bscv/asm/api/AnnotationChecker.java b/src/main/java/bscv/asm/api/AnnotationChecker.java
deleted file mode 100644
index a0abbda..0000000
--- a/src/main/java/bscv/asm/api/AnnotationChecker.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package bscv.asm.api;
-
-import org.objectweb.asm.AnnotationVisitor;
-import org.objectweb.asm.ClassVisitor;
-import org.objectweb.asm.Opcodes;
-import org.objectweb.asm.Type;
-
-import java.lang.annotation.Annotation;
-
-/**
- * ClassVisitor which checks whether a Class is annotated with
- * a specific Annotation.
- * @author Fraaz
- */
-public class AnnotationChecker extends ClassVisitor {
- private boolean annotationPresent;
- private final String annotationDesc;
-
- public AnnotationChecker(Class<? extends Annotation> a) {
- super(Opcodes.ASM8); //hopefully lol
- this.annotationPresent = false;
- this.annotationDesc = Type.getDescriptor(a);
- }
-
- @Override
- public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
- if (visible && desc.equals(this.annotationDesc))
- this.annotationPresent = true;
- return super.visitAnnotation(desc, visible);
- //returning null would delete our annotation, but we don't want that
- //so we jut delegate to superclass
- }
-
- public boolean isAnnotationPresent() {
- return this.annotationPresent;
- }
-}
diff --git a/src/main/java/bscv/asm/api/IInjector.java b/src/main/java/bscv/asm/api/IInjector.java
new file mode 100644
index 0000000..d02ddaa
--- /dev/null
+++ b/src/main/java/bscv/asm/api/IInjector.java
@@ -0,0 +1,56 @@
+package bscv.asm.api;
+
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.MethodNode;
+
+public interface IInjector {
+
+ /**
+ * @return name of injector, for logging
+ */
+ String name();
+
+ /**
+ * @return reason for patching for this injector, for loggin
+ */
+ default String reason() { return ""; }
+
+ /**
+ * This is used by the Launch Plugin to identify which classes should be
+ * altered, and on which classes this injector should operate.
+ *
+ * Class name should be dot-separated, for example "net.minecraft.client.Minecraft"
+ *
+ * @return target class to operate onto
+ */
+ String targetClass();
+
+ /**
+ * This is used by the Launch Plugin to identify which methods to provide
+ * to this injector for patching. It should return the Searge name of wanted function.
+ * example: "func_71407_l", which is "tick()" on "Minecraft" class in 1.16.5
+ *
+ * @return target method name to operate onto
+ */
+ String methodName();
+
+ /**
+ * This is used by the Launch Plugin to identify which methods to provide
+ * to this injector for patching. It should return the method descriptor, with
+ * parameters and return types. example: "()V" for void parameters and return.
+ *
+ * TODO better example...
+ *
+ * @return target method name to operate onto
+ */
+ String methodDesc();
+
+ /**
+ * Once the Launch Plugin has identified classes and methods for injectors,
+ * this method will be called providing the correct class and method nodes for patching.
+ *
+ * @param clazz class node which is being patched
+ * @param method main method node of requested function for patching
+ */
+ void inject(ClassNode clazz, MethodNode method);
+}
diff --git a/src/main/java/bscv/asm/api/annotations/Inject.java b/src/main/java/bscv/asm/api/annotations/Inject.java
deleted file mode 100644
index dcf5fcb..0000000
--- a/src/main/java/bscv/asm/api/annotations/Inject.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package bscv.asm.api.annotations;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-@Target({ElementType.METHOD})
-@Retention(RetentionPolicy.RUNTIME)
-public @interface Inject {
- String methodName();
-
- Class<?> returnType();
-
- Class<?>[] parameters();
-
- String description() default "No description given";
-}
diff --git a/src/main/java/bscv/asm/api/annotations/Patch.java b/src/main/java/bscv/asm/api/annotations/Patch.java
deleted file mode 100644
index 2b6963f..0000000
--- a/src/main/java/bscv/asm/api/annotations/Patch.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package bscv.asm.api.annotations;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-@Target(ElementType.TYPE)
-@Retention(RetentionPolicy.RUNTIME)
-public @interface Patch {
- String value();
-}
diff --git a/src/main/java/bscv/asm/patches/TestPatch.java b/src/main/java/bscv/asm/patches/TestPatch.java
index b1454ab..3d1a9a2 100644
--- a/src/main/java/bscv/asm/patches/TestPatch.java
+++ b/src/main/java/bscv/asm/patches/TestPatch.java
@@ -1,30 +1,51 @@
package bscv.asm.patches;
-import bscv.asm.api.annotations.Inject;
-import bscv.asm.api.annotations.Patch;
import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.MethodNode;
+import bscv.asm.api.IInjector;
+
/**
* When working as intended, this patch will crash the game
* as soon it finished loading with a NegativeArraySizeException.
*/
-@Patch("net.minecraft.client.Minecraft")
-public class TestPatch implements Opcodes {
+public class TestPatch {
+
+ public static class FramerateFix implements IInjector, Opcodes {
+ public String name() { return "FramerateFix"; }
+ public String targetClass() { return "net.minecraft.client.Minecraft"; }
+ public String methodName() { return "func_213243_aC"; } // getFramerateLimit()
+ public String methodDesc() { return "()I"; }
+
+ public void inject(ClassNode clazz, MethodNode main) {
+ InsnList insnList = new InsnList();
+ insnList.add(new InsnNode(POP));
+ main.instructions.insert(insnList);
+ }
+ }
+
- @Inject(methodName = "tick", returnType = void.class, parameters = {}, description = "Test injection!")
- public void inject(MethodNode main) {
- InsnList insnList = new InsnList();
- insnList.add(new InsnNode(POP));
- insnList.add(new InsnNode(POP));
- insnList.add(new InsnNode(POP));
- insnList.add(new InsnNode(POP));
- insnList.add(new InsnNode(POP));
- insnList.add(new InsnNode(POP));
- insnList.add(new InsnNode(POP));
- insnList.add(new InsnNode(POP));
- main.instructions.insert(insnList);
+ public static class TickPatch implements IInjector, Opcodes {
+ public String name() { return "TickPatch"; }
+ public String targetClass() { return "net.minecraft.client.Minecraft"; }
+ public String methodName() { return "func_71407_l"; } // tick()
+ public String methodDesc() { return "()V"; }
+
+ public void inject(ClassNode clazz, MethodNode main) {
+ InsnList insnList = new InsnList();
+ insnList.add(new InsnNode(POP));
+ insnList.add(new InsnNode(POP));
+ insnList.add(new InsnNode(POP));
+ insnList.add(new InsnNode(POP));
+ insnList.add(new InsnNode(POP));
+ insnList.add(new InsnNode(POP));
+ insnList.add(new InsnNode(POP));
+ insnList.add(new InsnNode(POP));
+ main.instructions.insert(insnList);
+ }
}
+
}
diff --git a/src/main/java/bscv/asm/util/ClassPath22.java b/src/main/java/bscv/asm/util/ClassPath22.java
deleted file mode 100644
index 0edc825..0000000
--- a/src/main/java/bscv/asm/util/ClassPath22.java
+++ /dev/null
@@ -1,670 +0,0 @@
-package bscv.asm.util;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.StandardSystemProperty.JAVA_CLASS_PATH;
-import static com.google.common.base.StandardSystemProperty.PATH_SEPARATOR;
-import static java.util.logging.Level.WARNING;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.CharMatcher;
-import com.google.common.base.Splitter;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Maps;
-import com.google.common.io.ByteSource;
-import com.google.common.io.CharSource;
-import com.google.common.io.Resources;
-import com.google.common.reflect.Reflection;
-
-import java.io.File;
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.nio.charset.Charset;
-import java.util.Enumeration;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.NoSuchElementException;
-import java.util.Set;
-import java.util.jar.Attributes;
-import java.util.jar.JarEntry;
-import java.util.jar.JarFile;
-import java.util.jar.Manifest;
-import java.util.logging.Logger;
-import javax.annotation.CheckForNull;
-
-/**
- * Scans the source of a {@link ClassLoader} and finds all loadable classes and resources.
- *
- * <h2>Prefer <a href="https://github.com/classgraph/classgraph/wiki">ClassGraph</a> over {@code
- * ClassPath22}</h2>
- *
- * <p>We recommend using <a href="https://github.com/classgraph/classgraph/wiki">ClassGraph</a>
- * instead of {@code ClassPath22}. ClassGraph improves upon {@code ClassPath22} in several ways,
- * including addressing many of its limitations. Limitations of {@code ClassPath22} include:
- *
- * <ul>
- * <li>It looks only for files and JARs in URLs available from {@link URLClassLoader} instances or
- * the {@linkplain ClassLoader#getSystemClassLoader() system class loader}. This means it does
- * not look for classes in the <i>module path</i>.
- * <li>It understands only {@code file:} URLs. This means that it does not understand <a
- * href="https://openjdk.java.net/jeps/220">{@code jrt:/} URLs</a>, among <a
- * href="https://github.com/classgraph/classgraph/wiki/Classpath-specification-mechanisms">others</a>.
- * <li>It does not know how to look for classes when running under an Android VM. (ClassGraph does
- * not support this directly, either, but ClassGraph documents how to <a
- * href="https://github.com/classgraph/classgraph/wiki/Build-Time-Scanning">perform build-time
- * classpath scanning and make the results available to an Android app</a>.)
- * <li>Like all of Guava, it is not tested under Windows. We have gotten <a
- * href="https://github.com/google/guava/issues/2130">a report of a specific bug under
- * Windows</a>.
- * <li>It <a href="https://github.com/google/guava/issues/2712">returns only one resource for a
- * given path</a>, even if resources with that path appear in multiple jars or directories.
- * <li>It assumes that <a href="https://github.com/google/guava/issues/3349">any class with a
- * {@code $} in its name is a nested class</a>.
- * </ul>
- *
- * <h2>{@code ClassPath22} and symlinks</h2>
- *
- * <p>In the case of directory classloaders, symlinks are supported but cycles are not traversed.
- * This guarantees discovery of each <em>unique</em> loadable resource. However, not all possible
- * aliases for resources on cyclic paths will be listed.
- *
- * @author Ben Yu
- * @since 14.0
- */
-public final class ClassPath22 {
- private static final Logger logger = Logger.getLogger(ClassPath22.class.getName());
-
- /** Separator for the Class-Path manifest attribute value in jar files. */
- private static final Splitter CLASS_PATH_ATTRIBUTE_SEPARATOR =
- Splitter.on(" ").omitEmptyStrings();
-
- private static final String CLASS_FILE_NAME_EXTENSION = ".class";
-
- private final ImmutableSet<ResourceInfo> resources;
-
- private ClassPath22(ImmutableSet<ResourceInfo> resources) {
- this.resources = resources;
- }
-
- /**
- * Returns a {@code ClassPath22} representing all classes and resources loadable from {@code
- * classloader} and its ancestor class loaders.
- *
- * <p><b>Warning:</b> {@code ClassPath22} can find classes and resources only from:
- *
- * <ul>
- * <li>{@link URLClassLoader} instances' {@code file:} URLs
- * <li>the {@linkplain ClassLoader#getSystemClassLoader() system class loader}. To search the
- * system class loader even when it is not a {@link URLClassLoader} (as in Java 9), {@code
- * ClassPath22} searches the files from the {@code java.class.path} system property.
- * </ul>
- *
- * @throws IOException if the attempt to read class path resources (jar files or directories)
- * failed.
- */
- public static ClassPath22 from(ClassLoader classloader) throws IOException {
- ImmutableSet<LocationInfo> locations = locationsFrom(classloader);
-
- // Add all locations to the scanned set so that in a classpath [jar1, jar2], where jar1 has a
- // manifest with Class-Path pointing to jar2, we won't scan jar2 twice.
- Set<File> scanned = new HashSet<>();
- for (LocationInfo location : locations) {
- scanned.add(location.file());
- }
-
- // Scan all locations
- ImmutableSet.Builder<ResourceInfo> builder = ImmutableSet.builder();
- for (LocationInfo location : locations) {
- builder.addAll(location.scanResources(scanned));
- }
- return new ClassPath22(builder.build());
- }
-
- /**
- * Returns all resources loadable from the current class path, including the class files of all
- * loadable classes but excluding the "META-INF/MANIFEST.MF" file.
- */
- public ImmutableSet<ResourceInfo> getResources() {
- return resources;
- }
-
- /**
- * Returns all classes loadable from the current class path.
- *
- * @since 16.0
- */
- public ImmutableSet<ClassInfo> getAllClasses() {
- return FluentIterable.from(resources).filter(ClassInfo.class).toSet();
- }
-
- /**
- * Returns all top level classes loadable from the current class path. Note that "top-level-ness"
- * is determined heuristically by class name (see {@link ClassInfo#isTopLevel}).
- */
- public ImmutableSet<ClassInfo> getTopLevelClasses() {
- return FluentIterable.from(resources)
- .filter(ClassInfo.class)
- .filter(ClassInfo::isTopLevel)
- .toSet();
- }
-
- /** Returns all top level classes whose package name is {@code packageName}. */
- public ImmutableSet<ClassInfo> getTopLevelClasses(String packageName) {
- checkNotNull(packageName);
- ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
- for (ClassInfo classInfo : getTopLevelClasses()) {
- if (classInfo.getPackageName().equals(packageName)) {
- builder.add(classInfo);
- }
- }
- return builder.build();
- }
-
- /**
- * Returns all top level classes whose package name is {@code packageName} or starts with {@code
- * packageName} followed by a '.'.
- */
- public ImmutableSet<ClassInfo> getTopLevelClassesRecursive(String packageName) {
- checkNotNull(packageName);
- String packagePrefix = packageName + '.';
- ImmutableSet.Builder<ClassInfo> builder = ImmutableSet.builder();
- for (ClassInfo classInfo : getTopLevelClasses()) {
- if (classInfo.getName().startsWith(packagePrefix)) {
- builder.add(classInfo);
- }
- }
- return builder.build();
- }
-
- /**
- * Represents a class path resource that can be either a class file or any other resource file
- * loadable from the class path.
- *
- * @since 14.0
- */
- public static class ResourceInfo {
- private final File file;
- private final String resourceName;
-
- final ClassLoader loader;
-
- static ResourceInfo of(File file, String resourceName, ClassLoader loader) {
- if (resourceName.endsWith(CLASS_FILE_NAME_EXTENSION)) {
- return new ClassInfo(file, resourceName, loader);
- } else {
- return new ResourceInfo(file, resourceName, loader);
- }
- }
-
- ResourceInfo(File file, String resourceName, ClassLoader loader) {
- this.file = checkNotNull(file);
- this.resourceName = checkNotNull(resourceName);
- this.loader = checkNotNull(loader);
- }
-
- /**
- * Returns the url identifying the resource.
- *
- * <p>See {@link ClassLoader#getResource}
- *
- * @throws NoSuchElementException if the resource cannot be loaded through the class loader,
- * despite physically existing in the class path.
- */
- public final URL url() {
- URL url = loader.getResource(resourceName);
- if (url == null) {
- throw new NoSuchElementException(resourceName);
- }
- return url;
- }
-
- /**
- * Returns a {@link ByteSource} view of the resource from which its bytes can be read.
- *
- * @throws NoSuchElementException if the resource cannot be loaded through the class loader,
- * despite physically existing in the class path.
- * @since 20.0
- */
- public final ByteSource asByteSource() {
- return Resources.asByteSource(url());
- }
-
- /**
- * Returns a {@link CharSource} view of the resource from which its bytes can be read as
- * characters decoded with the given {@code charset}.
- *
- * @throws NoSuchElementException if the resource cannot be loaded through the class loader,
- * despite physically existing in the class path.
- * @since 20.0
- */
- public final CharSource asCharSource(Charset charset) {
- return Resources.asCharSource(url(), charset);
- }
-
- /** Returns the fully qualified name of the resource. Such as "com/mycomp/foo/bar.txt". */
- public final String getResourceName() {
- return resourceName;
- }
-
- /** Returns the file that includes this resource. */
- final File getFile() {
- return file;
- }
-
- @Override
- public int hashCode() {
- return resourceName.hashCode();
- }
-
- @Override
- public boolean equals(@CheckForNull Object obj) {
- if (obj instanceof ResourceInfo) {
- ResourceInfo that = (ResourceInfo) obj;
- return resourceName.equals(that.resourceName) && loader == that.loader;
- }
- return false;
- }
-
- // Do not change this arbitrarily. We rely on it for sorting ResourceInfo.
- @Override
- public String toString() {
- return resourceName;
- }
- }
-
- /**
- * Represents a class that can be loaded through {@link #load}.
- *
- * @since 14.0
- */
- public static final class ClassInfo extends ResourceInfo {
- private final String className;
-
- ClassInfo(File file, String resourceName, ClassLoader loader) {
- super(file, resourceName, loader);
- this.className = getClassName(resourceName);
- }
-
- /**
- * Returns the package name of the class, without attempting to load the class.
- *
- * <p>Behaves similarly to {@code class.getPackage().}{@link Package#getName() getName()} but
- * does not require the class (or package) to be loaded.
- *
- * <p>But note that this method may behave differently for a class in the default package: For
- * such classes, this method always returns an empty string. But under some version of Java,
- * {@code class.getPackage().getName()} produces a {@code NullPointerException} because {@code
- * class.getPackage()} returns {@code null}.
- */
- public String getPackageName() {
- return Reflection.getPackageName(className);
- }
-
- /**
- * Returns the simple name of the underlying class as given in the source code.
- *
- * <p>Behaves similarly to {@link Class#getSimpleName()} but does not require the class to be
- * loaded.
- *
- * <p>But note that this class uses heuristics to identify the simple name. See a related
- * discussion in <a href="https://github.com/google/guava/issues/3349">issue 3349</a>.
- */
- public String getSimpleName() {
- int lastDollarSign = className.lastIndexOf('$');
- if (lastDollarSign != -1) {
- String innerClassName = className.substring(lastDollarSign + 1);
- // local and anonymous classes are prefixed with number (1,2,3...), anonymous classes are
- // entirely numeric whereas local classes have the user supplied name as a suffix
- return CharMatcher.inRange('0', '9').trimLeadingFrom(innerClassName);
- }
- String packageName = getPackageName();
- if (packageName.isEmpty()) {
- return className;
- }
-
- // Since this is a top level class, its simple name is always the part after package name.
- return className.substring(packageName.length() + 1);
- }
-
- /**
- * Returns the fully qualified name of the class.
- *
- * <p>Behaves identically to {@link Class#getName()} but does not require the class to be
- * loaded.
- */
- public String getName() {
- return className;
- }
-
- /**
- * Returns true if the class name "looks to be" top level (not nested), that is, it includes no
- * '$' in the name. This method may return false for a top-level class that's intentionally
- * named with the '$' character. If this is a concern, you could use {@link #load} and then
- * check on the loaded {@link Class} object instead.
- *
- * @since 30.1
- */
- public boolean isTopLevel() {
- return className.indexOf('$') == -1;
- }
-
- /**
- * Loads (but doesn't link or initialize) the class.
- *
- * @throws LinkageError when there were errors in loading classes that this class depends on.
- * For example, {@link NoClassDefFoundError}.
- */
- public Class<?> load() {
- try {
- return loader.loadClass(className);
- } catch (ClassNotFoundException e) {
- // Shouldn't happen, since the class name is read from the class path.
- throw new IllegalStateException(e);
- }
- }
-
- @Override
- public String toString() {
- return className;
- }
- }
-
- /**
- * Returns all locations that {@code classloader} and parent loaders load classes and resources
- * from. Callers can {@linkplain LocationInfo#scanResources scan} individual locations selectively
- * or even in parallel.
- */
- static ImmutableSet<LocationInfo> locationsFrom(ClassLoader classloader) {
- ImmutableSet.Builder<LocationInfo> builder = ImmutableSet.builder();
- for (Map.Entry<File, ClassLoader> entry : getClassPathEntries(classloader).entrySet()) {
- builder.add(new LocationInfo(entry.getKey(), entry.getValue()));
- }
- return builder.build();
- }
-
- /**
- * Represents a single location (a directory or a jar file) in the class path and is responsible
- * for scanning resources from this location.
- */
- static final class LocationInfo {
- final File home;
- private final ClassLoader classloader;
-
- LocationInfo(File home, ClassLoader classloader) {
- this.home = checkNotNull(home);
- this.classloader = checkNotNull(classloader);
- }
-
- /** Returns the file this location is from. */
- public final File file() {
- return home;
- }
-
- /** Scans this location and returns all scanned resources. */
- public ImmutableSet<ResourceInfo> scanResources() throws IOException {
- return scanResources(new HashSet<File>());
- }
-
- /**
- * Scans this location and returns all scanned resources.
- *
- * <p>This file and jar files from "Class-Path" entry in the scanned manifest files will be
- * added to {@code scannedFiles}.
- *
- * <p>A file will be scanned at most once even if specified multiple times by one or multiple
- * jar files' "Class-Path" manifest entries. Particularly, if a jar file from the "Class-Path"
- * manifest entry is already in {@code scannedFiles}, either because it was scanned earlier, or
- * it was intentionally added to the set by the caller, it will not be scanned again.
- *
- * <p>Note that when you call {@code location.scanResources(scannedFiles)}, the location will
- * always be scanned even if {@code scannedFiles} already contains it.
- */
- public ImmutableSet<ResourceInfo> scanResources(Set<File> scannedFiles) throws IOException {
- ImmutableSet.Builder<ResourceInfo> builder = ImmutableSet.builder();
- scannedFiles.add(home);
- scan(home, scannedFiles, builder);
- return builder.build();
- }
-
- private void scan(File file, Set<File> scannedUris, ImmutableSet.Builder<ResourceInfo> builder)
- throws IOException {
- try {
- if (!file.exists()) {
- return;
- }
- } catch (SecurityException e) {
- logger.warning("Cannot access " + file + ": " + e);
- // TODO(emcmanus): consider whether to log other failure cases too.
- return;
- }
- if (file.isDirectory()) {
- scanDirectory(file, builder);
- } else {
- scanJar(file, scannedUris, builder);
- }
- }
-
- private void scanJar(
- File file, Set<File> scannedUris, ImmutableSet.Builder<ResourceInfo> builder)
- throws IOException {
- JarFile jarFile;
- try {
- jarFile = new JarFile(file);
- } catch (IOException e) {
- // Not a jar file
- return;
- }
- try {
- for (File path : getClassPathFromManifest(file, jarFile.getManifest())) {
- // We only scan each file once independent of the classloader that file might be
- // associated with.
- if (scannedUris.add(path.getCanonicalFile())) {
- scan(path, scannedUris, builder);
- }
- }
- scanJarFile(jarFile, builder);
- } finally {
- try {
- jarFile.close();
- } catch (IOException ignored) { // similar to try-with-resources, but don't fail scanning
- }
- }
- }
-
- private void scanJarFile(JarFile file, ImmutableSet.Builder<ResourceInfo> builder) {
- Enumeration<JarEntry> entries = file.entries();
- while (entries.hasMoreElements()) {
- JarEntry entry = entries.nextElement();
- if (entry.isDirectory() || entry.getName().equals(JarFile.MANIFEST_NAME)) {
- continue;
- }
- builder.add(ResourceInfo.of(new File(file.getName()), entry.getName(), classloader));
- }
- }
-
- private void scanDirectory(File directory, ImmutableSet.Builder<ResourceInfo> builder)
- throws IOException {
- Set<File> currentPath = new HashSet<>();
- currentPath.add(directory.getCanonicalFile());
- scanDirectory(directory, "", currentPath, builder);
- }
-
- /**
- * Recursively scan the given directory, adding resources for each file encountered. Symlinks
- * which have already been traversed in the current tree path will be skipped to eliminate
- * cycles; otherwise symlinks are traversed.
- *
- * @param directory the root of the directory to scan
- * @param packagePrefix resource path prefix inside {@code classloader} for any files found
- * under {@code directory}
- * @param currentPath canonical files already visited in the current directory tree path, for
- * cycle elimination
- */
- private void scanDirectory(
- File directory,
- String packagePrefix,
- Set<File> currentPath,
- ImmutableSet.Builder<ResourceInfo> builder)
- throws IOException {
- File[] files = directory.listFiles();
- if (files == null) {
- logger.warning("Cannot read directory " + directory);
- // IO error, just skip the directory
- return;
- }
- for (File f : files) {
- String name = f.getName();
- if (f.isDirectory()) {
- File deref = f.getCanonicalFile();
- if (currentPath.add(deref)) {
- scanDirectory(deref, packagePrefix + name + "/", currentPath, builder);
- currentPath.remove(deref);
- }
- } else {
- String resourceName = packagePrefix + name;
- if (!resourceName.equals(JarFile.MANIFEST_NAME)) {
- builder.add(ResourceInfo.of(f, resourceName, classloader));
- }
- }
- }
- }
-
- @Override
- public boolean equals(@CheckForNull Object obj) {
- if (obj instanceof LocationInfo) {
- LocationInfo that = (LocationInfo) obj;
- return home.equals(that.home) && classloader.equals(that.classloader);
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return home.hashCode();
- }
-
- @Override
- public String toString() {
- return home.toString();
- }
- }
-
- /**
- * Returns the class path URIs specified by the {@code Class-Path} manifest attribute, according
- * to <a
- * href="http://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Main_Attributes">JAR
- * File Specification</a>. If {@code manifest} is null, it means the jar file has no manifest, and
- * an empty set will be returned.
- */
- @VisibleForTesting
- static ImmutableSet<File> getClassPathFromManifest(
- File jarFile, @CheckForNull Manifest manifest) {
- if (manifest == null) {
- return ImmutableSet.of();
- }
- ImmutableSet.Builder<File> builder = ImmutableSet.builder();
- String classpathAttribute =
- manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH.toString());
- if (classpathAttribute != null) {
- for (String path : CLASS_PATH_ATTRIBUTE_SEPARATOR.split(classpathAttribute)) {
- URL url;
- try {
- url = getClassPathEntry(jarFile, path);
- } catch (MalformedURLException e) {
- // Ignore bad entry
- logger.warning("Invalid Class-Path entry: " + path);
- continue;
- }
- if (url.getProtocol().equals("file")) {
- builder.add(toFile(url));
- }
- }
- }
- return builder.build();
- }
-
- @VisibleForTesting
- static ImmutableMap<File, ClassLoader> getClassPathEntries(ClassLoader classloader) {
- LinkedHashMap<File, ClassLoader> entries = Maps.newLinkedHashMap();
- // Search parent first, since it's the order ClassLoader#loadClass() uses.
- ClassLoader parent = classloader.getParent();
- if (parent != null) {
- entries.putAll(getClassPathEntries(parent));
- }
- for (URL url : getClassLoaderUrls(classloader)) {
- if (url.getProtocol().equals("file")) {
- File file = toFile(url);
- if (!entries.containsKey(file)) {
- entries.put(file, classloader);
- }
- }
- }
- return ImmutableMap.copyOf(entries);
- }
-
- private static ImmutableList<URL> getClassLoaderUrls(ClassLoader classloader) {
- if (classloader instanceof URLClassLoader) {
- return ImmutableList.copyOf(((URLClassLoader) classloader).getURLs());
- }
- if (classloader.equals(ClassLoader.getSystemClassLoader())) {
- return parseJavaClassPath();
- }
- return ImmutableList.of();
- }
-
- /**
- * Returns the URLs in the class path specified by the {@code java.class.path} {@linkplain
- * System#getProperty system property}.
- */
- @VisibleForTesting // TODO(b/65488446): Make this a public API.
- static ImmutableList<URL> parseJavaClassPath() {
- ImmutableList.Builder<URL> urls = ImmutableList.builder();
- for (String entry : Splitter.on(PATH_SEPARATOR.value()).split(JAVA_CLASS_PATH.value())) {
- try {
- try {
- urls.add(new File(entry).toURI().toURL());
- } catch (SecurityException e) { // File.toURI checks to see if the file is a directory
- urls.add(new URL("file", null, new File(entry).getAbsolutePath()));
- }
- } catch (MalformedURLException e) {
- logger.log(WARNING, "malformed classpath entry: " + entry, e);
- }
- }
- return urls.build();
- }
-
- /**
- * Returns the absolute uri of the Class-Path entry value as specified in <a
- * href="http://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Main_Attributes">JAR
- * File Specification</a>. Even though the specification only talks about relative urls, absolute
- * urls are actually supported too (for example, in Maven surefire plugin).
- */
- @VisibleForTesting
- static URL getClassPathEntry(File jarFile, String path) throws MalformedURLException {
- return new URL(jarFile.toURI().toURL(), path);
- }
-
- @VisibleForTesting
- static String getClassName(String filename) {
- int classNameEnd = filename.length() - CLASS_FILE_NAME_EXTENSION.length();
- return filename.substring(0, classNameEnd).replace('/', '.');
- }
-
- // TODO(benyu): Try java.nio.file.Paths#get() when Guava drops JDK 6 support.
- @VisibleForTesting
- static File toFile(URL url) {
- checkArgument(url.getProtocol().equals("file"));
- try {
- return new File(url.toURI()); // Accepts escaped characters like %20.
- } catch (URISyntaxException e) { // URL.toURI() doesn't escape chars.
- return new File(url.getPath()); // Accepts non-escaped chars like space.
- }
- }
-}