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

org.jboss.windup.decompiler.procyon.ProcyonDecompiler Maven / Gradle / Ivy

There is a newer version: 6.3.9.Final
Show newest version
package org.jboss.windup.decompiler.procyon;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;

import com.strobel.assembler.metadata.WindupClasspathTypeLoader;
import com.strobel.assembler.metadata.WindupMetadataSystem;
import org.apache.commons.lang3.StringUtils;
import org.jboss.windup.decompiler.api.ClassDecompileRequest;
import org.jboss.windup.decompiler.api.DecompilationException;
import org.jboss.windup.decompiler.api.DecompilationFailure;
import org.jboss.windup.decompiler.api.DecompilationListener;
import org.jboss.windup.decompiler.api.DecompilationResult;
import org.jboss.windup.decompiler.decompiler.AbstractDecompiler;
import org.jboss.windup.decompiler.util.Filter;
import org.jboss.windup.util.Checks;
import org.jboss.windup.util.ExecutionStatistics;
import org.jboss.windup.util.exception.WindupException;

import com.strobel.assembler.InputTypeLoader;
import com.strobel.assembler.metadata.ClasspathTypeLoader;
import com.strobel.assembler.metadata.CompositeTypeLoader;
import com.strobel.assembler.metadata.IMetadataResolver;
import com.strobel.assembler.metadata.ITypeLoader;
import com.strobel.assembler.metadata.MetadataParser;
import com.strobel.assembler.metadata.WindupMetadataSystem;
import com.strobel.assembler.metadata.NoRetryMetadataSystem;
import com.strobel.assembler.metadata.TypeDefinition;
import com.strobel.assembler.metadata.TypeReference;
import com.strobel.decompiler.DecompilationOptions;
import com.strobel.decompiler.DecompilerSettings;
import com.strobel.decompiler.PlainTextOutput;
import com.strobel.decompiler.languages.BytecodeLanguage;
import com.strobel.decompiler.languages.LineNumberPosition;
import com.strobel.decompiler.languages.TypeDecompilationResults;
import com.strobel.decompiler.languages.java.JavaFormattingOptions;
import com.strobel.io.PathHelper;
import org.jboss.windup.util.exception.WindupStopException;

/**
 * Decompiles Java classes with Procyon Decompiler. See https://bitbucket.org/mstrobel/procyon
 *
 * @author Ondrej Zizka
 * @author Lincoln Baxter, III
 */
public class ProcyonDecompiler extends AbstractDecompiler {
    private static final Logger log = Logger.getLogger(ProcyonDecompiler.class.getName());
    private final ProcyonConfiguration procyonConf;

    public ProcyonDecompiler() {
        this.procyonConf = new ProcyonConfiguration();
    }

    public ProcyonDecompiler(ProcyonConfiguration configuration) {
        if (configuration == null)
            throw new IllegalArgumentException("Configuration must not be null.");

        this.procyonConf = configuration;
    }

    @Override
    public Logger getLogger() {
        return log;
    }


