diff options
Diffstat (limited to 'src/main/java')
-rw-r--r-- | src/main/java/bscv/asm/BoSCoVicinoLoader.java | 266 | ||||
-rw-r--r-- | src/main/java/bscv/asm/api/AnnotationChecker.java | 37 | ||||
-rw-r--r-- | src/main/java/bscv/asm/api/IInjector.java | 56 | ||||
-rw-r--r-- | src/main/java/bscv/asm/api/annotations/Inject.java | 18 | ||||
-rw-r--r-- | src/main/java/bscv/asm/api/annotations/Patch.java | 12 | ||||
-rw-r--r-- | src/main/java/bscv/asm/patches/TestPatch.java | 53 | ||||
-rw-r--r-- | src/main/java/bscv/asm/util/ClassPath22.java | 670 |
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. - } - } -} |