Skip to content

Commit

Permalink
fix: Remove additional file-io from phantom generation, fix UI compil…
Browse files Browse the repository at this point in the history
…er not loading in phantoms
  • Loading branch information
Col-E committed Jan 8, 2022
1 parent 3bd51ea commit 7880623
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 90 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<groupId>me.coley</groupId>
<artifactId>recaf</artifactId>
<url>https://github.com/Col-E/Recaf/</url>
<version>2.21.10</version>
<version>2.21.11</version>
<name>Recaf</name>
<description>A modern java bytecode editor</description>
<!-- Variables -->
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/me/coley/recaf/Recaf.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
* @author Matt
*/
public class Recaf {
public static final String VERSION = "2.21.10";
public static final String VERSION = "2.21.11";
public static final String DOC_URL = "https://col-e.github.io/Recaf-documentation/";
public static final int ASM_VERSION = Opcodes.ASM9;
private static Controller currentController;
Expand Down
34 changes: 19 additions & 15 deletions src/main/java/me/coley/recaf/compiler/JavacCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import com.google.common.collect.Sets;
import me.coley.recaf.Recaf;
import me.coley.recaf.util.IOUtil;
import me.coley.recaf.util.Log;
import me.coley.recaf.util.Resource;
import me.coley.recaf.util.VMUtil;
import me.coley.recaf.workspace.JavaResource;

import javax.tools.*;
import javax.tools.JavaFileObject.Kind;
import java.io.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
Expand Down Expand Up @@ -61,7 +63,7 @@ public boolean compile() {
JavaCompiler.CompilationTask task = javac.getTask(null, fm, lll, args, null, unitMap.values());
Boolean b = task.call();
return b != null && b;
} catch(RuntimeException e) {
} catch (RuntimeException e) {
e.printStackTrace();
return false;
}
Expand All @@ -77,13 +79,12 @@ private String getClassPathText() {
char separator = File.pathSeparatorChar;
// add extra dependencies
if (pathItems != null) {
for(String path : pathItems)
for (String path : pathItems)
sb.append(separator).append(path);
}

try {
Stream<Path> paths = Files.walk(getCompilerClasspathDirectory());
//paths = Stream.concat(paths, Files.walk(getCompilerGeneratedClasspathDirectory()));
paths.filter(p -> p.toString().toLowerCase().endsWith(".jar"))
.filter(p -> p.toFile().length() < 10_000_000)
.forEach(p -> sb.append(separator).append(IOUtil.toString(p)));
Expand All @@ -97,14 +98,17 @@ private String getClassPathText() {
* @return Directory to contain additional classpath items.
*/
public static Path getCompilerClasspathDirectory() {
return Recaf.getDirectory("classpath").resolve("compiler");
}

/**
* @return Directory to contain additional classpath items that were generated by Recaf.
*/
public static Path getCompilerGeneratedClasspathDirectory() {
return Recaf.getDirectory("classpath").resolve("generated");
Path path = Recaf.getDirectory("classpath").resolve("compiler");
if (!Files.exists(path)) {
try {
Files.createDirectories(path);
} catch (IOException ex) {
// Shouldn't occur
Log.error("Failed to create compiler ext directory", ex);
throw new IllegalStateException(ex);
}
}
return path;
}

/**
Expand All @@ -127,7 +131,7 @@ public void addUnit(String className, String content) {
*/
public byte[] getUnitCode(String name) {
VirtualJavaFileObject file = unitMap.get(name);
if(file == null)
if (file == null)
return null;
return file.getBytecode();
}
Expand Down Expand Up @@ -196,7 +200,7 @@ private VirtualFileManager(JavaFileManager fallback) {

@Override
public Iterable<JavaFileObject> list(Location location, String packageName,
Set<Kind> kinds, boolean recurse) throws IOException {
Set<Kind> kinds, boolean recurse) throws IOException {
Iterable<JavaFileObject> list = super.list(location, packageName, kinds, recurse);
if ("CLASS_PATH".equals(location.getName()) && kinds.contains(Kind.CLASS)) {
String formatted = packageName.isEmpty() ? "" : packageName.replace('.', '/') + '/';
Expand Down Expand Up @@ -233,7 +237,7 @@ public JavaFileObject getJavaFileForOutput(Location location, String name, Kind
VirtualJavaFileObject obj = unitMap.get(internal);
// Unknown class, assumed to be an inner class.
// add to the unit map so it can be fetched.
if(obj == null) {
if (obj == null) {
obj = new VirtualJavaFileObject(internal, null);
unitMap.put(internal, obj);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public Map<String, byte[]> save(String name) {
JavacCompiler javac = new JavacCompiler();
javac.addToClassPath(resource);
javac.addToClassPath(controller.getWorkspace().getLibraries());
javac.addToClassPath(controller.getWorkspace().getPhantoms());
javac.addUnit(name, getText());
javac.options().lineNumbers = true;
javac.options().variables = true;
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/me/coley/recaf/util/ReflectUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package me.coley.recaf.util;

import java.lang.reflect.Constructor;

/**
* Reflection utils
*
* @author Matt
*/
public class ReflectUtil {
/**
* @param type
* Class to construct.
* @param argTypes
* Argument types.
* @param args
* Argument values.
* @param <T>
* Assumed class type.
*
* @return New instance of class.
*/
public static <T> T quietNew(Class<T> type, Class<?>[] argTypes, Object[] args) {
try {
Constructor<T> constructor = type.getDeclaredConstructor(argTypes);
constructor.setAccessible(true);
return constructor.newInstance(args);
} catch (ReflectiveOperationException ex) {
throw new IllegalStateException("Constructor failure: " + type.getName(), ex);
}
}
}
129 changes: 57 additions & 72 deletions src/main/java/me/coley/recaf/workspace/PhantomResource.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package me.coley.recaf.workspace;

import me.coley.recaf.Recaf;
import me.coley.recaf.command.impl.Export;
import me.coley.recaf.util.ClassUtil;
import me.coley.recaf.util.Log;
import me.coley.recaf.util.TypeUtil;
import me.coley.recaf.util.ReflectUtil;
import org.clyze.jphantom.ClassMembers;
import org.clyze.jphantom.JPhantom;
import org.clyze.jphantom.Options;
Expand All @@ -19,12 +18,7 @@
import org.objectweb.asm.tree.ClassNode;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
* Resource for holding phantom references.
Expand All @@ -33,7 +27,6 @@
*/
public class PhantomResource extends JavaResource {
private static final ResourceLocation LOCATION = LiteralResourceLocation.ofKind(ResourceKind.JAR, "Phantoms");
private static final Path PHANTOM_DIR = Recaf.getDirectory("classpath").resolve("generated");
// TODO: Update phantom refs when:
// - using the recompilers
// - assembling methods (just at startup?)
Expand All @@ -54,15 +47,7 @@ public PhantomResource() {
* When the files cannot be deleted.
*/
public void clear() throws IOException {
// Clear internal
getClasses().clear();
// Clear file cache
Path input = PHANTOM_DIR.resolve("input.jar");
Path output = PHANTOM_DIR.resolve("output.jar");
if (!Files.isDirectory(PHANTOM_DIR))
Files.createDirectories(PHANTOM_DIR);
Files.deleteIfExists(input);
Files.deleteIfExists(output);
}

/**
Expand All @@ -75,29 +60,23 @@ public void clear() throws IOException {
* @throws IOException
* Thrown when JPhantom cannot read from the temporary file where these classes are written to.
*/
public void populatePhantoms(Collection<byte[]> classes) throws IOException {
public void populatePhantoms(Map<String, byte[]> classes) throws IOException {
Log.debug("Begin generating phantom classes, given {} input classes", classes.size());
// Clear old classes
clear();
Path input = PHANTOM_DIR.resolve("input.jar");
Path output = PHANTOM_DIR.resolve("output.jar");
// Write the parameter passed classes to a temp jar
Map<String, byte[]> classMap = new HashMap<>();
Map<Type, ClassNode> nodes = new HashMap<>();
classes.forEach(c -> {
classes.forEach((name, c) -> {
ClassReader cr = new ClassReader(c);
ClassNode node = ClassUtil.getNode(cr, 0);
classMap.put(node.name + ".class", c);
nodes.put(Type.getObjectType(node.name), node);
});
Export.writeArchive(true, input.toFile(), classMap);
Log.debug("Wrote classes to temp file, starting phantom analysis...", classes.size());
// Read into JPhantom
Options.V().setSoftFail(true);
Options.V().setJavaVersion(8);
ClassHierarchy hierarchy = clsHierarchyFromArchive(new JarFile(input.toFile()));
ClassMembers members = ClassMembers.fromJar(new JarFile(input.toFile()), hierarchy);
classes.forEach(c -> {
ClassHierarchy hierarchy = createHierarchy(classes);
ClassMembers members = createMembers(classes, hierarchy);
classes.forEach((name, c) -> {
ClassReader cr = new ClassReader(c);
if (cr.getClassName().contains("$"))
return;
Expand All @@ -118,66 +97,72 @@ public void populatePhantoms(Collection<byte[]> classes) throws IOException {
JPhantom phantom = new JPhantom(nodes, hierarchy, members);
phantom.run();
phantom.getGenerated().forEach((k, v) -> getClasses().put(k.getInternalName(), decorate(v)));
classMap.clear();
getClasses().forEach((k, v) -> classMap.put(k + ".class", v));
Export.writeArchive(true, output.toFile(), classMap);
Log.debug("Phantom analysis complete, cleaning temp file", classes.size());
Log.debug("Phantom analysis complete, generated {} classes", classes.size());
// Cleanup
Phantoms.refresh();
ClassAccessStateMachine.refresh();
FieldAccessStateMachine.refresh();
MethodAccessStateMachine.refresh();
Files.deleteIfExists(input);
}

/**
* This is copy pasted from JPhantom, modified to be more lenient towards obfuscated inputs.
* @param classMap
* Map to pull classes from.
* @param hierarchy
* Hierarchy to pass to {@link ClassMembers} constructor.
*
* @param file
* Some jar file.
* @return Members instance.
*/
public static ClassMembers createMembers(Map<String, byte[]> classMap, ClassHierarchy hierarchy) {
Class<?>[] argTypes = new Class[]{ClassHierarchy.class};
Object[] argVals = new Object[]{hierarchy};
ClassMembers repo = ReflectUtil.quietNew(ClassMembers.class, argTypes, argVals);
try {
new ClassReader("java/lang/Object").accept(repo.new Feeder(), 0);
} catch (IOException ex) {
Log.error("Failed to get initial reader ClassMembers, could not lookup 'java/lang/Object'");
throw new IllegalStateException();
}
for (Map.Entry<String, byte[]> e : classMap.entrySet()) {
try {
new ClassReader(e.getValue()).accept(repo.new Feeder(), 0);
} catch (Throwable t) {
Log.debug("Could not supply {} to ClassMembers feeder", e.getKey(), t);
}
}
return repo;
}

/**
* @param classMap
* Map to pull classes from.
*
* @return Class hierarchy.
*
* @throws IOException
* When the archive cannot be read.
*/
private static ClassHierarchy clsHierarchyFromArchive(JarFile file) throws IOException {
try {
ClassHierarchy hierarchy = new IncrementalClassHierarchy();
for (Enumeration<JarEntry> e = file.entries(); e.hasMoreElements(); ) {
JarEntry entry = e.nextElement();
if (entry.isDirectory())
continue;
if (!entry.getName().endsWith(".class"))
continue;
try (InputStream stream = file.getInputStream(entry)) {
ClassReader reader = new ClassReader(stream);
String[] ifaceNames = reader.getInterfaces();
Type clazz = Type.getObjectType(reader.getClassName());
Type superclass = reader.getSuperName() == null ?
TypeUtil.OBJECT_TYPE : Type.getObjectType(reader.getSuperName());
Type[] ifaces = new Type[ifaceNames.length];
for (int i = 0; i < ifaces.length; i++)
ifaces[i] = Type.getObjectType(ifaceNames[i]);
// Add type to hierarchy
boolean isInterface = (reader.getAccess() & Opcodes.ACC_INTERFACE) != 0;
try {
if (isInterface) {
hierarchy.addInterface(clazz, ifaces);
} else {
hierarchy.addClass(clazz, superclass, ifaces);
}
} catch (Exception ex) {
Log.error(ex, "JPhantom: Hierarchy failure for: {}", clazz);
}
} catch (IOException ex) {
Log.error(ex, "JPhantom: IO Error reading from archive: {}", file.getName());
public static ClassHierarchy createHierarchy(Map<String, byte[]> classMap) {
ClassHierarchy hierarchy = new IncrementalClassHierarchy();
for (Map.Entry<String, byte[]> e : classMap.entrySet()) {
try {
ClassReader reader = new ClassReader(e.getValue());
String[] ifaceNames = reader.getInterfaces();
Type clazz = Type.getObjectType(reader.getClassName());
Type superclass = reader.getSuperName() == null ?
Type.getObjectType("java/lang/Object") : Type.getObjectType(reader.getSuperName());
Type[] ifaces = new Type[ifaceNames.length];
for (int i = 0; i < ifaces.length; i++)
ifaces[i] = Type.getObjectType(ifaceNames[i]);
// Add type to hierarchy
boolean isInterface = (reader.getAccess() & Opcodes.ACC_INTERFACE) != 0;
if (isInterface) {
hierarchy.addInterface(clazz, ifaces);
} else {
hierarchy.addClass(clazz, superclass, ifaces);
}
} catch (Exception ex) {
Log.error("JPhantom: Hierarchy failure for: {}", e.getKey(), ex);
}
return hierarchy;
} finally {
file.close();
}
return hierarchy;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/me/coley/recaf/workspace/Workspace.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public void analyzePhantoms() {
ThreadUtil.run(() -> {
try {
long start = System.currentTimeMillis();
phantoms.populatePhantoms(getPrimaryClasses());
phantoms.populatePhantoms(primary.getClasses());
Log.debug("Generated {} phantom classes in {} ms",
phantoms.getClasses().size(), (System.currentTimeMillis() - start));
} catch (Throwable t) {
Expand Down

0 comments on commit 7880623

Please sign in to comment.