Merge remote-tracking branch

'origin/GP-4707_ryanmkurtz_headless--SQUASHED' (Closes #6639)
This commit is contained in:
Ryan Kurtz 2024-06-25 13:41:03 -04:00
commit 2b73a6157f
2 changed files with 159 additions and 86 deletions

View File

@ -17,9 +17,9 @@ package ghidra.app.util.headless;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.*;
import java.util.*;
import java.util.stream.Collectors;
import generic.stl.Pair;
import ghidra.*;
@ -37,7 +37,76 @@ import ghidra.util.exception.InvalidInputException;
*/
public class AnalyzeHeadless implements GhidraLaunchable {
/**
* Headless command line arguments.
* <p>
* NOTE: Please update 'analyzeHeadlessREADME.html' if changing command line parameters
*/
private enum Arg {
//@formatter:off
IMPORT("-import", true, "[<directory>|<file>]+"),
PROCESS("-process", true, "[<project_file>]"),
PRE_SCRIPT("-prescript", true, "<ScriptName>"),
POST_SCRIPT("-postscript", true, "<ScriptName>"),
SCRIPT_PATH("-scriptPath", true, "\"<path1>[;<path2>...]\""),
PROPERTIES_PATH("-propertiesPath", true, "\"<path1>[;<path2>...]\""),
SCRIPT_LOG("-scriptlog", true, "<path to script log file>"),
LOG("-log", true, "<path to log file>"),
OVERWRITE("-overwrite", false),
RECURSIVE("-recursive", false),
READ_ONLY("-readOnly", false),
DELETE_PROJECT("-deleteproject", false),
NO_ANALYSIS("-noanalysis", false),
PROCESSOR("-processor", true, "<languageID>"),
CSPEC("-cspec", true, "<compilerSpecID>"),
ANALYSIS_TIMEOUT_PER_FILE("-analysisTimeoutPerFile", true, "<timeout in seconds>"),
KEYSTORE("-keystore", true, "<KeystorePath>"),
CONNECT("-connect", false, "[<userID>]"),
PASSWORD("-p", false),
COMMIT("-commit", false, "[\"<comment>\"]]"),
OK_TO_DELETE("-okToDelete", false),
MAX_CPU("-max-cpu", true, "<max cpu cores to use>"),
LIBRARY_SEARCH_PATHS("-librarySearchPaths", true, "<path1>[;<path2>...]"),
LOADER("-loader", true, "<desired loader name>"),
LOADER_ARGS(Loader.COMMAND_LINE_ARG_PREFIX + "-", true, "<loader argument value>") {
@Override
public boolean matches(String arg) {
return arg.startsWith(Loader.COMMAND_LINE_ARG_PREFIX + "-");
}
};
//@formatter:on
private String name;
private boolean requiresSubArgs;
private String subArgFormat;
private Arg(String name, boolean requiresSubArgs, String subArgFormat) {
this.name = name;
this.requiresSubArgs = requiresSubArgs;
this.subArgFormat = subArgFormat;
}
private Arg(String name, boolean requiresSubArgs) {
this(name, requiresSubArgs, "");
}
public String usage() {
return "%s%s%s".formatted(name, subArgFormat.isEmpty() ? "" : " ", subArgFormat);
}
public boolean matches(String arg) {
return arg.equalsIgnoreCase(name);
}
@Override
public String toString() {
return name;
}
}
private static final int EXIT_CODE_ERROR = 1;
private static final Set<String> ALL_ARG_NAMES =
Arrays.stream(Arg.values()).map(a -> a.name).collect(Collectors.toSet());
/**
* The entry point of 'analyzeHeadless.bat'. Parses the command line arguments to the script
@ -64,7 +133,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
if (args[0].startsWith("ghidra:")) {
optionStartIndex = 1;
try {
ghidraURL = new URL(args[0]);
ghidraURL = new URI(args[0]).toURL();
}
catch (MalformedURLException e) {
System.err.println("Invalid Ghidra URL: " + args[0]);
@ -98,10 +167,10 @@ public class AnalyzeHeadless implements GhidraLaunchable {
File logFile = null;
File scriptLogFile = null;
for (int argi = optionStartIndex; argi < args.length; argi++) {
if (checkArgument("-log", args, argi)) {
if (checkArgument(Arg.LOG, args, argi)) {
logFile = new File(args[++argi]);
}
else if (checkArgument("-scriptlog", args, argi)) {
else if (checkArgument(Arg.SCRIPT_LOG, args, argi)) {
scriptLogFile = new File(args[++argi]);
}
}
@ -158,7 +227,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
String languageId = null;
String compilerSpecId = null;
String keystorePath = null;
String serverUID = null;
String userId = null;
boolean allowPasswordPrompt = false;
List<Pair<String, String[]>> preScripts = new LinkedList<>();
List<Pair<String, String[]>> postScripts = new LinkedList<>();
@ -166,57 +235,57 @@ public class AnalyzeHeadless implements GhidraLaunchable {
for (int argi = startIndex; argi < args.length; argi++) {
String arg = args[argi];
if (checkArgument("-log", args, argi)) {
if (checkArgument(Arg.LOG, args, argi)) {
// Already processed
argi++;
}
else if (checkArgument("-scriptlog", args, argi)) {
else if (checkArgument(Arg.SCRIPT_LOG, args, argi)) {
// Already processed
argi++;
}
else if (arg.equalsIgnoreCase("-overwrite")) {
else if (checkArgument(Arg.OVERWRITE, args, argi)) {
options.enableOverwriteOnConflict(true);
}
else if (arg.equalsIgnoreCase("-noanalysis")) {
else if (checkArgument(Arg.NO_ANALYSIS, args, argi)) {
options.enableAnalysis(false);
}
else if (arg.equalsIgnoreCase("-deleteproject")) {
else if (checkArgument(Arg.DELETE_PROJECT, args, argi)) {
options.setDeleteCreatedProjectOnClose(true);
}
else if (checkArgument("-loader", args, argi)) {
else if (checkArgument(Arg.LOADER, args, argi)) {
loaderName = args[++argi];
}
else if (arg.startsWith(Loader.COMMAND_LINE_ARG_PREFIX)) {
if (args[argi + 1].startsWith("-")) {
else if (checkArgument(Arg.LOADER_ARGS, args, argi)) {
if (ALL_ARG_NAMES.contains(args[argi + 1])) {
throw new InvalidInputException(args[argi] + " expects value to follow.");
}
loaderArgs.add(new Pair<>(arg, args[++argi]));
}
else if (checkArgument("-processor", args, argi)) {
else if (checkArgument(Arg.PROCESSOR, args, argi)) {
languageId = args[++argi];
}
else if (checkArgument("-cspec", args, argi)) {
else if (checkArgument(Arg.CSPEC, args, argi)) {
compilerSpecId = args[++argi];
}
else if (checkArgument("-prescript", args, argi)) {
else if (checkArgument(Arg.PRE_SCRIPT, args, argi)) {
String scriptName = args[++argi];
String[] scriptArgs = getSubArguments(args, argi);
String[] scriptArgs = getSubArguments(args, argi, ALL_ARG_NAMES);
argi += scriptArgs.length;
preScripts.add(new Pair<>(scriptName, scriptArgs));
}
else if (checkArgument("-postscript", args, argi)) {
else if (checkArgument(Arg.POST_SCRIPT, args, argi)) {
String scriptName = args[++argi];
String[] scriptArgs = getSubArguments(args, argi);
String[] scriptArgs = getSubArguments(args, argi, ALL_ARG_NAMES);
argi += scriptArgs.length;
postScripts.add(new Pair<>(scriptName, scriptArgs));
}
else if (checkArgument("-scriptPath", args, argi)) {
else if (checkArgument(Arg.SCRIPT_PATH, args, argi)) {
options.setScriptDirectories(args[++argi]);
}
else if (checkArgument("-propertiesPath", args, argi)) {
else if (checkArgument(Arg.PROPERTIES_PATH, args, argi)) {
options.setPropertiesFileDirectories(args[++argi]);
}
else if (checkArgument("-import", args, argi)) {
else if (checkArgument(Arg.IMPORT, args, argi)) {
File inputFile = null;
try {
inputFile = new File(args[++argi]);
@ -242,7 +311,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
nextArg = args[++argi];
// Check if next argument is a parameter
if (nextArg.charAt(0) == '-') {
if (ALL_ARG_NAMES.contains(nextArg)) {
argi--;
break;
}
@ -258,29 +327,29 @@ public class AnalyzeHeadless implements GhidraLaunchable {
filesToImport.add(otherFile);
}
}
else if ("-connect".equals(args[argi])) {
else if (checkArgument(Arg.CONNECT, args, argi)) {
if ((argi + 1) < args.length) {
arg = args[argi + 1];
if (!arg.startsWith("-")) {
if (!ALL_ARG_NAMES.contains(arg)) {
// serverUID is optional argument after -connect
serverUID = arg;
userId = arg;
++argi;
}
}
}
else if ("-commit".equals(args[argi])) {
else if (checkArgument(Arg.COMMIT, args, argi)) {
String comment = null;
if ((argi + 1) < args.length) {
arg = args[argi + 1];
if (!arg.startsWith("-")) {
// comment is optional argument after -commit
if (!ALL_ARG_NAMES.contains(arg)) {
// commit is optional argument after -commit
comment = arg;
++argi;
}
}
options.setCommitFiles(true, comment);
}
else if (checkArgument("-keystore", args, argi)) {
else if (checkArgument(Arg.KEYSTORE, args, argi)) {
keystorePath = args[++argi];
File keystore = new File(keystorePath);
if (!keystore.isFile()) {
@ -288,13 +357,13 @@ public class AnalyzeHeadless implements GhidraLaunchable {
keystore.getAbsolutePath() + " is not a valid keystore file.");
}
}
else if (arg.equalsIgnoreCase("-p")) {
else if (checkArgument(Arg.PASSWORD, args, argi)) {
allowPasswordPrompt = true;
}
else if ("-analysisTimeoutPerFile".equalsIgnoreCase(args[argi])) {
else if (checkArgument(Arg.ANALYSIS_TIMEOUT_PER_FILE, args, argi)) {
options.setPerFileAnalysisTimeout(args[++argi]);
}
else if ("-process".equals(args[argi])) {
else if (checkArgument(Arg.PROCESS, args, argi)) {
if (options.runScriptsNoImport) {
throw new InvalidInputException(
"The -process option may only be specified once.");
@ -302,7 +371,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
String processBinary = null;
if ((argi + 1) < args.length) {
arg = args[argi + 1];
if (!arg.startsWith("-")) {
if (!ALL_ARG_NAMES.contains(arg)) {
// processBinary is optional argument after -process
processBinary = arg;
++argi;
@ -310,11 +379,11 @@ public class AnalyzeHeadless implements GhidraLaunchable {
}
options.setRunScriptsNoImport(true, processBinary);
}
else if ("-recursive".equals(args[argi])) {
else if (checkArgument(Arg.RECURSIVE, args, argi)) {
Integer depth = null;
if ((argi + 1) < args.length) {
arg = args[argi + 1];
if (!arg.startsWith("-")) {
if (!ALL_ARG_NAMES.contains(arg)) {
// depth is optional argument after -recursive
try {
depth = Integer.parseInt(arg);
@ -327,10 +396,10 @@ public class AnalyzeHeadless implements GhidraLaunchable {
}
options.enableRecursiveProcessing(true, depth);
}
else if ("-readOnly".equalsIgnoreCase(args[argi])) {
else if (checkArgument(Arg.READ_ONLY, args, argi)) {
options.enableReadOnlyProcessing(true);
}
else if (checkArgument("-max-cpu", args, argi)) {
else if (checkArgument(Arg.MAX_CPU, args, argi)) {
String cpuVal = args[++argi];
try {
options.setMaxCpu(Integer.parseInt(cpuVal));
@ -339,12 +408,15 @@ public class AnalyzeHeadless implements GhidraLaunchable {
throw new InvalidInputException("Invalid value for max-cpu: " + cpuVal);
}
}
else if ("-okToDelete".equalsIgnoreCase(args[argi])) {
else if (checkArgument(Arg.OK_TO_DELETE, args, argi)) {
options.setOkToDelete(true);
}
else if (checkArgument("-librarySearchPaths", args, argi)) {
else if (checkArgument(Arg.LIBRARY_SEARCH_PATHS, args, argi)) {
LibrarySearchPathManager.setLibraryPaths(args[++argi].split(";"));
}
else if (ALL_ARG_NAMES.contains(args[argi])) {
throw new AssertionError("Valid option was not processed: " + args[argi]);
}
else {
throw new InvalidInputException("Bad argument: " + arg);
}
@ -362,7 +434,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
// Set up optional Ghidra Server authenticator
try {
options.setClientCredentials(serverUID, keystorePath, allowPasswordPrompt);
options.setClientCredentials(userId, keystorePath, allowPasswordPrompt);
}
catch (IOException e) {
throw new InvalidInputException(
@ -438,47 +510,48 @@ public class AnalyzeHeadless implements GhidraLaunchable {
* @param execCmd the command used to run the headless analyzer from the calling method.
*/
public static void usage(String execCmd) {
System.out.println("Headless Analyzer Usage: " + execCmd);
System.out.println(" <project_location> <project_name>[/<folder_path>]");
System.out.println(
" | ghidra://<server>[:<port>]/<repository_name>[/<folder_path>]");
System.out.println(
" [[-import [<directory>|<file>]+] | [-process [<project_file>]]]");
System.out.println(" [-preScript <ScriptName>]");
System.out.println(" [-postScript <ScriptName>]");
System.out.println(" [-scriptPath \"<path1>[;<path2>...]\"]");
System.out.println(" [-propertiesPath \"<path1>[;<path2>...]\"]");
System.out.println(" [-scriptlog <path to script log file>]");
System.out.println(" [-log <path to log file>]");
System.out.println(" [-overwrite]");
System.out.println(" [-recursive]");
System.out.println(" [-readOnly]");
System.out.println(" [-deleteProject]");
System.out.println(" [-noanalysis]");
System.out.println(" [-processor <languageID>]");
System.out.println(" [-cspec <compilerSpecID>]");
System.out.println(" [-analysisTimeoutPerFile <timeout in seconds>]");
System.out.println(" [-keystore <KeystorePath>]");
System.out.println(" [-connect <userID>]");
System.out.println(" [-p]");
System.out.println(" [-commit [\"<comment>\"]]");
System.out.println(" [-okToDelete]");
System.out.println(" [-max-cpu <max cpu cores to use>]");
System.out.println(" [-loader <desired loader name>]");
// ** NOTE: please update 'analyzeHeadlessREADME.html' if changing command line parameters **
StringBuilder sb = new StringBuilder();
final String INDENT = " ";
sb.append("Headless Analyzer Usage: %s\n".formatted(execCmd));
sb.append(INDENT + "<project_location> <project_name>[/<folder_path>]\n");
sb.append(INDENT + " | ghidra://<server>[:<port>]/<repository_name>[/<folder_path>]\n");
for (Arg arg : Arg.values()) {
switch (arg) {
case IMPORT -> {
// Can't use both IMPORT and PROCESS, so must handle the usage a little
// differently
sb.append(
INDENT + "[[%s] | [%s]]\n".formatted(arg.usage(), Arg.PROCESS.usage()));
}
case PROCESS -> {
// Handled above by IMPORT
}
case LOADER_ARGS -> {
// Loader args are a little different because we don't know the full
// argument name ahead of time...just what it starts with
sb.append(INDENT + "[%s<loader argument name> %s]\n"
.formatted(Arg.LOADER_ARGS.name, Arg.LOADER_ARGS.subArgFormat));
}
default -> {
sb.append(INDENT + "[%s]\n".formatted(arg.usage()));
}
}
}
if (Platform.CURRENT_PLATFORM.getOperatingSystem() != OperatingSystem.WINDOWS) {
System.out.println();
System.out.println(
sb.append("\n");
sb.append(
" - All uses of $GHIDRA_HOME or $USER_HOME in script path must be" +
" preceded by '\\'");
" preceded by '\\'\n");
}
System.out.println();
System.out.println(
sb.append("\n");
sb.append(
"Please refer to 'analyzeHeadlessREADME.html' for detailed usage examples " +
"and notes.");
"and notes.\n");
System.out.println();
sb.append("\n");
System.out.println(sb);
System.exit(EXIT_CODE_ERROR);
}
@ -486,23 +559,22 @@ public class AnalyzeHeadless implements GhidraLaunchable {
usage("analyzeHeadless");
}
private String[] getSubArguments(String[] args, int argi) {
List<String> subArgs = new LinkedList<>();
private String[] getSubArguments(String[] args, int argi, Set<String> argNames) {
List<String> subArgs = new ArrayList<>();
int i = argi + 1;
while (i < args.length && !args[i].startsWith("-")) {
while (i < args.length && !argNames.contains(args[i])) {
subArgs.add(args[i++]);
}
return subArgs.toArray(new String[0]);
return subArgs.toArray(new String[subArgs.size()]);
}
private boolean checkArgument(String optionName, String[] args, int argi)
private boolean checkArgument(Arg arg, String[] args, int argi)
throws InvalidInputException {
// everything after this requires an argument
if (!optionName.equalsIgnoreCase(args[argi])) {
if (!arg.matches(args[argi])) {
return false;
}
if (argi + 1 == args.length) {
throw new InvalidInputException(optionName + " requires an argument");
if (arg.requiresSubArgs && argi + 1 == args.length) {
throw new InvalidInputException(args[argi] + " requires an argument");
}
return true;
}

View File

@ -132,6 +132,7 @@ The Headless Analyzer uses the command-line parameters discussed below. See <a h
[<a href="#max-cpu">-max-cpu &lt;max cpu cores to use&gt;</a>]
[<a href="#librarySearchPaths">-librarySearchPaths &lt;path1&gt;[;&lt;path2&gt;...]</a>]
[<a href="#loader">-loader &lt;desired loader name&gt;</a>]
[<a href="#loader">-loader-&lt;loader argument name&gt; &lt;loader argument value&gt;</a>]
</PRE>