    @Override
    public Collection> getDecompileTasks(final Map> requestMap, final DecompilationListener listener) {
        final AtomicInteger current = new AtomicInteger(0);
        Collection> tasks = new ArrayList<>();

        final Map settingsByOutputDirectory = new TreeMap<>();
        final Map> metadataSystemCaches = new TreeMap<>();
        final Map countByOutputDirectory = new TreeMap<>();

        for (Map.Entry> entry : requestMap.entrySet()) {
            ClassDecompileRequest mainRequest = entry.getValue().get(0);
            if (!settingsByOutputDirectory.containsKey(mainRequest.getOutputDirectory())) {
                final DecompilerSettings settings = getDefaultSettings(mainRequest.getOutputDirectory().toFile());
                final ITypeLoader typeLoader = new CompositeTypeLoader(new WindupClasspathTypeLoader(mainRequest.getRootDirectory().toString()), new ClasspathTypeLoader());
                settings.setTypeLoader(typeLoader);
                settingsByOutputDirectory.put(mainRequest.getOutputDirectory(), settings);

                final Queue metadataSystemCache = new LinkedList<>();
                refreshMetadataCache(metadataSystemCache, settings);
                metadataSystemCaches.put(mainRequest.getOutputDirectory(), metadataSystemCache);

                countByOutputDirectory.put(mainRequest.getOutputDirectory(), new AtomicInteger(1));
            } else {
                countByOutputDirectory.get(mainRequest.getOutputDirectory()).incrementAndGet();
            }
        }

        for (final Map.Entry> entry : requestMap.entrySet()) {
            final ClassDecompileRequest mainRequest = entry.getValue().get(0);

            // TODO - This approach is a hack, but it should work around the Procyon decompiler hangs for now
            Callable callable = new Callable() {
                @Override
                public File call() throws Exception {
                    if (listener.isCancelled())
                        return null;

                    List classFilePaths = pathsFromDecompilationRequests(entry.getValue());
                    final DecompilerSettings settings = settingsByOutputDirectory.get(mainRequest.getOutputDirectory());
                    Queue metadataSystemCache = metadataSystemCaches.get(mainRequest.getOutputDirectory());

                    WindupMetadataSystem metadataSystem = null;
                    try {
                        synchronized (metadataSystemCache) {
                            if (current.incrementAndGet() % 50 == 0) {
                                log.info("Decompiling " + current + " / " + requestMap.size());
                                refreshMetadataCache(metadataSystemCache, settings);
                            }
                            metadataSystem = metadataSystemCache.remove();
                        }

                        ExecutionStatistics.get().begin("ProcyonDecompiler.decompileIndividualItem");
                        String typeName = mainRequest.getClassFile().normalize().toAbsolutePath().toString()
                                .substring(mainRequest.getRootDirectory().normalize().toAbsolutePath().toString().length() + 1);
                        typeName = StringUtils.removeEnd(typeName, ".class");
                        final DecompileExecutor t = new DecompileExecutor(settings, metadataSystem, typeName);
                        // TODO - This approach is a hack, but it should work around the Procyon decompiler hangs
                        // for now
                        t.start();
                        t.join(60000L); // wait up to ten seconds
                        if (!t.success) {
                            if (t.e == null) {
                                t.cancelDecompilation();
                                throw new RuntimeException("Failed to decompile within 60 seconds... attempting abort", t.e);
                            } else {
                                throw new RuntimeException(t.e);
                            }
                        }

                        File outputFile = t.outputFile;
                        if (outputFile != null)
                            listener.fileDecompiled(classFilePaths, outputFile.getAbsolutePath());
                        return outputFile;
                    } catch (Throwable th) {
                        String msg = "Error during decompilation of " + mainRequest.getClassFile() + ": " + th.getMessage();
                        DecompilationFailure ex = new DecompilationFailure(msg, classFilePaths, th);
                        log.log(Level.SEVERE, msg, ex);
                        listener.decompilationFailed(classFilePaths, msg);
                    } finally {
                        if (metadataSystem != null) {
                            synchronized (metadataSystemCache) {
                                metadataSystemCache.add(metadataSystem);
                            }
                        }

                        if (countByOutputDirectory.get(mainRequest.getOutputDirectory()).decrementAndGet() == 0) {
                            settingsByOutputDirectory.remove(mainRequest.getOutputDirectory());
                            metadataSystemCaches.remove(mainRequest.getOutputDirectory());
                        }
                        ExecutionStatistics.get().end("ProcyonDecompiler.decompileIndividualItem");
                    }
                    return null;
                }
            };
            tasks.add(callable);
        }
        return tasks;

    }

    private List pathsFromDecompilationRequests(List requests) {
        List result = new ArrayList<>();
        for (ClassDecompileRequest request : requests) {
            result.add(request.getClassFile().toString());
        }
        return result;
    }

    /**
     * Decompiles the given .class file and creates the specified output source file.
     *
     * @param classFilePath the .class file to be decompiled.
     * @param outputDir     The directory where decompiled .java files will be placed.
     */
    @Override
    public DecompilationResult decompileClassFile(Path rootDir, Path classFilePath, Path outputDir)
            throws DecompilationException {
        Checks.checkDirectoryToBeRead(rootDir.toFile(), "Classes root dir");
        File classFile = classFilePath.toFile();
        Checks.checkFileToBeRead(classFile, "Class file");
        Checks.checkDirectoryToBeFilled(outputDir.toFile(), "Output directory");

        log.info("Decompiling .class '" + classFilePath + "' to '" + outputDir + "' from: '" + rootDir + "'");

        String name = classFilePath.normalize().toAbsolutePath().toString().substring(rootDir.toAbsolutePath().toString().length() + 1);
        final String typeName = StringUtils.removeEnd(name, ".class");// .replace('/', '.');

        DecompilationResult result = new DecompilationResult();
        try {
            DecompilerSettings settings = getDefaultSettings(outputDir.toFile());
            this.procyonConf.setDecompilerSettings(settings); // TODO: This is horrible mess.

            final ITypeLoader typeLoader = new CompositeTypeLoader(new WindupClasspathTypeLoader(rootDir.toString()), new ClasspathTypeLoader());
            WindupMetadataSystem metadataSystem = new WindupMetadataSystem(typeLoader);
            File outputFile = this.decompileType(settings, metadataSystem, typeName);
            result.addDecompiled(Collections.singletonList(classFilePath.toString()), outputFile.getAbsolutePath());
        } catch (Throwable e) {
            DecompilationFailure failure = new DecompilationFailure("Error during decompilation of "
                    + classFilePath.toString() + ":\n    " + e.getMessage(), Collections.singletonList(name), e);
            log.severe(failure.getMessage());
            result.addFailure(failure);
        }

        return result;
    }


