feat: simplified impl with interface + fix loading

only load patches rather than looking into all mods manually. patch
loading is implemented using ServiceLoader, thus removing most unchecked
casts and reflection accesses to methods, and directly invoking
inject(). This is much more efficient and allows to store only loader in
the Launch Plugin and only patches in mods themselves. The modder patch
interface is still super ripe, requiring each method to be implemented
returning a raw string with the unmapped name, but helpers and utils and
build plugins can be developed later on to make this API more friendly.
This commit is contained in:
əlemi 2023-02-06 01:10:18 +01:00
parent bc3eadea3b
commit 9821169333
No known key found for this signature in database
GPG key ID: BBCBFE5D7244634E
8 changed files with 164 additions and 960 deletions

View file

@ -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 void offerResource(Path resource, String name) {
LOGGER.warn(String.format("Resource offered to us: %s @ '%s'", name, resource.toString()));
}
@Override
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 {
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 (MalformedURLException e) {
LOGGER.error(String.format("Malformed URL for resource %s - 'file:%s'", row.getKey(), row.getValue().toString()));
}
}
}
// Filter only classes we need to patch
@Override
public EnumSet<Phase> handlesClass(Type classType, final boolean isEmpty) {
throw new IllegalStateException("Outdated ModLauncher"); //mixin does it
}
private static final EnumSet<Phase> YAY = EnumSet.of(Phase.BEFORE);
private static final EnumSet<Phase> NAY = EnumSet.noneOf(Phase.class);
@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());
// 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;
} else return NAY;
/* TODO: either bring back or delete this (probably the latter)
else {
try {
if(hasAnnotation(name)) {
Class<?> patch = Class.forName(name);
//patchClasses.put(patch.getAnnotation(Patch.class).value(), patch);
//LOGGER.info("Found patch class {}", patch.getName());
}
} catch(IOException | ClassNotFoundException e) {
LOGGER.debug("Could not load {}", name);
}
return NAY;
} */
}
// Process classes and inject methods
@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();
}
}
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();
}
private static Set<Method> getInjectorsInPatch(Class<?> patch) {
return Arrays.stream(patch.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(Inject.class))
.collect(Collectors.toSet());
}
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();
}
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);
}
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()
);
}
return false;
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 modified ? ComputeFlags.COMPUTE_FRAMES : ComputeFlags.NO_REWRITE;
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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";
}

View file

@ -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();
}

View file

@ -1,21 +1,40 @@
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 {
@Inject(methodName = "tick", returnType = void.class, parameters = {}, description = "Test injection!")
public void inject(MethodNode main) {
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);
}
}
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));
@ -28,3 +47,5 @@ public class TestPatch implements Opcodes {
main.instructions.insert(insnList);
}
}
}

View file

@ -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.
}
}
}

View file

@ -0,0 +1,2 @@
bscv.asm.patches.TestPatch$FramerateFix
bscv.asm.patches.TestPatch$TickPatch