de.loskutov.bco.asm.DecompiledMethod.java Source code

Java tutorial

Introduction

Here is the source code for de.loskutov.bco.asm.DecompiledMethod.java

Source

/*******************************************************************************
 * Copyright (c) 2011 Eric Bruneton.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the BSD License
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/bsd-license.php
 * Contributor:  Eric Bruneton - initial API and implementation
 * Contributor:  Andrey Loskutov - fixes
 *******************************************************************************/
package de.loskutov.bco.asm;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.IStatus;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.LocalVariableNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicValue;
import org.objectweb.asm.tree.analysis.Frame;
import org.objectweb.asm.tree.analysis.SimpleVerifier;
import org.objectweb.asm.tree.analysis.Value;

import de.loskutov.bco.BytecodeOutlinePlugin;
import de.loskutov.bco.preferences.BCOConstants;

/**
 * @author Eric Bruneton
 */

public class DecompiledMethod {

    private final List<Object> text;

    private final List localVariables;

    /**
     * decompiled line -> source line
     */
    private final Map<Integer, Integer> sourceLines;

    /**
     * source line -> decompiled line
     */
    private final Map<Integer, Integer> decompiledLines;

    /**
     * decompiled line -> insn
     */
    private final Map<Integer, Integer> insns;

    /**
     *  decompiled line -> opcode
     */
    private final Map<Integer, Integer> opcodes;

    /**
     * insn -> decompile line
     */
    private final Map<Integer, Integer> insnLines;

    private int lineCount;

    /**
     * first source line, if any
     */
    private int firstSourceLine;

    /**
     * last source line, if any
     */
    private int lastSourceLine;

    MethodNode meth;

    private Frame[] frames;

    private String error;

    private int errorInsn;

    private final String owner;

    private final Map<Label, Integer> lineNumbers;

    private final DecompilerOptions options;

    private final int access;

    public DecompiledMethod(final String owner, final Map<Label, Integer> lineNumbers, final MethodNode meth,
            DecompilerOptions options, int access) {
        this.meth = meth;
        this.owner = owner;
        this.lineNumbers = lineNumbers;
        this.options = options;
        this.access = access;
        this.text = new ArrayList<Object>();
        this.localVariables = meth.localVariables;
        this.sourceLines = new HashMap<Integer, Integer>();
        this.decompiledLines = new HashMap<Integer, Integer>();
        this.insns = new HashMap<Integer, Integer>();
        this.opcodes = new HashMap<Integer, Integer>();
        this.insnLines = new HashMap<Integer, Integer>();
    }

    void setText(final List inputText) {
        formatText(inputText, new HashMap<Integer, String>(), new StringBuffer(), this.text);
        computeMaps(lineNumbers);

        if (options.modes.get(BCOConstants.F_SHOW_ANALYZER) && (access & Opcodes.ACC_ABSTRACT) == 0) {
            analyzeMethod(options.cl);
        }
    }

    void addLineNumber(Label start, Integer integer) {
        lineNumbers.put(start, integer);
    }

    public boolean isInit() {
        return ("<init>".equals(meth.name) && "()V".equals(meth.desc)) || "<clinit>".equals(meth.name);
    }

    public boolean hasSourceLinesInfo() {
        return !sourceLines.isEmpty();
    }

    public boolean hasLocalVariablesInfo() {
        return !localVariables.isEmpty();
    }

    public String getSignature() {
        return meth.name + meth.desc;
    }

    public boolean containsSource(int sourceLine) {
        return sourceLine >= getFirstSourceLine() && sourceLine <= getLastSourceLine();
    }

    /**
     * @param sourceLine
     * @return nearest match above given source line or the given line for perfect match
     * or -1 for no match. The return value is method-relative, and need to be transformed
     * to class absolute
     */
    public int getBestDecompiledLine(final int sourceLine) {
        if (!containsSource(sourceLine)) {
            return -1;
        }
        Set<Integer> set = decompiledLines.keySet();
        if (set.size() == 0) {
            return -1;
        }
        int bestMatch = -1;
        for (Iterator<Integer> iter = set.iterator(); iter.hasNext();) {
            int line = iter.next().intValue();
            int delta = sourceLine - line;
            if (delta < 0) {
                continue;
            } else if (delta == 0) {
                return line;
            }
            if (bestMatch < 0 || delta < sourceLine - bestMatch) {
                bestMatch = line;
            }
        }
        if (bestMatch < 0) {
            return -1;
        }
        return decompiledLines.get(Integer.valueOf(bestMatch)).intValue();
    }

