From 425667e640a6881dfc6e994ca2d21bc714e10347 Mon Sep 17 00:00:00 2001 From: dev747368 <48332326+dev747368@users.noreply.github.com> Date: Tue, 21 Jul 2020 18:34:35 -0400 Subject: [PATCH] GP-42 Initial implementation of Pdb symbol store / symbol servers --- GPL/CabExtract/build.gradle | 4 + .../data/PDB_SYMBOL_SERVER_URLS.pdburl | 7 +- .../help/topics/ImporterPlugin/load_pdb.html | 156 --- .../ghidra/app/util/bin/BinaryReader.java | 47 +- .../app/util/bin/format/pdb/PdbFactory.java | 53 - .../app/util/bin/format/pdb/PdbInfo.java | 63 ++ .../util/bin/format/pdb/PdbInfoCodeView.java | 113 +++ .../util/bin/format/pdb/PdbInfoDotNet.java | 131 +++ .../bin/format/pdb/PdbInfoDotNetIface.java | 33 - .../app/util/bin/format/pdb/PdbInfoIface.java | 32 - .../bin/format/pe/DebugDataDirectory.java | 8 +- .../bin/format/pe/debug/DebugCodeView.java | 28 +- .../app/util/datatype/microsoft/GUID.java | 124 ++- .../util/opinion/AbstractPeDebugLoader.java | 54 +- Ghidra/Features/PDB/certification.manifest | 21 +- .../PdbExamplePrescript.java} | 33 +- .../PdbSymbolServerExamplePrescript.java | 53 + .../PDB/src/global/docs/README_PDB.html | 2 +- .../PDB/src/main/help/help/TOC_Source.xml | 4 +- .../main/help/help/topics/Pdb/LoadPDBNew.html | 226 +++++ .../PDB/src/main/help/help/topics/Pdb/PDB.htm | 130 ++- .../help/topics/Pdb/download_pdb_file.html | 184 ---- .../images/KnownSymbolServerURLsDialog.png | Bin 14100 -> 0 bytes .../images/LoadPdb_Advanced_NeedsConfig.png | Bin 0 -> 46293 bytes .../images/LoadPdb_Advanced_Screenshot.png | Bin 0 -> 81637 bytes .../Pdb/images/LoadPdb_Initial_Screenshot.png | Bin 0 -> 31547 bytes .../help/topics/Pdb/images/PdbOrXmlDialog.png | Bin 9684 -> 0 bytes .../Pdb/images/PeSpecifiedPathDialog.png | Bin 8995 -> 0 bytes .../help/help/topics/Pdb/images/Plus2.png | Bin 0 -> 752 bytes .../help/topics/Pdb/images/SuccessDialog.png | Bin 14850 -> 0 bytes .../SymbolServerConfig_AddButtonMenu.png | Bin 0 -> 10777 bytes .../images/SymbolServerConfig_Screenshot.png | Bin 0 -> 14525 bytes .../Pdb/images/SymbolServerURLDialog.png | Bin 11146 -> 0 bytes .../main/help/help/topics/Pdb/images/disk.png | Bin 0 -> 620 bytes .../main/help/help/topics/Pdb/images/down.png | Bin 0 -> 192 bytes .../help/help/topics/Pdb/images/error.png | Bin 0 -> 1150 bytes .../help/help/topics/Pdb/images/reload3.png | Bin 0 -> 979 bytes .../main/help/help/topics/Pdb/images/up.png | Bin 0 -> 193 bytes .../app/plugin/core/analysis/PdbAnalyzer.java | 150 +-- .../core/analysis/PdbAnalyzerCommon.java | 182 ++++ .../core/analysis/PdbUniversalAnalyzer.java | 173 +--- .../app/util/bin/format/pdb/PdbInfo.java | 90 -- .../util/bin/format/pdb/PdbInfoDotNet.java | 89 -- .../app/util/bin/format/pdb/PdbParser.java | 347 +------ .../format/pdb2/pdbreader/PdbIdentifiers.java | 35 +- .../main/java/pdb/AskPdbOptionsDialog.java | 128 --- .../src/main/java/pdb/AskPdbUrlDialog.java | 230 ----- .../PDB/src/main/java/pdb/LoadPdbTask.java | 40 +- .../PDB/src/main/java/pdb/PdbInitializer.java | 31 - .../PDB/src/main/java/pdb/PdbPlugin.java | 226 +++-- .../main/java/pdb/PdbSymbolServerPlugin.java | 856 ---------------- .../PDB/src/main/java/pdb/PdbUtils.java | 117 +++ .../PDB/src/main/java/pdb/URLChoice.java | 34 - .../symbolserver/AbstractSymbolServer.java | 135 +++ .../symbolserver/DisabledSymbolServer.java | 127 +++ .../java/pdb/symbolserver/FindOption.java | 61 ++ .../pdb/symbolserver/HttpSymbolServer.java | 150 +++ .../pdb/symbolserver/LocalSymbolStore.java | 389 ++++++++ .../pdb/symbolserver/SameDirSymbolStore.java | 164 +++ .../java/pdb/symbolserver/SymbolFileInfo.java | 274 +++++ .../pdb/symbolserver/SymbolFileLocation.java | 114 +++ .../java/pdb/symbolserver/SymbolServer.java | 110 ++ .../symbolserver/SymbolServerInputStream.java | 60 ++ .../SymbolServerInstanceCreatorContext.java | 64 ++ .../SymbolServerInstanceCreatorRegistry.java | 220 ++++ .../pdb/symbolserver/SymbolServerService.java | 288 ++++++ .../java/pdb/symbolserver/SymbolStore.java | 91 ++ .../pdb/symbolserver/ui/ConfigPdbDialog.java | 79 ++ .../pdb/symbolserver/ui/FilePromptDialog.java | 195 ++++ .../pdb/symbolserver/ui/LoadPdbDialog.java | 942 ++++++++++++++++++ .../pdb/symbolserver/ui/SymbolFilePanel.java | 182 ++++ .../pdb/symbolserver/ui/SymbolFileRow.java | 53 + .../symbolserver/ui/SymbolFileTableModel.java | 289 ++++++ .../symbolserver/ui/SymbolServerPanel.java | 594 +++++++++++ .../pdb/symbolserver/ui/SymbolServerRow.java | 76 ++ .../ui/SymbolServerTableModel.java | 309 ++++++ .../ui/WellKnownSymbolServerLocation.java | 112 +++ .../util/bin/format/pdb/PdbParserTest.java | 915 +---------------- .../pdb/symbolserver/DummySymbolServer.java | 85 ++ .../symbolserver/HttpSymbolServerTest.java | 40 + .../symbolserver/LocalSymbolServerTest.java | 225 +++++ ...mbolServerInstanceCreatorRegistryTest.java | 101 ++ .../symbolserver/SymbolServerServiceTest.java | 160 +++ .../java/docking/DialogComponentProvider.java | 9 +- .../widgets/textfield/IntegerTextField.java | 14 +- .../src/main/java/ghidra/net/HttpClients.java | 88 ++ .../ghidra/net/SSLContextInitializer.java | 4 + .../main/java/ghidra/net/http/HttpUtil.java | 31 +- .../util/filechooser/ExtensionFileFilter.java | 183 +--- .../ghidra/util/task/MonitoredRunnable.java | 15 +- .../java/ghidra/net/http/HttpUtilTest.java | 48 + .../java/help/screenshot/PdbScreenShots.java | 221 ++-- .../SymbolServerService2Test.java | 138 +++ 93 files changed, 7715 insertions(+), 3874 deletions(-) delete mode 100644 Ghidra/Features/Base/src/main/help/help/topics/ImporterPlugin/load_pdb.html delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbFactory.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfo.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoCodeView.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoDotNet.java delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoDotNetIface.java delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoIface.java rename Ghidra/Features/PDB/{src/main/java/ghidra/app/util/bin/format/pdb/GhidraPdbFactory.java => developer_scripts/PdbExamplePrescript.java} (50%) create mode 100644 Ghidra/Features/PDB/developer_scripts/PdbSymbolServerExamplePrescript.java create mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/LoadPDBNew.html delete mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/download_pdb_file.html delete mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/KnownSymbolServerURLsDialog.png create mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/LoadPdb_Advanced_NeedsConfig.png create mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/LoadPdb_Advanced_Screenshot.png create mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/LoadPdb_Initial_Screenshot.png delete mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/PdbOrXmlDialog.png delete mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/PeSpecifiedPathDialog.png create mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/Plus2.png delete mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/SuccessDialog.png create mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/SymbolServerConfig_AddButtonMenu.png create mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/SymbolServerConfig_Screenshot.png delete mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/SymbolServerURLDialog.png create mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/disk.png create mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/down.png create mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/error.png create mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/reload3.png create mode 100644 Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/up.png create mode 100644 Ghidra/Features/PDB/src/main/java/ghidra/app/plugin/core/analysis/PdbAnalyzerCommon.java delete mode 100644 Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfo.java delete mode 100644 Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoDotNet.java delete mode 100644 Ghidra/Features/PDB/src/main/java/pdb/AskPdbOptionsDialog.java delete mode 100644 Ghidra/Features/PDB/src/main/java/pdb/AskPdbUrlDialog.java delete mode 100644 Ghidra/Features/PDB/src/main/java/pdb/PdbInitializer.java delete mode 100644 Ghidra/Features/PDB/src/main/java/pdb/PdbSymbolServerPlugin.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/PdbUtils.java delete mode 100644 Ghidra/Features/PDB/src/main/java/pdb/URLChoice.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/AbstractSymbolServer.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/DisabledSymbolServer.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/FindOption.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/HttpSymbolServer.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/LocalSymbolStore.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SameDirSymbolStore.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolFileInfo.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolFileLocation.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServer.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInputStream.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorContext.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorRegistry.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerService.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolStore.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/ConfigPdbDialog.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/FilePromptDialog.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/LoadPdbDialog.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolFilePanel.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolFileRow.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolFileTableModel.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerPanel.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerRow.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerTableModel.java create mode 100644 Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/WellKnownSymbolServerLocation.java create mode 100644 Ghidra/Features/PDB/src/test/java/pdb/symbolserver/DummySymbolServer.java create mode 100644 Ghidra/Features/PDB/src/test/java/pdb/symbolserver/HttpSymbolServerTest.java create mode 100644 Ghidra/Features/PDB/src/test/java/pdb/symbolserver/LocalSymbolServerTest.java create mode 100644 Ghidra/Features/PDB/src/test/java/pdb/symbolserver/SymbolServerInstanceCreatorRegistryTest.java create mode 100644 Ghidra/Features/PDB/src/test/java/pdb/symbolserver/SymbolServerServiceTest.java create mode 100644 Ghidra/Framework/Generic/src/main/java/ghidra/net/HttpClients.java create mode 100644 Ghidra/Framework/Generic/src/test/java/ghidra/net/http/HttpUtilTest.java create mode 100644 Ghidra/Test/IntegrationTest/src/test/java/pdb/symbolserver/SymbolServerService2Test.java diff --git a/GPL/CabExtract/build.gradle b/GPL/CabExtract/build.gradle index 46994edbc7..374a463115 100644 --- a/GPL/CabExtract/build.gradle +++ b/GPL/CabExtract/build.gradle @@ -17,6 +17,10 @@ eclipse.project.name = 'GPL CabExtract' project.ext.cabextract = "cabextract-1.6" +/********************************************************************************* + * Deprecated - will be removed + *********************************************************************************/ + /********************************************************************************* * CabExtract platform specific tasks * diff --git a/Ghidra/Configurations/Public_Release/data/PDB_SYMBOL_SERVER_URLS.pdburl b/Ghidra/Configurations/Public_Release/data/PDB_SYMBOL_SERVER_URLS.pdburl index a01d71bf03..06da4577c6 100644 --- a/Ghidra/Configurations/Public_Release/data/PDB_SYMBOL_SERVER_URLS.pdburl +++ b/Ghidra/Configurations/Public_Release/data/PDB_SYMBOL_SERVER_URLS.pdburl @@ -1 +1,6 @@ -Internet,https://msdl.microsoft.com/download/symbols +Internet|https://msdl.microsoft.com/download/symbols/|WARNING: Check your organization's security policy before downloading files from the internet. +Internet|https://chromium-browser-symsrv.commondatastorage.googleapis.com|WARNING: Check your organization's security policy before downloading files from the internet. +Internet|https://symbols.mozilla.org/|WARNING: Check your organization's security policy before downloading files from the internet. +Internet|https://software.intel.com/sites/downloads/symbols/|WARNING: Check your organization's security policy before downloading files from the internet. +Internet|https://driver-symbols.nvidia.com/|WARNING: Check your organization's security policy before downloading files from the internet. +Internet|https://download.amd.com/dir/bin|WARNING: Check your organization's security policy before downloading files from the internet. diff --git a/Ghidra/Features/Base/src/main/help/help/topics/ImporterPlugin/load_pdb.html b/Ghidra/Features/Base/src/main/help/help/topics/ImporterPlugin/load_pdb.html deleted file mode 100644 index e54775e07a..0000000000 --- a/Ghidra/Features/Base/src/main/help/help/topics/ImporterPlugin/load_pdb.html +++ /dev/null @@ -1,156 +0,0 @@ - - - - - - - Load PDB File - - - - -

Load PDB File

- -

A program database (PDB) file holds debugging and project state information about a program - and can be created in a number of ways. Historically, it has been created using a Microsoft - compiler and written in C/C++, C#, and Visual Basic. - A user generates a PDB file using the /ZI or /Zi flag (for C/C++ programs) or the - /debug flag (for Visual Basic/C# programs).

- -

There are two mechanisms for processing a PDB file. First, the platform-independent - PDB Universal Reader/Analyzer, which can read a raw PDB file and apply it. Its capabilities - are expected to be expanded in future releases. Second, the legacy capability that uses the - DIA SDK to read information from the PDB file. This mechanism can only run - on a Windows platform, however it creates an XML representation of information gleaned using - the DIA SDK. These XML files can be saved and then used on Windows and non-Windows platforms - hosting Ghidra.

- -

If loading a PDB, this should be done prior to other analysis, except in special cases, - such as when only loading data types.

- -

Restricted loading of data types or public symbols is - supported by PDB Universal.

- -

To Load a PDB

- -
-
    -
  1. From the menu-bar of a tool, select File Load PDB File
  2. - -
  3. In the file chooser, select the PDB file (*.PDB or *.PDB.XML)
  4. - -
  5. Click the "Select PDB" button
  6. -
- -
- -

When a user chooses a PDB or XML file to load for a program, Ghidra will verify its - signature to be valid for the program. At this time, the PDB MSDIA loader cannot be used to - force-load a mismatched PDB. To perform a force-load of a PDB file, the user must choose the - PDB Universal loader if given the option. Force-loading an mismatched file can have - consequences, such as loading incorrect data types and symbols located at the wrong - addresses.

- -

- PDB files may also be loaded using the PDB Analyzer, which is available through - Auto Analysis or as - a One Shot Analyzer. -

-
- -

Information Loaded From PDB

- -
-
    -
  1. Structure and union definitions
  2. - -
  3. Typedefs
  4. - -
  5. Enumerations
  6. - -
  7. Class definitions
  8. - -
  9. Function prototypes
  10. - -
  11. Stack variable names and data types
  12. - -
  13. Source line numbers
  14. - -
  15. Instruction and data symbols
  16. -
-
- -

Loading Errors

- -
-

Before the PDB file is loaded into the program, then PDB signature and age are matched - against the information stored in the executable. If these values do not match, then the PDB - will not be loaded.

- -

- -

Figure 1

-
- -

The DIA SDK-Based Capability

- -

*.PDB.XML files can be created in three different ways: - -

-

-

NOTE: Execution of pdb.exe has runtime dependencies which must be satisfied. - Please refer to the README_PDB document for details.

- -

Debug Interface Access SDK

- -
-

The Microsoft Debug Interface Access Software Development Kit (DIA SDK) provides access to - debug information stored in program database (.PDB) files generated by Microsoft - post-compiler tools. Because the format of the .PDB file generated by the post-compiler tools - undergoes constant revision, exposing the format is impractical. Using the DIA API, you can - develop applications that search for and browse debug information stored in a .PDB file. Such - applications could, for example, report stack trace-back information and analyze performance - data.

- -

If you are attempting to load a PDB on a - Windows machine and see an error message such as "Unable to locate the DIA SDK," - you will need to add and register one or more files on your computer. Refer to the - README_PDB document for detailed instructions. -

-
- - diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/BinaryReader.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/BinaryReader.java index c07c127c00..c7006f9a4f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/BinaryReader.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/BinaryReader.java @@ -66,11 +66,13 @@ public class BinaryReader { this.provider = provider; setLittleEndian(isLittleEndian); } - + /** - * Returns a clone of this reader positioned at the new index. + * Returns a clone of this reader, with its own independent current position, + * positioned at the new index. + * * @param newIndex the new index - * @return a clone of this reader positioned at the new index + * @return an independent clone of this reader positioned at the new index */ public BinaryReader clone(long newIndex) { BinaryReader clone = new BinaryReader(provider, isLittleEndian()); @@ -88,6 +90,36 @@ public class BinaryReader { return clone(currentIndex); } + /** + * Returns a BinaryReader that is in BigEndian mode. + * + * @return either this same instance (if already BigEndian), or a new instance + * (at the same location) in BigEndian mode + */ + public BinaryReader asBigEndian() { + if (isBigEndian()) { + return this; + } + BinaryReader result = clone(currentIndex); + result.setLittleEndian(false); + return result; + } + + /** + * Returns a BinaryReader that is in LittleEndian mode. + * + * @return either this same instance (if already LittleEndian), or a new instance + * (at the same location) in LittleEndian mode + */ + public BinaryReader asLittleEndian() { + if (!isBigEndian()) { + return this; + } + BinaryReader result = clone(currentIndex); + result.setLittleEndian(true); + return result; + } + /** * Returns true if this reader will extract values in little endian, * otherwise in big endian. @@ -97,6 +129,15 @@ public class BinaryReader { return converter instanceof LittleEndianDataConverter; } + /** + * Returns true if this reader will extract values in big endian. + * + * @return true is big endian, false is little endian + */ + public boolean isBigEndian() { + return converter instanceof BigEndianDataConverter; + } + /** * Sets the endian of this binary reader. * @param isLittleEndian true for little-endian and false for big-endian diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbFactory.java deleted file mode 100644 index 7dbf1cecaf..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbFactory.java +++ /dev/null @@ -1,53 +0,0 @@ -/* ### - * IP: GHIDRA - * REVIEWED: YES - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.util.bin.format.pdb; - -import java.io.*; - -import ghidra.app.util.bin.*; -import ghidra.framework.*; - -public class PdbFactory { - static { - PluggableServiceRegistry.registerPluggableService(PdbFactory.class, - new PdbFactory()); - } - - public static PdbInfoDotNetIface getPdbInfoDotNetInstance( - BinaryReader reader, int ptr) throws IOException { - PdbFactory factory = PluggableServiceRegistry - .getPluggableService(PdbFactory.class); - return factory.doGetPdbInfoDotNetInstance(reader, ptr); - } - - public static PdbInfoIface getPdbInfoInstance(BinaryReader reader, int ptr) - throws IOException { - PdbFactory factory = PluggableServiceRegistry - .getPluggableService(PdbFactory.class); - return factory.doGetPdbInfoInstance(reader, ptr); - } - - protected PdbInfoDotNetIface doGetPdbInfoDotNetInstance( - BinaryReader reader, int ptr) throws IOException { - return null; - } - - protected PdbInfoIface doGetPdbInfoInstance(BinaryReader reader, int ptr) - throws IOException { - return null; - } -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfo.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfo.java new file mode 100644 index 0000000000..4e841fc272 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfo.java @@ -0,0 +1,63 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.pdb; + +import java.io.IOException; + +import ghidra.app.util.bin.BinaryReader; +import ghidra.framework.options.Options; + +/** + * Bag of information about a Pdb symbol file, usually extracted from information present in a PE + * binary. + * + */ +public interface PdbInfo { + + /** + * Read either a {@link PdbInfoCodeView} object or a {@link PdbInfoDotNet} object + * from the BinaryReader of a PE binary. + * + * @param reader BinaryReader + * @param offset position of the debug info + * @return new PdbInfoCodeView or PdbInfoDotNet object + * @throws IOException if error + */ + public static PdbInfo read(BinaryReader reader, long offset) throws IOException { + if (PdbInfoCodeView.isMatch(reader, offset)) { + return PdbInfoCodeView.read(reader, offset); + } + if (PdbInfoDotNet.isMatch(reader, offset)) { + return PdbInfoDotNet.read(reader, offset); + } + return null; + } + + /** + * Returns true if this instance is valid. + * + * @return boolean true if valid (magic signature matches and fields have valid data) + */ + boolean isValid(); + + /** + * Writes the various PDB info fields to a program's options. + * + * @param options Options of a Program to write to + */ + void serializeToOptions(Options options); + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoCodeView.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoCodeView.java new file mode 100644 index 0000000000..3a47e7bb1a --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoCodeView.java @@ -0,0 +1,113 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.pdb; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.io.FilenameUtils; + +import ghidra.app.util.bin.BinaryReader; +import ghidra.app.util.bin.StructConverter; +import ghidra.app.util.bin.format.pe.debug.DebugCodeViewConstants; +import ghidra.framework.options.Options; +import ghidra.program.model.data.*; +import ghidra.util.Conv; + +/** + * Older style pdb information, using a simple 32bit hash to link the pdb to its binary. + */ +public class PdbInfoCodeView implements StructConverter, PdbInfo { + private static final int MAGIC = + DebugCodeViewConstants.SIGNATURE_NB << 16 | DebugCodeViewConstants.VERSION_10; + + /** + * Returns true if the pdb information at the specified offset is a {@link PdbInfoCodeView} + * type (based on the signature at that offset). + * + * @param reader {@link BinaryReader} + * @param offset offset of the Pdb information + * @return boolean true if it is a {@link PdbInfoCodeView} type + * @throws IOException if error reading data + */ + public static boolean isMatch(BinaryReader reader, long offset) throws IOException { + //read value out as big endian + int value = reader.asBigEndian().readInt(offset); + return MAGIC == value; + } + + /** + * Reads the pdb information from a PE binary. + * + * @param reader {@link BinaryReader} + * @param offset offset of the Pdb information + * @return new {@link PdbInfoCodeView} instance, never null + * @throws IOException if error reading data + */ + public static PdbInfoCodeView read(BinaryReader reader, long offset) throws IOException { + reader = reader.clone(offset); + + PdbInfoCodeView result = new PdbInfoCodeView(); + result.magic = reader.readNextByteArray(4); + result.offset = reader.readNextInt(); + result.sig = reader.readNextInt(); + result.age = reader.readNextInt(); + result.pdbPath = reader.readNextAsciiString(); + result.pdbName = FilenameUtils.getName(result.pdbPath); + + return result; + } + + private byte[] magic; + private int offset; + private int sig; + private int age; + private String pdbName; + private String pdbPath; + + private PdbInfoCodeView() { + // nothing + } + + @Override + public boolean isValid() { + return magic.length == 4 && !pdbName.isBlank(); + } + + @Override + public void serializeToOptions(Options options) { + options.setString(PdbParserConstants.PDB_VERSION, + new String(magic, StandardCharsets.US_ASCII)); + options.setString(PdbParserConstants.PDB_SIGNATURE, Conv.toHexString(sig)); + options.setString(PdbParserConstants.PDB_AGE, Integer.toHexString(age)); + options.setString(PdbParserConstants.PDB_FILE, pdbName); + } + + @Override + public DataType toDataType() { + StructureDataType struct = new StructureDataType("PdbInfo", 0); + struct.add(new StringDataType(), magic.length, "signature", null); + struct.add(new DWordDataType(), "offset", null); + struct.add(new DWordDataType(), "sig", null); + struct.add(new DWordDataType(), "age", null); + if (pdbName.length() > 0) { + struct.add(new StringDataType(), pdbName.length(), "pdbname", null); + } + struct.setCategoryPath(new CategoryPath("/PDB")); + return struct; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoDotNet.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoDotNet.java new file mode 100644 index 0000000000..605ff1aa51 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoDotNet.java @@ -0,0 +1,131 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.pdb; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.io.FilenameUtils; + +import ghidra.app.util.bin.BinaryReader; +import ghidra.app.util.bin.StructConverter; +import ghidra.app.util.bin.format.pe.debug.DebugCodeViewConstants; +import ghidra.app.util.datatype.microsoft.GUID; +import ghidra.app.util.datatype.microsoft.GuidDataType; +import ghidra.framework.options.Options; +import ghidra.program.model.data.*; + +/** + * Newer style pdb information, using a GUID to link the pdb to its binary. + */ +public class PdbInfoDotNet implements StructConverter, PdbInfo { + private static final int MAGIC = + DebugCodeViewConstants.SIGNATURE_DOT_NET << 16 | DebugCodeViewConstants.VERSION_DOT_NET; + + /** + * Returns true if the pdb information at the specified offset is a {@link PdbInfoDotNet} + * type (based on the signature at that offset). + * + * @param reader {@link BinaryReader} + * @param offset offset of the Pdb information + * @return boolean true if it is a {@link PdbInfoDotNet} type + * @throws IOException if error reading data + */ + public static boolean isMatch(BinaryReader reader, long offset) throws IOException { + //read value out as big endian + int value = reader.asBigEndian().readInt(offset); + return MAGIC == value; + } + + /** + * Reads an instance from the stream. + * + * @param reader {@link BinaryReader} to read from + * @param offset position of the pdb info + * @return new instance, never null + * @throws IOException if IO error or data format error + */ + public static PdbInfoDotNet read(BinaryReader reader, long offset) throws IOException { + reader = reader.clone(offset); + + PdbInfoDotNet result = new PdbInfoDotNet(); + result.magic = reader.readNextByteArray(4); + result.guid = new GUID(reader); + result.age = reader.readNextInt(); + result.pdbPath = reader.readNextAsciiString(); + result.pdbName = FilenameUtils.getName(result.pdbPath); + + return result; + } + + /** + * Creates an instance from explicit values. + * + * @param pdbPath String path / filename of the pdb file + * @param age age + * @param guid {@link GUID} + * @return new instance, never null + */ + public static PdbInfoDotNet fromValues(String pdbPath, int age, GUID guid) { + PdbInfoDotNet result = new PdbInfoDotNet(); + result.pdbPath = pdbPath; + result.pdbName = FilenameUtils.getName(result.pdbPath); + result.age = age; + result.guid = guid; + result.magic = "????".getBytes(); + + return result; + } + + + private byte[] magic; + private GUID guid; + private int age; + private String pdbName; + private String pdbPath; + + private PdbInfoDotNet() { + // empty + } + + @Override + public boolean isValid() { + return magic.length == 4 && !pdbName.isBlank() && guid != null; + } + + @Override + public void serializeToOptions(Options options) { + options.setString(PdbParserConstants.PDB_VERSION, + new String(magic, StandardCharsets.US_ASCII)); + options.setString(PdbParserConstants.PDB_GUID, guid.toString()); + options.setString(PdbParserConstants.PDB_AGE, Integer.toHexString(age)); + options.setString(PdbParserConstants.PDB_FILE, pdbName); + } + + @Override + public DataType toDataType() { + StructureDataType struct = new StructureDataType("DotNetPdbInfo", 0); + struct.add(new StringDataType(), magic.length, "signature", null); + struct.add(new GuidDataType(), "guid", null); + struct.add(new DWordDataType(), "age", null); + if (pdbName.length() > 0) { + struct.add(new StringDataType(), pdbName.length(), "pdbname", null); + } + struct.setCategoryPath(new CategoryPath("/PDB")); + return struct; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoDotNetIface.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoDotNetIface.java deleted file mode 100644 index 381668ed56..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoDotNetIface.java +++ /dev/null @@ -1,33 +0,0 @@ -/* ### - * IP: GHIDRA - * REVIEWED: YES - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.util.bin.format.pdb; - -import ghidra.app.util.bin.*; -import ghidra.app.util.datatype.microsoft.*; - -public interface PdbInfoDotNetIface extends StructConverter { - - public abstract String getPdbName(); - - public abstract int getAge(); - - public abstract int getSignature(); - - public abstract GUID getGUID(); - - public abstract byte[] getMagic(); -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoIface.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoIface.java deleted file mode 100644 index ebb4ffac61..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoIface.java +++ /dev/null @@ -1,32 +0,0 @@ -/* ### - * IP: GHIDRA - * REVIEWED: YES - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.util.bin.format.pdb; - -import ghidra.app.util.bin.*; - -public interface PdbInfoIface extends StructConverter { - - public abstract byte[] getMagic(); - - public abstract int getOffset(); - - public abstract int getSig(); - - public abstract int getAge(); - - public abstract String getPdbName(); -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/DebugDataDirectory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/DebugDataDirectory.java index 4e9168a266..27569734f1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/DebugDataDirectory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/DebugDataDirectory.java @@ -19,8 +19,8 @@ import java.io.IOException; import java.io.RandomAccessFile; import ghidra.app.util.bin.format.FactoryBundledWithBinaryReader; -import ghidra.app.util.bin.format.pdb.PdbInfoDotNetIface; -import ghidra.app.util.bin.format.pdb.PdbInfoIface; +import ghidra.app.util.bin.format.pdb.PdbInfoCodeView; +import ghidra.app.util.bin.format.pdb.PdbInfoDotNet; import ghidra.app.util.bin.format.pe.debug.*; import ghidra.app.util.importer.MessageLog; import ghidra.program.model.address.Address; @@ -112,12 +112,12 @@ public class DebugDataDirectory extends DataDirectory { if (dcv != null) { Address dataAddr = getDataAddress(dcv.getDebugDirectory(), isBinary, space, ntHeader); if (dataAddr != null) { - PdbInfoIface pdbInfo = dcv.getPdbInfo(); + PdbInfoCodeView pdbInfo = dcv.getPdbInfo(); if (pdbInfo != null) { setPlateComment(program, dataAddr, "CodeView PDB Info"); PeUtils.createData(program, dataAddr, pdbInfo.toDataType(), log); } - PdbInfoDotNetIface dotNetPdbInfo = dcv.getDotNetPdbInfo(); + PdbInfoDotNet dotNetPdbInfo = dcv.getDotNetPdbInfo(); if (dotNetPdbInfo != null) { setPlateComment(program, dataAddr, ".NET PDB Info"); PeUtils.createData(program, dataAddr, dotNetPdbInfo.toDataType(), log); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/debug/DebugCodeView.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/debug/DebugCodeView.java index e61a0522a1..1c9154eca1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/debug/DebugCodeView.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/pe/debug/DebugCodeView.java @@ -15,30 +15,25 @@ */ package ghidra.app.util.bin.format.pe.debug; +import java.io.IOException; + import ghidra.app.util.bin.StructConverter; import ghidra.app.util.bin.format.FactoryBundledWithBinaryReader; -import ghidra.app.util.bin.format.pdb.PdbFactory; -import ghidra.app.util.bin.format.pdb.PdbInfoDotNetIface; -import ghidra.app.util.bin.format.pdb.PdbInfoIface; +import ghidra.app.util.bin.format.pdb.PdbInfoCodeView; +import ghidra.app.util.bin.format.pdb.PdbInfoDotNet; import ghidra.app.util.bin.format.pe.OffsetValidator; -import ghidra.program.model.data.ArrayDataType; -import ghidra.program.model.data.CategoryPath; -import ghidra.program.model.data.DataType; -import ghidra.program.model.data.Structure; -import ghidra.program.model.data.StructureDataType; +import ghidra.program.model.data.*; import ghidra.util.Msg; import ghidra.util.exception.DuplicateNameException; -import java.io.IOException; - /** * A class to represent the code view debug information. */ public class DebugCodeView implements StructConverter { private DebugDirectory debugDir; private DebugCodeViewSymbolTable symbolTable; - private PdbInfoIface pdbInfo; - private PdbInfoDotNetIface dotNetPdbInfo; + private PdbInfoCodeView pdbInfo; + private PdbInfoDotNet dotNetPdbInfo; /** * Constructor. @@ -70,8 +65,8 @@ public class DebugCodeView implements StructConverter { return; } - dotNetPdbInfo = PdbFactory.getPdbInfoDotNetInstance(reader, ptr); - pdbInfo = PdbFactory.getPdbInfoInstance(reader, ptr); + dotNetPdbInfo = PdbInfoDotNet.isMatch(reader, ptr) ? PdbInfoDotNet.read(reader, ptr) : null; + pdbInfo = PdbInfoCodeView.isMatch(reader, ptr) ? PdbInfoCodeView.read(reader, ptr) : null; if (DebugCodeViewSymbolTable.isMatch(reader, ptr)) { symbolTable = DebugCodeViewSymbolTable.createDebugCodeViewSymbolTable(reader, @@ -106,17 +101,18 @@ public class DebugCodeView implements StructConverter { * Returns the code view .PDB info. * @return the code view .PDB info */ - public PdbInfoIface getPdbInfo() { + public PdbInfoCodeView getPdbInfo() { return pdbInfo; } - public PdbInfoDotNetIface getDotNetPdbInfo() { + public PdbInfoDotNet getDotNetPdbInfo() { return dotNetPdbInfo; } /** * @see ghidra.app.util.bin.StructConverter#toDataType() */ + @Override public DataType toDataType() throws DuplicateNameException { Structure es = new StructureDataType("DebugCodeView", 0); es.add(WORD, "Signature", null); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/microsoft/GUID.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/microsoft/GUID.java index 0d7d073536..82424c5b49 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/microsoft/GUID.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/microsoft/GUID.java @@ -21,11 +21,7 @@ import java.util.Arrays; import ghidra.app.util.bin.BinaryReader; import ghidra.program.model.mem.MemBuffer; import ghidra.program.model.mem.MemoryAccessException; -import ghidra.util.BigEndianDataConverter; -import ghidra.util.Conv; -import ghidra.util.DataConverter; -import ghidra.util.LittleEndianDataConverter; -import ghidra.util.NumericUtilities; +import ghidra.util.*; /** * GUIDs identify objects such as interfaces, manager entry-point vectors (EPVs), @@ -61,54 +57,52 @@ public class GUID { /** * Creates a GUID object using the GUID string form. - * @param guidString - "6B29FC40-CA47-1067-B31D-00DD010662DA" + * @param guidString - either with or without dashes between parts - + * "6B29FC40-CA47-1067-B31D-00DD010662DA", or "6B29FC40CA471067B31D00DD010662DA", and + * with or without leading and trailing "{" "}" characters * @throws IllegalArgumentException if string does not represent a valid GUID */ - public GUID(String guidString) { - if (guidString.length() != 36) { - throw new IllegalArgumentException("Invalid GUID string."); - } - int pos = guidString.indexOf('-'); - if (pos == -1) { - throw new IllegalArgumentException("Invalid GUID string."); - } - data1 = (int) NumericUtilities.parseHexLong(guidString.substring(0, pos)); - - guidString = guidString.substring(pos + 1); - pos = guidString.indexOf('-'); - if (pos == -1) { - throw new IllegalArgumentException("Invalid GUID string."); - } - data2 = (short) Integer.parseInt(guidString.substring(0, pos), 16); - - guidString = guidString.substring(pos + 1); - pos = guidString.indexOf('-'); - if (pos == -1) { - throw new IllegalArgumentException("Invalid GUID string."); - } - data3 = (short) Integer.parseInt(guidString.substring(0, pos), 16); - - guidString = guidString.substring(pos + 1); - pos = guidString.indexOf('-'); - if (pos == -1) { - throw new IllegalArgumentException("Invalid GUID string."); - } - int value = Integer.parseInt(guidString.substring(0, pos), 16); + public GUID(String guidString) throws IllegalArgumentException { + String[] parts = getGUIDParts(guidString); + data1 = (int) NumericUtilities.parseHexLong(parts[0]); + data2 = (short) Integer.parseInt(parts[1], 16); + data3 = (short) Integer.parseInt(parts[2], 16); + int value = Integer.parseInt(parts[3], 16); data4[0] = (byte) (value >> 8); data4[1] = (byte) (value & 0xff); + data4[2] = (byte) Integer.parseInt(parts[4].substring(0, 2), 16); + data4[3] = (byte) Integer.parseInt(parts[4].substring(2, 4), 16); + data4[4] = (byte) Integer.parseInt(parts[4].substring(4, 6), 16); + data4[5] = (byte) Integer.parseInt(parts[4].substring(6, 8), 16); + data4[6] = (byte) Integer.parseInt(parts[4].substring(8, 10), 16); + data4[7] = (byte) Integer.parseInt(parts[4].substring(10, 12), 16); + } - guidString = guidString.substring(pos + 1); - data4[2] = (byte) Integer.parseInt(guidString.substring(0, 2), 16); - guidString = guidString.substring(2); - data4[3] = (byte) Integer.parseInt(guidString.substring(0, 2), 16); - guidString = guidString.substring(2); - data4[4] = (byte) Integer.parseInt(guidString.substring(0, 2), 16); - guidString = guidString.substring(2); - data4[5] = (byte) Integer.parseInt(guidString.substring(0, 2), 16); - guidString = guidString.substring(2); - data4[6] = (byte) Integer.parseInt(guidString.substring(0, 2), 16); - guidString = guidString.substring(2); - data4[7] = (byte) Integer.parseInt(guidString.substring(0, 2), 16); + private String[] getGUIDParts(String guidString) throws IllegalArgumentException { + String[] results = new String[5]; + guidString = (guidString.startsWith("{") && guidString.endsWith("}")) + ? guidString.substring(1, guidString.length() - 1) + : guidString; + if (guidString.length() == 36 && guidString.charAt(8) == '-' && + guidString.charAt(13) == '-' && guidString.charAt(18) == '-' && + guidString.charAt(23) == '-') { + results[0] = guidString.substring(0, 8); + results[1] = guidString.substring(9, 13); + results[2] = guidString.substring(14, 18); + results[3] = guidString.substring(19, 23); + results[4] = guidString.substring(24); + } + else if (guidString.length() == 32) { + results[0] = guidString.substring(0, 8); + results[1] = guidString.substring(8, 12); + results[2] = guidString.substring(12, 16); + results[3] = guidString.substring(16, 20); + results[4] = guidString.substring(20); + } + else { + throw new IllegalArgumentException("Invalid GUID string."); + } + return results; } /** @@ -172,23 +166,23 @@ public class GUID { @Override public String toString() { - StringBuffer buffer = new StringBuffer(); - buffer.append(Conv.toHexString(data1)); - buffer.append("-"); - buffer.append(Conv.toHexString(data2)); - buffer.append("-"); - buffer.append(Conv.toHexString(data3)); - buffer.append("-"); - buffer.append(Conv.toHexString(data4[0])); - buffer.append(Conv.toHexString(data4[1])); - buffer.append("-"); - buffer.append(Conv.toHexString(data4[2])); - buffer.append(Conv.toHexString(data4[3])); - buffer.append(Conv.toHexString(data4[4])); - buffer.append(Conv.toHexString(data4[5])); - buffer.append(Conv.toHexString(data4[6])); - buffer.append(Conv.toHexString(data4[7])); - return buffer.toString(); + StringBuilder sb = new StringBuilder(); + sb.append(Conv.toHexString(data1)); + sb.append("-"); + sb.append(Conv.toHexString(data2)); + sb.append("-"); + sb.append(Conv.toHexString(data3)); + sb.append("-"); + sb.append(Conv.toHexString(data4[0])); + sb.append(Conv.toHexString(data4[1])); + sb.append("-"); + sb.append(Conv.toHexString(data4[2])); + sb.append(Conv.toHexString(data4[3])); + sb.append(Conv.toHexString(data4[4])); + sb.append(Conv.toHexString(data4[5])); + sb.append(Conv.toHexString(data4[6])); + sb.append(Conv.toHexString(data4[7])); + return sb.toString(); } /** diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/AbstractPeDebugLoader.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/AbstractPeDebugLoader.java index b5ac32c59c..a0215524ea 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/AbstractPeDebugLoader.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/opinion/AbstractPeDebugLoader.java @@ -17,11 +17,11 @@ package ghidra.app.util.opinion; import java.util.*; -import ghidra.app.util.bin.format.pdb.*; +import ghidra.app.util.bin.format.pdb.PdbInfoCodeView; +import ghidra.app.util.bin.format.pdb.PdbInfoDotNet; import ghidra.app.util.bin.format.pe.FileHeader; import ghidra.app.util.bin.format.pe.SectionHeader; import ghidra.app.util.bin.format.pe.debug.*; -import ghidra.app.util.datatype.microsoft.GUID; import ghidra.app.util.demangler.DemangledObject; import ghidra.app.util.demangler.DemanglerUtil; import ghidra.framework.options.Options; @@ -112,56 +112,14 @@ abstract class AbstractPeDebugLoader extends AbstractLibrarySupportLoader { Options proplist = program.getOptions(Program.PROGRAM_INFO); - PdbInfoIface cvPdbInfo = dcv.getPdbInfo(); + PdbInfoCodeView cvPdbInfo = dcv.getPdbInfo(); if (cvPdbInfo != null) { - byte[] magic = cvPdbInfo.getMagic(); - int sig = cvPdbInfo.getSig(); - int age = cvPdbInfo.getAge(); - String name = cvPdbInfo.getPdbName(); - - proplist.setString(PdbParserConstants.PDB_VERSION, Conv.toString(magic)); - proplist.setString(PdbParserConstants.PDB_SIGNATURE, Conv.toHexString(sig)); - proplist.setString(PdbParserConstants.PDB_AGE, Integer.toHexString(age)); - proplist.setString(PdbParserConstants.PDB_FILE, name); -/* - DebugDirectory dd = dcv.getDebugDirectory(); - if (dd.getAddressOfRawData() > 0) { - Address address = space.getAddress(imageBase + dd.getAddressOfRawData()); - listing.setComment(address, CodeUnit.PLATE_COMMENT, "CodeView PDB Info"); - try { - listing.createData(address, cvPdbInfo.toDataType()); - } - catch (IOException e) {} - catch (DuplicateNameException e) {} - catch (CodeUnitInsertionException e) {} - } -*/ + cvPdbInfo.serializeToOptions(proplist); } - PdbInfoDotNetIface dotnetPdbInfo = dcv.getDotNetPdbInfo(); + PdbInfoDotNet dotnetPdbInfo = dcv.getDotNetPdbInfo(); if (dotnetPdbInfo != null) { - byte[] magic = dotnetPdbInfo.getMagic(); - GUID guid = dotnetPdbInfo.getGUID(); - int age = dotnetPdbInfo.getAge(); - String name = dotnetPdbInfo.getPdbName(); - - proplist.setString(PdbParserConstants.PDB_VERSION, Conv.toString(magic)); - proplist.setString(PdbParserConstants.PDB_GUID, guid.toString()); - proplist.setString(PdbParserConstants.PDB_AGE, Integer.toHexString(age)); - proplist.setString(PdbParserConstants.PDB_FILE, name); -/* - DebugDirectory dd = dcv.getDebugDirectory(); - if (dd.getAddressOfRawData() > 0) { - Address address = space.getAddress(imageBase + dd.getAddressOfRawData()); - listing.setComment(address, CodeUnit.PLATE_COMMENT, ".NET PDB Info"); - try { - listing.createData(address, dotnetPdbInfo.toDataType()); - } - catch (IOException e) {} - catch (DuplicateNameException e) {} - catch (CodeUnitInsertionException e) {} - } -*/ + dotnetPdbInfo.serializeToOptions(proplist); } DebugCodeViewSymbolTable dcvst = dcv.getSymbolTable(); diff --git a/Ghidra/Features/PDB/certification.manifest b/Ghidra/Features/PDB/certification.manifest index fa7172b4dc..dce9dc413e 100644 --- a/Ghidra/Features/PDB/certification.manifest +++ b/Ghidra/Features/PDB/certification.manifest @@ -1,4 +1,7 @@ ##VERSION: 2.0 +##MODULE IP: Crystal Clear Icons - LGPL 2.1 +##MODULE IP: FAMFAMFAM Icons - CC 2.5 +##MODULE IP: Nuvola Icons - LGPL 2.1 ##MODULE IP: Oxygen Icons - LGPL 3.0 Module.manifest||GHIDRA||||END| src/global/docs/README_PDB.html||GHIDRA||||END| @@ -13,13 +16,19 @@ src/main/help/help/shared/redo.png||GHIDRA||||END| src/main/help/help/shared/tip.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/help/help/shared/undo.png||GHIDRA||||END| src/main/help/help/shared/warning.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| +src/main/help/help/topics/Pdb/LoadPDBNew.html||GHIDRA||||END| src/main/help/help/topics/Pdb/PDB.htm||GHIDRA||||END| -src/main/help/help/topics/Pdb/download_pdb_file.html||GHIDRA||||END| -src/main/help/help/topics/Pdb/images/KnownSymbolServerURLsDialog.png||GHIDRA||||END| -src/main/help/help/topics/Pdb/images/PdbOrXmlDialog.png||GHIDRA||||END| -src/main/help/help/topics/Pdb/images/PeSpecifiedPathDialog.png||GHIDRA||||END| -src/main/help/help/topics/Pdb/images/SuccessDialog.png||GHIDRA||||END| -src/main/help/help/topics/Pdb/images/SymbolServerURLDialog.png||GHIDRA||||END| +src/main/help/help/topics/Pdb/images/LoadPdb_Advanced_NeedsConfig.png||GHIDRA||||END| +src/main/help/help/topics/Pdb/images/LoadPdb_Advanced_Screenshot.png||GHIDRA||||END| +src/main/help/help/topics/Pdb/images/LoadPdb_Initial_Screenshot.png||GHIDRA||||END| +src/main/help/help/topics/Pdb/images/Plus2.png||GHIDRA||||END| +src/main/help/help/topics/Pdb/images/SymbolServerConfig_AddButtonMenu.png||GHIDRA||||END| +src/main/help/help/topics/Pdb/images/SymbolServerConfig_Screenshot.png||GHIDRA||||END| +src/main/help/help/topics/Pdb/images/disk.png||FAMFAMFAM Icons - CC 2.5||||END| +src/main/help/help/topics/Pdb/images/down.png||GHIDRA||||END| +src/main/help/help/topics/Pdb/images/error.png||Nuvola Icons - LGPL 2.1||||END| +src/main/help/help/topics/Pdb/images/reload3.png||Crystal Clear Icons - LGPL 2.1||||END| +src/main/help/help/topics/Pdb/images/up.png||GHIDRA||||END| src/pdb/README.txt||GHIDRA||||END| src/pdb/pdb.sln||GHIDRA||||END| src/pdb/pdb.vcxproj||GHIDRA||||END| diff --git a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/GhidraPdbFactory.java b/Ghidra/Features/PDB/developer_scripts/PdbExamplePrescript.java similarity index 50% rename from Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/GhidraPdbFactory.java rename to Ghidra/Features/PDB/developer_scripts/PdbExamplePrescript.java index 66f34b5bbe..66f1a8dadf 100644 --- a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/GhidraPdbFactory.java +++ b/Ghidra/Features/PDB/developer_scripts/PdbExamplePrescript.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,29 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package ghidra.app.util.bin.format.pdb; +//Example preScript to force a PdbAnalyzer to use a custom PDB +//symbol file when analyzing a binary. +//@category PDB +import java.io.File; -import ghidra.app.util.bin.*; +import ghidra.app.plugin.core.analysis.PdbAnalyzer; +import ghidra.app.script.GhidraScript; -import java.io.*; - -public class GhidraPdbFactory extends PdbFactory { +public class PdbExamplePrescript extends GhidraScript { @Override - protected PdbInfoDotNetIface doGetPdbInfoDotNetInstance( - BinaryReader reader, int ptr) throws IOException { - if (PdbInfoDotNet.isMatch(reader, ptr)) { - return new PdbInfoDotNet(reader, ptr); - } - return null; - } + protected void run() throws Exception { + // contrived example of choosing a pdb file with custom logic + File pdbFile = new File(getProgramFile().getPath() + ".pdb"); - @Override - protected PdbInfoIface doGetPdbInfoInstance(BinaryReader reader, int ptr) - throws IOException { - if (PdbInfo.isMatch(reader, ptr)) { - return new PdbInfo(reader, ptr); - } - return null; + PdbAnalyzer.setPdbFileOption(currentProgram, pdbFile); + // or + //PdbUniversalAnalyzer.setPdbFileOption(currentProgram, pdbFile); } } diff --git a/Ghidra/Features/PDB/developer_scripts/PdbSymbolServerExamplePrescript.java b/Ghidra/Features/PDB/developer_scripts/PdbSymbolServerExamplePrescript.java new file mode 100644 index 0000000000..f8b29e9d89 --- /dev/null +++ b/Ghidra/Features/PDB/developer_scripts/PdbSymbolServerExamplePrescript.java @@ -0,0 +1,53 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +//Example preScript to configure the PDB symbol server service to use the ~/symbols directory +//as the location to store symbol files, and to search Microsoft's public +//symbol server. +//The ~/symbols directory should already exist and be initialized as a symbol +//storage location. +//@category PDB +import java.util.List; + +import java.io.File; +import java.net.URI; + +import ghidra.app.plugin.core.analysis.PdbAnalyzer; +import ghidra.app.plugin.core.analysis.PdbUniversalAnalyzer; +import ghidra.app.script.GhidraScript; +import pdb.PdbPlugin; +import pdb.symbolserver.*; + +public class PdbSymbolServerExamplePrescript extends GhidraScript { + + @Override + protected void run() throws Exception { + File homeDir = new File(System.getProperty("user.home")); + File symDir = new File(homeDir, "symbols"); + LocalSymbolStore localSymbolStore = new LocalSymbolStore(symDir); + HttpSymbolServer msSymbolServer = + new HttpSymbolServer(URI.create("https://msdl.microsoft.com/download/symbols/")); + SymbolServerService symbolServerService = + new SymbolServerService(localSymbolStore, List.of(msSymbolServer)); + + PdbPlugin.saveSymbolServerServiceConfig(symbolServerService); + + // You only need to enable the "allow remote" option on the specific + // analyzer you are using + PdbUniversalAnalyzer.setAllowRemoteOption(currentProgram, true); + PdbAnalyzer.setAllowRemoteOption(currentProgram, true); + } +} + diff --git a/Ghidra/Features/PDB/src/global/docs/README_PDB.html b/Ghidra/Features/PDB/src/global/docs/README_PDB.html index 3b39a8ef11..decf438fcd 100644 --- a/Ghidra/Features/PDB/src/global/docs/README_PDB.html +++ b/Ghidra/Features/PDB/src/global/docs/README_PDB.html @@ -50,7 +50,7 @@ native execution issue and the use of an intermediate XML format.

Although GHIDRA has been primarily designed to utilize locally stored PDB files during analysis, the ability to interactively download individual PDB files from a web-based Microsoft Symbol Server is also provided. This capability is accessed via the GUI while a program is open via the -File->Download PDB File... action. +File → Load PDB File... action.

DIA SDK Dependency

diff --git a/Ghidra/Features/PDB/src/main/help/help/TOC_Source.xml b/Ghidra/Features/PDB/src/main/help/help/TOC_Source.xml index 73d8d8d971..16c4f1abb5 100644 --- a/Ghidra/Features/PDB/src/main/help/help/TOC_Source.xml +++ b/Ghidra/Features/PDB/src/main/help/help/TOC_Source.xml @@ -51,8 +51,8 @@ - - + + diff --git a/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/LoadPDBNew.html b/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/LoadPDBNew.html new file mode 100644 index 0000000000..eca6a8b6de --- /dev/null +++ b/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/LoadPDBNew.html @@ -0,0 +1,226 @@ + + + + + + Load PDB + + + + + +

Load PDB

+ + +

Symbol Servers and Symbol Storage

+ +
+

In an effort to manage large collections of symbol files, Microsoft specified a scheme to organize + symbol files into directory structures.

+ +

Ghidra can search Microsoft-style symbol servers (web-based HTTP/HTTPS) or local file system symbol + storage directories as well as unorganized, non-MS symbol storage directories for the PE executable's + matching PDB symbol file.

+ + +
+ +

Menu Actions

+ +
+

Load PDB File

+
+

Allows the user to pick a PDB file or search for a PDB file and apply it to the currently open program in the CodeBrowser.

+

Use this action instead of the PDB Analyzer if the PDB file can't be found automatically with the currently configured + symbol server search locations, if you need to force load a non-exact PDB file, or you need to use other PDB options.

+

Steps:

+ +
+
+ +
+

Symbol Server Config

+
+

Allows the user to configure the location where PDB symbol files are stored and additional locations to search for + existing PDB files. This is also available in the Load PDB File, Advanced screen.

+

Steps:

+ + +

 (Add)

+
+

Allows the user to add a location to the search path list. Pick from the offered types of locations, or pick + a predefined location.

+
+
    +
  • Directory - allows the user to pick an existing directory that will be searched for symbol files. + See level 1/level 2 or + unorganized directory descriptions.
  • +
  • URL - allows the user to enter a HTTP or HTTPS URL to a web-based symbol server.
  • +
  • Program's Import Location - automatically references the directory from which the program was imported.
  • +
  • Import _NT_SYMBOL_PATH - parses the current value of the _NT_SYMBOL_PATH system environment variable to extract + URLs and symbol directory locations to be added to the Ghidra configuration. If no environment value is present, + the user can paste their own value into the text field.
  • +
+

All items listed after the menu dividing line are automatically added from resource files that have a + *.pdburl extension. The default file included with Ghidra is called PDB_SYMBOL_SERVER_URLS.pdburl and + is located in the Ghidra/Configurations/Public_Release/data directory under the Ghidra install directory.

+
+ +

 (Delete)

+
+

Deletes the currently selected locations from the Additional Search Paths table.

+
+ +

 (Up/Down)

+
+

Moves the currently selected item up or down in the Additional Search Paths table.

+
+ +

 (Refresh)

+
+

Updates the status column of the locations listed in the Additional Search Paths + table. Symbol servers or storage locations that are unreachable or misconfigured will show an error status in that column.

+
+ +

 (Save)

+
+

Saves the currently displayed search and storage locations to the preferences file. This is shared between all Ghidra tools.

+
+
+
+ +
+

PDB Search - Search Options

+
+

These options control how PDB symbol files are found.

+ +

Additionally, there are override checkboxes in the Program PDB Information panel in the Advanced screen. These override values only + change the search criteria, they are not persisted to your program's metadata.

+ +

After changing a search option, you will need to perform another search to use the new options.

+
+
+ +
+

PDB Parser

+
+

These options control which PDB parser will be used and any options used during parsing after the Load button is pressed.

+ +
+
+ +

Troubleshooting

+
+ +
+ +


+ +

Related Topics:

+ + +


+ + + diff --git a/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/PDB.htm b/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/PDB.htm index 388ef33464..c349ce849f 100644 --- a/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/PDB.htm +++ b/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/PDB.htm @@ -1,36 +1,130 @@ - + - Searching - + Microsoft Program Databases (PDB) - -

PDB

+

Using Microsoft Program Database (PDB) Files

-

Ghidra offers the ability to download and apply PDB debug information for programs that run - on Microsoft Windows operating systems. - The Download PDB File feature allows users to - download and optionally load/apply a PDB file that matches the user's current program, given an - accessible Symbol Server. - The Load PDB File feature - allows users to apply a local PDB file to the current program. The PDB Analyzer also - automatically applies PDB symbols (attempting a search for matching PDB files locally) during - Auto-Analysis.

+

A program database (PDB) file holds debugging and project state information about a program + and can be created in a number of ways. Historically, it has been created using a Microsoft + compiler and written in C/C++, C#, and Visual Basic. + A user generates a PDB file using the /ZI or /Zi flag (for C/C++ programs) or the + /debug flag (for Visual Basic/C# programs).

+

There are two mechanisms for processing a PDB file. First, the platform-independent + PDB Universal Reader/Analyzer, which can read a raw PDB file and apply it. Its capabilities + are expected to be expanded in future releases. Second, the legacy capability that uses the + DIA SDK to read information from the PDB file. This mechanism can only run + on a Windows platform, however it creates an XML representation of information gleaned using + the DIA SDK. These XML files can be saved and then used on Windows and non-Windows platforms + hosting Ghidra.

+ +

If loading a PDB, this should be done prior to other analysis, except in special cases, + such as when only loading data types.

+ +

Restricted loading of data types or public symbols is + supported by PDB Universal.

+ +

To Load a PDB

+ +
+

PDB files can be loaded in two ways:

+ +
+ +

Information Loaded From PDB

+ +
+
    +
  1. Structure and union definitions
  2. + +
  3. Typedefs
  4. + +
  5. Enumerations
  6. + +
  7. Class definitions
  8. + +
  9. Function prototypes
  10. + +
  11. Stack variable names and data types
  12. + +
  13. Source line numbers
  14. + +
  15. Instruction and data symbols
  16. +
+
+ +

The DIA SDK-Based Capability

+ +

*.PDB.XML files can be created in three different ways: + +

+

+

NOTE: Execution of pdb.exe has runtime dependencies which must be satisfied. + Please refer to the README_PDB document for details.

+ +

Debug Interface Access SDK

+ +
+

The Microsoft Debug Interface Access Software Development Kit (DIA SDK) provides access to + debug information stored in program database (.PDB) files generated by Microsoft + post-compiler tools. Because the format of the .PDB file generated by the post-compiler tools + undergoes constant revision, exposing the format is impractical. Using the DIA API, you can + develop applications that search for and browse debug information stored in a .PDB file. Such + applications could, for example, report stack trace-back information and analyze performance + data.

+ +

If you are attempting to load a PDB on a + Windows machine and see an error message such as "Unable to locate the DIA SDK," + you will need to add and register one or more files on your computer. Refer to the + README_PDB document for detailed instructions. +

+

Related Topics:

diff --git a/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/download_pdb_file.html b/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/download_pdb_file.html deleted file mode 100644 index a161cc6e43..0000000000 --- a/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/download_pdb_file.html +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - Download PDB File - - - - -

Download PDB File

- -

Ghidra offers the ability to download and apply a PDB file that corresponds to the program - currently open in the CodeBrowser. Successful downloading requires, at a minimum, that:

- -
-
    -
  1. A Symbol Server URL is available and accessible from the client or computer where you are running Ghidra.
  2. -
  3. The program open in the CodeBrowser is a PE file that was compiled by a Microsoft compiler.
  4. -
-
- -

A Note for Windows Users

-
-

If set, Ghidra parses the _NT_SYMBOL_PATH - environment variable that is used to specify a PDB download location and Symbol Server URL(s). - The syntax for _NT_SYMBOL_PATH is shown below:

- -

srv*[local symbols location]*[Symbol Server URL]

- -

The _NT_SYMBOL_PATH symbols location is used to pre-populate the dialog that asks - for the local storage location (as long as that location is valid). The _NT_SYMBOL_PATH - Symbol Server URL is used to pre-populate the dialog that asks for the Symbol Server location.

- -

Although multiple Symbol Server URLs can be - specified in the _NT_SYMBOL_PATH variable, Ghidra only uses the first listed URL.

-
- -

To Download a PDB

- -
- -
    -
  1. From the menu-bar of a tool, select File Download PDB File
  2. - -
    - -
  3. A dialog appears asking whether you want to download a PDB or XML (PDB.XML) file. Select - the type of file you want to download and click OK.
  4. - -

    - -

    A Symbol Server should always have PDB - files available for download. In contrast, .PDB.XML files are Ghidra-created files, and are - only available to download from the Symbol Server if Ghidra tools have been used to create - them and the server's admin has made them available. If you choose to download a .PDB.XML - file and it is not found on the server, you will see a dialog message telling you so. For - more information on creating and using .PDB.XML files, see the - Load PDB File section.

    - -
    - -
  5. Before attempting to download the file, an attempt will first be made to locate it - using file and path names associated with the program. A dialog appears asking whether you - want to include the PE-Header-Specified Path, which could include a Universal Naming - Convention (UNC) path of a location that might not be trusted. Select OK if you want to - perform this potentially unsafe retrieval.
  6. - -

    - -
    - -
  7. A dialog appears asking where to save the downloaded file. Pick a location to store your - PDB files. A common location on Windows is C:\Symbols.
  8. - -
    - -
  9. At this point, if a PDB file of the type you have chosen (either .PDB or .PDB.XML) already - exists in the selected location, you will see a message indicating that a potential matching - PDB has been found. You will then be asked if you would like to continue with the download. -

    -
      -
    • If you select "No", jump to Step 7.
    • -
      -
    • If you select "Yes", please keep the following things in mind relating to a found - .PDB or .PDB.XML file:
    • -
      -
        -
      • If the found file is not in a directory that contains the current binary's GUID - (i.e., C:\Symbols\<pdbfilename>\<GUID>), then the file is not - guaranteed to be an exact match for the current binary (when there is no GUID subfolder, - a matching file is found based on expected PDB filename).
      • - -
        - -
      • If there is any doubt about whether the found PDB file matches, it is a good - idea to try to download the matching file, anyway (the matching file will be saved - in a directory of the form <download location>/<pdbfilename>/ - <GUID>).
      • - -
        - -
      • If you do choose to continue to apply the found PDB file, and its GUID does not - match the GUID of the current binary, you will be warned and given the option of - canceling the application of the PDB file.
      • -
      -
    - -
    - -
  10. Next, you will see a dialog asking for the Symbol Server URL. - -

    - -

    - -
    - - If a list of known URLs exists in your distribution (the file will have the extension - .pdburl), the dialog will also include a button with the text "Choose from - known URLs". When this button is pressed, a separate dialog appears showing known Symbol - Server URLs. - -
    - -

    - -
    - - You may choose any of these URLs or manually type one in. If manually typing in a URL, - be sure to include the protocol (http or https). - -
    - -

    Always be sure to check your organization's - security policy before downloading any file from the internet.

    -
  11. - -
    - -
  12. Next, if the Symbol Server contains a matching PDB that is the same file type that you - chose earlier, it will return with a message indicating that the download was successful. - The message also contains the path where you can find the downloaded file.
  13. - -

    -
    - -
  14. If the download was successful or an existing PDB file was found, you may be asked - whether you want to apply the PDB to the program.
  15. - -

    If Yes is chosen, see - Load PDB File for continued help.

    - -
-
- -

-

Troubleshooting

-
- -
- - -


- -

Related Topics:

- - -


- - - diff --git a/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/KnownSymbolServerURLsDialog.png b/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/KnownSymbolServerURLsDialog.png deleted file mode 100644 index a9ec4e4bc6f0b9ba44af6d92a27468f02f22ab84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14100 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 MwDR-fXU0MQ1}Xn$XaE2J literal 0 HcmV?d00001 diff --git a/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/LoadPdb_Advanced_Screenshot.png b/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/LoadPdb_Advanced_Screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..38da23a6abdf255acc278016d8d589cae69d14e1 GIT binary patch literal 81637 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 literal 0 HcmV?d00001 diff --git a/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/LoadPdb_Initial_Screenshot.png b/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/LoadPdb_Initial_Screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..5a980f17a341fa87c774407bc53bbb1a0ae60369 GIT binary patch literal 31547 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 ^}@i^xgSJ6j~eX8qjQ6R}jz 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 R;NzE9U&LfYONF(4{}+{csf7Rl diff --git a/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/Plus2.png b/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/Plus2.png new file mode 100644 index 0000000000000000000000000000000000000000..add4ad53dd68e79d7e2cdf13644dfbb51373fbfd GIT binary patch literal 752 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0WW zg+Z8+Vb&Z8pn}NEkcg59UmvUF{9L`nl>DSry^7odplSvNn+hu+GdHy)QK2F?C$HG5 z!d3~a!V1U+3F|8Y3;nDA{o-C@9zzrKDK}xwt{K19`Se z86_nJR{Hwo<>h+i#(Mch>H3D2mX`VkM*2oZx=P7{9O-#x!EwNQn0$BtH5O0c3d|4@L;p!@;Rg)2@GTdPZ!4!3&E+A zeY2Px1zM{oP0UewapEFGp zR<64?<)7E&RV4vG?lc~sWw-Yo`nl|p=ynpGwnJ1OUkg4X=^jDa0{yb;2R#Ix=#Vb1YE4p%@$YnD(`5@fvSAQl9$x#{5m<7cK`TIB(gsdD`!+KKY-L;fpquiZ2hg5jShHUmR7~ zy7q$j{FTyAPm8=iwc-7Oo{I0M=4qwJ8oU2yKBYQy(%;q3=008-r1NIho?O#i|M&kt ZC;lMYt>DW!r{PyfPf%MN{A?dfPfJJ?^9sFfLEo{0Z$MR&JjrwK^1qs(@ba` zbn)jQ$qlkjBf;iXN4!%GE|@!1T6D#lpZ$8q>a;{SZAMY{GPEWA8{tMaOw<%?HZVd8 zFcU~v1MXDGetA$RZ&T~tt!L{5ZZJO{y!o#K*3He$vNM-6H^7n5)d!1%PkGIzsjjkkN4Oa|IH?1SWormcz2x-{Sb> zJh;YbiO>D|M+*~-wwoF&W!<^Q);hD=s8VC)4RmNTl zLz~@oQWloet!UXzo2~Gr`d0(1!95g0hAOq}ViU779iIAgogld+r?C8ma!q+h%{sHW z81p|NObBL+f8HkLCSu5o;Mm)7F#T2zPUg@03mqph1gUXQFcV~$jN}4hc`7!mbCTo* ze=R`LeFeqPdi4LqCJ12|k4mrKi6G?oaIsxyK9$AkL?H(LAs<6D5tJgT?WH|5A~IL# zlguf1j_f;!=ottNjvY|3C5TVKma%S;k&(Mw3t4hJJlY{Ta`KO6g9p>5afhj?tAFp# zm|Y?mQ|XB)_z4X*k}V9$2;%;560YKJcy-?_C9-aZs_(qy!h9+II|Dn?iwL81$TWgN z?6N?MRemvFm_&hY9qEb3Vx7z=3p*4;7}f(f3>$~RXAmrlBA8MX{7qcgpjv2t;4m?4 z4px>!wVbIMmIP*w=p5_t@aGV#!?TrNq=PW5_WsQgk7?a>S;TON&-#&kb4C`@7!_>s z5X6F8{W~q+S5>U4V0@_km!pEh!k$g|utg3|w&zQuS_hUu6;GL2kq7eXpr=VJ#h(}z zdJs{A-4x+OGRnPY>uve@`6EjqYlNiz7K8tpJ8|0GOL~BK$jF6r5*{Zi;A5ds9zznc zvN)=ndt)6;U^_RJh+_2LgZE_X_zpQdNNdE;5$ylu2p)EwZb?h}pI&>lxkC;V7sUSV z!!_roOvmHNPAp1d{CazJ(pm8xbO~hVx^I)!ZidE0MqcApfByU2W*9qiN_=8kStbT8 zcXDLOB3sG{j85iy@nueR1(z`xrrgPMeIfUf&e6S2yFp-3?Ot57vMBmiNUhDy7*#F6Fb5

    U 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%0Aom_M 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 diff --git a/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/SymbolServerConfig_AddButtonMenu.png b/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/SymbolServerConfig_AddButtonMenu.png new file mode 100644 index 0000000000000000000000000000000000000000..c3295e7c607134c09263bfd189ebe87e14fc2219 GIT binary patch literal 10777 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 literal 0 HcmV?d00001 diff --git a/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/SymbolServerURLDialog.png b/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/SymbolServerURLDialog.png deleted file mode 100644 index 303cdeb4c861a606caf83fcf76e5b126098be11f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11146 zcmcI~byQUEzpjdcf)dg#AT1!$E#1h_jevAZ42{wqQX(ZocbAkjlG0s64?~SG#1MDm z@AqBjoO|#2xyOyrS?0NUz??Lt2<6H($rQO|)UlFWxyuFA@G=9HrzrLj!^&x0Mwa zs*hVksa%^;LWk`1GT?*7Cco>;sS<6+r5|pG%dIP6z}iW2I-Bvnz1-B2K_+bs+0M;i zMVA{khsQ&KWB{ou1zm6etK{r&v(8kJ!(rC-k29UWnx zv5nop!W$c216_3Gr2lNd2c0@yAbQk8-BobN_`FVnq;2cz&5aVz3Zp8tqRElJ{5(sD ze&Gp;hm@-Z)Yky;Jx zI7^Z0N;Sjcb9-Pf6l~v&q*57X-%n1UVqJ&`D^vi^VK~jePIP?m+4!yVc6m=mGXYqQ zNJ9`TokM%k(=))w?6TJRsJj_(Qg?TEP*9M^zs@3oalvce*6KubJhB<2pYSk?VcX|h zH!Bv)6H%*f7S}jONhuEFgDvY;VG)sQ6n3hhGd~MeG%Dj)fAZP*T-&>lHXi?q7R|_9 z$9gZX+GrJyhGzKCS3fF#+);7-0_@$rP=&*yJr{OEwf#Q^^y#xZyJNJpB8oP@RAOz` z)-L8|oKx7%l~2#qDAK!g6g|E`gF(Kmw2)#`SNX`uNFqWn9_?Q~8A*BE7tEr6^lRnX zlv8UgM?->xX*R-t7GOr}1Y;xY2b5yJ?RoUee-ItzryeCS4`)^O`+Fg5$9z!m);_kE zDu>9PI8wd%rW=?TJV8!;@L&mE&qN%8wRTv_A z3?pT|Jh*Y*HGiMqZc-B~#@c?@U=2UMsA^nX#8SWd=Ij1SytdD`rlcvfZ*~*K;~*`i zp&7+)b1b>SN3gO$m|9`B-5&Yo!h}$;zk%81WM{0uU+!2S>m0iRx8iCDr{uRdaGYrj z!KO?E)3A@FX2ie0O9M&SM;+4YH8)p?_-MV_+A!e%Vd=fI8-!NESid8{$RniB%}zP( z+1;73QXt(n2RZxkOwnhFP;{~7cF)%~N%KrRA%)SxBKuEUaF_Q(Ha+1@W%HUtguRLB z1{^-A92qFJO(fD6+E_i+5jI$NRx@j#nxiY%P4L(Z^|AC79`VRwGBu8ZHn;xc-)1M8HA8J_ zif_o{AC(F5MWnhAk%Mkxt3~RJ052Jdud!*UZ;~L=q7_#s7qH8|3zIN(kf(-#W}t52 zGd|;){Uyn1o$M&&&rRN8_A-Z+{Q0iBttd(KrR4x}n9IAV`J!~ijsFdF&xEq&wVdnx zMWb2T;*ws5f@sA@DS=;j%~hHS$s#lM3;2Wys$OsTU%I-3vszqTHW~_&l@3Cmbv=L6 zP1Drue(rO2gU@KzZ9>go!Vf<=r{jm8hK|6hvj=XHgjoDFb`}SeooVOhNE7t9FSe^Z z#xgx`;BU3h+72Bj8VT`0nw$gE5-GEcEBR6ERqxuANexZ2e8_2w(Z`?D5sy;r?ZX(J zW>vuqWGpN!psK_TG+9hHze4>)yQVgW8xKbd{E3&Br|hreNw_md`J*o*;;bfXs>l19 z8p{MeqMxqP2a5PI=2@DDBbkRqP(yPpyjSlBIjj0kHr8h;FZ)95U0t4?GcZdElB_MP zd-tCBx1M}5K+`&W+!~kB>SwxOvCoh-_-W|yc})2Io_^cJ(1*}USr!}=6L$01TZ(#i z@?s!=s`14FxxUSHHRz%ou2E;yM+cwo8F0@aLq}g)F9KDpn8e&Vexks!XJteBt}Mk+ z2&f(Nd5+y>i55yo>q^<^Z$X-pvp_V}WO1Z!!C-O?kIi0e<9{qTryljTLWp8PCRlFtc3_qkb=J>>$44~K`8 z#+@a9rPA4&%O+)##EkHZr|;IfD`O1n)Y>m3>*jcy7IL)7XVnVaqR8V?SY3Q~w=l zxEOE2&Td=pA5+l*3b&G17CqR8RwLt}Zg1-{Q&R<(3yy8feb1nul^2v0M~5`(3(RVf z>Y~IMV)M>waLHGY+tdoa#>Vqd#@wWR!?nGA*6oT}dbWIqkhObSluWfggm1n|R0-Anss&@9!QWCR_UbDCzBNyL!^U{#m4<}9C1XI)3St>5BR@6jkh4Y0u8&Yyf zJPh=`S&FaPaoE(JujkOw<&2~!xv412ck2JnCAjE+cS1nk+mqgbTIS+(Mi5F#aZD$e zQsa$r$wi4RPjvhm8*`q7>?7Qyq)tpp^KkJ*_`;unnj7rqnr^MW_&meIYqg51NGyZij4n3b6s)ZO;{FNSqXiu4Ws`*DOR{0?irdHnC~pQ~bl z-Tg0KBmeGx*eiOsA@Jv{Eb#RYxFo2*H-BJ84P1^FfB%1ME-o5uUo-($(T(&CR9YjN z6A5y2-A!q3#y>2JxeN>r4$c!KZc$r1Rk(RBqhSbKt_uW@deCRI?LEiB!csit@Mwiv z`sH$vpbF6^jVdzPk%R;U8=IS(TU+^gc|G{MPx+oY zkS_{125*3;5?%xM%ppT50C`?w8RhF?S5Q!3`QgK2lZ&h)0E-uGM$v6tR|NEVyqM|myf`E^YqN+G}cOUf5qMeRAP~s;Sn&q?*Kiu2vk_vtSe zb)J8=@EKLEyweJTvAhr)@}4CQnU^F^S3=+e3Sp1e2gQLHI0M?+YUVhS`nSa*#RGcn ze%HGb1$jLuCns)u)8UuNZ2Cm3k1#M+8l5F!f$ocY}H)#VVYB# zWSmI6cq5PASzTp^Nkq{H79335fNudny?Cd1twPSKD zv$;DY&8pu~2gW!wTQW3v!g0DZwYk|lmP+Kesf`WG@SFJ+??Vym1eVVon?Oit&>}C+ z&sVYWPk;Yb6^<=XPGca#p%#U&uBsM_G+QOc$7k;^j00RTO$c1xqKP%fpCpK`=}`dP z-4(DLNjGZsa)V9ExBd9S;!QGtsl5-!B2KU)_|Mbk^`_+8xis(sQz; z7WgP-LoMplSo|sQHcblDQ9r($FzzutdH(bjQu!PA6UG-XF3XWp-5Q4H;W`w24ipDH68m$~ zIe>%PPzTM3>4Ai3^&+#lmoHzgMzI?-zA!$n1zwj7rF{X?)at2noGb)EV!*s|Ftz4a zNO>Twao0D>Z2ksa%j#^X@{yEIjYYIi@Ru+tm#rc3o1^37@aSm5{fmo>>&w#;?TXC0 zx>J3}R!tpb4S0S&Cnrb0!NK0dHGatkvV9``YAtc<0lge|<5&O`OYlqmYXt!A)CQD&A6f_@5ygc1opNmgOsPX(`@e~S$BBr#BlNLh1 z5TFj0TO$Rj+WHty|rzEgZ>KZV8Mf;7{qnx5~PNW;afz=CC)SA8E z6CQPLXFg`sBgI-}dSW-{bTIq*+K$xKw>?>y-RjiZ?Uk>Zl`@4%*X~(*T%9{t?O$E| zX>Dy~V`Jld2XTf?7Bx!04hRfHL*AS@Bzhs?P47D%fW*8`nCd0oMWtqB7`EkvIM-65 z9WW~0^MWP|I<3BK0wq$DP8WjA=Bmw$qkS(=NQK->YxzyPA8#y4Y_6?E;kr9B@;EH= zi;DW4!loLZq^GC1T_F_5NJBYII)i}l)zj5AA#`{%mYJ3oIb4?Lb#~x;fiN#>zdMdb zu-?d{u8}mp3jf<1E7|Movz3%f9Fo`X*E)lL*fDKsL(Jfk2#1;SjLC8~qzOkO#((lD zF+P4^O84ZX#swz5Fw?PIrwb+ywiI^|`4|-m2?;K4Tu~6FQ3~C>X@2m6UBv$QQ zYGvE0V!}+i$4)?HaCLFPkNyJK(QN1|r$+=0<;}L(Ei;JfX{NJ>Dl}W&z@XIL82Z(6 zG&9=w;_?zI5Yy+Nr8Qk?(sgvs-Vj@T6Xwg$@-(2Y#HRURJ4UuF%!>pg_T|Gg33H0> zPhnn!QTed&Dx8!Dc=ak7g5GBbN%K=?S-mA?&D-QChsDOvpMo>Py!tU1RnmW6hc@Ga zhGu8ax3m1&H7tm`Vd8=483Hco#}LU-Y}18kaz0ww?afUUYtYdb6KWw4HR*J||DB&hr@wO}>!j;nKhmvY_liB~R&7!5U}KUH;O%}Gei$g` zEIC(^B+Q9itENLhN0w`?6DG?!k0zFo^3X|RNM^IfyN5|uL z;o;%^@*b}gY=eu`3uv|FBl$^$gQZ&oe6tst58GJh8I`|ntgX@bfru?5Fv_j8gKs=u zuEo!{`Jw=1TvWg3cTk{`@rH{Fb?ATVeX;|3C=Ldn0X@mAJH!phIB+RYeQuI0iv0oWx8{|=CGsRLHiTxONoDWr540v+9zOyqJ z%h_70HNX*3Z1LiQ$)eJ{A7-VhJw8$CCF1J?x}>Uc<@@_vO0He(%bwemAZigWZU?gXWS~BV{7SxjpH#TLwPkt;NN)hp zFO~1yb)#`@Eir57t2yZI=rA z@IMGd=T^0GK>1`7&>U0K(#+TpL$Ik502hdP9IZ|iylZZ3)Oh_ZDkox{9#??}1; zB+$maW2)4+y{*vyxQAE{?LZ+Z+V`=~S2Tmfcski)e|DWh-k4r9ey7!YTg}!J`9|j0 z)S|EKA()3a575x?tl|K-3Qty>D{92)X_Xr=Ultyi0#z9znT;Th(=^MniIQ~a5<+t( z86SmZi1|}RHnvZp1`DZ@>RsZaf0t-;v>1y&0&n+5lVjBx2VuJJudaRqzaOXl$QJOu z&`iR4uBN7jGaC&9JMqp59vs25EVh#&|9If+aHXNPwtKG(XkNXQ0e#PmqyZbT&jWBE z0zfq!Hnzl(pFD#4rhz;GrHD4gO&6EEKx$T26omQih}lds39CA3RJ()xp-nk4ivbTq z93BVneCiJ2%)FBC=s;%=$h($B%a+PQ@ zp165c%jE0qq_^V49T#|x?a8W;Cs zuOi-6V}3-M_kje}%^YB7G`6*l}^1KX(yymQEVX;EYMh!M{P4nf^s6w zqn`Gm{R?+F_>mpOnk6<<#dS_=631*KP^f9kgY|uIcyeU`wukb;Lc?JzP>>g!CURtP zTK@Q7ombT(pKm(y5)cw*$7rMS6jLJZ$A7%KzV11r^}oFa+tJU{6Lz;@B2Qhba2Akz zGdRbRmqspmBdt=Y<3q#4&H?lTucLiUI7LKS1I9kY`70?a<2kDKnXEz|K6(_cSqw-F z1uv;Op!__&qS4y=;879eQk(C>(9qL&Pym5TY}Qb??b5YMu*GBHkymjD2nevUV$D!q z$h*GPlJvLM*DPMA5OjT9l=uDny+3^ZH&+Ns!Vl|K!su`LyI78gp+F+;&ez$FGXxK` zC&%~;Lph@87FR#ziK*>Cp#iV?0B}-N6H|rLFaoqer?LEZSw;;GhHy?Xv3BS6UZBXy zQkML1-3>^1F7~;A)7x5zC3>5d{C*eIqURao^&BN7p9YWpJon<pEnD{rPrP)Fs8Pa#1AslpUXi)bjg27U3v5b34P4X@{Cx0Qn5BRA5$CM6a5{N0j}`3o^3kQBpbLSpTX$Q zpKl)u)#ms|zkyDO|J&+>EMj^IqUDlgUEMES6>aOJ+0$_laRBxMcmeLGZbvsMS=q?; zowCaWlK>e1j#>8A(iX4%{QKeoutr>T&OQmQf?k6|bTiQmv5B0It!B0PAW=Hj!PBR~ ztJUuNde(&1`SFMp?OY;CGvfw_B?<}(r+ep&EWVwgd>ACgi@^Zc#iKqAzk$suE`Y3s ziBIu*W2s}k6$dJU1s$0>9?A0d>@LjDLq~eW(08?CHO;7htSKG4%jUXF3F-R?o{z~= zuWhlDpFa5)v`w64G-&q|;o?&FanFmqy7GySbG2?ScxfB7;)mJ?Dh@&`!+LQB>*xv@ z8IO;TpGQsYBRF4P&u%0I_qJ(jYJ#3o`Kf)GcU~7g_E&IMmS5J(WM3y>@t+aDuK4-b z^k(u_ZW%sT1Hef0=yxaUv0~6H3{zZeC7;X!Ozri7B-Uz~=vqomli+Q&ssV?v91%}M zZm*dK)jxF4`^T*SdHlx3&FI|IyEG=_wmRA^$Qsc8?a~GCZm%^`Gh+#f&i#dk&A{io z=zOs!&m@;Pks{)Hdhl4Y1*j z1_3-!-7!F5fh8y?$iN_jyq%>lCUGEHWxOpSoAe0+SYtgM`z zj*Mt2SG78x1ug)OzjVYF#xtYV6#D0xPdq-Zw0LsG%2_s=WYQiziSpIhFYFa1A|gtL z8Dxqe@ml5XK3aX`3Wm1sHnZOitTb)|W>|Yg$BBU}l!lj^>_+m(RNwwD@&HdTc=Q#p zrrP_BTf&Mt|EG$?|C5~Y|9f-i%3r-D%*Z}sR$JmukwO0{cSG~yPJC&N0Gt7=r~kd$ z@;`e$7Qze{u*v!1b;I?X^L1d%t&Yb#}A$;vV z{B~!)Lq$H{+yWEhGBY#dZt$Ech`t(WN1 zR;tONlE}&(%?s}Xf{D;M16*V?aEAnd4wb%mhnw5*I_<0a1vftFOThWTMp-L|?bmBR zmvAr+G?JI3(WHW6?M0;Kci`|{Y$m((r{WL&H{w>w6c;? zlrVo>1Ox&BdZnlBX^245JFa+fKtNNoYT7Ybv6oict}FNsN8_ZK)ERysDGA;;-VIwoukP-~0#%)S`kA*#7Z) zA0SrNSPYX1y6yx#!a~C5T+R**JqwE0|1B8(mv=t8J~9~{mKNb9Je@VrBZwqS{()2; zwl{$5wU{q8X6T}Af2$z%KG82-E>nom%t>Z9EGTNwEW<&qOzDJke3-`8T6Qzs8eoP0b zLVp7Ud-lAHP)0^Z8QoFs@B-d5(3I!0Z4KK`VlnD!Xt%J*UXF<`Bf9`z*`Bki8oVp2 zt#?9I66OnuVcnheYbn&9_2NE)G1rwCtuYc#>8fkAY>8|Z_+ApC(4Ucgu(Sm4O|wn) z0#LhzU5gC}yX&4=+1%eBR6wXxi12uo&MZ%zBbQ2Dp3!=Y1@8fbs&V6%_sFknwk5Nw zsV>^F{bydonAQGV-tkXmNMPBms%`8DE3KhOln@9;-^}Zy&gW8r>oa+ZMkPxFTY;5=omZU3}3RTeVHIb z{wc-M@@4G_iI?xrCM3tZQf+|&%!J>A_Tq@;QI`2*Ta`*Ssn^z@rI*C^oECBPpAR#H|q_4QPorcyF8 z=(L72<%ZHiknGmhYd|ET683Pqx$-P>{`u*C`_1JZfGd_37jyA|CUHVwMjaoZ_?xbs zF!oA|sKm!tg0sfnX2J_F?gaT>l+C1F)d^08fY_=DT+lVk3QXBT9J%bm!AFH}G&ozj zSuM5{LS4Q$K3)5{dGLFNA1f8D7*AUK=S(6Mwz6+X7Rs_ivh^RlkQny`> zA8BM_XB`@G*21c@hja+D>T9z}(h7tbDZEef$}J z*p6&nioiGIpyJ|NxE(Q5uND$%xTO%u&(bXl>PsrsU6?JDdG_=`6k0ebdyT2BlUTzL zQQhMrUuHwG;Oyaf)2OHpQ*fe)70(V*4vu$G!2BY&0I(E6I-@W`_p;wow+ix#O=~iRWp6&R$i4=>thcD1 z+e(Pg6N@kfkMXF*?(*gxr1e+2ZNw2bjQo%u$0qfwnSJY)B;32q<~OX>t1Zw#R`%;8 z7*AxF^4e}*nb@t5k8i4~GQQ51oYZYw95rOus$?$=irSlp^tesRPmQRcxzixSf7n?6 zwuop-Xs<2wdP)dasnZYq`NE~(9tceJJYcp!u#E{wSNr+VM4}7~>euzP#BK(^E^l(u zMh3g4Y8(WVU^#xltSpMuo{^-l()j<$R1OglO0MUmq|CT!mAa}z$bC`wkV3D8cie*buDtgFsD_%HVI{DTVU23?{U!G87Gg-RNzhT=G(kfP>K0`!!rhEIQ7}VwYA#ulU#b28Z z9}A?-HeFWmRp3%FvbYZ@U)2uNWdTDJXbPyOK%<#DRt$AJ;d7i+(W~9BTb>-zo+Q++ zo_eT}9+Jgf48o_oS}Ok-?8av9vi%x473=#iA%KG4>AR1#yu6Bv3Lv(0b#|(*RGLWk zRB`a}5py*IoVU7+P|D28s?f&l36MFWW_K-|A2NHLgD_t+MkFI1Wp@uaQm{BsUmfrZ zE)NwA+q&%=tZ=B7URU;a_fB-?#O^INm%H3#d9EKY@G38f`2=b5icHUNbMjg5hU#eyxfBOXDFlYLKO`X-1$zI!NZ6L9IK%r_s%=ChLPbSvXI2=LVSZ6b( z*XXpCBO6<(V_-n>{P~xR41i`v&w?)yn=LIZe9Inyxg<_zNKijlR#t|G*=T6)yVoXI>zG9g3Q@puMm5 ztj3PVaB}Z#QDb3IrWFVxi*iGoN#1gMt(D7Do;oGA;YIK7WBIq*JJuRC)3-X!x?nxE ziEfGc6pdGC`Cs|ZfiOZ9zXumDy}AxmqIe*+l;2K^EQ;ZX_>;aeyx6=R(5f{<^<=8M zDrq9>YCPNSo9?A@+>fcMkKE2r?M^!Ab}=JOP4`MfOG}v;%WGr)Rkm6G5_Vun0DwS1 z!Ngq$5Z0%~5G3&HK{9f3Kx*~};%w)IUx}nX9CfjsRfRdz?6CC1s>T0Slnjq_q0Tl` z$J)`+7_eVh*yI0Xxk@Tk`0F!{&?cyD5+RDYVZVT=utCgwjQMrgHqtht1T3k{mh%)1Xr!RH;7gcy|(PjzleGYk!3fh~g04m#E~-`i%2B7L~Oo+k}aU z33!f(nwpx7jEsV!CFKNv3;Qn%GF91}Q2G!DSm`FjvJ05q{*lWvK`+@L9Zg#!RMq4| z8)FUp%h>+g33=4{difoXt7uth_!E6MOK@ZWMr{ynMH!3av0n*{o5OdPPi%DSF4c2@ zzTeZ+^J@kd;Co{w(6ouMR@4cnpfgm;$?0|5*2c!h_V%Fn9UF}I`{5>J9DgDa~H+MJzd|s z^YP1Hc07G_>)Lgir!F1{Qn4GcTg%?koHo<=1qRN{}nPDolOeI^o4N5I>! zU$N=L=sg~ zDx#dOA*B0N~cqPsWI(^rbbkh)DS0_H_UN0C4l_kvWIm2#Kyy6%BCh z(yIUf003&1xdx>t$*eR2ZvXxT0001Z_R$y3Iju92q*wg58};}zm(OaAH=p|y0002M zh5O5#fxp|~jc?yi@+7$`d4Q6Hl%z;WiWG??NXR{Hx%)pMd~SE0000OQI literal 0 HcmV?d00001 diff --git a/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/down.png b/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/down.png new file mode 100644 index 0000000000000000000000000000000000000000..ff58c76dc1251f346088ea9f2b86f160e980c9ef GIT binary patch literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPFP2=EDU1=0;*+yJH;fV_r=0|yQ? zG&KANivMT$|DVBQ%P$}eWC1||yCM@%oV_H-FBmNFpFzr-*$pUU;pyTSQgLf-_eMbm z1s&A-ej;gyuG&s$%wkdfKA;H|vEO|L+|vIXqda)u00bMdO00RB}K{Dv*srmW&2A}`{b@l!In*{#;03!nf05}B&00;d100H{?6d?%- zyA%rxDwF2sr{VqmVBr7&00IC(0KWeNEf^RBIxKZ(&pwO*#H2t0suh(zW)PY0|U%qDJc;W zhK2wHJv{&>-`@iP;NV8n|NrO;0s_t*0Rape|Np->G&C?EYH9%gI5+?x6chm{<>fBD z=jR6k06_r0{{;2<`3MIA0RZ{j+yM9w4*(8YS^!x1_y+yR$dtzd0u>Yi0a7t1CommV zRRI40008&V(Etee_y7e31pxv7|1<)K`7s~gtFKR)bh@pZ~vp z?W*(mF$*KmfIq-MV+Cs9`T3JkR9TsUgO?Yqnd9kGhFiyuF%(_8^y1Lpzi~hZ?FI-S zW&uu4hA)5ro;}0B@Pg&jCpYC6FIfKl`o+NX^CtrrH#Y+d(6B!*UNHQA{hHz0ty>J` zH*UN*`uA@XQ1f0OW&j8vW^HkC20>s{@h~%=d&SB5TkzK}7qO>L8Gn8J$nfX=dxl>? zjXyqrX7~m)u>Ib>cSnBz4iIEuILrgo3#tYPZABX`0h*=L9U_bx==aKsH zz`#>QYFCB-7i%Bqn~jZGkDHtMFECZI0L7Vs zt}#_mV3GLu&u!n^w+{gVh)L%2XKihu=D$FV|A3eu7@&`Vn!CS!d$IG~JHLgmUuT~G z`SX_uP$Ls4!2x511(;sU<>c4|nVFLS0*LuF7ncG*C#QinFwR+lwwwlPuK)i1#rcmP zqu3Z353w^aoWK6%%S$m97B?LsAr?Vkn)n2aou;#Af9(AFbt*srF~|Z#S4UiY(JC>q zf1AX`|LgJdKLpF literal 0 HcmV?d00001 diff --git a/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/reload3.png b/Ghidra/Features/PDB/src/main/help/help/topics/Pdb/images/reload3.png new file mode 100644 index 0000000000000000000000000000000000000000..b80c72091f65b5983642df8a262d9ddabdb41784 GIT binary patch literal 979 zcmV;^11$WBP)dNaS~s#g$a^svC|EEli)sk{VEyIt z?#El_pPzr8Vfe-HoZ&ab0Du5u0TE!+{xPsJ3oH9hXQ;8l1x9CjDePH1**0M>bnD^82|!^ks%eV8DyFgiv!C|;V5C2zu*5dd|vYT z%g;+c3xQ7F0QBMypc)Q_{|p*H{!}1-55&$uU4H=rhy_SQ0{zYmqz(T_{$Y9f|0M&% z5g_jr!%?94SD->khR+O_nWPxL@CY#+`E-TB8t8>?pqv3v)meZ5Vq}N`D%D{yWl#ok z)qz+Di0=dO4X_~$K+Q?O@R`q~&%hul&%p5Z9s|RVM?mrKkT3uUASR&W{1_fFoMDg# z3a|q4Tc{x*`VB)R(5Cqe3JeVY*%-LqzG0a4n}@-JK?W$t2T>0YK+Fuf3||GRZ#jpj)Kg}=~7%F89;=o{I1v>N_LzlX?da|yiK7*yRl^Cxm?=oOGD1o#B1Q0WW zKEy9Th0hsYGF)MJ2K3#3hI|HLp!tGe<15%~*y;p51sSe0Tw{31_K<;Di&^B;-A~p) zZ*B$%AV!9Gr~&_h_&qdDumQ0o*w92^3Z2Jj&B*Xy4X8i^DE1nt{2Gvd8_2)Na2gFMGaQgLhPsg1l0 z3OvjQ*9RG Symbol Server Config.\n"; private static final String ERROR_TITLE = "Error in PDB Analyzer"; - private static final String SYMBOLPATH_OPTION_NAME = "Symbol Repository Path"; - private static final String SYMBOLPATH_OPTION_DESCRIPTION = - "Directory path to root of Microsoft Symbol Repository Directory"; - - private File symbolsRepositoryDir = PdbLocator.DEFAULT_SYMBOLS_DIR; - - //============================================================================================== - // Include the PE-Header-Specified PDB path for searching for appropriate PDB file. - private static final String OPTION_NAME_INCLUDE_PE_PDB_PATH = - "Unsafe: Include PE PDB Path in PDB Search"; - private static final String OPTION_DESCRIPTION_INCLUDE_PE_PDB_PATH = - "If checked, specifically searching for PDB in PE-Header-Specified Location."; - - private boolean includePeSpecifiedPdbPath = false; + private boolean searchRemoteLocations = false; // only try once per transaction due to extensive error logging which may get duplicated private long lastTransactionId = -1; @@ -101,75 +85,21 @@ public class PdbAnalyzer extends AbstractAnalyzer { return false; } - File pdb = lookForPdb(program, log); - - if (pdb == null) { - Msg.info(this, "PDB analyzer failed to locate PDB file"); + File pdbFile = PdbAnalyzerCommon.findPdb(this, program, searchRemoteLocations, monitor); + if (pdbFile == null) { + // warnings have already been logged return false; } - Msg.info(this, "PDB analyzer parsing file: " + pdb.getAbsolutePath()); AutoAnalysisManager mgr = AutoAnalysisManager.getAnalysisManager(program); - return parsePdb(pdb, program, mgr, monitor, log); - } - - private static class PdbMissingState implements AnalysisState { - // object existence indicates missing PDB has already been reported - } - - File lookForPdb(Program program, MessageLog log) { - String message = ""; - File pdb; - - try { - - pdb = PdbParser.findPDB(program, includePeSpecifiedPdbPath, symbolsRepositoryDir); - - if (pdb == null) { - - PdbMissingState missingState = - AnalysisStateInfo.getAnalysisState(program, PdbMissingState.class); - if (missingState != null) { - return null; // already notified user - } - AnalysisStateInfo.putAnalysisState(program, new PdbMissingState()); - - String pdbName = program.getOptions(Program.PROGRAM_INFO).getString( - PdbParserConstants.PDB_FILE, (String) null); - if (StringUtils.isBlank(pdbName)) { - message = "Program has no associated PDB file."; - } - else { - message = "Unable to locate PDB file \"" + pdbName + "\" with matching GUID."; - } - if (SystemUtilities.isInHeadlessMode()) { - message += "\n Use a script to set the PDB file location. I.e.,\n" + - " setAnalysisOption(currentProgram, \"PDB.Symbol Repository Path\", \"/path/to/pdb/folder\");\n" + - " This must be done using a pre-script (prior to analysis)."; - } - else { - message += "\n You may set the PDB \"Symbol Repository Path\"" + - "\n using \"Edit->Options for [program]\" prior to analysis." + - "\nIt is important that a PDB is used during initial analysis " + - "\nif available."; - } - } - - return pdb; - } - finally { - if (message.length() > 0) { - log.appendMsg(getName(), message); - log.setStatus(message); - } - } - + return parsePdb(pdbFile, program, mgr, monitor, log); } boolean parsePdb(File pdb, Program program, AutoAnalysisManager mgr, TaskMonitor monitor, MessageLog log) { DataTypeManagerService dataTypeManagerService = mgr.getDataTypeManagerService(); - PdbParser parser = new PdbParser(pdb, program, dataTypeManagerService, true, monitor); + PdbParser parser = + new PdbParser(pdb, program, dataTypeManagerService, true, false, monitor); String message; @@ -201,32 +131,52 @@ public class PdbAnalyzer extends AbstractAnalyzer { @Override public boolean canAnalyze(Program program) { - return PeLoader.PE_NAME.equals(program.getExecutableFormat()); + return PdbAnalyzerCommon.canAnalyzeProgram(program); + //return PeLoader.PE_NAME.equals(program.getExecutableFormat()); } @Override public void registerOptions(Options options, Program program) { - symbolsRepositoryDir = PdbLocator.getDefaultPdbSymbolsDir(); - - options.registerOption(SYMBOLPATH_OPTION_NAME, OptionType.FILE_TYPE, symbolsRepositoryDir, - null, SYMBOLPATH_OPTION_DESCRIPTION); - - options.registerOption(OPTION_NAME_INCLUDE_PE_PDB_PATH, includePeSpecifiedPdbPath, null, - OPTION_DESCRIPTION_INCLUDE_PE_PDB_PATH); + options.registerOption(PdbAnalyzerCommon.OPTION_NAME_SEARCH_REMOTE_LOCATIONS, + searchRemoteLocations, null, + PdbAnalyzerCommon.OPTION_DESCRIPTION_SEARCH_REMOTE_LOCATIONS); } @Override public void optionsChanged(Options options, Program program) { - - File symbolsDir = options.getFile(SYMBOLPATH_OPTION_NAME, symbolsRepositoryDir); - if (!symbolsDir.equals(symbolsRepositoryDir)) { - symbolsRepositoryDir = symbolsDir; - PdbLocator.setDefaultPdbSymbolsDir(symbolsDir); - } - - includePeSpecifiedPdbPath = - options.getBoolean(OPTION_NAME_INCLUDE_PE_PDB_PATH, includePeSpecifiedPdbPath); + searchRemoteLocations = options.getBoolean( + PdbAnalyzerCommon.OPTION_NAME_SEARCH_REMOTE_LOCATIONS, searchRemoteLocations); } + /** + * Sets the PDB file that will be used by the analyzer when it is next invoked + * on the specified program. + *

    + * Normally the analyzer would locate the PDB file on its own, but if a + * headless script wishes to override the analyzer's behaivor, it can + * use this method to specify a file. + * + * @param program {@link Program} + * @param pdbFile the pdb file + */ + public static void setPdbFileOption(Program program, File pdbFile) { + PdbAnalyzerCommon.setPdbFileOption(NAME, program, pdbFile); + } + + /** + * Sets the "allow remote" option that will be used by the analyzer when it is next invoked + * on the specified program. + *

    + * Normally when the analyzer attempts to locate a matching PDB file it + * will default to NOT searching remote symbol servers. A headless script could + * use this method to allow the analyzer to search remote symbol servers. + * + * @param program {@link Program} + * @param allowRemote boolean flag, true means analyzer can search remote symbol + * servers + */ + public static void setAllowRemoteOption(Program program, boolean allowRemote) { + PdbAnalyzerCommon.setAllowRemoteOption(NAME, program, allowRemote); + } } diff --git a/Ghidra/Features/PDB/src/main/java/ghidra/app/plugin/core/analysis/PdbAnalyzerCommon.java b/Ghidra/Features/PDB/src/main/java/ghidra/app/plugin/core/analysis/PdbAnalyzerCommon.java new file mode 100644 index 0000000000..f9149b9984 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/ghidra/app/plugin/core/analysis/PdbAnalyzerCommon.java @@ -0,0 +1,182 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.analysis; + +import java.util.Set; + +import java.io.File; + +import ghidra.app.services.Analyzer; +import ghidra.app.util.opinion.PeLoader; +import ghidra.framework.options.Options; +import ghidra.program.model.listing.Program; +import ghidra.util.Msg; +import ghidra.util.SystemUtilities; +import ghidra.util.task.TaskMonitor; +import pdb.PdbPlugin; +import pdb.symbolserver.FindOption; +import pdb.symbolserver.SymbolFileInfo; + +/** + * Shared configuration values and pdb searching logic + */ +public class PdbAnalyzerCommon { + static final String OPTION_DESCRIPTION_SEARCH_REMOTE_LOCATIONS = + "If checked, allow searching remote symbol servers for PDB files."; + static final String OPTION_NAME_SEARCH_REMOTE_LOCATIONS = "Search remote symbol servers"; + + static final String OPTION_DESCRIPTION_PDB_FILE = "Path to a manually chosen PDB file."; + static final String OPTION_NAME_PDB_FILE = "PDB File"; + + // TODO: I changed this method from what was lifted in the old code. I check for null string + // and I also check for MSCOFF_NAME (TODO: check on the validity of this!!!). Also, changed + // the comparison to a substring search from a .equals). + /** + * Returns true if the specified program is supported by either of the + * Pdb analyzers. + * + * @param program {@link Program} + * @return boolean true if program is supported by Pdb analyzers + */ + public static boolean canAnalyzeProgram(Program program) { + String executableFormat = program.getExecutableFormat(); + return executableFormat != null && (executableFormat.indexOf(PeLoader.PE_NAME) != -1); + // TODO: Check for MSCOFF_NAME. Initial investigation shows that the .debug$T section of + // the MSCOFF (*.obj) file has type records and the .debug$S section has symbol records. + // More than that, in at least one instance, there has been a TypeServer2MsType type + // record that give the GUID, age, and name of the PDB file associated with the MSCOFF + // file. At this point in time, these two sections of the MSCOFF are read (header and + // raw data), but we do not interpret these sections any further. Suggest that we "might" + // want to parse some of these records at load time? Maybe not. We could, at analysis + // time, add the ability to process these two sections (as part of analysis (though we + // will not be aware of a PDB file yet), and upon discovery of a TypeServer2MsType (or + // perhaps other?), proceed to find the file (if possible) and also process that file. + // We posit that if a record indicates a separate PDB for the types (Note: MSFT indicates + // that only data types will be found in an MSCOFF PDB file), then that will likely be + // the only record in the .debug$T section. + // TODO: If the MSCOFF file is located in a MSCOFF ARCHIVE (*.lib), there can be a PDB + // associated with the archive. We currently do not pass on this association of the + // PDB archive to each underlying MSCOFF file. Moreover, we believe that we are not + // currently discovering the associated MSCOFF ARCHIVE PDB file when processing the + // MSCOFF ARCHIVE. Initial indication is that each MSCOFF within the archive will have + // the PDB file that it needs listed, even if redundant for each MSCOFF within the + // archive. +// return executableFormat != null && (executableFormat.indexOf(PeLoader.PE_NAME) != -1 || +// executableFormat.indexOf(MSCoffLoader.MSCOFF_NAME) != -1); + + } + + /** + * Common logic to set a manual Pdb file that the specified analyzer will find and use + * when it is invoked later

    + * Each specific analyzer has a public method that calls this to supply the + * actual analyzer name to make it easier for script writers to call. + * + * @param analyzerName name of analyzer + * @param program {@link Program} + * @param pdbFile the file + */ + static void setPdbFileOption(String analyzerName, Program program, File pdbFile) { + Options options = program.getOptions(Program.ANALYSIS_PROPERTIES); + options.setFile(analyzerName + "." + OPTION_NAME_PDB_FILE, pdbFile); + } + + /** + * Common logic to set the "allow remote" option that the specified analyzer will find and use + * when it is invoked later

    + * Each specific analyzer has a public method that calls this to supply the + * actual analyzer name to make it easier for script writers to call. + * + * @param analyzerName name of analyzer + * @param program {@link Program} + * @param allowRemote boolean flag, true means the analyzer can search remote + * symbol servers + */ + static void setAllowRemoteOption(String analyzerName, Program program, boolean allowRemote) { + Options options = program.getOptions(Program.ANALYSIS_PROPERTIES); + options.setBoolean(analyzerName + "." + OPTION_NAME_SEARCH_REMOTE_LOCATIONS, allowRemote); + } + + /** + * Common pdb searching logic between both analyzers. + * + * @param pdbAnalyzer the analyzer doing the searching + * @param program the program + * @param allowRemote boolean flag, true means searching remote symbol servers + * is allowed + * @param monitor {@link TaskMonitor} to let user cancel + * @return File pointing to the found pdb, or null if not found or error + */ + static File findPdb(Analyzer pdbAnalyzer, Program program, boolean allowRemote, + TaskMonitor monitor) { + + SymbolFileInfo symbolFileInfo = SymbolFileInfo.fromMetadata(program.getMetadata()); + if (symbolFileInfo == null) { + Msg.info(pdbAnalyzer, + "Skipping PDB processing: missing PDB information in program metadata"); + return null; + } + + // First look in the program's analysis options to see if there is a + // manually specified pdbFile. (see setPdbFileOption) + // If not set, then do a search using the currently configured symbol servers. + Options options = program.getOptions(Program.ANALYSIS_PROPERTIES); + String pdbFileOptionName = pdbAnalyzer.getName() + "." + OPTION_NAME_PDB_FILE; + + // check existence first to avoid creating option value + File pdbFile = options.contains(pdbFileOptionName) + ? options.getFile(pdbFileOptionName, null) + : null; + if (pdbFile == null) { + Set findOpts = allowRemote + ? FindOption.of(FindOption.ALLOW_REMOTE) + : FindOption.NO_OPTIONS; + pdbFile = PdbPlugin.findPdb(program, findOpts, monitor); + } + if (pdbFile == null) { + Msg.info(pdbAnalyzer, + "Skipping PDB processing: failed to locate PDB file in configured locations"); + if (SystemUtilities.isInHeadlessMode()) { + Msg.info(pdbAnalyzer, + "Use a script to set the PDB file location. I.e.,\n" + + " PdbAnalyzer.setPdbFileOption(currentProgram, new File(\"/path/to/pdb/file.pdb\")); or\n" + + " PdbUniversalAnalyzer.setPdbFileOption(currentProgram, new File(\"/path/to/pdb/file.pdb\"));\n" + + "Or set the symbol server search configuration using:" + + " PdbPlugin.saveSymbolServerServiceConfig(...);\n" + + " This must be done using a pre-script (prior to analysis)."); + } + else { + Msg.info(pdbAnalyzer, + "You may set the PDB \"Symbol Server Config\"" + + "\n using \"Edit->Symbol Server Config\" prior to analysis." + + "\nIt is important that a PDB is used during initial analysis " + + "\nif available."); + } + } + else { + Msg.info(pdbAnalyzer, "PDB analyzer parsing file: " + pdbFile); + if (!pdbFile.isFile()) { + Msg.error(pdbAnalyzer, + "Skipping PDB processing: specified file does not exist or is not readable: " + + pdbFile); + return null; + } + + } + return pdbFile; + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/ghidra/app/plugin/core/analysis/PdbUniversalAnalyzer.java b/Ghidra/Features/PDB/src/main/java/ghidra/app/plugin/core/analysis/PdbUniversalAnalyzer.java index b8a0cf364b..4ba7249cb5 100644 --- a/Ghidra/Features/PDB/src/main/java/ghidra/app/plugin/core/analysis/PdbUniversalAnalyzer.java +++ b/Ghidra/Features/PDB/src/main/java/ghidra/app/plugin/core/analysis/PdbUniversalAnalyzer.java @@ -19,12 +19,9 @@ import java.io.File; import java.io.IOException; import java.util.Date; -import org.apache.commons.lang3.StringUtils; - import ghidra.app.services.*; import ghidra.app.util.bin.format.pdb2.pdbreader.*; import ghidra.app.util.importer.MessageLog; -import ghidra.app.util.opinion.PeLoader; import ghidra.app.util.pdb.PdbLocator; import ghidra.app.util.pdb.PdbProgramAttributes; import ghidra.app.util.pdb.pdbapplicator.PdbApplicator; @@ -35,7 +32,6 @@ import ghidra.framework.options.Options; import ghidra.program.model.address.AddressSetView; import ghidra.program.model.listing.Program; import ghidra.util.Msg; -import ghidra.util.SystemUtilities; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -63,7 +59,8 @@ public class PdbUniversalAnalyzer extends AbstractAnalyzer { static final boolean DEFAULT_ENABLEMENT = true; private static final String DESCRIPTION = "Platform-independent PDB analyzer (No XML support).\n" + - "NOTE: still undergoing development, so options may change."; + "NOTE: still undergoing development, so options may change.\n" + + "PDB Symbol Server searching is configured in Edit -> Symbol Server Config.\n"; //============================================================================================== // Force-load a PDB file. @@ -79,18 +76,7 @@ public class PdbUniversalAnalyzer extends AbstractAnalyzer { private File DEFAULT_FORCE_LOAD_FILE = new File(PdbLocator.DEFAULT_SYMBOLS_DIR, "sample.pdb"); private File forceLoadFile; - // Symbol Repository Path. - private static final String OPTION_NAME_SYMBOLPATH = "Symbol Repository Path"; - private static final String OPTION_DESCRIPTION_SYMBOLPATH = - "Directory path to root of Microsoft Symbol Repository Directory"; - private File symbolsRepositoryDir; - - // Include the PE-Header-Specified PDB path for searching for appropriate PDB file. - private static final String OPTION_NAME_INCLUDE_PE_PDB_PATH = - "Unsafe: Include PE PDB Path in PDB Search"; - private static final String OPTION_DESCRIPTION_INCLUDE_PE_PDB_PATH = - "If checked, specifically searching for PDB in PE-Header-Specified Location."; - private boolean includePeSpecifiedPdbPath = false; + private boolean searchRemoteLocations = false; //============================================================================================== // Additional instance data @@ -162,34 +148,21 @@ public class PdbUniversalAnalyzer extends AbstractAnalyzer { return true; } - if (failMissingFilename(programAttributes, log) || - failMissingAttributes(programAttributes, log)) { - return true; - } - - String pdbFilename; - if (doForceLoad) { - if (!confirmFile(forceLoadFile)) { + File pdbFile = null; + if (doForceLoad && forceLoadFile != null) { + if (!forceLoadFile.isFile()) { logFailure("Force-load PDB file does not exist: " + forceLoadFile, log); return false; } - pdbFilename = forceLoadFile.getAbsolutePath(); + pdbFile = forceLoadFile; } else { - PdbLocator locator = new PdbLocator(symbolsRepositoryDir); - pdbFilename = - locator.findPdb(program, programAttributes, !SystemUtilities.isInHeadlessMode(), - includePeSpecifiedPdbPath, monitor, log, getName()); - if (pdbFilename == null) { - if (!confirmDirectory(symbolsRepositoryDir)) { - logFailure("PDB symbol repository directory not found: " + symbolsRepositoryDir, - log); - } - Msg.info(this, "PDB analyzer failed to locate PDB file"); - return false; - } + pdbFile = PdbAnalyzerCommon.findPdb(this, program, searchRemoteLocations, monitor); + } + if (pdbFile == null) { + // warnings have already been logged + return false; } - Msg.info(this, "PDB analyzer parsing file: " + pdbFilename); PdbLog.message( "================================================================================"); @@ -197,61 +170,33 @@ public class PdbUniversalAnalyzer extends AbstractAnalyzer { PdbLog.message("Ghidra Version: " + Application.getApplicationVersion()); PdbLog.message(NAME); PdbLog.message(DESCRIPTION); - PdbLog.message("PDB Filename: " + pdbFilename + "\n"); + PdbLog.message("PDB Filename: " + pdbFile + "\n"); - try (AbstractPdb pdb = PdbParser.parse(pdbFilename, pdbReaderOptions, monitor)) { - monitor.setMessage("PDB: Parsing " + pdbFilename + "..."); + try (AbstractPdb pdb = PdbParser.parse(pdbFile.getPath(), pdbReaderOptions, monitor)) { + monitor.setMessage("PDB: Parsing " + pdbFile + "..."); pdb.deserialize(monitor); - PdbApplicator applicator = new PdbApplicator(pdbFilename, pdb); + PdbApplicator applicator = new PdbApplicator(pdbFile.getPath(), pdb); applicator.applyTo(program, program.getDataTypeManager(), program.getImageBase(), pdbApplicatorOptions, monitor, log); } catch (PdbException | IOException e) { log.appendMsg(getName(), - "Issue processing PDB file: " + pdbFilename + ":\n " + e.toString()); + "Issue processing PDB file: " + pdbFile + ":\n " + e.toString()); return false; } return true; } - // TODO: I changed this method from what was lifted in the old code. I check for null string - // and I also check for MSCOFF_NAME (TODO: check on the validity of this!!!). Also, changed - // the comparison to a substring search from a .equals). @Override public boolean canAnalyze(Program program) { - String executableFormat = program.getExecutableFormat(); - return executableFormat != null && (executableFormat.indexOf(PeLoader.PE_NAME) != -1); - // TODO: Check for MSCOFF_NAME. Initial investigation shows that the .debug$T section of - // the MSCOFF (*.obj) file has type records and the .debug$S section has symbol records. - // More than that, in at least one instance, there has been a TypeServer2MsType type - // record that give the GUID, age, and name of the PDB file associated with the MSCOFF - // file. At this point in time, these two sections of the MSCOFF are read (header and - // raw data), but we do not interpret these sections any further. Suggest that we "might" - // want to parse some of these records at load time? Maybe not. We could, at analysis - // time, add the ability to process these two sections (as part of analysis (though we - // will not be aware of a PDB file yet), and upon discovery of a TypeServer2MsType (or - // perhaps other?), proceed to find the file (if possible) and also process that file. - // We posit that if a record indicates a separate PDB for the types (Note: MSFT indicates - // that only data types will be found in an MSCOFF PDB file), then that will likely be - // the only record in the .debug$T section. - // TODO: If the MSCOFF file is located in a MSCOFF ARCHIVE (*.lib), there can be a PDB - // associated with the archive. We currently do not pass on this association of the - // PDB archive to each underlying MSCOFF file. Moreover, we believe that we are not - // currently discovering the associated MSCOFF ARCHIVE PDB file when processing the - // MSCOFF ARCHIVE. Initial indication is that each MSCOFF within the archive will have - // the PDB file that it needs listed, even if redundant for each MSCOFF within the - // archive. -// return executableFormat != null && (executableFormat.indexOf(PeLoader.PE_NAME) != -1 || -// executableFormat.indexOf(MSCoffLoader.MSCOFF_NAME) != -1); + return PdbAnalyzerCommon.canAnalyzeProgram(program); } @Override public void registerOptions(Options options, Program program) { - symbolsRepositoryDir = PdbLocator.getDefaultPdbSymbolsDir(); - // PDB file location information if (developerMode) { options.registerOption(OPTION_NAME_DO_FORCELOAD, Boolean.FALSE, null, @@ -259,10 +204,9 @@ public class PdbUniversalAnalyzer extends AbstractAnalyzer { options.registerOption(OPTION_NAME_FORCELOAD_FILE, OptionType.FILE_TYPE, DEFAULT_FORCE_LOAD_FILE, null, OPTION_DESCRIPTION_FORCELOAD_FILE); } - options.registerOption(OPTION_NAME_SYMBOLPATH, OptionType.FILE_TYPE, symbolsRepositoryDir, - null, OPTION_DESCRIPTION_SYMBOLPATH); - options.registerOption(OPTION_NAME_INCLUDE_PE_PDB_PATH, includePeSpecifiedPdbPath, null, - OPTION_DESCRIPTION_INCLUDE_PE_PDB_PATH); + options.registerOption(PdbAnalyzerCommon.OPTION_NAME_SEARCH_REMOTE_LOCATIONS, + searchRemoteLocations, null, + PdbAnalyzerCommon.OPTION_DESCRIPTION_SEARCH_REMOTE_LOCATIONS); pdbReaderOptions.registerOptions(options); pdbApplicatorOptions.registerAnalyzerOptions(options); @@ -279,14 +223,8 @@ public class PdbUniversalAnalyzer extends AbstractAnalyzer { forceLoadFile = options.getFile(OPTION_NAME_FORCELOAD_FILE, forceLoadFile); } - File symbolsDir = options.getFile(OPTION_NAME_SYMBOLPATH, symbolsRepositoryDir); - if (!symbolsDir.equals(symbolsRepositoryDir)) { - symbolsRepositoryDir = symbolsDir; - PdbLocator.setDefaultPdbSymbolsDir(symbolsDir); - } - - includePeSpecifiedPdbPath = - options.getBoolean(OPTION_NAME_INCLUDE_PE_PDB_PATH, includePeSpecifiedPdbPath); + searchRemoteLocations = options.getBoolean( + PdbAnalyzerCommon.OPTION_NAME_SEARCH_REMOTE_LOCATIONS, searchRemoteLocations); pdbReaderOptions.loadOptions(options); pdbApplicatorOptions.loadAnalyzerOptions(options); @@ -294,51 +232,40 @@ public class PdbUniversalAnalyzer extends AbstractAnalyzer { //============================================================================================== - private boolean failMissingFilename(PdbProgramAttributes attributes, MessageLog log) { - if (doForceLoad) { - return false; // PDB File property not used for forced load - } - if (StringUtils.isEmpty(attributes.getPdbFile())) { - logFailure("Missing 'PDB File' program property, unable to locate PDB", log); - return true; - } - return false; - } - private void logFailure(String msg, MessageLog log) { log.appendMsg(getName(), msg); log.appendMsg(getName(), "Skipping PDB processing"); log.setStatus(msg); } - private boolean failMissingAttributes(PdbProgramAttributes attributes, MessageLog log) { - if (doForceLoad) { - return false; // Attributes not used for forced load - } - // RSDS version should only have GUID; non-RSDS version should only have Signature. - String error; - if ("RSDS".equals(attributes.getPdbVersion())) { - if (!StringUtils.isEmpty(attributes.getPdbGuid())) { - return false; // Don't fail. - } - error = "Missing 'PDB GUID' program property, unable to locate PDB."; - } - else { - if (!StringUtils.isEmpty(attributes.getPdbSignature())) { - return false; // Don't fail. - } - error = "Missing 'PDB Signature' program property, unable to locate PDB."; - } - logFailure(error, log); - return true; + /** + * Sets the PDB file that will be used by the analyzer when it is next invoked + * on the specified program. + *

    + * Normally the analyzer would locate the PDB file on its own, but if a + * headless script wishes to override the analyzer's behaivor, it can + * use this method to specify a file. + * + * @param program {@link Program} + * @param pdbFile the pdb file + */ + public static void setPdbFileOption(Program program, File pdbFile) { + PdbAnalyzerCommon.setPdbFileOption(NAME, program, pdbFile); } - private boolean confirmDirectory(File path) { - return path.isDirectory(); + /** + * Sets the "allow remote" option that will be used by the analyzer when it is next invoked + * on the specified program. + *

    + * Normally when the analyzer attempts to locate a matching PDB file it + * will default to NOT searching remote symbol servers. A headless script could + * use this method to allow the analyzer to search remote symbol servers. + * + * @param program {@link Program} + * @param allowRemote boolean flag, true means analyzer can search remote symbol + * servers + */ + public static void setAllowRemoteOption(Program program, boolean allowRemote) { + PdbAnalyzerCommon.setAllowRemoteOption(NAME, program, allowRemote); } - - private boolean confirmFile(File path) { - return path.isFile(); - } - } diff --git a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfo.java b/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfo.java deleted file mode 100644 index 4abeab3c76..0000000000 --- a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfo.java +++ /dev/null @@ -1,90 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.util.bin.format.pdb; - -import ghidra.app.util.bin.BinaryReader; -import ghidra.app.util.bin.format.pe.debug.DebugCodeViewConstants; -import ghidra.program.model.data.*; -import ghidra.util.exception.DuplicateNameException; - -import java.io.IOException; - -public class PdbInfo implements PdbInfoIface { - public final static int MAGIC = - DebugCodeViewConstants.SIGNATURE_NB << 16 | - DebugCodeViewConstants.VERSION_10; - - public static boolean isMatch(BinaryReader reader, int ptr) throws IOException { - //read value out as big endian - int value = reader.readByte(ptr ) << 24 | - reader.readByte(ptr+1) << 16 | - reader.readByte(ptr+2) << 8 | - reader.readByte(ptr+3); - return MAGIC == value; - } - - private byte [] magic; - private int offset; - private int sig; - private int age; - private String pdbName; - - public PdbInfo(BinaryReader reader, int ptr) throws IOException { - long origIndex = reader.getPointerIndex(); - reader.setPointerIndex(ptr); - try { - magic = reader.readNextByteArray(4); - offset = reader.readNextInt(); - sig = reader.readNextInt(); - age = reader.readNextInt(); - pdbName = reader.readNextAsciiString(); - } - finally { - reader.setPointerIndex(origIndex); - } - } - - public byte [] getMagic() { - return magic; - } - - public int getOffset() { - return offset; - } - - public int getSig() { - return sig; - } - - public int getAge() { - return age; - } - - public String getPdbName() { - return pdbName; - } - - public DataType toDataType() throws DuplicateNameException, IOException { - StructureDataType struct = new StructureDataType("PdbInfo", 0); - struct.add(new StringDataType(), magic.length, "signature", null); - struct.add(new DWordDataType(), "offset", null); - struct.add(new DWordDataType(), "sig", null); - struct.add(new DWordDataType(), "age", null); - struct.add(new StringDataType(), pdbName.length(), "pdbname", null); - struct.setCategoryPath(new CategoryPath("/PDB")); - return struct; - } -} diff --git a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoDotNet.java b/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoDotNet.java deleted file mode 100644 index 27ff8ecacc..0000000000 --- a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/PdbInfoDotNet.java +++ /dev/null @@ -1,89 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.util.bin.format.pdb; - -import ghidra.app.util.bin.BinaryReader; -import ghidra.app.util.bin.format.pe.debug.DebugCodeViewConstants; -import ghidra.app.util.datatype.microsoft.GUID; -import ghidra.app.util.datatype.microsoft.GuidDataType; -import ghidra.program.model.data.*; -import ghidra.util.exception.DuplicateNameException; - -import java.io.IOException; - -public class PdbInfoDotNet implements PdbInfoDotNetIface { - public final static int MAGIC = DebugCodeViewConstants.SIGNATURE_DOT_NET << 16 | - DebugCodeViewConstants.VERSION_DOT_NET; - - public static boolean isMatch(BinaryReader reader, int ptr) throws IOException { - //read value out as big endian - int value = - reader.readByte(ptr) << 24 | reader.readByte(ptr + 1) << 16 | - reader.readByte(ptr + 2) << 8 | reader.readByte(ptr + 3); - return MAGIC == value; - } - - private byte[] magic; - private GUID guid; - private int age; - private String pdbName; - - public PdbInfoDotNet(BinaryReader reader, int ptr) throws IOException { - long origIndex = reader.getPointerIndex(); - reader.setPointerIndex(ptr); - try { - magic = reader.readNextByteArray(4); - guid = new GUID(reader); - age = reader.readNextInt(); - pdbName = reader.readNextAsciiString(); - } - finally { - reader.setPointerIndex(origIndex); - } - } - - public String getPdbName() { - return pdbName; - } - - public int getAge() { - return age; - } - - public int getSignature() { - return guid.getData1(); - } - - public GUID getGUID() { - return guid; - } - - public byte[] getMagic() { - return magic; - } - - public DataType toDataType() throws DuplicateNameException, IOException { - StructureDataType struct = new StructureDataType("DotNetPdbInfo", 0); - struct.add(new StringDataType(), magic.length, "signature", null); - struct.add(new GuidDataType(), "guid", null); - struct.add(new DWordDataType(), "age", null); - if (pdbName.length() > 0) { - struct.add(new StringDataType(), pdbName.length(), "pdbname", null); - } - struct.setCategoryPath(new CategoryPath("/PDB")); - return struct; - } -} diff --git a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/PdbParser.java b/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/PdbParser.java index e949271c8e..ea605100b5 100644 --- a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/PdbParser.java +++ b/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb/PdbParser.java @@ -15,9 +15,10 @@ */ package ghidra.app.util.bin.format.pdb; -import java.io.*; import java.util.*; +import java.io.*; + import org.xml.sax.SAXException; import docking.widgets.OptionDialog; @@ -27,9 +28,7 @@ import ghidra.app.plugin.core.datamgr.util.DataTypeArchiveUtility; import ghidra.app.services.DataTypeManagerService; import ghidra.app.util.NamespaceUtils; import ghidra.app.util.SymbolPath; -import ghidra.app.util.importer.LibrarySearchPathManager; import ghidra.app.util.importer.MessageLog; -import ghidra.app.util.pdb.PdbLocator; import ghidra.app.util.pdb.PdbProgramAttributes; import ghidra.framework.*; import ghidra.framework.options.Options; @@ -83,6 +82,7 @@ public class PdbParser { private PdbErrorHandler errHandler; private PdbErrorReaderThread thread; private boolean parsed = false; + private boolean allowNonExactMatch; private CategoryPath pdbCategory; @@ -94,13 +94,40 @@ public class PdbParser { private PdbDataTypeParser dataTypeParser; private Map namespaceMap = new TreeMap<>(); // false: simple namespace, true: class namespace + /** + * Creates a PdbParser instance. + * + * @param pdbFile the pdb file to parse, either .pdb or .pdb.xml + * @param program the {@link Program} to modify + * @param service {@link DataTypeManagerService} + * @param forceAnalysis boolean flag, currently always true, needs to be refactored out + * @param allowNonExactMatch boolean flag, if true skips warning user about mismatch + * between the program's PDB guid/id/age and the specified PDB file's guid/id/age, which + * can terminate the pdb import in headless + * @param monitor {@link TaskMonitor}, null ok + */ public PdbParser(File pdbFile, Program program, DataTypeManagerService service, - boolean forceAnalysis, TaskMonitor monitor) { - this(pdbFile, program, service, getPdbAttributes(program), forceAnalysis, monitor); + boolean forceAnalysis, boolean allowNonExactMatch, TaskMonitor monitor) { + this(pdbFile, program, service, getPdbAttributes(program), forceAnalysis, + allowNonExactMatch, monitor); } + /** + * Creates a PdbParser instance. + * + * @param pdbFile the pdb file to parse, either .pdb or .pdb.xml + * @param program the {@link Program} to modify + * @param service {@link DataTypeManagerService} + * @param programAttributes the PDB information specified by the program + * @param forceAnalysis boolean flag, currently always true, needs to be refactored out + * @param allowNonExactMatch boolean flag, if true skips warning user about mismatch + * between the program's PDB guid/id/age and the specified PDB file's guid/id/age, which + * can terminate the pdb import in headless + * @param monitor {@link TaskMonitor}, null ok + */ public PdbParser(File pdbFile, Program program, DataTypeManagerService service, - PdbProgramAttributes programAttributes, boolean forceAnalysis, TaskMonitor monitor) { + PdbProgramAttributes programAttributes, boolean forceAnalysis, + boolean allowNonExactMatch, TaskMonitor monitor) { this.pdbFile = pdbFile; this.pdbCategory = new CategoryPath(CategoryPath.ROOT, pdbFile.getName()); this.program = program; @@ -108,8 +135,9 @@ public class PdbParser { this.service = service; this.forceAnalysis = forceAnalysis; this.monitor = monitor != null ? monitor : TaskMonitor.DUMMY; - this.isXML = pdbFile.getAbsolutePath().endsWith(PdbFileType.XML.toString()); + this.isXML = pdbFile.getName().toLowerCase().endsWith(PdbFileType.XML.toString()); this.programAttributes = programAttributes; + this.allowNonExactMatch = allowNonExactMatch; } /** @@ -184,12 +212,12 @@ public class PdbParser { } private void checkFileType() throws PdbException { - String pdbFilename = pdbFile.getName(); + String pdbFilename = pdbFile.getName().toLowerCase(); if (!pdbFilename.endsWith(PdbFileType.PDB.toString()) && !pdbFilename.endsWith(PdbFileType.XML.toString())) { throw new PdbException( - "\nInvalid file type (expecting .pdb or .pdb.xml): '" + pdbFilename + "'"); + "\nInvalid file type (expecting .pdb or .pdb.xml): '" + pdbFile.getName() + "'"); } } @@ -616,18 +644,21 @@ public class PdbParser { pdbGuid = pdbGuid.toUpperCase(); pdbGuid = "{" + pdbGuid + "}"; - if (!xmlGuid.equals(pdbGuid)) { - warning = "PDB signature does not match.\n" + "Program GUID: " + pdbGuid + - "\nXML GUID: " + xmlGuid; } - else { - // Also check that PDB ages match, if they are both available - if ((xmlAge != null) && (pdbAge != null)) { + if (!allowNonExactMatch) { + if (!xmlGuid.equals(pdbGuid)) { + warning = "PDB signature does not match.\n" + "Program GUID: " + pdbGuid + + "\nXML GUID: " + xmlGuid; + } + else { + // Also check that PDB ages match, if they are both available + if ((xmlAge != null) && (pdbAge != null)) { - int pdbAgeDecimal = Integer.parseInt(pdbAge, 16); - int xmlAgeDecimal = Integer.parseInt(xmlAge); + int pdbAgeDecimal = Integer.parseInt(pdbAge, 16); + int xmlAgeDecimal = Integer.parseInt(xmlAge); - if (xmlAgeDecimal != pdbAgeDecimal) { - warning = "PDB ages do not match."; + if (xmlAgeDecimal != pdbAgeDecimal) { + warning = "PDB ages do not match."; + } } } } @@ -1042,17 +1073,6 @@ public class PdbParser { return new PdbProgramAttributes(program); } - /** - * Find the PDB associated with the given program using its attributes. - * The PDB path information within the program information will not be used. - * - * @param program program for which to find a matching PDB - * @return matching PDB for program, or null - */ - public static File findPDB(Program program) { - return findPDB(getPdbAttributes(program), false, null, null); - } - /** * Determine if the PDB has previously been loaded for the specified program. * @param program program for which to find a matching PDB @@ -1062,273 +1082,6 @@ public class PdbParser { return getPdbAttributes(program).isPdbLoaded(); } - /** - * Find the PDB associated with the given program using its attributes, specifying the - * location where symbols are stored. - * - * @param program program for which to find a matching PDB - * @param includePeSpecifiedPdbPath to also check the PE-header-specified PDB path - * @param symbolsRepositoryDir location where downloaded symbols are stored - * @return matching PDB for program, or null - */ - public static File findPDB(Program program, boolean includePeSpecifiedPdbPath, - File symbolsRepositoryDir) { - return findPDB(getPdbAttributes(program), includePeSpecifiedPdbPath, symbolsRepositoryDir, - null); - } - - /** - * Find a matching PDB file using attributes associated with the program. User can specify the - * type of file to search from (.pdb or .pdb.xml). - * - * @param pdbAttributes PDB attributes associated with the program - * @param includePeSpecifiedPdbPath to also check the PE-header-specified PDB path - * @param symbolsRepositoryDir location of the local symbols repository (can be null) - * @param fileType type of file to search for (can be null) - * @return matching PDB file (or null, if not found) - */ - public static File findPDB(PdbProgramAttributes pdbAttributes, - boolean includePeSpecifiedPdbPath, File symbolsRepositoryDir, PdbFileType fileType) { - - // Store potential names of PDB files and potential locations of those files, - // so that all possible combinations can be searched. - // LinkedHashSet is used when we need to preserve order - Set guidSubdirPaths = new HashSet<>(); - - String guidAgeString = pdbAttributes.getGuidAgeCombo(); - if (guidAgeString == null) { - return null; - } - - List potentialPdbNames = pdbAttributes.getPotentialPdbFilenames(); - for (String potentialName : potentialPdbNames) { - guidSubdirPaths.add(File.separator + potentialName + File.separator + guidAgeString); - } - - return checkPathsForPdb(symbolsRepositoryDir, guidSubdirPaths, potentialPdbNames, fileType, - pdbAttributes, includePeSpecifiedPdbPath); - } - - /** - * Check potential paths in a specific order. If the symbolsRepositoryPath parameter is - * supplied and the directory exists, that directory will be searched first for the - * matching PDB file. - * - * If the file type is supplied, then only that file type will be searched for. Otherwise, - * the search process depends on the current operating system that Ghidra is running from: - * - * - Windows: look in the symbolsRepositoryPath for a matching .pdb file. If one does not - * exist, look for a .pdb.xml file in symbolsRepositoryPath. If not found, then - * search for a matching .pdb file, then .pdb.xml file, in other directories. - * - non-Windows: look in the symbolsRepositoryPath for a matching .pdb.xml file. If one does - * not exist, look for a .pdb file. If a .pdb file is found, return an error saying - * that it was found, but could not be processed. If no matches found in - * symbolsRepositoryPath, then look for .pdb.xml file, then .pdb.xml file in other - * directories. - * - * @param symbolsRepositoryDir location of the local symbols repository (can be null) - * @param guidSubdirPaths subdirectory paths (that include the PDB's GUID) that may contain - * a matching PDB - * @param potentialPdbNames all potential filenames for the PDB file(s) that match the program - * @param fileType file type to search for (can be null) - * @param pdbAttributes PDB attributes associated with the program - * @return matching PDB file, if found (else null) - */ - private static File checkPathsForPdb(File symbolsRepositoryDir, Set guidSubdirPaths, - List potentialPdbNames, PdbFileType fileType, - PdbProgramAttributes pdbAttributes, boolean includePeSpecifiedPdbPath) { - - File foundPdb = null; - Set symbolsRepoPaths = - getSymbolsRepositoryPaths(symbolsRepositoryDir, guidSubdirPaths); - Set predefinedPaths = - getPredefinedPaths(guidSubdirPaths, pdbAttributes, includePeSpecifiedPdbPath); - boolean fileTypeSpecified = (fileType != null); - boolean checkForXml; - - // If the file type is specified, look for that type of file only. - if (fileTypeSpecified) { - checkForXml = (fileType == PdbFileType.XML) ? true : false; - - foundPdb = checkForPDBorXML(symbolsRepoPaths, potentialPdbNames, checkForXml); - - if (foundPdb != null) { - return foundPdb; - } - - foundPdb = checkForPDBorXML(predefinedPaths, potentialPdbNames, checkForXml); - - return foundPdb; - } - - // If the file type is not specified, look for both file types, starting with the - // file type that's most appropriate for the Operating System (PDB for Windows, XML for - // non-Windows). - checkForXml = onWindows ? false : true; - - // Start by searching in symbolsRepositoryPath, if available. - if (!symbolsRepoPaths.isEmpty()) { - foundPdb = checkSpecificPathsForPdb(symbolsRepoPaths, potentialPdbNames, checkForXml); - } - - if (foundPdb != null) { - return foundPdb; - } - - return checkSpecificPathsForPdb(predefinedPaths, potentialPdbNames, checkForXml); - - } - - private static File checkSpecificPathsForPdb(Set paths, List potentialPdbNames, - boolean checkForXmlFirst) { - - File foundPdb = checkForPDBorXML(paths, potentialPdbNames, checkForXmlFirst); - - if (foundPdb != null) { - return foundPdb; - } - - foundPdb = checkForPDBorXML(paths, potentialPdbNames, !checkForXmlFirst); - - return foundPdb; - } - - private static Set getSymbolsRepositoryPaths(File symbolsRepositoryDir, - Set guidSubdirPaths) { - - Set symbolsRepoPaths = new LinkedHashSet<>(); - - // Collect sub-directories of the symbol repository that exist - if (symbolsRepositoryDir != null && symbolsRepositoryDir.isDirectory()) { - - for (String guidSubdir : guidSubdirPaths) { - File testDir = new File(symbolsRepositoryDir, guidSubdir); - if (testDir.isDirectory()) { - symbolsRepoPaths.add(testDir); - } - } - - // Check outer folder last - symbolsRepoPaths.add(symbolsRepositoryDir); - } - - return symbolsRepoPaths; - } - - // Get list of "paths we know about" to search for PDBs - private static Set getPredefinedPaths(Set guidSubdirPaths, - PdbProgramAttributes pdbAttributes, boolean includePeSpecifiedPdbPath) { - - Set predefinedPaths = new LinkedHashSet<>(); - - getPathsFromAttributes(pdbAttributes, includePeSpecifiedPdbPath, predefinedPaths); - getSymbolPaths(PdbLocator.DEFAULT_SYMBOLS_DIR, guidSubdirPaths, predefinedPaths); - getSymbolPaths(PdbLocator.WINDOWS_SYMBOLS_DIR, guidSubdirPaths, predefinedPaths); - getLibraryPaths(guidSubdirPaths, predefinedPaths); - - return predefinedPaths; - } - - private static void getLibraryPaths(Set guidSubdirPaths, Set predefinedPaths) { - String[] libraryPaths = LibrarySearchPathManager.getLibraryPaths(); - - File libFile, subDir; - - for (String path : libraryPaths) { - - if ((libFile = new File(path)).isDirectory()) { - predefinedPaths.add(libFile); - - // Check alternate locations - for (String guidSubdir : guidSubdirPaths) { - if ((subDir = new File(path, guidSubdir)).isDirectory()) { - predefinedPaths.add(subDir); - } - } - } - } - } - - private static void getSymbolPaths(File symbolsDir, Set guidSubdirPaths, - Set predefinedPaths) { - // Don't have to call .exists(), since .isDirectory() does that already - if (symbolsDir == null || !symbolsDir.isDirectory()) { - return; - } - predefinedPaths.add(symbolsDir); - - // Check alternate locations - String specialPdbPath = symbolsDir.getAbsolutePath(); - - for (String guidSubdir : guidSubdirPaths) { - File testDir = new File(specialPdbPath + guidSubdir); - if (testDir.isDirectory()) { - predefinedPaths.add(testDir); - } - } - } - - private static void getPathsFromAttributes(PdbProgramAttributes pdbAttributes, - boolean includePeSpecifiedPdbPath, Set predefinedPaths) { - if (pdbAttributes != null) { - - String currentPath = pdbAttributes.getPdbFile(); - - if (currentPath != null && includePeSpecifiedPdbPath) { - File parentDir = new File(currentPath).getParentFile(); - - if (parentDir != null && parentDir.exists()) { - predefinedPaths.add(parentDir); - } - } - - currentPath = pdbAttributes.getExecutablePath(); - - if (currentPath != null && !currentPath.equals("unknown")) { - File parentDir = new File(currentPath).getParentFile(); - - if (parentDir != null && parentDir.exists()) { - predefinedPaths.add(parentDir); - } - } - } - } - - /** - * Returns the first PDB-type file found. Assumes list of potentialPdbDirs is in the order - * in which the directories should be searched. - * - * @param potentialPdbDirs potential PDB directories - * @param potentialPdbNames potential PDB names - * @param findXML - if true, only searches for the .pdb.xml version of the .pdb file - * @return the first file found - */ - private static File checkForPDBorXML(Set potentialPdbDirs, List potentialPdbNames, - boolean findXML) { - - File pdb; - - for (File pdbPath : potentialPdbDirs) { - - for (String filename : potentialPdbNames) { - - if (findXML) { - pdb = new File(pdbPath, filename + PdbFileType.XML.toString()); - } - else { - pdb = new File(pdbPath, filename); - } - - // Note: isFile() also checks for existence - if (pdb.isFile()) { - return pdb; - } - } - } - - return null; - } - PdbDataTypeParser getDataTypeParser() { if (program == null) { throw new AssertException("Parser was not constructed with program"); diff --git a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb2/pdbreader/PdbIdentifiers.java b/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb2/pdbreader/PdbIdentifiers.java index cc7ac1ddfc..996877ceab 100644 --- a/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb2/pdbreader/PdbIdentifiers.java +++ b/Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb2/pdbreader/PdbIdentifiers.java @@ -15,12 +15,14 @@ */ package ghidra.app.util.bin.format.pdb2.pdbreader; +import java.util.Objects; + import ghidra.app.util.datatype.microsoft.GUID; /** * This class holds fields used to identify a PDB. *

    - * These are Version, Signature, Age, and GUID. Som identifiers can be null if not found in + * These are Version, Signature, Age, and GUID. Some identifiers can be null if not found in * the specific version of the PDB. */ public class PdbIdentifiers { @@ -38,7 +40,7 @@ public class PdbIdentifiers { * @param age age used to verify PDB against age stored in program * @param guid The GUID (can be null for older PDBs). */ - PdbIdentifiers(int version, int signature, int age, GUID guid, Processor processor) { + public PdbIdentifiers(int version, int signature, int age, GUID guid, Processor processor) { this.version = version; this.signature = signature; this.age = age; @@ -78,4 +80,33 @@ public class PdbIdentifiers { return guid; } + + @Override + public String toString() { + return ((guid != null) ? guid.toString() : String.format("%08X", signature)) + ", " + age + + ", " + version + ", " + processor; + } + + @Override + public int hashCode() { + return Objects.hash(age, guid, processor, signature, version); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + PdbIdentifiers other = (PdbIdentifiers) obj; + return age == other.age && Objects.equals(guid, other.guid) && + processor == other.processor && signature == other.signature && + version == other.version; + } + } diff --git a/Ghidra/Features/PDB/src/main/java/pdb/AskPdbOptionsDialog.java b/Ghidra/Features/PDB/src/main/java/pdb/AskPdbOptionsDialog.java deleted file mode 100644 index 5fd619fe33..0000000000 --- a/Ghidra/Features/PDB/src/main/java/pdb/AskPdbOptionsDialog.java +++ /dev/null @@ -1,128 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package pdb; - -import java.awt.BorderLayout; -import java.awt.Component; - -import javax.swing.*; - -import docking.DialogComponentProvider; -import docking.DockingWindowManager; -import docking.widgets.combobox.GComboBox; -import ghidra.app.util.bin.format.pdb.PdbParser; -import ghidra.app.util.pdb.pdbapplicator.PdbApplicatorControl; -import ghidra.util.layout.PairLayout; - -class AskPdbOptionsDialog extends DialogComponentProvider { - - private boolean isCanceled; - - private boolean useMsDiaParser; - private PdbApplicatorControl control = PdbApplicatorControl.ALL; - - /** - * Popup PDB loader options - * @param parent parent component or null - * @param isPdbFile true if file to be loaded is a PDB file, false - * if MsDia XML file. - */ - AskPdbOptionsDialog(Component parent, boolean isPdbFile) { - super("Load PDB Options", true, true, true, false); - - JPanel panel = new JPanel(new BorderLayout(10, 10)); - panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - - JPanel optionsPanel = new JPanel(new PairLayout(10, 10)); - - final GComboBox controlCombo = - new GComboBox<>(PdbApplicatorControl.values()); - controlCombo.setSelectedItem(PdbApplicatorControl.ALL); - controlCombo.addActionListener(e -> { - control = (PdbApplicatorControl) controlCombo.getSelectedItem(); - }); - - optionsPanel.add(new JLabel("PDB Parser:")); - - if (isPdbFile) { - useMsDiaParser = false; // Use PDB Universal by default - if (PdbParser.onWindows) { - final GComboBox combo = - new GComboBox<>(new String[] { "PDB Universal", "PDB MSDIA" }); - combo.setSelectedIndex(0); - controlCombo.setEnabled(!useMsDiaParser); - combo.addActionListener(e -> { - useMsDiaParser = (combo.getSelectedIndex() == 1); - controlCombo.setEnabled(!useMsDiaParser); - if (useMsDiaParser) { - controlCombo.setSelectedItem(PdbApplicatorControl.ALL); - } - }); - optionsPanel.add(combo); - } - else { - useMsDiaParser = false; - JLabel label = new JLabel("PDB Universal"); - //label.setForeground(Color.red); // set color to emphasize prototype status - optionsPanel.add(label); - } - } - else { - useMsDiaParser = true; // XML file only supported by MsDia parser - return; // no interaction currently required - } - - optionsPanel.add(new JLabel("Control:")); - optionsPanel.add(controlCombo); - - panel.add(optionsPanel, BorderLayout.CENTER); - - addWorkPanel(panel); - - addApplyButton(); - addCancelButton(); - - setDefaultButton(applyButton); - setRememberSize(false); - - DockingWindowManager.showDialog(parent, AskPdbOptionsDialog.this); - } - - @Override - protected void applyCallback() { - isCanceled = false; - close(); - } - - @Override - protected void cancelCallback() { - isCanceled = true; - close(); - } - - boolean isCanceled() { - return isCanceled; - } - - boolean useMsDiaParser() { - return useMsDiaParser; - } - - PdbApplicatorControl getApplicatorControl() { - return control; - } - -} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/AskPdbUrlDialog.java b/Ghidra/Features/PDB/src/main/java/pdb/AskPdbUrlDialog.java deleted file mode 100644 index 015bc2bece..0000000000 --- a/Ghidra/Features/PDB/src/main/java/pdb/AskPdbUrlDialog.java +++ /dev/null @@ -1,230 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package pdb; - -import java.awt.*; -import java.awt.event.*; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Scanner; - -import javax.swing.*; - -import docking.DialogComponentProvider; -import docking.DockingWindowManager; -import docking.widgets.dialogs.ObjectChooserDialog; -import docking.widgets.label.GDLabel; -import generic.jar.ResourceFile; -import generic.util.WindowUtilities; -import ghidra.framework.Application; -import ghidra.framework.preferences.Preferences; -import ghidra.util.MessageType; - -public class AskPdbUrlDialog extends DialogComponentProvider { - - private boolean isCanceled; - private JLabel label; - private JTextField textField; - private KeyListener keyListener; - private List choices = null; - - protected AskPdbUrlDialog(String dialogTitle, String message) { - this(null, dialogTitle, message, null); - } - - public AskPdbUrlDialog(String dialogTitle, String message, Object defaultValue) { - this(null, dialogTitle, message, defaultValue); - } - - public AskPdbUrlDialog(Component parent, String title, String message) { - this(parent, title, message, null); - } - - public AskPdbUrlDialog(final Component parent, String title, String message, - Object defaultValue) { - super(title, true, true, true, false); - - // create the key listener all the text fields will use - keyListener = new KeyAdapter() { - @Override - public void keyPressed(KeyEvent e) { - int keyCode = e.getKeyCode(); - if (keyCode == KeyEvent.VK_ENTER) { - okCallback(); - } - } - }; - - JPanel panel = new JPanel(new BorderLayout(10, 10)); - panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - - label = new GDLabel(message); - panel.add(label, BorderLayout.WEST); - - textField = new JTextField(40); - textField.setName("JTextField");//for JUnits... - textField.addKeyListener(keyListener); - textField.setText(defaultValue == null ? "" : defaultValue.toString()); - textField.selectAll(); - panel.add(textField, BorderLayout.CENTER); - - if (urlFileAvailable()) { - JButton urlButton = new JButton("Choose from known URLs"); - urlButton.addActionListener(e -> urlCallback()); - - panel.add(urlButton, BorderLayout.EAST); - } - - addWorkPanel(panel); - - addOKButton(); - addCancelButton(); - - setDefaultButton(okButton); - setRememberSize(false); - - DockingWindowManager.showDialog(parent, AskPdbUrlDialog.this); - } - - @Override - protected void addOKButton() { - okButton = new JButton("Download from URL"); - okButton.setMnemonic('K'); - okButton.setName("OK"); - okButton.addActionListener(e -> okCallback()); - addButton(okButton); - } - - private boolean urlFileAvailable() { - List urlFiles = Application.findFilesByExtensionInApplication(".pdburl"); - - if (urlFiles.size() == 0) { - return false; - } - - try { - InputStream urlFileContents = null; - String currentLine; - choices = new ArrayList<>(); - - for (ResourceFile urlFile : urlFiles) { - urlFileContents = urlFile.getInputStream(); - - Scanner scanner = new Scanner(urlFileContents); - try { - while (scanner.hasNextLine()) { - - currentLine = scanner.nextLine(); - - // Find first comma, split on that - int commaIndex = currentLine.indexOf(','); - - if (commaIndex > -1) { - choices.add(new URLChoice(currentLine.substring(0, commaIndex).trim(), - currentLine.substring(commaIndex + 1).trim())); - } - } - } - finally { - scanner.close(); - } - } - } - catch (IOException ioe) { - return false; - } - return true; - } - - private void saveCurrentDimensions() { - Rectangle bounds = getBounds(); - Window window = WindowUtilities.windowForComponent(getComponent()); - - if (window != null) { - Point location = window.getLocation(); - bounds.x = location.x; - bounds.y = location.y; - } - - StringBuffer buffer = new StringBuffer(); - buffer.append(bounds.x).append(":"); - buffer.append(bounds.y).append(":"); - buffer.append(bounds.width).append(":"); - buffer.append(bounds.height).append(":"); - Preferences.setProperty("Ask Dialog Bounds", buffer.toString()); - } - - public Object getValue() { - return textField.getText(); - } - - @Override - protected void okCallback() { - isCanceled = false; - if (textField.getText().length() == 0) { - setStatusText("Please enter a valid URL."); - return; - } - saveCurrentDimensions(); - close(); - } - - @Override - protected void cancelCallback() { - isCanceled = true; - saveCurrentDimensions(); - close(); - } - - private void urlCallback() { - - ObjectChooserDialog urlDialog = new ObjectChooserDialog<>("Choose a URL", - URLChoice.class, choices, "getNetwork", "getUrl"); - - DockingWindowManager activeInstance = DockingWindowManager.getActiveInstance(); - activeInstance.showDialog(urlDialog); - - URLChoice pickedUrl = urlDialog.getSelectedObject(); - - if (pickedUrl != null) { - textField.setText(pickedUrl.getUrl()); - - if (pickedUrl.getNetwork().equalsIgnoreCase("internet")) { - setStatusText( - "WARNING: Check your organization's security policy before downloading files from the internet.", - MessageType.ERROR); - } - else { - setStatusText(null); - } - } - } - - public boolean isCanceled() { - return isCanceled; - } - - public String getValueAsString() { - Object val = getValue(); - if ("".equals(val)) { - return null; - } - return val != null ? val.toString() : null; - } - -} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/LoadPdbTask.java b/Ghidra/Features/PDB/src/main/java/pdb/LoadPdbTask.java index 985a0fb2c7..4940348759 100644 --- a/Ghidra/Features/PDB/src/main/java/pdb/LoadPdbTask.java +++ b/Ghidra/Features/PDB/src/main/java/pdb/LoadPdbTask.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import docking.DockingWindowManager; -import docking.widgets.OptionDialog; import docking.widgets.dialogs.MultiLineMessageDialog; import ghidra.app.plugin.core.analysis.*; import ghidra.app.plugin.core.datamgr.archive.DuplicateIdException; @@ -29,8 +28,6 @@ import ghidra.app.util.bin.format.pdb.PdbException; import ghidra.app.util.bin.format.pdb.PdbParser; import ghidra.app.util.bin.format.pdb2.pdbreader.*; import ghidra.app.util.importer.MessageLog; -import ghidra.app.util.pdb.PdbLocator; -import ghidra.app.util.pdb.PdbProgramAttributes; import ghidra.app.util.pdb.pdbapplicator.*; import ghidra.framework.options.Options; import ghidra.program.model.address.AddressSetView; @@ -45,19 +42,21 @@ class LoadPdbTask extends Task { private final Program program; private final boolean useMsDiaParser; private final PdbApplicatorControl control; // PDB Universal Parser only + private boolean debugLogging; LoadPdbTask(Program program, File pdbFile, boolean useMsDiaParser, PdbApplicatorControl control, - DataTypeManagerService service) { + boolean debugLogging, DataTypeManagerService service) { super("Load PDB", true, false, true, true); this.program = program; this.pdbFile = pdbFile; this.useMsDiaParser = useMsDiaParser; this.control = control; + this.debugLogging = debugLogging; this.service = service; } @Override - public void run(final TaskMonitor monitor) { + public void run(TaskMonitor monitor) { WrappingTaskMonitor wrappedMonitor = new WrappingTaskMonitor(monitor) { @Override @@ -134,7 +133,7 @@ class LoadPdbTask extends Task { private boolean parseWithMsDiaParser(MessageLog log, TaskMonitor monitor) throws IOException, CancelledException { - PdbParser parser = new PdbParser(pdbFile, program, service, true, monitor); + PdbParser parser = new PdbParser(pdbFile, program, service, true, true, monitor); try { parser.parse(); parser.openDataTypeArchives(); @@ -147,44 +146,19 @@ class LoadPdbTask extends Task { return false; } - // NOTE: OptionDialog will not display an empty line - private static final String BLANK_LINE = " \n"; - private boolean parseWithNewParser(MessageLog log, TaskMonitor monitor) throws IOException, CancelledException { + PdbLog.setEnabled(debugLogging); + PdbReaderOptions pdbReaderOptions = new PdbReaderOptions(); // use defaults PdbApplicatorOptions pdbApplicatorOptions = new PdbApplicatorOptions(); pdbApplicatorOptions.setProcessingControl(control); - PdbProgramAttributes programAttributes = new PdbProgramAttributes(program); - try (AbstractPdb pdb = ghidra.app.util.bin.format.pdb2.pdbreader.PdbParser.parse( pdbFile.getAbsolutePath(), pdbReaderOptions, monitor)) { - - PdbIdentifiers identifiers = pdb.getIdentifiers(); - if (!PdbLocator.verifyPdbSignature(programAttributes, identifiers)) { - - StringBuilder builder = new StringBuilder(); - builder.append("Selected PDB does not match program's PDB specification!\n"); - builder.append(BLANK_LINE); - builder.append("Program's PDB specification:\n"); - builder.append(PdbLocator.formatPdbIdentifiers(programAttributes)); - builder.append(BLANK_LINE); - builder.append("Selected PDB file specification:\n"); - builder.append( - PdbLocator.formatPdbIdentifiers(pdbFile.getAbsolutePath(), identifiers)); - builder.append(BLANK_LINE); - builder.append("Do you wish to force load this PDB?"); - - if (OptionDialog.YES_OPTION != OptionDialog.showYesNoDialog(null, - "Confirm PDB Load", builder.toString())) { - return false; - } - } - monitor.setMessage("PDB: Parsing " + pdbFile + "..."); pdb.deserialize(monitor); PdbApplicator applicator = new PdbApplicator(pdbFile.getAbsolutePath(), pdb); diff --git a/Ghidra/Features/PDB/src/main/java/pdb/PdbInitializer.java b/Ghidra/Features/PDB/src/main/java/pdb/PdbInitializer.java deleted file mode 100644 index 3ec5f94b63..0000000000 --- a/Ghidra/Features/PDB/src/main/java/pdb/PdbInitializer.java +++ /dev/null @@ -1,31 +0,0 @@ -/* ### - * IP: GHIDRA - * REVIEWED: YES - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package pdb; - -import ghidra.app.util.bin.format.pdb.*; -import ghidra.framework.*; - -public class PdbInitializer implements ModuleInitializer { - public void run() { - PluggableServiceRegistry.registerPluggableService(PdbFactory.class, - new GhidraPdbFactory()); - } - @Override - public String getName() { - return "PDB Support Module"; - } -} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/PdbPlugin.java b/Ghidra/Features/PDB/src/main/java/pdb/PdbPlugin.java index a8b9729860..c3e31a9e13 100644 --- a/Ghidra/Features/PDB/src/main/java/pdb/PdbPlugin.java +++ b/Ghidra/Features/PDB/src/main/java/pdb/PdbPlugin.java @@ -16,30 +16,33 @@ package pdb; import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; import javax.swing.SwingConstants; -import docking.action.MenuData; +import docking.action.builder.ActionBuilder; +import docking.tool.ToolConstants; import docking.widgets.OptionDialog; -import docking.widgets.filechooser.GhidraFileChooser; -import docking.widgets.filechooser.GhidraFileChooserMode; import ghidra.app.CorePluginPackage; import ghidra.app.context.ProgramActionContext; -import ghidra.app.context.ProgramContextAction; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.core.analysis.AutoAnalysisManager; +import ghidra.app.plugin.core.analysis.PdbAnalyzerCommon; import ghidra.app.services.DataTypeManagerService; -import ghidra.app.util.bin.format.pdb.PdbParser; -import ghidra.app.util.pdb.pdbapplicator.PdbApplicatorControl; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.framework.preferences.Preferences; import ghidra.program.model.listing.Program; -import ghidra.program.util.GhidraProgramUtilities; import ghidra.util.HelpLocation; import ghidra.util.Msg; -import ghidra.util.filechooser.ExtensionFileFilter; -import ghidra.util.task.TaskBuilder; -import ghidra.util.task.TaskLauncher; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.*; +import pdb.symbolserver.*; +import pdb.symbolserver.ui.ConfigPdbDialog; +import pdb.symbolserver.ui.LoadPdbDialog; +import pdb.symbolserver.ui.LoadPdbDialog.LoadPdbResults; //@formatter:off @PluginInfo( @@ -51,9 +54,14 @@ import ghidra.util.task.TaskLauncher; ) //@formatter:on public class PdbPlugin extends Plugin { + private static final String PDB_SYMBOL_SERVER_OPTIONS = "PdbSymbolServer"; + private static final String SYMBOL_STORAGE_DIR_OPTION = + PDB_SYMBOL_SERVER_OPTIONS + ".Symbol_Storage_Directory"; + private static final String SYMBOL_SEARCH_PATH_OPTION = + PDB_SYMBOL_SERVER_OPTIONS + ".Symbol_Search_Path"; - private ProgramContextAction loadPdbAction; - private GhidraFileChooser pdbChooser; + // the name of the help directory under src/main/help/help/topics + public static final String PDB_PLUGIN_HELP_TOPIC = "Pdb"; public PdbPlugin(PluginTool tool) { super(tool); @@ -62,33 +70,34 @@ public class PdbPlugin extends Plugin { } private void createActions() { - loadPdbAction = new ProgramContextAction("Load PDB File", this.getName()) { + new ActionBuilder("Load PDB File", this.getName()) + .supportsDefaultToolContext(true) + .withContext(ProgramActionContext.class) + .validContextWhen(pac -> pac.getProgram() != null && + PdbAnalyzerCommon.canAnalyzeProgram(pac.getProgram())) + .menuPath(ToolConstants.MENU_FILE, "Load PDB File...") + .menuGroup("Import PDB", "3") + .helpLocation(new HelpLocation(PDB_PLUGIN_HELP_TOPIC, "Load PDB File")) + .onAction(pac -> loadPDB(pac)) + .buildAndInstall(tool); - @Override - public boolean isEnabledForContext(ProgramActionContext context) { - return context.getProgram() != null; - } - - @Override - protected void actionPerformed(ProgramActionContext programContext) { - loadPDB(); - } - }; - - MenuData menuData = - new MenuData(new String[] { "&File", "Load PDB File..." }, null, "Import PDB"); - menuData.setMenuSubGroup("3"); // below the major actions in the "Import/Export" group - loadPdbAction.setMenuBarData(menuData); - - loadPdbAction.setEnabled(false); - loadPdbAction.setHelpLocation(new HelpLocation("ImporterPlugin", loadPdbAction.getName())); - tool.addAction(loadPdbAction); + new ActionBuilder("Symbol Server Config", this.getName()) + .menuPath(ToolConstants.MENU_EDIT, "Symbol Server Config") + .menuGroup(ToolConstants.TOOL_OPTIONS_MENU_GROUP) + .helpLocation(new HelpLocation(PDB_PLUGIN_HELP_TOPIC, "Symbol Server Config")) + .onAction(ac -> configPDB()) + .buildAndInstall(tool); } - private void loadPDB() { - Program program = GhidraProgramUtilities.getCurrentProgram(tool); - AutoAnalysisManager aam = AutoAnalysisManager.getAnalysisManager(program); - if (aam.isAnalyzing()) { + private void configPDB() { + ConfigPdbDialog.showSymbolServerConfig(); + } + + private void loadPDB(ProgramActionContext pac) { + Program program = pac.getProgram(); + AutoAnalysisManager currentAutoAnalysisManager = + AutoAnalysisManager.getAnalysisManager(program); + if (currentAutoAnalysisManager.isAnalyzing()) { Msg.showWarn(getClass(), null, "Load PDB", "Unable to load PDB file while analysis is running."); return; @@ -110,26 +119,17 @@ public class PdbPlugin extends Plugin { } try { - File pdb = getPdbFile(program); - if (pdb == null) { + LoadPdbResults loadPdbResults = LoadPdbDialog.choosePdbForProgram(program); + if (loadPdbResults == null) { tool.setStatusInfo("Loading PDB was cancelled."); return; } - boolean isPdbFile = pdb.getName().toLowerCase().endsWith(".pdb"); - - AskPdbOptionsDialog optionsDialog = new AskPdbOptionsDialog(null, isPdbFile); - if (optionsDialog.isCanceled()) { - return; - } - - boolean useMsDiaParser = optionsDialog.useMsDiaParser(); - PdbApplicatorControl control = optionsDialog.getApplicatorControl(); - tool.setStatusInfo(""); - DataTypeManagerService service = tool.getService(DataTypeManagerService.class); - if (service == null) { + DataTypeManagerService dataTypeManagerService = + tool.getService(DataTypeManagerService.class); + if (dataTypeManagerService == null) { Msg.showWarn(getClass(), null, "Load PDB", "Unable to locate DataTypeService in the current tool."); return; @@ -138,34 +138,122 @@ public class PdbPlugin extends Plugin { // note: We intentionally use a 0-delay here. Our underlying task may show modal // dialog prompts. We want the task progress dialog to be showing before any // prompts appear. - - LoadPdbTask task = new LoadPdbTask(program, pdb, useMsDiaParser, control, service); - TaskBuilder.withTask(task) + LoadPdbTask loadPdbTask = new LoadPdbTask(program, loadPdbResults.pdbFile, + loadPdbResults.useMsDiaParser, loadPdbResults.control, + loadPdbResults.debugLogging, dataTypeManagerService); + TaskBuilder.withTask(loadPdbTask) .setStatusTextAlignment(SwingConstants.LEADING) .setLaunchDelay(0); - new TaskLauncher(task, null, 0); + new TaskLauncher(loadPdbTask, null, 0); } catch (Exception pe) { Msg.showError(getClass(), null, "Error Loading PDB", pe.getMessage(), pe); } } - private File getPdbFile(Program program) { - File pdbFile = PdbParser.findPDB(program); - if (pdbChooser == null) { - pdbChooser = new GhidraFileChooser(tool.getToolFrame()); - pdbChooser.setTitle("Select PDB file to load:"); - pdbChooser.setApproveButtonText("Select PDB"); - pdbChooser.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY); - pdbChooser.setFileFilter(new ExtensionFileFilter(new String[] { "pdb", "xml" }, - "Program Database Files and PDB XML Representations")); - } + //------------------------------------------------------------------------------------------------------- - if (pdbFile != null) { - pdbChooser.setSelectedFile(pdbFile); - } + /** + * Searches the currently configured symbol server paths for a Pdb symbol file. + * + * @param program the program associated with the requested pdb file + * @param findOptions options that control how to search for the symbol file + * @param monitor a {@link TaskMonitor} that allows the user to cancel + * @return a File that points to the found Pdb symbol file, or null if no file was found + */ + public static File findPdb(Program program, Set findOptions, TaskMonitor monitor) { - File selectedFile = pdbChooser.getSelectedFile(); - return selectedFile; + try { + SymbolFileInfo symbolFileInfo = SymbolFileInfo.fromMetadata(program.getMetadata()); + if (symbolFileInfo == null) { + return null; + } + // make a copy and add in the ONLY_FIRST_RESULT option + findOptions = findOptions.isEmpty() ? EnumSet.noneOf(FindOption.class) + : EnumSet.copyOf(findOptions); + findOptions.add(FindOption.ONLY_FIRST_RESULT); + + SymbolServerInstanceCreatorContext temporarySymbolServerInstanceCreatorContext = + SymbolServerInstanceCreatorRegistry.getInstance().getContext(program); + + SymbolServerService temporarySymbolServerService = + getSymbolServerService(temporarySymbolServerInstanceCreatorContext); + + List results = + temporarySymbolServerService.find(symbolFileInfo, findOptions, monitor); + if (!results.isEmpty()) { + return temporarySymbolServerService.getSymbolFile(results.get(0), monitor); + } + } + catch (CancelledException e) { + // ignore + } + catch (IOException e) { + Msg.error(PdbPlugin.class, "Error getting symbol file", e); + } + return null; + } + + /** + * Returns a new instance of a {@link SymbolServerService} configured with values from the + * application's preferences, defaulting to a minimal instance if there is no config. + * + * @param symbolServerInstanceCreatorContext an object that provides the necessary context to + * the SymbolServerInstanceCreatorRegistry to create the SymbolServers that are listed in the + * config values + * @return a new {@link SymbolServerService} instance, never null + */ + public static SymbolServerService getSymbolServerService( + SymbolServerInstanceCreatorContext symbolServerInstanceCreatorContext) { + SymbolServer temporarySymbolServer = + symbolServerInstanceCreatorContext.getSymbolServerInstanceCreatorRegistry() + .newSymbolServer(Preferences.getProperty(SYMBOL_STORAGE_DIR_OPTION, "", true), + symbolServerInstanceCreatorContext); + SymbolStore symbolStore = + (temporarySymbolServer instanceof SymbolStore) ? (SymbolStore) temporarySymbolServer + : new SameDirSymbolStore(symbolServerInstanceCreatorContext.getRootDir()); + List symbolServers = + symbolServerInstanceCreatorContext.getSymbolServerInstanceCreatorRegistry() + .createSymbolServersFromPathList(getSymbolSearchPaths(), + symbolServerInstanceCreatorContext); + return new SymbolServerService(symbolStore, symbolServers); + } + + /** + * Persists the {@link SymbolStore} and {@link SymbolServer}s contained in the + * {@link SymbolServerService}. + * + * @param symbolServerService {@link SymbolServerService} to save, or null if clear p + * reference values + */ + public static void saveSymbolServerServiceConfig(SymbolServerService symbolServerService) { + if (symbolServerService != null) { + Preferences.setProperty(SYMBOL_STORAGE_DIR_OPTION, + symbolServerService.getSymbolStore().getName()); + + String path = symbolServerService.getSymbolServers() + .stream() + .map(SymbolServer::getName) + .collect(Collectors.joining(";")); + Preferences.setProperty(SYMBOL_SEARCH_PATH_OPTION, path); + } + else { + Preferences.setProperty(SYMBOL_STORAGE_DIR_OPTION, null); + Preferences.setProperty(SYMBOL_SEARCH_PATH_OPTION, null); + } + } + + private static List getSymbolSearchPaths() { + String searchPathStr = Preferences.getProperty(SYMBOL_SEARCH_PATH_OPTION, "", true); + + String[] pathParts = searchPathStr.split(";"); + List result = new ArrayList<>(); + for (String part : pathParts) { + part = part.trim(); + if (!part.isEmpty()) { + result.add(part); + } + } + return result; } } diff --git a/Ghidra/Features/PDB/src/main/java/pdb/PdbSymbolServerPlugin.java b/Ghidra/Features/PDB/src/main/java/pdb/PdbSymbolServerPlugin.java deleted file mode 100644 index 2e3037d425..0000000000 --- a/Ghidra/Features/PDB/src/main/java/pdb/PdbSymbolServerPlugin.java +++ /dev/null @@ -1,856 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package pdb; - -import java.io.*; -import java.net.*; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.List; -import java.util.Properties; - -import docking.action.MenuData; -import docking.widgets.OptionDialog; -import docking.widgets.filechooser.GhidraFileChooser; -import docking.widgets.filechooser.GhidraFileChooserMode; -import ghidra.app.CorePluginPackage; -import ghidra.app.context.ProgramActionContext; -import ghidra.app.context.ProgramContextAction; -import ghidra.app.plugin.PluginCategoryNames; -import ghidra.app.plugin.core.analysis.AutoAnalysisManager; -import ghidra.app.services.DataTypeManagerService; -import ghidra.app.util.bin.format.pdb.PdbException; -import ghidra.app.util.bin.format.pdb.PdbParser; -import ghidra.app.util.bin.format.pdb.PdbParser.PdbFileType; -import ghidra.app.util.pdb.PdbLocator; -import ghidra.app.util.pdb.PdbProgramAttributes; -import ghidra.app.util.pdb.pdbapplicator.PdbApplicatorControl; -import ghidra.framework.Application; -import ghidra.framework.plugintool.*; -import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.framework.preferences.Preferences; -import ghidra.net.http.HttpUtil; -import ghidra.program.model.listing.Program; -import ghidra.program.util.GhidraProgramUtilities; -import ghidra.util.*; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskLauncher; - -/** - * Plugin that allows users to download PDB files from a Symbol Server URL. - * - * PDB files can be of type .pdb, .pdb.xml, and .cab: - * - .pdb files are Microsoft's native representation of debug symbols - * - .pdb.xml files are representations of .pdb files using XML. Ghidra provides a script - * for users to transform .pdb files into .pdb.xml files. - * - .cab (cabinet) files are compressed .pdb files. A Symbol Server set up using Microsoft - * tools will allow download of .cab files, relying on the user to extract a .pdb from - * the .cab file. - * - * The Symbol Server can be a URL to a hosted file system or a server that was set up using Microsoft - * tools. This code will also take care of PKI authentication, if needed by the server. - */ -//@formatter:off -@PluginInfo( - status = PluginStatus.RELEASED, - packageName = CorePluginPackage.NAME, - category = PluginCategoryNames.COMMON, - shortDescription = "Download PDB Files from a Symbol Server", - description = "This plugin manages the downloading of PDB files from a Symbol Server." -) -//@formatter:on -public class PdbSymbolServerPlugin extends Plugin { - - private static final String symbolServerEnvVar = "_NT_SYMBOL_PATH"; - - private static final String PDB_URL_PROPERTY = "PDB Symbol Server"; - - private static String expectedPdbContentType = "application/octet-stream"; - private static String expectedXmlContentType = "text/xml"; - private static Properties urlProperties = null; - - // Store last-selected value(s) for askXxx methods - private static String serverUrl = null; - private static File localDir = null; - private PdbFileType fileType = PdbFileType.PDB; - private boolean includePePdbPath = false; - - enum RetrieveFileType { - PDB, XML, CAB - } - - enum ReturnPdbStatus { - DOWNLOADED, EXISTING, NOT_FOUND; - } - - public PdbSymbolServerPlugin(PluginTool tool) { - super(tool); - createActions(); - - urlProperties = new Properties(); - // Version # appears to be debugger version. 6.3.9600.17298 - urlProperties.setProperty("User-Agent", "Microsoft-Symbol-Server/6.3.9600.17298"); - } - - /** - * Sets the {@link PdbFileType} - * @param fileType the {@link PdbFileType} - */ - public void setPdbFileType(PdbFileType fileType) { - this.fileType = fileType; - } - - private void createActions() { - ProgramContextAction downloadPdbAction = - new ProgramContextAction("Download_PDB_File", this.getName()) { - - @Override - public boolean isEnabledForContext(ProgramActionContext context) { - return context.getProgram() != null; - } - - @Override - protected void actionPerformed(ProgramActionContext programContext) { - downloadPDB(); - } - }; - - MenuData menuData = - new MenuData(new String[] { "&File", "Download PDB File..." }, null, "Import PDB"); - menuData.setMenuSubGroup("4"); - downloadPdbAction.setMenuBarData(menuData); - - downloadPdbAction.setEnabled(false); - downloadPdbAction.setHelpLocation(new HelpLocation("Pdb", downloadPdbAction.getName())); - tool.addAction(downloadPdbAction); - } - - private void downloadPDB() { - Program program = GhidraProgramUtilities.getCurrentProgram(tool); - - try { - - PdbFileAndStatus returnPdb = getPdbFile(program); - - File returnedPdbFile = returnPdb.getPdbFile(); - - switch (returnPdb.getPdbStatus()) { - case NOT_FOUND: - Msg.showInfo(getClass(), null, "Error", "Could not download the " + fileType + - " file for this version of " + program.getName() + " from " + serverUrl); - break; - - case DOWNLOADED: - Msg.showInfo(getClass(), null, "File Retrieved", "Downloaded and saved file '" + - returnedPdbFile.getName() + "' to \n" + returnedPdbFile.getParent()); - // no break here, since we want it to continue - - case EXISTING: - tryToLoadPdb(returnedPdbFile, program); - break; - } - } - catch (CancelledException ce) { - tool.setStatusInfo("Downloading PDB from Symbol Server was cancelled."); - return; - } - catch (PdbException pe) { - Msg.showInfo(getClass(), null, "Error", "Error: " + pe.getMessage()); - } - catch (IOException ioe) { - Msg.showInfo(getClass(), null, "Error", - ioe.getClass().getSimpleName() + ": " + ioe.getMessage()); - - // If URL connection failed, then reset the dialog to show the default symbol server - // (instead of the last one we attempted to connect to). - if (ioe instanceof UnknownHostException) { - serverUrl = null; - } - } - } - - /** - * Retrieves PDB, using GUI to interact with user to get PDB and Symbol Server Information - * - * @param program program for which to retrieve the PDB file - * @return the retrieved PDB file (could be in .pdb or .xml form) - * @throws CancelledException upon user cancellation - * @throws IOException if an I/O issue occurred - * @throws PdbException if there was a problem with the PDB attributes - */ - private PdbFileAndStatus getPdbFile(Program program) - throws CancelledException, IOException, PdbException { - - try { - PdbProgramAttributes pdbAttributes = PdbParser.getPdbAttributes(program); - - if (pdbAttributes.getGuidAgeCombo() == null) { - throw new PdbException( - "Incomplete PDB information (GUID/Signature and/or age) associated with this program.\n" + - "Either the program is not a PE, or it was not compiled with debug information."); - } - - // 1. Ask if user wants .pdb or .pdb.xml file - fileType = askForFileExtension(); - - // 1.5 Ask if should search PE-specified PDB path. - includePePdbPath = askIncludePeHeaderPdbPath(); - - String symbolEnv = System.getenv(symbolServerEnvVar); - if (symbolEnv != null) { - parseSymbolEnv(symbolEnv); - } - - // 2. Ask for local storage location - localDir = askForLocalStorageLocation(); - - // 3. See if PDB can be found locally - File pdbFile = PdbParser.findPDB(pdbAttributes, includePePdbPath, localDir, fileType); - - // 4. If not found locally, ask if it should be retrieved - if (pdbFile != null && pdbFile.getName().endsWith(fileType.toString())) { - - String htmlString = - HTMLUtilities.toWrappedHTML("Found potential* matching PDB at: \n " + - pdbFile.getAbsolutePath() + "\n\n* Match determined by file name only; " + - "not vetted for matching GUID/version." + - "\n\nContinue with download?\n\n" + - "(downloaded file will be saved in a directory of the form " + - localDir.getAbsolutePath() + File.separator + "<pdbFilename>" + - File.separator + "<GUID>" + File.separator + ")"); - - // Warn that there is already a matching file - int response = - OptionDialog.showYesNoDialog(null, "Potential Matching PDB Found", htmlString); - - switch (response) { - case 0: - // User cancelled - throw new CancelledException(); - - case 1: - // Yes -- do nothing here - break; - - case 2: - // No - return new PdbFileAndStatus(pdbFile, ReturnPdbStatus.EXISTING); - - default: - // do nothing - } - } - - // 5. Ask for Symbol Server location - serverUrl = askForSymbolServerUrl(); - - // Fix up URL - if (!serverUrl.endsWith("/")) { - serverUrl += "/"; - } - - File downloadedPdb = attemptToDownloadPdb(pdbAttributes, serverUrl, localDir); - - if (downloadedPdb != null) { - return new PdbFileAndStatus(downloadedPdb, ReturnPdbStatus.DOWNLOADED); - } - - return new PdbFileAndStatus(); - } - finally { - // Store the dialog choices - Preferences.store(); - } - } - - private void parseSymbolEnv(String envString) { - - // Expect the environment string to be of the form: - // srv*[local cache]*[private symbol server]*https://msdl.microsoft.com/download/symbols - // srv*c:\symbols*https://msdl.microsoft.com/download/symbols - - if (!envString.startsWith("srv") && !envString.startsWith("SRV")) { - return; - } - - String[] envParts = envString.split("\\*"); - - if (envParts.length < 3) { - return; - } - - File storageDir = new File(envParts[1]); - if (storageDir.isDirectory()) { - localDir = storageDir; - } - - serverUrl = envParts[2]; - - Msg.info(getClass(), "Using server URL: " + serverUrl); - } - - private PdbFileType askForFileExtension() throws CancelledException { - //@formatter:off - int choice = OptionDialog.showOptionDialog( - null, - "pdb or pdb.xml", - "Download a .pdb or .pdb.xml file?", - "PDB", - "XML"); - //@formatter:on - - if (choice == OptionDialog.CANCEL_OPTION) { - throw new CancelledException(); - } - return (choice == OptionDialog.OPTION_ONE) ? PdbFileType.PDB : PdbFileType.XML; - } - - private boolean askIncludePeHeaderPdbPath() throws CancelledException { - //@formatter:off - int choice = OptionDialog.showOptionDialog( - null, - "PE-specified PDB Path", - "Unsafe: Include PE-specified PDB Path in search for existing PDB", - "Yes", - "No"); - //@formatter:on - - if (choice == OptionDialog.CANCEL_OPTION) { - throw new CancelledException(); - } - return (choice == OptionDialog.OPTION_ONE); - } - - String askForSymbolServerUrl() throws CancelledException { - - AskPdbUrlDialog dialog; - String dialogResponse = null; - String storedURL; - - if (serverUrl != null) { - storedURL = serverUrl; - } - else { - storedURL = Preferences.getProperty(PDB_URL_PROPERTY); - - if (storedURL == null) { - storedURL = ""; - } - } - - while (dialogResponse == null) { - dialog = new AskPdbUrlDialog("Symbol Server URL", "What is the Symbol Server URL?", - storedURL); - - if (dialog.isCanceled()) { - throw new CancelledException(); - } - - dialogResponse = dialog.getValueAsString(); - - // Make sure user has included either 'http' or 'https' - if (!dialogResponse.startsWith("http")) { - Msg.showInfo(getClass(), null, "Incomplete URL", - "URL should start with either 'http' or 'https'."); - dialogResponse = null; - continue; - } - - // Make sure that URL has valid syntax - try { - new URL(dialogResponse); - } - catch (MalformedURLException malExc) { - Msg.showInfo(getClass(), null, "Malformed URL", malExc.toString()); - dialogResponse = null; - } - } - - Preferences.setProperty(PDB_URL_PROPERTY, dialogResponse); - - return dialogResponse; - } - - private File askForLocalStorageLocation() throws CancelledException { - - final GhidraFileChooser fileChooser = new GhidraFileChooser(tool.getActiveWindow()); - - // Need to store the variable in an array to allow the final variable to be reassigned. - // Using an array prevents the compiler from warning about "The final local variable - // cannot be assigned, since it is defined in an enclosing type." - final File[] chosenDir = new File[1]; - - File testDirectory = null; - - // localDir is not null if we already parsed the _NT_SYMBOL_PATH environment var - if (localDir != null) { - testDirectory = localDir; - } - else { - testDirectory = PdbLocator.getDefaultPdbSymbolsDir(); - } - - final File storedDirectory = testDirectory; - - Runnable r = () -> { - while (chosenDir[0] == null && !fileChooser.wasCancelled()) { - fileChooser.setSelectedFile(storedDirectory); - - fileChooser.setTitle("Select Location to Save Retrieved File"); - fileChooser.setApproveButtonText("OK"); - fileChooser.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); - chosenDir[0] = fileChooser.getSelectedFile(); - - if (chosenDir[0] != null) { - if (!chosenDir[0].exists()) { - Msg.showInfo(getClass(), null, "Directory does not exist", - "The directory '" + chosenDir[0].getAbsolutePath() + - "' does not exist. Please create it or choose a valid directory."); - chosenDir[0] = null; - } - else if (chosenDir[0].isFile()) { - Msg.showInfo(getClass(), null, "Invalid Directory", - "The location '" + chosenDir[0].getAbsolutePath() + - "' represents a file, not a directory. Please choose a directory."); - chosenDir[0] = null; - } - } - } - }; - SystemUtilities.runSwingNow(r); - - if (fileChooser.wasCancelled()) { - throw new CancelledException(); - } - - PdbLocator.setDefaultPdbSymbolsDir(chosenDir[0]); - - return chosenDir[0]; - } - - /** - * Attempt to download a file from a URL and save it to the specified location. - * - * @param fileUrl URL from which to download the file - * @param fileDestination location at which to save the downloaded file - * @return whether download/save succeeded - * @throws IOException if an I/O issue occurred - * @throws PdbException if issue with PKI certificate - */ - boolean retrieveFile(String fileUrl, File fileDestination) throws IOException, PdbException { - return retrieveFile(fileUrl, fileDestination, null); - } - - /** - * Attempt to download a file from a URL and save it to the specified location. - * - * @param fileUrl URL from which to download the file - * @param fileDestination location at which to save the downloaded file - * @param retrieveProperties optional HTTP request header values to be included (may be null) - * @return whether download/save succeeded - * @throws IOException if an I/O issue occurred - * @throws PdbException if issue with PKI certificate - */ - boolean retrieveFile(String fileUrl, File fileDestination, Properties retrieveProperties) - throws IOException, PdbException { - - String expectedContentType = - (fileType == PdbFileType.PDB) ? expectedPdbContentType : expectedXmlContentType; - - try { - String contentType = - HttpUtil.getFile(fileUrl, retrieveProperties, true, fileDestination); - - if (contentType != null && !contentType.equals(expectedContentType)) { - fileDestination.delete(); - return false; - } - } - catch (IOException ioe) { - - // No PKI Certificate installed - if (ioe.getMessage().equals("Forbidden")) { - throw new PdbException( - "PKI Certificate needed for user authentication.\nTo set a " + - "certificate, use the Project Window's 'Edit -> Set PKI Certificate' Action."); - } - - if (!ioe.getMessage().equals("Not Found")) { - throw ioe; - } - } - - return fileDestination.exists(); - - } - - /** - * Take given file and move it to the specified destination folder in the location - * <destination folder>/<pdbFilename>/>guidAgeString< (subfolders that do not - * already exist will be created). - * - * @param destinationFolder root folder to which the given file will be moved - * @param pdbFilename name of PDB file (subfolder with this name will be created under destination - * folder, if it doesn't already exist) - * @param guidAgeString guidAge string of the PDB (subfolder with this name will be created under - * <destination folder>/<pdbFilename> folder, if it doesn't already exist) - * @param downloadFilename name of final moved file (can be same as pdbFilename) - * @param tempFile actual file to be moved - * @return file that was moved (and optionally renamed) in its new location - * @throws IOException if there was an IO-related problem making the directory or moving the file - */ - File createSubFoldersAndMoveFile(File destinationFolder, String pdbFilename, - String guidAgeString, String downloadFilename, File tempFile) throws IOException { - - File pdbOuterSaveDir = makeDirectory(destinationFolder, pdbFilename); - File pdbInnerSaveDir = makeDirectory(pdbOuterSaveDir, guidAgeString); - - File finalDestFile = new File(pdbInnerSaveDir, downloadFilename); - - try { - Files.move(tempFile.toPath(), finalDestFile.toPath(), - StandardCopyOption.REPLACE_EXISTING); - } - catch (IOException e) { - tempFile.delete(); - throw new IOException("Could not save file: " + finalDestFile.getAbsolutePath()); - } - - return finalDestFile; - } - - private File makeDirectory(File parentFolder, String directoryName) throws IOException { - File newDir = new File(parentFolder, directoryName); - - if (newDir.isFile()) { - throw new IOException("Trying to create folder " + newDir.getAbsolutePath() + - ",\nbut it shares the same name as an existing file.\n" + - "Please try downloading PDB again, selecting a " + - "non-conflicting destination folder."); - } - - if (!newDir.isDirectory()) { - boolean madeDir = newDir.mkdir(); - if (!madeDir) { - throw new IOException( - "Trying to create parent folders to store PDB file. Could not create directory " + - newDir.getAbsolutePath() + "."); - } - } - - return newDir; - } - - /** - * Expand cabinet (.cab) files (Windows compressed format). - * - * When on Windows, use the 'expand' command (should already be included with the OS). - * When on Unix/Mac, use 'cabextract', which has been included with Ghidra. - * - * @param cabFile file to expand/uncompress - * @param targetFilename file to save uncompressed *.pdb to - * @return the file that was uncompressed - * @throws PdbException if failure with cabinet extraction - * @throws IOException if issue starting the {@link ProcessBuilder} - */ - File uncompressCabFile(File cabFile, String targetFilename) throws PdbException, IOException { - - String cabextractPath = null; - String[] cabextractCmdLine; - - if (PdbParser.onWindows) { - File cabextractExe = new File("C:\\Windows\\System32\\expand.exe"); - - if (!cabextractExe.exists()) { - throw new PdbException( - "Expected to find cabinet expansion utility 'expand.exe' in " + - cabextractExe.getParent()); - } - - cabextractPath = cabextractExe.getAbsolutePath(); - - // expand -R .cab -F: - // -R renames from .cab to .pdb - // -F specifies which files within cab to expand - cabextractCmdLine = new String[] { cabextractPath, "-R", cabFile.getAbsolutePath(), - "-F:" + targetFilename, cabFile.getParent() }; - } - else { - - // On Mac/Linux - try { - cabextractPath = Application.getOSFile("cabextract").getAbsolutePath(); - } - catch (FileNotFoundException e) { - throw new PdbException("Unable to find 'cabextract' executable."); - } - - // -q for quiet - // -d to specify where to extract to - // -F to specify filter pattern of file(s) to extract - cabextractCmdLine = new String[] { cabextractPath, "-q", "-d", cabFile.getParent(), - "-F", targetFilename, cabFile.getAbsolutePath() }; - } - - ProcessBuilder builder = new ProcessBuilder(cabextractCmdLine); - Process currentProcess = builder.start(); - - try { - int exitValue = currentProcess.waitFor(); - - if (exitValue != 0) { - throw new PdbException("Abnormal termination of 'cabextract' process."); - } - } - catch (InterruptedException ie) { - // do nothing - } - - // Look for the file - FilenameFilter pdbFilter = (dir, filename) -> { - String lowercaseName = filename.toLowerCase(); - return (lowercaseName.endsWith(fileType.toString())); - }; - - File[] files = cabFile.getParentFile().listFiles(pdbFilter); - if (files != null) { - for (File childFile : files) { - if (childFile.getName().equals(targetFilename)) { - return childFile; - } - } - } - - return null; - } - - /** - * Download a file, then move it to its final destination. URL for download is created by - * combining downloadURL and PDB file attributes. Final move destination is also determined - * by the PDB file attributes. - * - * @param pdbAttributes PDB attributes (GUID, age, potential PDB locations, etc.) - * @param downloadUrl Root URL to search for the PDB - * @param saveToLocation Final root directory to save the file - * @return the downloaded and moved file - * @throws IOException if an I/O issue occurred - * @throws PdbException if issue with PKI certificate or cabinet extraction - */ - private File attemptToDownloadPdb(PdbProgramAttributes pdbAttributes, String downloadUrl, - File saveToLocation) throws PdbException, IOException { - - // Get location of the user's 'temp' directory - String tempDirPath = System.getProperty("java.io.tmpdir"); - File tempDir = new File(tempDirPath); - - RetrieveFileType retrieveType = - (fileType == PdbFileType.XML) ? RetrieveFileType.XML : RetrieveFileType.PDB; - - // Attempt retrieval from connection (encrypted or non-encrypted are handled) by HttpUtil - File createdFile = downloadExtractAndMoveFile(pdbAttributes, downloadUrl, tempDir, - saveToLocation, retrieveType); - - if (createdFile != null) { - return createdFile; - } - - // If Microsoft-specific server, need to do more (i.e., filename will be named *.pd_ and in - // .cab format). Need to change http connection properties to be able to pull back file. - - // Attempt retrieval as if it was a Microsoft-specific URL - if (retrieveType == RetrieveFileType.PDB) { - return downloadExtractAndMoveFile(pdbAttributes, downloadUrl, tempDir, saveToLocation, - RetrieveFileType.CAB); - } - - return null; - } - - /** - * Download a file, then move it to its final destination. URL for download is created by - * combining downloadURL and PDB file attributes. Final move destination is also determined - * by the PDB file attributes. - * - * @param pdbAttributes PDB attributes (GUID, age, potential PDB locations, etc.) - * @param downloadUrl Root URL to search for the PDB - * @param tempSaveDirectory Temporary local directory to save downloaded file (which will be moved) - * @param finalSaveDirectory Final root directory to save the file - * @param retrieveFileType the {@link RetrieveFileType} - * @return the downloaded and moved file - * @throws IOException if an I/O issue occurred - * @throws PdbException if issue with PKI certificate or cabinet extraction - */ - File downloadExtractAndMoveFile(PdbProgramAttributes pdbAttributes, String downloadUrl, - File tempSaveDirectory, File finalSaveDirectory, RetrieveFileType retrieveFileType) - throws IOException, PdbException { - - // TODO: This should be performed by a monitored Task with ability to cancel - - String guidAgeString = pdbAttributes.getGuidAgeCombo(); - List potentialPdbFilenames = pdbAttributes.getPotentialPdbFilenames(); - File tempFile = null; - String tempFileExtension = (retrieveFileType == RetrieveFileType.CAB) ? "cab" : "pdb"; - - File returnFile = null; - - try { - - tempFile = new File(tempSaveDirectory, "TempPDB." + tempFileExtension); - - // Attempt retrieval from connection (encrypted or non-encrypted are handled) - for (String pdbFilename : potentialPdbFilenames) { - - String downloadFilename = pdbFilename; - String currentUrl = downloadUrl + pdbFilename + "/" + guidAgeString + "/"; - - boolean retrieveSuccess = false; - - switch (retrieveFileType) { - case CAB: - currentUrl += downloadFilename; - currentUrl = currentUrl.substring(0, currentUrl.length() - 1) + "_"; - retrieveSuccess = retrieveFile(currentUrl, tempFile, urlProperties); - - if (!retrieveSuccess) { - continue; - } - - File extractedFile = uncompressCabFile(tempFile, pdbFilename); - - if (extractedFile == null) { - throw new IOException( - "Unable to uncompress .cab file extracted for " + pdbFilename); - } - returnFile = extractedFile; - - break; - - case PDB: - currentUrl += downloadFilename; - retrieveSuccess = retrieveFile(currentUrl, tempFile); - - if (!retrieveSuccess) { - continue; - } - - returnFile = tempFile; - break; - - case XML: - downloadFilename += ".xml"; - currentUrl += downloadFilename; - retrieveSuccess = retrieveFile(currentUrl, tempFile); - - if (!retrieveSuccess) { - continue; - } - - returnFile = tempFile; - break; - } - - return createSubFoldersAndMoveFile(finalSaveDirectory, pdbFilename, guidAgeString, - downloadFilename, returnFile); - - } - } - finally { - if (tempFile != null && tempFile.exists()) { - tempFile.delete(); - } - } - return null; - } - - private void tryToLoadPdb(File downloadedPdb, Program currentProgram) { - - AutoAnalysisManager aam = AutoAnalysisManager.getAnalysisManager(currentProgram); - if (aam.isAnalyzing()) { - Msg.showWarn(getClass(), null, "Load PDB", - "Unable to load PDB file while analysis is running."); - return; - } - - boolean analyzed = - currentProgram.getOptions(Program.PROGRAM_INFO).getBoolean(Program.ANALYZED, false); - - String message = "Would you like to apply the following PDB:\n\n" + - downloadedPdb.getAbsolutePath() + "\n\n to " + currentProgram.getName() + "?"; - if (analyzed) { - message += "\n \nWARNING: Loading PDB after analysis has been performed may produce" + - "\npoor results. PDBs should generally be loaded prior to analysis or" + - "\nautomatically during auto-analysis."; - } - - String htmlString = HTMLUtilities.toWrappedHTML(message); - int response = OptionDialog.showYesNoDialog(null, "Load PDB?", htmlString); - if (response != OptionDialog.YES_OPTION) { - return; - } - - AskPdbOptionsDialog optionsDialog = - new AskPdbOptionsDialog(null, fileType == PdbFileType.PDB); - if (optionsDialog.isCanceled()) { - return; - } - - boolean useMsDiaParser = optionsDialog.useMsDiaParser(); - PdbApplicatorControl control = optionsDialog.getApplicatorControl(); - - tool.setStatusInfo(""); - - try { - DataTypeManagerService service = tool.getService(DataTypeManagerService.class); - if (service == null) { - Msg.showWarn(getClass(), null, "Load PDB", - "Unable to locate DataTypeService in the current tool."); - return; - } - - TaskLauncher - .launch( - new LoadPdbTask(currentProgram, downloadedPdb, useMsDiaParser, control, - service)); - } - catch (Exception pe) { - Msg.showError(getClass(), null, "Error", pe.getMessage()); - } - } - - class PdbFileAndStatus { - - private File pdbFile; - private ReturnPdbStatus pdbStatus; - - public PdbFileAndStatus() { - pdbFile = null; - pdbStatus = ReturnPdbStatus.NOT_FOUND; - } - - public PdbFileAndStatus(File pdbFile, ReturnPdbStatus pdbStatus) { - this.pdbFile = pdbFile; - this.pdbStatus = pdbStatus; - } - - public File getPdbFile() { - return pdbFile; - } - - public ReturnPdbStatus getPdbStatus() { - return pdbStatus; - } - } -} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/PdbUtils.java b/Ghidra/Features/PDB/src/main/java/pdb/PdbUtils.java new file mode 100644 index 0000000000..0d6548dfd2 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/PdbUtils.java @@ -0,0 +1,117 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb; + +import java.util.List; + +import java.io.*; + +import org.apache.commons.io.FilenameUtils; +import org.xml.sax.SAXException; + +import ghidra.app.util.bin.format.pdb2.pdbreader.*; +import ghidra.app.util.datatype.microsoft.GUID; +import ghidra.formats.gfilesystem.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import ghidra.xml.*; +import utilities.util.FileUtilities; + +public class PdbUtils { + + /** + * Attempts to extract {@link PdbIdentifiers} from the specified file, which + * can be either a pdb or pdb.xml file. + *

    + * + * @param file File to examine + * @param monitor {@link TaskMonitor}to allow cancel and progress + * @return new {@link PdbIdentifiers} instance with GUID/ID and age info, or null if + * not a valid pdb or pdb.xml file + */ + public static PdbIdentifiers getPdbIdentifiers(File file, TaskMonitor monitor) { + String extension = FilenameUtils.getExtension(file.getName()).toLowerCase(); + switch (extension) { + case "pdb": + try (AbstractPdb pdb = + PdbParser.parse(file.getPath(), new PdbReaderOptions(), monitor)) { + PdbIdentifiers identifiers = pdb.getIdentifiers(); + return identifiers; + } + catch (Exception e) { + return null; + } + case "xml": + XmlPullParser parser = null; + try { + parser = XmlPullParserFactory.create(file, null, false); + + XmlElement xmlelem = parser.peek(); + + if (!"pdb".equals(xmlelem.getName())) { + return null; + } + + String guidStr = xmlelem.getAttribute("guid"); + GUID guid = new GUID(guidStr); + int age = Integer.parseInt(xmlelem.getAttribute("age")); + + return new PdbIdentifiers(0, 0, age, guid, null); + } + catch (SAXException | IOException | RuntimeException e) { + // don't care, return null + return null; + } + finally { + if (parser != null) { + parser.dispose(); + } + } + default: + return null; + } + } + + /** + * Extracts a singleton file from a cab file that only has 1 file + * + * @param cabFile Compressed cab file that only has 1 file embedded in it + * @param destFile where to write the extracted file to + * @param monitor {@link TaskMonitor} to allow canceling + * @return original name of the file + * @throws CancelledException if cancelled + * @throws IOException if error reading / writing file or cab file has more than 1 file in it + */ + public static String extractSingletonCabToFile(File cabFile, File destFile, TaskMonitor monitor) + throws CancelledException, IOException { + FileSystemService fsService = FileSystemService.getInstance(); + FSRL cabFSRL = fsService.getLocalFSRL(cabFile); + try (GFileSystem fs = fsService.openFileSystemContainer(cabFSRL, monitor)) { + if (fs != null) { + List rootListing = fs.getListing(null); + if (rootListing.size() == 1) { + GFile f = rootListing.get(0); + try (InputStream is = fs.getInputStream(f, monitor)) { + FileUtilities.copyStreamToFile(is, destFile, false, monitor); + return f.getName(); + } + } + } + } + throw new IOException("Unable to find file to extract"); + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/URLChoice.java b/Ghidra/Features/PDB/src/main/java/pdb/URLChoice.java deleted file mode 100644 index 1e51892d52..0000000000 --- a/Ghidra/Features/PDB/src/main/java/pdb/URLChoice.java +++ /dev/null @@ -1,34 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package pdb; - -public class URLChoice { - private String network; - private String url; - - public URLChoice(String network, String url) { - this.network = network; - this.url = url; - } - - public String getNetwork() { - return network; - } - - public String getUrl() { - return url; - } -} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/AbstractSymbolServer.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/AbstractSymbolServer.java new file mode 100644 index 0000000000..bb05641817 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/AbstractSymbolServer.java @@ -0,0 +1,135 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.util.List; +import java.util.Set; + +import java.io.IOException; + +import org.apache.commons.io.FilenameUtils; + +import ghidra.util.Msg; +import ghidra.util.task.TaskMonitor; + +/** + * Common functionality of File and Http symbol servers. + */ +public abstract class AbstractSymbolServer implements SymbolServer { + protected static final String INDEX_TWO_FILENAME = "index2.txt"; + protected static final String PINGME_FILENAME = "pingme.txt"; // per MS custom + + protected int storageLevel = -1; + + @Override + public List find(SymbolFileInfo symbolFileInfo, Set options, + TaskMonitor monitor) { + initStorageLevelIfNeeded(monitor); + + try { + // "ke/kernelstuff.pdb/12345ABCFF0/" + String uniqueFileDir = getUniqueFileDir(symbolFileInfo); + + // "ke/kernelstuff.pdb/12345ABCFF0/kernelstuff.pdb" or + // "ke/kernelstuff.pdb/12345ABCFF0/kernelstuff.pd_" + String filePath = getFirstExists(uniqueFileDir, monitor, symbolFileInfo.getName(), + getCompressedFilename(symbolFileInfo)); + + return (filePath != null) + ? List.of(new SymbolFileLocation(filePath, this, symbolFileInfo)) + : List.of(); + } + catch (IOException ioe) { + Msg.warn(this, "Error searching for " + symbolFileInfo.getName(), ioe); + return List.of(); + } + } + + protected int detectStorageLevel(TaskMonitor monitor) { + return exists(INDEX_TWO_FILENAME, monitor) ? 2 : 1; + } + + protected void initStorageLevelIfNeeded(TaskMonitor monitor) { + if (storageLevel < 0) { + storageLevel = detectStorageLevel(monitor); + } + } + + protected String getFileDir(String filename) throws IOException { + switch (storageLevel) { + case 0: + return ""; + case 1: + return filename + "/"; + case 2: + if (filename.length() <= 2) { + throw new IOException( + "Symbol filename too short to store in two-level index: " + filename); + } + return filename.substring(0, 2).toLowerCase() + "/" + filename + "/"; + default: + throw new IllegalArgumentException( + "Unsupported Symbol Server storage level: " + storageLevel); + } + } + + protected String getUniqueFileDir(SymbolFileInfo symbolFileInfo) throws IOException { + switch (storageLevel) { + case 0: + return ""; + case 1: + case 2: + // "ke/kernelstuff.pdb/" or just "kernelstuff.pdb/" + String fileRoot = getFileDir(symbolFileInfo.getName()); + + // "ke/kernelstuff.pdb/12345ABCFF0/" + String uniqueFileDir = fileRoot + symbolFileInfo.getUniqueDirName() + "/"; + + return uniqueFileDir; + default: + throw new IllegalArgumentException( + "Unsupported Symbol Server storage level: " + storageLevel); + } + } + + protected String getFirstExists(String subDir, TaskMonitor monitor, String... filenames) { + for (String filename : filenames) { + String pathname = subDir + filename; + if (exists(pathname, monitor)) { + return pathname; + } + } + return null; + } + + static String makeCompressedExtension(String fileTypeExtension) { + return (!fileTypeExtension.isEmpty() + ? fileTypeExtension.substring(0, fileTypeExtension.length() - 1) + : "") + + "_"; + } + + static String getCompressedFilename(SymbolFileInfo symbolFileInfo) { + return FilenameUtils.getBaseName(symbolFileInfo.getName()) + "." + + makeCompressedExtension(FilenameUtils.getExtension(symbolFileInfo.getName())); + } + + static String getCompressedFilename(String filename) { + return FilenameUtils.getBaseName(filename) + "." + + makeCompressedExtension(FilenameUtils.getExtension(filename)); + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/DisabledSymbolServer.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/DisabledSymbolServer.java new file mode 100644 index 0000000000..dea6f2d56a --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/DisabledSymbolServer.java @@ -0,0 +1,127 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.util.List; +import java.util.Set; + +import java.io.IOException; + +import ghidra.util.task.TaskMonitor; + +/** + * A wrapper around a real symbol server that indicates that the symbol server has been disabled. + *

    + * Any find() operations will return an empty list, but file retrieval will still be passed through + * to the original symbol server instance. + */ +public class DisabledSymbolServer implements SymbolServer { + + private static String DISABLED_PREFIX = "disabled://"; + + /** + * Predicate that tests if the location string is an instance of a disabled location. + * + * @param loc location string + * @return boolean true if the string should be handled by the DisabledSymbolServer class + */ + public static boolean isDisabledSymbolServerLocation(String loc) { + return loc.startsWith(DISABLED_PREFIX); + } + + /** + * Factory method to create new instances from a location string. + * + * @param locationString location string + * @param context {@link SymbolServerInstanceCreatorContext} + * @return new instance, or null if invalid location string + */ + public static SymbolServer createInstance(String locationString, + SymbolServerInstanceCreatorContext context) { + SymbolServer delegate = + context.getSymbolServerInstanceCreatorRegistry() + .newSymbolServer(locationString.substring(DISABLED_PREFIX.length()), context); + return (delegate != null) ? new DisabledSymbolServer(delegate) : null; + } + + private SymbolServer delegate; + + /** + * Creates a new instance, wrapping an existing SymbolServer. + * + * @param delegate the SymbolServer that is being disabled + */ + public DisabledSymbolServer(SymbolServer delegate) { + this.delegate = delegate; + } + + /** + * Returns the wrapped (disabled) SymbolServer. + * + * @return wrapped / disabled SymbolServer + */ + public SymbolServer getSymbolServer() { + return delegate; + } + + @Override + public String getName() { + return DISABLED_PREFIX + delegate.getName(); + } + + @Override + public String getDescriptiveName() { + return "Disabled - " + delegate.getDescriptiveName(); + } + + @Override + public boolean isValid(TaskMonitor monitor) { + return delegate.isValid(monitor); + } + + @Override + public boolean exists(String filename, TaskMonitor monitor) { + return false; + } + + @Override + public List find(SymbolFileInfo fileInfo, Set findOptions, + TaskMonitor monitor) { + return List.of(); + } + + @Override + public SymbolServerInputStream getFileStream(String filename, TaskMonitor monitor) + throws IOException { + return delegate.getFileStream(filename, monitor); + } + + @Override + public String getFileLocation(String filename) { + return delegate.getFileLocation(filename); + } + + @Override + public boolean isLocal() { + return delegate.isLocal(); + } + + @Override + public String toString() { + return String.format("DisabledSymbolServer: [ %s ]", delegate.toString()); + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/FindOption.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/FindOption.java new file mode 100644 index 0000000000..55c7dbbf05 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/FindOption.java @@ -0,0 +1,61 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +/** + * Options that control how Pdb files are searched for on a SymbolServer. + */ +public enum FindOption { + /** + * Allow connections to remote symbol servers + */ + ALLOW_REMOTE, + /** + * Only return the first result + */ + ONLY_FIRST_RESULT, + /** + * Match any Pdb with the same name, regardless of GUID / signature id / age. + * (implies ANY_AGE) + */ + ANY_ID, + /** + * Match any Pdb with the same name and ID, regardless of age. + */ + ANY_AGE; + + /** + * Static constant empty set of no FindOptions. + */ + public static final Set NO_OPTIONS = Set.of(); + + /** + * Create a container of FindOptions. + * + * @param findOptions varargs list of FindOption enum values + * @return set of the specified FindOptions + */ + public static Set of(FindOption... findOptions) { + EnumSet result = EnumSet.noneOf(FindOption.class); + result.addAll(List.of(findOptions)); + return result; + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/HttpSymbolServer.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/HttpSymbolServer.java new file mode 100644 index 0000000000..a9d6980c4a --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/HttpSymbolServer.java @@ -0,0 +1,150 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; + +import ghidra.net.HttpClients; +import ghidra.util.Msg; +import ghidra.util.task.TaskMonitor; + +/** + * A {@link SymbolServer} that is accessed via HTTP. + *

    + * + */ +public class HttpSymbolServer extends AbstractSymbolServer { + private static final String GHIDRA_USER_AGENT = "Ghidra_HttpSymbolServer_client"; + private static final int HTTP_STATUS_OK = HttpURLConnection.HTTP_OK; + private static final int HTTP_REQUEST_TIMEOUT_MS = 10000; // 10 seconds + + /** + * Predicate that tests if the location string is an instance of a HttpSymbolServer location. + * + * @param locationString symbol server location string + * @return boolean true if the string should be handled by the HttpSymbolServer class + */ + public static boolean isHttpSymbolServerLocation(String locationString) { + return locationString.startsWith("http://") || locationString.startsWith("https://"); + } + + private final URI serverURI; + + /** + * Creates a new instance of a HttpSymbolServer. + * + * @param serverURI URI / URL of the symbol server + */ + public HttpSymbolServer(URI serverURI) { + String path = serverURI.getPath(); + this.serverURI = + path.endsWith("/") ? serverURI : serverURI.resolve(serverURI.getPath() + "/"); + } + + @Override + public String getName() { + return serverURI.toString(); + } + + @Override + public boolean isValid(TaskMonitor monitor) { + // NOTE: checking a http symbolserver's state by testing the + // existence of a file is not 100% universally correct, as different + // webserver implementations will handle this differently, but + // no better options are apparent. + // Just getting any HTTP response, including a 404 not found, isn't a + // good indication that the symbol server is valid as it could be + // a missing subtree of a parent web site. + return exists("", monitor) || exists(PINGME_FILENAME, monitor); + } + + private HttpRequest.Builder request(String str) { + return HttpRequest.newBuilder(serverURI.resolve(str)) + .timeout(Duration.ofMillis(HTTP_REQUEST_TIMEOUT_MS)) + .setHeader("User-Agent", GHIDRA_USER_AGENT); + } + + @Override + public boolean exists(String filename, TaskMonitor monitor) { + try { + HttpRequest request = request(filename).method("HEAD", BodyPublishers.noBody()).build(); + + Msg.debug(this, + logPrefix() + ": Checking exist for [" + filename + "]: " + request.toString()); + HttpResponse response = + HttpClients.getHttpClient().send(request, BodyHandlers.discarding()); + int statusCode = response.statusCode(); + Msg.debug(this, logPrefix() + ": Response: " + response.statusCode()); + + return statusCode == HTTP_STATUS_OK; + } + catch (InterruptedException | IOException e) { + // ignore, return false + return false; + } + } + + @Override + public SymbolServerInputStream getFileStream(String filename, TaskMonitor monitor) + throws IOException { + try { + HttpRequest request = request(filename).GET().build(); + Msg.debug(this, + logPrefix() + ": Getting file [" + filename + "]: " + request.toString()); + HttpResponse response = + HttpClients.getHttpClient().send(request, BodyHandlers.ofInputStream()); + int statusCode = response.statusCode(); + Msg.debug(this, logPrefix() + ": Http response: " + response.statusCode()); + if (statusCode == HTTP_STATUS_OK) { + long contentLen = response.headers().firstValueAsLong("Content-Length").orElse(-1); + return new SymbolServerInputStream(response.body(), contentLen); + } + throw new IOException("Unable to get file: " + statusCode); + } + catch (InterruptedException e) { + throw new IOException("Http get interrupted"); + } + } + + @Override + public String getFileLocation(String filename) { + return serverURI.resolve(filename).toString(); + } + + @Override + public boolean isLocal() { + return false; + } + + @Override + public String toString() { + return String.format("HttpSymbolServer: [ url: %s, storageLevel: %d]", serverURI.toString(), + storageLevel); + } + + private String logPrefix() { + return getClass().getSimpleName() + "[" + serverURI + "]"; + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/LocalSymbolStore.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/LocalSymbolStore.java new file mode 100644 index 0000000000..9d991e0112 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/LocalSymbolStore.java @@ -0,0 +1,389 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.util.*; + +import java.io.*; + +import org.apache.commons.io.FilenameUtils; + +import ghidra.util.Msg; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; + +/** + * Stores Pdb symbol files in a local directory. + *

    + * This is both a {@link SymbolServer} and a {@link SymbolStore} + *

    + */ +public class LocalSymbolStore extends AbstractSymbolServer implements SymbolStore { + private static final String ADMIN_DIRNAME = "000admin"; // per MS custom + + /** + * Predicate that returns true if the location string is a LocalSymbolStore path + * + * @param locationString symbol server location string + * @return boolean true if a LocalSymbolStore path + */ + public static boolean isLocalSymbolStoreLocation(String locationString) { + if (locationString == null || locationString.isBlank()) { + return false; + } + + File dir = new File(locationString); + return dir.isAbsolute() && dir.isDirectory(); + } + + /** + * Creates a (hopefully) MS-compatible symbol server directory location. + *

    + * + * @param rootDir Directory location of the new symbol store + * @param indexLevel the 'level' of the storage directory. Typical directories + * are either level 1, with pdb files stored directly under + * the root directory, or level 2, using the first 2 + * characters of the pdb filename as a bucket to place each + * pdb file-directory in. Level 0 indexLevel is a special + * Ghidra construct that is just a user-friendlier plain + * directory with a collection of Pdb files + * @throws IOException if error creating directory or admin files + */ + public static void create(File rootDir, int indexLevel) throws IOException { + FileUtilities.checkedMkdirs(rootDir); + switch (indexLevel) { + case 0: + // don't have to do anything + break; + case 2: + File index2File = new File(rootDir, INDEX_TWO_FILENAME); + if (!index2File.exists()) { + FileUtilities.writeStringToFile(index2File, + "created by Ghidra LocalSymbolStore " + new Date()); + } + // fall thru to create pingme and admin dir + case 1: + File pingmeFile = new File(rootDir, PINGME_FILENAME); + if (!pingmeFile.exists()) { + FileUtilities.writeStringToFile(pingmeFile, + "created by Ghidra LocalSymbolStore " + new Date()); + } + File adminDir = new File(rootDir, ADMIN_DIRNAME); + if (!adminDir.isDirectory()) { + FileUtilities.checkedMkdir(adminDir); + } + break; + default: + throw new IOException("Unsupported storage index level: " + indexLevel); + } + } + + private final File rootDir; + + /** + * Creates an instance of LocalSymbolStore. + * + * @param rootDir the root directory of the symbol storage + */ + public LocalSymbolStore(File rootDir) { + this.rootDir = rootDir; + } + + /** + * Returns the root directory of this symbol store. + * + * @return root directory of this symbol store + */ + public File getRootDir() { + return rootDir; + } + + @Override + public String getName() { + return rootDir.getPath(); + } + + @Override + public File getAdminDir() { + return (storageLevel == 0) ? rootDir : new File(rootDir, ADMIN_DIRNAME); + } + + @Override + public boolean isValid(TaskMonitor monitor) { + return isValid(); + } + + /** + * Non-task monitor variant of {@link #isValid(TaskMonitor)}. + * + * @return boolean true if this is a valid symbol store + */ + public boolean isValid() { + return rootDir.isDirectory(); + } + + @Override + public boolean exists(String filename, TaskMonitor monitor) { + File f = new File(rootDir, filename); + return f.isFile(); + } + + @Override + protected int detectStorageLevel(TaskMonitor monitor) { + // if the PINGME files exists, it means this directory was initialized as + // a real symbol server. If not, its probably just a normal directory + // that contains files. + File pingMeFile = new File(rootDir, PINGME_FILENAME); + File adminDir = new File(rootDir, ADMIN_DIRNAME); + if (pingMeFile.isFile() && adminDir.isDirectory()) { + return super.detectStorageLevel(monitor); + } + return 0; + } + + @Override + public List find(SymbolFileInfo symbolFileInfo, Set options, + TaskMonitor monitor) { + + initStorageLevelIfNeeded(monitor); + + List matches = new ArrayList<>(); + + // search for exact matches using the built-in logic in AbstractSymbolServer + if (storageLevel != 0) { + matches.addAll(super.find(symbolFileInfo, options, monitor)); + } + + if (storageLevel == 0 || options.contains(FindOption.ANY_AGE) || + options.contains(FindOption.ANY_ID)) { + + try { + if (storageLevel == 0) { + searchLevel0(rootDir, this, symbolFileInfo, options, matches, monitor); + } + else { + searchLevelN(symbolFileInfo, options, matches, monitor); + } + } + catch (IOException ioe) { + Msg.warn(this, "Error searching for " + symbolFileInfo.getName() + " in " + rootDir, + ioe); + } + } + + return matches; + } + + static void searchLevel0(File rootDir, SymbolStore symbolStore, SymbolFileInfo symbolFileInfo, + Set options, List matches, TaskMonitor monitor) { + + // if its a "0 level" bag-of-files, we have to open each Pdb to find its UID and + // AGE (after filtering for similar filenames as requested pdb file) + for (File f : list(rootDir, + ff -> ff.isFile() && isFilenameStartsWithMatch(symbolFileInfo, ff))) { + if (monitor.isCancelled()) { + break; + } + SymbolFileInfo fileInfo = SymbolFileInfo.fromFile(f, monitor); + if (fileInfo != null) { + if (hasSymbolFileInfoMatch(symbolFileInfo, fileInfo, options)) { + matches.add(new SymbolFileLocation(f.getName(), symbolStore, fileInfo)); + } + } + } + } + + private void searchLevelN(SymbolFileInfo symbolFileInfo, Set options, + List matches, + TaskMonitor monitor) throws IOException { + + // enbiggen the search by grubing through our subdirectories. + // "ke/kernelstuff.pdb/" or just "kernelstuff.pdb/" + String fileDir = getFileDir(symbolFileInfo.getName()); + + // since its a normal 1 or 2 level, we can get UID and AGE info from the subpath + // without opening the symbol file + for (File subDir : list(new File(rootDir, fileDir), File::isDirectory)) { + if (monitor.isCancelled()) { + break; + } + searchSubDir(subDir, symbolFileInfo, fileDir, options, matches); + } + } + + private void searchSubDir(File subDir, SymbolFileInfo symbolFileInfo, String relativeFileDir, + Set options, List results) { + + String symbolFileName = symbolFileInfo.getName(); + SymbolFileInfo subDirSymbolFileInfo = + SymbolFileInfo.fromSubdirectoryPath(symbolFileName, subDir.getName()); + + if (subDirSymbolFileInfo != null && !symbolFileInfo.isExactMatch(subDirSymbolFileInfo)) { + // don't examine this subfolder if its fingerprints indicate its an exact match, + // since exact matches will already have been added to the results + + // "ke/kernelstuff.pdb/112233440/" + String uniqueDir = relativeFileDir + subDir.getName() + "/"; + + if (hasSymbolFileInfoMatch(symbolFileInfo, subDirSymbolFileInfo, options)) { + String matchingFile = getFirstExists(uniqueDir, null, symbolFileName, + getCompressedFilename(symbolFileName)); + + if (matchingFile != null) { + results.add(new SymbolFileLocation(matchingFile, this, subDirSymbolFileInfo)); + } + } + } + } + + @Override + public String getFileLocation(String filename) { + return getFile(filename).getPath(); + } + + @Override + public File getFile(String path) { + return new File(rootDir, path); + } + + @Override + public String giveFile(SymbolFileInfo symbolFileInfo, File file, String filename, + TaskMonitor monitor) throws IOException { + initStorageLevelIfNeeded(monitor); + filename = FilenameUtils.getName(filename); // make sure no relative path shenanigans + String relativeDestinationFilename = getUniqueFileDir(symbolFileInfo) + filename; + File destinationFile = new File(rootDir, relativeDestinationFilename); + FileUtilities.checkedMkdirs(destinationFile.getParentFile()); + if (destinationFile.isFile()) { + Msg.info(this, logPrefix() + ": File already exists: " + destinationFile); + if (!file.delete()) { + Msg.warn(this, logPrefix() + ": Unable to delete source file: " + file); + } + return relativeDestinationFilename; + } + monitor.setMessage("Storing " + filename + " in local symbol store "); + if (!file.renameTo(destinationFile)) { + throw new IOException("Could not move " + file + " to " + destinationFile); + } + + return relativeDestinationFilename; + } + + @Override + public String putStream(SymbolFileInfo symbolFileInfo, + SymbolServerInputStream symbolServerInputStream, String filename, TaskMonitor monitor) + throws IOException { + initStorageLevelIfNeeded(monitor); + filename = FilenameUtils.getName(filename); // make sure no relative path shenanigans + String relativeDestinationFilename = getUniqueFileDir(symbolFileInfo) + filename; + File destinationFile = new File(rootDir, relativeDestinationFilename); + FileUtilities.checkedMkdirs(destinationFile.getParentFile()); + if (destinationFile.isFile()) { + Msg.info(this, logPrefix() + ": File already exists: " + destinationFile); + return relativeDestinationFilename; + } + + File destinationFileTmp = new File(rootDir, relativeDestinationFilename + ".tmp"); + destinationFileTmp.delete(); + + monitor.setMessage("Storing " + filename + " in local symbol store "); + + if (symbolServerInputStream.getExpectedLength() >= 0) { + monitor.initialize(symbolServerInputStream.getExpectedLength()); + } + try { + long bytesCopied = FileUtilities.copyStreamToFile( + symbolServerInputStream.getInputStream(), destinationFileTmp, false, monitor); + if (symbolServerInputStream.getExpectedLength() >= 0 && + bytesCopied != symbolServerInputStream.getExpectedLength()) { + throw new IOException("Copy length mismatch, expected " + + symbolServerInputStream.getExpectedLength() + " bytes, got " + bytesCopied); + } + if (!destinationFileTmp.renameTo(destinationFile)) { + throw new IOException( + "Error renaming temp file " + destinationFileTmp + " to " + destinationFile); + } + return relativeDestinationFilename; + } + finally { + destinationFileTmp.delete(); + } + + } + + @Override + public SymbolServerInputStream getFileStream(String filename, TaskMonitor monitor) + throws IOException { + File file = new File(rootDir, filename); + return new SymbolServerInputStream(new FileInputStream(file), file.length()); + } + + @Override + public boolean isLocal() { + return true; + } + + @Override + public String toString() { + return String.format("LocalSymbolStore: [ rootDir: %s, storageLevel: %d]", + rootDir.getPath(), storageLevel); + } + + private String logPrefix() { + return getClass().getSimpleName() + "[" + rootDir + "]"; + } + + // ----------------------------------------------------------------------------------- + // Static helpers + + static File[] list(File dir, FileFilter filter) { + File[] files = dir.listFiles(filter); + return files != null ? files : new File[] {}; + } + + static boolean isFilenameStartsWithMatch(SymbolFileInfo symbolFileInfo, File file) { + String symbolFilenameNoExtension = FilenameUtils.getBaseName(symbolFileInfo.getName()); + String fileNoExtension = FilenameUtils.getBaseName(file.getName()); + + // use case-insensitive compare since these are PDB files, which + // come from a Windows env + if (!fileNoExtension.toLowerCase().startsWith(symbolFilenameNoExtension.toLowerCase())) { + return false; + } + + // match on ext ("pdb"), compressed ext ("pd_") + String symbolFilenameExtension = + FilenameUtils.getExtension(symbolFileInfo.getName()).toLowerCase(); + String fileExtension = FilenameUtils.getExtension(file.getName()).toLowerCase(); + return fileExtension.equals(symbolFilenameExtension) || + fileExtension.equals(makeCompressedExtension(symbolFilenameExtension)); + } + + static boolean hasSymbolFileInfoMatch(SymbolFileInfo symbolFileInfo, + SymbolFileInfo otherSymbolFileInfo, Set options) { + boolean idMatches = + symbolFileInfo.getUniqueName().equalsIgnoreCase(otherSymbolFileInfo.getUniqueName()); + boolean ageMatches = symbolFileInfo.getIdentifiers() + .getAge() == otherSymbolFileInfo.getIdentifiers().getAge(); + + if (!options.contains(FindOption.ANY_ID)) { + return idMatches && (ageMatches || options.contains(FindOption.ANY_AGE)); + } + return true; + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SameDirSymbolStore.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SameDirSymbolStore.java new file mode 100644 index 0000000000..18034e8399 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SameDirSymbolStore.java @@ -0,0 +1,164 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.util.*; + +import java.io.*; + +import ghidra.util.task.TaskMonitor; + +/** + * A Pdb symbol server / symbol store, similar to the {@link LocalSymbolStore}, + * but limited to searching just the single directory that the original executable is located in. + *

    + * Matches symbol files that have a similar name to the requested symbol file (but the identifier + * info - guid/id & age must still match as per the find options specified). + * + */ +public class SameDirSymbolStore implements SymbolStore { + + /** + * Descriptive string + */ + public static String PROGRAMS_IMPORT_LOCATION_DESCRIPTION_STR = "Program's Import Location"; + + /** + * Factory helper, indicates if the specified location is the special + * magic string that indicates the location is the "same dir" symbol store. + * + * @param locationString Symbol server location string + * @return boolean true if the location string is the special magic "same dir" string (".") + */ + public static boolean isSameDirLocation(String locationString) { + return ".".equals(locationString); + } + + /** + * Reuse / abuse the {@link SameDirSymbolStore} to be the container/wrapper for an already known + * symbol file. Useful to wrap a file that was picked by the user in an + * {@link SymbolFileLocation}. + * + * @param symbolFile symbol file + * @param symbolFileInfo symbol file information + * @return a new {@link SymbolFileLocation} with a {@link SameDirSymbolStore} parent + */ + public static SymbolFileLocation createManuallySelectedSymbolFileLocation(File symbolFile, + SymbolFileInfo symbolFileInfo) { + SameDirSymbolStore samedirSymbolStore = new SameDirSymbolStore(symbolFile.getParentFile()); + SymbolFileLocation symbolFileLocation = + new SymbolFileLocation(symbolFile.getName(), samedirSymbolStore, symbolFileInfo); + return symbolFileLocation; + } + + private final File rootDir; + + /** + * Create a new instance, based on the directory where the program was originally imported from. + * + * @param rootDir directory path where the program was originally imported from, or null if not + * bound to an actual Program + */ + public SameDirSymbolStore(File rootDir) { + this.rootDir = rootDir; + } + + @Override + public File getAdminDir() { + return rootDir; + } + + @Override + public File getFile(String path) { + return new File(rootDir, path); + } + + @Override + public String giveFile(SymbolFileInfo symbolFileInfo, File f, String filename, + TaskMonitor monitor) throws IOException { + throw new IOException("Unsupported"); + } + + @Override + public String putStream(SymbolFileInfo symbolFileInfo, SymbolServerInputStream streamInfo, + String filename, TaskMonitor monitor) throws IOException { + throw new IOException("Unsupported"); + } + + @Override + public String getName() { + return "."; + } + + @Override + public String getDescriptiveName() { + return String.format(PROGRAMS_IMPORT_LOCATION_DESCRIPTION_STR + " - %s", + isValid() ? rootDir.getPath() : "unspecified"); + } + + @Override + public boolean isValid(TaskMonitor monitor) { + return isValid(); + } + + private boolean isValid() { + return rootDir != null && rootDir.isDirectory(); + } + + @Override + public boolean exists(String filename, TaskMonitor monitor) { + return isValid() && getFile(filename).isFile(); + } + + @Override + public List find(SymbolFileInfo fileInfo, Set findOptions, + TaskMonitor monitor) { + + List results = new ArrayList<>(); + + if (isValid()) { + LocalSymbolStore.searchLevel0(rootDir, this, fileInfo, findOptions, results, monitor); + } + + return results; + } + + @Override + public SymbolServerInputStream getFileStream(String filename, TaskMonitor monitor) + throws IOException { + if (!isValid(monitor)) { + throw new IOException("Unknown rootdir"); + } + File file = getFile(filename); + return new SymbolServerInputStream(new FileInputStream(file), file.length()); + } + + @Override + public String getFileLocation(String filename) { + return getFile(filename).getPath(); + } + + @Override + public boolean isLocal() { + return true; + } + + @Override + public String toString() { + return String.format("SameDirSymbolStore: [ dir: %s ]", rootDir); + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolFileInfo.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolFileInfo.java new file mode 100644 index 0000000000..4f970ebb69 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolFileInfo.java @@ -0,0 +1,274 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.util.Map; +import java.util.Objects; + +import java.io.File; + +import org.apache.commons.io.FilenameUtils; + +import ghidra.app.util.bin.format.pdb.PdbParserConstants; +import ghidra.app.util.bin.format.pdb2.pdbreader.PdbIdentifiers; +import ghidra.app.util.datatype.microsoft.GUID; +import ghidra.util.task.TaskMonitor; +import pdb.PdbUtils; + +/** + * Information about a pdb symbol file: its filename and its + * {@link PdbIdentifiers pdb guid/id fingerprints} + * + */ +public class SymbolFileInfo { + private static final int MIN_SIG_HEX_STR_LEN = 8; + private static final int GUID_HEX_STR_LEN = 32; + + /** + * Create a SymbolFileInfo instance that represents an unknown / bad + * file. + * + * @param path path string to use + * @return new SymbolFileInfo with a PdbIdentifier with bogus / default values + */ + public static SymbolFileInfo unknown(String path) { + return new SymbolFileInfo(path, new PdbIdentifiers(0, 0, 0, null, null)); + } + + /** + * Create a SymbolFileInfo instance from the metadata found in a program + * + * @param metadata Map of String-to-String values taken from a program + * @return new SymbolFileInfo instance, or null if no Pdb info found + */ + public static SymbolFileInfo fromMetadata(Map metadata) { + try { + int sig = + Integer.parseUnsignedInt( + metadata.getOrDefault(PdbParserConstants.PDB_SIGNATURE, "0"), 16); + String guidString = metadata.getOrDefault(PdbParserConstants.PDB_GUID, ""); + GUID guid = (guidString != null && !guidString.isBlank()) ? new GUID(guidString) : null; + int age = Integer + .parseUnsignedInt(metadata.getOrDefault(PdbParserConstants.PDB_AGE, "0"), 16); + String path = metadata.getOrDefault(PdbParserConstants.PDB_FILE, ""); + + PdbIdentifiers pdbIdentifiers = new PdbIdentifiers(0, sig, age, guid, null); + + return new SymbolFileInfo(path, pdbIdentifiers); + } + catch (IllegalArgumentException e) { + return null; + } + } + + /** + * Create a new {@link SymbolFileInfo} instance using information scraped from a pdb symbol + * server subdir path. + * + * @param path name of the pdb file + * @param uniqueSubdir string that is a combo of 32_hexchar_GUID + age or + * 8_hexchar_signature + age + * @return new {@link SymbolFileInfo} instance, or null if invalid info in path + * or subdir names + */ + public static SymbolFileInfo fromSubdirectoryPath(String path, String uniqueSubdir) { + try { + if (MIN_SIG_HEX_STR_LEN < uniqueSubdir.length() && + uniqueSubdir.length() < GUID_HEX_STR_LEN) { + int sig = Integer.parseUnsignedInt(uniqueSubdir.substring(0, 8), 16); + int age = Integer.parseUnsignedInt(uniqueSubdir.substring(8), 16); + + return new SymbolFileInfo(path, new PdbIdentifiers(0, sig, age, null, null)); + } + else if (uniqueSubdir.length() > GUID_HEX_STR_LEN) { + String guidString = uniqueSubdir.substring(0, GUID_HEX_STR_LEN); + GUID guid = new GUID(guidString); + + int age = Integer.parseUnsignedInt(uniqueSubdir.substring(GUID_HEX_STR_LEN), 16); + + return new SymbolFileInfo(path, new PdbIdentifiers(0, 0, age, guid, null)); + } + + } + catch (IllegalArgumentException e) { + // ignore + } + return null; + } + + /** + * Creates a new instance using the specified path and guid/id string and age. + * + * @param path String pdb path filename + * @param uid String GUID or signature id + * @param age int value + * @return new {@link SymbolFileInfo} instance made of specified path and identity info, + * or null if bad GUID / signature id string + */ + public static SymbolFileInfo fromValues(String path, String uid, int age) { + try { + GUID guid = new GUID(uid); + return new SymbolFileInfo(path, new PdbIdentifiers(0, 0, age, guid, null)); + } + catch (IllegalArgumentException e) { + // ignore, try older codeview + } + try { + int sig = Integer.parseUnsignedInt(uid, 16); + return new SymbolFileInfo(path, new PdbIdentifiers(0, sig, age, null, null)); + } + catch (IllegalArgumentException e) { + // fail + } + return null; + } + + /** + * Create a new instance using the specified path and {@link PdbIdentifiers}. + * + * @param path String pdb path filename + * @param pdbIdent {@link PdbIdentifiers} + * @return new {@link SymbolFileInfo} instance made of specified path and ident info + */ + public static SymbolFileInfo fromPdbIdentifiers(String path, PdbIdentifiers pdbIdent) { + return new SymbolFileInfo(path, pdbIdent); + } + + /** + * Create a new instance using the information found inside the specified file. + *

    + * The file will be opened and parsed to determine its GUID/ID and age. + * + * @param pdbFile pdb file to create a SymbolFileInfo for + * @param monitor {@link TaskMonitor} for progress and cancel + * @return new {@link SymbolFileInfo} instance or null if file is not a valid pdb or pdb.xml + * file + */ + public static SymbolFileInfo fromFile(File pdbFile, TaskMonitor monitor) { + PdbIdentifiers pdbIdentifiers = PdbUtils.getPdbIdentifiers(pdbFile, monitor); + return (pdbIdentifiers != null) ? new SymbolFileInfo(pdbFile.getName(), pdbIdentifiers) + : null; + } + + private final PdbIdentifiers pdbIdentifiers; + private final String pdbPath; + + private SymbolFileInfo(String pdbPath, PdbIdentifiers pdbIdentifiers) { + this.pdbPath = pdbPath; + this.pdbIdentifiers = pdbIdentifiers; + } + + /** + * Returns the {@link PdbIdentifiers} of this instance. + * + * @return {@link PdbIdentifiers} of this instance + */ + public PdbIdentifiers getIdentifiers() { + return pdbIdentifiers; + } + + /** + * The name of the pdb file, derived from the {@link #getPath() path} value. + * + * @return String name of the pdb file + */ + public String getName() { + return FilenameUtils.getName(pdbPath); + } + + /** + * The 'path' of the pdb file, which contains the full path and filename recovered from the + * original binary's debug data. Typically, this is just a plain name string without any + * path info. + * + * @return original pdb path string recovered from binary's debug data + */ + public String getPath() { + return pdbPath; + } + + /** + * A string that represents the unique fingerprint of a Pdb file. Does not + * include the age. + * + * @return either GUID str or signature hexstring + */ + public String getUniqueName() { + return (pdbIdentifiers.getGuid() != null) + ? pdbIdentifiers.getGuid().toString().replace("-", "").toUpperCase() + : String.format("%08X", pdbIdentifiers.getSignature()); + + } + + /** + * Returns a string that is a combination of the GUID/ID and the age, in a format + * used by symbol servers to create subdirectories in their directory structure. + * + * @return String combination of GUID/ID and age, e.g. "112233441" + */ + public String getUniqueDirName() { + return getUniqueName() + Integer.toUnsignedString(pdbIdentifiers.getAge(), 16); + } + + /** + * Returns true if this SymbolFileInfo instance exactly matches the {@link PdbIdentifiers} + * info of the other instance. + * + * @param other {@link SymbolFileInfo} to compare + * @return boolean true if exact match of {@link PdbIdentifiers} info + */ + public boolean isExactMatch(SymbolFileInfo other) { + return getUniqueName().equalsIgnoreCase(other.getUniqueName()) && + pdbIdentifiers.getAge() == other.getIdentifiers().getAge(); + } + + /** + * Returns a description of this instance. + * + * @return String description + */ + public String getDescription() { + return getName() + ", " + getIdentifiers(); + } + + @Override + public String toString() { + return String.format("SymbolFileInfo: [ pdb: %s, uid: %s]", getName(), + getIdentifiers().toString()); + } + + @Override + public int hashCode() { + return Objects.hash(pdbIdentifiers, pdbPath); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SymbolFileInfo other = (SymbolFileInfo) obj; + return Objects.equals(pdbIdentifiers, other.pdbIdentifiers) && + Objects.equals(pdbPath, other.pdbPath); + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolFileLocation.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolFileLocation.java new file mode 100644 index 0000000000..29febaa841 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolFileLocation.java @@ -0,0 +1,114 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.util.Objects; + +/** + * Represents a symbol file on a {@link SymbolServer} or an associated file. + */ +public class SymbolFileLocation { + private final SymbolFileInfo fileInfo; + private final String path; + private final SymbolServer symbolServer; + + /** + * Creates a new instance. + * + * @param path raw path to file (relative to the {@link SymbolServer}) + * @param symbolServer {@link SymbolServer} the file resides on + * @param fileInfo the {@link SymbolFileInfo pdb file} that this file is associated with + */ + public SymbolFileLocation(String path, SymbolServer symbolServer, SymbolFileInfo fileInfo) { + this.path = path; + this.symbolServer = symbolServer; + this.fileInfo = fileInfo; + } + + /** + * The raw path inside the SymbolServer to the file. + * + * @return raw path inside the SymbolServer to the file + */ + public String getPath() { + return path; + } + + /** + * The {@link SymbolServer} that holds the file. + * + * @return the {@link SymbolServer} that holds the file + */ + public SymbolServer getSymbolServer() { + return symbolServer; + } + + /** + * The {@link SymbolFileInfo pdb file} that this file is associated with. + * + * @return the {@link SymbolFileInfo pdb file} that this file is associated with + */ + public SymbolFileInfo getFileInfo() { + return fileInfo; + } + + /** + * Returns true if this file is an 'exact match' for the + * specified {@link SymbolFileInfo other pdb file}. + * + * @param otherSymbolFileInfo the other pdb file's info + * @return boolean true if exact match (GUID & age match), false if not an exact match + */ + public boolean isExactMatch(SymbolFileInfo otherSymbolFileInfo) { + return fileInfo.isExactMatch(otherSymbolFileInfo); + } + + /** + * The 'absolute' location of this file, including the symbol server's location. + * + * @return a string representing the 'absolute' location of this file + */ + public String getLocationStr() { + return symbolServer.getFileLocation(path); + } + + @Override + public String toString() { + return path + " in " + symbolServer.getName() + " for " + fileInfo.getDescription(); + } + + @Override + public int hashCode() { + return Objects.hash(fileInfo, path, symbolServer); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SymbolFileLocation other = (SymbolFileLocation) obj; + return Objects.equals(fileInfo, other.fileInfo) && Objects.equals(path, other.path) && + symbolServer == other.symbolServer; + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServer.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServer.java new file mode 100644 index 0000000000..e958183a75 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServer.java @@ -0,0 +1,110 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.util.List; +import java.util.Set; + +import java.io.IOException; +import java.io.InputStream; + +import ghidra.util.task.TaskMonitor; + +/** + * Represents the common functionality of different types of symbol servers: querying for + * files containing symbol information and getting those files. + * + */ +public interface SymbolServer { + + /** + * Name of the symbol server, suitable to use as the identity of this instance, + * and which will allow the SymbolServerInstanceCreatorRegistry to recreate an instance. + * + * @return string name + */ + String getName(); + + /** + * Descriptive name of the symbol server, used in UI lists, etc. + * + * @return string descriptive name + */ + default String getDescriptiveName() { + return getName(); + } + + /** + * Returns true if the symbol server is valid and can be queried. + * @param monitor {@link TaskMonitor} + * + * @return boolean true if symbol server is working + */ + boolean isValid(TaskMonitor monitor); + + /** + * Returns true if the raw filename exists in the symbol server. + * + * @param filename raw path filename string + * @param monitor {@link TaskMonitor} + * @return boolean true if file exists + */ + boolean exists(String filename, TaskMonitor monitor); + + /** + * Searches for a symbol file on the server. + *

    + * HttpSymbolServers only support exact matches, LocalSymbolStores can + * possibly have fuzzy matches. + * + * @param fileInfo {@link SymbolFileInfo} bag of information about the file to search for + * @param findOptions set of {@link FindOption} to control the search. + * See {@link FindOption#NO_OPTIONS} or + * {@link FindOption#of(FindOption...) FindOptions.of(option1, option2...)} + * @param monitor {@link TaskMonitor} + * @return list of {@link SymbolFileLocation location information instances} about matches + */ + List find(SymbolFileInfo fileInfo, Set findOptions, + TaskMonitor monitor); + + /** + * Returns a wrapped InputStream for the specified raw path filename. + * + * @param filename raw path filename + * @param monitor {@link TaskMonitor} + * @return {@link SymbolServerInputStream} wrapped {@link InputStream}, never null + * @throws IOException if error or not found + */ + SymbolServerInputStream getFileStream(String filename, TaskMonitor monitor) throws IOException; + + /** + * Returns a location description string of a specific file contained in this symbol server. + *

    + * + * @param filename raw path and name of a file in this server + * (typically from {@link SymbolFileLocation#getPath()} + * @return a descriptive string with the 'absolute' location of this file + */ + String getFileLocation(String filename); + + /** + * Returns true if this {@link SymbolServer} is 'local', meaning + * it can be searched without security issues / warning the user. + * + * @return boolean true if this symbolserver is 'local', false if remote + */ + boolean isLocal(); +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInputStream.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInputStream.java new file mode 100644 index 0000000000..5073c092b7 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInputStream.java @@ -0,0 +1,60 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.io.*; + +/** + * A {@link InputStream} wrapper returned from a {@link SymbolServer} + * that also contains the expected length of the stream. + */ +public class SymbolServerInputStream implements Closeable { + private final InputStream inputStream; + private final long expectedLength; + + /** + * Create a new instance. + * + * @param inputStream {@link InputStream} to wrap + * @param expectedLength the expected length of the input stream + */ + public SymbolServerInputStream(InputStream inputStream, long expectedLength) { + this.inputStream = inputStream; + this.expectedLength = expectedLength; + } + + /** + * Returns the wrapped input stream + * @return the wrapped input stream + */ + public InputStream getInputStream() { + return inputStream; + } + + /** + * Returns the expected length of the input stream + * + * @return expected length of the input stream + */ + public long getExpectedLength() { + return expectedLength; + } + + @Override + public void close() throws IOException { + inputStream.close(); + } +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorContext.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorContext.java new file mode 100644 index 0000000000..4e880ec480 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorContext.java @@ -0,0 +1,64 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.io.File; + +/** + * Context for the {@link SymbolServerInstanceCreatorRegistry} when creating new + * {@link SymbolServer} instances. + *

    + * This allows the method that is creating a new SymbolServer to know the location the + * Ghidra program was imported from, as well as to reach back to the registry itself and + * use it to create other SymbolServer instances (if necessary). + *

    + * Created via {@link SymbolServerInstanceCreatorRegistry#getContext()} or + * {@link SymbolServerInstanceCreatorRegistry#getContext(ghidra.program.model.listing.Program)} + */ +public class SymbolServerInstanceCreatorContext { + private final File rootDir; + private final SymbolServerInstanceCreatorRegistry symbolServerInstanceCreatorRegistry; + + SymbolServerInstanceCreatorContext( + SymbolServerInstanceCreatorRegistry symbolServerInstanceCreatorRegistry) { + this(null, symbolServerInstanceCreatorRegistry); + } + + SymbolServerInstanceCreatorContext(File rootDir, + SymbolServerInstanceCreatorRegistry symbolServerInstanceCreatorRegistry) { + this.rootDir = rootDir; + this.symbolServerInstanceCreatorRegistry = symbolServerInstanceCreatorRegistry; + } + + /** + * The {@link SymbolServerInstanceCreatorRegistry} associated with this context. + * + * @return the {@link SymbolServerInstanceCreatorRegistry} + */ + public SymbolServerInstanceCreatorRegistry getSymbolServerInstanceCreatorRegistry() { + return symbolServerInstanceCreatorRegistry; + } + + /** + * The root directory of the imported binary. + * + * @return directory of the binary, or null if no associated program + */ + public File getRootDir() { + return rootDir; + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorRegistry.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorRegistry.java new file mode 100644 index 0000000000..9905b4477a --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerInstanceCreatorRegistry.java @@ -0,0 +1,220 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.util.*; +import java.util.function.Predicate; + +import java.io.File; +import java.net.URI; + +import org.apache.commons.io.FilenameUtils; + +import ghidra.program.model.listing.Program; +import ghidra.util.Msg; + +/** + * Registry of {@link SymbolServer} instance creators. + */ +public class SymbolServerInstanceCreatorRegistry { + + private static final SymbolServerInstanceCreatorRegistry instance = + new SymbolServerInstanceCreatorRegistry(); + + /** + * A static singleton pre-configured with the default symbol server implementations. + * + * @return static singleton {@link SymbolServerInstanceCreatorRegistry} instance. + */ + public static SymbolServerInstanceCreatorRegistry getInstance() { + return instance; + } + + private final TreeMap symbolServerInstanceCreatorsByPriority = + new TreeMap<>(); + + private SymbolServerInstanceCreatorRegistry() { + registerDefaultSymbolServerInstanceCreators(); + } + + /** + * Registers a new SymbolServer implementation so that instances of + * it can be created by the user and saved / restored from preferences. + * + * @param priority relative order of precedence of polling this + * implementation's predicate to detect the specific SymbolServer + * implementation from a locationString. + * @param locationStringMatcher predicate that returns true / false if the specified String is + * handled by this SymbolServer implementation + * @param symbolServerInstanceCreator a method that creates a SymbolServer + * instance based on the specified location string and context + */ + public void registerSymbolServerInstanceCreator(int priority, + Predicate locationStringMatcher, + SymbolServerInstanceCreator symbolServerInstanceCreator) { + SymbolServerInstanceCreatorInfo symbolServerInstanceCreatorInfo = + new SymbolServerInstanceCreatorInfo(locationStringMatcher, symbolServerInstanceCreator); + + symbolServerInstanceCreatorsByPriority.put(priority, symbolServerInstanceCreatorInfo); + } + + /** + * Converts a list of symbol server location strings to a list of SymbolServer instances. + * + * @param locationStrings list of symbol server location strings + * @param symbolServerInstanceCreatorContext a {@link SymbolServerInstanceCreatorContext} + * - see {@link #getContext()} or {@link #getContext(Program)} + * @return list of {@link SymbolServer} + */ + public List createSymbolServersFromPathList(List locationStrings, + SymbolServerInstanceCreatorContext symbolServerInstanceCreatorContext) { + List result = new ArrayList<>(); + for (String locationString : locationStrings) { + SymbolServer symbolServer = + newSymbolServer(locationString, symbolServerInstanceCreatorContext); + if (symbolServer != null) { + result.add(symbolServer); + } + } + return result; + } + + /** + * Creates a new SymbolServer instance, using the registered SymbolServer types. + * + * @param symbolServerLocationString SymbolServer location - see {@link SymbolServer#getName()} + * @param symbolServerInstanceCreatorContext a {@link SymbolServerInstanceCreatorContext} + * - see {@link #getContext()} + * or {@link #getContext(Program)} + * @return new SymbolServer instance, or null if bad location string + */ + public SymbolServer newSymbolServer(String symbolServerLocationString, + SymbolServerInstanceCreatorContext symbolServerInstanceCreatorContext) { + return newSymbolServer(symbolServerLocationString, symbolServerInstanceCreatorContext, + SymbolServer.class); + } + + /** + * Creates a new SymbolServer instance, using the registered SymbolServer types. + * + * @param symbolServerLocationString SymbolServer location - see {@link SymbolServer#getName()} + * @param symbolServerInstanceCreatorContext a {@link SymbolServerInstanceCreatorContext} + * - see {@link #getContext()} + * @param expectedSymbolServerClass expected class of the new symbol server being created + * @return new SymbolServer instance, or null if bad location string + */ + public T newSymbolServer(String symbolServerLocationString, + SymbolServerInstanceCreatorContext symbolServerInstanceCreatorContext, + Class expectedSymbolServerClass) { + if (symbolServerLocationString == null || symbolServerLocationString.isBlank()) { + return null; + } + for (SymbolServerInstanceCreatorInfo symbolServerInstanceCreatorInfo : symbolServerInstanceCreatorsByPriority + .values()) { + if (symbolServerInstanceCreatorInfo.getLocationStringMatcher() + .test(symbolServerLocationString)) { + SymbolServer result = + symbolServerInstanceCreatorInfo.getSymbolServerInstanceCreator() + .createSymbolServerFromLocationString( + symbolServerLocationString, symbolServerInstanceCreatorContext); + if (result == null) { + return null; + } + if (!expectedSymbolServerClass.isInstance(result)) { + Msg.debug(this, "SymbolServer location unexpected class type. Wanted " + + expectedSymbolServerClass.getName() + ", got " + + result.getClass().getName()); + return null; + } + return expectedSymbolServerClass.cast(result); + } + } + Msg.debug(SymbolServerService.class, + "Symbol server location [" + symbolServerLocationString + "] not valid, skipping."); + return null; + } + + /** + * Creates a {@link SymbolServerInstanceCreatorContext} that is not bound to a Program. + * + * @return new {@link SymbolServerInstanceCreatorContext} + */ + public SymbolServerInstanceCreatorContext getContext() { + return new SymbolServerInstanceCreatorContext(this); + } + + /** + * Creates a new {@link SymbolServerInstanceCreatorContext} that is bound to a Program. + * + * @param program Ghidra program + * @return new {@link SymbolServerInstanceCreatorContext} + */ + public SymbolServerInstanceCreatorContext getContext(Program program) { + File exeLocation = new File(FilenameUtils.getFullPath(program.getExecutablePath())); + return new SymbolServerInstanceCreatorContext(exeLocation, this); + } + + private void registerDefaultSymbolServerInstanceCreators() { + registerSymbolServerInstanceCreator(0, DisabledSymbolServer::isDisabledSymbolServerLocation, + DisabledSymbolServer::createInstance); + registerSymbolServerInstanceCreator(100, HttpSymbolServer::isHttpSymbolServerLocation, + (loc, context) -> new HttpSymbolServer(URI.create(loc))); + registerSymbolServerInstanceCreator(200, SameDirSymbolStore::isSameDirLocation, + (loc, context) -> new SameDirSymbolStore(context.getRootDir())); + registerSymbolServerInstanceCreator(300, LocalSymbolStore::isLocalSymbolStoreLocation, + (loc, context) -> new LocalSymbolStore(new File(loc))); + } + + /** + * Functional interface that creates a new {@link SymbolServer} instance using a + * location string and a context instance. + *

    + * See {@link #createSymbolServerFromLocationString(String, SymbolServerInstanceCreatorContext)} + */ + public interface SymbolServerInstanceCreator { + /** + * Creates a new {@link SymbolServer} instance using the specified location string + * and the context available in the symbolServerInstanceCreatorContext. + * + * @param symbolServerLocationString location string + * @param symbolServerInstanceCreatorContext context + * @return new {@link SymbolServer} instance, null if error + */ + SymbolServer createSymbolServerFromLocationString(String symbolServerLocationString, + SymbolServerInstanceCreatorContext symbolServerInstanceCreatorContext); + } + + private static class SymbolServerInstanceCreatorInfo { + private Predicate locationStringMatcher; + private SymbolServerInstanceCreator symbolServerInstanceCreator; + + SymbolServerInstanceCreatorInfo(Predicate locationStringMatcher, + SymbolServerInstanceCreator symbolServerInstanceCreator) { + this.locationStringMatcher = locationStringMatcher; + this.symbolServerInstanceCreator = symbolServerInstanceCreator; + } + + Predicate getLocationStringMatcher() { + return locationStringMatcher; + } + + SymbolServerInstanceCreator getSymbolServerInstanceCreator() { + return symbolServerInstanceCreator; + } + + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerService.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerService.java new file mode 100644 index 0000000000..75cbd02c4a --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolServerService.java @@ -0,0 +1,288 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.util.*; +import java.util.stream.Collectors; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FilenameUtils; + +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import pdb.PdbUtils; + +/** + * A (lowercase-'S') service that searches for and fetches symbol files + * from a set of local and remote {@link SymbolServer symbolservers}. (not to be + * confused with a Plugin service) + *

    + * Instances of this class are meant to be easily created when needed + * and just as easily thrown away when not used or when the search + * path configuration changes. + *

    + * A SymbolServerService instance requires a {@link SymbolStore} and + * optionally a list of {@link SymbolServer}s. + */ +public class SymbolServerService { + + private SymbolStore symbolStore; // also the first element of the symbolServers list + private List symbolServers; + + /** + * Creates a new SymbolServerService instance. + *

    + * @param symbolStore a {@link SymbolStore} - where all + * remote files are placed when downloaded. Also treated as a SymbolServer + * and searched first + * @param symbolServers a list of {@link SymbolServer symbol servers} - searched in order + */ + public SymbolServerService(SymbolStore symbolStore, List symbolServers) { + this.symbolStore = symbolStore; + this.symbolServers = new ArrayList<>(); + this.symbolServers.add(symbolStore); + this.symbolServers.addAll(symbolServers); + } + + /** + * Returns true if this SymbolServerService is fully valid. + * Will be false if the symbol storage location isn't a {@link LocalSymbolStore}. + * + * @return boolean true if this instance is valid, false if not valid + */ + public boolean isValid() { + return symbolStore instanceof LocalSymbolStore; + } + + /** + * Returns the {@link SymbolStore}, which is the primary / first location queried and + * used to store any symbol files retrieved from a remote symbol server. + * + * @return the {@link SymbolStore} + */ + public SymbolStore getSymbolStore() { + return symbolStore; + } + + /** + * Returns the list of {@link SymbolServer}s. + * + * @return the list of {@link SymbolServer}s + */ + public List getSymbolServers() { + return new ArrayList<>(symbolServers.subList(1, symbolServers.size())); + } + + /** + * Returns the number of configured symbol servers that are considered 'remote'. + * @return number of remote symbol servers + */ + public int getRemoteSymbolServerCount() { + int remoteSymbolServerCount = (int) getSymbolServers() + .stream() + .filter(ss -> !ss.isLocal()) + .count(); + + return remoteSymbolServerCount; + } + + /** + * Searches all {@link SymbolServer symbol servers} for a matching pdb symbol file. + * + * @param symbolFileInfo {@link SymbolFileInfo} bag of information + * about the file to search for + * @param monitor {@link TaskMonitor} to update with search progress and to + * allow the user to cancel the operation + * @return a list of {@link SymbolFileLocation} instances + * @throws CancelledException if cancelled + */ + public List find(SymbolFileInfo symbolFileInfo, TaskMonitor monitor) + throws CancelledException { + return find(symbolFileInfo, FindOption.NO_OPTIONS, monitor); + } + + /** + * Searches all {@link SymbolServer symbol servers} for a matching pdb symbol file. + *

    + * Returns a list of matches. + *

    + * Use {@link SymbolFileLocation#isExactMatch(SymbolFileInfo)} to test elements in the + * result list for exactness. + *

    + * + * @param symbolFileInfo Pdb file info to search for + * @param findOptions set of {@link FindOption} to control the search. + * See {@link FindOption#NO_OPTIONS} or + * {@link FindOption#of(FindOption...) FindOptions.of(option1, option2...)} + * @param monitor {@link TaskMonitor} + * @return list of {@link SymbolFileLocation}s + * @throws CancelledException if operation canceled by user + */ + public List find(SymbolFileInfo symbolFileInfo, + Set findOptions, TaskMonitor monitor) throws CancelledException { + + List allFindResults = new ArrayList<>(); + Set uniqueSymbolFilePaths = new HashSet<>(); + + for_each_symbol_server_loop: for (SymbolServer symbolServer : symbolServers) { + monitor.checkCanceled(); + if (!symbolServer.isLocal() && !findOptions.contains(FindOption.ALLOW_REMOTE)) { + Msg.debug(this, + logPrefix() + ": skipping non-local symbol server " + + symbolServer.getDescriptiveName()); + continue; + } + + Msg.debug(this, logPrefix() + ": querying " + symbolServer.getDescriptiveName() + + " for " + symbolFileInfo.getDescription()); + + List symbolServerFindResults = + symbolServer.find(symbolFileInfo, findOptions, monitor); + + Msg.debug(this, + logPrefix() + ": got " + symbolServerFindResults.size() + " results from " + + symbolServer.getDescriptiveName()); + + // only add unique file locations + for (SymbolFileLocation symbolFileLocation : symbolServerFindResults) { + if (uniqueSymbolFilePaths.add(symbolFileLocation.getLocationStr())) { + allFindResults.add(symbolFileLocation); + if (findOptions.contains(FindOption.ONLY_FIRST_RESULT)) { + break for_each_symbol_server_loop; + } + } + } + } + + Msg.debug(this, logPrefix() + ": found " + allFindResults.size() + " matches"); + + return allFindResults; + + } + + /** + * Returns the local file path of the symbol file specified by symbolFileLocation. + * + * @param symbolFileLocation {@link SymbolFileLocation}, returned + * by {@link #find(SymbolFileInfo, Set, TaskMonitor) find()} + * @param monitor {@link TaskMonitor} + * @return {@link File} path to the local pdb file, never null + * @throws CancelledException if user cancels operation + * @throws IOException if error or problem getting file + */ + public File getSymbolFile(SymbolFileLocation symbolFileLocation, TaskMonitor monitor) + throws CancelledException, IOException { + Msg.debug(this, + logPrefix() + ": getting symbol file: " + symbolFileLocation.getLocationStr()); + + SymbolFileLocation localSymbolFileLocation = + ensureLocalUncompressedFile(symbolFileLocation, monitor); + + Msg.debug(this, + logPrefix() + ": local file now: " + localSymbolFileLocation.getLocationStr()); + + SymbolStore symbolStore = (SymbolStore) localSymbolFileLocation.getSymbolServer(); + + return symbolStore.getFile(localSymbolFileLocation.getPath()); + } + + /** + * Converts a possibly remote {@link SymbolFileLocation} to a location that is local and + * uncompressed. + * + * @param symbolFileLocation possibly remote {@link SymbolFileLocation} + * @param monitor {@link TaskMonitor} to display progress and allow canceling + * @return {@link SymbolFileLocation} that is local (possibly the same instance if already + * local) + * @throws CancelledException if canceled + * @throws IOException if error + */ + public SymbolFileLocation getLocalSymbolFileLocation(SymbolFileLocation symbolFileLocation, + TaskMonitor monitor) throws CancelledException, IOException { + Msg.debug(this, + logPrefix() + ": getting symbol file: " + symbolFileLocation.getLocationStr()); + + SymbolFileLocation localSymbolFileLocation = + ensureLocalUncompressedFile(symbolFileLocation, monitor); + + return localSymbolFileLocation; + } + + private SymbolFileLocation ensureLocalUncompressedFile(SymbolFileLocation symbolFileLocation, + TaskMonitor monitor) throws IOException, CancelledException { + if (!(symbolFileLocation.getSymbolServer() instanceof SymbolStore)) { + Msg.debug(this, logPrefix() + ": copying file " + symbolFileLocation.getLocationStr() + + " from remote to local " + symbolStore.getName()); + + // copy from remote store to our main local symbol store + String remoteFilename = FilenameUtils.getName(symbolFileLocation.getPath()); + try (SymbolServerInputStream symbolServerInputStream = + symbolFileLocation.getSymbolServer() + .getFileStream(symbolFileLocation.getPath(), monitor)) { + String newPath = + symbolStore.putStream(symbolFileLocation.getFileInfo(), symbolServerInputStream, + remoteFilename, monitor); + symbolFileLocation = + new SymbolFileLocation(newPath, symbolStore, symbolFileLocation.getFileInfo()); + } + } + + // symbolFileLocation now must be on a SymbolStore, so safe to cast + SymbolStore localSymbolStore = (SymbolStore) symbolFileLocation.getSymbolServer(); + + if (SymbolStore.isCompressedFilename(symbolFileLocation.getPath())) { + File cabFile = localSymbolStore.getFile(symbolFileLocation.getPath()); + File temporaryExtractFile = new File(symbolStore.getAdminDir(), + "ghidra_cab_extract_tmp_" + System.currentTimeMillis()); + + Msg.debug(this, + logPrefix() + ": decompressing file " + symbolFileLocation.getLocationStr()); + + String originalName = + PdbUtils.extractSingletonCabToFile(cabFile, temporaryExtractFile, monitor); + String uncompressedPath = + symbolStore.giveFile(symbolFileLocation.getFileInfo(), temporaryExtractFile, + originalName, monitor); + + symbolFileLocation = new SymbolFileLocation(uncompressedPath, symbolStore, + symbolFileLocation.getFileInfo()); + + Msg.debug(this, + logPrefix() + ": new decompressed file " + symbolFileLocation.getLocationStr()); + } + + return symbolFileLocation; + } + + private String logPrefix() { + return getClass().getSimpleName(); + } + + @Override + public String toString() { + return String.format( + "SymbolServerService:\n\tsymbolStore: %s,\n\tsymbolServers:\n\t\t%s\n", + symbolStore.toString(), + symbolServers.subList(1, symbolServers.size()) + .stream() + .map(SymbolServer::toString) + .collect(Collectors.joining("\n\t\t"))); + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolStore.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolStore.java new file mode 100644 index 0000000000..229bc78b37 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/SymbolStore.java @@ -0,0 +1,91 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.io.File; +import java.io.IOException; + +import ghidra.util.task.TaskMonitor; + +/** + * A local writable {@link SymbolServer}. + */ +public interface SymbolStore extends SymbolServer { + + /** + * Returns the 'admin' directory of this SymbolStore, which allows files created here + * to be efficiently {@link #giveFile(SymbolFileInfo, File, String, TaskMonitor) given} + * to the store. + *

    + * + * @return directory + */ + File getAdminDir(); + + /** + * Returns an absolute {@link File} instance based on the specified relative path + * to a file inside the symbol store. + *

    + * + * @param path relative local path + * @return absolute {@link File} based on the specified relative path + */ + File getFile(String path); + + /** + * Offers the specified file to the SymbolStore. The file should be + * located in the admin directory of the SymbolStore to ensure no problems + * with ingesting the file. + *

    + * The file will be 'consumed' by this SymbolStore, and the caller's + * responsibility to the file ends. + * + * @param symbolFileInfo {@link SymbolFileInfo} bag of information about the file + * @param file {@link File} to ingest + * @param filename real name of the ingested file + * @param monitor {@link TaskMonitor} + * @return relative raw local path to the newly ingested file + * @throws IOException if error + */ + String giveFile(SymbolFileInfo symbolFileInfo, File file, String filename, TaskMonitor monitor) + throws IOException; + + /** + * Places the contents of the stream into a file in this SymbolStore. + *

    + * + * @param symbolFileInfo {@link SymbolFileInfo} bag of information about the file + * @param symbolServerInputStream the stream to ingest + * @param filename real name of the ingested file + * @param monitor {@link TaskMonitor} + * @return relative raw local path to the newly ingested file + * @throws IOException if error + */ + String putStream(SymbolFileInfo symbolFileInfo, SymbolServerInputStream symbolServerInputStream, + String filename, TaskMonitor monitor) throws IOException; + + /** + * Returns true if the specified filename indicates that the file is a compressed + * cab file. + * + * @param filename filename + * @return boolean true if filename indicates that the file is compressed + */ + public static boolean isCompressedFilename(String filename) { + return filename.endsWith("_"); + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/ConfigPdbDialog.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/ConfigPdbDialog.java new file mode 100644 index 0000000000..cbeb0de846 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/ConfigPdbDialog.java @@ -0,0 +1,79 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver.ui; + +import docking.DialogComponentProvider; +import docking.DockingWindowManager; +import docking.widgets.OptionDialog; +import pdb.symbolserver.SymbolServerInstanceCreatorRegistry; +import pdb.symbolserver.SymbolServerService; + +/** + * Dialog that allows the user to configure the Pdb search locations and symbol directory + */ +public class ConfigPdbDialog extends DialogComponentProvider { + + public static void showSymbolServerConfig() { + ConfigPdbDialog choosePdbDialog = new ConfigPdbDialog(); + DockingWindowManager.showDialog(choosePdbDialog); + } + + private SymbolServerPanel symbolServerConfigPanel; + + public ConfigPdbDialog() { + super("Configure Symbol Server Search", true, false, true, false); + + build(); + } + + @Override + protected void cancelCallback() { + close(); + } + + @Override + protected void okCallback() { + if (symbolServerConfigPanel.isConfigChanged() && + OptionDialog.showYesNoDialog(getComponent(), + "Save Configuration", + "Symbol server configuration changed. Save?") == OptionDialog.YES_OPTION) { + symbolServerConfigPanel.saveConfig(); + } + close(); + } + + private void build() { + symbolServerConfigPanel = new SymbolServerPanel(this::onSymbolServerServiceChange, + SymbolServerInstanceCreatorRegistry.getInstance().getContext()); + + addButtons(); + addWorkPanel(symbolServerConfigPanel); + setRememberSize(false); + okButton.setEnabled(symbolServerConfigPanel.getSymbolServerService() != null); + setMinimumSize(400, 250); + } + + private void onSymbolServerServiceChange(SymbolServerService newService) { + okButton.setEnabled(newService != null); + rootPanel.revalidate(); + } + + private void addButtons() { + addOKButton(); + addCancelButton(); + setDefaultButton(cancelButton); + } +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/FilePromptDialog.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/FilePromptDialog.java new file mode 100644 index 0000000000..2b6fe7cf6a --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/FilePromptDialog.java @@ -0,0 +1,195 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver.ui; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.io.File; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import docking.DialogComponentProvider; +import docking.DockingWindowManager; +import docking.options.editor.ButtonPanelFactory; +import docking.widgets.OptionDialog; +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.filechooser.GhidraFileChooserMode; +import docking.widgets.label.GHtmlLabel; +import ghidra.util.filechooser.GhidraFileFilter; +import ghidra.util.layout.PairLayout; + +/** + * Non-public, package-only dialog that prompts the user to enter a path + * in a text field (similar to an {@link OptionDialog}) and allows them to click + * a "..." browse button to pick the file and/or directory via a + * {@link GhidraFileChooser} dialog. + */ +class FilePromptDialog extends DialogComponentProvider { + + /** + * Prompts the user to enter the path to a directory, + * or to pick it using a browser dialog. + * + * @param title the dialog title + * @param prompt HTML enabled prompt + * @param initialValue initial value to pre-populate the input field with + * @return the {@link File} the user entered / picked, or null if canceled + */ + public static File chooseDirectory(String title, String prompt, File initialValue) { + return chooseFile(title, prompt, "Choose", null, initialValue, + GhidraFileChooserMode.DIRECTORIES_ONLY); + } + + /** + * Prompts the user to entry the path to a file and/or directory, + * or to pick it using a browser dialog. + *

    + * + * @param title the dialog title + * @param prompt HTML enabled prompt + * @param chooseButtonText text of the choose button in the browser dialog + * @param directory the initial directory of the browser dialog + * @param initialFileValue the initial value to pre-populate the input field with + * @param chooserMode {@link GhidraFileChooserMode} of the browser dialog + * @param fileFilters optional {@link GhidraFileFilter filters} + * @return the {@link File} the user entered / picked, or null if canceled + */ + public static File chooseFile(String title, String prompt, String chooseButtonText, + File directory, File initialFileValue, GhidraFileChooserMode chooserMode, + GhidraFileFilter... fileFilters) { + FilePromptDialog filePromptDialog = new FilePromptDialog(title, prompt, chooseButtonText, + directory, initialFileValue, chooserMode, fileFilters); + DockingWindowManager.showDialog(filePromptDialog); + return filePromptDialog.chosenValue; + } + + private GhidraFileChooser chooser; + private GhidraFileFilter[] fileFilters; + private File directory; + private File file; + private String approveButtonText; + private JTextField filePathTextField; + private GhidraFileChooserMode chooserMode; + private File chosenValue; + + protected FilePromptDialog(String title, String prompt, String approveButtonText, + File directory, File file, GhidraFileChooserMode chooserMode, + GhidraFileFilter... fileFilters) { + super(title, true, false, true, false); + + this.approveButtonText = approveButtonText; + this.directory = directory; + this.file = file; + this.chooserMode = chooserMode; + this.fileFilters = fileFilters; + setRememberSize(false); + + build(prompt); + updateButtonEnablement(); + } + + private void build(String prompt) { + + GHtmlLabel promptLabel = new GHtmlLabel(prompt); + filePathTextField = new JTextField(file != null ? file.getPath() : null, 40); + filePathTextField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void removeUpdate(DocumentEvent e) { + updateButtonEnablement(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + updateButtonEnablement(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + updateButtonEnablement(); + } + }); + JButton browseButton = ButtonPanelFactory.createButton(ButtonPanelFactory.BROWSE_TYPE); + browseButton.addActionListener(e -> browse()); + + JPanel textFieldWithButtonPanel = new JPanel(new BorderLayout()); + textFieldWithButtonPanel.add(filePathTextField, BorderLayout.CENTER); + textFieldWithButtonPanel.add(browseButton, BorderLayout.EAST); + + JPanel mainPanel = new JPanel(new PairLayout()); + mainPanel.add(promptLabel); + mainPanel.add(textFieldWithButtonPanel); + Dimension size = mainPanel.getPreferredSize(); + size.width = Math.max(size.width, 500); + mainPanel.setPreferredSize(size); + mainPanel.setMinimumSize(size); + JPanel newMain = new JPanel(new BorderLayout()); + newMain.add(mainPanel, BorderLayout.CENTER); + + addWorkPanel(newMain); + addOKButton(); + addCancelButton(); + } + + private void updateButtonEnablement() { + okButton.setEnabled(!filePathTextField.getText().isBlank()); + } + + @Override + protected void okCallback() { + chosenValue = new File(filePathTextField.getText()); + close(); + } + + @Override + protected void cancelCallback() { + chosenValue = null; + close(); + } + + private void browse() { + initChooser(); + String filePathText = filePathTextField.getText(); + filePathText = filePathText.isBlank() && file != null ? file.getPath() : ""; + if (!filePathText.isBlank()) { + chooser.setSelectedFile(new File(filePathText)); + } + File selectedFile = chooser.getSelectedFile(); + if (selectedFile != null) { + filePathTextField.setText(selectedFile.getPath()); + } + filePathTextField.requestFocusInWindow(); + } + + private void initChooser() { + + if (chooser == null) { + chooser = new GhidraFileChooser(rootPanel); + for (GhidraFileFilter gff : fileFilters) { + chooser.addFileFilter(gff); + } + chooser.setMultiSelectionEnabled(false); + chooser.setApproveButtonText(approveButtonText); + chooser.setFileSelectionMode(chooserMode); + chooser.setTitle(getTitle()); + + if (directory != null) { + chooser.setCurrentDirectory(directory); + } + } + } +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/LoadPdbDialog.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/LoadPdbDialog.java new file mode 100644 index 0000000000..00087d802c --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/LoadPdbDialog.java @@ -0,0 +1,942 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver.ui; + +import java.awt.*; +import java.awt.event.*; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import javax.swing.*; + +import docking.DialogComponentProvider; +import docking.DockingWindowManager; +import docking.event.mouse.GMouseListenerAdapter; +import docking.options.editor.ButtonPanelFactory; +import docking.widgets.OptionDialog; +import docking.widgets.checkbox.GCheckBox; +import docking.widgets.combobox.GComboBox; +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.filechooser.GhidraFileChooserMode; +import docking.widgets.label.GIconLabel; +import docking.widgets.label.GLabel; +import docking.widgets.textfield.HintTextField; +import docking.widgets.textfield.IntegerTextField; +import ghidra.app.util.bin.format.pdb.PdbParser; +import ghidra.app.util.pdb.pdbapplicator.PdbApplicatorControl; +import ghidra.framework.preferences.Preferences; +import ghidra.program.model.listing.Program; +import ghidra.util.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.filechooser.ExtensionFileFilter; +import ghidra.util.filechooser.GhidraFileFilter; +import ghidra.util.layout.PairLayout; +import ghidra.util.task.*; +import pdb.PdbPlugin; +import pdb.symbolserver.*; +import resources.Icons; +import resources.ResourceManager; + +/** + * A dialog that allows the user to pick or search for a Pdb file for a program. + */ +public class LoadPdbDialog extends DialogComponentProvider { + + private static final String LAST_PDBFILE_PREFERENCE_KEY = "Pdb.LastFile"; + static final Icon MATCH_OK_ICON = + ResourceManager.loadImage("images/checkmark_green.gif", 16, 16); + static final Icon MATCH_BAD_ICON = + ResourceManager.loadImage("images/emblem-important.png", 16, 16); + public static final GhidraFileFilter PDB_FILES_FILTER = + ExtensionFileFilter.forExtensions("Microsoft Program Databases", "pdb", "pd_", "pdb.xml"); + + public static class LoadPdbResults { + public File pdbFile; + public PdbApplicatorControl control; + public boolean useMsDiaParser; + public boolean debugLogging; + } + + /** + * Shows a modal dialog to the user, allowing them to pick or search for a Pdb + * file.

    + * The selected file and parser options are returned in a LoadPdbResults instance. + * + * @param program the Ghidra {@link Program} that has Pdb info + * @return LoadPdbResults instance with the selected file and options, or null if canceled + */ + public static LoadPdbResults choosePdbForProgram(Program program) { + LoadPdbDialog choosePdbDlg = new LoadPdbDialog(program); + DockingWindowManager.showDialog(choosePdbDlg); + File pdbFile = choosePdbDlg.getLocalSymbolFile(choosePdbDlg.selectedSymbolFile); + if (pdbFile == null) { + return null; + } + LoadPdbResults results = new LoadPdbResults(); + results.pdbFile = pdbFile; + results.control = + (PdbApplicatorControl) choosePdbDlg.restrictionsCombo.getSelectedItem(); + results.useMsDiaParser = choosePdbDlg.msdiaParserButton.isSelected(); + results.debugLogging = choosePdbDlg.debugLoggingCheckbox.isSelected(); + return results; + } + + private SymbolFileLocation selectedSymbolFile; + + private SymbolServerService symbolServerService; + private SymbolServerInstanceCreatorContext symbolServerInstanceCreatorContext; + + private SymbolFileInfo programSymbolFileInfo; + + private List> statusTextSuppliers = new ArrayList<>(); + private boolean hasPerformedSearch; + private boolean searchCanceled; + + private Program program; + + private SymbolServerPanel symbolServerConfigPanel; + private SymbolFilePanel symbolFilePanel; + + private JTextField programNameTextField; + private JTextField pdbPathTextField; + private GCheckBox overridePdbPathCheckBox; + private JTextField pdbUniqueIdTextField; + private GCheckBox overridePdbUniqueIdCheckBox; + private IntegerTextField pdbAgeTextField; + private GCheckBox overridePdbAgeCheckBox; + private HintTextField pdbLocationTextField; + private GIconLabel exactMatchIconLabel; + + private JToggleButton advancedToggleButton; + + private GhidraFileChooser chooser; + + private JButton choosePdbLocationButton; + private JButton loadPdbButton; + + private JPanel pdbLocationPanel; + private JPanel programPdbPanel; + private JComponent workComp; + + private JPanel parserOptionsPanel; + private JRadioButton universalParserButton; + private JRadioButton msdiaParserButton; + private GComboBox restrictionsCombo; + private GCheckBox debugLoggingCheckbox; + + /** + * Creates a new instance of the LoadPdbDialog class. + * + * @param program the ghidra {@link Program} that is loading the Pdb + */ + public LoadPdbDialog(Program program) { + super("Load PDB for " + program.getName(), true, true, true, true); + setRememberSize(false); + + this.program = program; + this.programSymbolFileInfo = SymbolFileInfo.fromMetadata(program.getMetadata()); + if (programSymbolFileInfo == null) { + programSymbolFileInfo = SymbolFileInfo.unknown("missing"); + } + this.symbolServerInstanceCreatorContext = + SymbolServerInstanceCreatorRegistry.getInstance().getContext(program); + this.symbolServerService = + PdbPlugin.getSymbolServerService(symbolServerInstanceCreatorContext); + + build(); + } + + @Override + protected void dialogShown() { + pdbPathTextField.setText(programSymbolFileInfo.getPath()); + pdbUniqueIdTextField.setText(programSymbolFileInfo.getUniqueName()); + pdbAgeTextField.setValue(programSymbolFileInfo.getIdentifiers().getAge()); + programNameTextField.setText(program.getName()); + cancelButton.requestFocusInWindow(); + + executeMonitoredRunnable("Search for PDB using built-in locations", true, true, 0, + this::doInitialDefaultSearch); + } + + private void doInitialDefaultSearch(TaskMonitor monitor) { + try { + List results = + symbolServerService.find(programSymbolFileInfo, FindOption.NO_OPTIONS, monitor); + if (!results.isEmpty()) { + SymbolFileLocation symbolFileLocation = + symbolServerService.getLocalSymbolFileLocation(results.get(0), monitor); + File symbolFile = getLocalSymbolFile(symbolFileLocation); + Swing.runLater(() -> { + setSearchResults(results); + setPdbLocationValue(symbolFileLocation, symbolFile); + setSelectedPdbFile(symbolFileLocation); + selectRowByLocation(symbolFileLocation); + updateStatusText(); + updateButtonEnablement(); + updateParserOptionEnablement(true); + }); + } + } + catch (CancelledException | IOException e) { + // ignore + } + } + + @Override + protected void cancelCallback() { + selectedSymbolFile = null; + close(); + } + + /** + * For screenshot use only + * + * @param options set of {@link FindOption} enum + */ + public void setSearchOptions(Set options) { + symbolFilePanel.setFindOptions(options); + } + + /** + * For screenshot use only + * + * @param pathStr path of symbol storage directory + */ + public void setSymbolStorageDirectoryTextOnly(String pathStr) { + symbolServerConfigPanel.setSymbolStorageDirectoryTextOnly(pathStr); + } + + /** + * For screenshot use only + * + * @param symbolServers list of symbol servers + */ + public void setSymbolServers(List symbolServers) { + symbolServerConfigPanel.setSymbolServers(symbolServers); + } + + /** + * For screenshot use only + */ + public void pushAddLocationBution() { + symbolServerConfigPanel.pushAddLocationButton(); + } + + private void setSelectedPdbFile(SymbolFileLocation symbolFileLocation) { + this.selectedSymbolFile = symbolFileLocation; + } + + /** + * Sets the contents of the search results table. + *

    + * Public only for screenshot usage, treat as private otherwise. + * + * @param results list of {@link SymbolFileLocation}s to add to results + */ + public void setSearchResults(List results) { + hasPerformedSearch = true; + symbolFilePanel.getTableModel().setSearchResults(programSymbolFileInfo, results); + } + + /** + * Selects a row in the results table. + *

    + * Public only for screenshot usage. Treat as private. + * + * @param symbolFileLocation {@link SymbolFileLocation} to select in results table + */ + public void selectRowByLocation(SymbolFileLocation symbolFileLocation) { + for (int i = 0; i < symbolFilePanel.getTableModel().getModelData().size(); i++) { + SymbolFileRow symbolFileRow = symbolFilePanel.getTableModel().getModelData().get(i); + if (symbolFileRow.getLocation().equals(symbolFileLocation)) { + symbolFilePanel.getTable().selectRow(i); + return; + } + } + symbolFilePanel.getTable().clearSelection(); + } + + private StatusText getSelectedPdbNoticeText() { + if (selectedSymbolFile == null) { + return null; + } + if (selectedSymbolFile.getFileInfo() == null) { + return new StatusText("Unable to read Pdb information", MessageType.ERROR, false); + } + return !selectedSymbolFile.isExactMatch(programSymbolFileInfo) + ? new StatusText("WARNING: Selected PDB is not an exact match!", + MessageType.WARNING, false) + : null; + } + + private String getSymbolFileToolText(SymbolFileLocation symbolFileLocation) { + return symbolFileLocation != null + ? String.format( + "" + + "" + + "" + + "" + + "" + + "
    PDB Name:%s
    Path:%s
    GUID/ID:%s
    Age:%x
    Is Exact Match:%b" + + "
    ", + HTMLUtilities.escapeHTML(symbolFileLocation.getFileInfo().getName()), + HTMLUtilities.escapeHTML(symbolFileLocation.getLocationStr()), + symbolFileLocation.getFileInfo().getUniqueName(), + symbolFileLocation.getFileInfo().getIdentifiers().getAge(), + symbolFileLocation.getFileInfo().isExactMatch(programSymbolFileInfo)) + : null; + } + + private void updateButtonEnablement() { + boolean hasLocation = selectedSymbolFile != null; + loadPdbButton.setEnabled(hasLocation); + } + + private void setSymbolServerService(SymbolServerService symbolServerService) { + this.symbolServerService = symbolServerService; + symbolFilePanel.setEnablement(symbolServerService != null); + updateStatusText(); + } + + private SymbolFileInfo getCurrentSymbolFileInfo() { + String pdbPath = pdbPathTextField.getText(); + String uid = pdbUniqueIdTextField.getText(); + int age = pdbAgeTextField.getIntValue(); + + return SymbolFileInfo.fromValues(pdbPath, uid, age); + } + + private void searchForPdbs(ActionEvent e) { + if (symbolServerService == null || !symbolServerService.isValid()) { + return; + } + if (pdbAgeTextField.getText().isBlank()) { + Msg.showWarn(this, null, "Bad PDB Age", "Invalid PDB Age value"); + return; + } + SymbolFileInfo symbolFileInfo = getCurrentSymbolFileInfo(); + if (symbolFileInfo == null) { + Msg.showWarn(this, null, "Bad PDB GUID/Id", + "Invalid PDB GUID / UID value: " + pdbUniqueIdTextField.getText()); + return; + } + Set findOptions = symbolFilePanel.getFindOptions(); + executeMonitoredRunnable("Search for PDBs", true, true, 0, monitor -> { + try { + searchCanceled = false; + List results = + symbolServerService.find(symbolFileInfo, findOptions, monitor); + Swing.runLater(() -> { + setSearchResults(results); + if (results.size() == 1) { + selectRowByLocation(results.get(0)); + } + updateStatusText(); + updateButtonEnablement(); + }); + } + catch (CancelledException e1) { + searchCanceled = true; + Swing.runLater(() -> updateStatusText()); + } + }); + + } + + private void build() { + buildSymbolFilePanel(); + buildSSConfigPanel(); + buildPdbLocationPanel(); + buildProgramPdbPanel(); + buildParserOptionsPanel(); + setHelpLocation(new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, "Load PDB File")); + + addStatusTextSupplier(() -> hasPerformedSearch && advancedToggleButton.isSelected() + ? symbolServerConfigPanel.getSymbolServerWarnings() + : null); + addStatusTextSupplier(this::getSelectedPdbNoticeText); + addStatusTextSupplier(this::getConfigChangedWarning); + addStatusTextSupplier(this::getAllowRemoteWarning); + addStatusTextSupplier(this::getFoundCountInfo); + + addButtons(); + layoutSimple(); + + updateStatusText(); + updateButtonEnablement(); + // later dialogShow() will be called + } + + private void buildSSConfigPanel() { + symbolServerConfigPanel = + new SymbolServerPanel(this::setSymbolServerService, symbolServerInstanceCreatorContext); + } + + private void buildSymbolFilePanel() { + symbolFilePanel = new SymbolFilePanel(this::searchForPdbs); // panel will be added in layoutAdvanced() + + symbolFilePanel.getTable() + .getSelectionModel() + .addListSelectionListener(e -> updateSelectedRow()); + symbolFilePanel.addMouseListener(new GMouseListenerAdapter() { + @Override + public void doubleClickTriggered(MouseEvent e) { + if (loadPdbButton.isEnabled()) { + e.consume(); + loadPdbButton.doClick(); + } + } + }); + } + + private void updateSelectedRow() { + SymbolFileRow row = symbolFilePanel.getSelectedRow(); + setSelectedPdbFile(row != null ? row.getLocation() : null); + updateStatusText(); + updateButtonEnablement(); + updateParserOptionEnablement(true); + } + + private JPanel buildProgramPdbPanel() { + + programNameTextField = new BetterNonEditableTextField(20); + programNameTextField.setEditable(false); + + pdbPathTextField = new BetterNonEditableTextField(20); + pdbPathTextField.setEditable(false); + + overridePdbPathCheckBox = new GCheckBox(); + overridePdbPathCheckBox.setVisible(false); + overridePdbPathCheckBox.setToolTipText("Override PDB name (when searching)."); + overridePdbPathCheckBox.addItemListener(e -> { + pdbPathTextField.setEditable(overridePdbPathCheckBox.isSelected()); + if (overridePdbPathCheckBox.isSelected()) { + pdbPathTextField.requestFocusInWindow(); + } + else { + pdbPathTextField.setText(programSymbolFileInfo.getPath()); + } + }); + DockingWindowManager.getHelpService() + .registerHelp(overridePdbPathCheckBox, + new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, + SymbolFilePanel.SEARCH_OPTIONS_HELP_ANCHOR)); + + pdbUniqueIdTextField = new BetterNonEditableTextField(36); + pdbUniqueIdTextField.setEditable(false); + pdbUniqueIdTextField.setToolTipText( + "PDB GUID - either 36 or 32 hexadecimal characters:
    " + + "  '012345678-0123-0123-0123-0123456789ABC' or '0123456780123012301230123456789ABC', or
    " + + "PDB Signature Id - 8 hexadecimal character Id:
    " + + "  '11223344'"); + + overridePdbUniqueIdCheckBox = new GCheckBox(); + overridePdbUniqueIdCheckBox.setVisible(false); + overridePdbUniqueIdCheckBox.setToolTipText("Override PDB unique id (when searching)."); + overridePdbUniqueIdCheckBox.addItemListener(e -> { + pdbUniqueIdTextField.setEditable(overridePdbUniqueIdCheckBox.isSelected()); + if (overridePdbUniqueIdCheckBox.isSelected()) { + pdbUniqueIdTextField.requestFocusInWindow(); + } + else { + pdbUniqueIdTextField.setText(programSymbolFileInfo.getUniqueName()); + } + }); + DockingWindowManager.getHelpService() + .registerHelp(overridePdbUniqueIdCheckBox, + new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, + SymbolFilePanel.SEARCH_OPTIONS_HELP_ANCHOR)); + + pdbAgeTextField = new IntegerTextField(8); + pdbAgeTextField.setAllowNegativeValues(false); + pdbAgeTextField.setShowNumberMode(true); + pdbAgeTextField.setHexMode(); + pdbAgeTextField.setEditable(false); + + overridePdbAgeCheckBox = new GCheckBox(); + overridePdbAgeCheckBox.setVisible(false); + overridePdbAgeCheckBox.setToolTipText("Override PDB age (when searching)."); + overridePdbAgeCheckBox.addItemListener(e -> { + pdbAgeTextField.setEditable(overridePdbAgeCheckBox.isSelected()); + if (overridePdbAgeCheckBox.isSelected()) { + pdbAgeTextField.requestFocus(); + } + else { + pdbAgeTextField.setValue(programSymbolFileInfo.getIdentifiers().getAge()); + } + }); + DockingWindowManager.getHelpService() + .registerHelp(overridePdbAgeCheckBox, + new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, + SymbolFilePanel.SEARCH_OPTIONS_HELP_ANCHOR)); + + programPdbPanel = new JPanel(new PairLayout(5, 5)); + programPdbPanel.setBorder(BorderFactory.createTitledBorder("Program PDB Information")); + programPdbPanel.add(new GLabel("Program:", SwingConstants.RIGHT)); + programPdbPanel.add(programNameTextField); + + programPdbPanel.add( + join(null, new GLabel("PDB Name:", SwingConstants.RIGHT), overridePdbPathCheckBox)); + programPdbPanel.add(pdbPathTextField); + + programPdbPanel.add(join(null, new GLabel("PDB Unique Id:", SwingConstants.RIGHT), + overridePdbUniqueIdCheckBox)); + programPdbPanel.add(pdbUniqueIdTextField); + + programPdbPanel.add( + join(null, new GLabel("PDB Age:", SwingConstants.RIGHT), overridePdbAgeCheckBox)); + programPdbPanel.add(join(pdbAgeTextField.getComponent(), new JPanel(), null)); + + return programPdbPanel; + } + + private JPanel buildPdbLocationPanel() { + pdbLocationTextField = new HintTextField("Browse [...] for PDB file or use 'Advanced'"); + pdbLocationTextField.setEditable(false); + + choosePdbLocationButton = ButtonPanelFactory.createButton(ButtonPanelFactory.BROWSE_TYPE); + choosePdbLocationButton.addActionListener(e -> choosePdbFile()); + + exactMatchIconLabel = new GIconLabel(Icons.EMPTY_ICON); + + pdbLocationPanel = new JPanel(new PairLayout(5, 5)); + pdbLocationPanel.setBorder(BorderFactory.createTitledBorder("PDB Location")); + pdbLocationPanel.add(new GLabel("PDB Location:", SwingConstants.RIGHT)); + pdbLocationPanel + .add(join(exactMatchIconLabel, pdbLocationTextField, choosePdbLocationButton)); + return pdbLocationPanel; + } + + private void updateParserOptionEnablement(boolean trySetUniversal) { + if (trySetUniversal) { + universalParserButton.setSelected(true); + msdiaParserButton.setSelected(false); + } + + boolean isXML = (selectedSymbolFile != null && + selectedSymbolFile.getPath().toLowerCase().endsWith(".pdb.xml")); + boolean isWindows = PdbParser.onWindows; + msdiaParserButton.setEnabled(isXML || isWindows); + if (isXML) { + msdiaParserButton.setSelected(true); + } + if (msdiaParserButton.isSelected() && !msdiaParserButton.isEnabled()) { + msdiaParserButton.setSelected(false); + } + if (!isWindows && !isXML) { + universalParserButton.setSelected(true); + } + universalParserButton.setEnabled(!isXML); + if (universalParserButton.isSelected() && !universalParserButton.isEnabled()) { + universalParserButton.setSelected(false); + } + restrictionsCombo.setEnabled(universalParserButton.isSelected()); + debugLoggingCheckbox.setEnabled(universalParserButton.isSelected()); + } + + private JPanel buildParserOptionsPanel() { + + ActionListener l = (e) -> updateParserOptionEnablement(false); + universalParserButton = new JRadioButton("Universal"); + universalParserButton + .setToolTipText("Platform-independent PDB analyzer (No PDB.XML support)."); + msdiaParserButton = new JRadioButton("MSDIA"); + msdiaParserButton.setToolTipText( + "Legacy PDB Analyzer.
    " + + "Requires MS DIA-SDK for raw PDB processing (Windows only), or preprocessed PDB.XML file."); + universalParserButton.setSelected(true); + universalParserButton.addActionListener(l); + msdiaParserButton.addActionListener(l); + + ButtonGroup buttonGroup = new ButtonGroup(); + buttonGroup.add(msdiaParserButton); + buttonGroup.add(universalParserButton); + + JPanel radioButtons = new JPanel(new FlowLayout(FlowLayout.LEFT)); + radioButtons.add(universalParserButton); + radioButtons.add(msdiaParserButton); + + restrictionsCombo = new GComboBox<>(PdbApplicatorControl.values()); + restrictionsCombo.setSelectedItem(PdbApplicatorControl.ALL); + + debugLoggingCheckbox = new GCheckBox(); + debugLoggingCheckbox.setToolTipText( + "If checked, logs information to the pdb.analyzer.log file for debug/development."); + + parserOptionsPanel = new JPanel(new PairLayout(5, 5)); + parserOptionsPanel.setBorder(BorderFactory.createTitledBorder("PDB Parser")); + DockingWindowManager.getHelpService() + .registerHelp(parserOptionsPanel, + new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, + "PDB Parser Panel")); + + parserOptionsPanel.add(new GLabel("Parser:")); + parserOptionsPanel.add(radioButtons); + + parserOptionsPanel.add(new GLabel("Control:")); + parserOptionsPanel.add(restrictionsCombo); + + parserOptionsPanel.add(new GLabel("[Dev] PDB Reader/Applicator Debug Logging:")); + parserOptionsPanel.add(debugLoggingCheckbox); + + return parserOptionsPanel; + } + + private void addButtons() { + + loadPdbButton = new JButton("Load"); + loadPdbButton.setName("Load"); + + loadPdbButton.addActionListener(e -> { + if (selectedSymbolFile == null || + (!selectedSymbolFile.isExactMatch(programSymbolFileInfo) && + OptionDialog.showYesNoDialog(loadPdbButton, "Mismatched Pdb File Warning", + "The selected file is not an exact match for the current program.
    " + + "Note: Invalid disassembly may be produced!
    Continue anyway?") != OptionDialog.YES_OPTION)) { + return; + } + executeMonitoredRunnable("Prepare Selected Symbol File", + true, true, 0, this::prepareSelectedSymbolFileAndClose); + }); + addButton(loadPdbButton); + + addCancelButton(); + setDefaultButton(cancelButton); + + advancedToggleButton = new JToggleButton("Advanced >>"); + advancedToggleButton.addActionListener(e -> toggleAdvancedSearch()); + buttonPanel.add(advancedToggleButton); + } + + private void prepareSelectedSymbolFileAndClose(TaskMonitor monitor) { + try { + if (selectedSymbolFile != null && symbolServerService != null) { + selectedSymbolFile = + symbolServerService.getLocalSymbolFileLocation(selectedSymbolFile, monitor); + } + Swing.runLater(() -> close()); + return; + } + catch (IOException ioe) { + Msg.showError(this, getComponent(), "Error Getting Symbol File", ioe); + } + catch (CancelledException ce) { + // ignore + } + } + + private StatusText getConfigChangedWarning() { + return advancedToggleButton.isSelected() && symbolServerConfigPanel.isConfigChanged() + ? new StatusText( + "Symbol Server Search Config Changed. Click \"Save Configuration\" button to save.", + MessageType.INFO, false) + : null; + } + + private StatusText getAllowRemoteWarning() { + int remoteSymbolServerCount = + symbolServerService != null ? symbolServerService.getRemoteSymbolServerCount() : 0; + Set findOptions = symbolFilePanel.getFindOptions(); + return hasPerformedSearch && advancedToggleButton.isSelected() && + remoteSymbolServerCount != 0 && !findOptions.contains(FindOption.ALLOW_REMOTE) + ? new StatusText( + "Remote servers were excluded. Select \"Allow remote\" checkbox to search remote servers.", + MessageType.INFO, false) + : null; + } + + private StatusText getFoundCountInfo() { + if (advancedToggleButton.isSelected()) { + if (searchCanceled) { + return new StatusText("Search canceled", MessageType.INFO, false); + } + if (hasPerformedSearch) { + int foundCount = symbolFilePanel.getTableModel().getModelData().size(); + return new StatusText( + "Found " + foundCount + " file" + (foundCount != 1 ? "s" : ""), + MessageType.INFO, false); + } + } + return null; + } + + private void toggleAdvancedSearch() { + boolean isAdvanced = advancedToggleButton.isSelected(); + advancedToggleButton.setText("Advanced " + (isAdvanced ? "<<" : ">>")); + + overridePdbAgeCheckBox.setVisible(isAdvanced); + overridePdbPathCheckBox.setVisible(isAdvanced); + overridePdbUniqueIdCheckBox.setVisible(isAdvanced); + setPdbLocationValue(null, null); + + if (isAdvanced) { + if (symbolServerService == null || !symbolServerService.isValid()) { + setSelectedPdbFile(null); + } + layoutAdvanced(); + } + else { + if (selectedSymbolFile != null) { + File localSymbolFile = getLocalSymbolFile(selectedSymbolFile); + if (localSymbolFile != null) { + setPdbLocationValue(selectedSymbolFile, localSymbolFile); + } + } + else { + setSelectedPdbFile(null); + } + layoutSimple(); + } + + updateStatusText(); + updateButtonEnablement(); + updateParserOptionEnablement(false); + repack(); + } + + private void layoutSimple() { + Box box = Box.createVerticalBox(); + box.add(programPdbPanel); + box.add(pdbLocationPanel); + box.add(parserOptionsPanel); + + JPanel panel = new JPanel(new BorderLayout()); + panel.add(box, BorderLayout.NORTH); + + overrideWorkPanel(panel); + } + + private void overrideWorkPanel(JComponent workComp) { + if (this.workComp != null && this.workComp.getParent() != null) { + this.workComp.getParent().remove(this.workComp); + } + this.workComp = workComp; + addWorkPanel(workComp); + } + + private void layoutAdvanced() { + Box topPanel = Box.createHorizontalBox(); + topPanel.add(programPdbPanel); + topPanel.add(symbolServerConfigPanel); + + JPanel mainPanel = new JPanel(new BorderLayout()); + mainPanel.add(topPanel, BorderLayout.NORTH); + mainPanel.add(symbolFilePanel, BorderLayout.CENTER); + mainPanel.add(parserOptionsPanel, BorderLayout.SOUTH); + + overrideWorkPanel(mainPanel); + } + + private void choosePdbFile() { + File file = getChooser().getSelectedFile(); + if (file != null && file.isFile()) { + Preferences.setProperty(LAST_PDBFILE_PREFERENCE_KEY, file.getPath()); + executeMonitoredRunnable("Get PDB Info", true, true, 0, monitor -> { + SymbolFileInfo pdbSymbolFileInfo = SymbolFileInfo.fromFile(file, monitor); + if (pdbSymbolFileInfo == null) { + pdbSymbolFileInfo = SymbolFileInfo.unknown(file.getName()); + } + SymbolFileLocation symbolFileLocation = + SameDirSymbolStore.createManuallySelectedSymbolFileLocation(file, + pdbSymbolFileInfo); + Swing.runLater(() -> { + setSearchResults(List.of(symbolFileLocation)); + setSelectedPdbFile(symbolFileLocation); + setPdbLocationValue(symbolFileLocation, file); + selectRowByLocation(symbolFileLocation); + hasPerformedSearch = false; + updateStatusText(); + updateButtonEnablement(); + updateParserOptionEnablement(true); + }); + }); + + } + } + + private void setPdbLocationValue(SymbolFileLocation symbolFileLocation, File file) { + boolean isExactMatch = symbolFileLocation != null + ? symbolFileLocation.isExactMatch(programSymbolFileInfo) + : false; + pdbLocationTextField.setText(file != null ? file.getPath() : ""); + pdbLocationTextField.setToolTipText(getSymbolFileToolText(symbolFileLocation)); + exactMatchIconLabel + .setIcon(file == null ? null : isExactMatch ? MATCH_OK_ICON : MATCH_BAD_ICON); + exactMatchIconLabel.setToolTipText( + file == null ? null : isExactMatch ? "Exact match" : "Not exact match"); + + } + + private GhidraFileChooser getChooser() { + + if (chooser == null) { + chooser = new GhidraFileChooser(getComponent()); + chooser.addFileFilter(PDB_FILES_FILTER); + chooser.setMultiSelectionEnabled(false); + chooser.setApproveButtonText("Choose"); + chooser.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY); + chooser.setTitle("Select PDB"); + + String lastFile = Preferences.getProperty(LAST_PDBFILE_PREFERENCE_KEY); + if (lastFile != null) { + chooser.setSelectedFile(new File(lastFile)); + } + } + + return chooser; + } + + /** + * Adds a supplier of status text messages. The supplier will be polled + * whenever the updateStatusText() method is called. + *

    + * Use this status text scheme instead of {@link #setStatusText(String)} if + * there are multiple locations that need to provide a status message at the + * bottom of the dialog. + * + * @param supplier StatusText supplier + */ + private void addStatusTextSupplier(Supplier supplier) { + statusTextSuppliers.remove(supplier); + statusTextSuppliers.add(supplier); + } + + /** + * Polls all {@link #addStatusTextSupplier(Supplier) registered} StatusText suppliers and + * sets the status message at the bottom of the dialog to the resulting message. + *

    + * Not compatible with {@link #setStatusText(String)}. Either use it, or this. + */ + private void updateStatusText() { + StringBuilder sb = new StringBuilder(); + boolean alert = false; + MessageType mt = MessageType.INFO; + for (Supplier supplier : statusTextSuppliers) { + StatusText statusText = supplier.get(); + if (statusText != null && statusText.message != null && !statusText.message.isEmpty()) { + if (sb.length() != 0) { + sb.append("
    "); + } + sb.append(HTMLUtilities.colorString(getStatusColor(statusText.messageType), + statusText.message)); + alert |= statusText.alert; + if (mt.ordinal() < statusText.messageType.ordinal()) { + mt = statusText.messageType; + } + } + } + if (sb.length() != 0) { + setStatusText("" + sb.toString(), mt, alert); + } + else { + clearStatusText(); + } + + } + + private File getLocalSymbolFile(SymbolFileLocation symbolFileLocation) { + if (symbolFileLocation == null) { + return null; + } + SymbolServer symbolServer = symbolFileLocation.getSymbolServer(); + if (!(symbolServer instanceof SymbolStore)) { + return null; + } + SymbolStore symbolStore = (SymbolStore) symbolServer; + File file = symbolStore.getFile(symbolFileLocation.getPath()); + return SymbolStore.isCompressedFilename(file.getName()) ? null : file; + } + + /** + * Execute a non-modal task that has progress and can be cancelled. + *

    + * See {@link #executeProgressTask(Task, int)}. + * + * @param taskTitle String title of task + * @param canCancel boolean flag, if true task can be canceled by the user + * @param hasProgress boolean flag, if true the task has a progress meter + * @param delay int number of milliseconds to delay before showing the task's + * progress + * @param runnable {@link MonitoredRunnable} to run + */ + private void executeMonitoredRunnable(String taskTitle, boolean canCancel, + boolean hasProgress, int delay, MonitoredRunnable runnable) { + Task task = new Task(taskTitle, canCancel, hasProgress, false) { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + runnable.monitoredRun(monitor); + } + }; + executeProgressTask(task, delay); + } + + //----------------------------------------------------------------------------------- + + static class StatusText { + + public StatusText(String message, MessageType messageType, boolean alert) { + this.message = message; + this.messageType = messageType; + this.alert = alert; + } + + public String message; + public MessageType messageType; + public boolean alert; + } + + static JPanel join(JComponent left, JComponent main, JComponent right) { + JPanel panel = new JPanel(new BorderLayout()); + if (left != null) { + panel.add(left, BorderLayout.WEST); + } + panel.add(main, BorderLayout.CENTER); + if (right != null) { + panel.add(right, BorderLayout.EAST); + } + + return panel; + } + + /** + * A customized JTextField that changes the background of non-editable + * text fields to be the same color as the parent container's background. + */ + static class BetterNonEditableTextField extends JTextField { + + BetterNonEditableTextField(int columns) { + super(columns); + } + + @Override + public Color getBackground() { + Container parent = getParent(); + if (parent != null && isEditable() == false) { + Color bg = parent.getBackground(); + // mint a new Color object to avoid it being + // ignored because the parent handed us a DerivedColor + // instance + return new Color(bg.getRGB()); + } + return super.getBackground(); + } + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolFilePanel.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolFilePanel.java new file mode 100644 index 0000000000..b4848cfee5 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolFilePanel.java @@ -0,0 +1,182 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver.ui; + +import java.util.EnumSet; +import java.util.Set; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.ActionListener; + +import javax.swing.*; +import javax.swing.table.TableColumn; + +import docking.DockingWindowManager; +import docking.widgets.checkbox.GCheckBox; +import docking.widgets.label.GHtmlLabel; +import docking.widgets.label.GLabel; +import ghidra.util.HelpLocation; +import ghidra.util.table.GhidraTable; +import pdb.PdbPlugin; +import pdb.symbolserver.FindOption; + +/** + * Displays the results of a 'find' operation in a table. + * Also allows the user to tweak search options. + */ +class SymbolFilePanel extends JPanel { + static final String SEARCH_OPTIONS_HELP_ANCHOR = "PDB_Search_Search_Options"; + private SymbolFileTableModel tableModel; + private GhidraTable table; + + private JPanel tablePanel; + private JPanel welcomePanel; + + private JButton searchButton; + private GCheckBox allowRemote; + private GCheckBox ignorePdbUid; + private GCheckBox ignorePdbAge; + + SymbolFilePanel(ActionListener searchButtonActionListener) { + super(new BorderLayout()); + + build(); + setEnablement(false); + searchButton.addActionListener(searchButtonActionListener); + } + + SymbolFileTableModel getTableModel() { + return tableModel; + } + + GhidraTable getTable() { + return table; + } + + Set getFindOptions() { + Set findOptions = EnumSet.noneOf(FindOption.class); + if (allowRemote.isSelected()) { + findOptions.add(FindOption.ALLOW_REMOTE); + } + if (ignorePdbAge.isSelected()) { + findOptions.add(FindOption.ANY_AGE); + } + if (ignorePdbUid.isSelected()) { + findOptions.add(FindOption.ANY_ID); + } + return findOptions; + } + + void setFindOptions(Set findOptions) { + allowRemote.setSelected(findOptions.contains(FindOption.ALLOW_REMOTE)); + ignorePdbAge.setSelected(findOptions.contains(FindOption.ANY_AGE)); + ignorePdbUid.setSelected(findOptions.contains(FindOption.ANY_ID)); + } + + void setEnablement(boolean hasSymbolServerService) { + searchButton.setEnabled(hasSymbolServerService); + + if (welcomePanel != null && hasSymbolServerService) { + remove(welcomePanel); + welcomePanel = null; + add(tablePanel, BorderLayout.CENTER); + revalidate(); + } + } + + SymbolFileRow getSelectedRow() { + return table.getSelectedRow() != -1 + ? tableModel.getRowObject(table.getSelectedRow()) + : null; + } + + int getSelectedRowIndex() { + return table.getSelectedRow(); + } + + private void build() { + setBorder(BorderFactory.createTitledBorder("PDB Search")); + add(buildButtonPanel(), BorderLayout.NORTH); + buildTable(); // don't add it yet + add(buildWelcomePanel(), BorderLayout.CENTER); + } + + private JPanel buildWelcomePanel() { + welcomePanel = new JPanel(); + welcomePanel.add(new GHtmlLabel( + "

    Local Symbol Storage location must be set first!")); + welcomePanel.setPreferredSize(tablePanel.getPreferredSize()); + + return welcomePanel; + } + + private JPanel buildTable() { + this.tableModel = new SymbolFileTableModel(); + this.table = new GhidraTable(tableModel); + + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + TableColumn isMatchColumn = table.getColumnModel().getColumn(0); + isMatchColumn.setResizable(false); + isMatchColumn.setPreferredWidth(32); + isMatchColumn.setMaxWidth(32); + isMatchColumn.setMinWidth(32); + + // a few extra rows than needed since the table component + // will be resized according to the number of warning text + // lines at the bottom of the dialog + table.setVisibleRowCount(8); + table.setPreferredScrollableViewportSize(new Dimension(100, 100)); + + tablePanel = new JPanel(new BorderLayout()); + tablePanel.add(new JScrollPane(table), BorderLayout.CENTER); + + return tablePanel; + } + + private JPanel buildButtonPanel() { + searchButton = new JButton("Search"); + + allowRemote = new GCheckBox("Allow Remote"); + allowRemote.setToolTipText("Allow searching remote symbol servers."); + + ignorePdbUid = new GCheckBox("Ignore GUID/ID"); + ignorePdbUid.setToolTipText("Find any PDB with same name (local locations only)."); + + ignorePdbAge = new GCheckBox("Ignore Age"); + ignorePdbAge.setToolTipText("Find PDB with any age value (local locations only)."); + + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS)); + + panel.add(new GLabel("Search Options:")); + panel.add(Box.createHorizontalStrut(10)); + panel.add(ignorePdbAge); + panel.add(Box.createHorizontalStrut(10)); + panel.add(ignorePdbUid); + panel.add(Box.createHorizontalStrut(10)); + panel.add(allowRemote); + panel.add(Box.createHorizontalGlue()); + panel.add(searchButton); + + DockingWindowManager.getHelpService() + .registerHelp(panel, + new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, SEARCH_OPTIONS_HELP_ANCHOR)); + + return panel; + } +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolFileRow.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolFileRow.java new file mode 100644 index 0000000000..94e0418727 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolFileRow.java @@ -0,0 +1,53 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver.ui; + +import pdb.symbolserver.*; + +/** + * A row in the {@link SymbolFilePanel} find results table + */ +class SymbolFileRow { + private SymbolFileLocation symbolFileLocation; + private boolean isExactMatch; + + SymbolFileRow(SymbolFileLocation symbolFileLocation, boolean isExactMatch) { + this.symbolFileLocation = symbolFileLocation; + this.isExactMatch = isExactMatch; + } + + SymbolFileInfo getSymbolFileInfo() { + return symbolFileLocation.getFileInfo(); + } + + SymbolFileLocation getLocation() { + return symbolFileLocation; + } + + boolean isExactMatch() { + return isExactMatch; + } + + boolean isAvailableLocal() { + return symbolFileLocation.getSymbolServer() instanceof SymbolStore; + } + + void update(SymbolFileLocation newLocation, boolean newIsExactMatch) { + this.symbolFileLocation = newLocation; + this.isExactMatch = newIsExactMatch; + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolFileTableModel.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolFileTableModel.java new file mode 100644 index 0000000000..e36d080322 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolFileTableModel.java @@ -0,0 +1,289 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver.ui; + +import java.util.ArrayList; +import java.util.List; + +import java.awt.Component; + +import javax.swing.*; + +import docking.widgets.table.*; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.framework.plugintool.ServiceProviderStub; +import ghidra.util.table.column.AbstractGColumnRenderer; +import ghidra.util.table.column.GColumnRenderer; +import pdb.symbolserver.SymbolFileInfo; +import pdb.symbolserver.SymbolFileLocation; + +/** + * Table model for the SymbolFilePanel table. + */ +class SymbolFileTableModel + extends GDynamicColumnTableModel> { + + private List rows = new ArrayList<>(); + + SymbolFileTableModel() { + super(new ServiceProviderStub()); + setDefaultTableSortState(null); + } + + void setRows(List rows) { + this.rows = rows; + fireTableDataChanged(); + } + + void setSearchResults(SymbolFileInfo symbolFileInfo, List results) { + List newRows = new ArrayList<>(); + for (SymbolFileLocation symbolFileLocation : results) { + newRows.add(new SymbolFileRow(symbolFileLocation, + symbolFileLocation.isExactMatch(symbolFileInfo))); + } + rows = newRows; + fireTableDataChanged(); + } + + @Override + public String getName() { + return "Symbol Files"; + } + + @Override + public List getModelData() { + return rows; + } + + @Override + public List getDataSource() { + return rows; + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + + descriptor.addVisibleColumn(new PdbExactMatchColumn()); + descriptor.addVisibleColumn(new PdbFileNameColumn()); + descriptor.addHiddenColumn(new PdbFilePathColumn()); + descriptor.addVisibleColumn(new GuidColumn()); + descriptor.addVisibleColumn(new PdbAgeColumn()); + descriptor.addHiddenColumn(new PdbVersionColumn()); + descriptor.addVisibleColumn(new PdbFileStatusColumn()); + descriptor.addVisibleColumn(new PdbFileLocationColumn()); + + return descriptor; + } + + private class PdbExactMatchColumn + extends AbstractDynamicTableColumnStub { + + BooleanIconColumnRenderer renderer = + new BooleanIconColumnRenderer(LoadPdbDialog.MATCH_OK_ICON, + LoadPdbDialog.MATCH_BAD_ICON, null, "Exact Match", "Not Exact Match", null); + + @Override + public Boolean getValue(SymbolFileRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.isExactMatch(); + } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + + @Override + public String getColumnName() { + return "Exact Match"; + } + + @Override + public String getColumnDisplayName(Settings settings) { + return ""; + } + + } + + private class PdbFileNameColumn extends AbstractDynamicTableColumnStub { + + @Override + public String getValue(SymbolFileRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getSymbolFileInfo().getName(); + } + + @Override + public String getColumnName() { + return "PDB Filename"; + } + + @Override + public int getColumnPreferredWidth() { + return 200; + } + + } + + private class PdbFilePathColumn extends AbstractDynamicTableColumnStub { + + @Override + public String getValue(SymbolFileRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getSymbolFileInfo().getPath(); + } + + @Override + public String getColumnName() { + return "PDB Filepath"; + } + + } + + private class GuidColumn extends AbstractDynamicTableColumnStub { + + @Override + public String getValue(SymbolFileRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getSymbolFileInfo().getUniqueName(); + } + + @Override + public String getColumnName() { + return "GUID / Signature"; + } + + @Override + public int getColumnPreferredWidth() { + return 300; + } + + } + + private class PdbVersionColumn extends AbstractDynamicTableColumnStub { + + @Override + public String getValue(SymbolFileRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return Integer.toString(rowObject.getSymbolFileInfo().getIdentifiers().getVersion()); + } + + @Override + public String getColumnName() { + return "PDB Version"; + } + + } + + private class PdbAgeColumn extends AbstractDynamicTableColumnStub { + + @Override + public Integer getValue(SymbolFileRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getSymbolFileInfo().getIdentifiers().getAge(); + } + + @Override + public String getColumnName() { + return "PDB Age"; + } + + @Override + public int getColumnPreferredWidth() { + return 120; + } + + } + + private class PdbFileStatusColumn + extends AbstractDynamicTableColumnStub { + + @Override + public String getValue(SymbolFileRow row, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return row.isAvailableLocal() ? "Local" : "Remote"; + } + + @Override + public String getColumnName() { + return "PDB File Status"; + } + + @Override + public int getColumnPreferredWidth() { + return 120; + } + + } + + private class PdbFileLocationColumn + extends AbstractDynamicTableColumnStub { + + @Override + public String getValue(SymbolFileRow row, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return row.getLocation().getLocationStr(); + } + + @Override + public String getColumnName() { + return "File Location"; + } + + } + + /** + * Table column renderer to render a boolean value as an icon + */ + private static class BooleanIconColumnRenderer extends AbstractGColumnRenderer { + + private Icon[] icons; + private String[] toolTipStrings; + + BooleanIconColumnRenderer(Icon trueIcon, Icon falseIcon, Icon missingIcon, + String trueTooltip, String falseTooltip, String missingTooltip) { + this.icons = new Icon[] { missingIcon, falseIcon, trueIcon }; + this.toolTipStrings = new String[] { missingTooltip, falseTooltip, trueTooltip }; + } + + private int getValueOrdinal(GTableCellRenderingData data) { + Boolean booleanValue = (Boolean) data.getValue(); + + return booleanValue == null ? 0 : booleanValue.booleanValue() ? 2 : 1; + } + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + + JLabel renderer = (JLabel) super.getTableCellRendererComponent(data); + + int ordinal = getValueOrdinal(data); + renderer.setHorizontalAlignment(SwingConstants.CENTER); + renderer.setText(""); + renderer.setIcon(icons[ordinal]); + renderer.setToolTipText(toolTipStrings[ordinal]); + return renderer; + } + + @Override + public String getFilterString(Boolean booleanValue, Settings settings) { + return booleanValue == null ? "" : booleanValue.toString(); + } + + } +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerPanel.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerPanel.java new file mode 100644 index 0000000000..d811efae8b --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerPanel.java @@ -0,0 +1,594 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver.ui; + +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.io.File; +import java.io.IOException; +import java.net.URI; + +import javax.swing.*; +import javax.swing.table.TableColumn; + +import docking.DockingWindowManager; +import docking.options.editor.ButtonPanelFactory; +import docking.widgets.OptionDialog; +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.filechooser.GhidraFileChooserMode; +import docking.widgets.label.GHtmlLabel; +import docking.widgets.label.GLabel; +import docking.widgets.table.GTable; +import docking.widgets.textfield.HintTextField; +import ghidra.framework.preferences.Preferences; +import ghidra.util.*; +import ghidra.util.layout.PairLayout; +import pdb.PdbPlugin; +import pdb.symbolserver.*; +import pdb.symbolserver.ui.LoadPdbDialog.StatusText; +import resources.Icons; +import utilities.util.FileUtilities; + +/** + * Panel that allows the user to configure a SymbolServerService: a local + * symbol storage directory and a list of search locations. + */ +class SymbolServerPanel extends JPanel { + private static final String MS_SYMBOLSERVER_ENVVAR = "_NT_SYMBOL_PATH"; + + private List knownSymbolServers = + WellKnownSymbolServerLocation.loadAll(); + + private SymbolStore localSymbolStore; + private SymbolServerInstanceCreatorContext symbolServerInstanceCreatorContext; + + private SymbolServerTableModel tableModel; + private GTable table; + private JPanel additionalSearchLocationsPanel; + private JPanel defaultConfigNotice; + private GhidraFileChooser chooser; + private Consumer changeCallback; + + private JButton refreshSearchLocationsStatusButton; + private JButton moveLocationUpButton; + private JButton moveLocationDownButton; + private JButton deleteLocationButton; + private JButton addLocationButton; + private JPanel symbolStorageLocationPanel; + private HintTextField symbolStorageLocationTextField; + private JButton chooseSymbolStorageLocationButton; + private JButton saveSearchLocationsButton; + private boolean configChanged; + + SymbolServerPanel(Consumer changeCallback, + SymbolServerInstanceCreatorContext symbolServerInstanceCreatorContext) { + this.symbolServerInstanceCreatorContext = symbolServerInstanceCreatorContext; + + build(); + + DockingWindowManager.getHelpService() + .registerHelp(this, + new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, "Symbol Server Config")); + + SymbolServerService temporarySymbolServerService = + PdbPlugin.getSymbolServerService(symbolServerInstanceCreatorContext); + if (temporarySymbolServerService.getSymbolStore() instanceof LocalSymbolStore) { + setSymbolStorageLocation( + ((LocalSymbolStore) temporarySymbolServerService.getSymbolStore()).getRootDir(), + false); + } + tableModel.addSymbolServers(temporarySymbolServerService.getSymbolServers()); + setConfigChanged(false); + + this.changeCallback = changeCallback; + } + + private void build() { + setLayout(new BorderLayout()); + setBorder(BorderFactory.createTitledBorder("Symbol Server Search Config")); + + buildSymbolStorageLocationPanel(); + JPanel buttonPanel = buildButtonPanel(); + JScrollPane tableScrollPane = buildTable(); + defaultConfigNotice = new JPanel(); + defaultConfigNotice.add( + new GHtmlLabel( + "

    " + + "Missing / invalid configuration.

    " + + "Using default search location:
    " + + "Program's Import Location
    ", + SwingConstants.CENTER)); + defaultConfigNotice.setPreferredSize(tableScrollPane.getPreferredSize()); + + additionalSearchLocationsPanel = new JPanel(); + additionalSearchLocationsPanel + .setLayout(new BoxLayout(additionalSearchLocationsPanel, BoxLayout.Y_AXIS)); + additionalSearchLocationsPanel.add(buttonPanel); + additionalSearchLocationsPanel.add(tableScrollPane); + + add(symbolStorageLocationPanel, BorderLayout.NORTH); + add(additionalSearchLocationsPanel, BorderLayout.CENTER); + } + + private void updateLayout(boolean showTable) { + if (showTable == (additionalSearchLocationsPanel.getParent() != null)) { + return; + } + + remove(additionalSearchLocationsPanel); + remove(defaultConfigNotice); + add(showTable ? additionalSearchLocationsPanel : defaultConfigNotice, BorderLayout.CENTER); + invalidate(); + } + + /** + * Returns a new {@link SymbolServerService} instance representing the currently + * displayed configuration, or null if the displayed configuration is not valid. + * + * @return new {@link SymbolServerService} or null + */ + SymbolServerService getSymbolServerService() { + return (localSymbolStore != null) + ? new SymbolServerService(localSymbolStore, tableModel.getSymbolServers()) + : null; + } + + void setSymbolServers(List symbolServers) { + tableModel.setSymbolServers(symbolServers); + } + + /** + * The union of the changed status of the local storage path and the additional + * search paths table model changed status. + * + * @return boolean true if the config has changed + */ + boolean isConfigChanged() { + return configChanged || tableModel.isDataChanged(); + } + + void setConfigChanged(boolean configChanged) { + this.configChanged = configChanged; + tableModel.setDataChanged(configChanged); + } + + private JScrollPane buildTable() { + tableModel = new SymbolServerTableModel(); + table = new GTable(tableModel); + table.setVisibleRowCount(4); + table.setUserSortingEnabled(false); + table.getSelectionManager().addListSelectionListener(e -> { + updateButtonEnablement(); + }); + tableModel.addTableModelListener(e -> { + updateButtonEnablement(); + fireChanged(); + }); + + TableColumn enabledColumn = table.getColumnModel().getColumn(0); + enabledColumn.setResizable(false); + enabledColumn.setPreferredWidth(32); + enabledColumn.setMaxWidth(32); + enabledColumn.setMinWidth(32); + + TableColumn statusColumn = table.getColumnModel().getColumn(1); + statusColumn.setResizable(false); + statusColumn.setPreferredWidth(32); + statusColumn.setMaxWidth(32); + statusColumn.setMinWidth(32); + + table.setPreferredScrollableViewportSize(new Dimension(100, 100)); + + return new JScrollPane(table); + } + + private JPanel buildButtonPanel() { + refreshSearchLocationsStatusButton = + ButtonPanelFactory.createImageButton(Icons.REFRESH_ICON, "Refresh Status", + ButtonPanelFactory.ARROW_SIZE); + refreshSearchLocationsStatusButton.addActionListener(e -> refreshSearchLocationStatus()); + DockingWindowManager.getHelpService() + .registerHelp(refreshSearchLocationsStatusButton, + new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, + "SymbolServerConfig Refresh Status")); + + moveLocationUpButton = ButtonPanelFactory.createButton(ButtonPanelFactory.ARROW_UP_TYPE); + moveLocationUpButton.addActionListener(e -> moveLocation(-1)); + moveLocationUpButton.setToolTipText("Move location up"); + DockingWindowManager.getHelpService() + .registerHelp(moveLocationUpButton, + new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, + "SymbolServerConfig MoveUpDown")); + + moveLocationDownButton = + ButtonPanelFactory.createButton(ButtonPanelFactory.ARROW_DOWN_TYPE); + moveLocationDownButton.addActionListener(e -> moveLocation(1)); + moveLocationDownButton.setToolTipText("Move location down"); + DockingWindowManager.getHelpService() + .registerHelp(moveLocationDownButton, + new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, + "SymbolServerConfig MoveUpDown")); + + deleteLocationButton = ButtonPanelFactory.createImageButton(Icons.DELETE_ICON, "Delete", + ButtonPanelFactory.ARROW_SIZE); + deleteLocationButton.addActionListener(e -> deleteLocation()); + DockingWindowManager.getHelpService() + .registerHelp(deleteLocationButton, + new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, + "SymbolServerConfig Delete")); + + addLocationButton = ButtonPanelFactory.createImageButton(Icons.ADD_ICON, "Add", + ButtonPanelFactory.ARROW_SIZE); + addLocationButton.addActionListener(e -> addLocation()); + DockingWindowManager.getHelpService() + .registerHelp(addLocationButton, + new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, + "SymbolServerConfig Add")); + + saveSearchLocationsButton = + ButtonPanelFactory.createImageButton(Icons.get("images/disk.png"), + "Save Configuration", ButtonPanelFactory.ARROW_SIZE); + saveSearchLocationsButton.addActionListener(e -> saveConfig()); + DockingWindowManager.getHelpService() + .registerHelp(saveSearchLocationsButton, + new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, + "SymbolServerConfig Save")); + + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)); + buttonPanel.add(new GLabel("Additional Search Paths:")); + buttonPanel.add(Box.createHorizontalGlue()); + buttonPanel.add(addLocationButton); + buttonPanel.add(deleteLocationButton); + buttonPanel.add(moveLocationUpButton); + buttonPanel.add(moveLocationDownButton); + buttonPanel.add(refreshSearchLocationsStatusButton); + buttonPanel.add(saveSearchLocationsButton); + + return buttonPanel; + } + + private JPanel buildSymbolStorageLocationPanel() { + symbolStorageLocationTextField = new HintTextField(" Required "); + symbolStorageLocationTextField.setEditable(false); + + chooseSymbolStorageLocationButton = + ButtonPanelFactory.createButton(ButtonPanelFactory.BROWSE_TYPE); + chooseSymbolStorageLocationButton.addActionListener(e -> chooseSymbolStorageLocation()); + + symbolStorageLocationPanel = new JPanel(new PairLayout(5, 5)); + GLabel symbolStorageLocLabel = new GLabel("Local Symbol Storage:", SwingConstants.RIGHT); + symbolStorageLocLabel + .setToolTipText("User-specified directory where PDB files are stored. Required."); + symbolStorageLocationPanel.add(symbolStorageLocLabel); + symbolStorageLocationPanel.add(LoadPdbDialog.join(null, symbolStorageLocationTextField, + chooseSymbolStorageLocationButton)); + return symbolStorageLocationPanel; + } + + private void updateButtonEnablement() { + boolean hasLocalSymbolStore = localSymbolStore != null; + boolean singleRow = table.getSelectedRowCount() == 1; + boolean moreThanOneRow = table.getRowCount() > 1; + + refreshSearchLocationsStatusButton.setEnabled(hasLocalSymbolStore && !tableModel.isEmpty()); + moveLocationUpButton.setEnabled(hasLocalSymbolStore && singleRow && moreThanOneRow); + moveLocationDownButton.setEnabled(hasLocalSymbolStore && singleRow && moreThanOneRow); + addLocationButton.setEnabled(hasLocalSymbolStore); + deleteLocationButton.setEnabled(hasLocalSymbolStore && table.getSelectedRowCount() > 0); + saveSearchLocationsButton.setEnabled(hasLocalSymbolStore && isConfigChanged()); + updateLayout(hasLocalSymbolStore); + } + + StatusText getSymbolServerWarnings() { + Map warningsByLocation = new HashMap<>(); + for (WellKnownSymbolServerLocation ssloc : knownSymbolServers) { + if (ssloc.getWarning() != null && !ssloc.getWarning().isBlank()) { + warningsByLocation.put(ssloc.getLocation(), ssloc.getWarning()); + } + } + String warning = tableModel.getDataSource() + .stream() + .map(row -> warningsByLocation.get(row.getSymbolServer().getName())) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.joining("
    \n")); + + return !warning.isEmpty() ? new StatusText(warning, MessageType.WARNING, false) : null; + } + + private void setSymbolStorageLocation(File symbolStorageDir, boolean allowGUIPrompt) { + if (symbolStorageDir == null) { + return; + } + if (!symbolStorageDir.exists()) { + if (!allowGUIPrompt) { + return; + } + + int opt = OptionDialog.showOptionDialog(this, "Create Local Symbol Storage Directory?", + "Symbol storage directory
    " + + HTMLUtilities.escapeHTML(symbolStorageDir.getPath()) + + "
    does not exist. Create?", + "Yes", OptionDialog.QUESTION_MESSAGE); + if (opt == OptionDialog.CANCEL_OPTION) { + return; + } + try { + FileUtilities.checkedMkdirs(symbolStorageDir); + } + catch (IOException e) { + Msg.showError(this, this, "Failure", "Failed to create symbol storage directory " + + symbolStorageDir + ": " + e.getMessage()); + return; + } + } + + if (allowGUIPrompt && isEmptyDirectory(symbolStorageDir)) { + if (OptionDialog.showYesNoDialog(this, + "Initialize Symbol Storage Directory?", + "Initialize new directory as Microsoft symbol storage directory?") == OptionDialog.YES_OPTION) { + try { + LocalSymbolStore.create(symbolStorageDir, + 1 /* level1 MS symbol storage directory */); + } + catch (IOException e) { + Msg.showError(this, this, "Initialize Failure", + "Failed to initialize symbol storage directory " + symbolStorageDir, e); + } + } + } + + localSymbolStore = + symbolServerInstanceCreatorContext.getSymbolServerInstanceCreatorRegistry() + .newSymbolServer(symbolStorageDir.getPath(), symbolServerInstanceCreatorContext, + SymbolStore.class); + symbolStorageLocationTextField.setText(symbolStorageDir.getPath()); + fireChanged(); + } + + private void fireChanged() { + if (changeCallback != null) { + changeCallback.accept(getSymbolServerService()); + } + } + + private void chooseSymbolStorageLocation() { + configChanged = true; + setSymbolStorageLocation(getChooser().getSelectedFile(), true); + updateButtonEnablement(); + } + + private void importLocations() { + String envVar = (String) JOptionPane.showInputDialog(this, + "Enter value:

    Example: SVR*c:\\symbols*https://msdl.microsoft.com/download/symbols/

    ", + "Enter Symbol Server Search Path Value", JOptionPane.QUESTION_MESSAGE, null, null, + Objects.requireNonNullElse(System.getenv(MS_SYMBOLSERVER_ENVVAR), "")); + if (envVar == null) { + return; + } + + List symbolServerPaths = getSymbolPathsFromEnvStr(envVar); + if (!symbolServerPaths.isEmpty()) { + // if the first item in the path list looks like a local symbol storage path, + // allow the user to set it as the storage dir (and remove it from the elements + // that will be added to the search list) + String firstSearchPath = symbolServerPaths.get(0); + SymbolServer symbolServer = + symbolServerInstanceCreatorContext.getSymbolServerInstanceCreatorRegistry() + .newSymbolServer(firstSearchPath, symbolServerInstanceCreatorContext); + if (symbolServer instanceof LocalSymbolStore && + ((LocalSymbolStore) symbolServer).isValid()) { + int choice = OptionDialog.showYesNoCancelDialog(this, "Set Symbol Storage Location", + "Set symbol storage location to " + firstSearchPath + "?"); + if (choice == OptionDialog.CANCEL_OPTION) { + return; + } + if (choice == OptionDialog.YES_OPTION) { + symbolServerPaths.remove(0); + configChanged = true; + setSymbolStorageLocation(((LocalSymbolStore) symbolServer).getRootDir(), true); + symbolStorageLocationTextField.setText(symbolServer.getName()); + } + } + } + + tableModel.addSymbolServers( + symbolServerInstanceCreatorContext.getSymbolServerInstanceCreatorRegistry() + .createSymbolServersFromPathList(symbolServerPaths, + symbolServerInstanceCreatorContext)); + fireChanged(); + } + + private List getSymbolPathsFromEnvStr(String envString) { + // Expect the environment string to be in the MS symbol server search path form: + // srv*[local cache]*[private symbol server]*https://msdl.microsoft.com/download/symbols + // srv*c:\symbols*https://msdl.microsoft.com/download/symbols;srv*c:\additional*https://symbol.server.tld/ + String[] envParts = envString.split("[*;]"); + List results = new ArrayList<>(); + Set locationStringDeduplicationSet = new HashSet<>(); + for (int i = 0; i < envParts.length; i++) { + String locationString = envParts[i].trim(); + if (!locationString.isBlank() && !locationString.equalsIgnoreCase("srv") && + !locationStringDeduplicationSet.contains(locationString)) { + results.add(locationString); + locationStringDeduplicationSet.add(locationString); + } + } + + return results; + } + + private void addLocation() { + JPopupMenu menu = createAddLocationPopupMenu(); + menu.show(addLocationButton, 0, 0); + } + + private JPopupMenu createAddLocationPopupMenu() { + JPopupMenu menu = new JPopupMenu(); + JMenuItem addDirMenuItem = new JMenuItem("Directory"); + addDirMenuItem.addActionListener(e -> addDirectoryLocation()); + menu.add(addDirMenuItem); + + JMenuItem addURLMenuItem = new JMenuItem("URL"); + addURLMenuItem.addActionListener(e -> addUrlLocation()); + menu.add(addURLMenuItem); + + JMenuItem addProgLocMenuItem = + new JMenuItem(SameDirSymbolStore.PROGRAMS_IMPORT_LOCATION_DESCRIPTION_STR); + addProgLocMenuItem.addActionListener(e -> addSameDirLocation()); + menu.add(addProgLocMenuItem); + + JMenuItem importEnvMenuItem = new JMenuItem("Import _NT_SYMBOL_PATH"); + importEnvMenuItem.addActionListener(e -> importLocations()); + menu.add(importEnvMenuItem); + + if (!knownSymbolServers.isEmpty()) { + menu.add(new JSeparator()); + for (WellKnownSymbolServerLocation ssloc : knownSymbolServers) { + JMenuItem mi = new JMenuItem(ssloc.getLocation()); + mi.addActionListener(e -> addKnownLocation(ssloc)); + mi.setToolTipText(" [from " + ssloc.getFileOrigin() + "]"); + menu.add(mi); + } + } + DockingWindowManager.getHelpService() + .registerHelp(menu, + new HelpLocation(PdbPlugin.PDB_PLUGIN_HELP_TOPIC, "SymbolServerConfig_Add")); + return menu; + } + + private void addSameDirLocation() { + SameDirSymbolStore sameDirSymbolStore = + new SameDirSymbolStore(symbolServerInstanceCreatorContext.getRootDir()); + tableModel.addSymbolServer(sameDirSymbolStore); + } + + private void addKnownLocation(WellKnownSymbolServerLocation ssloc) { + SymbolServer symbolServer = + symbolServerInstanceCreatorContext.getSymbolServerInstanceCreatorRegistry() + .newSymbolServer(ssloc.getLocation(), symbolServerInstanceCreatorContext); + if (symbolServer != null) { + tableModel.addSymbolServer(symbolServer); + } + } + + private void addUrlLocation() { + String urlLocationString = OptionDialog.showInputSingleLineDialog(this, "Enter URL", + "Enter the URL of a Symbol Server: ", "https://"); + if (urlLocationString == null || urlLocationString.isBlank()) { + return; + } + urlLocationString = urlLocationString.toLowerCase(); + if (!(urlLocationString.startsWith("http://") || + urlLocationString.startsWith("https://"))) { + Msg.showWarn(this, this, "Bad URL", "Invalid URL: " + urlLocationString); + return; + } + try { + HttpSymbolServer httpSymbolServer = new HttpSymbolServer(URI.create(urlLocationString)); + tableModel.addSymbolServer(httpSymbolServer); + } + catch (IllegalArgumentException e) { + Msg.showWarn(this, this, "Bad URL", "Invalid URL: " + urlLocationString); + } + } + + private void addDirectoryLocation() { + File dir = FilePromptDialog.chooseDirectory("Enter Path", "Symbol Storage Location: ", + null); + if (dir == null) { + return; + } + if (!dir.exists() || !dir.isDirectory()) { + Msg.showError(this, this, "Bad path", "Invalid path: " + dir); + return; + } + LocalSymbolStore localSymbolStore = new LocalSymbolStore(dir); + tableModel.addSymbolServer(localSymbolStore); + } + + private void deleteLocation() { + int selectedRow = table.getSelectedRow(); + tableModel.deleteRows(table.getSelectedRows()); + if (selectedRow >= 0 && selectedRow < table.getRowCount()) { + table.selectRow(selectedRow); + } + } + + private void moveLocation(int delta) { + if (table.getSelectedRowCount() == 1) { + tableModel.moveRow(table.getSelectedRow(), delta); + } + } + + private void refreshSearchLocationStatus() { + tableModel.refreshSymbolServerLocationStatus(); + updateButtonEnablement(); + } + + /* package */ void saveConfig() { + SymbolServerService temporarySymbolServerService = getSymbolServerService(); + if (temporarySymbolServerService != null) { + PdbPlugin.saveSymbolServerServiceConfig(temporarySymbolServerService); + Preferences.store(); + setConfigChanged(false); + fireChanged(); + updateButtonEnablement(); + } + } + + private GhidraFileChooser getChooser() { + + if (chooser == null) { + chooser = new GhidraFileChooser(this); + chooser.setMultiSelectionEnabled(false); + chooser.setApproveButtonText("Choose"); + chooser.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); + chooser.setTitle("Select Symbol Storage Dir"); + } + + return chooser; + } + + /* screen shot usage */ void pushAddLocationButton() { + addLocation(); + } + + /* screen shot usage */ void setSymbolStorageDirectoryTextOnly(String pathStr) { + symbolStorageLocationTextField.setText(pathStr); + } + + /** + * Returns true if the given file path is a directory that contains no files. + *

    + * + * @param directory path to a location on the file system + * @return true if is a directory and it contains no files + */ + private static boolean isEmptyDirectory(File directory) { + if (directory.isDirectory()) { + File[] dirContents = directory.listFiles(); + return dirContents != null && dirContents.length == 0; + } + return false; + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerRow.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerRow.java new file mode 100644 index 0000000000..58de169690 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerRow.java @@ -0,0 +1,76 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver.ui; + +import pdb.symbolserver.DisabledSymbolServer; +import pdb.symbolserver.SymbolServer; + +/** + * Represents a row in the {@link SymbolServerTableModel} + */ +class SymbolServerRow { + + public enum LocationStatus { + UNKNOWN, VALID, INVALID + } + + private SymbolServer symbolServer; + private LocationStatus status = LocationStatus.UNKNOWN; + + SymbolServerRow(SymbolServer symbolServer) { + this.symbolServer = symbolServer; + } + + SymbolServer getSymbolServer() { + return symbolServer; + } + + void setSymbolServer(SymbolServer symbolServer) { + this.symbolServer = symbolServer; + } + + boolean isEnabled() { + return !(symbolServer instanceof DisabledSymbolServer); + } + + void setEnabled(boolean enabled) { + if (isEnabled() == enabled) { + return; + } + if (enabled) { + DisabledSymbolServer dss = (DisabledSymbolServer) symbolServer; + symbolServer = dss.getSymbolServer(); + } + else { + symbolServer = new DisabledSymbolServer(symbolServer); + } + } + + LocationStatus getStatus() { + return status; + } + + void setStatus(LocationStatus status) { + this.status = status; + } + + @Override + public String toString() { + return String.format("SymbolServerRow: [ status: %s, server: %s]", status.toString(), + symbolServer.toString()); + } + +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerTableModel.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerTableModel.java new file mode 100644 index 0000000000..bc33ebb483 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/SymbolServerTableModel.java @@ -0,0 +1,309 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver.ui; + +import static java.util.stream.Collectors.toList; +import static pdb.symbolserver.ui.SymbolServerRow.LocationStatus.INVALID; +import static pdb.symbolserver.ui.SymbolServerRow.LocationStatus.VALID; + +import java.util.ArrayList; +import java.util.List; + +import java.awt.Component; + +import javax.swing.*; + +import docking.widgets.table.*; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.framework.plugintool.ServiceProviderStub; +import ghidra.util.Swing; +import ghidra.util.table.column.AbstractGColumnRenderer; +import ghidra.util.table.column.GColumnRenderer; +import ghidra.util.task.TaskLauncher; +import pdb.symbolserver.SymbolServer; +import resources.Icons; + +/** + * Table model for the {@link SymbolServerPanel} table + */ +class SymbolServerTableModel + extends GDynamicColumnTableModel> { + + private List rows = new ArrayList<>(); + private boolean dataChanged; + + SymbolServerTableModel() { + super(new ServiceProviderStub()); + setDefaultTableSortState(null); + } + + boolean isEmpty() { + return rows.isEmpty(); + } + + void setSymbolServers(List symbolServers) { + rows.clear(); + for (SymbolServer symbolServer : symbolServers) { + rows.add(new SymbolServerRow(symbolServer)); + } + fireTableDataChanged(); + } + + List getSymbolServers() { + return rows.stream() + .map(SymbolServerRow::getSymbolServer) + .collect(toList()); + } + + void addSymbolServer(SymbolServer ss) { + SymbolServerRow row = new SymbolServerRow(ss); + rows.add(row); + dataChanged = true; + fireTableDataChanged(); + } + + void addSymbolServers(List symbolServers) { + for (SymbolServer symbolServer : symbolServers) { + rows.add(new SymbolServerRow(symbolServer)); + } + dataChanged = true; + fireTableDataChanged(); + } + + void deleteRows(int[] rowIndexes) { + for (int i = rowIndexes.length - 1; i >= 0; i--) { + rows.remove(rowIndexes[i]); + } + dataChanged = true; + fireTableDataChanged(); + } + + void refreshSymbolServerLocationStatus() { + List rowsCopy = new ArrayList<>(this.rows); + TaskLauncher.launchNonModal("Refresh Symbol Server Location Status", monitor -> { + monitor.initialize(rowsCopy.size()); + monitor.setMessage("Refreshing symbol server status"); + try { + for (SymbolServerRow row : rowsCopy) { + if (monitor.isCancelled()) { + break; + } + monitor.setMessage("Checking " + row.getSymbolServer().getName()); + row.setStatus(row.getSymbolServer().isValid(monitor) ? VALID : INVALID); + } + } + finally { + Swing.runLater(SymbolServerTableModel.this::fireTableDataChanged); + } + }); + } + + void moveRow(int rowIndex, int deltaIndex) { + int destIndex = rowIndex + deltaIndex; + if (rowIndex < 0 || rowIndex >= rows.size() || destIndex < 0 || destIndex >= rows.size()) { + return; + } + + SymbolServerRow symbolServerRow1 = rows.get(rowIndex); + SymbolServerRow symbolServerRow2 = rows.get(destIndex); + rows.set(destIndex, symbolServerRow1); + rows.set(rowIndex, symbolServerRow2); + + dataChanged = true; + + fireTableDataChanged(); + } + + boolean isDataChanged() { + return dataChanged; + } + + void setDataChanged(boolean b) { + this.dataChanged = b; + } + + @Override + public String getName() { + return "Symbol Server Locations"; + } + + @Override + public List getModelData() { + return rows; + } + + @Override + public List getDataSource() { + return rows; + } + + @Override + public boolean isSortable(int columnIndex) { + return false; + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + DynamicTableColumn column = getColumn(columnIndex); + if (column instanceof EnabledColumn) { + SymbolServerRow row = getRowObject(rowIndex); + row.setEnabled((Boolean) aValue); + dataChanged = true; + fireTableDataChanged(); + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + DynamicTableColumn column = getColumn(columnIndex); + return column instanceof EnabledColumn; + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + + descriptor.addVisibleColumn(new EnabledColumn()); + descriptor.addVisibleColumn(new StatusColumn()); + descriptor.addVisibleColumn(new LocationColumn()); + + return descriptor; + } + + //------------------------------------------------------------------------------------------- + + private static class StatusColumn extends + AbstractDynamicTableColumnStub { + + private static final Icon VALID_ICON = Icons.get("images/checkmark_green.gif"); + private static final Icon INVALID_ICON = Icons.ERROR_ICON; + + private static Icon[] icons = new Icon[] { null, VALID_ICON, INVALID_ICON }; + private static String[] toolTips = new String[] { null, "Status: Ok", "Status: Failed" }; + + EnumIconColumnRenderer renderer = + new EnumIconColumnRenderer<>(SymbolServerRow.LocationStatus.class, icons, toolTips); + + @Override + public SymbolServerRow.LocationStatus getValue(SymbolServerRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getStatus(); + } + + @Override + public String getColumnDisplayName(Settings settings) { + return ""; + } + + @Override + public String getColumnName() { + return "Status"; + } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + + } + + private static class EnabledColumn + extends AbstractDynamicTableColumnStub { + + @Override + public String getColumnDisplayName(Settings settings) { + return ""; + } + + @Override + public Boolean getValue(SymbolServerRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.isEnabled(); + } + + @Override + public String getColumnName() { + return "Enabled"; + } + + } + + private static class LocationColumn + extends AbstractDynamicTableColumnStub { + + @Override + public String getValue(SymbolServerRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getSymbolServer().getDescriptiveName(); + } + + @Override + public String getColumnName() { + return "Location"; + } + + @Override + public int getColumnPreferredWidth() { + return 250; + } + + } + + /** + * Table column renderer to render an enum value as a icon + * + * @param enum type + */ + private static class EnumIconColumnRenderer> + extends AbstractGColumnRenderer { + + private Icon[] icons; + private String[] toolTips; + + EnumIconColumnRenderer(Class enumClass, Icon[] icons, String[] toolTips) { + if (enumClass.getEnumConstants().length != icons.length || + icons.length != toolTips.length) { + throw new IllegalArgumentException(); + } + this.icons = icons; + this.toolTips = toolTips; + } + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + + JLabel renderer = (JLabel) super.getTableCellRendererComponent(data); + + E e = (E) data.getValue(); + renderer.setHorizontalAlignment(SwingConstants.CENTER); + renderer.setText(""); + renderer.setIcon(e != null ? icons[e.ordinal()] : null); + renderer.setToolTipText(e != null ? toolTips[e.ordinal()] : null); + return renderer; + } + + @Override + protected String getText(Object value) { + return ""; + } + + @Override + public String getFilterString(E t, Settings settings) { + return t == null ? "" : t.toString(); + } + } +} diff --git a/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/WellKnownSymbolServerLocation.java b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/WellKnownSymbolServerLocation.java new file mode 100644 index 0000000000..c452fd3683 --- /dev/null +++ b/Ghidra/Features/PDB/src/main/java/pdb/symbolserver/ui/WellKnownSymbolServerLocation.java @@ -0,0 +1,112 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver.ui; + +import java.io.IOException; +import java.util.*; + +import generic.jar.ResourceFile; +import ghidra.framework.Application; +import ghidra.util.Msg; +import utilities.util.FileUtilities; + +/** + * Represents a well-known symbol server location. + *

    + * See the PDB_SYMBOL_SERVER_URLS.pdburl file. + */ +class WellKnownSymbolServerLocation { + private String locationCategory; + private String location; + private String warning; + private String fileOrigin; + + WellKnownSymbolServerLocation(String location, String locationCategory, String warning, + String fileOrigin) { + this.location = location; + this.locationCategory = locationCategory; + this.warning = warning; + this.fileOrigin = fileOrigin; + } + + String getLocationCategory() { + return locationCategory; + } + + String getLocation() { + return location; + } + + String getWarning() { + return warning; + } + + String getFileOrigin() { + return fileOrigin; + } + + @Override + public int hashCode() { + return Objects.hash(location, locationCategory, warning); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + WellKnownSymbolServerLocation other = (WellKnownSymbolServerLocation) obj; + return Objects.equals(location, other.location) && + Objects.equals(locationCategory, other.locationCategory) && + Objects.equals(warning, other.warning); + } + + /** + * Loads all symbol server location files (*.pdburl) and returns a list of entries. + * + * @return list of {@link WellKnownSymbolServerLocation} elements + */ + public static List loadAll() { + List pdbUrlFiles = Application.findFilesByExtensionInApplication(".pdburl"); + + List results = new ArrayList<>(); + for (ResourceFile file : pdbUrlFiles) { + try { + List lines = FileUtilities.getLines(file); + for (String line : lines) { + // format: location_category|location_string|warning_string + // example: "Internet|https://msdl.microsoft.com/download/symbols|Warning: be careful!" + String[] fields = line.split("\\|"); + if (fields.length > 1) { + results.add(new WellKnownSymbolServerLocation(fields[1], fields[0], + fields.length > 2 ? fields[2] : null, file.getName())); + } + } + } + catch (IOException e) { + Msg.warn(WellKnownSymbolServerLocation.class, "Unable to read pdburl file: " + file); + } + } + return results; + } + +} diff --git a/Ghidra/Features/PDB/src/test/java/ghidra/app/util/bin/format/pdb/PdbParserTest.java b/Ghidra/Features/PDB/src/test/java/ghidra/app/util/bin/format/pdb/PdbParserTest.java index 9e82411e99..b187a20369 100644 --- a/Ghidra/Features/PDB/src/test/java/ghidra/app/util/bin/format/pdb/PdbParserTest.java +++ b/Ghidra/Features/PDB/src/test/java/ghidra/app/util/bin/format/pdb/PdbParserTest.java @@ -15,17 +15,16 @@ */ package ghidra.app.util.bin.format.pdb; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import java.io.*; -import java.util.ArrayList; -import java.util.List; -import org.junit.*; +import org.junit.Before; +import org.junit.Test; import ghidra.app.plugin.core.analysis.AutoAnalysisManager; import ghidra.app.services.DataTypeManagerService; -import ghidra.app.util.bin.format.pdb.PdbParser.PdbFileType; import ghidra.app.util.importer.MessageLog; import ghidra.framework.options.Options; import ghidra.program.database.ProgramBuilder; @@ -35,79 +34,34 @@ import ghidra.program.model.listing.FunctionManager; import ghidra.program.model.listing.Program; import ghidra.test.AbstractGhidraHeadlessIntegrationTest; import ghidra.util.task.TaskMonitor; -import utilities.util.FileUtilities; public class PdbParserTest extends AbstractGhidraHeadlessIntegrationTest { private ProgramBuilder builder; - private static String notepadGUID = "36cfd5f9-888c-4483-b522-b9db242d8478"; + private static final String notepadGUID = "36cfd5f9-888c-4483-b522-b9db242d8478"; + private static final String programBasename = "notepad"; // Note: this is in hex. Code should translate it to decimal when creating GUID/Age folder name - private static String notepadAge = "21"; - - // Name of subfolder that stores the actual PDB file - private static String guidAgeCombo = "36CFD5F9888C4483B522B9DB242D847833"; - private String programBasename = "notepad"; + private static final String notepadAge = "21"; private File tempDir, fileLocation; private Program testProgram; - //private static String guidAgeCombo = PdbParserNEW.getGuidAgeString(notepadGUID, notepadAge); - - // Bogus symbol repository directory or null directory should not break anything - private static final File noSuchSymbolsRepoDir = null; - private String pdbFilename, pdbXmlFilename; - private String exeFolderName = "exe", pdbXmlFolderName = "pdb_xml", - symbolsFolderName = "symbols"; - - private File pdbFile = null, pdbXmlFile = null, pdbXmlDir = null, symbolsFolder = null; - - private List createdFiles; - - enum PdbLocation { - NONE, SYMBOLS_SUBDIR, SYMBOLS_NO_SUBDIR, SAME_AS_EXE_SUBDIR, SAME_AS_EXE_NO_SUBDIR - } - - enum PdbXmlLocation { - NONE, SAME_AS_PDB, OWN_DIR - } TestFunction[] programFunctions = new TestFunction[] { new TestFunction("function1", "0x110", "0x35") }; - public PdbParserTest() { - super(); - } - @Before public void setUp() throws Exception { // Get temp directory in which to store files - String tempDirPath = getTestDirectoryPath(); - tempDir = new File(tempDirPath); - - fileLocation = new File(tempDir, exeFolderName); - + tempDir = createTempDirectory("pdb_parser"); + fileLocation = new File(tempDir, "exe"); testProgram = buildProgram(fileLocation.getAbsolutePath()); pdbFilename = programBasename + ".pdb"; - pdbXmlFilename = pdbFilename + PdbFileType.XML.toString(); - - createdFiles = null; - } - - @After - public void tearDown() throws Exception { - - if (fileLocation != null) { - FileUtilities.deleteDir(fileLocation); - } - if (createdFiles != null) { - deleteCreatedFiles(createdFiles); - } - - System.gc(); + pdbXmlFilename = programBasename + ".pdb.xml"; } private Program buildProgram(String exeLocation) throws Exception { @@ -138,838 +92,10 @@ public class PdbParserTest extends AbstractGhidraHeadlessIntegrationTest { return currentTestProgram; } - private List createFiles(PdbLocation pdbLoc, PdbXmlLocation pdbXmlLoc) { - pdbFile = null; - pdbXmlFile = null; - - pdbXmlDir = new File(tempDir, pdbXmlFolderName); - - List filesCreated = new ArrayList<>(); - - createDirectory(fileLocation); - filesCreated.add(fileLocation); - - File subDir, subSubDir; - - switch (pdbLoc) { - // Put PDB file in the /symbols/ folder with subdirectories that - // include the PDB name and GUID. I.e., the full path to the PDB is: - // /symbols/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/notepad.pdb - case SYMBOLS_SUBDIR: - symbolsFolder = new File(tempDir, symbolsFolderName); - createDirectory(symbolsFolder); - filesCreated.add(symbolsFolder); - - subDir = new File(symbolsFolder, pdbFilename); - createDirectory(subDir); - filesCreated.add(subDir); - - subSubDir = new File(subDir, guidAgeCombo); - createDirectory(subSubDir); - filesCreated.add(subSubDir); - - pdbFile = new File(subSubDir, pdbFilename); - break; - - // Put the PDB file directly into the /symbols folder. - // I.e., /symbols/notepad.pdb - case SYMBOLS_NO_SUBDIR: - symbolsFolder = new File(tempDir, symbolsFolderName); - createDirectory(symbolsFolder); - filesCreated.add(symbolsFolder); - - pdbFile = new File(symbolsFolder, pdbFilename); - break; - - // Put the PDB file in the same folder as the binary with subdirectories that - // include the PDB name and GUID. - // I.e., /exe/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/notepad.pdb - case SAME_AS_EXE_SUBDIR: - subDir = new File(fileLocation, pdbFilename); - createDirectory(subDir); - filesCreated.add(subDir); - - subSubDir = new File(subDir, guidAgeCombo); - createDirectory(subSubDir); - filesCreated.add(subSubDir); - - pdbFile = new File(subSubDir, pdbFilename); - break; - - // Put the PDB file in the same folder as the binary - // I.e., /exe/notepad.pdb - case SAME_AS_EXE_NO_SUBDIR: - pdbFile = new File(fileLocation, pdbFilename); - break; - - case NONE: - // Do nothing - break; - - default: - fail("Unrecognized pdbLocation choice: " + pdbLoc); - } - - if (pdbFile != null) { - createFile(pdbFile); - filesCreated.add(pdbFile); - } - - switch (pdbXmlLoc) { - // Put the PDB XML file in the same location as the PDB file - case SAME_AS_PDB: - if (pdbFile != null) { - pdbXmlFile = new File(pdbFile.getParentFile(), pdbXmlFilename); - } - else { - fail("Does not make sense to create a .pdb.xml file in the same directory as " + - "the .pdb file when there is no .pdb file!"); - } - break; - - // Put the PDB XML file in the /pdb_xml directory - case OWN_DIR: - // Create directory that will server as .pdb.xml location - createDirectory(pdbXmlDir); - filesCreated.add(pdbXmlDir); - - pdbXmlFile = new File(pdbXmlDir, pdbXmlFilename); - break; - - // Do not create a PDB XML file - case NONE: - break; - - default: - fail("Unrecognized pdbXmlLocation choice: " + pdbXmlLoc); - - } - - if (pdbXmlFile != null) { - createFile(pdbXmlFile); - filesCreated.add(pdbXmlFile); - } - - verifyFilesCreated(pdbLoc, pdbXmlLoc); - - return filesCreated; - } - - private void verifyFilesCreated(PdbLocation pdbLoc, PdbXmlLocation pdbXmlLoc) { - - File expectedDir1, expectedDir2; - - switch (pdbLoc) { - case NONE: - assertNull(pdbFile); - break; - - case SAME_AS_EXE_SUBDIR: - assertNotNull(pdbFile); - expectedDir1 = new File(fileLocation, pdbFilename); - expectedDir2 = new File(expectedDir1, guidAgeCombo); - assertEquals(expectedDir2, pdbFile.getParentFile()); - break; - - case SAME_AS_EXE_NO_SUBDIR: - assertNotNull(pdbFile); - assertEquals(fileLocation, pdbFile.getParentFile()); - break; - - case SYMBOLS_SUBDIR: - assertNotNull(pdbFile); - expectedDir1 = new File(symbolsFolder, pdbFilename); - expectedDir2 = new File(expectedDir1, guidAgeCombo); - assertEquals(expectedDir2, pdbFile.getParentFile()); - break; - - case SYMBOLS_NO_SUBDIR: - assertNotNull(pdbFile); - assertEquals(symbolsFolder, pdbFile.getParentFile()); - break; - - default: - fail("Unrecognized pdbLocation choice: " + pdbLoc); - } - - switch (pdbXmlLoc) { - case SAME_AS_PDB: - assertNotNull(pdbXmlFile); - assertEquals(pdbXmlFile.getParentFile(), pdbFile.getParentFile()); - break; - - case OWN_DIR: - assertNotNull(pdbXmlFile); - assertEquals(pdbXmlFile.getParentFile(), pdbXmlDir); - break; - - case NONE: - assertNull(pdbXmlFile); - break; - } - } - - private void deleteCreatedFiles(List filesToDelete) { - // Delete in the reverse order the files were added - for (int i = filesToDelete.size() - 1; i >= 0; i--) { - filesToDelete.get(i).delete(); - } - } - - /** - * notepad.pdb is here: /symbols/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * notepad.pdb.xml does not exist - * Repo location left alone (default) - * - * PDB file should not be found, since default repo does not point to symbols folder. - * - * @throws Exception - */ - @Test - public void testFindPdb1() throws Exception { - - createdFiles = null; - - createdFiles = createFiles(PdbLocation.SYMBOLS_SUBDIR, PdbXmlLocation.NONE); - File pdb = PdbParser.findPDB(testProgram, false, noSuchSymbolsRepoDir); - - // Should not find anything since repo is set to an invalid path - assertNull(pdb); - - } - - /** - * notepad.pdb is here: /symbols/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * notepad.pdb.xml does not exist - * Repo location set to /symbols - * - * On Windows, PDB file should be found. - * On non-Windows, Pdb file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb2() throws Exception { - - createdFiles = createFiles(PdbLocation.SYMBOLS_SUBDIR, PdbXmlLocation.NONE); - - File pdb = PdbParser.findPDB(testProgram, false, symbolsFolder); - - assertNotNull(pdb); - assertEquals(pdbFile, pdb); - - } - - /** - * notepad.pdb is here: /symbols/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * notepad.pdb.xml is here: /symbols/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * Repo location set to /symbols - * - * On Windows, PDB file should be found. - * On non-Windows, PDB XML file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb3() throws Exception { - - createdFiles = createFiles(PdbLocation.SYMBOLS_SUBDIR, PdbXmlLocation.SAME_AS_PDB); - - File pdb = PdbParser.findPDB(testProgram, false, symbolsFolder); - - assertNotNull(pdb); - - if (PdbParser.onWindows) { - assertEquals(pdbFile, pdb); - } - else { - assertEquals(pdbXmlFile, pdb); - } - } - - /** - * notepad.pdb is here: /symbols/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * notepad.pdb.xml is here: /pdb_xml/ - * Repo location set to /symbols - * - * On Windows, PDB file should be found. - * On non-Windows, PDB files should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb4() throws Exception { - - createdFiles = createFiles(PdbLocation.SYMBOLS_SUBDIR, PdbXmlLocation.OWN_DIR); - - File pdb = PdbParser.findPDB(testProgram, false, symbolsFolder); - - assertNotNull(pdb); - assertEquals(pdb, pdb); - - } - - /** - * notepad.pdb is here: /symbols/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * notepad.pdb.xml is here: /pdb_xml/ - * Repo location set to /pdb_xml - * - * On Windows, PDB XML file should be found. - * On non-Windows, PDB XML file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb5() throws Exception { - - createdFiles = createFiles(PdbLocation.SYMBOLS_SUBDIR, PdbXmlLocation.OWN_DIR); - - File pdb = PdbParser.findPDB(testProgram, false, pdbXmlDir); - - assertNotNull(pdb); - - if (PdbParser.onWindows) { - assertEquals(pdbXmlFile, pdb); - } - else { - assertEquals(pdbXmlFile, pdb); - } - } - - /** - * notepad.pdb is here: /symbols/ - * notepad.pdb.xml does not exist - * Repo location set to /symbols - * - * On Windows, PDB file should be found. - * On non-Windows, PDB file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb6() throws Exception { - - createdFiles = createFiles(PdbLocation.SYMBOLS_NO_SUBDIR, PdbXmlLocation.NONE); - - File pdb = PdbParser.findPDB(testProgram, false, symbolsFolder); - - assertNotNull(pdb); - assertEquals(pdbFile, pdb); - - } - - /** - * notepad.pdb is here: /symbols/ - * notepad.pdb.xml does not exist - * Repo location set to /pdb_xml/ - * - * PDB file should not be found, since default repo does not point to symbols folder. - * - * @throws Exception - */ - @Test - public void testFindPdb7() throws Exception { - - createdFiles = createFiles(PdbLocation.SYMBOLS_NO_SUBDIR, PdbXmlLocation.NONE); - - File pdb = PdbParser.findPDB(testProgram, false, pdbXmlDir); - - // Should not find anything since repo is set to an invalid path - assertNull(pdb); - } - - /** - * notepad.pdb is here: /symbols/ - * notepad.pdb.xml is here: /symbols/ - * Repo location set to /symbols - * - * On Windows, PDB file should be found. - * On non-Windows, PDB XML file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb8() throws Exception { - - createdFiles = createFiles(PdbLocation.SYMBOLS_NO_SUBDIR, PdbXmlLocation.SAME_AS_PDB); - - File pdb = PdbParser.findPDB(testProgram, false, symbolsFolder); - - assertNotNull(pdb); - - if (PdbParser.onWindows) { - assertEquals(pdbFile, pdb); - } - else { - assertEquals(pdbXmlFile, pdb); - } - } - - /** - * notepad.pdb is here: /symbols/ - * notepad.pdb.xml is here: /pdb_xml/ - * Repo location set to /symbols - * - * On Windows, PDB file should be found. - * On non-Windows, PDB file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb9() throws Exception { - - createdFiles = createFiles(PdbLocation.SYMBOLS_NO_SUBDIR, PdbXmlLocation.OWN_DIR); - - File pdb = PdbParser.findPDB(testProgram, false, symbolsFolder); - - assertNotNull(pdb); - assertEquals(pdbFile, pdb); - - } - - /** - * notepad.pdb is here: /symbols/ - * notepad.pdb.xml is here: /pdb_xml/ - * Repo location set to /pdb_xml - * - * On Windows, PDB XML file should be found. - * On non-Windows, PDB XML file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb10() throws Exception { - - createdFiles = createFiles(PdbLocation.SYMBOLS_NO_SUBDIR, PdbXmlLocation.OWN_DIR); - - File pdb = PdbParser.findPDB(testProgram, false, pdbXmlDir); - - assertNotNull(pdb); - - if (PdbParser.onWindows) { - assertEquals(pdbXmlFile, pdb); - } - else { - assertEquals(pdbXmlFile, pdb); - } - } - - /** - * notepad.pdb is here: /exe/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * notepad.pdb.xml does not exist - * Repo location set to /exe/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * - * On Windows, PDB file should be found - * On non-Windows, PDB file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb11() throws Exception { - - createdFiles = createFiles(PdbLocation.SAME_AS_EXE_SUBDIR, PdbXmlLocation.NONE); - - File pdb = PdbParser.findPDB(testProgram, false, pdbFile.getParentFile()); - - assertNotNull(pdb); - assertEquals(pdbFile, pdb); - - } - - /** - * notepad.pdb is here: /exe/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * notepad.pdb.xml is here: /exe/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * Repo location set to default location - * - * PDB file should not be found, since default repo does not point to exe folder. - * - * @throws Exception - */ - @Test - public void testFindPdb12() throws Exception { - - createdFiles = createFiles(PdbLocation.SAME_AS_EXE_SUBDIR, PdbXmlLocation.SAME_AS_PDB); - - File pdb = PdbParser.findPDB(testProgram, false, noSuchSymbolsRepoDir); - - // Should not find anything since repo is set to an invalid path - assertNull(pdb); - } - - /** - * notepad.pdb is here: /exe/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * notepad.pdb.xml is here: /exe/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * Repo location set to /exe/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * - * On Windows, PDB file should be found. - * On non-Windows, PDB XML file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb13() throws Exception { - - createdFiles = createFiles(PdbLocation.SAME_AS_EXE_SUBDIR, PdbXmlLocation.SAME_AS_PDB); - - File pdb = PdbParser.findPDB(testProgram, false, pdbFile.getParentFile()); - - assertNotNull(pdb); - - if (PdbParser.onWindows) { - assertEquals(pdbFile, pdb); - } - else { - assertEquals(pdbXmlFile, pdb); - } - } - - /** - * notepad.pdb is here: /exe/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * notepad.pdb.xml is here: /pdb_xml/ - * Repo location set to /exe/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * - * On Windows, PDB file should be found. - * On non-Windows, PDB file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb14() throws Exception { - - createdFiles = createFiles(PdbLocation.SAME_AS_EXE_SUBDIR, PdbXmlLocation.OWN_DIR); - - File pdb = PdbParser.findPDB(testProgram, false, pdbFile.getParentFile()); - - assertNotNull(pdb); - assertEquals(pdbFile, pdb); - - } - - /** - * notepad.pdb is here: /exe/notepad.pdb/36CFD5F9888C4483B522B9DB242D84782/ - * notepad.pdb.xml is here: /pdb_xml/ - * Repo location set to /pdb_xml/ - * - * On Windows, PDB XML file should be found. - * On non-Windows, PDB XML file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb15() throws Exception { - - createdFiles = createFiles(PdbLocation.SAME_AS_EXE_SUBDIR, PdbXmlLocation.OWN_DIR); - - File pdb = PdbParser.findPDB(testProgram, false, pdbXmlFile.getParentFile()); - - assertNotNull(pdb); - - if (PdbParser.onWindows) { - assertEquals(pdbXmlFile, pdb); - } - else { - assertEquals(pdbXmlFile, pdb); - } - } - - /** - * notepad.pdb is here: /exe/ - * notepad.pdb.xml does not exist - * Repo location set to default location - * - * Special case: Even if repo location is set to a location that doesn't contain a .pdb or - * .pdb.xml file, it will still be found if there is a file in the same folder as the binary - * - * On Windows, PDB file should be found. - * On non-Windows, PDB file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb16() throws Exception { - - createdFiles = createFiles(PdbLocation.SAME_AS_EXE_NO_SUBDIR, PdbXmlLocation.NONE); - - File pdb = PdbParser.findPDB(testProgram, false, noSuchSymbolsRepoDir); - - assertNotNull(pdb); - assertEquals(pdbFile, pdb); - - } - - /** - * notepad.pdb is here: /exe/ - * notepad.pdb.xml does not exist - * Repo location set to /exe/ - * - * On Windows, PDB file should be found. - * On non-Windows, PDB file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb17() throws Exception { - - createdFiles = createFiles(PdbLocation.SAME_AS_EXE_NO_SUBDIR, PdbXmlLocation.NONE); - - File pdb = PdbParser.findPDB(testProgram, false, pdbFile.getParentFile()); - - assertNotNull(pdb); - assertEquals(pdbFile, pdb); - - } - - /** - * notepad.pdb is here: /exe/ - * notepad.pdb.xml does not exist - * Repo location set to /pdb_xml/ - * - * Special case: Even if repo location is set to a location that doesn't contain a .pdb or - * .pdb.xml file, it will still be found if there is a file in the same folder as the binary - * - * On Windows, PDB file should be found. - * On non-Windows, PDB file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb18() throws Exception { - - createdFiles = createFiles(PdbLocation.SAME_AS_EXE_NO_SUBDIR, PdbXmlLocation.NONE); - - File pdb = PdbParser.findPDB(testProgram, false, pdbXmlDir); - - assertNotNull(pdb); - assertEquals(pdbFile, pdb); - - } - - /** - * notepad.pdb is here: /exe/ - * notepad.pdb.xml is here: /exe/ - * Repo location set to the default location - * - * Special case: Even if repo location is set to a location that doesn't contain a .pdb or - * .pdb.xml file, it will still be found if there is a file in the same folder as the binary - * - * On Windows, PDB file should be found. - * On non-Windows, PDB XML file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb19() throws Exception { - - createdFiles = createFiles(PdbLocation.SAME_AS_EXE_NO_SUBDIR, PdbXmlLocation.SAME_AS_PDB); - - File pdb = PdbParser.findPDB(testProgram, false, noSuchSymbolsRepoDir); - - assertNotNull(pdb); - - if (PdbParser.onWindows) { - assertEquals(pdbFile, pdb); - } - else { - assertEquals(pdbXmlFile, pdb); - } - } - - /** - * notepad.pdb is here: /exe/ - * notepad.pdb.xml is here: /exe/ - * Repo location set to /exe/ - * - * On Windows, PDB file should be found. - * On non-Windows, PDB XML file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb20() throws Exception { - - createdFiles = createFiles(PdbLocation.SAME_AS_EXE_NO_SUBDIR, PdbXmlLocation.SAME_AS_PDB); - - File pdb = PdbParser.findPDB(testProgram, false, pdbFile.getParentFile()); - - assertNotNull(pdb); - - if (PdbParser.onWindows) { - assertEquals(pdbFile, pdb); - } - else { - assertEquals(pdbXmlFile, pdb); - } - } - - /** - * notepad.pdb is here: /exe/ - * notepad.pdb.xml is here: /pdb_xml/ - * Repo location set to default location - * - * On Windows, PDB file should be found (Repo path is invalid, so it looks in same location as exe). - * On non-Windows, PDB file should be found (Repo path is invalid, so it looks in same location as exe). - * - * @throws Exception - */ - @Test - public void testFindPdb21() throws Exception { - - createdFiles = createFiles(PdbLocation.SAME_AS_EXE_NO_SUBDIR, PdbXmlLocation.OWN_DIR); - - File pdb = PdbParser.findPDB(testProgram, false, noSuchSymbolsRepoDir); - - assertNotNull(pdb); - assertEquals(pdbFile, pdb); - } - - /** - * notepad.pdb is here: /exe/ - * notepad.pdb.xml is here: /pdb_xml/ - * Repo location set to /exe/ - * - * On Windows, PDB file should be found - * On non-Windows, PDB file should be found. - * - * @throws Exception - */ - @Test - public void testFindPdb22() throws Exception { - - createdFiles = createFiles(PdbLocation.SAME_AS_EXE_NO_SUBDIR, PdbXmlLocation.OWN_DIR); - - File pdb = PdbParser.findPDB(testProgram, false, pdbFile.getParentFile()); - - assertNotNull(pdb); - assertEquals(pdbFile, pdb); - } - - /** - * notepad.pdb is here: /exe/ - * notepad.pdb.xml is here: /pdb_xml/ - * Repo location set to /pdb_xml/ - * - * Special case: Even if repo location is set to a location that doesn't contain a .pdb or - * .pdb.xml file, it will still be found if there is a file in the same folder as the binary - * - * On Windows, PDB XML file should be found (looks first in user-defined repo location) - * On non-Windows, PDB XML file should be found - * - * @throws Exception - */ - @Test - public void testFindPdb23() throws Exception { - - createdFiles = createFiles(PdbLocation.SAME_AS_EXE_NO_SUBDIR, PdbXmlLocation.OWN_DIR); - - File pdb = PdbParser.findPDB(testProgram, false, pdbXmlDir); - - assertNotNull(pdb); - - if (PdbParser.onWindows) { - assertEquals(pdbXmlFile, pdb); - } - else { - assertEquals(pdbXmlFile, pdb); - } - } - - /** - * notepad.pdb does not exist - * notepad.pdb.xml does not exist - * Repo location set to the default location - * - * On Windows, no file should be found - * On non-Windows, no file should be found - * - * @throws Exception - */ - @Test - public void testFindPdb24() throws Exception { - - createdFiles = createFiles(PdbLocation.NONE, PdbXmlLocation.NONE); - - File pdb = PdbParser.findPDB(testProgram, false, noSuchSymbolsRepoDir); - assertNull(pdb); - - } - - /** - * notepad.pdb does not exist - * notepad.pdb.xml is here: /pdb_xml/ - * Repo location set to the default location - * - * On Windows, no file should be found - * On non-Windows, no file should be found - * - * @throws Exception - */ - @Test - public void testFindPdb25() throws Exception { - - createdFiles = createFiles(PdbLocation.NONE, PdbXmlLocation.OWN_DIR); - - File pdb = PdbParser.findPDB(testProgram, false, noSuchSymbolsRepoDir); - assertNull(pdb); - - } - - /** - * notepad.pdb does not exist - * notepad.pdb.xml is here: /pdb_xml/ - * Repo location set to /pdb_xml/ - * - * On Windows, PDB XML file should be found - * On non-Windows, PDB XML file should be found - * - * @throws Exception - */ - @Test - public void testFindPdb26() throws Exception { - - createdFiles = createFiles(PdbLocation.NONE, PdbXmlLocation.OWN_DIR); - - File pdb = PdbParser.findPDB(testProgram, false, pdbXmlDir); - - assertNotNull(pdb); - - if (PdbParser.onWindows) { - assertEquals(pdbXmlFile, pdb); - } - else { - assertEquals(pdbXmlFile, pdb); - } - } - - private void createDirectory(File directory) { - directory.mkdir(); - if (!directory.isDirectory()) { - fail("Should have created directory: " + directory); - } - } - - private void createFile(File file) { - boolean createSuccess; - - try { - createSuccess = file.createNewFile(); - - if (!createSuccess) { - fail("Failed creation of file: " + file); - } - } - catch (IOException ioe) { - fail("Exception while creating: " + file); - } - } - - private void buildPdbXml() throws Exception { - // Write to the pdb.xml file - FileWriter xmlFileWriter; - - try { - xmlFileWriter = new FileWriter(pdbXmlFile); - BufferedWriter xmlBuffWriter = new BufferedWriter(xmlFileWriter); + private File buildPdbXml() throws IOException { + File destFile = new File(tempDir, pdbXmlFilename); + try (BufferedWriter xmlBuffWriter = new BufferedWriter(new FileWriter(destFile))) { xmlBuffWriter.write(""); xmlBuffWriter.write("\n"); - xmlBuffWriter.close(); - } - catch (IOException ioe) { - fail("IOException writing to temporary file (" + pdbXmlFile + "). " + - ioe.toString()); } + return destFile; } @Test public void testApplyFunctions() throws Exception { - createdFiles = createFiles(PdbLocation.NONE, PdbXmlLocation.OWN_DIR); - - buildPdbXml(); - - File pdb = PdbParser.findPDB(testProgram, false, pdbXmlDir); + File pdbXmlFile = buildPdbXml(); AutoAnalysisManager mgr = AutoAnalysisManager.getAnalysisManager(testProgram); DataTypeManagerService dataTypeManagerService = mgr.getDataTypeManagerService(); PdbParser parser = - new PdbParser(pdb, testProgram, dataTypeManagerService, false, TaskMonitor.DUMMY); + new PdbParser(pdbXmlFile, testProgram, dataTypeManagerService, false, false, + TaskMonitor.DUMMY); parser.openDataTypeArchives(); parser.parse(); diff --git a/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/DummySymbolServer.java b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/DummySymbolServer.java new file mode 100644 index 0000000000..ce1d1f608c --- /dev/null +++ b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/DummySymbolServer.java @@ -0,0 +1,85 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import ghidra.util.task.TaskMonitor; + +/** + * A "remote" symbol server that answers affirmatively for any query. + */ +public class DummySymbolServer implements SymbolServer { + + private final byte[] dummyPayload; + private final boolean returnCompressedFilenames; + + public DummySymbolServer(String dummyPayload) { + this(dummyPayload.getBytes(), false); + } + + public DummySymbolServer(byte[] dummyPayload, boolean returnCompressedFilenames) { + this.dummyPayload = dummyPayload; + this.returnCompressedFilenames = returnCompressedFilenames; + } + + @Override + public String getName() { + return "dummy"; + } + + @Override + public boolean isValid(TaskMonitor monitor) { + return true; + } + + @Override + public boolean exists(String filename, TaskMonitor monitor) { + return true; + } + + @Override + public List find(SymbolFileInfo pdbInfo, Set findOptions, + TaskMonitor monitor) { + String name = pdbInfo.getName(); + if (returnCompressedFilenames) { + name = name.substring(0, name.length() - 1) + "_"; + } + SymbolFileLocation symLoc = new SymbolFileLocation(name, this, pdbInfo); + return List.of(symLoc); + } + + @Override + public SymbolServerInputStream getFileStream(String filename, TaskMonitor monitor) + throws IOException { + return new SymbolServerInputStream(new ByteArrayInputStream(dummyPayload), + dummyPayload.length); + } + + @Override + public String getFileLocation(String filename) { + return "dummy-" + filename; + } + + @Override + public boolean isLocal() { + return false; + } + +} diff --git a/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/HttpSymbolServerTest.java b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/HttpSymbolServerTest.java new file mode 100644 index 0000000000..bcaccd4f8b --- /dev/null +++ b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/HttpSymbolServerTest.java @@ -0,0 +1,40 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import static org.junit.Assert.*; + +import java.net.URI; +import java.util.List; + +import ghidra.util.task.TaskMonitor; + +public class HttpSymbolServerTest { + + //@Test + public void test() { + // This test is not enabled by default as it depends on an third-party resource + HttpSymbolServer httpSymbolServer = + new HttpSymbolServer(URI.create("http://msdl.microsoft.com/download/symbols/")); + SymbolFileInfo pdbInfo = + SymbolFileInfo.fromValues("kernelbase.pdb", "C1C44EDD93E1B8BA671874B5C1490C2D", 1); + + List results = + httpSymbolServer.find(pdbInfo, FindOption.NO_OPTIONS, TaskMonitor.DUMMY); + assertEquals(1, results.size()); + } + +} diff --git a/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/LocalSymbolServerTest.java b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/LocalSymbolServerTest.java new file mode 100644 index 0000000000..72f38b7797 --- /dev/null +++ b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/LocalSymbolServerTest.java @@ -0,0 +1,225 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import static org.junit.Assert.*; + +import java.util.List; + +import java.io.File; +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; + +import generic.test.AbstractGenericTest; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; + +/** + * Test searching for symbol files in a local directory structure. + *

    + * Directory level 1, 2 are MS compatible layouts of pdb symbol files. + * Directory level 0 is a ghidra-ism where pdb symbol files can + * be found in a un-organized directory with non-exact file names. + *

    + * Testing level 0 searching is a TODO item because creating test + * files that can be parsed isn't possible right now. (level 1, 2 + * directories can skip parsing the file since the guid/age is + * in the path) + */ +public class LocalSymbolServerTest extends AbstractGenericTest { + + private File temporaryDir; + + private File mkFile(File file) throws IOException { + FileUtilities.checkedMkdirs(file.getParentFile()); + FileUtilities.writeStringToFile(file, "test"); + return file; + } + + @Before + public void setup() throws IOException { + temporaryDir = createTempDirectory("localsymbolserver"); + } + + @Test + public void testCreate_Level0() throws IOException { + File root = new File(temporaryDir, "symbols"); + LocalSymbolStore.create(root, 0); + + assertTrue("Should not create files", root.list().length == 0); + } + + @Test + public void testCreate_Level1() throws IOException { + File root = new File(temporaryDir, "symbols"); + LocalSymbolStore.create(root, 1); + + assertTrue("Pingme should exist", new File(root, "pingme.txt").exists()); + assertTrue("Admin dir should exist", new File(root, "000admin").exists()); + assertFalse("Index2 should not exist", new File(root, "index2.txt").exists()); + } + + @Test + public void testCreate_Level2() throws IOException { + File root = new File(temporaryDir, "symbols"); + LocalSymbolStore.create(root, 2); + + assertTrue("Pingme should exist", new File(root, "pingme.txt").exists()); + assertTrue("Admin dir should exist", new File(root, "000admin").exists()); + assertTrue("Index2 should exist", new File(root, "index2.txt").exists()); + } + + @Test + public void findExact_Level1() throws IOException { + File root = new File(temporaryDir, "symbols"); + LocalSymbolStore.create(root, 1); + LocalSymbolStore localSymbolStore = new LocalSymbolStore(root); + + File pdbFile = mkFile(new File(root, "file1.pdb/112233445/file1.pdb")); + mkFile(new File(root, "file1.pdb/112233446/file1.pdb")); + + List results = + localSymbolStore.find(SymbolFileInfo.fromValues("file1.pdb", "11223344", 5), + FindOption.NO_OPTIONS, TaskMonitor.DUMMY); + + assertEquals(1, results.size()); + + String resultLocation = localSymbolStore.getFileLocation(results.get(0).getPath()); + assertEquals(pdbFile.getPath(), resultLocation); + } + + @Test + public void findAnyAges_Level1() throws IOException { + // find pdbs with the same UID, but any AGE + File root = new File(temporaryDir, "symbols"); + LocalSymbolStore.create(root, 1); + LocalSymbolStore localSymbolStore = new LocalSymbolStore(root); + + mkFile(new File(root, "file1.pdb/112233445/file1.pdb")); + mkFile(new File(root, "file1.pdb/112233446/file1.pdb")); + mkFile(new File(root, "file1.pdb/112233450/file1.pdb")); + + List results = + localSymbolStore.find(SymbolFileInfo.fromValues("file1.pdb", "11223344", 0), + FindOption.of(FindOption.ANY_AGE), TaskMonitor.DUMMY); + + assertEquals(2, results.size()); + assertFalse(results.stream() + .map(symbolFileLocation -> symbolFileLocation.getFileInfo().getUniqueName()) + .anyMatch(s -> !s.equals("11223344"))); + } + + @Test + public void findAnyUIDs_Level1() throws IOException { + File root = new File(temporaryDir, "symbols"); + LocalSymbolStore.create(root, 1); + LocalSymbolStore localSymbolStore = new LocalSymbolStore(root); + + mkFile(new File(root, "file1.pdb/112233400/file1.pdb")); + mkFile(new File(root, "file1.pdb/112233410/file1.pdb")); + mkFile(new File(root, "file1.pdb/112233420/file1.pdb")); + + List results = + localSymbolStore.find(SymbolFileInfo.fromValues("file1.pdb", "11223344", 0), + FindOption.of(FindOption.ANY_ID), TaskMonitor.DUMMY); + + assertEquals(3, results.size()); + } + + @Test + public void findExact_Level2() throws IOException { + File root = new File(temporaryDir, "symbols"); + LocalSymbolStore.create(root, 2); + LocalSymbolStore localSymbolStore = new LocalSymbolStore(root); + + File similarPdbFile1 = mkFile(new File(root, "fi/file1.pdb/112233445/file1.pdb")); + mkFile(new File(root, "fi/file1.pdb/112233446/file1.pdb")); + + List results = + localSymbolStore.find(SymbolFileInfo.fromValues("file1.pdb", "11223344", 5), + FindOption.NO_OPTIONS, TaskMonitor.DUMMY); + + assertEquals(1, results.size()); + + String resultLocation = localSymbolStore.getFileLocation(results.get(0).getPath()); + assertEquals(similarPdbFile1.getPath(), resultLocation); + } + + @Test + public void giveFile_Level0() throws IOException { + File root = new File(temporaryDir, "symbols"); + LocalSymbolStore.create(root, 0); + + LocalSymbolStore localSymbolStore = new LocalSymbolStore(root); + + File file1 = mkFile(new File(temporaryDir, "file1.pdb")); + localSymbolStore.giveFile(SymbolFileInfo.fromValues("file1.pdb", "11223344", 0), file1, + "file1.pdb", TaskMonitor.DUMMY); + + assertFalse(file1.exists()); + + // can't search for the pdb file because a level0 LocalSymbolStore would + // need to open up any 'pdb' files it finds to read the guid/id and age, + // and we can't create good pdbs right now that would enable this. + + File expectedFile = new File(root, "file1.pdb"); + assertTrue(expectedFile.exists()); + } + + @Test + public void giveFile_Level1() throws IOException { + File root = new File(temporaryDir, "symbols"); + LocalSymbolStore.create(root, 1); + LocalSymbolStore localSymbolStore = new LocalSymbolStore(root); + + File file1 = mkFile(new File(temporaryDir, "file1.pdb")); + localSymbolStore.giveFile(SymbolFileInfo.fromValues("file1.pdb", "11223344", 0), file1, + "file1.pdb", TaskMonitor.DUMMY); + + assertFalse(file1.exists()); + + List results = + localSymbolStore.find(SymbolFileInfo.fromValues("file1.pdb", "11223344", 0), + FindOption.NO_OPTIONS, TaskMonitor.DUMMY); + assertEquals(1, results.size()); + assertEquals("file1.pdb/112233440/file1.pdb", results.get(0).getPath()); + assertEquals("11223344", results.get(0).getFileInfo().getUniqueName()); + assertEquals(0, results.get(0).getFileInfo().getIdentifiers().getAge()); + } + + @Test + public void giveFile_Level2() throws IOException { + File root = new File(temporaryDir, "symbols"); + LocalSymbolStore.create(root, 1); + LocalSymbolStore localSymbolStore = new LocalSymbolStore(root); + + File file1 = mkFile(new File(temporaryDir, "file1.pdb")); + localSymbolStore.giveFile(SymbolFileInfo.fromValues("file1.pdb", "11223344", 0), file1, + "file1.pdb", TaskMonitor.DUMMY); + + assertFalse(file1.exists()); + + List results = + localSymbolStore.find(SymbolFileInfo.fromValues("file1.pdb", "11223344", 0), + FindOption.NO_OPTIONS, TaskMonitor.DUMMY); + assertEquals(1, results.size()); + assertEquals("file1.pdb/112233440/file1.pdb", results.get(0).getPath()); + assertEquals("11223344", results.get(0).getFileInfo().getUniqueName()); + assertEquals(0, results.get(0).getFileInfo().getIdentifiers().getAge()); + } +} diff --git a/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/SymbolServerInstanceCreatorRegistryTest.java b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/SymbolServerInstanceCreatorRegistryTest.java new file mode 100644 index 0000000000..00d0c2fc07 --- /dev/null +++ b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/SymbolServerInstanceCreatorRegistryTest.java @@ -0,0 +1,101 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import static org.junit.Assert.*; + +import java.util.List; + +import java.io.File; +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; + +import generic.test.AbstractGenericTest; + +public class SymbolServerInstanceCreatorRegistryTest extends AbstractGenericTest { + + private SymbolServerInstanceCreatorRegistry symbolServerInstanceCreatorRegistry = + SymbolServerInstanceCreatorRegistry.getInstance(); + private SymbolServerInstanceCreatorContext symbolServerInstanceCreatorContext = + symbolServerInstanceCreatorRegistry.getContext(); + private File temporaryDir; + + @Before + public void setup() throws IOException { + temporaryDir = createTempDirectory("localsymbolserver"); + } + + @Test + public void testCreateLocalSymbolStore() { + SymbolServer symbolServer = symbolServerInstanceCreatorRegistry + .newSymbolServer(temporaryDir.getPath(), symbolServerInstanceCreatorContext); + assertNotNull(symbolServer); + assertTrue(symbolServer instanceof LocalSymbolStore); + } + + @Test + public void testCreateHttpSymbolServer() { + SymbolServer symbolServer = symbolServerInstanceCreatorRegistry + .newSymbolServer("http://localhost/blah", symbolServerInstanceCreatorContext); + assertNotNull(symbolServer); + assertTrue(symbolServer instanceof HttpSymbolServer); + } + + @Test + public void testCreateHttpsSymbolServer() { + SymbolServer symbolServer = symbolServerInstanceCreatorRegistry + .newSymbolServer("https://localhost/blah", symbolServerInstanceCreatorContext); + assertNotNull(symbolServer); + assertTrue(symbolServer instanceof HttpSymbolServer); + } + + @Test + public void testCreateSameDirSymbolStore() { + SymbolServer symbolServer = symbolServerInstanceCreatorRegistry.newSymbolServer(".", + symbolServerInstanceCreatorContext); + assertNotNull(symbolServer); + assertTrue(symbolServer instanceof SameDirSymbolStore); + } + + @Test + public void testCreateDisabledSymbolServer() { + SymbolServer symbolServer = symbolServerInstanceCreatorRegistry + .newSymbolServer("disabled://.", symbolServerInstanceCreatorContext); + assertNotNull(symbolServer); + assertTrue(symbolServer instanceof DisabledSymbolServer); + assertTrue( + ((DisabledSymbolServer) symbolServer).getSymbolServer() instanceof SameDirSymbolStore); + } + + @Test + public void testBogusLocation() { + SymbolServer symbolServer = symbolServerInstanceCreatorRegistry.newSymbolServer("blah://", + symbolServerInstanceCreatorContext); + assertNull(symbolServer); + } + + @Test + public void testPath() { + List symbolServerResultList = + symbolServerInstanceCreatorRegistry.createSymbolServersFromPathList( + List.of(".", "http://localhost/blah"), symbolServerInstanceCreatorContext); + assertEquals(2, symbolServerResultList.size()); + assertTrue(symbolServerResultList.get(0) instanceof SameDirSymbolStore); + assertTrue(symbolServerResultList.get(1) instanceof HttpSymbolServer); + } +} diff --git a/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/SymbolServerServiceTest.java b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/SymbolServerServiceTest.java new file mode 100644 index 0000000000..c32f8ca3c7 --- /dev/null +++ b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/SymbolServerServiceTest.java @@ -0,0 +1,160 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.junit.Before; +import org.junit.Test; + +import generic.test.AbstractGenericTest; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; + +/** + * Also see SymbolServerService2Test in the _Integration Tests module for tests that + * decompress compressed pdb files. + */ +public class SymbolServerServiceTest extends AbstractGenericTest { + private File temporaryDir; + private File localSymbolStore1Root; + private File localSymbolStore2Root; + private LocalSymbolStore localSymbolStore1; + private LocalSymbolStore localSymbolStore2; + + private File mkFile(File file) throws IOException { + FileUtilities.checkedMkdirs(file.getParentFile()); + FileUtilities.writeStringToFile(file, "test"); + return file; + } + + @Before + public void setup() throws IOException { + temporaryDir = createTempDirectory("symbolservers"); + localSymbolStore1Root = new File(temporaryDir, "symbols1"); + localSymbolStore2Root = new File(temporaryDir, "symbols2"); + LocalSymbolStore.create(localSymbolStore1Root, 1); + LocalSymbolStore.create(localSymbolStore2Root, 1); + + localSymbolStore1 = new LocalSymbolStore(localSymbolStore1Root); + localSymbolStore2 = new LocalSymbolStore(localSymbolStore2Root); + } + + @Test + public void test_Exact_AlreadyLocal() throws IOException, CancelledException { + File pdbFile1 = mkFile(new File(localSymbolStore1Root, "file1.pdb/112233440/file1.pdb")); + File pdbFile2 = mkFile(new File(localSymbolStore2Root, "file1.pdb/112233440/file1.pdb")); + + SymbolServerService symbolServerService = new SymbolServerService(localSymbolStore1, + List.of(localSymbolStore1, localSymbolStore2)); + List results = + symbolServerService.find(SymbolFileInfo.fromValues("file1.pdb", "11223344", 0), + TaskMonitor.DUMMY); + + assertEquals(2, results.size()); + + File foundPdbFile1 = symbolServerService.getSymbolFile(results.get(0), TaskMonitor.DUMMY); + File foundPdbFile2 = symbolServerService.getSymbolFile(results.get(1), TaskMonitor.DUMMY); + + assertEquals(pdbFile1, foundPdbFile1); + assertEquals(pdbFile2, foundPdbFile2); + } + + @Test + public void test_AnyAge() throws IOException, CancelledException { + // search for similar pdbs, across multiple storage servers + mkFile(new File(localSymbolStore1Root, "file1.pdb/000000001/file1.pdb")); + mkFile(new File(localSymbolStore1Root, "file1.pdb/112233441/file1.pdb")); + mkFile(new File(localSymbolStore2Root, "file1.pdb/112233442/file1.pdb")); + + SymbolServerService symbolServerService = + new SymbolServerService(localSymbolStore1, List.of(localSymbolStore2)); + List results = + symbolServerService.find(SymbolFileInfo.fromValues("file1.pdb", "11223344", 0), + FindOption.of(FindOption.ANY_AGE), TaskMonitor.DUMMY); + + assertEquals(2, results.size()); + Set uids = results.stream() + .map(symbolFileLocation -> symbolFileLocation.getFileInfo().getUniqueName()) + .collect(Collectors.toSet()); + assertEquals(1, uids.size()); + assertTrue(uids.contains("11223344")); + } + + @Test + public void test_AnyUID() throws IOException, CancelledException { + // search for similar pdbs, across multiple storage servers + mkFile(new File(localSymbolStore1Root, "file2.pdb/000000001/file2.pdb")); + mkFile(new File(localSymbolStore1Root, "file1.pdb/000000001/file1.pdb")); + mkFile(new File(localSymbolStore1Root, "file1.pdb/112233441/file1.pdb")); + mkFile(new File(localSymbolStore2Root, "file1.pdb/112233442/file1.pdb")); + + SymbolServerService symbolServerService = + new SymbolServerService(localSymbolStore1, List.of(localSymbolStore2)); + List results = + symbolServerService.find(SymbolFileInfo.fromValues("file1.pdb", "11223344", 0), + FindOption.of(FindOption.ANY_ID), TaskMonitor.DUMMY); + + assertEquals(3, results.size()); + Set uids = results.stream() + .map(symbolFileLocation -> symbolFileLocation.getFileInfo().getUniqueName()) + .collect(Collectors.toSet()); + assertEquals(2, uids.size()); + assertTrue(uids.contains("11223344")); + assertTrue(uids.contains("00000000")); + } + + @Test + public void test_Remote() throws IOException, CancelledException { + String payload = "testdummy"; + SymbolServerService symbolServerService = + new SymbolServerService(localSymbolStore1, + List.of(localSymbolStore2, new DummySymbolServer(payload))); + SymbolFileInfo searchPdb = SymbolFileInfo.fromValues("file1.pdb", "11223344", 0); + List results = + symbolServerService.find(searchPdb, FindOption.of(FindOption.ALLOW_REMOTE), + TaskMonitor.DUMMY); + + assertEquals(1, results.size()); + assertTrue(results.get(0).isExactMatch(searchPdb)); + + File pdbFile = symbolServerService.getSymbolFile(results.get(0), TaskMonitor.DUMMY); + assertEquals(payload, Files.readString(pdbFile.toPath())); + } + + @Test + public void test_NoRemote() throws CancelledException { + String payload = "testdummy"; + SymbolServerService symbolServerService = + new SymbolServerService(localSymbolStore1, List.of(new DummySymbolServer(payload))); + SymbolFileInfo searchPdb = SymbolFileInfo.fromValues("file1.pdb", "11223344", 0); + List results = + symbolServerService.find(searchPdb, FindOption.NO_OPTIONS, TaskMonitor.DUMMY); + + assertEquals(0, results.size()); + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java index 24f7964841..7cd9f9e1d2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java @@ -15,9 +15,10 @@ */ package docking; +import java.util.*; + import java.awt.*; import java.awt.event.*; -import java.util.*; import javax.swing.*; import javax.swing.Timer; @@ -62,14 +63,14 @@ public class DialogComponentProvider protected JPanel rootPanel; private JPanel mainPanel; private JComponent workPanel; - private JPanel buttonPanel; + protected JPanel buttonPanel; private JPanel statusPanel; protected JButton okButton; protected JButton applyButton; protected JButton cancelButton; protected JButton dismissButton; private boolean isAlerting; - private JLabel statusLabel; + private GDHtmlLabel statusLabel; private JPanel statusProgPanel; // contains status panel and progress panel private Timer showTimer; private TaskScheduler taskScheduler; @@ -697,7 +698,7 @@ public class DialogComponentProvider }); } - private Color getStatusColor(MessageType type) { + protected Color getStatusColor(MessageType type) { switch (type) { case ALERT: return Color.orange; diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/IntegerTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/IntegerTextField.java index d7686304b1..ae14f37cdb 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/IntegerTextField.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/textfield/IntegerTextField.java @@ -15,11 +15,12 @@ */ package docking.widgets.textfield; +import java.util.ArrayList; +import java.util.List; + import java.awt.*; import java.awt.event.*; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; import javax.swing.*; import javax.swing.event.*; @@ -372,6 +373,15 @@ public class IntegerTextField { textField.setEnabled(enabled); } + /** + * Sets the editable mode for the JTextField component + * + * @param editable boolean flag, if true component is editable + */ + public void setEditable(boolean editable) { + textField.setEditable(editable); + } + /** * Requests focus to the JTextField */ diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/net/HttpClients.java b/Ghidra/Framework/Generic/src/main/java/ghidra/net/HttpClients.java new file mode 100644 index 0000000000..985e28516b --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/net/HttpClients.java @@ -0,0 +1,88 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.net; + +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Redirect; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.SSLContext; + +public class HttpClients { + /** + * Note: java.net.http.HttpClient instances can allocate system resources (file handles), + * and frequently creating a new HttpClient could exhaust system resources. + *

    + * There is no "close()" on a HttpClient to release resources. The system resources + * allocated by HttpClient instances will be released when the instance is gc'd. + * However, since the resources in question (filehandles) are not tied to memory pressure, + * its possible a gc() won't happen before running out of file handles if a few hundred + * HttpClient instances have been created / discarded. + *

    + * Also note, there is no per-connection ability to disable hostname verification in a + * SSL/TLS connection. There is a global flag: + * -Djdk.internal.httpclient.disableHostnameVerification + * + */ + private static HttpClient client; + + /** + * Creates a HttpClient Builder using Ghidra SSL/TLS context info. + * + * @return a new HttpClient Builder + * @throws IOException if error in PKI settings or crypto configuration + */ + public static HttpClient.Builder newHttpClientBuilder() throws IOException { + if (!ApplicationKeyManagerFactory.initialize()) { + if (ApplicationKeyManagerFactory.getKeyStore() != null) { + throw new IOException("Failed to initialize PKI certificate keystore"); + } + } + + try { + return HttpClient.newBuilder() + .sslContext(SSLContext.getDefault()) + .followRedirects(Redirect.NORMAL); + } + catch (NoSuchAlgorithmException nsae) { + throw new IOException("Missing algorithm", nsae); + } + } + + /** + * Returns a shared, plain (no special options) {@link HttpClient}. + * + * @return a {@link HttpClient} + * @throws IOException if error in PKI settings or crypto configuration + */ + public static synchronized HttpClient getHttpClient() throws IOException { + if (client == null) { + client = newHttpClientBuilder().build(); + } + + return client; + } + + /** + * Clears the currently cached {@link HttpClient}, forcing it to be + * rebuilt during the next call to {@link #getHttpClient()}. + */ + public static synchronized void clearHttpClient() { + client = null; + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/net/SSLContextInitializer.java b/Ghidra/Framework/Generic/src/main/java/ghidra/net/SSLContextInitializer.java index fa631e97fa..3708f05ed4 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/net/SSLContextInitializer.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/net/SSLContextInitializer.java @@ -110,6 +110,10 @@ public class SSLContextInitializer implements ModuleInitializer { // Establish default HTTPS socket factory HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); + // Force the HttpClient to be re-created by the next request to + // HttpClients.getHttpClient() so that the new SSLContext is used + HttpClients.clearHttpClient(); + return true; } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/net/http/HttpUtil.java b/Ghidra/Framework/Generic/src/main/java/ghidra/net/http/HttpUtil.java index ed85bdf285..2f406ccdb2 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/net/http/HttpUtil.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/net/http/HttpUtil.java @@ -15,39 +15,15 @@ */ package ghidra.net.http; -import ghidra.net.ApplicationKeyManagerFactory; -import ghidra.util.Msg; - import java.io.*; import java.net.*; import java.util.Properties; +import ghidra.net.ApplicationKeyManagerFactory; +import ghidra.util.Msg; + public class HttpUtil { - public static void main(String[] args) { - - Properties properties = new Properties(); - properties.setProperty("User-Agent", "Microsoft-Symbol-Server/6.3.9600.17298"); - - String urlStr = - "http://msdl.microsoft.com/download/symbols/write.pdb/4FD8CA6696F445A7B969AB9BBD76E4591/write.pd_"; - - String homeDir = System.getProperty("user.home"); - File f = new File(homeDir + "/Downloads", "write.pdb.deleteme"); - - try { - getFile(urlStr, properties, true, f); - System.out.println("getFile completed: " + f); - } - catch (MalformedURLException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } /** * Execute an HTTP/HTTPS GET request and return the resulting HttpURLConnection. @@ -155,4 +131,5 @@ public class HttpUtil { return connection.getContentType(); } + } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/filechooser/ExtensionFileFilter.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/filechooser/ExtensionFileFilter.java index 8a253c8def..fe026f04c1 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/filechooser/ExtensionFileFilter.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/filechooser/ExtensionFileFilter.java @@ -15,25 +15,23 @@ */ package ghidra.util.filechooser; +import java.util.*; +import java.util.stream.Collectors; + import java.io.File; import java.io.FileFilter; -import java.util.Enumeration; -import java.util.Hashtable; /** * A convenience implementation of FileFilter that filters out * all files except for those type extensions that it knows about. - * - * Extensions are of the type ".foo", which is typically found on - * Windows and Unix boxes, but not on Mac. Case is ignored. - * + *

    + * Extensions are of the type "foo" (no leading dot). Case is ignored. + *

    * Example - create a new filter that filters out all files * but gif and jpg image files: *

      *     GhidraFileChooser chooser = new GhidraFileChooser();
    - *     ExtensionFileFilter filter = new ExtensionFileFilter(
    - *                   new String{"gif", "jpg"}, "JPEG and GIF Images")
    - *     chooser.addFileFilter(filter);
    + *     chooser.addFileFilter(ExtensionFilFilter.forExtensions("JPEG and GIF Images", "gif", "jpg"));
      *
    */ public class ExtensionFileFilter implements GhidraFileFilter { @@ -50,19 +48,16 @@ public class ExtensionFileFilter implements GhidraFileFilter { return eff; } - private Hashtable filters = null; - private String description = null; - private String fullDescription = null; - private boolean useExtensionsInDescription = true; + private List extensions; + private String description; + private String fullDescription; /** * Creates a file filter that accepts the given file type. - * Example: new ExtensionFileFilter("jpg", "JPEG Image Images"); + * Example: new ExtensionFileFilter("jpg", "JPEG Images"); * - * Note that the "." before the extension is not needed. If - * provided, it will be ignored. - * - * @see #addExtension + * @param extension file extension to match, without leading dot + * @param description descriptive string of the filter */ public ExtensionFileFilter(String extension, String description) { this(new String[] { extension }, description); @@ -72,16 +67,15 @@ public class ExtensionFileFilter implements GhidraFileFilter { * Creates a file filter from the given string array and description. * Example: new ExtensionFileFilter(String {"gif", "jpg"}, "Gif and JPG Images"); * - * Note that the "." before the extension is not needed and will be ignored. - * - * @see #addExtension + * @param filters array of file name extensions, each without a leading dot + * @param description descriptive string of the filter */ public ExtensionFileFilter(String[] filters, String description) { - this.filters = new Hashtable(filters.length); - for (String filter : filters) { - addExtension(filter);//add filters one by one - } - setDescription(description); + this.extensions = Arrays.asList(filters) + .stream() + .map(String::toLowerCase) + .collect(Collectors.toList()); + this.description = description; } /** @@ -90,7 +84,6 @@ public class ExtensionFileFilter implements GhidraFileFilter { * * Files that begin with "." are ignored. * - * @see #getExtension * @see FileFilter#accept */ @Override @@ -101,133 +94,37 @@ public class ExtensionFileFilter implements GhidraFileFilter { if (model.isDirectory(f)) { return true; } - if (filters.size() == 0) { + if (extensions.isEmpty()) { return true; } - String extension = getExtension(f); - return extension != null && filters.get(extension) != null; - } - - /** - * Return the extension portion of the file's name . - * - * @see #getExtension - * @see FileFilter#accept - */ - public String getExtension(File f) { - if (f != null) { - String filename = f.getName(); - int i = filename.lastIndexOf('.'); - if (i > 0 && i < filename.length() - 1) { - return filename.substring(i + 1).toLowerCase(); + String filename = f.getName().toLowerCase(); + if (filename.startsWith(".")) { + return false; + } + int fnLen = filename.length(); + for (String ext : extensions) { + int extLen = ext.length(); + int extStart = fnLen - extLen; + if (extStart > 0 && filename.substring(extStart).equals(ext) && + filename.charAt(extStart - 1) == '.') { + return true; } } - return null; + return false; } - /** - * Adds a filetype "dot" extension to filter against. - * - * For example: the following code will create a filter that filters - * out all files except those that end in ".jpg" and ".tif": - * - * ExtensionFileFilter filter = new ExtensionFileFilter(); - * filter.addExtension("jpg"); - * filter.addExtension("tif"); - * - * Note that the "." before the extension is not needed and will be ignored. - */ - public void addExtension(String extension) { - if (filters == null) { - filters = new Hashtable(5); - } - filters.put(extension.toLowerCase(), this); - fullDescription = null; - } - - /** - * Returns the human readable description of this filter. For - * example: "JPEG and GIF Image Files (*.jpg, *.gif)" - */ @Override public String getDescription() { if (fullDescription == null) { - fullDescription = ""; - if (description == null || isExtensionListInDescription()) { - if (description != null) { - fullDescription = description; - } - fullDescription += " ("; - // build the description from the extension list + fullDescription = Objects.requireNonNullElse(description, ""); - if (filters.size() == 0) { - fullDescription += "*.*"; - } - else { - boolean firstExt = true; - Enumeration extensions = filters.keys(); - if (extensions != null) { - while (extensions.hasMoreElements()) { - if (!firstExt) { - fullDescription += ","; - } - else { - firstExt = false; - } - fullDescription += "*." + extensions.nextElement(); - } - } - } - fullDescription += ")"; - } - else { - fullDescription = description; - } + // add prettified extensions to the description string + fullDescription += " ("; + fullDescription += extensions.isEmpty() + ? "*.*" + : extensions.stream().map(s -> "*." + s).collect(Collectors.joining(",")); + fullDescription += ")"; } return fullDescription; } - - /** - * Sets the human readable description of this filter. For - * example: filter.setDescription("Gif and JPG Images"); - * - * @see #setDescription - * @see #setExtensionListInDescription - * @see #isExtensionListInDescription - */ - public void setDescription(String description) { - this.description = description; - fullDescription = null; - } - - /** - * Determines whether the extension list (.jpg, .gif, etc) should - * show up in the human readable description. - * - * Only relevant if a description was provided in the constructor - * or using setDescription(); - * - * @see #getDescription - * @see #setDescription - * @see #isExtensionListInDescription - */ - public void setExtensionListInDescription(boolean b) { - useExtensionsInDescription = b; - fullDescription = null; - } - - /** - * Returns whether the extension list (.jpg, .gif, etc) should - * show up in the human readable description. - * - * Only relevant if a description was provided in the constructor - * or using setDescription(); - * - * @see #getDescription - * @see #setDescription - * @see #setExtensionListInDescription - */ - public final boolean isExtensionListInDescription() { - return useExtensionsInDescription; - } } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/MonitoredRunnable.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/MonitoredRunnable.java index f5365e5b57..f4dfde9d95 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/MonitoredRunnable.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/MonitoredRunnable.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +15,16 @@ */ package ghidra.util.task; +/** + * Similar to a {@link Runnable} except the {@link #monitoredRun(TaskMonitor) run} + * method is given a monitor to report progress and check for cancellation. + */ public interface MonitoredRunnable { /** - * Similar to a runnable except that is given a monitor to report progress and check for - * cancellation. - * @param monitor the TaskMonitor to use. - */ - public void monitoredRun(TaskMonitor monitor); + * Similar to a runnable except the run method is given a monitor + * to report progress and check for cancellation. + * @param monitor the TaskMonitor to use. + */ + void monitoredRun(TaskMonitor monitor); } diff --git a/Ghidra/Framework/Generic/src/test/java/ghidra/net/http/HttpUtilTest.java b/Ghidra/Framework/Generic/src/test/java/ghidra/net/http/HttpUtilTest.java new file mode 100644 index 0000000000..b61fce7e1b --- /dev/null +++ b/Ghidra/Framework/Generic/src/test/java/ghidra/net/http/HttpUtilTest.java @@ -0,0 +1,48 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.net.http; + +import java.util.Properties; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; + +public class HttpUtilTest { + public static void main(String[] args) { + + Properties properties = new Properties(); + properties.setProperty("User-Agent", "Microsoft-Symbol-Server/6.3.9600.17298"); + + String urlStr = + "http://msdl.microsoft.com/download/symbols/write.pdb/4FD8CA6696F445A7B969AB9BBD76E4591/write.pd_"; + + String homeDir = System.getProperty("user.home"); + File f = new File(homeDir + "/Downloads", "write.pdb.deleteme"); + + try { + HttpUtil.getFile(urlStr, properties, true, f); + System.out.println("getFile completed: " + f); + } + catch (MalformedURLException e) { + e.printStackTrace(); + } + catch (IOException e) { + e.printStackTrace(); + } + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/PdbScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/PdbScreenShots.java index 3de680ed42..63c56cb4fe 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/PdbScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/PdbScreenShots.java @@ -15,102 +15,183 @@ */ package help.screenshot; -import java.awt.Dimension; -import java.awt.Window; -import java.util.ArrayList; import java.util.List; -import org.junit.Test; +import java.io.File; +import java.io.IOException; +import java.net.URI; -import docking.widgets.dialogs.ObjectChooserDialog; -import ghidra.app.util.pdb.PdbLocator; -import ghidra.util.Msg; -import pdb.URLChoice; +import org.apache.commons.io.FilenameUtils; +import org.junit.*; + +import ghidra.app.util.bin.format.pdb.PdbInfo; +import ghidra.app.util.bin.format.pdb.PdbInfoDotNet; +import ghidra.app.util.datatype.microsoft.GUID; +import ghidra.framework.options.Options; +import ghidra.program.model.listing.Program; +import pdb.PdbPlugin; +import pdb.symbolserver.*; +import pdb.symbolserver.ui.ConfigPdbDialog; +import pdb.symbolserver.ui.LoadPdbDialog; public class PdbScreenShots extends GhidraScreenShotGenerator { - @Test - public void testPdbOrXmlDialog() throws Exception { + private static final String GUID1_STR = "012345670123012301230123456789AB"; - performAction("Download_PDB_File", "PdbSymbolServerPlugin", false); + private int tx; + private File temporaryDir; - Window pdbDialog = waitForWindow("pdb or pdb.xml"); - pdbDialog.setSize(new Dimension(750, 200)); - captureWindow(pdbDialog); + @Override + @Before + public void setUp() throws Exception { + super.setUp(); - pressButtonByText(pdbDialog, "Cancel"); + temporaryDir = createTempDirectory("example_pdb"); + tx = program.startTransaction("set analyzed flag"); + Options proplist = program.getOptions(Program.PROGRAM_INFO); + proplist.setBoolean(Program.ANALYZED, false); + PdbInfo pdbInfo = PdbInfoDotNet.fromValues("HelloWorld.pdb", 1, new GUID(GUID1_STR)); + pdbInfo.serializeToOptions(proplist); + proplist.setString("Executable Location", + new File(temporaryDir, program.getName()).getPath()); + } + + @Override + @After + public void tearDown() throws Exception { + program.endTransaction(tx, false); + super.tearDown(); } @Test - public void testPeSpecifiedPathDialog() throws Exception { - - performAction("Download_PDB_File", "PdbSymbolServerPlugin", false); - - Window pdbDialog = waitForWindow("pdb or pdb.xml"); - pressButtonByText(pdbDialog, "PDB"); - - Window peSpecifiedPathDialog = waitForWindow("PE-specified PDB Path"); - captureWindow(peSpecifiedPathDialog); - - pressButtonByText(peSpecifiedPathDialog, "Cancel"); + public void testSymbolServerConfig_Screenshot() throws IOException { + PdbPlugin.saveSymbolServerServiceConfig(null); + ConfigPdbDialog configPdbDialog = new ConfigPdbDialog(); + showDialogWithoutBlocking(tool, configPdbDialog); + waitForSwing(); + captureDialog(ConfigPdbDialog.class); } @Test - public void testSymbolServerURLDialog() throws Exception { - - // Set up for local directory - PdbLocator.setDefaultPdbSymbolsDir(getTestDataDirectory()); - - performAction("Download_PDB_File", "PdbSymbolServerPlugin", false); - - Window pdbDialog = waitForWindow("pdb or pdb.xml"); - pressButtonByText(pdbDialog, "PDB"); - - Window peSpecifiedPathDialog = waitForWindow("PE-specified PDB Path"); - pressButtonByText(peSpecifiedPathDialog, "Yes"); - - Window saveLocationDialog = waitForWindow("Select Location to Save Retrieved File"); - pressButtonByText(saveLocationDialog, "OK"); - - Window urlDialog = waitForWindow("Symbol Server URL"); - urlDialog.setSize(new Dimension(850, 135)); - - captureWindow(urlDialog); - - pressButtonByText(urlDialog, "Cancel"); + public void testLoadPdb_Initial_Screenshot() throws IOException { + LoadPdbDialog loadPdbDialog = new LoadPdbDialog(program); + showDialogWithoutBlocking(tool, loadPdbDialog); + captureDialog(loadPdbDialog); + pressButtonByText(loadPdbDialog, "Cancel"); } @Test - public void testKnownSymbolServerURLsDialog() throws Exception { - - List urlChoices = new ArrayList<>(); - urlChoices.add(new URLChoice("Internet", "https://msdl.microsoft.com/download/symbols")); - urlChoices.add(new URLChoice("My Network", "https://my_symbol_server.my.org")); - - final ObjectChooserDialog urlDialog = new ObjectChooserDialog<>("Choose a URL", - URLChoice.class, urlChoices, "getNetwork", "getUrl"); + public void testSymbolServerConfig_AddButtonMenu() throws IOException { + File localSymbolStore1Root = new File(temporaryDir, "symbols"); + LocalSymbolStore.create(localSymbolStore1Root, 1); + LocalSymbolStore localSymbolStore1 = + new LocalSymbolStoreWithFakePath(localSymbolStore1Root, "/home/user/symbols"); + SymbolServerService symbolServerService = + new SymbolServerService(localSymbolStore1, List.of()); + PdbPlugin.saveSymbolServerServiceConfig(symbolServerService); + LoadPdbDialog choosePdbDialog = new LoadPdbDialog(program); + showDialogWithoutBlocking(tool, choosePdbDialog); + waitForSwing(); + pressButtonByText(choosePdbDialog, "Advanced >>"); runSwing(() -> { - // Do nothing + choosePdbDialog.pushAddLocationBution(); }); - showDialogWithoutBlocking(tool, urlDialog); - captureDialog(); - - pressButtonByText(urlDialog, "Cancel"); + waitForSwing(); + captureMenu(); } @Test - public void testSuccessDialog() throws Exception { + public void testLoadPdb_Advanced_NeedsConfig() throws IOException { + PdbPlugin.saveSymbolServerServiceConfig(null); + LoadPdbDialog choosePdbDialog = new LoadPdbDialog(program); + showDialogWithoutBlocking(tool, choosePdbDialog); + waitForSwing(); + pressButtonByText(choosePdbDialog, "Advanced >>"); + waitForSwing(); + captureDialog(LoadPdbDialog.class); + pressButtonByText(choosePdbDialog, "Cancel"); + } - // Can't really get success message without actually downloading a file. - // So, fake out the message by showing the same sort of dialog the user would see. - Msg.showInfo(getClass(), null, "File Retrieved", - "Downloaded and saved file 'example.pdb' to \n" + - "C:\\Symbols\\example.pdb\\1123A456B7889012C3DDFA4556789B011"); + @Test + public void testLoadPdb_Advanced_Screenshot() throws IOException { + // Show the advanced side of the LoadPdbDialog, with + // some faked search locations and search results so we + // can have pretty paths + File localSymbolStore1Root = new File(temporaryDir, "symbols"); + LocalSymbolStore.create(localSymbolStore1Root, 1); + LocalSymbolStore localSymbolStore1 = + new LocalSymbolStoreWithFakePath(localSymbolStore1Root, "/home/user/symbols"); + SameDirSymbolStoreWithFakePath sameDirSymbolStoreWithFakePath = + new SameDirSymbolStoreWithFakePath(temporaryDir, "/home/user/examples"); + List symbolServers = List.of(sameDirSymbolStoreWithFakePath, + new HttpSymbolServer(URI.create("https://msdl.microsoft.com/download/symbols/"))); + SymbolServerService symbolServerService = + new SymbolServerService(localSymbolStore1, symbolServers); + PdbPlugin.saveSymbolServerServiceConfig(symbolServerService); - Window successDialog = waitForWindow("File Retrieved"); - captureWindow(successDialog); + LoadPdbDialog loadPdbDialog = new LoadPdbDialog(program); + showDialogWithoutBlocking(tool, loadPdbDialog); + waitForSwing(); + pressButtonByText(loadPdbDialog, "Advanced >>"); + List symbolFileLocations = List.of( + new SymbolFileLocation("HelloWorld.pdb/" + GUID1_STR + "1/HelloWorld.pdb", + localSymbolStore1, SymbolFileInfo.fromValues("HelloWorld.pdb", GUID1_STR, 1)), + new SymbolFileLocation("HelloWorld.pdb/" + GUID1_STR + "2/HelloWorld.pdb", + localSymbolStore1, SymbolFileInfo.fromValues("HelloWorld.pdb", GUID1_STR, 2)), + new SymbolFileLocation("HelloWorld.pdb", sameDirSymbolStoreWithFakePath, + SymbolFileInfo.fromValues("HelloWorld.pdb", GUID1_STR, 1)), + new SymbolFileLocation("HelloWorld_ver2.pdb", sameDirSymbolStoreWithFakePath, + SymbolFileInfo.fromValues("HelloWorld.pdb", GUID1_STR, 2))); + runSwing(() -> { + loadPdbDialog + .setSearchOptions(FindOption.of(FindOption.ALLOW_REMOTE, FindOption.ANY_AGE)); + loadPdbDialog.setSymbolServers(symbolServers); + loadPdbDialog.setSymbolStorageDirectoryTextOnly("/home/user/symbols"); + loadPdbDialog.setSearchResults(symbolFileLocations); + loadPdbDialog.selectRowByLocation(symbolFileLocations.get(0)); + }); + waitForSwing(); + captureDialog(LoadPdbDialog.class); + pressButtonByText(loadPdbDialog, "Cancel"); + } - pressButtonByText(successDialog, "OK"); + private static class LocalSymbolStoreWithFakePath extends LocalSymbolStore { + private String fakeRootDirPath; + + public LocalSymbolStoreWithFakePath(File rootDir, String fakeRootDirPath) { + super(rootDir); + this.fakeRootDirPath = fakeRootDirPath; + } + + @Override + public String getDescriptiveName() { + return fakeRootDirPath; + } + + @Override + public String getFileLocation(String filename) { + return FilenameUtils.concat(fakeRootDirPath, filename); + } + } + + private static class SameDirSymbolStoreWithFakePath extends SameDirSymbolStore { + private String fakeRootDirPath; + + public SameDirSymbolStoreWithFakePath(File rootDir, String fakeRootDirPath) { + super(rootDir); + this.fakeRootDirPath = fakeRootDirPath; + } + + @Override + public String getDescriptiveName() { + return String.format(PROGRAMS_IMPORT_LOCATION_DESCRIPTION_STR + " - %s", + fakeRootDirPath); + } + + @Override + public String getFileLocation(String filename) { + return FilenameUtils.concat(fakeRootDirPath, filename); + } } } diff --git a/Ghidra/Test/IntegrationTest/src/test/java/pdb/symbolserver/SymbolServerService2Test.java b/Ghidra/Test/IntegrationTest/src/test/java/pdb/symbolserver/SymbolServerService2Test.java new file mode 100644 index 0000000000..947d47b932 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test/java/pdb/symbolserver/SymbolServerService2Test.java @@ -0,0 +1,138 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pdb.symbolserver; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FilenameUtils; +import org.junit.Before; +import org.junit.Test; + +import com.google.common.io.BaseEncoding; + +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; + +/** + * Tests for Pdb SymbolServer stuff that need to be in the Integration module because they depend + * on FileFormat's file system stuff to decompress .cab files + */ +public class SymbolServerService2Test extends AbstractGhidraHeadedIntegrationTest { + private File temporaryDir; + private File localSymbolStore1Root; + private LocalSymbolStore localSymbolStore1; + + // Bytes for a very small .cab file that contains a singleton file named 'test.pdb' with + // contents of "test" + byte[] smallCabFileBytes = BaseEncoding.base16() + .decode(("4d5343460000000055000000000000002c000000000000000301010001" + + "00000000000000450000000100010004000000000000000000a248bc5c2000746573742e7064620" + + "066652e4908000400434b2b492d2e0100").toUpperCase()); + + private File mkFile(File file, byte[] bytes) throws IOException { + FileUtilities.checkedMkdirs(file.getParentFile()); + FileUtilities.writeBytes(file, bytes); + return file; + } + + @Before + public void setup() throws IOException { + temporaryDir = createTempDirectory("symbolservers"); + localSymbolStore1Root = new File(temporaryDir, "symbols1"); + LocalSymbolStore.create(localSymbolStore1Root, 1); + + localSymbolStore1 = new LocalSymbolStore(localSymbolStore1Root); + } + + @Test + public void testLocalCab() throws IOException, CancelledException { + mkFile(new File(localSymbolStore1Root, "test.pdb/112233441/test.pd_"), smallCabFileBytes); + + SymbolServerService symbolServerService = + new SymbolServerService(localSymbolStore1, List.of()); + List results = + symbolServerService.find(SymbolFileInfo.fromValues("test.pdb", "11223344", 1), + FindOption.NO_OPTIONS, TaskMonitor.DUMMY); + + assertEquals(1, results.size()); + assertEquals("test.pd_", FilenameUtils.getName(results.get(0).getPath())); + + File pdbFile = symbolServerService.getSymbolFile(results.get(0), TaskMonitor.DUMMY); + assertEquals("test\n" /* extra \n because FileUtilities.getText() adds it */, + FileUtilities.getText(pdbFile)); + + // search again and we should only find the now decompressed pdb file + List results2 = + symbolServerService.find(SymbolFileInfo.fromValues("test.pdb", "11223344", 1), + FindOption.NO_OPTIONS, TaskMonitor.DUMMY); + + assertEquals(1, results2.size()); + assertEquals("test.pdb", FilenameUtils.getName(results2.get(0).getPath())); + } + + @Test + public void testRemoteCab() throws IOException, CancelledException { + + SymbolServerService symbolServerService = + new SymbolServerService(localSymbolStore1, + List.of(new DummySymbolServer(smallCabFileBytes, true))); + + List results = + symbolServerService.find(SymbolFileInfo.fromValues("test.pdb", "11223344", 1), + FindOption.of(FindOption.ALLOW_REMOTE), TaskMonitor.DUMMY); + + assertEquals(1, results.size()); + System.out.println(results.get(0).getLocationStr()); + + File pdbFile = symbolServerService.getSymbolFile(results.get(0), TaskMonitor.DUMMY); + assertEquals("test\n" /* extra \n because FileUtilities.getText() adds it */, + FileUtilities.getText(pdbFile)); + } + + @Test + public void testRemoteCabAlreadyExistLocal() throws IOException, CancelledException { + + SymbolServerService symbolServerService = + new SymbolServerService(localSymbolStore1, + List.of(new DummySymbolServer(smallCabFileBytes, true))); + + List results = + symbolServerService.find(SymbolFileInfo.fromValues("test.pdb", "11223344", 1), + FindOption.of(FindOption.ALLOW_REMOTE), TaskMonitor.DUMMY); + + assertEquals(1, results.size()); + System.out.println(results.get(0).getLocationStr()); + + // cheese the file into the local symbol store after the remote file has been found + // but before it has been downloaded + mkFile(new File(localSymbolStore1Root, "test.pdb/112233441/test.pdb"), + "nottest".getBytes()); + + // normally this would download the remote file and decompress it + File pdbFile = symbolServerService.getSymbolFile(results.get(0), TaskMonitor.DUMMY); + + // ensure that the original file wasn't overwritten by the new file + assertEquals("nottest\n" /* extra \n because FileUtilities.getText() adds it */, + FileUtilities.getText(pdbFile)); + } +}