> loaderArgs)
491 | throws InvalidInputException {
492 | if (loaderName != null) {
493 | this.loaderClass = LoaderService.getLoaderClassByName(loaderName);
494 | if (this.loaderClass == null) {
495 | throw new InvalidInputException("Invalid loader name specified: " + loaderName);
496 | }
497 | this.loaderArgs = loaderArgs;
498 | }
499 | else {
500 | if (loaderArgs != null && loaderArgs.size() > 0) {
501 | throw new InvalidInputException(
502 | "Loader arguments defined without a loader being specified.");
503 | }
504 | this.loaderClass = null;
505 | this.loaderArgs = null;
506 | }
507 | }
508 | }
509 |
--------------------------------------------------------------------------------
/GhidraLib/LibHeadlessScript.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | /*
18 | Modified version of the Headless analyzer code, for any feedback, contact NADER SHALLABI at nader@nosecurecode.com
19 | */
20 |
21 | package com.nosecurecode.libghidra;
22 |
23 | import java.io.IOException;
24 |
25 | import generic.jar.ResourceFile;
26 | import ghidra.app.script.*;
27 | import ghidra.framework.model.DomainFolder;
28 | import ghidra.util.InvalidNameException;
29 |
30 | /**
31 | * This class is analogous to GhidraScript, except that is only meant to be used with
32 | * the HeadlessAnalyzer. That is, if a user writes a script that extends HeadlessScript,
33 | * it should only be run in the Headless environment.
34 | */
35 | public abstract class LibHeadlessScript extends GhidraScript {
36 |
37 | /**
38 | * Options for controlling disposition of program after the current script completes.
39 | */
40 | public enum LibHeadlessContinuationOption {
41 | /**
42 | * Continue running scripts and/or analysis; -import and -process
43 | * modes complete normally.
44 | */
45 | CONTINUE,
46 |
47 | /**
48 | * Continue running scripts and/or analysis;
49 | * -import mode does not save program,
50 | * -process mode deletes program.
51 | */
52 | CONTINUE_THEN_DELETE,
53 |
54 | /**
55 | * Abort any scripts or analysis that come after this script;
56 | * -import mode does not save program, -process mode deletes program.
57 | */
58 | ABORT_AND_DELETE,
59 |
60 | /**
61 | * Abort any scripts or analysis that come after this script; -import mode does
62 | * save program (but it may not be processed completely),
63 | * -process mode completes normally, minus scripts or analysis that
64 | * runs after the ABORT request.
65 | */
66 | ABORT
67 | }
68 |
69 | private LibHeadlessAnalyzer headless = null;
70 |
71 | private LibHeadlessContinuationOption currentOption = LibHeadlessContinuationOption.CONTINUE;
72 | private LibHeadlessContinuationOption scriptSetOption = null;
73 |
74 | private boolean runningInnerScript = false;
75 |
76 | // This is necessary because it determine when we nullify the 'scriptSetOption' variable
77 | private void setRunningInnerScript(boolean b) {
78 | runningInnerScript = b;
79 | }
80 |
81 | /**
82 | * Sets the current headless instance -- doing so gives the user the ability to manipulate
83 | * headless analyzer-specific parameters.
84 | *
85 | * This method is declared with no access modifier to only allow package-level (no subclass)
86 | * access. This method is meant to only be used by the HeadlessAnalyzer class.
87 | *
88 | * @param ha HeadlessAnalyzer instance
89 | */
90 | void setHeadlessInstance(LibHeadlessAnalyzer ha) {
91 | headless = ha;
92 | }
93 |
94 | /**
95 | * Sets the "beginning-of-script" continuation status.
96 | *
97 | * This method is declare with no access modifier to only allow package-level (no
98 | * subclass) access. This method is meant to only be used by the HeadlessAnalyzer class.
99 | *
100 | * @param option initial continuation option for this script
101 | */
102 | void setInitialContinuationOption(LibHeadlessContinuationOption option) {
103 | currentOption = option;
104 | }
105 |
106 | /**
107 | * Returns the final resolved continuation option (after script processing is done).
108 | *
109 | * The continuation option specifies whether to continue or abort follow-on processing,
110 | * and whether to delete or keep the current program.
111 | *
112 | * This method is declared with no access modifier to only allow package-level (no
113 | * subclass) access. This method is meant to only be used by the HeadlessAnalyzer class.
114 | *
115 | * @return the script's final HeadlessContinuationOption
116 | */
117 | LibHeadlessContinuationOption getContinuationOption() {
118 | return currentOption;
119 | }
120 |
121 | /**
122 | * Checks to see if this script is running in headless mode (it should be!).
123 | *
124 | * This method should be called at the beginning of every public method in HeadlessScript
125 | * that accesses HeadlessAnalyzer methods (for instance, 'headless.isAnalysisEnabled()').
126 | * The call to this method can not be placed in the constructor, because 'setHeadlessInstance',
127 | * which connects the script with the current headless instance, is not called until after the
128 | * call to the constructor.
129 | *
130 | * @throws ImproperUseException if not in headless mode or headless instance not set
131 | */
132 | private void checkHeadlessStatus() throws ImproperUseException {
133 | if (headless == null || !isRunningHeadless()) {
134 | throw new ImproperUseException("This method can only be used in the headless case!");
135 | }
136 | }
137 |
138 | /**
139 | * Stores a key/value pair in the HeadlessAnalyzer instance for later use.
140 | *
141 | * This method, along with the 'getStoredHeadlessValue' method, is useful for debugging and
142 | * testing the Headless Analyzer (when the user has directly instantiated the HeadlessAnalyzer
143 | * instead of running it from analyzeHeadless.sh or analyzeHeadless.bat). This method is
144 | * intended to allow a HeadlessScript to store variables that reflect the current state of
145 | * processing (at the time the script is being run). Storing variables in the HeadlessAnalyzer
146 | * instance may be the only way to access the state of processing during cases when the user
147 | * is forced to run in -readOnly mode, or if there is a value that is only accessible at the
148 | * scripts stage.
149 | *
150 | * @param key storage key in String form
151 | * @param value value to store
152 | * @throws ImproperUseException if not in headless mode or headless instance not set
153 | * @see #getStoredHeadlessValue(String)
154 | * @see #headlessStorageContainsKey(String)
155 | */
156 | public void storeHeadlessValue(String key, Object value) throws ImproperUseException {
157 | checkHeadlessStatus();
158 | headless.addVariableToStorage(key, value);
159 | }
160 |
161 | /**
162 | * Get stored value by key from the HeadlessAnalyzer instance.
163 | *
164 | * This method, along with the 'storedHeadlessValue' method, is useful for debugging and
165 | * testing the Headless Analyzer (when the user has directly instantiated the HeadlessAnalyzer
166 | * instead of running it from analyzeHeadless.sh or analyzeHeadless.bat). This method is
167 | * intended to allow a HeadlessScript to store variables that reflect the current state of
168 | * processing (at the time the script is being run). Storing variables in the HeadlessAnalyzer
169 | * instance may be the only way to access the state of processing during cases when the user
170 | * is forced to run in -readOnly mode, or if there is a value that is only accessible at the
171 | * scripts stage.
172 | *
173 | * @param key key to retrieve the desired stored value
174 | * @return stored Object, or null if none exists for that key
175 | * @throws ImproperUseException if not in headless mode or headless instance not set
176 | * @see #storeHeadlessValue(String, Object)
177 | * @see #headlessStorageContainsKey(String)
178 | */
179 | public Object getStoredHeadlessValue(String key) throws ImproperUseException {
180 | checkHeadlessStatus();
181 | return headless.getVariableFromStorage(key);
182 | }
183 |
184 | /**
185 | * Returns whether the specified key was stored in the HeadlessAnalyzer instance.
186 | *
187 | * @param key value of key to check for in Headless Analyzer instance
188 | * @return true if the specified key exists
189 | * @throws ImproperUseException if not in headless mode or headless instance not set
190 | * @see #storeHeadlessValue(String, Object)
191 | * @see #getStoredHeadlessValue(String)
192 | */
193 | public boolean headlessStorageContainsKey(String key) throws ImproperUseException {
194 | checkHeadlessStatus();
195 | return headless.storageContainsKey(key);
196 | }
197 |
198 | /**
199 | * Sets the continuation option for this script
200 | *
201 | * The continuation option specifies whether to continue or abort follow-on processing,
202 | * and whether to delete or keep the current program.
203 | *
204 | * @param option HeadlessContinuationOption set by this script
205 | * @see #getHeadlessContinuationOption()
206 | */
207 | public void setHeadlessContinuationOption(LibHeadlessContinuationOption option) {
208 | scriptSetOption = option;
209 | }
210 |
211 | /**
212 | * Returns the continuation option for the current script (if one has not been set in this
213 | * script, the option defaults to CONTINUE).
214 | *
215 | * The continuation option specifies whether to continue or abort follow-on processing,
216 | * and whether to delete or keep the current program.
217 | *
218 | * @return the current HeadlessContinuationOption
219 | * @see #setHeadlessContinuationOption(LibHeadlessContinuationOption)
220 | */
221 | public LibHeadlessContinuationOption getHeadlessContinuationOption() {
222 | if (scriptSetOption == null) {
223 | return LibHeadlessContinuationOption.CONTINUE;
224 | }
225 |
226 | return scriptSetOption;
227 | }
228 |
229 | /**
230 | * Enables or disables analysis according to the passed-in boolean value.
231 | *
232 | * A script that calls this method should run as a 'preScript', since preScripts
233 | * execute before analysis would typically run. Running the script as a 'postScript'
234 | * is ineffective, since the stage at which analysis would have happened has already
235 | * passed.
236 | *
237 | * This change will persist throughout the current HeadlessAnalyzer session, unless
238 | * changed again (in other words, once analysis is enabled via script for one program,
239 | * it will also be enabled for future programs in the current session, unless changed).
240 | *
241 | * @param b true to enable analysis, false to disable analysis
242 | * @throws ImproperUseException if not in headless mode or headless instance not set
243 | * @see #isHeadlessAnalysisEnabled()
244 | */
245 | public void enableHeadlessAnalysis(boolean b) throws ImproperUseException {
246 | checkHeadlessStatus();
247 |
248 | headless.getOptions().enableAnalysis(b);
249 | }
250 |
251 | /**
252 | * Returns whether analysis is currently enabled or disabled in the HeadlessAnalyzer.
253 | *
254 | * @return whether analysis has been enabled or not
255 | * @throws ImproperUseException if not in headless mode or headless instance not set
256 | * @see #enableHeadlessAnalysis(boolean)
257 | */
258 | public boolean isHeadlessAnalysisEnabled() throws ImproperUseException {
259 | checkHeadlessStatus();
260 |
261 | return headless.getOptions().analyze;
262 | }
263 |
264 | /**
265 | * Returns whether the headless analyzer is currently set to -import mode or not (if not,
266 | * it is in -process mode). The use of -import mode implies that binaries are actively being
267 | * imported into the project (with optional scripts/analysis). The use of -process mode implies
268 | * that existing project files are being processed (using scripts and/or analysis).
269 | *
270 | * @return whether we are in -import mode or not
271 | * @throws ImproperUseException if not in headless mode or headless instance not set
272 | */
273 | public boolean isImporting() throws ImproperUseException {
274 | checkHeadlessStatus();
275 |
276 | return !headless.getOptions().runScriptsNoImport;
277 | }
278 |
279 | /**
280 | * Changes the path in the Ghidra project where imported files are saved.
281 | * The passed-in path is assumed to be relative to the project root. For example,
282 | * if the directory structure for the Ghidra project looks like this:
283 | *
284 | *
285 | * MyGhidraProject:
286 | * /dir1
287 | * /innerDir1
288 | * /innerDir2
289 | *
290 | *
291 | * Then the following usage would ensure that any files imported after this call would
292 | * be saved in the MyGhidraProject:/dir1/innerDir2 folder.
293 | *
294 | * setHeadlessImportDirectory("dir1/innerDir2");
295 | *
296 | * In contrast, the following usages would add new folders to the Ghidra project and save
297 | * the imported files into the newly-created path:
298 | *
299 | * setHeadlessImportDirectory("innerDir2/my/folder");
300 | *
301 | * changes the directory structure to:
302 | *
303 | * MyGhidraProject:
304 | * /dir1
305 | * /innerDir1
306 | * /innerDir2
307 | * /my
308 | * /folder
309 | *
310 | * and:
311 | *
312 | * setHeadlessImportDirectory("newDir/saveHere");
313 | *
314 | * changes the directory structure to:
315 | *
316 | * MyGhidraProject:
317 | * /dir1
318 | * /innerDir1
319 | * /innerDir2
320 | * /newDir
321 | * /saveHere
322 | *
323 | * As in the examples above, if the desired folder does not already exist, it is created.
324 | *
325 | * A change in the import save folder will persist throughout the current HeadlessAnalyzer
326 | * session, unless changed again (in other words, once the import directory has been changed,
327 | * it will remain the 'save' directory for import files in the current session, unless changed).
328 | *
329 | * To revert back to the default import location (that which was specified via command line),
330 | * pass the null object as the argument to this method, as below:
331 | *
332 | * setHeadlessImportDirectory(null); // Sets import save directory to default
333 | *
334 | * If a file with the same name already exists in the desired location, it will only be
335 | * overwritten if "-overwrite" is true.
336 | *
337 | * This method is only applicable when using the HeadlessAnalyzer -import mode and
338 | * is ineffective in -process mode.
339 | *
340 | * @param importDir the absolute path (relative to root) where inputs will be saved
341 | * @throws ImproperUseException if not in headless mode or headless instance not set
342 | * @throws IOException if there are issues creating the folder
343 | * @throws InvalidNameException if folder name is invalid
344 | */
345 | public void setHeadlessImportDirectory(String importDir)
346 | throws ImproperUseException, IOException, InvalidNameException {
347 | checkHeadlessStatus();
348 |
349 | // Do nothing if not importing -- we don't want to have arbitrary folders
350 | // created when not being used!
351 |
352 | if (!headless.getOptions().runScriptsNoImport) {
353 | DomainFolder saveFolder = null;
354 |
355 | if (importDir != null) {
356 |
357 | if (!importDir.startsWith("/")) {
358 | importDir = "/" + importDir;
359 | }
360 |
361 | // Add ending slash so the dir gets created for server projects
362 | if (!importDir.endsWith("/")) {
363 | importDir += "/";
364 | }
365 |
366 | // Gets folder -- creates path if it doesn't already exist
367 | saveFolder = headless.getDomainFolder(importDir, true);
368 | }
369 |
370 | headless.setSaveFolder(saveFolder);
371 | }
372 | }
373 |
374 | /**
375 | * Returns whether analysis for the current program has timed out.
376 | *
377 | * Analysis will time out only in the case where:
378 | *
379 | * - the users has set an analysis timeout period using the
-analysisTimeoutPerFile
380 | * parameter
381 | * - analysis is enabled and has completed
382 | * - the current script is being run as a postScript (since postScripts run after
383 | * analysis)
384 | *
385 | *
386 | * @return whether analysis timeout occurred
387 | * @throws ImproperUseException if not in headless mode or headless instance not set
388 | */
389 | public boolean analysisTimeoutOccurred() throws ImproperUseException {
390 | checkHeadlessStatus();
391 | return headless.checkAnalysisTimedOut();
392 | }
393 |
394 | @Override
395 | public void runScript(String scriptName, String[] scriptArguments, GhidraState scriptState)
396 | throws Exception {
397 |
398 | boolean isHeadlessScript = false;
399 |
400 | if (scriptSetOption != null) {
401 | resolveContinuationOptionWith(scriptSetOption);
402 | scriptSetOption = null;
403 | }
404 | ResourceFile scriptSource = GhidraScriptUtil.findScriptByName(scriptName);
405 | if (scriptSource != null) {
406 | GhidraScriptProvider provider = GhidraScriptUtil.getProvider(scriptSource);
407 |
408 | if (provider == null) {
409 | throw new IOException("Attempting to run subscript '" + scriptName +
410 | "': unable to run this script type.");
411 | }
412 |
413 | GhidraScript script = provider.getScriptInstance(scriptSource, writer);
414 | isHeadlessScript = script instanceof LibHeadlessScript ? true : false;
415 |
416 | if (potentialPropertiesFileLocs.size() > 0) {
417 | script.setPotentialPropertiesFileLocations(potentialPropertiesFileLocs);
418 | }
419 |
420 | if (scriptState == state) {
421 | updateStateFromVariables();
422 | }
423 |
424 | if (isHeadlessScript) {
425 | ((LibHeadlessScript) script).setHeadlessInstance(headless);
426 | ((LibHeadlessScript) script).setRunningInnerScript(true);
427 | }
428 |
429 | script.setScriptArgs(scriptArguments);
430 |
431 | script.execute(scriptState, monitor, writer);
432 |
433 | if (scriptState == state) {
434 | loadVariablesFromState();
435 | }
436 |
437 | // Resolve continuations options, if they have changed
438 | if (isHeadlessScript) {
439 | LibHeadlessContinuationOption innerScriptOpt =
440 | ((LibHeadlessScript) script).getHeadlessContinuationOption();
441 |
442 | if (innerScriptOpt != null) {
443 | resolveContinuationOptionWith(innerScriptOpt);
444 | }
445 |
446 | ((LibHeadlessScript) script).setRunningInnerScript(false);
447 | }
448 |
449 | return;
450 | }
451 |
452 | throw new IllegalArgumentException("Script does not exist: " + scriptName);
453 | }
454 |
455 | @Override
456 | public void cleanup(boolean success) {
457 | resolveContinuationOption();
458 |
459 | if (!runningInnerScript) {
460 | scriptSetOption = null;
461 | }
462 | }
463 |
464 | private void resolveContinuationOption() {
465 | resolveContinuationOptionWith(scriptSetOption);
466 | }
467 |
468 | /**
469 | * Resolve continuation options according to the table in 'analyzeHeadlessREADME.html'.
470 | * (See "Multiple Scripts" section).
471 | *
472 | * @param opt continuation option to combine with current continuation option
473 | */
474 | private void resolveContinuationOptionWith(LibHeadlessContinuationOption opt) {
475 |
476 | if (opt == null) {
477 | return;
478 | }
479 |
480 | switch (currentOption) {
481 |
482 | case CONTINUE:
483 | currentOption = opt;
484 | break;
485 |
486 | case CONTINUE_THEN_DELETE:
487 | switch (opt) {
488 | case ABORT:
489 |
490 | case ABORT_AND_DELETE:
491 | currentOption = LibHeadlessContinuationOption.ABORT_AND_DELETE;
492 | break;
493 |
494 | default:
495 | break;
496 | }
497 | break;
498 |
499 | case ABORT_AND_DELETE:
500 | // nothing changes
501 | break;
502 |
503 | case ABORT:
504 | // nothing changes
505 | break;
506 | }
507 | }
508 | }
509 |
--------------------------------------------------------------------------------
/GhidraLib/LibHeadlessAnalyzer.java:
--------------------------------------------------------------------------------
1 | /* ###
2 | * IP: GHIDRA
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | /*
18 | Modified version of the Headless analyzer code, for any feedback, contact NADER SHALLABI at nader@nosecurecode.com
19 | */
20 |
21 | package com.nosecurecode.libghidra;
22 |
23 | import java.io.*;
24 | import java.net.*;
25 | import java.util.*;
26 | import java.util.regex.Pattern;
27 |
28 | import generic.jar.ResourceFile;
29 | import generic.stl.Pair;
30 | import generic.util.Path;
31 | import ghidra.GhidraApplicationLayout;
32 | import ghidra.GhidraJarApplicationLayout;
33 | import ghidra.app.plugin.core.analysis.AutoAnalysisManager;
34 | import ghidra.app.plugin.core.osgi.BundleHost;
35 | import ghidra.app.script.*;
36 | import com.nosecurecode.libghidra.LibHeadlessScript.LibHeadlessContinuationOption;
37 | import ghidra.app.util.importer.AutoImporter;
38 | import ghidra.app.util.importer.MessageLog;
39 | import ghidra.app.util.opinion.BinaryLoader;
40 | import ghidra.framework.*;
41 | import ghidra.framework.client.ClientUtil;
42 | import ghidra.framework.client.RepositoryAdapter;
43 | import ghidra.framework.data.*;
44 | import ghidra.framework.model.*;
45 | import ghidra.framework.project.DefaultProject;
46 | import ghidra.framework.project.DefaultProjectManager;
47 | import ghidra.framework.protocol.ghidra.*;
48 | import ghidra.framework.remote.User;
49 | import ghidra.framework.store.LockException;
50 | import ghidra.framework.store.local.LocalFileSystem;
51 | import ghidra.program.database.ProgramContentHandler;
52 | import ghidra.program.database.ProgramDB;
53 | import ghidra.program.model.address.AddressSetView;
54 | import ghidra.program.model.listing.Program;
55 | import ghidra.program.util.GhidraProgramUtilities;
56 | import ghidra.program.util.ProgramLocation;
57 | import ghidra.util.*;
58 | import ghidra.util.exception.*;
59 | import ghidra.util.task.TaskMonitor;
60 | import utilities.util.FileUtilities;
61 |
62 | /**
63 | * The class used kick-off and interact with headless processing. All headless options have been
64 | * broken out into their own class: {@link LibHeadlessOptions}. This class is intended to be used
65 | * one of two ways:
66 | *
67 | * - Used by {@link LibGhidra} to perform headless analysis based on arguments specified
68 | * on the command line.
69 | * - Used by another tool as a library to perform headless analysis.
70 | *
71 | *
72 | * Note: This class is not thread safe.
73 | */
74 | public class LibHeadlessAnalyzer {
75 |
76 | private static LibHeadlessAnalyzer instance;
77 |
78 | private LibHeadlessOptions options;
79 | private HeadlessGhidraProjectManager projectManager;
80 | private Project project;
81 | private boolean analysisTimedOut;
82 | private DomainFolder saveDomainFolder;
83 | private Map storage;
84 | private URLClassLoader classLoaderForDotClassScripts;
85 |
86 | private LibProgramHandler programHandler = null;
87 |
88 | /**
89 | * Gets a headless analyzer, initializing the application if necessary with the specified
90 | * logging parameters. An {@link IllegalStateException} will be thrown if the application has
91 | * already been initialized or a headless analyzer has already been retrieved. In these cases,
92 | * the headless analyzer should be gotten with {@link LibHeadlessAnalyzer#getInstance(LibProgramHandler)}.
93 | *
94 | * @param logFile The desired application log file. If null, no application logging will take place.
95 | * @param scriptLogFile The desired scripting log file. If null, no script logging will take place.
96 | * @param useLog4j true if log4j is to be used; otherwise, false. If this class is being used by
97 | * another tool as a library, using log4j might interfere with that tool.
98 | * @return An instance of a new headless analyzer.
99 | * @throws IllegalStateException if an application or headless analyzer instance has already been initialized.
100 | * @throws IOException if there was a problem reading the application.properties file.
101 | */
102 | public static LibHeadlessAnalyzer getLoggableInstance(File logFile, File scriptLogFile,
103 | boolean useLog4j, LibProgramHandler handler) throws IllegalStateException, IOException {
104 |
105 | // Prevent more than one headless analyzer from being instantiated. Too much about it
106 | // messes with global system settings, so under the current design of Ghidra, allowing
107 | // more than one to exist could result in unpredictable behavior.
108 | if (instance != null) {
109 | throw new IllegalStateException(
110 | "A headless analzyer instance has already been retrieved. " +
111 | "Use HeadlessAnalyzer.getInstance() to get it.");
112 | }
113 |
114 | // Cannot set logging because application has already been initialized.
115 | if (Application.isInitialized()) {
116 | throw new IllegalStateException(
117 | "Logging cannot be set because the application has already been initialized. " +
118 | "Use HeadlessAnalyzer.getInstance() to get the headless analyzer.");
119 | }
120 |
121 | // Initialize application with the provided logging parameters
122 | ApplicationConfiguration configuration = new HeadlessGhidraApplicationConfiguration();
123 | if (useLog4j) {
124 | if (logFile != null) {
125 | configuration.setApplicationLogFile(logFile);
126 | }
127 | if (scriptLogFile != null) {
128 | configuration.setScriptLogFile(scriptLogFile);
129 | }
130 | }
131 | else {
132 | configuration.setInitializeLogging(false);
133 | Msg.setErrorLogger(new LibHeadlessErrorLogger(logFile));
134 | }
135 | Application.initializeApplication(getApplicationLayout(), configuration);
136 |
137 | // Instantiate and return singleton headless analyzer
138 | instance = new LibHeadlessAnalyzer();
139 |
140 | // Set our program handler
141 | instance.programHandler = handler;
142 |
143 | return instance;
144 | }
145 |
146 | /**
147 | * Gets a headless analyzer instance, with the assumption that the application has already been
148 | * initialized. If this is called before the application has been initialized, it will
149 | * initialize the application with no logging.
150 | *
151 | * @return An instance of a new headless analyzer.
152 | * @throws IOException if there was a problem reading the application.properties file (only possible
153 | * if the application had not be initialized).
154 | */
155 | public static LibHeadlessAnalyzer getInstance(LibProgramHandler handler) throws IOException {
156 |
157 | // Prevent more than one headless analyzer from being instantiated. Too much about it
158 | // messes with global system settings, so under the current design of Ghidra, allowing
159 | // more than one to exist could result in unpredictable behavior.
160 | if (instance != null) {
161 | return instance;
162 | }
163 |
164 | // Initialize application (if necessary)
165 | if (!Application.isInitialized()) {
166 | ApplicationConfiguration configuration = new HeadlessGhidraApplicationConfiguration();
167 | configuration.setInitializeLogging(false);
168 | Msg.setErrorLogger(new LibHeadlessErrorLogger(null));
169 | Application.initializeApplication(getApplicationLayout(), configuration);
170 | }
171 |
172 | // Instantiate and return singleton headless analyzer
173 | instance = new LibHeadlessAnalyzer();
174 |
175 | // Set our program handler
176 | instance.programHandler = handler;
177 |
178 | return instance;
179 | }
180 |
181 | /**
182 | * Gets the appropriate Ghidra application layout for this headless analyzer.
183 | *
184 | * The headless analyzer can be used in both "normal" mode and single jar mode, so
185 | * we need to use the appropriate layout for either case.
186 | *
187 | * @return The appropriate Ghidra application layout for this headless analyzer.
188 | * @throws IOException if there was a problem getting an appropriate application layout.
189 | */
190 | private static GhidraApplicationLayout getApplicationLayout() throws IOException {
191 | GhidraApplicationLayout layout;
192 | try {
193 | layout = new GhidraApplicationLayout();
194 | }
195 | catch (IOException e) {
196 | layout = new GhidraJarApplicationLayout();
197 |
198 | }
199 | return layout;
200 | }
201 |
202 | /**
203 | * Creates a new headless analyzer object with default settings.
204 | */
205 | private LibHeadlessAnalyzer() {
206 | // Create default options which the caller can later set prior to processing.
207 | options = new LibHeadlessOptions();
208 |
209 | // Ghidra URL handler registration. There's no harm in doing this more than once.
210 | Handler.registerHandler();
211 |
212 | // Ensure that we are running in "headless mode", preventing Swing-based methods from
213 | // running (causing headless operation to lose focus).
214 | System.setProperty("java.awt.headless", "true");
215 | System.setProperty(SystemUtilities.HEADLESS_PROPERTY, Boolean.TRUE.toString());
216 |
217 | // Put analyzer in its default state
218 | reset();
219 | }
220 |
221 | /**
222 | * Resets the state of the headless analyzer to the default settings.
223 | */
224 | public void reset() {
225 | options.reset();
226 | project = null;
227 | analysisTimedOut = false;
228 | saveDomainFolder = null;
229 | storage = new HashMap<>();
230 | classLoaderForDotClassScripts = null;
231 | }
232 |
233 | /**
234 | * Gets the headless analyzer's options.
235 | *
236 | * @return The headless analyer's options.
237 | */
238 | public LibHeadlessOptions getOptions() {
239 | return options;
240 | }
241 |
242 | /**
243 | * Process the optional import file/directory list and process each imported file:
244 | *
245 | * - execute ordered list of pre-scripts
246 | * - perform auto-analysis if not disabled
247 | * - execute ordered list of post-scripts
248 | *
249 | * If no import files or directories have been specified the ordered list
250 | * of pre/post scripts will be executed once.
251 | *
252 | * @param ghidraURL ghidra URL for existing server repository and optional
253 | * folder path
254 | * @param filesToImport directories and files to be imported (null or empty
255 | * is acceptable if we are in -process mode)
256 | * @throws IOException if there was an IO-related problem
257 | * @throws URISyntaxException specified URL is invalid
258 | */
259 | public void processURL(URL ghidraURL, List filesToImport)
260 | throws IOException, URISyntaxException {
261 |
262 | if (options.readOnly && options.commit) {
263 | Msg.error(this,
264 | "Abort due to Headless analyzer error: The requested readOnly option is in conflict " +
265 | "with the commit option");
266 | return;
267 | }
268 |
269 | if (!"ghidra".equals(ghidraURL.getProtocol())) {
270 | throw new URISyntaxException(ghidraURL.toString(), "Unsupported repository URL");
271 | }
272 |
273 | if (GhidraURL.isLocalProjectURL(ghidraURL)) {
274 | Msg.error(this,
275 | "Ghidra URL command form does not supported local project URLs (ghidra:/path...)");
276 | return;
277 | }
278 |
279 | String path = ghidraURL.getPath();
280 | if (path == null) {
281 | throw new URISyntaxException(ghidraURL.toString(), "Unsupported repository URL");
282 | }
283 |
284 | path = path.trim();
285 | if (path.length() == 0) {
286 | throw new URISyntaxException(ghidraURL.toString(), "Unsupported repository URL");
287 | }
288 |
289 | if (!options.runScriptsNoImport) { // Running in -import mode
290 | if ((filesToImport == null || filesToImport.size() == 0) &&
291 | options.preScripts.isEmpty() && options.postScripts.isEmpty()) {
292 | Msg.warn(this, "REPORT: Nothing to do ... must specify files for import.");
293 | return;
294 | }
295 |
296 | if (!path.endsWith("/")) {
297 | // force explicit folder path so that non-existent folders are created on import
298 | ghidraURL = new URI("ghidra", null, ghidraURL.getHost(), ghidraURL.getPort(), path + "/", null, null).toURL();
299 | }
300 | }
301 | else { // Running in -process mode
302 | if (path.endsWith("/") && path.length() > 1) {
303 | ghidraURL = new URI("ghidra", null, ghidraURL.getHost(), ghidraURL.getPort(),
304 | path.substring(0, path.length() - 1), null, null).toURL();
305 | }
306 | }
307 |
308 | List parsedScriptPaths = parseScriptPaths(options.scriptPaths);
309 | GhidraScriptUtil.initialize(new BundleHost(), parsedScriptPaths);
310 | try {
311 | showConfiguredScriptPaths();
312 | compileScripts();
313 |
314 | Msg.info(LibHeadlessAnalyzer.class, "HEADLESS: execution starts");
315 |
316 | GhidraURLConnection c = (GhidraURLConnection) ghidraURL.openConnection();
317 | c.setReadOnly(options.readOnly); // writable repository connection
318 |
319 | if (c.getRepositoryName() == null) {
320 | throw new MalformedURLException("Unsupported repository URL: " + ghidraURL);
321 | }
322 |
323 | Msg.info(this, "Opening ghidra repository project: " + ghidraURL);
324 | Object obj = c.getContent();
325 | if (!(obj instanceof GhidraURLWrappedContent)) {
326 | throw new IOException(
327 | "Connect to repository folder failed. Status code: " + c.getStatusCode());
328 | }
329 | GhidraURLWrappedContent wrappedContent = (GhidraURLWrappedContent) obj;
330 | Object content = null;
331 | try {
332 | content = wrappedContent.getContent(this);
333 | if (!(content instanceof DomainFolder)) {
334 | throw new IOException("Connect to repository folder failed");
335 | }
336 |
337 | DomainFolder folder = (DomainFolder) content;
338 | project = new HeadlessProject(getProjectManager(), c);
339 |
340 | if (!checkUpdateOptions()) {
341 | return; // TODO: Should an exception be thrown?
342 | }
343 |
344 | if (options.runScriptsNoImport) {
345 | processNoImport(folder.getPathname());
346 | }
347 | else {
348 | processWithImport(folder.getPathname(), filesToImport);
349 | }
350 | }
351 | finally {
352 | if (content != null) {
353 | wrappedContent.release(content, this);
354 | }
355 | if (project != null) {
356 | project.close();
357 | }
358 | }
359 | }
360 | finally {
361 | GhidraScriptUtil.dispose();
362 | }
363 | }
364 |
365 | /**
366 | * Process the optional import file/directory list and process each imported file:
367 | *
368 | * - execute ordered list of pre-scripts
369 | * - perform auto-analysis if not disabled
370 | * - execute ordered list of post-scripts
371 | *
372 | * If no import files or directories have been specified the ordered list
373 | * of pre/post scripts will be executed once.
374 | *
375 | * @param projectLocation directory path of project
376 | * If project exists it will be opened, otherwise it will be created.
377 | * @param projectName project name
378 | * @param rootFolderPath root folder for imports
379 | * @param filesToImport directories and files to be imported (null or empty is acceptable if
380 | * we are in -process mode)
381 | * @throws IOException if there was an IO-related problem
382 | */
383 | public void processLocal(String projectLocation, String projectName, String rootFolderPath,
384 | List filesToImport) throws IOException {
385 |
386 | if (options.readOnly && options.commit) {
387 | Msg.error(this,
388 | "Abort due to Headless analyzer error: The requested readOnly option is " +
389 | "in conflict with the commit option");
390 | return;
391 | }
392 |
393 | // If not importing, remove trailing slash so that non-existent folders aren't created
394 | if (options.runScriptsNoImport) {
395 | if ((rootFolderPath.endsWith("/")) && (rootFolderPath.length() > 1)) {
396 | rootFolderPath = rootFolderPath.substring(0, rootFolderPath.length() - 1);
397 | }
398 | }
399 | else {
400 | // If we are importing, need some files to import or at least a script to run!
401 | if ((filesToImport == null || filesToImport.size() == 0) &&
402 | options.preScripts.isEmpty() && options.postScripts.isEmpty()) {
403 | Msg.warn(this, "REPORT: Nothing to do ... must specify file(s) for import.");
404 | return;
405 | }
406 |
407 | // If importing, add trailing slash if it isn't there so that non-existent folders are created
408 | if (!rootFolderPath.endsWith("/")) {
409 | rootFolderPath += "/";
410 | }
411 | }
412 |
413 | List parsedScriptPaths = parseScriptPaths(options.scriptPaths);
414 | GhidraScriptUtil.initialize(new BundleHost(), parsedScriptPaths);
415 | try {
416 | showConfiguredScriptPaths();
417 | compileScripts();
418 |
419 | Msg.info(LibHeadlessAnalyzer.class, "HEADLESS: execution starts");
420 |
421 | File dir = new File(projectLocation);
422 | ProjectLocator locator = new ProjectLocator(dir.getAbsolutePath(), projectName);
423 |
424 | if (locator.getProjectDir().exists()) {
425 | project = openProject(locator);
426 | }
427 | else {
428 | if (options.runScriptsNoImport) {
429 | Msg.error(this, "Could not find project: " + locator +
430 | " -- should already exist in -process mode.");
431 | throw new IOException("Could not find project: " + locator);
432 | }
433 |
434 | if (!options.runScriptsNoImport && options.readOnly) {
435 | // assume temporary when importing with readOnly option
436 | options.deleteProject = true;
437 | }
438 |
439 | Msg.info(this, "Creating " + (options.deleteProject ? "temporary " : "") +
440 | "project: " + locator);
441 | project = getProjectManager().createProject(locator, null, false);
442 | }
443 |
444 | try {
445 |
446 | if (!checkUpdateOptions()) {
447 | return; // TODO: Should an exception be thrown?
448 | }
449 |
450 | if (options.runScriptsNoImport) {
451 | processNoImport(rootFolderPath);
452 | }
453 | else {
454 | processWithImport(rootFolderPath, filesToImport);
455 | }
456 | }
457 | finally {
458 | project.close();
459 | if (!options.runScriptsNoImport && options.deleteProject) {
460 | FileUtilities.deleteDir(locator.getProjectDir());
461 | locator.getMarkerFile().delete();
462 | }
463 | }
464 | }
465 | finally {
466 | GhidraScriptUtil.dispose();
467 | }
468 | }
469 |
470 | /**
471 | * Checks to see if the most recent analysis timed out.
472 | *
473 | * @return true if the most recent analysis timed out; otherwise, false.
474 | */
475 | public boolean checkAnalysisTimedOut() {
476 | return analysisTimedOut;
477 | }
478 |
479 | void setSaveFolder(DomainFolder domFolder) {
480 | saveDomainFolder = domFolder;
481 |
482 | if (domFolder != null) {
483 | Msg.info(this, "Save location changed to: " + domFolder.getPathname());
484 | }
485 | }
486 |
487 | void addVariableToStorage(String nameOfVar, Object valOfVar) {
488 | if (storage.containsKey(nameOfVar)) {
489 | Msg.warn(this, "Overwriting existing storage variable: " + nameOfVar);
490 | }
491 |
492 | storage.put(nameOfVar, valOfVar);
493 | }
494 |
495 | Set getStorageKeys() {
496 | return storage.keySet();
497 | }
498 |
499 | Object getVariableFromStorage(String nameOfVar) {
500 | if (!storage.containsKey(nameOfVar)) {
501 | Msg.warn(this, "The storage variable '" + nameOfVar +
502 | "' does not exist in HeadlessAnalyzer storage.");
503 | return null;
504 | }
505 |
506 | return storage.get(nameOfVar);
507 | }
508 |
509 | /**
510 | * Get/Create specified folder path within project
511 | *
512 | * @param folderPath the folder path within the project
513 | * @param create if true, folder will be created if it does not exist
514 | * @return DomainFolder for specified path
515 | * @throws InvalidNameException if folder name is invalid
516 | * @throws IOException if folder can not be created
517 | */
518 | DomainFolder getDomainFolder(String folderPath, boolean create)
519 | throws IOException, InvalidNameException {
520 |
521 | DomainFolder domFolder = project.getProjectData().getFolder(folderPath);
522 |
523 | if (create && domFolder == null) {
524 | // Create any folder that doesn't exist
525 | String cleanPath = folderPath.replaceAll("^" + DomainFolder.SEPARATOR + "+", "");
526 | cleanPath = cleanPath.replaceAll(DomainFolder.SEPARATOR + "+$", "");
527 |
528 | String[] subfolders = cleanPath.split(DomainFolder.SEPARATOR + "+");
529 |
530 | int folderIndex = 0;
531 | String currPath = DomainFolder.SEPARATOR + subfolders[folderIndex];
532 |
533 | DomainFolder testFolder = project.getProjectData().getFolder(currPath);
534 | DomainFolder baseFolder = null;
535 |
536 | // Stay in loop while we see folders that exist
537 | while ((testFolder != null) && (folderIndex < (subfolders.length - 1))) {
538 | folderIndex++;
539 | baseFolder = testFolder;
540 | testFolder = baseFolder.getFolder(subfolders[folderIndex]);
541 | }
542 |
543 | // If none of the folders exist, create new files starting from the root
544 | if (folderIndex == 0) {
545 | baseFolder = project.getProjectData().getRootFolder();
546 | }
547 |
548 | // Since this method is only called by import, we create any folder that
549 | // does not exist.
550 | for (int i = folderIndex; i < subfolders.length; i++) {
551 | baseFolder = baseFolder.createFolder(subfolders[i]);
552 | Msg.info(this, "Created project folder: " + subfolders[i]);
553 | }
554 |
555 | domFolder = baseFolder;
556 | }
557 |
558 | return domFolder;
559 | }
560 |
561 | boolean storageContainsKey(String nameOfVar) {
562 | return storage.containsKey(nameOfVar);
563 | }
564 |
565 | /**
566 | * Runs the specified script with the specified state.
567 | *
568 | * @param scriptState State representing environment variables that the script is able
569 | * to access.
570 | * @param script Script to be run.
571 | * @return whether the script successfully completed running
572 | */
573 | private boolean runScript(GhidraState scriptState, GhidraScript script) {
574 | if (script instanceof LibHeadlessScript) {
575 | ((LibHeadlessScript) script).setHeadlessInstance(this);
576 | }
577 |
578 | ResourceFile srcFile = script.getSourceFile();
579 | String scriptName =
580 | srcFile != null ? srcFile.getAbsolutePath() : (script.getClass().getName() + ".class");
581 |
582 | try {
583 | PrintWriter writer = new PrintWriter(System.out);
584 | Msg.info(this, "SCRIPT: " + scriptName);
585 | script.execute(scriptState, TaskMonitor.DUMMY, writer);
586 | writer.flush();
587 | }
588 | catch (Exception exc) {
589 | Program prog = scriptState.getCurrentProgram();
590 | String path = (prog != null ? " ( " + prog.getExecutablePath() + " ) " : "");
591 | String logErrorMsg =
592 | "REPORT SCRIPT ERROR: " + path + " " + scriptName + " : " + exc.getMessage();
593 | Msg.error(this, logErrorMsg, exc);
594 | return false;
595 | }
596 |
597 | return true;
598 | }
599 |
600 | /**
601 | * Check file update options (i.e., readOnly, commit) and change defaults if needed.
602 | * @return true if OK to continue
603 | */
604 | private boolean checkUpdateOptions() {
605 |
606 | boolean isImport = !options.runScriptsNoImport;
607 | boolean commitAllowed = isCommitAllowed();
608 |
609 | if (options.readOnly) {
610 | String readOnlyError =
611 | "Abort due to Headless analyzer error: The requested -readOnly option " +
612 | "is in conflict with the ";
613 |
614 | if (options.commit) {
615 | Msg.error(this, readOnlyError + "-commit option.");
616 | return false;
617 | }
618 |
619 | if (options.okToDelete) {
620 | Msg.error(this, readOnlyError + "-okToDelete option.");
621 | return false;
622 | }
623 | }
624 |
625 | if (options.commit && !commitAllowed) {
626 | Msg.error(this,
627 | "Commit to repository not possible (due to permission or connection issue)");
628 | return false;
629 | }
630 |
631 | if (project.getProjectLocator().isTransient()) {
632 | if (!options.commit) {
633 | if (commitAllowed && !options.readOnly) {
634 | Msg.info(this,
635 | "When processing a URL, -commit is automatically enabled unless -readOnly mode " +
636 | "is specified. Enabling -commit and continuing.");
637 | options.commit = true;
638 | }
639 | }
640 | }
641 |
642 | if (options.overwrite) {
643 | if (!isImport) {
644 | Msg.info(this,
645 | "Ignoring -overwrite because it is not applicable to -process mode.");
646 | }
647 | else if (options.readOnly) {
648 | Msg.info(this,
649 | "Ignoring -overwrite because it is not applicable to -readOnly import mode.");
650 | options.overwrite = false;
651 | }
652 | }
653 |
654 | return true;
655 | }
656 |
657 | private boolean isCommitAllowed() {
658 | RepositoryAdapter repository = project.getRepository();
659 | if (repository == null) {
660 | return true;
661 | }
662 | try {
663 | repository.connect();
664 | if (!repository.isConnected()) {
665 | return false;
666 | }
667 | User user = repository.getUser();
668 | if (!user.hasWritePermission()) {
669 | Msg.warn(this, "User '" + user.getName() +
670 | "' does not have write permission to repository - commit not allowed");
671 | return false;
672 | }
673 | return true;
674 | }
675 | catch (IOException e) {
676 | Msg.error(this, "Repository connection failed (" + repository.getServerInfo() +
677 | ") - commit not allowed");
678 | return false;
679 | }
680 | }
681 |
682 | private List parseScriptPaths(List scriptPaths) {
683 | if (scriptPaths == null) {
684 | return null;
685 | }
686 | List parsedScriptPaths = new ArrayList<>();
687 | for (String path : scriptPaths) {
688 | ResourceFile pathFile = Path.fromPathString(path);
689 | String absPath = pathFile.getAbsolutePath();
690 | if (pathFile.exists()) {
691 | parsedScriptPaths.add(absPath);
692 | }
693 | else {
694 |
695 | Msg.warn(this, "REPORT: Could not find -scriptPath entry, skipping: " + absPath);
696 | }
697 | }
698 | return parsedScriptPaths;
699 | }
700 |
701 | private void showConfiguredScriptPaths() {
702 | StringBuffer buf = new StringBuffer("HEADLESS Script Paths:");
703 | for (ResourceFile dir : GhidraScriptUtil.getScriptSourceDirectories()) {
704 | buf.append("\n ");
705 | buf.append(dir.getAbsolutePath());
706 | }
707 | Msg.info(LibHeadlessAnalyzer.class, buf.toString());
708 | }
709 |
710 | private ResourceFile findScript(String scriptName) {
711 | ResourceFile scriptSource = new ResourceFile(scriptName);
712 | scriptSource = scriptSource.getCanonicalFile();
713 | if (scriptSource.exists()) {
714 | return scriptSource;
715 | }
716 | scriptSource = GhidraScriptUtil.findScriptByName(scriptName);
717 | if (scriptSource != null) {
718 | return scriptSource;
719 | }
720 | throw new IllegalArgumentException("Script not found: " + scriptName);
721 | }
722 |
723 | /**
724 | * Checks the script name to ensure it exists. If the script type has a GhidraScriptProvider
725 | * (any type of script but .class), then return the ResourceFile that represents that script.
726 | *
727 | * If the script is a class file, return null (one class loader is stored to allow the
728 | * Headless Analyzer to find all the class files).
729 | *
730 | * GhidraScript is not instantiated here, because it is important that each script be
731 | * instantiated at the time it's used. If a GhidraScript object is re-used, this causes
732 | * problems where GhidraScript variables aren't being re-initialized at each use of the script.
733 | *
734 | * @param scriptName The name of the script to check
735 | * @return ResourceFile representing the source file, or null (if script is a .class file)
736 | */
737 | private ResourceFile checkScript(String scriptName) {
738 |
739 | // Check for pre-compiled GhidraScript (e.g., my.package.Impl.class)
740 | String classExtension = ".class";
741 |
742 | if (scriptName.endsWith(classExtension)) {
743 | String className =
744 | scriptName.substring(0, scriptName.length() - classExtension.length());
745 | try {
746 |
747 | // Create a classloader that contains all the ghidra_script paths (especially the one
748 | // specified in -scriptPath!)
749 | List dirs = GhidraScriptUtil.getScriptSourceDirectories();
750 | List urls = new ArrayList<>();
751 |
752 | for (ResourceFile dir : dirs) {
753 | try {
754 | urls.add(dir.toURL());
755 | }
756 | catch (MalformedURLException e) {
757 | // Do nothing. If can't make a URL out of the dir, don't add it.
758 | }
759 | }
760 |
761 | classLoaderForDotClassScripts =
762 | URLClassLoader.newInstance(urls.toArray(new URL[0]));
763 |
764 | Class> c = Class.forName(className, true, classLoaderForDotClassScripts);
765 |
766 | if (GhidraScript.class.isAssignableFrom(c)) {
767 | // No issues, but return null, which signifies we don't actually have a
768 | // ResourceFile to associate with the script name
769 | return null;
770 | }
771 |
772 | Msg.error(this,
773 | "REPORT SCRIPT ERROR: java class '" + className + "' is not a GhidraScript");
774 | }
775 | catch (ClassNotFoundException e) {
776 | Msg.error(this,
777 | "REPORT SCRIPT ERROR: java class not found for '" + className + "'");
778 | }
779 | throw new IllegalArgumentException("Invalid script: " + scriptName);
780 | }
781 |
782 | try {
783 | ResourceFile scriptSource = findScript(scriptName);
784 | GhidraScriptProvider provider = GhidraScriptUtil.getProvider(scriptSource);
785 |
786 | if (provider == null) {
787 | throw new IOException("Missing plugin needed to run scripts of this type. Please " +
788 | "ensure you have installed the necessary plugin.");
789 | }
790 |
791 | return scriptSource;
792 | }
793 | catch (Exception | NoClassDefFoundError exc) {
794 | String logErrorMsg = "REPORT SCRIPT ERROR: " + scriptName + " : " + exc.getMessage();
795 | Msg.error(this, logErrorMsg);
796 | }
797 | throw new IllegalArgumentException("Invalid script: " + scriptName);
798 | }
799 |
800 | /**
801 | * Creates mapping from script name to actual Script object
802 | *
803 | * @param scriptsList List of scripts
804 | * @return mapping of script name to its associated Script object
805 | */
806 | private Map checkScriptsList(List> scriptsList) {
807 | Map map = new HashMap<>();
808 | for (Pair scriptPair : scriptsList) {
809 | String scriptName = scriptPair.first;
810 | ResourceFile scriptFile = checkScript(scriptName);
811 | map.put(scriptName, scriptFile);
812 | }
813 | return map;
814 | }
815 |
816 | private void compileScripts() throws IOException {
817 |
818 | // Check that given locations for .properties files are valid
819 | if (options.propertiesFileStrPaths.size() > 0) {
820 |
821 | options.propertiesFilePaths.clear();
822 |
823 | for (String path : options.propertiesFileStrPaths) {
824 | Path currPath = new Path(path, true, false, true);
825 |
826 | ResourceFile resource = currPath.getPath();
827 |
828 | if (!resource.isDirectory()) {
829 | throw new IOException("Properties file path: '" + path +
830 | "' either does not exist, " + "or is not a valid directory.");
831 | }
832 |
833 | if (currPath.isEnabled() && !options.propertiesFilePaths.contains(resource)) {
834 | options.propertiesFilePaths.add(resource);
835 | }
836 | }
837 | }
838 |
839 | if (options.preScriptFileMap == null) {
840 | options.preScriptFileMap = checkScriptsList(options.preScripts);
841 | }
842 |
843 | if (options.postScriptFileMap == null) {
844 | options.postScriptFileMap = checkScriptsList(options.postScripts);
845 | }
846 | }
847 |
848 | /**
849 | * Run a list of scripts
850 | *
851 | * @param scriptsList list of script names to run
852 | * @param scriptFileMap mapping of script names to Script objects
853 | * @param scriptState the GhidraState to be passed into each script
854 | * @param continueOption option that could have been set by script(s)
855 | * @return option that could have been set by script(s)
856 | */
857 | private LibHeadlessContinuationOption runScriptsList(List> scriptsList,
858 | Map scriptFileMap, GhidraState scriptState,
859 | LibHeadlessContinuationOption continueOption) {
860 |
861 | ResourceFile currScriptFile;
862 | LibHeadlessContinuationOption retOption = continueOption;
863 |
864 | boolean scriptSuccess;
865 | boolean isHeadlessScript = false;
866 | String scriptName = "";
867 | GhidraScript currScript;
868 |
869 | try {
870 | for (Pair scriptPair : scriptsList) {
871 | scriptName = scriptPair.first;
872 | String[] scriptArgs = scriptPair.second;
873 |
874 | // For .class files, there is no ResourceFile mapping. Need to load from the
875 | // stored 'classLoaderForDotClassScripts'
876 | if (scriptName.endsWith(".class")) {
877 |
878 | if (classLoaderForDotClassScripts == null) {
879 | throw new IllegalArgumentException("Invalid script: " + scriptName);
880 | }
881 |
882 | String className = scriptName.substring(0, scriptName.length() - 6);
883 | Class> c = Class.forName(className, true, classLoaderForDotClassScripts);
884 |
885 | // Get parent folder to pass to GhidraScript
886 | File parentFile = new File(c.getResource(c.getSimpleName() + ".class").toURI())
887 | .getParentFile();
888 |
889 | currScript = (GhidraScript) c.getConstructor().newInstance();
890 | currScript.setScriptArgs(scriptArgs);
891 |
892 | if (options.propertiesFilePaths.size() > 0) {
893 | currScript.setPotentialPropertiesFileLocations(options.propertiesFilePaths);
894 | }
895 |
896 | currScript.setPropertiesFileLocation(parentFile.getAbsolutePath(), className);
897 | }
898 | else {
899 | currScriptFile = scriptFileMap.get(scriptName);
900 |
901 | // GhidraScriptProvider case
902 | GhidraScriptProvider provider = GhidraScriptUtil.getProvider(currScriptFile);
903 | PrintWriter writer = new PrintWriter(System.out);
904 | currScript = provider.getScriptInstance(currScriptFile, writer);
905 | currScript.setScriptArgs(scriptArgs);
906 |
907 | if (options.propertiesFilePaths.size() > 0) {
908 | currScript.setPotentialPropertiesFileLocations(options.propertiesFilePaths);
909 | }
910 | }
911 |
912 | isHeadlessScript = currScript instanceof LibHeadlessScript ? true : false;
913 |
914 | if (isHeadlessScript) {
915 | ((LibHeadlessScript) currScript).setInitialContinuationOption(retOption);
916 | }
917 |
918 | scriptSuccess = runScript(scriptState, currScript);
919 |
920 | if (isHeadlessScript) {
921 | if (scriptSuccess) {
922 | retOption = ((LibHeadlessScript) currScript).getContinuationOption();
923 |
924 | // If script wants to abort, return without running any scripts that follow
925 | if ((retOption == LibHeadlessContinuationOption.ABORT) ||
926 | (retOption == LibHeadlessContinuationOption.ABORT_AND_DELETE)) {
927 | return retOption;
928 | }
929 |
930 | }
931 | else {
932 | // If script did not run successfully, abort further processing automatically
933 | Msg.warn(this,
934 | "Script does not exist or encountered problems; further processing is aborted.");
935 |
936 | return LibHeadlessContinuationOption.ABORT;
937 | }
938 | }
939 | }
940 | }
941 | catch (Exception exc) {
942 | String logErrorMsg = "REPORT SCRIPT ERROR: " + scriptName + " : " + exc.getMessage();
943 | Msg.error(this, logErrorMsg, exc);
944 | }
945 |
946 | return retOption;
947 | }
948 |
949 | private GhidraState getInitialProgramState(Program program) {
950 | ProgramLocation location = null;
951 | AddressSetView initializedMem = program.getMemory().getLoadedAndInitializedAddressSet();
952 | if (!initializedMem.isEmpty()) {
953 | location = new ProgramLocation(program, initializedMem.getMinAddress());
954 | }
955 | return new GhidraState(null, project, program, location, null, null);
956 | }
957 |
958 | /**
959 | *{@literal Run prescripts -> analysis -> postscripts (any of these steps is optional).}
960 | * @param fileAbsolutePath Path of the file to analyze.
961 | * @param program The program to analyze.
962 | * @return true if the program file should be kept. If analysis or scripts have marked
963 | * the program as temporary changes should not be saved. Returns false in
964 | * these cases:
965 | * - One of the scripts sets the Headless Continuation Option to "ABORT_AND_DELETE" or
966 | * "CONTINUE_THEN_DELETE".
967 | */
968 | private boolean analyzeProgram(String fileAbsolutePath, Program program) {
969 |
970 | analysisTimedOut = false;
971 |
972 | AutoAnalysisManager mgr = AutoAnalysisManager.getAnalysisManager(program);
973 | mgr.initializeOptions();
974 |
975 | GhidraState scriptState = null;
976 | LibHeadlessContinuationOption scriptStatus = LibHeadlessContinuationOption.CONTINUE;
977 |
978 | boolean abortProcessing = false;
979 | boolean deleteProgram = false;
980 |
981 | if (!options.preScripts.isEmpty()) {
982 | // create one state, in case each script might want to modify it to pass information
983 | scriptState = getInitialProgramState(program);
984 |
985 | scriptStatus = runScriptsList(options.preScripts, options.preScriptFileMap, scriptState,
986 | scriptStatus);
987 | }
988 |
989 | switch (scriptStatus) {
990 | case ABORT_AND_DELETE:
991 | abortProcessing = true;
992 | deleteProgram = true;
993 | break;
994 |
995 | case CONTINUE_THEN_DELETE:
996 | abortProcessing = false;
997 | deleteProgram = true;
998 | break;
999 |
1000 | case ABORT:
1001 | abortProcessing = true;
1002 | deleteProgram = false;
1003 | break;
1004 |
1005 | default:
1006 | // do nothing
1007 | }
1008 |
1009 | if (abortProcessing) {
1010 | Msg.info(this, "Processing aborted as a result of pre-script.");
1011 | return !deleteProgram;
1012 | }
1013 |
1014 | int txId = program.startTransaction("Analysis");
1015 | try {
1016 | if (options.analyze) {
1017 | Msg.info(this, "ANALYZING all memory and code: " + fileAbsolutePath);
1018 | mgr.initializeOptions();
1019 |
1020 | // Note: Want to analyze regardless of whether we have already analyzed or not
1021 | // (user could have changed options).
1022 | mgr.reAnalyzeAll(null);
1023 |
1024 | if (options.perFileTimeout == -1) {
1025 | mgr.startAnalysis(TaskMonitor.DUMMY); // kick start
1026 |
1027 | Msg.info(this, "REPORT: Analysis succeeded for file: " + fileAbsolutePath);
1028 |
1029 | GhidraProgramUtilities.markProgramAnalyzed(program);
1030 | }
1031 | else {
1032 | LibHeadlessTimedTaskMonitor timerMonitor =
1033 | new LibHeadlessTimedTaskMonitor(options.perFileTimeout);
1034 | mgr.startAnalysis(timerMonitor);
1035 |
1036 | if (timerMonitor.isCancelled()) {
1037 | Msg.error(this, "REPORT: Analysis timed out at " + options.perFileTimeout +
1038 | " seconds. Processing not completed for file: " + fileAbsolutePath);
1039 |
1040 | // If no further scripts, just return the current program disposition
1041 | if (options.postScripts.isEmpty()) {
1042 | return !deleteProgram;
1043 | }
1044 |
1045 | analysisTimedOut = true;
1046 | }
1047 | else {
1048 | // If timeout didn't already happen at this point, cancel the monitor
1049 | timerMonitor.cancel();
1050 |
1051 | Msg.info(this, "REPORT: Analysis succeeded for file: " + fileAbsolutePath);
1052 | GhidraProgramUtilities.markProgramAnalyzed(program);
1053 | }
1054 | }
1055 | }
1056 | }
1057 | finally {
1058 | program.endTransaction(txId, true);
1059 | }
1060 |
1061 | if (!options.postScripts.isEmpty()) {
1062 |
1063 | if (scriptState == null) {
1064 | scriptState = getInitialProgramState(program);
1065 | }
1066 |
1067 | scriptStatus = runScriptsList(options.postScripts, options.postScriptFileMap,
1068 | scriptState, scriptStatus);
1069 |
1070 | switch (scriptStatus) {
1071 | case ABORT_AND_DELETE:
1072 | abortProcessing = true;
1073 | deleteProgram = true;
1074 | break;
1075 |
1076 | case CONTINUE_THEN_DELETE:
1077 | abortProcessing = false;
1078 | deleteProgram = true;
1079 | break;
1080 |
1081 | case ABORT:
1082 | abortProcessing = true;
1083 | // If deleteProgram is already true, don't change it to false
1084 | // (basically, leave as-is)
1085 | break;
1086 |
1087 | default:
1088 | // Do nothing, assume want to carry over options from before
1089 |
1090 | }
1091 |
1092 | if (abortProcessing) {
1093 | Msg.info(this, "Processing aborted as a result of post-script.");
1094 | }
1095 | else if (options.analyze && !options.postScripts.isEmpty()) {
1096 | Msg.info(this, "ANALYZING changes made by post scripts: " + fileAbsolutePath);
1097 | txId = program.startTransaction("Post-Analysis");
1098 | try {
1099 | mgr.startAnalysis(TaskMonitor.DUMMY); // kick start
1100 | }
1101 | finally {
1102 | program.endTransaction(txId, true);
1103 | }
1104 | Msg.info(this, "REPORT: Post-analysis succeeded for file: " + fileAbsolutePath);
1105 | }
1106 |
1107 | }
1108 |
1109 | // Our hook after the analysis
1110 | if (programHandler != null) {
1111 | programHandler.PostProcessHandler(program);
1112 | }
1113 |
1114 | return !deleteProgram;
1115 | }
1116 |
1117 | private void processFileNoImport(DomainFile domFile) throws IOException {
1118 |
1119 | if (domFile.isHijacked()) {
1120 | Msg.error(this,
1121 | "Skipped processing for " + domFile.getPathname() + " -- file is hijacked");
1122 | return;
1123 | }
1124 |
1125 | if (!ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) {
1126 | return; // skip non-Program files
1127 | }
1128 |
1129 | Program program = null;
1130 | boolean keepFile = true; // if false file should be deleted after release
1131 | boolean terminateCheckoutWhenDone = false;
1132 |
1133 | boolean readOnlyFile = options.readOnly || domFile.isReadOnly();
1134 |
1135 | try {
1136 | // Exclusive checkout required when commit option specified
1137 | if (!readOnlyFile) {
1138 | if (domFile.isVersioned()) {
1139 | if (!domFile.isCheckedOut()) {
1140 | if (!domFile.checkout(options.commit, TaskMonitor.DUMMY)) {
1141 | Msg.warn(this, "Skipped processing for " + domFile.getPathname() +
1142 | " -- failed to get exclusive file checkout required for commit");
1143 | return;
1144 | }
1145 | }
1146 | else if (options.commit && !domFile.isCheckedOutExclusive()) {
1147 | Msg.error(this, "Skipped processing for " + domFile.getPathname() +
1148 | " -- file is checked-out non-exclusive (commit requires exclusive checkout)");
1149 | return;
1150 | }
1151 | }
1152 | terminateCheckoutWhenDone = true;
1153 | }
1154 |
1155 | program = (Program) domFile.getDomainObject(this, true, false, TaskMonitor.DUMMY);
1156 |
1157 | Msg.info(this, "REPORT: Processing project file: " + domFile.getPathname());
1158 |
1159 | // This method already takes into account whether the user has set the "noanalysis"
1160 | // flag or not
1161 | keepFile = analyzeProgram(domFile.getPathname(), program) || readOnlyFile;
1162 |
1163 | if (!keepFile) {
1164 | program.setTemporary(true); // don't save changes
1165 | if (!options.okToDelete) {
1166 | // Don't remove file unless okToDelete was specified
1167 | Msg.warn(this, "Due to script activity, " + domFile.getPathname() +
1168 | " deletion was requested but denied -- 'okToDelete' parameter was not specified");
1169 | keepFile = true;
1170 | }
1171 | }
1172 |
1173 | if (readOnlyFile) {
1174 | if (program.isChanged()) {
1175 | Msg.info(this, "REPORT: Discarding changes to the following read-only file: " +
1176 | domFile.getPathname());
1177 | }
1178 | return;
1179 | }
1180 |
1181 | if (program.isTemporary()) {
1182 | if (program.isChanged()) {
1183 | Msg.info(this,
1184 | "REPORT: Discarding changes to the following file as a result of script activity: " +
1185 | domFile.getPathname());
1186 | }
1187 | return;
1188 | }
1189 |
1190 | if (domFile.canSave()) {
1191 | domFile.save(TaskMonitor.DUMMY);
1192 | Msg.info(this,
1193 | "REPORT: Save succeeded for processed file: " + domFile.getPathname());
1194 | }
1195 | if (program.isChanged()) {
1196 | Msg.error(this,
1197 | "REPORT: Error trying to save changes to file: " + domFile.getPathname());
1198 | }
1199 |
1200 | if (options.commit) {
1201 |
1202 | AutoAnalysisManager.getAnalysisManager(program).dispose();
1203 | program.release(this);
1204 | program = null;
1205 |
1206 | // Only commit if it's a shared project.
1207 | commitProgram(domFile);
1208 | }
1209 | }
1210 | catch (VersionException e) {
1211 |
1212 | if (e.isUpgradable()) {
1213 | Msg.error(this,
1214 | domFile.getPathname() +
1215 | ": this file was created with an older version of Ghidra. Automatic " +
1216 | "upgrading of the file to the current version is possible, but " +
1217 | "requires an exclusive check-out of the file. Please check out the file " +
1218 | " using the Ghidra GUI and then re-run Headless.");
1219 | }
1220 | else {
1221 | Msg.error(this, domFile.getPathname() +
1222 | ": this file was created with a newer version of Ghidra, and can not be processed.");
1223 | }
1224 | }
1225 | catch (CancelledException e) {
1226 | // This can never happen because there is no user interaction in headless!
1227 | }
1228 | catch (Exception exc) {
1229 | Msg.error(this, domFile.getPathname() + " Error during analysis: " + exc.getMessage(),
1230 | exc);
1231 | }
1232 | finally {
1233 |
1234 | if (program != null) {
1235 | AutoAnalysisManager.getAnalysisManager(program).dispose();
1236 | program.release(this);
1237 | program = null;
1238 | }
1239 |
1240 | if (!readOnlyFile) { // can't change anything if read-only file
1241 |
1242 | // Undo checkout of it is still checked-out and either the file is to be
1243 | // deleted, or we just checked it out and file changes have been committed
1244 | if (domFile.isCheckedOut()) {
1245 | if (!keepFile ||
1246 | (terminateCheckoutWhenDone && !domFile.modifiedSinceCheckout())) {
1247 | domFile.undoCheckout(false);
1248 | }
1249 | }
1250 |
1251 | if (!keepFile) {
1252 | deleteDomainFile(domFile);
1253 | }
1254 | }
1255 | }
1256 | }
1257 |
1258 | private void deleteDomainFile(DomainFile domFile) {
1259 | if (domFile.isCheckedOut()) {
1260 | Msg.error(this, "Failed to delete file as requested due to pre-existing checkout: " +
1261 | domFile.getPathname());
1262 | return;
1263 | }
1264 |
1265 | try {
1266 | domFile.delete();
1267 | }
1268 | catch (IOException e) {
1269 | Msg.error(this, "Failed to delete file as requested - " + e.getMessage() + ": " +
1270 | domFile.getPathname());
1271 | }
1272 | }
1273 |
1274 | /**
1275 | * Process all files within parentFolder which satisfies the specified filenamePattern.
1276 | * If filenamePattern is null, all files will be processed
1277 | * @param parentFolder domain folder to be searched
1278 | * @param filenamePattern filename pattern or null for all files
1279 | * @return true if one or more files processed
1280 | * @throws IOException if an IO problem occurred.
1281 | */
1282 | private boolean processFolderNoImport(DomainFolder parentFolder, Pattern filenamePattern)
1283 | throws IOException {
1284 |
1285 | if (parentFolder.isEmpty()) {
1286 | return false;
1287 | }
1288 |
1289 | boolean filesProcessed = false;
1290 |
1291 | for (DomainFile domFile : parentFolder.getFiles()) {
1292 | if (filenamePattern == null || filenamePattern.matcher(domFile.getName()).matches()) {
1293 | if (ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) {
1294 | filesProcessed = true;
1295 | processFileNoImport(domFile);
1296 | }
1297 | }
1298 | }
1299 |
1300 | if (options.recursive) {
1301 | for (DomainFolder folder : parentFolder.getFolders()) {
1302 | filesProcessed |= processFolderNoImport(folder, filenamePattern);
1303 | }
1304 | }
1305 |
1306 | return filesProcessed;
1307 | }
1308 |
1309 | /**
1310 | * Process the specified filename within parentFolder.
1311 | * @param parentFolder domain folder to be searched
1312 | * @param filename name of file to be imported
1313 | * @return true if one or more files processed
1314 | * @throws IOException if an IO problem occurred.
1315 | */
1316 | private boolean processFolderNoImport(DomainFolder parentFolder, String filename)
1317 | throws IOException {
1318 |
1319 | if (parentFolder.isEmpty()) {
1320 | return false;
1321 | }
1322 |
1323 | boolean filesProcessed = false;
1324 |
1325 | DomainFile domFile = parentFolder.getFile(filename);
1326 | if (ProgramContentHandler.PROGRAM_CONTENT_TYPE.equals(domFile.getContentType())) {
1327 | filesProcessed = true;
1328 | processFileNoImport(domFile);
1329 | }
1330 |
1331 | if (options.recursive) {
1332 | for (DomainFolder folder : parentFolder.getFolders()) {
1333 | filesProcessed |= processFolderNoImport(folder, filename);
1334 | }
1335 | }
1336 |
1337 | return filesProcessed;
1338 | }
1339 |
1340 | private void processNoImport(String rootFolderPath) throws IOException {
1341 |
1342 | storage.clear();
1343 |
1344 | DomainFolder domFolder = project.getProjectData().getFolder(rootFolderPath);
1345 | if (domFolder == null) {
1346 | throw new IOException("Specified project folder not found: " + rootFolderPath);
1347 | }
1348 |
1349 | Pattern filenamePattern = null;
1350 | if (options.domainFileNameToProcess != null) {
1351 | filenamePattern = createFilenamePattern(options.domainFileNameToProcess);
1352 | }
1353 |
1354 | boolean filesProcessed = false;
1355 | if (filenamePattern == null && options.domainFileNameToProcess != null) {
1356 | // assume domainFileNameToProcess was a specific filename and not a pattern
1357 | filesProcessed = processFolderNoImport(domFolder, options.domainFileNameToProcess);
1358 | }
1359 | else {
1360 | filesProcessed = processFolderNoImport(domFolder, filenamePattern);
1361 | }
1362 |
1363 | if (!filesProcessed) {
1364 | if (options.domainFileNameToProcess != null) {
1365 | throw new IOException("Requested project program file(s) not found: " +
1366 | options.domainFileNameToProcess);
1367 | }
1368 | throw new IOException("No program files found within specified project folder: " +
1369 | domFolder.getPathname());
1370 | }
1371 | }
1372 |
1373 | private Pattern createFilenamePattern(String name) {
1374 |
1375 | if ((name.indexOf('*') == -1) && (name.indexOf('?') == -1)) {
1376 | // not a 'search' pattern
1377 | return null;
1378 | }
1379 |
1380 | // If surrounded by single-quotes, strip them, as to not interfere with the Pattern
1381 | if ((name.startsWith("\'")) && (name.endsWith("\'"))) {
1382 | name = name.substring(1, name.length() - 1);
1383 | }
1384 |
1385 | // Find files that match the wildcard pattern
1386 | Pattern p = UserSearchUtils.createSearchPattern(name, true);
1387 | return p;
1388 | }
1389 |
1390 | private boolean checkOverwrite(DomainFile df) throws IOException {
1391 | if (options.overwrite) {
1392 | try {
1393 | if (df.isHijacked()) {
1394 | Msg.error(this,
1395 | "REPORT: Found conflicting program file in project which is hijacked - overwrite denied: " +
1396 | df.getPathname());
1397 | return false;
1398 | }
1399 | if (df.isVersioned()) {
1400 | if (!options.commit) {
1401 | Msg.error(this,
1402 | "REPORT: Found conflicting versioned program file in project with changes - overwrite denied when commit disabled: " +
1403 | df.getPathname());
1404 | return false;
1405 | }
1406 | if (df.isCheckedOut()) {
1407 | df.undoCheckout(false);
1408 | }
1409 | }
1410 | try {
1411 | df.delete();
1412 | }
1413 | catch (IOException e) {
1414 | Msg.error(this, "REPORT: Failed to remove conflicting program file (" +
1415 | e.getMessage() + "): " + df.getPathname());
1416 | return false;
1417 | }
1418 | }
1419 | catch (UserAccessException e) {
1420 | Msg.error(this,
1421 | "REPORT: Found conflicting program file in project which user is unable to overwrite: " +
1422 | df.getPathname());
1423 | return false;
1424 | }
1425 | Msg.warn(this,
1426 | "REPORT: Removed conflicting program file from project: " + df.getPathname());
1427 | }
1428 | else {
1429 | Msg.error(this,
1430 | "REPORT: Found conflicting program file in project: " + df.getPathname());
1431 | return false;
1432 | }
1433 | return true;
1434 | }
1435 |
1436 | private void commitProgram(DomainFile df) throws IOException {
1437 |
1438 | RepositoryAdapter rep = project.getRepository();
1439 | if (rep != null) {
1440 | try {
1441 | rep.connect();
1442 | }
1443 | catch (IOException e) {
1444 | ClientUtil.handleException(rep, e, "Connect", null);
1445 | }
1446 | if (!rep.isConnected()) {
1447 | Msg.error(this,
1448 | df.getPathname() + ": File check-in failed - repository connection error");
1449 | throw new IOException(
1450 | df.getPathname() + ": File check-in failed - repository connection error");
1451 | }
1452 | }
1453 |
1454 | if (df.canAddToRepository()) {
1455 | try {
1456 | df.addToVersionControl(options.commitComment, false, TaskMonitor.DUMMY);
1457 | Msg.info(this, "REPORT: Added file to repository: " + df.getPathname());
1458 | }
1459 | catch (IOException e) {
1460 | Msg.error(this, df.getPathname() + ": File check-in failed - " + e.getMessage());
1461 | throw e;
1462 | }
1463 | catch (CancelledException e) {
1464 | // this can never happen because there is no user interaction in headless!
1465 | }
1466 | }
1467 | else if (df.canCheckin()) {
1468 | try {
1469 | df.checkin(new CheckinHandler() {
1470 | @Override
1471 | public boolean keepCheckedOut() throws CancelledException {
1472 | return true;
1473 | }
1474 |
1475 | @Override
1476 | public String getComment() throws CancelledException {
1477 | return options.commitComment;
1478 | }
1479 |
1480 | @Override
1481 | public boolean createKeepFile() throws CancelledException {
1482 | return false;
1483 | }
1484 | }, TaskMonitor.DUMMY);
1485 | Msg.info(this, "REPORT: Committed file changes to repository: " + df.getPathname());
1486 | }
1487 | catch (IOException e) {
1488 | Msg.error(this, df.getPathname() + ": File check-in failed - " + e.getMessage());
1489 | throw e;
1490 | }
1491 | catch (VersionException e) {
1492 | Msg.error(this,
1493 | df.getPathname() + ": File check-in failed - version error occurred");
1494 | }
1495 | catch (CancelledException e) {
1496 | // this can never happen because there is no user interaction in headless!
1497 | }
1498 | }
1499 | else {
1500 | Msg.error(this, df.getPathname() + ": Unable to commit file");
1501 | }
1502 | }
1503 |
1504 | private boolean processFileWithImport(File file, String folderPath) {
1505 |
1506 | Msg.info(this, "IMPORTING: " + file.getAbsolutePath());
1507 |
1508 | Program program = null;
1509 |
1510 | try {
1511 | String dfName = null;
1512 | DomainFile df = null;
1513 | DomainFolder domainFolder = null;
1514 | try {
1515 | // Gets parent folder for import (creates path if doesn't exist)
1516 | domainFolder = getDomainFolder(folderPath, false);
1517 |
1518 | dfName = file.getName();
1519 |
1520 | if (dfName.toLowerCase().endsWith(".gzf") ||
1521 | dfName.toLowerCase().endsWith(".xml")) {
1522 | // Use filename without .gzf
1523 | int index = dfName.lastIndexOf('.');
1524 | dfName = dfName.substring(0, index);
1525 | }
1526 |
1527 | if (!options.readOnly) {
1528 | if (domainFolder != null) {
1529 | df = domainFolder.getFile(dfName);
1530 | }
1531 | if (df != null && !checkOverwrite(df)) {
1532 | return false;
1533 | }
1534 | df = null;
1535 | }
1536 |
1537 | program = loadProgram(file);
1538 | if (program == null) {
1539 | return false;
1540 | }
1541 |
1542 | // Check if there are defined memory blocks; abort if not (there is nothing
1543 | // to work with!)
1544 | if (program.getMemory().getAllInitializedAddressSet().isEmpty()) {
1545 | Msg.error(this, "REPORT: Error: No memory blocks were defined for file '" +
1546 | file.getAbsolutePath() + "'.");
1547 | return false;
1548 | }
1549 | }
1550 | catch (Exception exc) {
1551 | Msg.error(this, "REPORT: " + exc.getMessage(), exc);
1552 | exc.printStackTrace();
1553 | return false;
1554 | }
1555 |
1556 | Msg.info(this,
1557 | "REPORT: Import succeeded with language \"" +
1558 | program.getLanguageID().getIdAsString() + "\" and cspec \"" +
1559 | program.getCompilerSpec().getCompilerSpecID().getIdAsString() +
1560 | "\" for file: " + file.getAbsolutePath());
1561 |
1562 | boolean doSave;
1563 | try {
1564 |
1565 | doSave = analyzeProgram(file.getAbsolutePath(), program) && !options.readOnly;
1566 |
1567 | if (!doSave) {
1568 | program.setTemporary(true);
1569 | }
1570 |
1571 | // The act of marking the program as temporary by a script will signal
1572 | // us to discard any program changes.
1573 | if (program.isTemporary()) {
1574 | if (options.readOnly) {
1575 | Msg.info(this, "REPORT: Discarded file import due to readOnly option: " +
1576 | file.getAbsolutePath());
1577 | }
1578 | else {
1579 | Msg.info(this, "REPORT: Discarded file import as a result of script " +
1580 | "activity or analysis timeout: " + file.getAbsolutePath());
1581 | }
1582 | return true;
1583 | }
1584 |
1585 | try {
1586 | if (saveDomainFolder != null) {
1587 |
1588 | df = saveDomainFolder.getFile(dfName);
1589 |
1590 | // Return if file already exists and overwrite == false
1591 | if (df != null && !checkOverwrite(df)) {
1592 | return false;
1593 | }
1594 |
1595 | domainFolder = saveDomainFolder;
1596 | }
1597 | else if (domainFolder == null) {
1598 | domainFolder = getDomainFolder(folderPath, true);
1599 | }
1600 | df = domainFolder.createFile(dfName, program, TaskMonitor.DUMMY);
1601 | Msg.info(this, "REPORT: Save succeeded for file: " + df.getPathname());
1602 |
1603 | if (options.commit) {
1604 |
1605 | AutoAnalysisManager.getAnalysisManager(program).dispose();
1606 | program.release(this);
1607 | program = null;
1608 |
1609 | commitProgram(df);
1610 | }
1611 | }
1612 | catch (IOException e) {
1613 | e.printStackTrace();
1614 | throw new IOException("Cannot create file: " + domainFolder.getPathname() +
1615 | DomainFolder.SEPARATOR + dfName, e);
1616 | }
1617 | }
1618 | catch (Exception exc) {
1619 | String logErrorMsg =
1620 | file.getAbsolutePath() + " Error during analysis: " + exc.getMessage();
1621 | Msg.info(this, logErrorMsg);
1622 | return false;
1623 | }
1624 | finally {
1625 | if (program != null) {
1626 | AutoAnalysisManager.getAnalysisManager(program).dispose();
1627 | }
1628 | }
1629 |
1630 | return true;
1631 | }
1632 | finally {
1633 | // Program must be released here, since the AutoAnalysisManager uses program to
1634 | // call dispose() in the finally() block above.
1635 | if (program != null) {
1636 | program.release(this);
1637 | program = null;
1638 | }
1639 | }
1640 | }
1641 |
1642 | private Program loadProgram(File file) throws VersionException, InvalidNameException,
1643 | DuplicateNameException, CancelledException, IOException {
1644 |
1645 | MessageLog messageLog = new MessageLog();
1646 | Program program = null;
1647 |
1648 | // NOTE: we must pass a null DomainFolder to the AutoImporter so as not to
1649 | // allow the DomainFile to be saved at this point. DomainFile should be
1650 | // saved after all applicable analysis/scripts are run.
1651 |
1652 | if (options.loaderClass == null) {
1653 | // User did not specify a loader
1654 | if (options.language == null) {
1655 | program = AutoImporter.importByUsingBestGuess(file, null, null, this, messageLog,
1656 | TaskMonitor.DUMMY).getPrimaryDomainObject();
1657 | }
1658 | else {
1659 | program = AutoImporter.importByLookingForLcs(file, null, null, options.language,
1660 | options.compilerSpec, this, messageLog, TaskMonitor.DUMMY).getPrimaryDomainObject();
1661 | }
1662 | }
1663 | else {
1664 | // User specified a loader
1665 | if (options.language == null) {
1666 | program = AutoImporter.importByUsingSpecificLoaderClass(file, null, null,
1667 | options.loaderClass, options.loaderArgs, this, messageLog, TaskMonitor.DUMMY).getPrimaryDomainObject();
1668 | }
1669 | else {
1670 | program = AutoImporter.importByUsingSpecificLoaderClassAndLcs(file, null, null,
1671 | options.loaderClass, options.loaderArgs, options.language, options.compilerSpec,
1672 | this, messageLog, TaskMonitor.DUMMY).getPrimaryDomainObject();
1673 | }
1674 | }
1675 |
1676 | if (program == null) {
1677 | Msg.error(this, "The AutoImporter could not successfully load " +
1678 | file.getAbsolutePath() +
1679 | " with the provided import parameters. Please ensure that any specified" +
1680 | " processor/cspec arguments are compatible with the loader that is used during" +
1681 | " import and try again.");
1682 |
1683 | if (options.loaderClass != null && options.loaderClass != BinaryLoader.class) {
1684 | Msg.error(this,
1685 | "NOTE: Import failure may be due to missing opinion for \"" +
1686 | options.loaderClass.getSimpleName() +
1687 | "\". If so, please contact Ghidra team for assistance.");
1688 | }
1689 |
1690 | return null;
1691 | }
1692 |
1693 | return program;
1694 | }
1695 |
1696 | private void processWithImport(File file, String folderPath, boolean isFirstTime)
1697 | throws IOException {
1698 |
1699 | boolean importSucceeded;
1700 |
1701 | if (file.isFile()) {
1702 |
1703 | importSucceeded = processFileWithImport(file, folderPath);
1704 |
1705 | // Check to see if there are transient programs lying around due
1706 | // to programs not being released during Importing
1707 | List domainFileContainer = new ArrayList<>();
1708 | TransientDataManager.getTransients(domainFileContainer);
1709 | if (domainFileContainer.size() > 0) {
1710 | TransientDataManager.releaseFiles(this);
1711 | }
1712 |
1713 | if (!importSucceeded) {
1714 | Msg.error(this, "REPORT: Import failed for file: " + file.getAbsolutePath());
1715 | }
1716 |
1717 | return;
1718 | }
1719 |
1720 | // Looks inside the folder if one of two situations is applicable:
1721 | // - If user supplied a directory to import, and it is currently being
1722 | // processed (if so, this will be the first time that this method is called)
1723 | // - If -recursive is specified
1724 | if ((isFirstTime) || (!isFirstTime && options.recursive)) {
1725 | // Otherwise, is a directory
1726 | Msg.info(this, "REPORT: Importing all files from " + file.getName());
1727 |
1728 | File dirFile = file;
1729 |
1730 | if (!folderPath.endsWith(DomainFolder.SEPARATOR)) {
1731 | folderPath += DomainFolder.SEPARATOR;
1732 | }
1733 |
1734 | String subfolderPath = folderPath + file.getName();
1735 |
1736 | String[] names = dirFile.list();
1737 | if (names != null) {
1738 | Collections.sort(Arrays.asList(names));
1739 | for (String name : names) {
1740 | if (name.charAt(0) == '.') {
1741 | Msg.warn(this, "Ignoring file '" + name + "'.");
1742 | continue;
1743 | }
1744 | file = new File(dirFile, name);
1745 |
1746 | // Even a directory name has to have valid characters --
1747 | // can't create a folder if it's not valid
1748 | try {
1749 | checkValidFilename(file);
1750 | processWithImport(file, subfolderPath, false);
1751 | }
1752 | catch (InvalidInputException e) {
1753 | // Just move on if not valid
1754 | }
1755 | }
1756 | }
1757 | }
1758 | }
1759 |
1760 | private void processWithImport(String folderPath, List inputDirFiles) throws IOException {
1761 |
1762 | storage.clear();
1763 |
1764 | if (inputDirFiles != null && !inputDirFiles.isEmpty()) {
1765 | Msg.info(this, "REPORT: Processing input files: ");
1766 | Msg.info(this, " project: " + project.getProjectLocator());
1767 | for (File f : inputDirFiles) {
1768 | processWithImport(f, folderPath, true);
1769 | }
1770 | }
1771 | else {
1772 | //no input, just run the scripts
1773 |
1774 | //create one state, in case each script might want to modify it to pass information
1775 | GhidraState scriptState = new GhidraState(null, project, null, null, null, null);
1776 |
1777 | LibHeadlessContinuationOption scriptStatus = LibHeadlessContinuationOption.CONTINUE;
1778 |
1779 | scriptStatus = runScriptsList(options.preScripts, options.preScriptFileMap, scriptState,
1780 | scriptStatus);
1781 |
1782 | // Since there is no program, "DELETE" is meaningless here.
1783 | // If status asks for ABORT, then don't continue running the postscript.
1784 | switch (scriptStatus) {
1785 | case ABORT:
1786 | case ABORT_AND_DELETE:
1787 | return;
1788 |
1789 | default:
1790 | // Just continue
1791 | }
1792 |
1793 | runScriptsList(options.postScripts, options.postScriptFileMap, scriptState,
1794 | scriptStatus);
1795 | }
1796 | }
1797 |
1798 | private Project openProject(ProjectLocator locator) throws IOException {
1799 | Project tempProject;
1800 |
1801 | if (options.deleteProject) {
1802 | Msg.warn(this, "Project already exists and will not be deleted: " + locator);
1803 | options.deleteProject = false;
1804 | }
1805 |
1806 | Msg.info(this, "Opening existing project: " + locator);
1807 | try {
1808 | tempProject = new HeadlessProject(getProjectManager(), locator);
1809 | }
1810 | catch (NotOwnerException e) {
1811 | throw new IOException(e);
1812 | }
1813 | catch (LockException e) {
1814 | throw new IOException(e);
1815 | }
1816 |
1817 | return tempProject;
1818 |
1819 | }
1820 |
1821 | /**
1822 | * Checks to make sure the given file contains only valid characters in its name.
1823 | *
1824 | * @param currFile The file to check.
1825 | * @throws InvalidInputException if the given file contains invalid characters in it.
1826 | */
1827 | static void checkValidFilename(File currFile) throws InvalidInputException {
1828 | boolean isDir = currFile.isDirectory();
1829 | String filename = currFile.getName();
1830 |
1831 | for (int i = 0; i < filename.length(); i++) {
1832 | char c = filename.charAt(i);
1833 | if (!LocalFileSystem.isValidNameCharacter(c)) {
1834 | if (isDir) {
1835 | throw new InvalidInputException("The directory '" + filename +
1836 | "' contains the invalid characgter: \'" + c +
1837 | "\' and can not be created in the project (full path: " +
1838 | currFile.getAbsolutePath() +
1839 | "). To allow successful import of the directory and its contents, please rename the directory.");
1840 | }
1841 | throw new InvalidInputException(
1842 | "The file '" + filename + "' contains the invalid character: \'" + c +
1843 | "\' and can not be imported (full path: " + currFile.getAbsolutePath() +
1844 | "). Please rename the file.");
1845 | }
1846 | }
1847 | }
1848 |
1849 | private HeadlessGhidraProjectManager getProjectManager() {
1850 | if (projectManager == null) {
1851 | projectManager = new HeadlessGhidraProjectManager();
1852 | }
1853 | return projectManager;
1854 | }
1855 |
1856 | /**
1857 | * Ghidra project class required to gain access to specialized project constructor
1858 | * for URL connection.
1859 | */
1860 | private static class HeadlessProject extends DefaultProject {
1861 |
1862 | HeadlessProject(HeadlessGhidraProjectManager projectManager, GhidraURLConnection connection)
1863 | throws IOException {
1864 | super(projectManager, (DefaultProjectData) connection.getProjectData());
1865 | }
1866 |
1867 | HeadlessProject(HeadlessGhidraProjectManager projectManager, ProjectLocator projectLocator)
1868 | throws NotOwnerException, LockException, IOException {
1869 | super(projectManager, projectLocator, false);
1870 | }
1871 | }
1872 |
1873 | private static class HeadlessGhidraProjectManager extends DefaultProjectManager {
1874 | // this exists just to allow access to the constructor
1875 | }
1876 | }
1877 |
--------------------------------------------------------------------------------