diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java index b60925091e..5cd2db6daf 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java @@ -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. + *

+ * NOTE: Please update 'analyzeHeadlessREADME.html' if changing command line parameters + */ + private enum Arg { + //@formatter:off + IMPORT("-import", true, "[|]+"), + PROCESS("-process", true, "[]"), + PRE_SCRIPT("-prescript", true, ""), + POST_SCRIPT("-postscript", true, ""), + SCRIPT_PATH("-scriptPath", true, "\"[;...]\""), + PROPERTIES_PATH("-propertiesPath", true, "\"[;...]\""), + SCRIPT_LOG("-scriptlog", true, ""), + LOG("-log", true, ""), + OVERWRITE("-overwrite", false), + RECURSIVE("-recursive", false), + READ_ONLY("-readOnly", false), + DELETE_PROJECT("-deleteproject", false), + NO_ANALYSIS("-noanalysis", false), + PROCESSOR("-processor", true, ""), + CSPEC("-cspec", true, ""), + ANALYSIS_TIMEOUT_PER_FILE("-analysisTimeoutPerFile", true, ""), + KEYSTORE("-keystore", true, ""), + CONNECT("-connect", false, "[]"), + PASSWORD("-p", false), + COMMIT("-commit", false, "[\"\"]]"), + OK_TO_DELETE("-okToDelete", false), + MAX_CPU("-max-cpu", true, ""), + LIBRARY_SEARCH_PATHS("-librarySearchPaths", true, "[;...]"), + LOADER("-loader", true, ""), + LOADER_ARGS(Loader.COMMAND_LINE_ARG_PREFIX + "-", true, "") { + @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 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> preScripts = new LinkedList<>(); List> 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(" [/]"); - System.out.println( - " | ghidra://[:]/[/]"); - System.out.println( - " [[-import [|]+] | [-process []]]"); - System.out.println(" [-preScript ]"); - System.out.println(" [-postScript ]"); - System.out.println(" [-scriptPath \"[;...]\"]"); - System.out.println(" [-propertiesPath \"[;...]\"]"); - System.out.println(" [-scriptlog ]"); - System.out.println(" [-log ]"); - System.out.println(" [-overwrite]"); - System.out.println(" [-recursive]"); - System.out.println(" [-readOnly]"); - System.out.println(" [-deleteProject]"); - System.out.println(" [-noanalysis]"); - System.out.println(" [-processor ]"); - System.out.println(" [-cspec ]"); - System.out.println(" [-analysisTimeoutPerFile ]"); - System.out.println(" [-keystore ]"); - System.out.println(" [-connect ]"); - System.out.println(" [-p]"); - System.out.println(" [-commit [\"\"]]"); - System.out.println(" [-okToDelete]"); - System.out.println(" [-max-cpu ]"); - System.out.println(" [-loader ]"); - // ** 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 + " [/]\n"); + sb.append(INDENT + " | ghidra://[:]/[/]\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 %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 subArgs = new LinkedList<>(); + private String[] getSubArguments(String[] args, int argi, Set argNames) { + List 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; } diff --git a/Ghidra/RuntimeScripts/Common/support/analyzeHeadlessREADME.html b/Ghidra/RuntimeScripts/Common/support/analyzeHeadlessREADME.html index d4f7376435..963a5f2161 100644 --- a/Ghidra/RuntimeScripts/Common/support/analyzeHeadlessREADME.html +++ b/Ghidra/RuntimeScripts/Common/support/analyzeHeadlessREADME.html @@ -132,6 +132,7 @@ The Headless Analyzer uses the command-line parameters discussed below. See -max-cpu <max cpu cores to use>] [-librarySearchPaths <path1>[;<path2>...]] [-loader <desired loader name>] + [-loader-<loader argument name> <loader argument value>]