    private void analyzeMethod(final ClassLoader cl) {
        Analyzer<BasicValue> a = new Analyzer<BasicValue>(new SimpleVerifier() {

            @Override
            protected Class getClass(final Type t) {
                try {
                    if (t.getSort() == Type.ARRAY) {
                        return Class.forName(t.getDescriptor().replace('/', '.'), true, cl);
                    }
                    return cl.loadClass(t.getClassName());
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException(e.toString() + " " + cl, e);
                }
            }
        });
        try {
            a.analyze(owner, meth);
        } catch (AnalyzerException e) {
            error = e.getMessage();
            if (error.startsWith("Error at instruction ")) {
                error = error.substring("Error at instruction ".length());
                errorInsn = Integer.parseInt(error.substring(0, error.indexOf(':')));
                error = error.substring(error.indexOf(':') + 2);
            } else {
                BytecodeOutlinePlugin.log(e, IStatus.ERROR);
                error = null;
            }
        }
        frames = a.getFrames();
    }

    private void formatText(final List input, final Map<Integer, String> locals, StringBuffer line,
            final List<Object> result) {
        for (int i = 0; i < input.size(); ++i) {
            Object o = input.get(i);
            if (o instanceof List) {
                formatText((List) o, locals, line, result);
            } else if (o instanceof Index) {
                result.add(o);
                updateLocals((Index) o, locals);
            } else if (o instanceof Integer) {
                String localVariableName = locals.get(o);
                if (localVariableName == null) {
                    Index index = getNextIndex(input, i);
                    if (index != null) {
                        updateLocals(index, locals);
                        localVariableName = locals.get(o);
                    }
                }
                if (localVariableName != null) {
                    line.append(": ").append(localVariableName);
                }
            } else {
                String s = o.toString();
                int p;
                do {
                    p = s.indexOf('\n');
                    if (p == -1) {
                        line.append(s);
                    } else {
                        result.add(line.toString() + s.substring(0, p + 1));
                        s = s.substring(p + 1);
                        line.setLength(0);
                    }
                } while (p != -1);
            }
        }
    }

    private static Index getNextIndex(List input, int startOffset) {
        for (int i = startOffset + 1; i < input.size(); i++) {
            Object object = input.get(i);
            if (object instanceof Index) {
                return (Index) object;
            }
        }
        return null;
    }

    private void updateLocals(final Index index, final Map<Integer, String> locals) {
        for (int i = 0; i < localVariables.size(); ++i) {
            LocalVariableNode lvNode = (LocalVariableNode) localVariables.get(i);
            if (lvNode.start == index.labelNode) {
                locals.put(Integer.valueOf(lvNode.index), lvNode.name);
            } else if (lvNode.end == index.labelNode) {
                locals.remove(Integer.valueOf(lvNode.index));
            }
        }
    }

    private void computeMaps(final Map<Label, Integer> lineNumbers1) {
        int currentDecompiledLine = 0;
        int firstLine = -1;
        int lastLine = -1;
        for (int i = 0; i < text.size(); ++i) {
            int currentOpcode = -1;
            int currentInsn1 = -1;
            int currentSourceLine = -1;
            Object o = text.get(i);
            if (o instanceof Index) {
                Index index = (Index) o;
                Integer sourceLine = null;
                if (index.labelNode != null) {
                    sourceLine = lineNumbers1.get(index.labelNode.getLabel());
                }
                if (sourceLine != null) {
                    currentSourceLine = sourceLine.intValue();
                    if (firstLine == -1 || currentSourceLine < firstLine) {
                        firstLine = currentSourceLine;
                    }
                    if (lastLine == -1 || currentSourceLine > lastLine) {
                        lastLine = currentSourceLine;
                    }
                }
                currentInsn1 = index.insn;
                currentOpcode = index.opcode;
            } else {
                ++currentDecompiledLine;
            }
            Integer cdl = Integer.valueOf(currentDecompiledLine);
            Integer ci = Integer.valueOf(currentInsn1);
            Integer co = Integer.valueOf(currentOpcode);
            if (currentSourceLine >= 0) {
                Integer csl = Integer.valueOf(currentSourceLine);
                sourceLines.put(cdl, csl);
                if (decompiledLines.get(csl) == null) {
                    decompiledLines.put(csl, cdl);
                }
            }
            insns.put(cdl, ci);
            opcodes.put(cdl, co);
            if (insnLines.get(ci) == null) {
                insnLines.put(ci, cdl);
            }
        }
        lineCount = currentDecompiledLine;
        firstSourceLine = firstLine;
        lastSourceLine = lastLine;
    }

