All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.imagej.patcher.CodeHacker Maven / Gradle / Ivy

Go to download

A runtime patcher to introduce extension points into the original ImageJ (1.x). This project offers extension points for use with ImageJ2 and it also offers limited support for headless operations.

There is a newer version: 1.2.7
Show newest version
/*
 * #%L
 * ImageJ software for multidimensional image processing and analysis.
 * %%
 * Copyright (C) 2009 - 2016 Board of Regents of the University of
 * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck
 * Institute of Molecular Cell Biology and Genetics.
 * %%
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

package net.imagej.patcher;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarOutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javassist.CannotCompileException;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewConstructor;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.BadBytecode;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.CodeIterator;
import javassist.bytecode.ConstPool;
import javassist.bytecode.InstructionPrinter;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.Opcode;
import javassist.expr.Cast;
import javassist.expr.ConstructorCall;
import javassist.expr.ExprEditor;
import javassist.expr.FieldAccess;
import javassist.expr.Handler;
import javassist.expr.MethodCall;
import javassist.expr.NewExpr;

/**
 * The code hacker provides a mechanism for altering the behavior of classes
 * before they are loaded, for the purpose of injecting new methods and/or
 * altering existing ones.
 * 

* In ImageJ, this mechanism is used to provide new seams into legacy ImageJ1 * code, so that (e.g.) the modern UI is aware of legacy ImageJ events as they * occur. *

* * @author Curtis Rueden * @author Rick Lentz * @author Johannes Schindelin */ class CodeHacker { private final ClassPool pool; protected final ClassLoader classLoader; private final Map handledClasses = new LinkedHashMap(); private final boolean onlyLogExceptions; public CodeHacker(final ClassLoader classLoader, final ClassPool classPool) { this.classLoader = classLoader; pool = classPool != null ? classPool : ClassPool.getDefault(); pool.appendClassPath(new ClassClassPath(getClass())); pool.appendClassPath(new LoaderClassPath(classLoader)); onlyLogExceptions = !Utils.stackTraceContains("junit."); } public CodeHacker(final ClassLoader classLoader) { this(classLoader, null); } private void maybeThrow(final IllegalArgumentException e) { if (onlyLogExceptions) e.printStackTrace(); else throw e; } /** * Modifies a class by injecting the provided code string at the end of the * specified method's body. * * @param fullClass Fully qualified name of the class to modify. * @param methodSig Method signature of the method to modify; e.g., * "public void updateAndDraw()" * @param newCode The string of code to add; e.g., System.out.println(\"Hello * World!\"); */ public void insertAtBottomOfMethod(final String fullClass, final String methodSig, final String newCode) { try { getBehavior(fullClass, methodSig).insertAfter(newCode); } catch (final Throwable e) { maybeThrow(new IllegalArgumentException("Cannot modify method: " + methodSig, e)); } } /** * Modifies a class by injecting the provided code string at the start of the * specified method's body. * * @param fullClass Fully qualified name of the class to override. * @param methodSig Method signature of the method to override; e.g., * "public void updateAndDraw()" * @param newCode The string of code to add; e.g., System.out.println(\"Hello * World!\"); */ public void insertAtTopOfMethod(final String fullClass, final String methodSig, final String newCode) { try { final CtBehavior behavior = getBehavior(fullClass, methodSig); if (behavior instanceof CtConstructor) { ((CtConstructor) behavior).insertBeforeBody(newCode); } else { behavior.insertBefore(newCode); } } catch (final Throwable e) { maybeThrow(new IllegalArgumentException("Cannot modify method: " + methodSig, e)); } } /** * Modifies a class by injecting the provided code string as a new method. * * @param fullClass Fully qualified name of the class to override. * @param methodSig Method signature of the method to override; e.g., * "public void updateAndDraw()" * @param newCode The string of code to add; e.g., System.out.println(\"Hello * World!\"); */ public void insertNewMethod(final String fullClass, final String methodSig, final String newCode) { final CtClass classRef = getClass(fullClass); final String methodBody = methodSig + " { " + newCode + " } "; try { final CtMethod methodRef = CtNewMethod.make(methodBody, classRef); classRef.addMethod(methodRef); } catch (final Throwable e) { maybeThrow(new IllegalArgumentException( "Cannot add method: " + methodSig, e)); } } /** * Works around a bug where the horizontal scroll wheel of the mighty mouse is * mistaken for a popup trigger. */ public void handleMightyMousePressed(final String fullClass) { final ExprEditor editor = new ExprEditor() { @Override public void edit(final MethodCall call) throws CannotCompileException { if (call.getMethodName().equals("isPopupTrigger")) { call.replace("$_ = $0.isPopupTrigger() && $0.getButton() != 0;"); } } }; final CtClass classRef = getClass(fullClass); for (final String methodName : new String[] { "mousePressed", "mouseDragged" }) try { final CtMethod method = classRef.getMethod(methodName, "(Ljava/awt/event/MouseEvent;)V"); method.instrument(editor); } catch (final NotFoundException e) { /* ignore */ } catch (final Throwable e) { maybeThrow(new IllegalArgumentException("Cannot instrument method: " + methodName, e)); } } public void insertPrivateStaticField(final String fullClass, final Class clazz, final String name) { insertStaticField(fullClass, Modifier.PRIVATE, clazz, name, null); } public void insertPublicStaticField(final String fullClass, final Class clazz, final String name, final String initializer) { insertStaticField(fullClass, Modifier.PUBLIC, clazz, name, initializer); } public void insertStaticField(final String fullClass, final int modifiers, final Class clazz, final String name, final String initializer) { final CtClass classRef = getClass(fullClass); try { final CtField field = new CtField(pool.get(clazz.getName()), name, classRef); field.setModifiers(modifiers | Modifier.STATIC); classRef.addField(field); if (initializer != null) { addToClassInitializer(fullClass, name + " = " + initializer + ";"); } } catch (final Throwable e) { maybeThrow(new IllegalArgumentException("Cannot add field " + name + " to " + fullClass, e)); } } public void addToClassInitializer(final String fullClass, final String code) { final CtClass classRef = getClass(fullClass); try { CtConstructor method = classRef.getClassInitializer(); if (method != null) { method.insertAfter(code); } else { method = CtNewConstructor.make(new CtClass[0], new CtClass[0], code, classRef); method.getMethodInfo().setName(""); method.setModifiers(Modifier.STATIC); classRef.addConstructor(method); } } catch (final Throwable e) { maybeThrow(new IllegalArgumentException("Cannot add " + code + " to class initializer of " + fullClass, e)); } } public void addCatch(final String fullClass, final String methodSig, final String exceptionClassName, final String src) { try { final CtBehavior method = getBehavior(fullClass, methodSig); method.addCatch(src, getClass(exceptionClassName), "$e"); } catch (final Throwable e) { maybeThrow(new IllegalArgumentException( "Cannot add catch for exception of type'" + exceptionClassName + " in " + fullClass + "'s " + methodSig, e)); } } public void insertAtTopOfExceptionHandlers(final String fullClass, final String methodSig, final String exceptionClassName, final String src) { try { final CtBehavior method = getBehavior(fullClass, methodSig); new EagerExprEditor() { @Override public void edit(final Handler handler) throws CannotCompileException { try { if (handler.getType().getName().equals(exceptionClassName)) { handler.insertBefore(src); markEdited(); } } catch (final Exception e) { e.printStackTrace(); } } }.instrument(method); } catch (final Throwable e) { maybeThrow(new IllegalArgumentException( "Cannot edit exception handler for type'" + exceptionClassName + " in " + fullClass + "'s " + methodSig, e)); } } /** * Replaces the application name in the given method in the given parameter to * the given constructor call. *

* Fails silently if the specified method does not exist (e.g. CommandFinder's * export() function just went away in 1.47i). *

* * @param fullClass Fully qualified name of the class to override. * @param methodSig Method signature of the method to override; e.g., * "public void showMessage(String title, String message)" * @param newClassName the name of the class which is to be constructed by the * new operator * @param parameterIndex the index of the parameter containing the application * name * @param replacement the code to use instead of the specified parameter */ protected void replaceAppNameInNew(final String fullClass, final String methodSig, final String newClassName, final int parameterIndex, final String replacement) { try { final CtBehavior method = getBehavior(fullClass, methodSig); new EagerExprEditor() { @Override public void edit(final NewExpr expr) throws CannotCompileException { if (expr.getClassName().equals(newClassName)) try { final CtClass[] parameterTypes = expr.getConstructor().getParameterTypes(); if (parameterTypes[parameterIndex - 1] != CodeHacker.this .getClass("java.lang.String")) { maybeThrow(new IllegalArgumentException("Parameter " + parameterIndex + " of " + expr.getConstructor() + " is not a String!")); return; } final String replace = replaceAppName(parameterIndex, parameterTypes.length, replacement); expr.replace("$_ = new " + newClassName + replace + ";"); markEdited(); } catch (final NotFoundException e) { maybeThrow(new IllegalArgumentException( "Cannot find the parameters of the constructor of " + newClassName, e)); } } }.instrument(method); } catch (final Throwable e) { maybeThrow(new IllegalArgumentException("Cannot handle app name in " + fullClass + "'s " + methodSig, e)); } } /** * Replaces the application name in the given method in the given parameter to * the given method call. *

* Fails silently if the specified method does not exist (e.g. CommandFinder's * export() function just went away in 1.47i). *

* * @param fullClass Fully qualified name of the class to override. * @param methodSig Method signature of the method to override; e.g., * "public void showMessage(String title, String message)" * @param calledMethodName the name of the method to which the application * name is passed * @param parameterIndex the index of the parameter containing the application * name * @param replacement the code to use instead of the specified parameter */ protected void replaceAppNameInCall(final String fullClass, final String methodSig, final String calledMethodName, final int parameterIndex, final String replacement) { try { final CtBehavior method = getBehavior(fullClass, methodSig); new EagerExprEditor() { @Override public void edit(final MethodCall call) throws CannotCompileException { if (call.getMethodName().equals(calledMethodName)) try { final boolean isSuper = call.isSuper(); final CtClass[] parameterTypes = isSuper ? ((ConstructorCall) call).getConstructor() .getParameterTypes() : call.getMethod().getParameterTypes(); if (parameterTypes.length < parameterIndex) { maybeThrow(new IllegalArgumentException("Index " + parameterIndex + " is outside of " + call.getMethod() + "'s parameter list!")); return; } if (parameterTypes[parameterIndex - 1] != CodeHacker.this .getClass("java.lang.String")) { maybeThrow(new IllegalArgumentException("Parameter " + parameterIndex + " of " + call.getMethod() + " is not a String!")); return; } final String replace = replaceAppName(parameterIndex, parameterTypes.length, replacement); call.replace((isSuper ? "" : "$0.") + calledMethodName + replace + ";"); markEdited(); } catch (final NotFoundException e) { maybeThrow(new IllegalArgumentException( "Cannot find the parameters of the method " + calledMethodName, e)); } } @Override public void edit(final ConstructorCall call) throws CannotCompileException { edit((MethodCall) call); } }.instrument(method); } catch (final Throwable e) { maybeThrow(new IllegalArgumentException("Cannot handle app name in " + fullClass + "'s " + methodSig, e)); } } private String replaceAppName(final int parameterIndex, final int parameterCount, final String replacement) { final StringBuilder builder = new StringBuilder(); builder.append("("); for (int i = 1; i <= parameterCount; i++) { if (i > 1) { builder.append(", "); } builder.append("$").append(i); if (i == parameterIndex) { builder.append(".replace(\"ImageJ\", " + replacement + ")"); } } builder.append(")"); return builder.toString(); } /** * Patches the bytecode of the given method to not return on a null check. *

* This is needed to patch support for alternative editors into ImageJ 1.x. *

* * @param fullClass the class of the method to instrument * @param methodSig the signature of the method to instrument */ public void dontReturnOnNull(final String fullClass, final String methodSig) { final CtBehavior behavior = getBehavior(fullClass, methodSig); final MethodInfo info = behavior.getMethodInfo(); final CodeIterator iterator = info.getCodeAttribute().iterator(); while (iterator.hasNext()) try { int pos = iterator.next(); final int c = iterator.byteAt(pos); if (c == Opcode.IFNONNULL && iterator.byteAt(pos + 3) == Opcode.RETURN) { iterator.writeByte(Opcode.POP, pos++); iterator.writeByte(Opcode.NOP, pos++); iterator.writeByte(Opcode.NOP, pos++); iterator.writeByte(Opcode.NOP, pos++); return; } } catch (final Throwable e) { maybeThrow(new IllegalArgumentException(e)); return; } maybeThrow(new IllegalArgumentException("Method " + methodSig + " in " + fullClass + " does not return on null")); } /** * Replaces the given methods with stub methods. * * @param fullClass the class to patch * @param methodNames the names of the methods to replace */ public void replaceWithStubMethods(final String fullClass, final String... methodNames) { final CtClass clazz = getClass(fullClass); final Set override = new HashSet(Arrays.asList(methodNames)); for (final CtMethod method : clazz.getMethods()) if (override.contains(method.getName())) try { final CtMethod stub = makeStubMethod(clazz, method); method.setBody(stub, null); } catch (final Throwable e) { maybeThrow(new IllegalArgumentException("Cannot instrument method: " + method.getName(), e)); } } /** * Replaces the superclass. * * @param fullClass * @param fullNewSuperclass */ public void replaceSuperclassAndStubifyAWTMethods(final String fullClass, final String fullNewSuperclass) { final CtClass clazz = getClass(fullClass); try { final CtClass originalSuperclass = clazz.getSuperclass(); clazz.setSuperclass(getClass(fullNewSuperclass)); for (final CtConstructor ctor : clazz.getConstructors()) ctor.instrument(new ExprEditor() { @Override public void edit(final ConstructorCall call) throws CannotCompileException { if (call.getMethodName().equals("super")) call.replace("super();"); } }); letSuperclassMethodsOverride(clazz); // stub'ify remaining methods for (CtMethod method : clazz.getMethods()) { if (method.getDeclaringClass() == clazz && !method.getName().startsWith("narf")) { if (!isAWTMethod(method)) { continue; } final CtMethod stub = makeStubMethod(clazz, method); method.setBody(stub, null); } } addMissingMethods(clazz, originalSuperclass); } catch (final Throwable e) { maybeThrow(new IllegalArgumentException( "Could not replace superclass of " + fullClass + " with " + fullNewSuperclass, e)); } } private boolean isAWTMethod(final CtMethod method) throws NotFoundException { if (isAWTClass(method.getReturnType())) return true; for (final CtClass type : method.getParameterTypes()) { if (isAWTClass(type)) return true; } return false; } private boolean isAWTClass(final CtClass clazz) { if (clazz == null) return false; final String name = clazz.getName(); return name != null && name.startsWith("java.awt."); } /** * Replaces all instantiations of a subset of AWT classes with nulls. *

* This is used by the partial headless support of legacy code. *

*/ public void skipAWTInstantiations(final String fullClass) { try { skipAWTInstantiations(getClass(fullClass)); } catch (final Throwable e) { maybeThrow(new IllegalArgumentException( "Could not skip AWT class instantiations in " + fullClass, e)); } } /** * Replaces every field write access to the specified field in the specified * method. * * @param fullClass the full class * @param methodSig the signature of the method to instrument * @param fieldName the field whose write access to override * @param newCode the code to run instead of the field access */ public void overrideFieldWrite(final String fullClass, final String methodSig, final String fieldName, final String newCode) { try { final CtBehavior method = getBehavior(fullClass, methodSig); new EagerExprEditor() { @Override public void edit(final FieldAccess access) throws CannotCompileException { if (access.getFieldName().equals(fieldName)) { access.replace(newCode); markEdited(); } } }.instrument(method); } catch (final Throwable e) { maybeThrow(new IllegalArgumentException( "Cannot override field access to " + fieldName + " in " + fullClass + "'s " + methodSig, e)); } } /** * Replaces a call in the given method. * * @param fullClass the class of the method to edit * @param methodSig the signature of the method to edit * @param calledClass the class of the called method to replace * @param calledMethodName the name of the called method to replace * @param newCode the code to replace the call with */ public void replaceCallInMethod(final String fullClass, final String methodSig, final String calledClass, final String calledMethodName, final String newCode) { replaceCallInMethod(fullClass, methodSig, calledClass, calledMethodName, newCode, -1); } /** * Replaces a call in the given method. * * @param fullClass the class of the method to edit * @param methodSig the signature of the method to edit * @param calledClass the class of the called method to replace * @param calledMethodName the name of the called method to replace * @param newCode the code to replace the call with * @param onlyNth if positive, only replace the nth call */ public void replaceCallInMethod(final String fullClass, final String methodSig, final String calledClass, final String calledMethodName, final String newCode, final int onlyNth) { try { final CtBehavior method = getBehavior(fullClass, methodSig); new EagerExprEditor() { private int counter = 0; private final boolean debug = false; @Override public void edit(final MethodCall call) throws CannotCompileException { if (debug) { System.err.println("editing call " + call.getClassName() + "#" + call.getMethodName() + " (wanted " + calledClass + "#" + calledMethodName + ")"); } if (call.getMethodName().equals(calledMethodName) && call.getClassName().equals(calledClass)) { if (onlyNth > 0 && ++counter != onlyNth) return; call.replace(newCode); markEdited(); } } @Override public void edit(final ConstructorCall call) throws CannotCompileException { if (debug) { System.err.println("editing ctor " + call.getClassName() + "#" + call.getMethodName() + " (wanted " + calledClass + "#" + calledMethodName + ")"); } if (call.getMethodName().equals(calledMethodName) && call.getClassName().equals(calledClass)) { if (onlyNth > 0 && ++counter != onlyNth) return; call.replace(newCode); markEdited(); } } @Override public void edit(final NewExpr expr) throws CannotCompileException { if (debug) { System.err.println("editing call " + expr.getClassName() + "#" + "" + " (wanted " + calledClass + "#" + calledMethodName + ")"); } if ("".equals(calledMethodName) && expr.getClassName().equals(calledClass)) { if (onlyNth > 0 && ++counter != onlyNth) return; expr.replace(newCode); markEdited(); } } }.instrument(method); } catch (final Throwable e) { maybeThrow(new IllegalArgumentException("Cannot handle replace call to " + calledMethodName + " in " + fullClass + "'s " + methodSig, e)); } } /** * Guards a careless cast behind an {@code instanceof} test. *

* Defensive programming entails verifying that an object can be cast to a * certain class before doing so. This method applies that technique, * assigning {@code null} as cast result when the object is not actually of * the correct type. *

* * @param fullClass the class of the method to edit * @param methodSig the signature of the method to edit * @param targetClass the target class of the cast */ public void guardCast(final String fullClass, final String methodSig, final String targetClass) { final CtBehavior method = getBehavior(fullClass, methodSig); try { method.instrument(new ExprEditor() { @Override public void edit(final Cast cast) { try { if (cast.getType().getName().equals(targetClass)) { cast.replace("if ($1 != null && $1 instanceof " + targetClass + ") {" + // " $_ = (" + targetClass + ") $1;" + // "} else {" + // " $_ = null;" + // "}"); } } catch (Exception e) { maybeThrow(new IllegalArgumentException( "Cannot handle cast to " + targetClass, e)); } } }); } catch (final Throwable e) { maybeThrow(new IllegalArgumentException("Cannot handle cast to " + targetClass + " in " + fullClass + "'s " + methodSig, e)); } } /** * Loads the given, possibly modified, class. *

* This method must be called to confirm any changes made with * {@link #insertAtBottomOfMethod}, {@link #insertAtTopOfMethod}, or * {@link #insertNewMethod}. *

* * @param fullClass Fully qualified class name to load. * @return the loaded class */ public Class loadClass(final String fullClass) { final CtClass classRef = getClass(fullClass); return loadClass(classRef); } /** * Loads the given, possibly modified, class. *

* This method must be called to confirm any changes made with * {@link #insertAtBottomOfMethod}, {@link #insertAtTopOfMethod}, or * {@link #insertNewMethod}. *

* * @param classRef class to load. * @return the loaded class */ public Class loadClass(final CtClass classRef) { try { return classRef.toClass(classLoader, null); } catch (final CannotCompileException e) { // Cannot use LogService; it will not be initialized by the time the // LegacyService class is loaded, which is when the CodeHacker is run if (e.getCause() != null && e.getCause() instanceof LinkageError) { throw javaAgentHint("Cannot load class: " + classRef.getName() + " (loader: " + classLoader + ")", e.getCause()); } System.err.println("Warning: Cannot load class: " + classRef.getName() + " into " + classLoader); e.printStackTrace(); return null; } finally { classRef.freeze(); } } static RuntimeException javaAgentHint(final String message, final Throwable cause) { final URL url = Utils.getLocation(JavaAgent.class); final String path = url != null && "file".equals(url.getProtocol()) && url.getPath().endsWith(".jar") ? url.getPath() : "/path/to/ij1-patcher.jar"; return new RuntimeException(message + "\n" + "It appears that this class was already defined in the class loader!\n" + "Please make sure that you initialize the LegacyService before using\n" + "any ImageJ 1.x class. You can do that by adding this static initializer:\n\n" + "\tstatic {\n" + "\t\tLegacyInjector.preinit();\n" + "\t}\n\n" + "To debug this issue, start the JVM with the option:\n\n" + "\t-javaagent:" + path + "\n\n" + "To enforce pre-initialization, start the JVM with the option:\n\n" + "\t-javaagent:" + path + "=init\n", cause); } public void loadClasses() { try { JavaAgent.stop(); } catch (final Throwable t) { // ignore } final Iterator iter = handledClasses.values().iterator(); while (iter.hasNext()) { final CtClass classRef = iter.next(); if (!classRef.isFrozen() && classRef.isModified()) { loadClass(classRef); } iter.remove(); } } /** Gets the list of patched classes. */ Collection getPatchedClasses() { final Set result = new HashSet(); for (final CtClass clazz : handledClasses.values()) { if (!clazz.isFrozen() && clazz.isModified()) { result.add(clazz); } } return result; } /** Gets the Javassist class object corresponding to the given class name. */ CtClass getClass(final String fullClass) { try { final CtClass classRef = pool.get(fullClass); if (classRef.getClassPool() == pool) handledClasses.put(classRef.getName(), classRef); return classRef; } catch (final NotFoundException e) { throw new IllegalArgumentException("No such class: " + fullClass, e); } } /** * Gets the method or constructor of the specified class and signature. * * @param fullClass the class containing the method or constructor * @param methodSig the method (or if the name is <init>, * the constructor) * @return the method or constructor */ private CtBehavior getBehavior(final String fullClass, final String methodSig) { if (methodSig.indexOf("") < 0) { return getMethod(fullClass, methodSig); } return getConstructor(fullClass, methodSig); } /** * Gets the Javassist method object corresponding to the given method * signature of the specified class name. */ private CtMethod getMethod(final String fullClass, final String methodSig) { final CtClass cc = getClass(fullClass); final String name = getMethodName(methodSig); final String[] argTypes = getMethodArgTypes(methodSig); final CtClass[] params = new CtClass[argTypes.length]; for (int i = 0; i < params.length; i++) { params[i] = getClass(argTypes[i]); } try { return cc.getDeclaredMethod(name, params); } catch (final NotFoundException e) { throw new IllegalArgumentException("No such method: " + methodSig, e); } } /** * Gets the Javassist constructor object corresponding to the given * constructor signature of the specified class name. */ private CtConstructor getConstructor(final String fullClass, final String constructorSig) { final CtClass cc = getClass(fullClass); final String[] argTypes = getMethodArgTypes(constructorSig); final CtClass[] params = new CtClass[argTypes.length]; for (int i = 0; i < params.length; i++) { params[i] = getClass(argTypes[i]); } try { return cc.getDeclaredConstructor(params); } catch (final NotFoundException e) { throw new IllegalArgumentException("No such method: " + constructorSig, e); } } /** Extracts the method name from the given method signature. */ private String getMethodName(final String methodSig) { final int parenIndex = methodSig.indexOf("("); final int spaceIndex = methodSig.lastIndexOf(" ", parenIndex); return methodSig.substring(spaceIndex + 1, parenIndex); } private String[] getMethodArgTypes(final String methodSig) { final int parenIndex = methodSig.indexOf("("); final String methodArgs = methodSig.substring(parenIndex + 1, methodSig.length() - 1); final String[] args = methodArgs.equals("") ? new String[0] : methodArgs.split(","); for (int i = 0; i < args.length; i++) { args[i] = args[i].trim().split(" ")[0]; } return args; } /** * Determines whether the specified class has the specified field. * * @param fullName the class name * @param fieldName the field name * @return whether the field exists */ public boolean hasField(final String fullName, final String fieldName) { final CtClass clazz = getClass(fullName); try { return clazz.getField(fieldName) != null; } catch (final Throwable e) { return false; } } /** * Determines whether the specified class has the specified method. * * @param fullClass the class name * @param methodSig the signature of the method * @return whether the class has the specified method */ public boolean hasMethod(final String fullClass, final String methodSig) { try { return getBehavior(fullClass, methodSig) != null; } catch (final Throwable e) { return false; } } /** * Determines whether the specified class is known to Javassist. * * @param fullClass the class name * @return whether the class exists */ public boolean existsClass(final String fullClass) { try { return pool.get(fullClass) != null; } catch (final Throwable e) { return false; } } public boolean hasSuperclass(final String fullClass, final String fullSuperclass) { try { final CtClass clazz = getClass(fullClass); return fullSuperclass.equals(clazz.getSuperclass().getName()); } catch (final Throwable e) { return false; } } private static int verboseLevel = 0; private static CtMethod makeStubMethod(final CtClass clazz, final CtMethod original) throws CannotCompileException, NotFoundException { // add a stub String prefix = ""; if (verboseLevel > 0) { prefix = "System.err.println(\"Called " + original.getLongName() + "\\n\""; if (verboseLevel > 1) { prefix += "+ \"\\t(\" + fiji.Headless.toString($args) + \")\\n\""; } prefix += ");"; } final CtClass type = original.getReturnType(); final String body = "{" + prefix + (type == CtClass.voidType ? "" : "return " + defaultReturnValue(type) + ";") + "}"; final CtClass[] types = original.getParameterTypes(); return CtNewMethod.make(type, original.getName(), types, new CtClass[0], body, clazz); } private static String defaultReturnValue(final CtClass type) { if (type == CtClass.booleanType) return "false"; if (type == CtClass.byteType) return "(byte)0"; if (type == CtClass.charType) return "'\0'"; if (type == CtClass.doubleType) return "0.0"; if (type == CtClass.floatType) return "0.0f"; if (type == CtClass.intType) return "0"; if (type == CtClass.longType) return "0l"; if (type == CtClass.shortType) return "(short)0"; return "null"; } private void addMissingMethods(final CtClass fakeClass, final CtClass originalClass) throws CannotCompileException, NotFoundException { if (verboseLevel > 0) { System.err.println("adding missing methods from " + originalClass.getName() + " to " + fakeClass.getName()); } final Set available = new HashSet(); for (final CtMethod method : fakeClass.getMethods()) available.add(stripPackage(method.getLongName())); for (final CtMethod original : originalClass.getDeclaredMethods()) { if (available.contains(stripPackage(original.getLongName()))) { if (verboseLevel > 1) { System.err.println("Skipping available method " + original); } continue; } final CtMethod method = makeStubMethod(fakeClass, original); fakeClass.addMethod(method); if (verboseLevel > 1) { System.err.println("adding missing method " + method); } } // interfaces final Set availableInterfaces = new HashSet(); for (final CtClass iface : fakeClass.getInterfaces()) availableInterfaces.add(iface); for (final CtClass iface : originalClass.getInterfaces()) if (!availableInterfaces.contains(iface)) fakeClass.addInterface(iface); final CtClass superClass = originalClass.getSuperclass(); if (superClass != null && !superClass.getName().equals("java.lang.Object")) { addMissingMethods(fakeClass, superClass); } } private void letSuperclassMethodsOverride(final CtClass clazz) throws CannotCompileException, NotFoundException { for (final CtMethod method : clazz.getSuperclass().getDeclaredMethods()) { final CtMethod method2 = clazz.getMethod(method.getName(), method.getSignature()); if (method2.getDeclaringClass().equals(clazz)) { // make sure no calls/accesses to GUI components are remaining method2.setBody(method, null); method2.setName("narf" + method.getName()); } } } private static String stripPackage(final String className) { int lastDot = -1; for (int i = 0; i < className.length(); i++) { final char c = className.charAt(i); if (c == '.' || c == '$') lastDot = i; else if (c >= 'A' && c <= 'Z') continue; else if (c >= 'a' && c <= 'z') continue; else if (i > lastDot + 1 && c >= '0' && c <= '9') continue; else return className.substring(lastDot + 1); } return className.substring(lastDot + 1); } private void skipAWTInstantiations(final CtClass clazz) throws CannotCompileException { clazz.instrument(new ExprEditor() { @Override public void edit(final NewExpr expr) throws CannotCompileException { final String name = expr.getClassName(); if (name.startsWith("java.awt.Menu") || name.equals("java.awt.PopupMenu") || name.startsWith("java.awt.Checkbox") || name.equals("java.awt.Frame")) { expr.replace("$_ = null;"); } else if (expr.getClassName().equals("ij.gui.StackWindow")) { expr.replace("$1.show(); $_ = null;"); } } @Override public void edit(final MethodCall call) throws CannotCompileException { final String className = call.getClassName(); final String methodName = call.getMethodName(); if (className.startsWith("java.awt.Menu") || className.equals("java.awt.PopupMenu") || className.startsWith("java.awt.Checkbox")) try { final CtClass type = call.getMethod().getReturnType(); if (type == CtClass.voidType) { call.replace(""); } else { call.replace("$_ = " + defaultReturnValue(type) + ";"); } } catch (final NotFoundException e) { e.printStackTrace(); } else if (methodName.equals("put") && className.equals("java.util.Properties")) { call.replace("if ($1 != null && $2 != null) $_ = $0.put($1, $2);" + "else $_ = null;"); } else if (methodName.equals("get") && className.equals("java.util.Properties")) { call.replace("$_ = $1 != null ? $0.get($1) : null;"); } else if (className.equals("java.lang.Integer") && methodName.equals("intValue")) { call.replace("$_ = $0 == null ? 0 : $0.intValue();"); } else if (methodName.equals("addTextListener")) { call.replace(""); } else if (methodName.equals("elementAt")) { call.replace("$_ = $0 == null ? null : $0.elementAt($$);"); } } }); } /** * Augments all startsWith("http://") calls with * || startsWith("https://"). * * @param fullClass the class containing the method to instrument * @param methodSig the signature of the method to instrument */ public void handleHTTPS(final String fullClass, final String methodSig) { try { final CtBehavior method = getBehavior(fullClass, methodSig); new EagerExprEditor() { @Override public void edit(final MethodCall call) throws CannotCompileException { try { if (call.getMethodName().equals("startsWith") && "http://".equals(getLastConstantArgument(call, 0))) { call .replace("$_ = $0.startsWith($1) || $0.startsWith(\"https://\");"); markEdited(); } } catch (final BadBytecode e) { e.printStackTrace(); } } }.instrument(method); } catch (final Throwable e) { maybeThrow(new IllegalArgumentException("Could not handle HTTPS in " + methodSig + " in " + fullClass)); } } private String getLastConstantArgument(final MethodCall call, final int skip) throws BadBytecode { final int[] indices = new int[skip + 1]; int counter = 0; final MethodInfo info = ((CtMethod) call.where()).getMethodInfo(); final CodeIterator iterator = info.getCodeAttribute().iterator(); final int currentPos = call.indexOfBytecode(); while (iterator.hasNext()) { final int pos = iterator.next(); if (pos >= currentPos) break; switch (iterator.byteAt(pos)) { case Opcode.LDC: indices[(counter++) % indices.length] = iterator.byteAt(pos + 1); break; case Opcode.LDC_W: indices[(counter++) % indices.length] = iterator.u16bitAt(pos + 1); break; } } if (counter < skip) { return null; } counter %= indices.length; if (skip > 0) { counter -= skip; if (counter < 0) counter += indices.length; } return info.getConstPool().getStringInfo(indices[counter]); } /** * An {@link ExprEditor} that complains when it did not edit anything. * * @author Johannes Schindelin */ private abstract static class EagerExprEditor extends ExprEditor { private int count = 0; protected void markEdited() { count++; } protected boolean wasSuccessful() { return count > 0; } public void instrument(final CtBehavior behavior) throws CannotCompileException { count = 0; behavior.instrument(this); if (!wasSuccessful()) { throw new CannotCompileException("No code replaced!"); } } } /** * Disassembles all methods of a class. * * @param fullName the class name * @param out the output stream */ public void disassemble(final String fullName, final PrintStream out) { disassemble(fullName, out, false); } /** * Disassembles all methods of a class, optionally including superclass * methods. * * @param fullName the class name * @param out the output stream * @param evenSuperclassMethods whether to disassemble methods defined in * superclasses */ public void disassemble(final String fullName, final PrintStream out, final boolean evenSuperclassMethods) { final CtClass clazz = getClass(fullName); out.println("Class " + clazz.getName()); for (final CtConstructor ctor : clazz.getConstructors()) { disassemble(ctor, out); } for (final CtMethod method : clazz.getDeclaredMethods()) if (evenSuperclassMethods || method.getDeclaringClass().equals(clazz)) disassemble( method, out); } private void disassemble(final CtBehavior method, final PrintStream out) { out.println(method.getLongName()); final MethodInfo info = method.getMethodInfo2(); final ConstPool constPool = info.getConstPool(); final CodeAttribute code = info.getCodeAttribute(); if (code == null) return; final CodeIterator iterator = code.iterator(); while (iterator.hasNext()) { int pos; try { pos = iterator.next(); } catch (final BadBytecode e) { throw new RuntimeException(e); } out.println(pos + ": " + InstructionPrinter.instructionString(iterator, pos, constPool)); } out.println(""); } /** * Writes a .jar file with the modified classes. *

* This comes in handy e.g. when ImageJ is to be run in an environment where * redefining classes is not allowed. If users need to run, say, the legacy * headless support in such an environment, they need to generate a * headless.jar file using this method and prepend it to the class path * (so that the classes of ij.jar are overridden by * headless.jar's classes). *

* * @param path the .jar file to write to * @throws IOException */ public void writeJar(final File path) throws IOException { final JarOutputStream jar = new JarOutputStream(new FileOutputStream(path)); final DataOutputStream dataOut = new DataOutputStream(jar); for (final CtClass clazz : handledClasses.values()) { if (!clazz.isModified() && !clazz.getName().startsWith("net.imagej.patcher.")) continue; final ZipEntry entry = new ZipEntry(clazz.getName().replace('.', '/') + ".class"); jar.putNextEntry(entry); clazz.getClassFile().write(dataOut); dataOut.flush(); } jar.close(); } public void writeJar(final URL directory, final File jarFile) throws IOException, NotFoundException { final int prefixLength = directory.getPath().length(); final Collection urls = Utils.listContents(directory); final byte[] buffer = new byte[16384]; final ZipOutputStream jar = new ZipOutputStream(new FileOutputStream(jarFile)); final DataOutputStream dataOut = new DataOutputStream(jar); for (final URL url : urls) { final String path = url.getPath().substring(prefixLength); final ZipEntry entry = new ZipEntry(path); jar.putNextEntry(entry); if (path.endsWith(".class")) { final String classname = path.substring(0, path.length() - 6).replace('/', '.'); final CtClass clazz = pool.get(classname); clazz.getClassFile().write(dataOut); handledClasses.remove(clazz); } else { final InputStream in = url.openStream(); for (;;) { int count = in.read(buffer); if (count < 0) break; dataOut.write(buffer, 0, count); } in.close(); } dataOut.flush(); } for (final CtClass clazz : handledClasses.values()) { if (!clazz.isModified() && !clazz.getName().startsWith("net.imagej.patcher.")) continue; final ZipEntry entry = new ZipEntry(clazz.getName().replace('.', '/') + ".class"); jar.putNextEntry(entry); clazz.getClassFile().write(dataOut); dataOut.flush(); } jar.close(); } private void verify(final CtClass clazz, final PrintWriter output) { try { final ByteArrayOutputStream stream = new ByteArrayOutputStream(); final DataOutputStream out = new DataOutputStream(stream); clazz.getClassFile().write(out); out.flush(); out.close(); verify(stream.toByteArray(), output); } catch (final Exception e) { e.printStackTrace(); } } private void verify(final byte[] bytecode, final PrintWriter out) { try { Collection urls; urls = Utils.listContents(new File(System.getProperty("user.home"), "fiji/jars/").toURI().toURL()); if (urls.size() == 0) { urls = Utils.listContents(new File(System.getProperty("user.home"), "Fiji.app/jars/").toURI().toURL()); if (urls.size() == 0) { urls = Utils.listContents(new File("/Applications/Fiji.app/jars/") .toURI().toURL()); } } final ClassLoader loader = new java.net.URLClassLoader(urls.toArray(new URL[urls.size()])); Class readerClass = null, checkerClass = null; for (final String prefix : new String[] { "org.", "jruby.", "org.jruby.org." }) { try { readerClass = loader.loadClass(prefix + "objectweb.asm.ClassReader"); checkerClass = loader.loadClass(prefix + "objectweb.asm.util.CheckClassAdapter"); break; } catch (final ClassNotFoundException e) { /* ignore */} } final java.lang.reflect.Constructor ctor = readerClass.getConstructor(new Class[] { bytecode.getClass() }); final Object reader = ctor.newInstance(bytecode); final java.lang.reflect.Method verify = checkerClass.getMethod("verify", new Class[] { readerClass, Boolean.TYPE, PrintWriter.class }); verify.invoke(null, new Object[] { reader, false, out }); } catch (final Throwable e) { if (e.getClass().getName().endsWith(".AnalyzerException")) { final Pattern pattern = Pattern.compile("Error at instruction (\\d+): " + "Argument (\\d+): expected L([^ ,;]+);, but found L(.*);"); final Matcher matcher = pattern.matcher(e.getMessage()); if (matcher.matches()) { final CtClass clazz1 = getClass(matcher.group(3)); final CtClass clazz2 = getClass(matcher.group(4)); try { if (clazz2.subtypeOf(clazz1)) return; } catch (final NotFoundException e1) { e1.printStackTrace(); } } } e.printStackTrace(); } } protected void verify(final PrintWriter out) { out.println("Verifying " + handledClasses.size() + " classes"); for (final CtClass clazz : handledClasses.values()) { out.println("Verifying class " + clazz.getName()); out.flush(); verify(clazz, out); } } /** * Applies legacy patches, optionally including the headless ones. *

* Intended to be used in unit tests only, for newly-created class loaders, * via reflection. *

* * @param forceHeadless also apply the headless patches */ @SuppressWarnings("unused") private static void patch(final boolean forceHeadless) { final ClassLoader loader = CodeHacker.class.getClassLoader(); new LegacyInjector().injectHooks(loader, forceHeadless); } /** * Makes sure that the given class is defined in the class loader. * * @param clazz the class to commit */ public void commitClass(Class clazz) { commitClass(clazz.getName()); } /** * Makes sure that the given class is defined in the class loader. * * @param clazz the name of the class to commit */ public void commitClass(String clazz) { getClass(clazz); } public String getConstant(final String clazz, final String fieldName) throws NotFoundException { final CtClass c = getClass(clazz); return (String) c.getField(fieldName).getConstantValue(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy