From f0885819aeeb2edbfcfc0b23566cccb571166a02 Mon Sep 17 00:00:00 2001 From: Runemoro Date: Mon, 6 Jul 2020 06:34:10 -0400 Subject: Make class loading more flexible (#277) --- .../src/main/java/cuchaz/enigma/ClassProvider.java | 10 -- enigma/src/main/java/cuchaz/enigma/Enigma.java | 19 ++- .../src/main/java/cuchaz/enigma/EnigmaProject.java | 40 +++--- .../java/cuchaz/enigma/analysis/BuiltinPlugin.java | 3 +- .../java/cuchaz/enigma/analysis/ClassCache.java | 126 ------------------- .../cuchaz/enigma/analysis/index/JarIndex.java | 24 +++- .../enigma/api/service/JarIndexerService.java | 15 ++- .../enigma/classhandle/ClassHandleProvider.java | 17 +-- .../enigma/classprovider/CachingClassProvider.java | 36 ++++++ .../cuchaz/enigma/classprovider/ClassProvider.java | 17 +++ .../classprovider/ClasspathClassProvider.java | 27 ++++ .../classprovider/CombiningClassProvider.java | 31 +++++ .../enigma/classprovider/JarClassProvider.java | 64 ++++++++++ .../classprovider/ObfuscationFixClassProvider.java | 85 +++++++++++++ .../cuchaz/enigma/source/DecompilerService.java | 2 +- .../cuchaz/enigma/source/cfr/CfrDecompiler.java | 20 +-- .../enigma/source/procyon/ProcyonDecompiler.java | 23 +++- .../typeloader/CachingClasspathTypeLoader.java | 33 ----- .../procyon/typeloader/CachingTypeLoader.java | 38 ------ .../typeloader/CompiledSourceTypeLoader.java | 140 --------------------- .../procyon/typeloader/NoRetryMetadataSystem.java | 38 ------ .../procyon/typeloader/SynchronizedTypeLoader.java | 20 --- .../src/main/java/cuchaz/enigma/utils/AsmUtil.java | 20 +++ 23 files changed, 365 insertions(+), 483 deletions(-) delete mode 100644 enigma/src/main/java/cuchaz/enigma/ClassProvider.java delete mode 100644 enigma/src/main/java/cuchaz/enigma/analysis/ClassCache.java create mode 100644 enigma/src/main/java/cuchaz/enigma/classprovider/CachingClassProvider.java create mode 100644 enigma/src/main/java/cuchaz/enigma/classprovider/ClassProvider.java create mode 100644 enigma/src/main/java/cuchaz/enigma/classprovider/ClasspathClassProvider.java create mode 100644 enigma/src/main/java/cuchaz/enigma/classprovider/CombiningClassProvider.java create mode 100644 enigma/src/main/java/cuchaz/enigma/classprovider/JarClassProvider.java create mode 100644 enigma/src/main/java/cuchaz/enigma/classprovider/ObfuscationFixClassProvider.java delete mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingClasspathTypeLoader.java delete mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingTypeLoader.java delete mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CompiledSourceTypeLoader.java delete mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/NoRetryMetadataSystem.java delete mode 100644 enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/SynchronizedTypeLoader.java create mode 100644 enigma/src/main/java/cuchaz/enigma/utils/AsmUtil.java (limited to 'enigma/src/main') diff --git a/enigma/src/main/java/cuchaz/enigma/ClassProvider.java b/enigma/src/main/java/cuchaz/enigma/ClassProvider.java deleted file mode 100644 index 2b91379..0000000 --- a/enigma/src/main/java/cuchaz/enigma/ClassProvider.java +++ /dev/null @@ -1,10 +0,0 @@ -package cuchaz.enigma; - -import org.objectweb.asm.tree.ClassNode; - -import javax.annotation.Nullable; - -public interface ClassProvider { - @Nullable - ClassNode getClassNode(String name); -} diff --git a/enigma/src/main/java/cuchaz/enigma/Enigma.java b/enigma/src/main/java/cuchaz/enigma/Enigma.java index 73c9a09..2e9be34 100644 --- a/enigma/src/main/java/cuchaz/enigma/Enigma.java +++ b/enigma/src/main/java/cuchaz/enigma/Enigma.java @@ -13,7 +13,6 @@ package cuchaz.enigma; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableListMultimap; -import cuchaz.enigma.analysis.ClassCache; import cuchaz.enigma.analysis.index.JarIndex; import cuchaz.enigma.api.EnigmaPlugin; import cuchaz.enigma.api.EnigmaPluginContext; @@ -21,6 +20,10 @@ import cuchaz.enigma.api.service.EnigmaService; import cuchaz.enigma.api.service.EnigmaServiceFactory; import cuchaz.enigma.api.service.EnigmaServiceType; import cuchaz.enigma.api.service.JarIndexerService; +import cuchaz.enigma.classprovider.CachingClassProvider; +import cuchaz.enigma.classprovider.ClassProvider; +import cuchaz.enigma.classprovider.CombiningClassProvider; +import cuchaz.enigma.classprovider.JarClassProvider; import cuchaz.enigma.utils.Utils; import org.objectweb.asm.Opcodes; @@ -28,6 +31,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.List; import java.util.ServiceLoader; +import java.util.Set; public class Enigma { public static final String NAME = "Enigma"; @@ -51,13 +55,16 @@ public class Enigma { return new Builder(); } - public EnigmaProject openJar(Path path, ProgressListener progress) throws IOException { - ClassCache classCache = ClassCache.of(path); - JarIndex jarIndex = classCache.index(progress); + public EnigmaProject openJar(Path path, ClassProvider libraryClassProvider, ProgressListener progress) throws IOException { + JarClassProvider jarClassProvider = new JarClassProvider(path); + ClassProvider classProvider = new CachingClassProvider(new CombiningClassProvider(jarClassProvider, libraryClassProvider)); + Set scope = jarClassProvider.getClassNames(); - services.get(JarIndexerService.TYPE).forEach(indexer -> indexer.acceptJar(classCache, jarIndex)); + JarIndex index = JarIndex.empty(); + index.indexJar(scope, classProvider, progress); + services.get(JarIndexerService.TYPE).forEach(indexer -> indexer.acceptJar(scope, classProvider, index)); - return new EnigmaProject(this, classCache, jarIndex, Utils.zipSha1(path)); + return new EnigmaProject(this, classProvider, index, Utils.zipSha1(path)); } public EnigmaProfile getProfile() { diff --git a/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java b/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java index 43ea14e..3999572 100644 --- a/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java +++ b/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java @@ -2,16 +2,20 @@ package cuchaz.enigma; import com.google.common.base.Functions; import com.google.common.base.Preconditions; -import cuchaz.enigma.analysis.ClassCache; import cuchaz.enigma.analysis.EntryReference; import cuchaz.enigma.analysis.index.JarIndex; import cuchaz.enigma.api.service.NameProposalService; import cuchaz.enigma.bytecode.translators.SourceFixVisitor; import cuchaz.enigma.bytecode.translators.TranslationClassVisitor; -import cuchaz.enigma.source.*; +import cuchaz.enigma.classprovider.ClassProvider; +import cuchaz.enigma.source.Decompiler; +import cuchaz.enigma.source.DecompilerService; +import cuchaz.enigma.source.SourceSettings; import cuchaz.enigma.translation.ProposingTranslator; import cuchaz.enigma.translation.Translator; -import cuchaz.enigma.translation.mapping.*; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.mapping.MappingsChecker; import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree; import cuchaz.enigma.translation.mapping.tree.EntryTree; import cuchaz.enigma.translation.representation.entry.ClassEntry; @@ -19,11 +23,11 @@ import cuchaz.enigma.translation.representation.entry.Entry; import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; import cuchaz.enigma.translation.representation.entry.MethodEntry; import cuchaz.enigma.utils.I18n; - import org.objectweb.asm.ClassWriter; import org.objectweb.asm.tree.ClassNode; -import java.io.*; +import java.io.BufferedWriter; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; @@ -37,16 +41,16 @@ import java.util.stream.Collectors; public class EnigmaProject { private final Enigma enigma; - private final ClassCache classCache; + private final ClassProvider classProvider; private final JarIndex jarIndex; private final byte[] jarChecksum; private EntryRemapper mapper; - public EnigmaProject(Enigma enigma, ClassCache classCache, JarIndex jarIndex, byte[] jarChecksum) { + public EnigmaProject(Enigma enigma, ClassProvider classProvider, JarIndex jarIndex, byte[] jarChecksum) { Preconditions.checkArgument(jarChecksum.length == 20); this.enigma = enigma; - this.classCache = classCache; + this.classProvider = classProvider; this.jarIndex = jarIndex; this.jarChecksum = jarChecksum; @@ -65,8 +69,8 @@ public class EnigmaProject { return enigma; } - public ClassCache getClassCache() { - return classCache; + public ClassProvider getClassProvider() { + return classProvider; } public JarIndex getJarIndex() { @@ -103,20 +107,6 @@ public class EnigmaProject { return droppedMappings.keySet(); } - public Decompiler createDecompiler(DecompilerService decompilerService) { - return decompilerService.create(name -> { - ClassNode node = this.getClassCache().getClassNode(name); - - if (node == null) { - return null; - } - - ClassNode fixedNode = new ClassNode(); - node.accept(new SourceFixVisitor(Enigma.ASM_VERSION, fixedNode, getJarIndex())); - return fixedNode; - }, new SourceSettings(true, true)); - } - public boolean isRenamable(Entry obfEntry) { if (obfEntry instanceof MethodEntry) { // HACKHACK: Object methods are not obfuscated identifiers @@ -171,7 +161,7 @@ public class EnigmaProject { ClassEntry translatedEntry = deobfuscator.translate(entry); progress.step(count.getAndIncrement(), translatedEntry.toString()); - ClassNode node = classCache.getClassNode(entry.getFullName()); + ClassNode node = classProvider.get(entry.getFullName()); if (node != null) { ClassNode translatedNode = new ClassNode(); node.accept(new TranslationClassVisitor(deobfuscator, Enigma.ASM_VERSION, new SourceFixVisitor(Enigma.ASM_VERSION, translatedNode, jarIndex))); diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java index 4685b39..989464d 100644 --- a/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java +++ b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java @@ -12,7 +12,6 @@ import cuchaz.enigma.translation.representation.entry.ClassEntry; import cuchaz.enigma.translation.representation.entry.Entry; import cuchaz.enigma.translation.representation.entry.FieldEntry; import cuchaz.enigma.utils.Pair; -import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; @@ -51,7 +50,7 @@ public final class BuiltinPlugin implements EnigmaPlugin { final Map, String> names = new HashMap<>(); final EnumFieldNameFindingVisitor visitor = new EnumFieldNameFindingVisitor(names); - ctx.registerService("enigma:enum_initializer_indexer", JarIndexerService.TYPE, ctx1 -> (classCache, jarIndex) -> classCache.visit(() -> visitor, ClassReader.SKIP_FRAMES)); + ctx.registerService("enigma:enum_initializer_indexer", JarIndexerService.TYPE, ctx1 -> JarIndexerService.fromVisitor(visitor)); ctx.registerService("enigma:enum_name_proposer", NameProposalService.TYPE, ctx1 -> (obfEntry, remapper) -> Optional.ofNullable(names.get(obfEntry))); } diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/ClassCache.java b/enigma/src/main/java/cuchaz/enigma/analysis/ClassCache.java deleted file mode 100644 index a3d24e3..0000000 --- a/enigma/src/main/java/cuchaz/enigma/analysis/ClassCache.java +++ /dev/null @@ -1,126 +0,0 @@ -package cuchaz.enigma.analysis; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.collect.ImmutableSet; -import cuchaz.enigma.ClassProvider; -import cuchaz.enigma.Enigma; -import cuchaz.enigma.ProgressListener; -import cuchaz.enigma.analysis.index.JarIndex; -import cuchaz.enigma.bytecode.translators.LocalVariableFixVisitor; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.tree.ClassNode; - -import javax.annotation.Nullable; -import java.io.IOException; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; - -public final class ClassCache implements AutoCloseable, ClassProvider { - private final FileSystem fileSystem; - private final ImmutableSet classNames; - - private final Cache nodeCache = CacheBuilder.newBuilder() - .maximumSize(128) - .expireAfterAccess(1, TimeUnit.MINUTES) - .build(); - - private ClassCache(FileSystem fileSystem, ImmutableSet classNames) { - this.fileSystem = fileSystem; - this.classNames = classNames; - } - - public static ClassCache of(Path jarPath) throws IOException { - FileSystem fileSystem = FileSystems.newFileSystem(jarPath, (ClassLoader) null); - ImmutableSet classNames = collectClassNames(fileSystem); - - return new ClassCache(fileSystem, classNames); - } - - private static ImmutableSet collectClassNames(FileSystem fileSystem) throws IOException { - ImmutableSet.Builder classNames = ImmutableSet.builder(); - for (Path root : fileSystem.getRootDirectories()) { - Files.walk(root).map(Path::toString) - .forEach(path -> { - if (path.endsWith(".class")) { - String name = path.substring(1, path.length() - ".class".length()); - classNames.add(name); - } - }); - } - - return classNames.build(); - } - - @Nullable - @Override - public ClassNode getClassNode(String name) { - if (!classNames.contains(name)) { - return null; - } - - try { - return nodeCache.get(name, () -> parseNode(name)); - } catch (ExecutionException e) { - throw new RuntimeException(e); - } - } - - private ClassNode parseNode(String name) throws IOException { - ClassReader reader = getReader(name); - - ClassNode node = new ClassNode(); - - LocalVariableFixVisitor visitor = new LocalVariableFixVisitor(Enigma.ASM_VERSION, node); - reader.accept(visitor, 0); - - return node; - } - - private ClassReader getReader(String name) throws IOException { - Path path = fileSystem.getPath(name + ".class"); - byte[] bytes = Files.readAllBytes(path); - return new ClassReader(bytes); - } - - public int getClassCount() { - return classNames.size(); - } - - public void visit(Supplier visitorSupplier, int readFlags) { - for (String className : classNames) { - ClassVisitor visitor = visitorSupplier.get(); - - ClassNode cached = nodeCache.getIfPresent(className); - if (cached != null) { - cached.accept(visitor); - continue; - } - - try { - ClassReader reader = getReader(className); - reader.accept(visitor, readFlags); - } catch (IOException e) { - System.out.println("Failed to visit class " + className); - e.printStackTrace(); - } - } - } - - @Override - public void close() throws IOException { - this.fileSystem.close(); - } - - public JarIndex index(ProgressListener progress) { - JarIndex index = JarIndex.empty(); - index.indexJar(this, progress); - return index; - } -} diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/index/JarIndex.java b/enigma/src/main/java/cuchaz/enigma/analysis/index/JarIndex.java index de3f5f5..b5ad91a 100644 --- a/enigma/src/main/java/cuchaz/enigma/analysis/index/JarIndex.java +++ b/enigma/src/main/java/cuchaz/enigma/analysis/index/JarIndex.java @@ -15,20 +15,21 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import cuchaz.enigma.Enigma; import cuchaz.enigma.ProgressListener; -import cuchaz.enigma.analysis.ClassCache; import cuchaz.enigma.analysis.ReferenceTargetType; +import cuchaz.enigma.classprovider.ClassProvider; import cuchaz.enigma.translation.mapping.EntryResolver; import cuchaz.enigma.translation.mapping.IndexEntryResolver; import cuchaz.enigma.translation.representation.Lambda; import cuchaz.enigma.translation.representation.entry.*; import cuchaz.enigma.utils.I18n; -import org.objectweb.asm.ClassReader; - import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; +import java.util.Set; public class JarIndex implements JarIndexer { + private final Set indexedClasses = new HashSet<>(); private final EntryIndex entryIndex; private final InheritanceIndex inheritanceIndex; private final ReferenceIndex referenceIndex; @@ -59,14 +60,21 @@ public class JarIndex implements JarIndexer { return new JarIndex(entryIndex, inheritanceIndex, referenceIndex, bridgeMethodIndex, packageVisibilityIndex); } - public void indexJar(ClassCache classCache, ProgressListener progress) { + public void indexJar(Set classNames, ClassProvider classProvider, ProgressListener progress) { + indexedClasses.addAll(classNames); progress.init(4, I18n.translate("progress.jar.indexing")); progress.step(1, I18n.translate("progress.jar.indexing.entries")); - classCache.visit(() -> new IndexClassVisitor(this, Enigma.ASM_VERSION), ClassReader.SKIP_CODE); + + for (String className : classNames) { + classProvider.get(className).accept(new IndexClassVisitor(this, Enigma.ASM_VERSION)); + } progress.step(2, I18n.translate("progress.jar.indexing.references")); - classCache.visit(() -> new IndexReferenceVisitor(this, entryIndex, inheritanceIndex, Enigma.ASM_VERSION), 0); + + for (String className : classNames) { + classProvider.get(className).accept(new IndexReferenceVisitor(this, entryIndex, inheritanceIndex, Enigma.ASM_VERSION)); + } progress.step(3, I18n.translate("progress.jar.indexing.methods")); bridgeMethodIndex.findBridgeMethods(); @@ -167,4 +175,8 @@ public class JarIndex implements JarIndexer { public EntryResolver getEntryResolver() { return entryResolver; } + + public boolean isIndexed(String internalName) { + return indexedClasses.contains(internalName); + } } diff --git a/enigma/src/main/java/cuchaz/enigma/api/service/JarIndexerService.java b/enigma/src/main/java/cuchaz/enigma/api/service/JarIndexerService.java index 0cda199..5417531 100644 --- a/enigma/src/main/java/cuchaz/enigma/api/service/JarIndexerService.java +++ b/enigma/src/main/java/cuchaz/enigma/api/service/JarIndexerService.java @@ -1,10 +1,21 @@ package cuchaz.enigma.api.service; -import cuchaz.enigma.analysis.ClassCache; import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.classprovider.ClassProvider; +import org.objectweb.asm.ClassVisitor; + +import java.util.Set; public interface JarIndexerService extends EnigmaService { EnigmaServiceType TYPE = EnigmaServiceType.create("jar_indexer"); - void acceptJar(ClassCache classCache, JarIndex jarIndex); + void acceptJar(Set scope, ClassProvider classProvider, JarIndex jarIndex); + + static JarIndexerService fromVisitor(ClassVisitor visitor) { + return (scope, classProvider, jarIndex) -> { + for (String className : scope) { + classProvider.get(className).accept(visitor); + } + }; + } } diff --git a/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleProvider.java b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleProvider.java index f9e4eff..bc38e61 100644 --- a/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleProvider.java +++ b/enigma/src/main/java/cuchaz/enigma/classhandle/ClassHandleProvider.java @@ -11,11 +11,10 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.annotation.Nullable; -import org.objectweb.asm.tree.ClassNode; +import cuchaz.enigma.classprovider.CachingClassProvider; +import cuchaz.enigma.classprovider.ObfuscationFixClassProvider; -import cuchaz.enigma.Enigma; import cuchaz.enigma.EnigmaProject; -import cuchaz.enigma.bytecode.translators.SourceFixVisitor; import cuchaz.enigma.events.ClassHandleListener; import cuchaz.enigma.events.ClassHandleListener.InvalidationType; import cuchaz.enigma.source.*; @@ -89,17 +88,7 @@ public final class ClassHandleProvider { } private Decompiler createDecompiler() { - return ds.create(name -> { - ClassNode node = project.getClassCache().getClassNode(name); - - if (node == null) { - return null; - } - - ClassNode fixedNode = new ClassNode(); - node.accept(new SourceFixVisitor(Enigma.ASM_VERSION, fixedNode, project.getJarIndex())); - return fixedNode; - }, new SourceSettings(true, true)); + return ds.create(new CachingClassProvider(new ObfuscationFixClassProvider(project.getClassProvider(), project.getJarIndex())), new SourceSettings(true, true)); } /** diff --git a/enigma/src/main/java/cuchaz/enigma/classprovider/CachingClassProvider.java b/enigma/src/main/java/cuchaz/enigma/classprovider/CachingClassProvider.java new file mode 100644 index 0000000..47f5eb8 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/classprovider/CachingClassProvider.java @@ -0,0 +1,36 @@ +package cuchaz.enigma.classprovider; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.objectweb.asm.tree.ClassNode; + +import javax.annotation.Nullable; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +/** + * Wraps a ClassProvider to provide caching and synchronization. + */ +public class CachingClassProvider implements ClassProvider { + private final ClassProvider classProvider; + private final Cache> cache = CacheBuilder.newBuilder() + .maximumSize(128) + .expireAfterAccess(1, TimeUnit.MINUTES) + .concurrencyLevel(1) + .build(); + + public CachingClassProvider(ClassProvider classProvider) { + this.classProvider = classProvider; + } + + @Override + @Nullable + public ClassNode get(String name) { + try { + return cache.get(name, () -> Optional.ofNullable(classProvider.get(name))).orElse(null); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/classprovider/ClassProvider.java b/enigma/src/main/java/cuchaz/enigma/classprovider/ClassProvider.java new file mode 100644 index 0000000..6e4a665 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/classprovider/ClassProvider.java @@ -0,0 +1,17 @@ +package cuchaz.enigma.classprovider; + +import org.objectweb.asm.tree.ClassNode; + +import javax.annotation.Nullable; + +public interface ClassProvider { + /** + * Gets the {@linkplain ClassNode} for a class. The class provider may return a cached result, + * so it's important to not mutate it. + * + * @param name the internal name of the class + * @return the {@linkplain ClassNode} for that class, or {@code null} if it was not found + */ + @Nullable + ClassNode get(String name); +} diff --git a/enigma/src/main/java/cuchaz/enigma/classprovider/ClasspathClassProvider.java b/enigma/src/main/java/cuchaz/enigma/classprovider/ClasspathClassProvider.java new file mode 100644 index 0000000..e9472fa --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/classprovider/ClasspathClassProvider.java @@ -0,0 +1,27 @@ +package cuchaz.enigma.classprovider; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.ClassNode; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.InputStream; + +/** + * Provides classes by loading them from the classpath. + */ +public class ClasspathClassProvider implements ClassProvider { + @Nullable @Override public ClassNode get(String name) { + try (InputStream in = ClasspathClassProvider.class.getResourceAsStream("/" + name + ".class")) { + if (in == null) { + return null; + } + + ClassNode node = new ClassNode(); + new ClassReader(in).accept(node, 0); + return node; + } catch (IOException e) { + return null; + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/classprovider/CombiningClassProvider.java b/enigma/src/main/java/cuchaz/enigma/classprovider/CombiningClassProvider.java new file mode 100644 index 0000000..865464c --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/classprovider/CombiningClassProvider.java @@ -0,0 +1,31 @@ +package cuchaz.enigma.classprovider; + +import org.objectweb.asm.tree.ClassNode; + +import javax.annotation.Nullable; + +/** + * Combines a list of {@link ClassProvider}s into one, calling each one in a row + * until one can provide the class. + */ +public class CombiningClassProvider implements ClassProvider { + private final ClassProvider[] classProviders; + + public CombiningClassProvider(ClassProvider... classProviders) { + this.classProviders = classProviders; + } + + @Override + @Nullable + public ClassNode get(String name) { + for (ClassProvider cp : classProviders) { + ClassNode node = cp.get(name); + + if (node != null) { + return node; + } + } + + return null; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/classprovider/JarClassProvider.java b/enigma/src/main/java/cuchaz/enigma/classprovider/JarClassProvider.java new file mode 100644 index 0000000..c614b0a --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/classprovider/JarClassProvider.java @@ -0,0 +1,64 @@ +package cuchaz.enigma.classprovider; + +import com.google.common.collect.ImmutableSet; +import cuchaz.enigma.utils.AsmUtil; +import org.objectweb.asm.tree.ClassNode; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; + +/** + * Provides classes by loading them from a JAR file. + */ +public class JarClassProvider implements AutoCloseable, ClassProvider { + private final FileSystem fileSystem; + private final Set classNames; + + public JarClassProvider(Path jarPath) throws IOException { + this.fileSystem = FileSystems.newFileSystem(jarPath, (ClassLoader) null); + this.classNames = collectClassNames(fileSystem); + } + + private static ImmutableSet collectClassNames(FileSystem fileSystem) throws IOException { + ImmutableSet.Builder classNames = ImmutableSet.builder(); + for (Path root : fileSystem.getRootDirectories()) { + Files.walk(root).map(Path::toString) + .forEach(path -> { + if (path.endsWith(".class")) { + String name = path.substring(1, path.length() - ".class".length()); + classNames.add(name); + } + }); + } + + return classNames.build(); + } + + public Set getClassNames() { + return classNames; + } + + @Nullable + @Override + public ClassNode get(String name) { + if (!classNames.contains(name)) { + return null; + } + + try { + return AsmUtil.bytesToNode(Files.readAllBytes(fileSystem.getPath(name + ".class"))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void close() throws Exception { + fileSystem.close(); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/classprovider/ObfuscationFixClassProvider.java b/enigma/src/main/java/cuchaz/enigma/classprovider/ObfuscationFixClassProvider.java new file mode 100644 index 0000000..db9e2d0 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/classprovider/ObfuscationFixClassProvider.java @@ -0,0 +1,85 @@ +package cuchaz.enigma.classprovider; + +import cuchaz.enigma.Enigma; +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.bytecode.translators.LocalVariableFixVisitor; +import cuchaz.enigma.bytecode.translators.SourceFixVisitor; +import cuchaz.enigma.classprovider.ClassProvider; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import javax.annotation.Nullable; + +/** + * Wraps a ClassProvider to apply fixes to the following problems introduced by the obfuscator, + * so that the classes can be decompiled correctly: + *
    + *
  • Bridge methods missing the "bridge" or "synthetic" access modifier + *
  • .getClass() calls added by Proguard (Proguard adds these to preserve semantics when Proguard removes + * a field access which may have caused a {@code NullPointerException}). + *
  • LVT names that don't match parameter table name + *
  • LVT names that are invalid or duplicate + *
  • Enum constructor parameters that are incorrectly named or missing the "synthetic" access modifier + *
  • "this" parameter which is incorrectly named + *
+ *

+ * These fixes are only applied to classes that were indexed by the JarIndex provided, and not library classes. + */ +public class ObfuscationFixClassProvider implements ClassProvider { + private final ClassProvider classProvider; + private final JarIndex jarIndex; + + public ObfuscationFixClassProvider(ClassProvider classProvider, JarIndex jarIndex) { + this.classProvider = classProvider; + this.jarIndex = jarIndex; + } + + @Override + @Nullable + public ClassNode get(String name) { + ClassNode node = classProvider.get(name); + + if (!jarIndex.isIndexed(name)) { + return node; + } + + ClassNode fixedNode = new ClassNode(); + ClassVisitor visitor = fixedNode; + visitor = new LocalVariableFixVisitor(Enigma.ASM_VERSION, visitor); + visitor = new SourceFixVisitor(Enigma.ASM_VERSION, visitor, jarIndex); + node.accept(visitor); + removeRedundantClassCalls(fixedNode); + + return fixedNode; + } + + private void removeRedundantClassCalls(ClassNode node) { + // Removes .getClass() calls added by Proguard: + // DUP + // INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class; + // POP + for (MethodNode methodNode : node.methods) { + AbstractInsnNode insnNode = methodNode.instructions.getFirst(); + while (insnNode != null) { + if (insnNode instanceof MethodInsnNode && insnNode.getOpcode() == Opcodes.INVOKEVIRTUAL) { + MethodInsnNode methodInsnNode = (MethodInsnNode) insnNode; + if (methodInsnNode.name.equals("getClass") && methodInsnNode.owner.equals("java/lang/Object") && methodInsnNode.desc.equals("()Ljava/lang/Class;")) { + AbstractInsnNode previous = methodInsnNode.getPrevious(); + AbstractInsnNode next = methodInsnNode.getNext(); + if (previous.getOpcode() == Opcodes.DUP && next.getOpcode() == Opcodes.POP) { + insnNode = previous.getPrevious();//reset the iterator so it gets the new next instruction + methodNode.instructions.remove(previous); + methodNode.instructions.remove(methodInsnNode); + methodNode.instructions.remove(next); + } + } + } + insnNode = insnNode.getNext(); + } + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/DecompilerService.java b/enigma/src/main/java/cuchaz/enigma/source/DecompilerService.java index 377ccbc..638498f 100644 --- a/enigma/src/main/java/cuchaz/enigma/source/DecompilerService.java +++ b/enigma/src/main/java/cuchaz/enigma/source/DecompilerService.java @@ -1,6 +1,6 @@ package cuchaz.enigma.source; -import cuchaz.enigma.ClassProvider; +import cuchaz.enigma.classprovider.ClassProvider; import cuchaz.enigma.api.service.EnigmaService; import cuchaz.enigma.api.service.EnigmaServiceType; diff --git a/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDecompiler.java b/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDecompiler.java index 9e37f16..941a699 100644 --- a/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDecompiler.java +++ b/enigma/src/main/java/cuchaz/enigma/source/cfr/CfrDecompiler.java @@ -1,10 +1,10 @@ package cuchaz.enigma.source.cfr; -import com.google.common.io.ByteStreams; -import cuchaz.enigma.ClassProvider; +import cuchaz.enigma.classprovider.ClassProvider; import cuchaz.enigma.source.Decompiler; import cuchaz.enigma.source.Source; import cuchaz.enigma.source.SourceSettings; +import cuchaz.enigma.utils.AsmUtil; import org.benf.cfr.reader.apiunreleased.ClassFileSource2; import org.benf.cfr.reader.apiunreleased.JarContent; import org.benf.cfr.reader.bytecode.analysis.parse.utils.Pair; @@ -19,16 +19,12 @@ import org.benf.cfr.reader.util.CannotLoadClassException; import org.benf.cfr.reader.util.collections.ListFactory; import org.benf.cfr.reader.util.getopt.Options; import org.benf.cfr.reader.util.getopt.OptionsImpl; -import org.objectweb.asm.ClassWriter; import org.objectweb.asm.tree.ClassNode; -import java.io.IOException; -import java.io.InputStream; import java.util.Collection; import java.util.HashMap; import java.util.Map; - public class CfrDecompiler implements Decompiler { private final DCCommonState state; @@ -58,21 +54,13 @@ public class CfrDecompiler implements Decompiler { @Override public Pair getClassFileContent(String path) { - ClassNode node = classProvider.getClassNode(path.substring(0, path.lastIndexOf('.'))); + ClassNode node = classProvider.get(path.substring(0, path.lastIndexOf('.'))); if (node == null) { - try (InputStream classResource = CfrDecompiler.class.getClassLoader().getResourceAsStream(path)) { - if (classResource != null) { - return new Pair<>(ByteStreams.toByteArray(classResource), path); - } - } catch (IOException ignored) {} - return null; } - ClassWriter cw = new ClassWriter(0); - node.accept(cw); - return new Pair<>(cw.toByteArray(), path); + return new Pair<>(AsmUtil.nodeToBytes(node), path); } }); } diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/ProcyonDecompiler.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/ProcyonDecompiler.java index 3cbd680..9dc1e0a 100644 --- a/enigma/src/main/java/cuchaz/enigma/source/procyon/ProcyonDecompiler.java +++ b/enigma/src/main/java/cuchaz/enigma/source/procyon/ProcyonDecompiler.java @@ -11,14 +11,13 @@ import com.strobel.decompiler.languages.java.JavaFormattingOptions; import com.strobel.decompiler.languages.java.ast.AstBuilder; import com.strobel.decompiler.languages.java.ast.CompilationUnit; import com.strobel.decompiler.languages.java.ast.InsertParenthesesVisitor; -import cuchaz.enigma.ClassProvider; +import cuchaz.enigma.classprovider.ClassProvider; import cuchaz.enigma.source.Source; import cuchaz.enigma.source.Decompiler; import cuchaz.enigma.source.SourceSettings; import cuchaz.enigma.source.procyon.transformers.*; -import cuchaz.enigma.source.procyon.typeloader.CompiledSourceTypeLoader; -import cuchaz.enigma.source.procyon.typeloader.NoRetryMetadataSystem; -import cuchaz.enigma.source.procyon.typeloader.SynchronizedTypeLoader; +import cuchaz.enigma.utils.AsmUtil; +import org.objectweb.asm.tree.ClassNode; public class ProcyonDecompiler implements Decompiler { private final SourceSettings settings; @@ -26,9 +25,21 @@ public class ProcyonDecompiler implements Decompiler { private final MetadataSystem metadataSystem; public ProcyonDecompiler(ClassProvider classProvider, SourceSettings settings) { - ITypeLoader typeLoader = new SynchronizedTypeLoader(new CompiledSourceTypeLoader(classProvider)); + ITypeLoader typeLoader = (name, buffer) -> { + ClassNode node = classProvider.get(name); - metadataSystem = new NoRetryMetadataSystem(typeLoader); + if (node == null) { + return false; + } + + byte[] data = AsmUtil.nodeToBytes(node); + buffer.reset(data.length); + System.arraycopy(data, 0, buffer.array(), buffer.position(), data.length); + buffer.position(0); + return true; + }; + + metadataSystem = new MetadataSystem(typeLoader); metadataSystem.setEagerMethodLoadingEnabled(true); decompilerSettings = DecompilerSettings.javaDefaults(); diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingClasspathTypeLoader.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingClasspathTypeLoader.java deleted file mode 100644 index e702956..0000000 --- a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingClasspathTypeLoader.java +++ /dev/null @@ -1,33 +0,0 @@ -package cuchaz.enigma.source.procyon.typeloader; - -import com.strobel.assembler.metadata.Buffer; -import com.strobel.assembler.metadata.ClasspathTypeLoader; -import com.strobel.assembler.metadata.ITypeLoader; - -/** - * Caching version of {@link ClasspathTypeLoader} - */ -public class CachingClasspathTypeLoader extends CachingTypeLoader { - private static ITypeLoader extraClassPathLoader = null; - - public static void setExtraClassPathLoader(ITypeLoader loader){ - extraClassPathLoader = loader; - } - - private final ITypeLoader classpathLoader = new ClasspathTypeLoader(); - - @Override - protected byte[] doLoad(String className) { - Buffer parentBuf = new Buffer(); - if (classpathLoader.tryLoadType(className, parentBuf)) { - return parentBuf.array(); - } - if (extraClassPathLoader != null){ - parentBuf.reset(); - if (extraClassPathLoader.tryLoadType(className, parentBuf)){ - return parentBuf.array(); - } - } - return EMPTY_ARRAY;//need to return *something* as null means no store - } -} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingTypeLoader.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingTypeLoader.java deleted file mode 100644 index 5be5ddd..0000000 --- a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CachingTypeLoader.java +++ /dev/null @@ -1,38 +0,0 @@ -package cuchaz.enigma.source.procyon.typeloader; - -import com.google.common.collect.Maps; -import com.strobel.assembler.metadata.Buffer; -import com.strobel.assembler.metadata.ITypeLoader; - -import java.util.Map; - -/** - * Common cache functions - */ -public abstract class CachingTypeLoader implements ITypeLoader { - protected static final byte[] EMPTY_ARRAY = {}; - - private final Map cache = Maps.newHashMap(); - - protected abstract byte[] doLoad(String className); - - @Override - public boolean tryLoadType(String className, Buffer out) { - - // check the cache - byte[] data = this.cache.computeIfAbsent(className, this::doLoad); - - if (data == EMPTY_ARRAY) { - return false; - } - - out.reset(data.length); - System.arraycopy(data, 0, out.array(), out.position(), data.length); - out.position(0); - return true; - } - - public void clearCache() { - this.cache.clear(); - } -} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CompiledSourceTypeLoader.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CompiledSourceTypeLoader.java deleted file mode 100644 index e703d3b..0000000 --- a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/CompiledSourceTypeLoader.java +++ /dev/null @@ -1,140 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2015 Jeff Martin. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Lesser General Public - * License v3.0 which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/lgpl.html - *

- * Contributors: - * Jeff Martin - initial API and implementation - ******************************************************************************/ - -package cuchaz.enigma.source.procyon.typeloader; - -import com.google.common.collect.Lists; -import com.strobel.assembler.metadata.Buffer; -import com.strobel.assembler.metadata.ITypeLoader; -import cuchaz.enigma.ClassProvider; -import cuchaz.enigma.translation.representation.entry.ClassEntry; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; - -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.function.Function; - -public class CompiledSourceTypeLoader extends CachingTypeLoader { - //Store one instance as the classpath shouldn't change during load - private static final ITypeLoader CLASSPATH_TYPE_LOADER = new CachingClasspathTypeLoader(); - - private final ClassProvider compiledSource; - private final LinkedList> visitors = new LinkedList<>(); - - public CompiledSourceTypeLoader(ClassProvider compiledSource) { - this.compiledSource = compiledSource; - } - - public void addVisitor(Function visitor) { - this.visitors.addFirst(visitor); - } - - @Override - protected byte[] doLoad(String className) { - byte[] data = loadType(className); - if (data == null) { - return loadClasspath(className); - } - - return data; - } - - private byte[] loadClasspath(String name) { - Buffer parentBuf = new Buffer(); - if (CLASSPATH_TYPE_LOADER.tryLoadType(name, parentBuf)) { - return parentBuf.array(); - } - return EMPTY_ARRAY; - } - - private byte[] loadType(String className) { - ClassEntry entry = new ClassEntry(className); - - // find the class in the jar - ClassNode node = findClassNode(entry); - if (node == null) { - // couldn't find it - return null; - } - - removeRedundantClassCalls(node); - - ClassWriter writer = new ClassWriter(0); - - ClassVisitor visitor = writer; - for (Function visitorFunction : this.visitors) { - visitor = visitorFunction.apply(visitor); - } - - node.accept(visitor); - - // we have a transformed class! - return writer.toByteArray(); - } - - private void removeRedundantClassCalls(ClassNode node) { - // remove .getClass() calls that are seemingly injected - // DUP - // INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class; - // POP - for (MethodNode methodNode : node.methods) { - AbstractInsnNode insnNode = methodNode.instructions.getFirst(); - while (insnNode != null) { - if (insnNode instanceof MethodInsnNode && insnNode.getOpcode() == Opcodes.INVOKEVIRTUAL) { - MethodInsnNode methodInsnNode = (MethodInsnNode) insnNode; - if (methodInsnNode.name.equals("getClass") && methodInsnNode.owner.equals("java/lang/Object") && methodInsnNode.desc.equals("()Ljava/lang/Class;")) { - AbstractInsnNode previous = methodInsnNode.getPrevious(); - AbstractInsnNode next = methodInsnNode.getNext(); - if (previous.getOpcode() == Opcodes.DUP && next.getOpcode() == Opcodes.POP) { - insnNode = previous.getPrevious();//reset the iterator so it gets the new next instruction - methodNode.instructions.remove(previous); - methodNode.instructions.remove(methodInsnNode); - methodNode.instructions.remove(next); - } - } - } - insnNode = insnNode.getNext(); - } - } - } - - private ClassNode findClassNode(ClassEntry entry) { - // try to find the class in the jar - for (String className : getClassNamesToTry(entry)) { - ClassNode node = compiledSource.getClassNode(className); - if (node != null) { - return node; - } - } - - // didn't find it ;_; - return null; - } - - private Collection getClassNamesToTry(ClassEntry entry) { - List classNamesToTry = Lists.newArrayList(); - classNamesToTry.add(entry.getFullName()); - - ClassEntry outerClass = entry.getOuterClass(); - if (outerClass != null) { - classNamesToTry.addAll(getClassNamesToTry(outerClass)); - } - - return classNamesToTry; - } -} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/NoRetryMetadataSystem.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/NoRetryMetadataSystem.java deleted file mode 100644 index c4732b0..0000000 --- a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/NoRetryMetadataSystem.java +++ /dev/null @@ -1,38 +0,0 @@ -package cuchaz.enigma.source.procyon.typeloader; - -import com.strobel.assembler.metadata.ITypeLoader; -import com.strobel.assembler.metadata.MetadataSystem; -import com.strobel.assembler.metadata.TypeDefinition; -import com.strobel.assembler.metadata.TypeReference; - -import java.util.Collections; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -public final class NoRetryMetadataSystem extends MetadataSystem { - private final Set failedTypes = Collections.newSetFromMap(new ConcurrentHashMap<>()); - - public NoRetryMetadataSystem(final ITypeLoader typeLoader) { - super(typeLoader); - } - - @Override - protected synchronized TypeDefinition resolveType(final String descriptor, final boolean mightBePrimitive) { - if (failedTypes.contains(descriptor)) { - return null; - } - - final TypeDefinition result = super.resolveType(descriptor, mightBePrimitive); - - if (result == null) { - failedTypes.add(descriptor); - } - - return result; - } - - @Override - public synchronized TypeDefinition resolve(final TypeReference type) { - return super.resolve(type); - } -} diff --git a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/SynchronizedTypeLoader.java b/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/SynchronizedTypeLoader.java deleted file mode 100644 index 86c6ecc..0000000 --- a/enigma/src/main/java/cuchaz/enigma/source/procyon/typeloader/SynchronizedTypeLoader.java +++ /dev/null @@ -1,20 +0,0 @@ -package cuchaz.enigma.source.procyon.typeloader; - -import com.strobel.assembler.metadata.Buffer; -import com.strobel.assembler.metadata.ITypeLoader; - -/** - * Typeloader with synchronized tryLoadType method - */ -public class SynchronizedTypeLoader implements ITypeLoader { - private final ITypeLoader delegate; - - public SynchronizedTypeLoader(ITypeLoader delegate) { - this.delegate = delegate; - } - - @Override - public synchronized boolean tryLoadType(String internalName, Buffer buffer) { - return delegate.tryLoadType(internalName, buffer); - } -} diff --git a/enigma/src/main/java/cuchaz/enigma/utils/AsmUtil.java b/enigma/src/main/java/cuchaz/enigma/utils/AsmUtil.java new file mode 100644 index 0000000..7d34b02 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/AsmUtil.java @@ -0,0 +1,20 @@ +package cuchaz.enigma.utils; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.ClassNode; + +public class AsmUtil { + public static byte[] nodeToBytes(ClassNode node) { + ClassWriter w = new ClassWriter(0); + node.accept(w); + return w.toByteArray(); + } + + public static ClassNode bytesToNode(byte[] bytes) { + ClassReader r = new ClassReader(bytes); + ClassNode node = new ClassNode(); + r.accept(node, 0); + return node; + } +} -- cgit v1.2.3