    private void decompileDirectory(final Path rootDir, Path outputDir, Path subPath, final DecompilationResult result)
            throws DecompilationException {
        Checks.checkDirectoryToBeRead(rootDir.toFile(), "Directory to decompile");
        Checks.checkDirectoryToBeFilled(outputDir.toFile(), "Output directory");

        log.info("Decompiling subdir '" + subPath + "'");

        /*
         * This forces an initialization of the settings.
         */
        final DecompilerSettings settings = getDefaultSettings(outputDir.toFile());

        // TODO: Rewrite with Commons IO's DirectoryWalker.
        File curDirFull = rootDir.resolve(subPath).toFile();
        final List files = Arrays.asList(curDirFull.listFiles());
        Collection> tasks = new ArrayList<>();
        for (File file : files) {
            final WindupMetadataSystem metadataSystem = new NoRetryMetadataSystem(new InputTypeLoader());
            if (file.isDirectory()) {
                Path subPathNew = subPath.resolve(file.getName());
                decompileDirectory(rootDir, outputDir, subPathNew, result);
                continue;
            }

            if (!file.getName().endsWith(".class"))
                continue;

            if (file.getName().contains("$"))
                continue;

            final String fileSubPath = subPath.resolve(file.getName()).toString();
            final String fqcn = StringUtils.removeEnd(fileSubPath, ".class").replace('/', '.');
            final String fileAbsolutePath = file.getAbsolutePath();

            Callable callable = new Callable() {
                @Override
                public File call() throws Exception {
                    File outputFile;
                    try {
                        outputFile = decompileType(settings, metadataSystem, fqcn);
                        if (null == outputFile)
                            throw new IllegalStateException("Unknown Procyon error, type not found.");
                        result.addDecompiled(Collections.singletonList(fileAbsolutePath), outputFile.getAbsolutePath());
                        return outputFile;
                    } catch (Exception e) {
                        DecompilationFailure failure = new DecompilationFailure("Error during decompilation of "
                                + rootDir + " / " + fileSubPath + ":\n    " + e.getMessage(),
                                Collections.singletonList(fileSubPath.toString()), e);
                        log.log(Level.SEVERE, failure.getMessage(), failure);
                        result.addFailure(failure);
                    }
                    return null;
                }

            };
            tasks.add(callable);
        }
        try {
            getExecutorService().invokeAll(tasks);
        } catch (InterruptedException e) {
            throw new IllegalStateException("Was not able to decompile in the given time limit.");
        }
    }