    public String getText() {
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < text.size(); ++i) {
            Object o = text.get(i);
            if (!(o instanceof Index)) {
                buf.append((String) o);
            }
        }
        return buf.toString();
    }

    public String[][] getTextTable() {
        Frame frame = null;
        String error1 = "";
        List<String[]> lines = new ArrayList<String[]>();
        String offsStr = null;
        for (int i = 0; i < text.size(); ++i) {
            Object o = text.get(i);
            if (o instanceof Index) {
                Index index = (Index) o;
                int insn = index.insn;

                offsStr = "" + insn;
                if (frames != null && insn < frames.length) {
                    frame = frames[insn];
                    if (this.error != null && insn == this.errorInsn) {
                        error1 = this.error;
                    }
                }
            } else {
                if (offsStr == null) {
                    offsStr = "";
                }
                String locals = " ";
                String stack = " ";
                if (frame != null) {
                    StringBuffer buf = new StringBuffer();
                    appendFrame(buf, frame);
                    int p = buf.indexOf(" ");
                    locals = buf.substring(0, p);
                    if ("".equals(locals)) {
                        locals = " ";
                    }
                    stack = buf.substring(p + 1);
                    if ("".equals(stack)) {
                        stack = " ";
                    }
                }

                lines.add(new String[] { offsStr, locals, stack, o.toString(), error1 });
                frame = null;
                error1 = "";
                offsStr = null;
            }
        }
        return lines.toArray(new String[lines.size()][]);
    }

    public int getLineCount() {
        return lineCount;
    }

    public String getError() {
        return error;
    }

    public int getErrorLine() {
        if (error == null) {
            return -1;
        }
        Integer i = insnLines.get(Integer.valueOf(errorInsn));
        return i == null ? -1 : i.intValue();
    }

    private static void appendFrame(final StringBuffer buf, final Frame f) {
        try {
            for (int i = 0; i < f.getLocals(); ++i) {
                appendValue(buf, f.getLocal(i));
            }
            buf.append(' ');
            for (int i = 0; i < f.getStackSize(); ++i) {
                appendValue(buf, f.getStack(i));
            }
        } catch (IndexOutOfBoundsException e) {
            BytecodeOutlinePlugin.log(e, IStatus.ERROR);
        }
    }

    private static void appendValue(final StringBuffer buf, final Value v) {
        if (((BasicValue) v).isReference()) {
            buf.append("R");
        } else {
            buf.append(v.toString());
        }
    }

    public int getFirstSourceLine() {
        return firstSourceLine;
    }

    public int getLastSourceLine() {
        return lastSourceLine;
    }

    public int getSourceLine(final int decompiledLine) {
        Integer i = sourceLines.get(Integer.valueOf(decompiledLine));
        return i == null ? -1 : i.intValue();
    }

    /**
     *
     * @param decompiledLine
     * @return array with two elements, first is the local variables table,
     * second is the operands stack content. "null" value could be returned too.
     */
    public String[] getFrame(final int decompiledLine, final boolean useQualifiedNames) {
        Integer insn = getBytecodeOffset(decompiledLine);
        if (error != null && insn != null && insn.intValue() == errorInsn) {
            return new String[] { error, error };
        }
        if (frames != null && insn != null) {
            Frame f = frames[insn.intValue()];
            if (f == null) {
                return null;
            }

            try {
                StringBuffer localsBuf = new StringBuffer();

                for (int i = 0; i < f.getLocals(); ++i) {
                    String s = f.getLocal(i).toString();
                    appendTypeName(i, useQualifiedNames, localsBuf, s);

                    for (Iterator it = localVariables.iterator(); it.hasNext();) {
                        LocalVariableNode lvnode = (LocalVariableNode) it.next();
                        int n = lvnode.index;
                        if (n == i) {
                            localsBuf.append(" : ").append(lvnode.name);
                        }
                    }

                    localsBuf.append('\n');
                }
                StringBuffer stackBuf = new StringBuffer();
                for (int i = 0; i < f.getStackSize(); ++i) {
                    String s = f.getStack(i).toString();
                    appendTypeName(i, useQualifiedNames, stackBuf, s);
                    stackBuf.append('\n');
                }
                return new String[] { localsBuf.toString(), stackBuf.toString() };
            } catch (IndexOutOfBoundsException e) {
                BytecodeOutlinePlugin.log(e, IStatus.ERROR);
            }
        }
        return null;
    }

    public Integer getBytecodeOffset(final int decompiledLine) {
        Integer insn = insns.get(Integer.valueOf(decompiledLine));
        return insn;
    }

    public Integer getBytecodeInsn(final int decompiledLine) {
        Integer insn = opcodes.get(Integer.valueOf(decompiledLine));
        return insn;
    }

    public String[][][] getFrameTables(final int decompiledLine, boolean useQualifiedNames) {
        Integer insn = getBytecodeOffset(decompiledLine);
        if (insn == null) {
            return null;
        }
        return getFrameTablesForInsn(insn.intValue(), useQualifiedNames);
    }

    public String[][][] getFrameTablesForInsn(final int insn, boolean useQualifiedNames) {
        if (error != null && insn == errorInsn) {
            return null;
        }
        if (frames != null && insn >= 0 && insn < frames.length) {
            Frame f = frames[insn];
            if (f == null) {
                return null;
            }

            try {
                ArrayList<String[]> locals = new ArrayList<String[]>();
                for (int i = 0; i < f.getLocals(); ++i) {
                    String varName = "";
                    for (Iterator it = localVariables.iterator(); it.hasNext();) {
                        LocalVariableNode lvnode = (LocalVariableNode) it.next();
                        int n = lvnode.index;
                        if (n == i) {
                            varName = lvnode.name;
                            // TODO take into account variable scope!
                            break;
                        }
                    }

                    locals.add(new String[] { "" + i, getTypeName(useQualifiedNames, f.getLocal(i).toString()),
                            varName });
                }

                ArrayList<String[]> stack = new ArrayList<String[]>();
                for (int i = 0; i < f.getStackSize(); ++i) {
                    stack.add(new String[] { "" + i, getTypeName(useQualifiedNames, f.getStack(i).toString()) });
                }
                return new String[][][] { locals.toArray(new String[3][]), stack.toArray(new String[2][]) };
            } catch (IndexOutOfBoundsException e) {
                BytecodeOutlinePlugin.log(e, IStatus.ERROR);
            }
        }
        return null;
    }

    /**
     * Appends full type name or only simply name, depends on boolean flag.
     *
     * @param useQualifiedNames if false, then e.g. "Object" will be appended to
     * buffer instead of "Ljava/lang/Object;" etc
     * @param buf buffer to append
     * @param s string with bytecode type name, like "Ljava/lang/Object;"
     */
    private static void appendTypeName(int n, final boolean useQualifiedNames, StringBuffer buf, String s) {
        buf.append(n).append(" ");
        if (!useQualifiedNames) {
            int idx = s.lastIndexOf('/');
            if (idx > 0) {
                // from "Ljava/lang/Object;" to "Object"
                buf.append(s.substring(idx + 1, s.length() - 1));
                return;
            }
        }
        if ("Lnull;".equals(s)) {
            buf.append("null");
        } else {
            buf.append(s);
        }
    }

    private static String getTypeName(final boolean useQualifiedNames, String s) {
        if (!useQualifiedNames) {
            // get leading array symbols
            String arraySymbols = "";
            while (s.startsWith("[")) {
                arraySymbols += "[";
                s = s.substring(1);
            }

            int idx = s.lastIndexOf('/');
            if (idx > 0) {
                // from "Ljava/lang/Object;" to "Object"
                return arraySymbols + s.substring(idx + 1, s.length() - 1);
            }
            // this is the case on LVT view - ignore it
            if ("." == s) {
                return arraySymbols + s;
            }
            // resolve primitive types
            return arraySymbols + CommentedClassVisitor.getSimpleName(Type.getType(s));
        }
        return "Lnull;".equals(s) ? "null" : s;
    }

    public int getDecompiledLine(final int sourceLine) {
        Integer i = decompiledLines.get(Integer.valueOf(sourceLine));
        return i == null ? -1 : i.intValue();
    }

    /**
     * Returns <code>true</code> if this <code>DecompiledMethod</code> is the same as the o argument.
     *
     * @return <code>true</code> if this <code>DecompiledMethod</code> is the same as the o argument.
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof DecompiledMethod)) {
            return false;
        }
        DecompiledMethod another = (DecompiledMethod) o;
        return getSignature().equals(another.getSignature())
                && (owner != null ? owner.equals(another.owner) : true);
    }

    @Override
    public int hashCode() {
        return getSignature().hashCode() + (owner != null ? owner.hashCode() : 0);
    }
}