cuchaz.enigma.Deobfuscator.java Source code

Java tutorial

Introduction

Here is the source code for cuchaz.enigma.Deobfuscator.java

Source

/*******************************************************************************
 * 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
 * <p>
 * Contributors:
 * Jeff Martin - initial API and implementation
 ******************************************************************************/

package cuchaz.enigma;

import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
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 com.strobel.decompiler.DecompilerContext;
import com.strobel.decompiler.DecompilerSettings;
import com.strobel.decompiler.PlainTextOutput;
import com.strobel.decompiler.languages.java.JavaOutputVisitor;
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 com.strobel.decompiler.languages.java.ast.transforms.IAstTransform;
import cuchaz.enigma.analysis.*;
import cuchaz.enigma.bytecode.ClassProtectifier;
import cuchaz.enigma.bytecode.ClassPublifier;
import cuchaz.enigma.mapping.*;
import cuchaz.enigma.mapping.entry.*;
import cuchaz.enigma.throwables.IllegalNameException;
import cuchaz.enigma.utils.Utils;
import oml.ast.transformers.InvalidIdentifierFix;
import oml.ast.transformers.Java8Generics;
import oml.ast.transformers.ObfuscatedEnumSwitchRewriterTransform;
import oml.ast.transformers.RemoveObjectCasts;
import oml.ast.transformers.VarargsFixer;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;

import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

public class Deobfuscator {

    private final ReferencedEntryPool entryPool = new ReferencedEntryPool();
    private final ParsedJar parsedJar;
    private final DecompilerSettings settings;
    private final JarIndex jarIndex;
    private final MappingsRenamer renamer;
    private final Map<TranslationDirection, Translator> translatorCache;
    private Mappings mappings;

    public Deobfuscator(ParsedJar jar) {
        this.parsedJar = jar;

        // build the jar index
        this.jarIndex = new JarIndex(entryPool);
        this.jarIndex.indexJar(this.parsedJar, true);

        // config the decompiler
        this.settings = DecompilerSettings.javaDefaults();
        this.settings.setMergeVariables(Utils.getSystemPropertyAsBoolean("enigma.mergeVariables", true));
        this.settings
                .setForceExplicitImports(Utils.getSystemPropertyAsBoolean("enigma.forceExplicitImports", true));
        this.settings.setForceExplicitTypeArguments(
                Utils.getSystemPropertyAsBoolean("enigma.forceExplicitTypeArguments", true));
        // DEBUG
        this.settings
                .setShowDebugLineNumbers(Utils.getSystemPropertyAsBoolean("enigma.showDebugLineNumbers", false));
        this.settings
                .setShowSyntheticMembers(Utils.getSystemPropertyAsBoolean("enigma.showSyntheticMembers", false));

        // init defaults
        this.translatorCache = Maps.newTreeMap();
        this.renamer = new MappingsRenamer(this.jarIndex, null, this.entryPool);
        // init mappings
        setMappings(new Mappings());
    }

    public Deobfuscator(JarFile jar) throws IOException {
        this(new ParsedJar(jar));
    }

    public ParsedJar getJar() {
        return this.parsedJar;
    }

    public JarIndex getJarIndex() {
        return this.jarIndex;
    }

    public Mappings getMappings() {
        return this.mappings;
    }

    public void setMappings(Mappings val) {
        setMappings(val, true);
    }