    /**
     * Decompiles .class files and nested archives in the given archive, as allowed by the given filter.
     * 

* Nested archives will be decompiled into directories matching the name of the archive, e.g. * foo.ear/bar.jar/src/com/foo/bar/Baz.java. *

* Required directories will be created as needed. * * @param archive The archive containing source files and archives. * @param outputDir The directory where decompiled .java files will be placed. * @param filter Decides which classes will be decompiled. * @returns Result with all decompilation failures. Never throws. */ @Override public DecompilationResult decompileArchiveImpl(final Path archive, Path outputDir, Filter filter, final DecompilationListener listener) throws DecompilationException { Checks.checkFileToBeRead(archive.toFile(), "Archive to decompile"); Checks.checkDirectoryToBeFilled(outputDir.toFile(), "Output directory"); log.info("Decompiling archive '" + archive.toAbsolutePath() + "' to '" + outputDir.toAbsolutePath() + "'"); JarFile jar = loadJar(archive.toFile()); try { final AtomicInteger jarEntryCount = new AtomicInteger(0); Enumeration countEnum = jar.entries(); while (countEnum.hasMoreElements()) { countEnum.nextElement(); jarEntryCount.incrementAndGet(); } // MetadataSystem, TypeLoader's final DecompilerSettings settings = getDefaultSettings(outputDir.toFile()); settings.setTypeLoader(new CompositeTypeLoader(new WindupJarTypeLoader(jar), settings.getTypeLoader())); final DecompilationResult res = new DecompilationResult(); final AtomicInteger current = new AtomicInteger(0); final Enumeration entries = jar.entries(); Collection> tasks = new ArrayList<>(); final Queue metadataSystemCache = new LinkedList<>(); refreshMetadataCache(metadataSystemCache, settings); while (entries.hasMoreElements()) { final JarEntry entry = entries.nextElement(); final String name = entry.getName(); if (!name.endsWith(".class")) { jarEntryCount.decrementAndGet(); continue; } if (filter != null) { Filter.Result filterRes = filter.decide(entry); if (filterRes == Filter.Result.REJECT) { jarEntryCount.decrementAndGet(); continue; } if (filterRes == Filter.Result.STOP) { break; } } final String typeName = StringUtils.removeEnd(name, ".class"); // TODO - This approach is a hack, but it should work around the Procyon decompiler hangs for now Callable callable = new Callable() { @Override public File call() throws Exception { WindupMetadataSystem metadataSystem = null; try { synchronized (metadataSystemCache) { if (current.incrementAndGet() % 50 == 0) { log.info("Decompiling " + current + " / " + jarEntryCount); refreshMetadataCache(metadataSystemCache, settings); } metadataSystem = metadataSystemCache.remove(); } ExecutionStatistics.get().begin("ProcyonDecompiler.decompileIndividualItem"); final DecompileExecutor t = new DecompileExecutor(settings, metadataSystem, typeName); // TODO - This approach is a hack, but it should work around the Procyon decompiler hangs // for now t.start(); t.join(60000L); // timeout if it is taking too long if (!t.success) { if (t.e == null) { t.cancelDecompilation(); throw new RuntimeException("Failed to decompile file within 60 seconds... attempting abort", t.e); } else { throw new RuntimeException(t.e); } } File outputFile = t.outputFile; if (outputFile != null) { listener.fileDecompiled(Collections.singletonList(name), outputFile.getAbsolutePath()); res.addDecompiled(Collections.singletonList(name), outputFile.getAbsolutePath()); } return outputFile; } catch (WindupStopException ex) { String msg = "Detected a request to stop during decompilation of " + archive.toString() + "!" + name + ":\n " + ex.getMessage(); log.log(Level.WARNING, msg + System.lineSeparator() + " (Rethrowing)"); throw new WindupStopException(msg, ex); } catch (Throwable th) { String msg = "Error during decompilation of " + archive.toString() + "!" + name + ":\n " + th.getMessage(); DecompilationFailure ex = new DecompilationFailure(msg, Collections.singletonList(name), th); log.log(Level.SEVERE, msg, ex); res.addFailure(ex); } finally { if (metadataSystem != null) { synchronized (metadataSystemCache) { metadataSystemCache.add(metadataSystem); } } ExecutionStatistics.get().end("ProcyonDecompiler.decompileIndividualItem"); } return null; } }; tasks.add(callable); } try { // Wait until all finish. getExecutorService().invokeAll(tasks); } catch (InterruptedException e) { throw new IllegalStateException("Decompilation was interrupted."); } finally { listener.decompilationProcessComplete(); } return res; } finally { try { jar.close(); } catch (IOException e) { log.warning("Failed to close jar file: " + jar.getName()); } } } /** * The metadata cache can become huge over time. This simply flushes it periodically. */ private void refreshMetadataCache(final Queue metadataSystemCache, final DecompilerSettings settings) { metadataSystemCache.clear(); for (int i = 0; i < this.getNumberOfThreads(); i++) { metadataSystemCache.add(new NoRetryMetadataSystem(settings.getTypeLoader())); } } private class DecompileExecutor extends Thread { private DecompilerSettings settings; private WindupMetadataSystem metadataSystem; private String typeName; private Exception e; private File outputFile; private boolean success; public DecompileExecutor(DecompilerSettings settings, WindupMetadataSystem metadataSystem, String typeName) { this.settings = settings; this.metadataSystem = metadataSystem; this.typeName = typeName; setDaemon(true); } @Override public void run() { try { this.outputFile = decompileType(settings, metadataSystem, typeName); this.success = true; } catch (Exception e) { this.e = e; } } @SuppressWarnings("deprecation") public void cancelDecompilation() { this.interrupt(); // sleep up to 10 seconds for (int i = 0; i < 10; i++) { if (this.isAlive()) { try { Thread.sleep(1000L); } catch (InterruptedException e) { throw new WindupException("Interrupted while attempting to abort thread", e); } } } if (this.isAlive()) { // make one last (desperate) attempt to kill it this.stop(); } } } /** * Decompiles a single type. * * @param metadataSystem * @param typeName * @return * @throws IOException */ private File decompileType(final DecompilerSettings settings, final WindupMetadataSystem metadataSystem, final String typeName) throws IOException { log.fine("Decompiling " + typeName); final TypeReference type; // Hack to get around classes whose descriptors clash with primitive types. if (typeName.length() == 1) { final MetadataParser parser = new MetadataParser(IMetadataResolver.EMPTY); final TypeReference reference = parser.parseTypeDescriptor(typeName); type = metadataSystem.resolve(reference); } else type = metadataSystem.lookupType(typeName); if (type == null) { log.severe("Failed to load class: " + typeName); return null; } final TypeDefinition resolvedType = type.resolve(); if (resolvedType == null) { log.severe("Failed to resolve type: " + typeName); return null; } boolean nested = resolvedType.isNested() || resolvedType.isAnonymous() || resolvedType.isSynthetic(); if (!this.procyonConf.isIncludeNested() && nested) return null; settings.setJavaFormattingOptions(new JavaFormattingOptions()); final FileOutputWriter writer = createFileWriter(resolvedType, settings); final PlainTextOutput output; output = new PlainTextOutput(writer); output.setUnicodeOutputEnabled(settings.isUnicodeOutputEnabled()); if (settings.getLanguage() instanceof BytecodeLanguage) output.setIndentToken(" "); DecompilationOptions options = new DecompilationOptions(); options.setSettings(settings); // I'm missing why these two classes are split. // --------- DECOMPILE --------- final TypeDecompilationResults results = settings.getLanguage().decompileType(resolvedType, output, options); writer.flush(); writer.close(); // If we're writing to a file and we were asked to include line numbers in any way, // then reformat the file to include that line number information. final List lineNumberPositions = results.getLineNumberPositions(); if (!this.procyonConf.getLineNumberOptions().isEmpty()) { final LineNumberFormatter lineFormatter = new LineNumberFormatter(writer.getFile(), lineNumberPositions, this.procyonConf.getLineNumberOptions()); lineFormatter.reformatFile(); } return writer.getFile(); } /** * Default settings set type loader to ClasspathTypeLoader if not set before. */ private DecompilerSettings getDefaultSettings(File outputDir) { DecompilerSettings settings = new DecompilerSettings(); procyonConf.setDecompilerSettings(settings); settings.setOutputDirectory(outputDir.getPath()); settings.setShowSyntheticMembers(false); settings.setForceExplicitImports(true); if (settings.getTypeLoader() == null) settings.setTypeLoader(new ClasspathTypeLoader()); return settings; } /** * Opens the jar, wraps any IOException. */ private JarFile loadJar(File archive) throws DecompilationException { try { return new JarFile(archive); } catch (IOException ex) { throw new DecompilationException("Can't load .jar: " + archive.getPath(), ex); } } /** * Constructs the path from FQCN, validates writability, and creates a writer. */ private static synchronized FileOutputWriter createFileWriter(final TypeDefinition type, final DecompilerSettings settings) throws IOException { final String outputDirectory = settings.getOutputDirectory(); final String fileName = type.getName() + settings.getLanguage().getFileExtension(); final String packageName = type.getPackageName(); // foo.Bar -> foo/Bar.java final String subDir = StringUtils.defaultIfEmpty(packageName, "").replace('.', File.separatorChar); final String outputPath = PathHelper.combine(outputDirectory, subDir, fileName); final File outputFile = new File(outputPath); final File parentDir = outputFile.getParentFile(); if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) { throw new IllegalStateException("Could not create directory:" + parentDir); } if (!outputFile.exists() && !outputFile.createNewFile()) { throw new IllegalStateException("Could not create output file: " + outputPath); } return new FileOutputWriter(outputFile, settings); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy