/* ###
 * IP: GHIDRA
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
// Finds potential problems in code that will cause trouble for the decompiler.
//
//  This script runs the decompiler on each currently defined function.
//  Function that has potential issues in the decompiler output is flagged in a table with a suggestion.
//  For example any references to "in_" variables, or "unaff_" are flagged with a potential solution.
//  This script is still a work in progress, but it can help diagnose initial analysis of a binary for problems that
//  affect decompiled output and thus the cleanness of code for follow on uses.  Things like unidentified
//  epilog functions that have side-effects on the stack will be uncovered.
//
// @category Analysis

import java.util.*;

import ghidra.app.decompiler.*;
import ghidra.app.decompiler.parallel.*;
import ghidra.app.script.GhidraScript;
import ghidra.app.tablechooser.*;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.*;
import ghidra.program.model.pcode.*;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.symbol.ReferenceIterator;
import ghidra.util.task.TaskMonitor;
import util.CollectionUtils;

public class FindPotentialDecompilerProblems extends GhidraScript {

	@Override
	public void run() throws Exception {

		if (isRunningHeadless()) {
			printf("This script cannot be run in headless mode.\n");
			return;
		}

		TableChooserDialog tableDialog = createTableChooserDialog(
			"Possible Decompiler Problems: " + currentProgram.getName(), null);
		configureTableColumns(tableDialog);
		tableDialog.show();
		IssueEntries entryList = new TableEntryList(tableDialog);

		DecompilerCallback<Void> callback =
			new DecompilerCallback<Void>(currentProgram, new BasicConfigurer(currentProgram)) {

				@Override
				public Void process(DecompileResults results, TaskMonitor tMonitor)
						throws Exception {
					for (ProblemLocation probLoc : processFunc(results)) {
						entryList.add(probLoc);
					}
					return null;
				}
			};

		Set<Function> funcsToDecompile = new HashSet<>();
		FunctionIterator fIter = currentProgram.getFunctionManager().getFunctionsNoStubs(true);
		fIter.forEach(e -> funcsToDecompile.add(e));

		if (funcsToDecompile.isEmpty()) {
			popup("No functions to decompile!");
			return;
		}

		ParallelDecompiler.decompileFunctions(callback, funcsToDecompile, monitor);
		monitor.checkCancelled();
		tableDialog.setMessage("Finished");
	}

	private List<ProblemLocation> processFunc(DecompileResults decompResult) {

		//TODO: skip if func name contains SEH_epilog or SEH_prolog?
		//add general way to have architecture-specific messages?

		List<ProblemLocation> problems = new ArrayList<>();
		Function func = decompResult.getFunction();
		HighFunction hf = decompResult.getHighFunction();
		if (hf == null) {
			problems.add(new ProblemLocation(currentProgram, func.getEntryPoint(),
				func.getEntryPoint(), "", "Decompilation Error"));
			return problems;
		}

		Iterator<HighSymbol> symIter = hf.getLocalSymbolMap().getSymbols();
		while (symIter.hasNext() && !monitor.isCancelled()) {
			HighSymbol sym = symIter.next();
			HighVariable highVar = sym.getHighVariable();
			if (!(highVar instanceof HighLocal)) {
				continue;
			}
			if (sym.getName().startsWith("in_") && !sym.getName().equals("in_FS_OFFSET")) {
				// Has an input variable that is not a parameter
				String possible =
					"Function signature missing register param, called function passed too many register params, or only a subpiece" +
						" of a register actually used.";
				if (!(hf.getFunction().getSymbol().isGlobal()) &&
					!(hf.getFunction().getCallingConventionName().contains("thiscall"))) {
					possible += " Function might need calling convention changed to thiscall";
				}
				if (sym.getName().startsWith("in_stack_ff")) {
					possible =
						"Too many stack parameters passed to a called function.  May need to redefine in the called function (could be varargs).";
				}
				if (sym.getName().startsWith("in_stack_00")) {
					possible =
						"Too few stack parameters defined for this function.  May need to redefine parameters.";
				}

				Address funcAddr =
					getFirstFuncWithVar(func, sym.getHighVariable().getRepresentative());

				//if we didn't find a good location for the cause of the problem, 
				//just use the entry point of the function with the problem
				if (funcAddr == null || funcAddr.equals(Address.NO_ADDRESS)) {
					funcAddr = func.getEntryPoint();
				}
				problems.add(new ProblemLocation(currentProgram, func.getEntryPoint(), funcAddr,
					sym.getName(), possible));
			}

			if (sym.getName().startsWith("unaff_")) {
				Address firstAddr = getFirstCalledFunction(func);
				if (sym.getName().equals("unaff_EBP")) {
					problems.add(
						new ProblemLocation(currentProgram, firstAddr, func.getEntryPoint(),
							sym.getName(), "Suspect function is EH_PROLOG/EH_EPILOG"));
					continue;
				}
				// Has a side effect variable
				String possible = (firstAddr != null ? "Side effect from a call"
						: "Undefined parameter or global register save");
				//TODO: sym.getPCAddress() points outside of function bodies in certain cases...
				problems.add(new ProblemLocation(currentProgram, func.getEntryPoint(),
					sym.getPCAddress(), sym.getName(), possible));
			}
			//extraout_X: will sym.getHighVariable().getRepresentative return X?
			if (sym.getName().startsWith("extraout")) {
				// Has a side effect variable
				Address funcAddr =
					getFirstFuncWithVar(func, sym.getHighVariable().getRepresentative());

				if (funcAddr.equals(Address.NO_ADDRESS)) {
					funcAddr = func.getEntryPoint();
				}

				String possible =
					"Bad parameter in called function or extra return value/global register/function register side effect";
				if (sym.getName().startsWith("extraout_var")) {
					possible = "Function containing problem may need return type adjusted.";
				}
				problems.add(new ProblemLocation(currentProgram, func.getEntryPoint(), funcAddr,
					sym.getName(), possible));
			}
		}
		return problems;
	}

	/**
	 * Returns the target of the first (in address order) call in the body of {@code func}
	 * which takes {@code vn} as a parameter.
	 * @param func {@link Function} whose body to search for calls
	 * @param vn {@link Varnode} representing required parameter
	 * @return entry point of first function called by {@code func} which uses {@code vn}
	 * as a parameter, or {@code Address.NO_ADDRESS} if no such function found.
	 */
	private Address getFirstFuncWithVar(Function func, Varnode vn) {
		Address variableAddr = vn.getAddress();
		if (variableAddr == null) {
			return Address.NO_ADDRESS;
		}

		// Note: this handles some cases where functions consist of non-contiguous blocks,
		// but since we start at the entry point we might miss things if part of the body of
		// the function is before the entry point (in address order)
		ReferenceIterator refIter =
			func.getProgram().getReferenceManager().getReferenceIterator(func.getEntryPoint());

		Address maxAddr = func.getBody().getMaxAddress();

		// return the first call to a function which takes vn as an argument
		for (Reference ref : CollectionUtils.asIterable(refIter)) {
			// check whether we are at an address not in the function
			// only necessary in case func consists of non-contiguous blocks
			// TODO: handle tail-call elimination
			if (!func.getBody().contains(ref.getFromAddress())) {
				continue;
			}
			if (isValidCallReference(ref)) {
				Function calledFunc =
					currentProgram.getFunctionManager().getFunctionAt(ref.getToAddress());
				Parameter[] params = calledFunc.getParameters();
				for (Parameter param : params) {
					Address addr = param.getMinAddress();
					if (addr != null && addr.equals(variableAddr)) {
						return ref.getToAddress();
					}
				}
			}
			// The references are sorted by their "from" addresses, so if this condition is true, 
			// we've searched all the references from the body of func and haven't found anything.
			// So, stop looking.
			if (ref.getFromAddress().compareTo(maxAddr) > 0) {
				return Address.NO_ADDRESS;
			}
		}
		//in case there are no references with "from" addresses after the body of func
		return Address.NO_ADDRESS;
	}

	/**
	 * Returns the address of first function called by {@code func}.  That is, the returned {@link Address}
	 * is the target of the call instruction with the least address within the body of {@code func}. 
	 * @param func the {@link Function} to search for calls
	 * @return the {@link Address} of the first called function, or {@code Address.NO_ADDRESS} if
	 * no calls are found.
	 */
	private Address getFirstCalledFunction(Function func) {

		//could be issues if func's body has addresses that are before the entry point of
		//func - see the comment in getFirstFuncWithVar
		ReferenceIterator refIter =
			func.getProgram().getReferenceManager().getReferenceIterator(func.getEntryPoint());

		Address maxAddr = func.getBody().getMaxAddress();

		for (Reference ref : CollectionUtils.asIterable(refIter)) {
			// check whether we are at an address not in the function
			// only necessary in case func consists of non-contiguous blocks
			// TODO: handle tail-call elimination
			if (!func.getBody().contains(ref.getFromAddress())) {
				continue;
			}

			// return the first call for the function
			if (isValidCallReference(ref)) {
				return ref.getToAddress();
			}
			// The references are sorted by their "from" addresses, so if this condition is true, 
			// we've searched all the references from the body of func and haven't found anything.
			// So, stop looking.
			if (ref.getFromAddress().compareTo(maxAddr) > 0) {
				return Address.NO_ADDRESS;
			}
		}
		//in case there are no references with "from" addresses after the body of func
		return Address.NO_ADDRESS;
	}

	//returns true precisely when ref is a call reference to a defined function
	private boolean isValidCallReference(Reference ref) {
		if (!ref.getReferenceType().isCall()) {
			return false;
		}
		if (ref.getToAddress() == null) {
			return false;
		}
		if (currentProgram.getFunctionManager().getFunctionAt(ref.getToAddress()) != null) {
			return true;
		}
		return false;

	}

	static class BasicConfigurer implements DecompileConfigurer {
		private Program p;

		public BasicConfigurer(Program prog) {
			p = prog;
		}

		@Override
		public void configure(DecompInterface decompiler) {
			decompiler.toggleCCode(true);
			decompiler.toggleSyntaxTree(true);
			decompiler.setSimplificationStyle("decompile");
			DecompileOptions opts = new DecompileOptions();
			opts.grabFromProgram(p);
			decompiler.setOptions(opts);
		}
	}

	////////////////////////////////////////////////////////////////////////////////////
	//                          table stuff                                           //
	////////////////////////////////////////////////////////////////////////////////////

	private void configureTableColumns(TableChooserDialog dialog) {
		StringColumnDisplay explanationColumn = new StringColumnDisplay() {
			@Override
			public String getColumnName() {
				return "Potential Problem";
			}

			@Override
			public String getColumnValue(AddressableRowObject rowObject) {
				ProblemLocation entry = (ProblemLocation) rowObject;
				return entry.getExplanation();
			}
		};

		StringColumnDisplay funcColumn = new StringColumnDisplay() {
			@Override
			public String getColumnName() {
				return "Func Name";
			}

			@Override
			public String getColumnValue(AddressableRowObject rowObject) {
				ProblemLocation entry = (ProblemLocation) rowObject;
				Function func = entry.getProgram().getFunctionManager().getFunctionContaining(
					entry.getAddress());
				if (func == null) {
					return "";
				}
				return func.getName();
			}
		};

		ColumnDisplay<Address> probLocColumn = new ColumnDisplay<Address>() {
			@Override
			public String getColumnName() {
				return "Location of Possible Cause";
			}

			@Override
			public Address getColumnValue(AddressableRowObject rowObject) {
				ProblemLocation probLocation = (ProblemLocation) rowObject;
				return probLocation.getWhyAddr();
			}

			@Override
			public int compare(AddressableRowObject o1, AddressableRowObject o2) {
				return getColumnValue(o1).compareTo(getColumnValue(o2));
			}

			@Override
			public Class<Address> getColumnClass() {
				return Address.class;
			}
		};

		StringColumnDisplay varNameColumn = new StringColumnDisplay() {
			@Override
			public String getColumnName() {
				return "Problematic Variable";
			}

			@Override
			public String getColumnValue(AddressableRowObject rowObject) {
				ProblemLocation probLocation = (ProblemLocation) rowObject;
				return probLocation.getVarName();
			}

			@Override
			public int compare(AddressableRowObject o1, AddressableRowObject o2) {
				return getColumnValue(o1).compareTo(getColumnValue(o2));
			}
		};

		dialog.addCustomColumn(funcColumn);
		dialog.addCustomColumn(probLocColumn);
		dialog.addCustomColumn(varNameColumn);
		dialog.addCustomColumn(explanationColumn);
	}

	class ProblemLocation implements AddressableRowObject {
		private Program program;
		private Address problemAddress;
		private Address causeAddress;
		private String varName;
		private String explanation;

		ProblemLocation(Program prog, Address problemAddress, Address whyAddr, String varName,
				String explanation) {
			this.problemAddress = problemAddress;
			this.causeAddress = whyAddr;
			this.varName = varName;
			this.explanation = explanation;
			this.program = prog;
		}

		public Program getProgram() {
			return program;
		}

		@Override
		public Address getAddress() {
			return getFuncAddr();
		}

		public Address getFuncAddr() {
			if (problemAddress == null) {
				return Address.NO_ADDRESS;
			}
			return problemAddress;
		}

		public Address getWhyAddr() {
			if (causeAddress == null) {
				return Address.NO_ADDRESS;
			}
			return causeAddress;
		}

		public String getVarName() {
			return varName;
		}

		public String getExplanation() {
			return explanation;
		}

		@Override
		public String toString() {
			return "Issue at:" + getAddress() + "  found: " + getVarName() + "  " +
				getExplanation() + " at " + getWhyAddr();
		}
	}

	interface IssueEntries {

		void add(ProblemLocation location);

		int getNumEntries();

		void setMessage(String string);

	}

	class TableEntryList implements IssueEntries {

		private TableChooserDialog tableDialog;

		public TableEntryList(TableChooserDialog tableDialog) {
			this.tableDialog = tableDialog;
		}

		@Override
		public void add(ProblemLocation location) {
			tableDialog.add(location);
		}

		@Override
		public void setMessage(String string) {
			tableDialog.setMessage(string);
		}

		@Override
		public int getNumEntries() {
			return tableDialog.getRowCount();
		}

	}

}