    public void setMappings(Mappings val, boolean warnAboutDrops) {
        if (val == null) {
            val = new Mappings();
        }

        // drop mappings that don't match the jar
        MappingsChecker checker = new MappingsChecker(this.jarIndex);
        checker.dropBrokenMappings(val);
        if (warnAboutDrops) {
            for (Map.Entry<ClassEntry, ClassMapping> mapping : checker.getDroppedClassMappings().entrySet()) {
                System.out.println("WARNING: Couldn't find class entry " + mapping.getKey() + " ("
                        + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped.");
            }
            for (Map.Entry<ClassEntry, ClassMapping> mapping : checker.getDroppedInnerClassMappings().entrySet()) {
                System.out.println("WARNING: Couldn't find inner class entry " + mapping.getKey() + " ("
                        + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped.");
            }
            for (Map.Entry<FieldEntry, FieldMapping> mapping : checker.getDroppedFieldMappings().entrySet()) {
                System.out.println("WARNING: Couldn't find field entry " + mapping.getKey() + " ("
                        + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped.");
            }
            for (Map.Entry<MethodEntry, MethodMapping> mapping : checker.getDroppedMethodMappings().entrySet()) {
                System.out.println("WARNING: Couldn't find behavior entry " + mapping.getKey() + " ("
                        + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped.");
            }
        }

        this.mappings = val;
        this.renamer.setMappings(mappings);
        this.translatorCache.clear();
    }

    public Translator getTranslator(TranslationDirection direction) {
        return this.translatorCache.computeIfAbsent(direction,
                k -> this.mappings.getTranslator(direction, this.jarIndex.getTranslationIndex()));
    }

    public void getSeparatedClasses(List<ClassEntry> obfClasses, List<ClassEntry> deobfClasses) {
        for (ClassEntry obfClassEntry : this.jarIndex.getObfClassEntries()) {
            // skip inner classes
            if (obfClassEntry.isInnerClass()) {
                continue;
            }

            // separate the classes
            ClassEntry deobfClassEntry = deobfuscateEntry(obfClassEntry);
            if (!deobfClassEntry.equals(obfClassEntry)) {
                // if the class has a mapping, clearly it's deobfuscated
                deobfClasses.add(deobfClassEntry);
            } else if (obfClassEntry.getPackageName() != null) {
                // also call it deobufscated if it's not in the none package
                deobfClasses.add(obfClassEntry);
            } else {
                // otherwise, assume it's still obfuscated
                obfClasses.add(obfClassEntry);
            }
        }
    }

    public TranslatingTypeLoader createTypeLoader() {
        return new TranslatingTypeLoader(this.parsedJar, this.jarIndex, this.entryPool,
                getTranslator(TranslationDirection.OBFUSCATING), getTranslator(TranslationDirection.DEOBFUSCATING));
    }

    public CompilationUnit getSourceTree(String className) {
        return getSourceTree(className, createTypeLoader());
    }

    public CompilationUnit getSourceTree(String className, ITranslatingTypeLoader loader) {
        return getSourceTree(className, loader, new NoRetryMetadataSystem(loader));
    }

    public CompilationUnit getSourceTree(String className, ITranslatingTypeLoader loader,
            MetadataSystem metadataSystem) {

        // we don't know if this class name is obfuscated or deobfuscated
        // we need to tell the decompiler the deobfuscated name so it doesn't get freaked out
        // the decompiler only sees classes after deobfuscation, so we need to load it by the deobfuscated name if there is one

        // first, assume class name is deobf
        String deobfClassName = className;

        // if it wasn't actually deobf, then we can find a mapping for it and get the deobf name
        ClassMapping classMapping = this.mappings.getClassByObf(className);
        if (classMapping != null && classMapping.getDeobfName() != null) {
            deobfClassName = classMapping.getDeobfName();
        }

        // set the desc loader
        this.settings.setTypeLoader(loader);

        // see if procyon can find the desc
        TypeReference type = metadataSystem.lookupType(deobfClassName);
        if (type == null) {
            throw new Error(String.format("Unable to find desc: %s (deobf: %s)\nTried class names: %s", className,
                    deobfClassName, loader.getClassNamesToTry(deobfClassName)));
        }
        TypeDefinition resolvedType = type.resolve();

        // decompile it!
        DecompilerContext context = new DecompilerContext();
        context.setCurrentType(resolvedType);
        context.setSettings(this.settings);
        AstBuilder builder = new AstBuilder(context);
        builder.addType(resolvedType);
        builder.runTransformations(null);
        runCustomTransforms(builder, context);
        return builder.getCompilationUnit();
    }

    public SourceIndex getSourceIndex(CompilationUnit sourceTree, String source) {
        return getSourceIndex(sourceTree, source, null);
    }

    public SourceIndex getSourceIndex(CompilationUnit sourceTree, String source, Boolean ignoreBadTokens) {

        // build the source index
        SourceIndex index;
        if (ignoreBadTokens != null) {
            index = new SourceIndex(source, ignoreBadTokens);
        } else {
            index = new SourceIndex(source);
        }
        sourceTree.acceptVisitor(new SourceIndexVisitor(entryPool), index);

        // DEBUG
        // sourceTree.acceptVisitor( new TreeDumpVisitor( new File( "tree.txt" ) ), null );

        // resolve all the classes in the source references
        for (Token token : index.referenceTokens()) {
            EntryReference<Entry, Entry> deobfReference = index.getDeobfReference(token);

            // get the obfuscated entry
            Entry obfEntry = obfuscateEntry(deobfReference.entry);

            // try to resolve the class
            ClassEntry resolvedObfClassEntry = this.jarIndex.getTranslationIndex().resolveEntryOwner(obfEntry);
            if (resolvedObfClassEntry != null && !resolvedObfClassEntry.equals(obfEntry.getOwnerClassEntry())) {
                // change the class of the entry
                obfEntry = obfEntry.updateOwnership(resolvedObfClassEntry);

                // save the new deobfuscated reference
                deobfReference.entry = deobfuscateEntry(obfEntry);
                index.replaceDeobfReference(token, deobfReference);
            }

            // DEBUG
            // System.out.println( token + " -> " + reference + " -> " + index.getReferenceToken( reference ) );
        }

        return index;
    }

    public String getSource(CompilationUnit sourceTree) {
        // render the AST into source
        StringWriter buf = new StringWriter();
        sourceTree.acceptVisitor(new InsertParenthesesVisitor(), null);
        sourceTree.acceptVisitor(new JavaOutputVisitor(new PlainTextOutput(buf), this.settings), null);
        return buf.toString();
    }

    public void writeSources(File dirOut, ProgressListener progress) {
        // get the classes to decompile
        Set<ClassEntry> classEntries = Sets.newHashSet();
        for (ClassEntry obfClassEntry : this.jarIndex.getObfClassEntries()) {
            // skip inner classes
            if (obfClassEntry.isInnerClass()) {
                continue;
            }

            classEntries.add(obfClassEntry);
        }

        if (progress != null) {
            progress.init(classEntries.size(), "Decompiling classes...");
        }

        //create a common instance outside the loop as mappings shouldn't be changing while this is happening
        //synchronized to make sure the parallelStream doesn't CME with the cache
        ITranslatingTypeLoader typeLoader = new SynchronizedTypeLoader(createTypeLoader());

        MetadataSystem metadataSystem = new NoRetryMetadataSystem(typeLoader);
        metadataSystem.setEagerMethodLoadingEnabled(true);//ensures methods are loaded on classload and prevents race conditions

        // DEOBFUSCATE ALL THE THINGS!! @_@
        Stopwatch stopwatch = Stopwatch.createStarted();
        AtomicInteger count = new AtomicInteger();
        classEntries.parallelStream().forEach(obfClassEntry -> {
            ClassEntry deobfClassEntry = deobfuscateEntry(new ClassEntry(obfClassEntry));
            if (progress != null) {
                progress.onProgress(count.getAndIncrement(), deobfClassEntry.toString());
            }

            try {
                // get the source
                CompilationUnit sourceTree = getSourceTree(obfClassEntry.getName(), typeLoader, metadataSystem);

                // write the file
                File file = new File(dirOut, deobfClassEntry.getName().replace('.', '/') + ".java");
                file.getParentFile().mkdirs();
                try (Writer writer = new BufferedWriter(new FileWriter(file))) {
                    sourceTree.acceptVisitor(new InsertParenthesesVisitor(), null);
                    sourceTree.acceptVisitor(new JavaOutputVisitor(new PlainTextOutput(writer), settings), null);
                }
            } catch (Throwable t) {
                // don't crash the whole world here, just log the error and keep going
                // TODO: set up logback via log4j
                System.err.println("Unable to deobfuscate class " + deobfClassEntry + " (" + obfClassEntry + ")");
                t.printStackTrace(System.err);
            }
        });
        stopwatch.stop();
        System.out.println("writeSources Done in : " + stopwatch.toString());
        if (progress != null) {
            progress.onProgress(count.get(), "Done:");
        }
    }

    private void addAllPotentialAncestors(Set<ClassEntry> classEntries, ClassEntry classObfEntry) {
        for (ClassEntry interfaceEntry : jarIndex.getTranslationIndex().getInterfaces(classObfEntry)) {
            if (classEntries.add(interfaceEntry)) {
                addAllPotentialAncestors(classEntries, interfaceEntry);
            }
        }

        ClassEntry superClassEntry = jarIndex.getTranslationIndex().getSuperclass(classObfEntry);
        if (superClassEntry != null && classEntries.add(superClassEntry)) {
            addAllPotentialAncestors(classEntries, superClassEntry);
        }
    }

    public boolean isMethodProvider(MethodEntry methodEntry) {
        Set<ClassEntry> classEntries = new HashSet<>();
        addAllPotentialAncestors(classEntries, methodEntry.getOwnerClassEntry());

        for (ClassEntry parentEntry : classEntries) {
            MethodEntry ancestorMethodEntry = entryPool.getMethod(parentEntry, methodEntry.getName(),
                    methodEntry.getDesc());
            if (jarIndex.containsObfMethod(ancestorMethodEntry)) {
                return false;
            }
        }

        return true;
    }

    @Deprecated
    public boolean isMethodProvider(ClassEntry classObfEntry, MethodEntry methodEntry) {
        Set<ClassEntry> classEntries = new HashSet<>();
        addAllPotentialAncestors(classEntries, classObfEntry);

        for (ClassEntry parentEntry : classEntries) {
            MethodEntry ancestorMethodEntry = entryPool.getMethod(parentEntry, methodEntry.getName(),
                    methodEntry.getDesc());
            if (jarIndex.containsObfMethod(ancestorMethodEntry)) {
                return false;
            }
        }

        return true;
    }

    public void rebuildMethodNames(ProgressListener progress) {
        final AtomicInteger i = new AtomicInteger();
        Map<ClassMapping, Map<Entry, String>> renameClassMap = new ConcurrentHashMap<>();

        progress.init(getMappings().classes().size() * 3, "Rebuilding method names");

        Lists.newArrayList(getMappings().classes()).parallelStream().forEach(classMapping -> {
            progress.onProgress(i.getAndIncrement(), classMapping.getDeobfName());
            rebuildMethodNames(classMapping, renameClassMap);
        });

        renameClassMap.entrySet().stream().forEach(renameClassMapEntry -> {
            progress.onProgress(i.getAndIncrement(), renameClassMapEntry.getKey().getDeobfName());
            for (Map.Entry<Entry, String> entry : renameClassMapEntry.getValue().entrySet()) {
                Entry obfEntry = entry.getKey();

                removeMapping(obfEntry, false);
            }
        });

        translatorCache.clear();

        renameClassMap.entrySet().stream().forEach(renameClassMapEntry -> {
            progress.onProgress(i.getAndIncrement(), renameClassMapEntry.getKey().getDeobfName());

            for (Map.Entry<Entry, String> entry : renameClassMapEntry.getValue().entrySet()) {
                Entry obfEntry = entry.getKey();
                String name = entry.getValue();

                if (name != null) {
                    try {
                        rename(obfEntry, name);
                    } catch (IllegalNameException exception) {
                        System.out.println("WARNING: " + exception.getMessage());
                    }
                }
            }
        });
    }

    private void rebuildMethodNames(ClassMapping classMapping,
            Map<ClassMapping, Map<Entry, String>> renameClassMap) {
        Map<Entry, String> renameEntries = new HashMap<>();

        for (MethodMapping methodMapping : Lists.newArrayList(classMapping.methods())) {
            ClassEntry classObfEntry = classMapping.getObfEntry();
            MethodEntry obfEntry = methodMapping.getObfEntry(classObfEntry);
            boolean isProvider = isMethodProvider(obfEntry);

            if (hasDeobfuscatedName(obfEntry)
                    && !(methodMapping.getDeobfName().equals(methodMapping.getObfName()))) {
                renameEntries.put(obfEntry, isProvider ? methodMapping.getDeobfName() : null);
            }

            if (isProvider) {
                for (LocalVariableMapping localVariableMapping : methodMapping.arguments()) {
                    Entry argObfEntry = localVariableMapping.getObfEntry(obfEntry);
                    if (hasDeobfuscatedName(argObfEntry)) {
                        renameEntries.put(argObfEntry, deobfuscateEntry(argObfEntry).getName());
                    }
                }
            }
        }

        classMapping.markDirty();
        renameClassMap.put(classMapping, renameEntries);
        for (ClassMapping innerClass : classMapping.innerClasses()) {
            rebuildMethodNames(innerClass, renameClassMap);
        }
    }

    public void writeJar(File out, ProgressListener progress) {
        transformJar(out, progress, createTypeLoader()::transformInto);
    }

    public void protectifyJar(File out, ProgressListener progress) {
        transformJar(out, progress, (node, writer) -> {
            node.accept(new ClassProtectifier(Opcodes.ASM5, writer));
            return node.name;
        });
    }

    public void publifyJar(File out, ProgressListener progress) {
        transformJar(out, progress, (node, writer) -> {
            node.accept(new ClassPublifier(Opcodes.ASM5, writer));
            return node.name;
        });
    }

    public void transformJar(File out, ProgressListener progress, ClassTransformer transformer) {
        try (JarOutputStream outJar = new JarOutputStream(new FileOutputStream(out))) {
            if (progress != null) {
                progress.init(parsedJar.getClassCount(), "Transforming classes...");
            }

            AtomicInteger i = new AtomicInteger();
            parsedJar.visit(node -> {
                if (progress != null) {
                    progress.onProgress(i.getAndIncrement(), node.name);
                }

                try {
                    ClassWriter writer = new ClassWriter(0);
                    String transformedName = transformer.transform(node, writer);
                    outJar.putNextEntry(new JarEntry(transformedName.replace('.', '/') + ".class"));
                    outJar.write(writer.toByteArray());
                    outJar.closeEntry();
                } catch (Throwable t) {
                    throw new Error("Unable to transform class " + node.name, t);
                }
            });

            if (progress != null) {
                progress.onProgress(i.get(), "Done!");
            }
        } catch (IOException ex) {
            throw new Error("Unable to write to Jar file!");
        }
    }

    public <T extends Entry> T obfuscateEntry(T deobfEntry) {
        if (deobfEntry == null) {
            return null;
        }
        T translatedEntry = getTranslator(TranslationDirection.OBFUSCATING).getTranslatedEntry(deobfEntry);
        if (translatedEntry == null) {
            return deobfEntry;
        }
        return translatedEntry;
    }

    public <T extends Entry> T deobfuscateEntry(T obfEntry) {
        if (obfEntry == null) {
            return null;
        }
        T translatedEntry = getTranslator(TranslationDirection.DEOBFUSCATING).getTranslatedEntry(obfEntry);
        if (translatedEntry == null) {
            return obfEntry;
        }
        return translatedEntry;
    }

    public <E extends Entry, C extends Entry> EntryReference<E, C> obfuscateReference(
            EntryReference<E, C> deobfReference) {
        if (deobfReference == null) {
            return null;
        }
        return new EntryReference<>(obfuscateEntry(deobfReference.entry), obfuscateEntry(deobfReference.context),
                deobfReference);
    }

    public <E extends Entry, C extends Entry> EntryReference<E, C> deobfuscateReference(
            EntryReference<E, C> obfReference) {
        if (obfReference == null) {
            return null;
        }
        return new EntryReference<>(deobfuscateEntry(obfReference.entry), deobfuscateEntry(obfReference.context),
                obfReference);
    }

    public boolean isObfuscatedIdentifier(Entry obfEntry) {
        return isObfuscatedIdentifier(obfEntry, false);
    }

    public boolean isObfuscatedIdentifier(Entry obfEntry, boolean hack) {

        if (obfEntry instanceof MethodEntry) {

            // HACKHACK: Object methods are not obfuscated identifiers
            MethodEntry obfMethodEntry = (MethodEntry) obfEntry;
            String name = obfMethodEntry.getName();
            String sig = obfMethodEntry.getDesc().toString();
            if (name.equals("clone") && sig.equals("()Ljava/lang/Object;")) {
                return false;
            } else if (name.equals("equals") && sig.equals("(Ljava/lang/Object;)Z")) {
                return false;
            } else if (name.equals("finalize") && sig.equals("()V")) {
                return false;
            } else if (name.equals("getClass") && sig.equals("()Ljava/lang/Class;")) {
                return false;
            } else if (name.equals("hashCode") && sig.equals("()I")) {
                return false;
            } else if (name.equals("notify") && sig.equals("()V")) {
                return false;
            } else if (name.equals("notifyAll") && sig.equals("()V")) {
                return false;
            } else if (name.equals("toString") && sig.equals("()Ljava/lang/String;")) {
                return false;
            } else if (name.equals("wait") && sig.equals("()V")) {
                return false;
            } else if (name.equals("wait") && sig.equals("(J)V")) {
                return false;
            } else if (name.equals("wait") && sig.equals("(JI)V")) {
                return false;
            }

            // FIXME: HACK EVEN MORE HACK!
            if (hack && this.jarIndex.containsObfEntry(obfEntry.getOwnerClassEntry()))
                return true;
        }

        return this.jarIndex.containsObfEntry(obfEntry);
    }

    public boolean isRenameable(EntryReference<Entry, Entry> obfReference, boolean activeHack) {
        return obfReference.isNamed() && isObfuscatedIdentifier(obfReference.getNameableEntry(), activeHack);
    }

    public boolean isRenameable(EntryReference<Entry, Entry> obfReference) {
        return isRenameable(obfReference, false);
    }

    public boolean hasDeobfuscatedName(Entry obfEntry) {
        Translator translator = getTranslator(TranslationDirection.DEOBFUSCATING);
        if (obfEntry instanceof ClassEntry) {
            ClassEntry obfClass = (ClassEntry) obfEntry;
            List<ClassMapping> mappingChain = this.mappings.getClassMappingChain(obfClass);
            ClassMapping classMapping = mappingChain.get(mappingChain.size() - 1);
            return classMapping != null && classMapping.getDeobfName() != null;
        } else if (obfEntry instanceof FieldEntry) {
            return translator.hasFieldMapping((FieldEntry) obfEntry);
        } else if (obfEntry instanceof MethodEntry) {
            MethodEntry methodEntry = (MethodEntry) obfEntry;
            if (methodEntry.isConstructor()) {
                return false;
            }
            return translator.hasMethodMapping(methodEntry);
        } else if (obfEntry instanceof LocalVariableEntry) {
            return translator.hasLocalVariableMapping((LocalVariableEntry) obfEntry);
        } else {
            throw new Error("Unknown entry desc: " + obfEntry.getClass().getName());
        }
    }

    public void rename(Entry obfEntry, String newName) {
        rename(obfEntry, newName, true);
    }

    // NOTE: these methods are a bit messy... oh well

    public void rename(Entry obfEntry, String newName, boolean clearCache) {
        if (obfEntry instanceof ClassEntry) {
            this.renamer.setClassName((ClassEntry) obfEntry, newName);
        } else if (obfEntry instanceof FieldEntry) {
            this.renamer.setFieldName((FieldEntry) obfEntry, newName);
        } else if (obfEntry instanceof MethodEntry) {
            if (((MethodEntry) obfEntry).isConstructor()) {
                throw new IllegalArgumentException("Cannot rename constructors");
            }
            this.renamer.setMethodTreeName((MethodEntry) obfEntry, newName);
        } else if (obfEntry instanceof LocalVariableEntry) {
            this.renamer.setLocalVariableTreeName((LocalVariableEntry) obfEntry, newName);
        } else {
            throw new Error("Unknown entry desc: " + obfEntry.getClass().getName());
        }

        // clear caches
        if (clearCache)
            this.translatorCache.clear();
    }

    public void removeMapping(Entry obfEntry) {
        removeMapping(obfEntry, true);
    }

    public void removeMapping(Entry obfEntry, boolean clearCache) {
        if (obfEntry instanceof ClassEntry) {
            this.renamer.removeClassMapping((ClassEntry) obfEntry);
        } else if (obfEntry instanceof FieldEntry) {
            this.renamer.removeFieldMapping((FieldEntry) obfEntry);
        } else if (obfEntry instanceof MethodEntry) {
            if (((MethodEntry) obfEntry).isConstructor()) {
                throw new IllegalArgumentException("Cannot rename constructors");
            }
            this.renamer.removeMethodTreeMapping((MethodEntry) obfEntry);
        } else if (obfEntry instanceof LocalVariableEntry) {
            this.renamer.removeLocalVariableMapping((LocalVariableEntry) obfEntry);
        } else {
            throw new Error("Unknown entry desc: " + obfEntry);
        }

        // clear caches
        if (clearCache)
            this.translatorCache.clear();
    }

    public void markAsDeobfuscated(Entry obfEntry) {
        markAsDeobfuscated(obfEntry, true);
    }

    public void markAsDeobfuscated(Entry obfEntry, boolean clearCache) {
        if (obfEntry instanceof ClassEntry) {
            this.renamer.markClassAsDeobfuscated((ClassEntry) obfEntry);
        } else if (obfEntry instanceof FieldEntry) {
            this.renamer.markFieldAsDeobfuscated((FieldEntry) obfEntry);
        } else if (obfEntry instanceof MethodEntry) {
            MethodEntry methodEntry = (MethodEntry) obfEntry;
            if (methodEntry.isConstructor()) {
                throw new IllegalArgumentException("Cannot rename constructors");
            }
            this.renamer.markMethodTreeAsDeobfuscated(methodEntry);
        } else if (obfEntry instanceof LocalVariableEntry) {
            this.renamer.markArgumentAsDeobfuscated((LocalVariableEntry) obfEntry);
        } else {
            throw new Error("Unknown entry desc: " + obfEntry);
        }

        // clear caches
        if (clearCache)
            this.translatorCache.clear();
    }

    public void changeModifier(Entry entry, Mappings.EntryModifier modifierEntry) {
        Entry obfEntry = obfuscateEntry(entry);
        if (obfEntry instanceof ClassEntry)
            this.renamer.setClassModifier((ClassEntry) obfEntry, modifierEntry);
        else if (obfEntry instanceof FieldEntry)
            this.renamer.setFieldModifier((FieldEntry) obfEntry, modifierEntry);
        else if (obfEntry instanceof MethodEntry)
            this.renamer.setMethodModifier((MethodEntry) obfEntry, modifierEntry);
        else
            throw new Error("Unknown entry desc: " + obfEntry);
    }

    public Mappings.EntryModifier getModifier(Entry obfEntry) {
        Entry entry = obfuscateEntry(obfEntry);
        if (entry != null)
            obfEntry = entry;
        if (obfEntry instanceof ClassEntry)
            return this.renamer.getClassModifier((ClassEntry) obfEntry);
        else if (obfEntry instanceof FieldEntry)
            return this.renamer.getFieldModifier((FieldEntry) obfEntry);
        else if (obfEntry instanceof MethodEntry)
            return this.renamer.getMethodModfifier((MethodEntry) obfEntry);
        else
            throw new Error("Unknown entry desc: " + obfEntry);
    }

    public static void runCustomTransforms(AstBuilder builder, DecompilerContext context) {
        List<IAstTransform> transformers = Arrays.asList(new ObfuscatedEnumSwitchRewriterTransform(context),
                new VarargsFixer(context), new RemoveObjectCasts(context), new Java8Generics(),
                new InvalidIdentifierFix());
        for (IAstTransform transform : transformers) {
            transform.run(builder.getCompilationUnit());
        }
    }

    public interface ProgressListener {
        void init(int totalWork, String title);

        void onProgress(int numDone, String message);
    }

    public interface ClassTransformer {
        String transform(ClassNode node, ClassWriter writer);
    }

    public static class NoRetryMetadataSystem extends MetadataSystem {
        private final Set<String> _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;
        }

        public synchronized TypeDefinition resolve(final TypeReference type) {
            return super.resolve(type);
        }
    }
}