From fe5ea1c91a9902abf68af61486591c82b5fe9124 Mon Sep 17 00:00:00 2001
From: dev747368 <48332326+dev747368@users.noreply.github.com>
Date: Wed, 17 Sep 2025 16:37:40 +0000
Subject: [PATCH] GP-5924 DWARF debuginfod support
---
.../Public_Release/certification.manifest | 1 +
.../Public_Release/data/DWARF.debuginfod_urls | 8 +
Ghidra/Features/Base/certification.manifest | 1 +
.../DWARFLineInfoSourceMapScript.java | 15 +
...etExternalDebugFilesLocationPrescript.java | 15 +-
.../DWARFExternalDebugFilesPlugin.html | 98 ++-
.../images/ExternalDebugFilesConfigDialog.png | Bin 0 -> 20768 bytes
.../LibreTranslatePlugin.htm | 18 +-
.../plugin/core/analysis/DWARFAnalyzer.java | 4 +
.../bin/format/dwarf/DWARFImportOptions.java | 5 +-
.../util/bin/format/dwarf/DWARFProgram.java | 21 +-
.../external/BuildIdDebugFileProvider.java | 101 +++
.../dwarf/external/BuildIdSearchLocation.java | 97 ---
.../DWARFExternalDebugFilesPlugin.java | 91 +--
...chLocation.java => DebugFileProvider.java} | 30 +-
.../dwarf/external/DebugFileStorage.java | 32 +
.../dwarf/external/DebugInfoProvider.java | 41 ++
.../DebugInfoProviderCreatorContext.java | 26 +
.../external/DebugInfoProviderRegistry.java | 110 ++++
.../external/DebugInfoProviderStatus.java | 20 +
.../dwarf/external/DebugStreamProvider.java | 37 ++
.../external/DisabledDebugInfoProvider.java | 76 +++
.../external/ExternalDebugFilesService.java | 169 ++++-
.../dwarf/external/ExternalDebugInfo.java | 85 ++-
.../external/HttpDebugInfoDProvider.java | 246 +++++++
.../external/LocalDirDebugInfoDProvider.java | 369 +++++++++++
...on.java => LocalDirDebugLinkProvider.java} | 78 ++-
.../bin/format/dwarf/external/ObjectType.java | 24 +
.../external/SameDirDebugInfoProvider.java | 121 ++++
.../dwarf/external/SameDirSearchLocation.java | 99 ---
.../SearchLocationCreatorContext.java | 52 --
.../external/SearchLocationRegistry.java | 112 ----
.../external/gui/EnumIconColumnRenderer.java | 69 ++
.../gui/ExternalDebugFilesConfigDialog.java | 601 ++++++++++++++++++
.../ExternalDebugInfoProviderTableModel.java | 255 ++++++++
.../ExternalDebugInfoProviderTableRow.java | 73 +++
.../dwarf/external/gui/FilePromptDialog.java | 207 ++++++
.../external/gui/TableColumnInitializer.java | 62 ++
.../external/gui/WellKnownDebugProvider.java | 71 +++
.../ExternalDebugFileSectionProvider.java | 23 +-
.../java/ghidra/test/MockHttpServerUtils.java | 203 ++++++
...TranslateStringTranslationServiceTest.java | 99 +--
.../BuildIdDebugFileProviderTest.java | 57 ++
.../external/HttpDebugInfoDProviderTest.java | 201 ++++++
.../LocalDirDebugInfoDProviderTest.java | 115 ++++
.../LocalDirDebugLinkProviderTest.java | 55 ++
.../symbolserver/HttpSymbolServerTest.java | 79 ++-
.../ghidra/util/layout/ThreeColumnLayout.java | 135 ++++
.../application/ApplicationUtilities.java | 2 +-
.../java/utility/application/XdgUtils.java | 7 +-
...RFExternalDebugFilesPluginScreenShots.java | 53 ++
51 files changed, 3861 insertions(+), 708 deletions(-)
create mode 100644 Ghidra/Configurations/Public_Release/data/DWARF.debuginfod_urls
create mode 100644 Ghidra/Features/Base/src/main/help/help/topics/DWARFExternalDebugFilesPlugin/images/ExternalDebugFilesConfigDialog.png
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProvider.java
delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdSearchLocation.java
rename Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/{SearchLocation.java => DebugFileProvider.java} (54%)
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileStorage.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProvider.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderCreatorContext.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderRegistry.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderStatus.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugStreamProvider.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DisabledDebugInfoProvider.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProvider.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProvider.java
rename Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/{LocalDirectorySearchLocation.java => LocalDirDebugLinkProvider.java} (53%)
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ObjectType.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirDebugInfoProvider.java
delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirSearchLocation.java
delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationCreatorContext.java
delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationRegistry.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/EnumIconColumnRenderer.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugFilesConfigDialog.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableModel.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableRow.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/FilePromptDialog.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/TableColumnInitializer.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/WellKnownDebugProvider.java
create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/test/MockHttpServerUtils.java
create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProviderTest.java
create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProviderTest.java
create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProviderTest.java
create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProviderTest.java
create mode 100644 Ghidra/Framework/Gui/src/main/java/ghidra/util/layout/ThreeColumnLayout.java
create mode 100644 Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DWARFExternalDebugFilesPluginScreenShots.java
diff --git a/Ghidra/Configurations/Public_Release/certification.manifest b/Ghidra/Configurations/Public_Release/certification.manifest
index 2dca2b3c20..5deffac084 100644
--- a/Ghidra/Configurations/Public_Release/certification.manifest
+++ b/Ghidra/Configurations/Public_Release/certification.manifest
@@ -1,6 +1,7 @@
##VERSION: 2.0
Module.manifest||GHIDRA||||END|
README.md||GHIDRA||||END|
+data/DWARF.debuginfod_urls||GHIDRA||||END|
data/PDB_SYMBOL_SERVER_URLS.pdburl||GHIDRA||||END|
src/global/docs/ChangeHistory.md||GHIDRA||||END|
src/global/docs/WhatsNew.md||GHIDRA||||END|
diff --git a/Ghidra/Configurations/Public_Release/data/DWARF.debuginfod_urls b/Ghidra/Configurations/Public_Release/data/DWARF.debuginfod_urls
new file mode 100644
index 0000000000..0beade30b1
--- /dev/null
+++ b/Ghidra/Configurations/Public_Release/data/DWARF.debuginfod_urls
@@ -0,0 +1,8 @@
+Internet|https://debuginfod.elfutils.org/|WARNING: Check your organization's security policy before downloading files from the internet.
+Internet|https://debuginfod.fedoraproject.org/|WARNING: Check your organization's security policy before downloading files from the internet.
+Internet|https://debuginfod.ubuntu.com/|WARNING: Check your organization's security policy before downloading files from the internet.
+Internet|https://debuginfod.debian.net/|WARNING: Check your organization's security policy before downloading files from the internet.
+Internet|https://debuginfod.opensuse.org/|WARNING: Check your organization's security policy before downloading files from the internet.
+Internet|https://debuginfod.archlinux.org/|WARNING: Check your organization's security policy before downloading files from the internet.
+Internet|https://debuginfod.centos.org/|WARNING: Check your organization's security policy before downloading files from the internet.
+
diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest
index 74fa75a598..9c4f4d006c 100644
--- a/Ghidra/Features/Base/certification.manifest
+++ b/Ghidra/Features/Base/certification.manifest
@@ -316,6 +316,7 @@ src/main/help/help/topics/ComputeChecksumsPlugin/images/Dialog_Blank.png||GHIDRA
src/main/help/help/topics/ConsolePlugin/console.html||GHIDRA||||END|
src/main/help/help/topics/ConsolePlugin/images/Console.png||GHIDRA||||END|
src/main/help/help/topics/DWARFExternalDebugFilesPlugin/DWARFExternalDebugFilesPlugin.html||GHIDRA||||END|
+src/main/help/help/topics/DWARFExternalDebugFilesPlugin/images/ExternalDebugFilesConfigDialog.png||GHIDRA||||END|
src/main/help/help/topics/DataPlugin/Data.htm||GHIDRA||||END|
src/main/help/help/topics/DataPlugin/images/CreateStructureDialog.png||GHIDRA||||END|
src/main/help/help/topics/DataPlugin/images/CreateStructureDialogWithTableSelection.png||GHIDRA||||END|
diff --git a/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoSourceMapScript.java b/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoSourceMapScript.java
index 1bd7e86625..588d505d5b 100644
--- a/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoSourceMapScript.java
+++ b/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoSourceMapScript.java
@@ -19,12 +19,14 @@
// Note that you can run this script on a program that has already been analyzed by the
// DWARF analyzer.
//@category DWARF
+import java.io.File;
import java.io.IOException;
import java.util.*;
import ghidra.app.script.GhidraScript;
import ghidra.app.util.bin.BinaryReader;
import ghidra.app.util.bin.format.dwarf.*;
+import ghidra.app.util.bin.format.dwarf.external.*;
import ghidra.app.util.bin.format.dwarf.line.DWARFLine;
import ghidra.app.util.bin.format.dwarf.line.DWARFLine.SourceFileAddr;
import ghidra.app.util.bin.format.dwarf.line.DWARFLine.SourceFileInfo;
@@ -76,6 +78,11 @@ public class DWARFLineInfoSourceMapScript extends GhidraScript {
popup("Unable to get reader for debug line info");
return;
}
+ ExternalDebugInfo extDebugInfo = ExternalDebugInfo.fromProgram(dprog.getGhidraProgram());
+ boolean hasBuildId = extDebugInfo != null && extDebugInfo.hasBuildId();
+ ExternalDebugFilesService edfs =
+ ExternalDebugFilesService.forProgram(dprog.getGhidraProgram());
+
int entryCount = 0;
List These files contain DWARF debug information that has been stripped from the original binary and
- placed into a separate file (typically to save space). These external files can be found using
- information embedded in the original binary's ".gnu_debuglink" section (a filename and crc32) and/or
- ".note.gnu.build-id" section (a hash value). Use the ExtractELFDebugFilesScript to pull external debug files from pre-packaged install
- files, typically provided by Linux / BSD distributions, for later consumption by Ghidra. These files contain DWARF debug information that has been stripped from the original binary and
+ placed into a separate file (typically to save space). These external files can be found using
+ information embedded in the original binary's ".gnu_debuglink" section (a filename and
+ crc32) and/or ".note.gnu.build-id" section (a hash value). Allows the user to pick a directory where Ghidra will search for DWARF external debug files. Ghidra will search for external debug files under the selected directory
- as ".build-id/NN/hexhash.debug" if build-id information is available, falling back to trying
- the debuglink filename in any subdirectory, and lastly in the original binary's import location. Use the The DWARF analyzer will use the configured external debug file locations to search for
+ debug files when it encounters a binary that has external debug information and is missing its
+ .debug_info sections. See Edit This plugin adds a string translation service that will appear in the Translate
menu of a string data instance. The Translate menu will appear in the right-click
@@ -19,21 +19,21 @@
LibreTranslate (currently hosted at libretranslate.com) is an independant project that
provides an open source translation package that can be self-hosted. This plugin queries a LibreTranslate server via HTTP to translate each specified string into
a target language. The results of that translation will be determined by the LibreTranslate
- server.DWARF External Debug Files
+
+ DWARF External Debug Files
- Menu Actions
-
-
-
+ DWARF External Debug Config
+
-
- ExtractELFDebugFilesScript to pull external debug files from
+ pre-packaged install files, typically provided by Linux / BSD distributions, for later
+ consumption by Ghidra.Configuration
+
DWARF External Debug Config
- Provided by: DWARF External Debug Files Plugin
+
- Button actions:
+
+
+
(Add) Adds a location. See Debug location types
(Delete) Deletes the highlighted row
(Up/Down) Moves the highlighted row up or down
(Refresh) Updates the status of all rows
(Save) Saves the current information
-
-
-
+ Debug location types:
+
+
+
+ aabbcc...zz.debug, where aa..zz is the build-id hash in hex.
+ Debug files are named aa/bbccdd...zz.debug under the base directory
+ This storage scheme for build-id debug files is distinct from debuginfod's scheme.
+ Example: /usr/lib/debug/.build-id
+ NOTE: This directory is searched recursively for a matching file./home/user/.cache/debuginfod_client.
+ Provided by: DWARF External Debug Files Plugin
+
+
+
diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DWARFExternalDebugFilesPlugin/images/ExternalDebugFilesConfigDialog.png b/Ghidra/Features/Base/src/main/help/help/topics/DWARFExternalDebugFilesPlugin/images/ExternalDebugFilesConfigDialog.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc2c0dd0d09066f1f67db16c232b5508df72bee8
GIT binary patch
literal 20768
zcmbrmWk8f|yEQxpNQfXvNF&nS4Khe~H%K=q9RsKgA|N5%jdVyi(%s!kcXz%g-p}68
zv%h!0zrJ6N&UJakdBi%_TIcvH$cdw&;G;kw5Hv}Nw@MJmLk$Sz0X@<~@GqhIT1^m$
z!kXk;VHH=sokV1q%*53lQ|>}*5!4XXFdD@s^D=XhqyQr$J4K6OY}%z@yfRaTUR6cX
z{8B_a^DTpoPZ7@CsC>4Th(AS1AjMCUQUw3&yye-E!gM&3+5I1<@~<7Fxb`WpF~d)x_!6LLibreTranslate Plugin
+ LibreTranslate Plugin
A LibreTranslate server can be installed locally by following the instructions provided on LibreTranslate's website, and then this plugin can connect to it via a URL such as http://localhost:5000/ (when configured with suggested defaults).
- +It is also possible to use someone else's LibreTranslate server, and typically they will issue an API key that will authorize the user to connect.
-When a string has been translated, the translated value will be shown in place of - the original value, bracketed with »chevrons«
- +When a string has been translated, the translated value will be shown in place of + the original value, bracketed with »chevrons«
+See
Edit
@@ -41,7 +41,7 @@
Strings | LibreTranslate
-+
- URL - required. Example: http://localhost:5000/ (if self hosted and following suggested values)
- API Key - a unique key that authorizes you to connect to the LibreTranslate diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/DWARFAnalyzer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/DWARFAnalyzer.java index 4445fe59da..2aea5dc07f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/DWARFAnalyzer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/DWARFAnalyzer.java @@ -121,6 +121,10 @@ public class DWARFAnalyzer extends AbstractAnalyzer { catch (CancelledException ce) { throw ce; } + catch (DWARFException e) { + log.appendMsg("Error during DWARFAnalyzer import: " + e.getMessage()); + Msg.error(this, "Error during DWARFAnalyzer import: " + e.getMessage()); + } catch (IOException e) { log.appendMsg("Error during DWARFAnalyzer import: " + e); Msg.error(this, "Error during DWARFAnalyzer import: ", e); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImportOptions.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImportOptions.java index fdf524ac27..f2a8b99be6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImportOptions.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImportOptions.java @@ -88,7 +88,8 @@ public class DWARFImportOptions { "Copy External Debug File Symbols"; private static final String OPTION_COPY_EXTERNAL_DEBUG_FILE_SYMBOLS_DESC = "Copies symbols (which will typically be mangled) from a found external debug file into " + - "the main program"; + "the main program. See Edit | DWARF External Debug Config to control how those " + + "external debug files are found."; private static final String OPTION_CHARSET_NAME = "Debug Strings Charset"; private static final String OPTION_CHARSET_NAME_DESC = """ @@ -144,7 +145,7 @@ public class DWARFImportOptions { /** * Used to control which macro info entries are used to create enums. */ - public static enum MacroEnumSetting { + public enum MacroEnumSetting { NONE, IGNORE_COMMAND_LINE, ALL; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFProgram.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFProgram.java index dc980409b4..29ca7d2146 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFProgram.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFProgram.java @@ -83,16 +83,13 @@ public class DWARFProgram implements Closeable { public static boolean isDWARF(Program program) { String format = Objects.requireNonNullElse(program.getExecutableFormat(), ""); - switch (format) { - case ElfLoader.ELF_NAME: - case PeLoader.PE_NAME: - return hasExpectedDWARFSections(program) || - ExternalDebugInfo.fromProgram(program) != null; - case MachoLoader.MACH_O_NAME: - return hasExpectedDWARFSections(program) || - DSymSectionProvider.getDSYMForProgram(program) != null; - } - return false; + return switch (format) { + case ElfLoader.ELF_NAME, PeLoader.PE_NAME -> hasExpectedDWARFSections(program) || + ExternalDebugInfo.fromProgram(program) != null; + case MachoLoader.MACH_O_NAME -> hasExpectedDWARFSections(program) || + DSymSectionProvider.getDSYMForProgram(program) != null; + default -> false; + }; } private static boolean hasExpectedDWARFSections(Program program) { @@ -193,8 +190,8 @@ public class DWARFProgram implements Closeable { protected WeakValueHashMap
diesByOffset = new WeakValueHashMap<>(); private WeakValueHashMap aggsByOffset = new WeakValueHashMap<>(); - // Map of DIE offsets of {@link DIEAggregate}s that are being pointed to by - // other {@link DIEAggregate}s with a DW_AT_type property. + // Map of DIE offsets of DIEAggregates that are being pointed to by + // other DIEAggregates with a DW_AT_type property. // In other words, a map of inbound links to a DIEA. private ListValuedMap typeReferers = new ArrayListValuedHashMap<>(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProvider.java new file mode 100644 index 0000000000..fb7928cdf2 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProvider.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 ghidra.app.util.bin.format.dwarf.external; + +import java.io.File; +import java.io.IOException; + +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A {@link DebugFileProvider} that expects the external debug files to be named using the hexadecimal + * value of the hash of the file, and to be arranged in a bucketed directory hierarchy using the + * first 2 hexdigits of the hash. + * + * For example, the debug file with hash {@code 6addc39dc19c1b45f9ba70baf7fd81ea6508ea7f} would + * be stored as "6a/ddc39dc19c1b45f9ba70baf7fd81ea6508ea7f.debug" (under some root directory). + */ +public class BuildIdDebugFileProvider implements DebugFileProvider { + private static final String BUILDID_NAME_PREFIX = "build-id://"; + + /** + * Returns true if the specified name string specifies a BuildIdDebugFileProvider. + * + * @param name string to test + * @return boolean true if name specifies a BuildIdDebugFileProvider + */ + public static boolean matches(String name) { + return name.startsWith(BUILDID_NAME_PREFIX); + } + + /** + * Creates a new {@link BuildIdDebugFileProvider} instance using the specified name string. + * + * @param name string, earlier returned from {@link #getName()} + * @param context {@link DebugInfoProviderCreatorContext} to allow accessing information outside + * of the name string that might be needed to create a new instance + * @return new {@link BuildIdDebugFileProvider} instance + */ + public static BuildIdDebugFileProvider create(String name, + DebugInfoProviderCreatorContext context) { + name = name.substring(BUILDID_NAME_PREFIX.length()); + + return new BuildIdDebugFileProvider(new File(name)); + } + + private final File rootDir; + + /** + * Creates a new {@link BuildIdDebugFileProvider} at the specified directory. + * + * @param rootDir path to the root directory of the build-id directory (typically ends with + * "./build-id") + */ + public BuildIdDebugFileProvider(File rootDir) { + this.rootDir = rootDir; + } + + @Override + public String getName() { + return BUILDID_NAME_PREFIX + rootDir.getPath(); + } + + @Override + public String getDescriptiveName() { + return rootDir.getPath() + " (.build-id dir)"; + } + + @Override + public DebugInfoProviderStatus getStatus(TaskMonitor monitor) { + return rootDir.isDirectory() + ? DebugInfoProviderStatus.VALID + : DebugInfoProviderStatus.INVALID; + } + + @Override + public File getFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) + throws IOException, CancelledException { + String buildId = debugInfo.getBuildId(); + if (buildId == null || buildId.length() < 4 /* 2 bytes = 4 hex digits */ ) { + return null; + } + File bucketDir = new File(rootDir, buildId.substring(0, 2)); + File file = new File(bucketDir, buildId.substring(2) + ".debug"); + return file.isFile() ? file : null; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdSearchLocation.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdSearchLocation.java deleted file mode 100644 index c3e4adc0a6..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdSearchLocation.java +++ /dev/null @@ -1,97 +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.dwarf.external; - -import java.io.File; -import java.io.IOException; - -import ghidra.formats.gfilesystem.FSRL; -import ghidra.formats.gfilesystem.FileSystemService; -import ghidra.util.NumericUtilities; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; - -/** - * A {@link SearchLocation} that expects the external debug files to be named using the hexadecimal - * value of the hash of the file, and to be arranged in a bucketed directory hierarchy using the - * first 2 hexdigits of the hash. - *
- * For example, the debug file with hash {@code 6addc39dc19c1b45f9ba70baf7fd81ea6508ea7f} would - * be stored as "6a/ddc39dc19c1b45f9ba70baf7fd81ea6508ea7f.debug" (under some root directory). - */ -public class BuildIdSearchLocation implements SearchLocation { - - /** - * Returns true if the specified location string specifies a BuildIdSearchLocation. - * - * @param locString string to test - * @return boolean true if locString specifies a BuildId location - */ - public static boolean isBuildIdSearchLocation(String locString) { - return locString.startsWith(BUILD_ID_PREFIX); - } - - /** - * Creates a new {@link BuildIdSearchLocation} instance using the specified location string. - * - * @param locString string, earlier returned from {@link #getName()} - * @param context {@link SearchLocationCreatorContext} to allow accessing information outside - * of the location string that might be needed to create a new instance - * @return new {@link BuildIdSearchLocation} instance - */ - public static BuildIdSearchLocation create(String locString, - SearchLocationCreatorContext context) { - locString = locString.substring(BUILD_ID_PREFIX.length()); - - return new BuildIdSearchLocation(new File(locString)); - } - - private static final String BUILD_ID_PREFIX = "build-id://"; - private final File rootDir; - - /** - * Creates a new {@link BuildIdSearchLocation} at the specified location. - * - * @param rootDir path to the root directory of the build-id directory (typically ends with - * "./build-id") - */ - public BuildIdSearchLocation(File rootDir) { - this.rootDir = rootDir; - } - - @Override - public String getName() { - return BUILD_ID_PREFIX + rootDir.getPath(); - } - - @Override - public String getDescriptiveName() { - return rootDir.getPath() + " (build-id)"; - } - - @Override - public FSRL findDebugFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) - throws IOException, CancelledException { - String hash = NumericUtilities.convertBytesToString(debugInfo.getHash()); - if (hash == null || hash.length() < 4 /* 2 bytes = 4 hex digits */ ) { - return null; - } - File bucketDir = new File(rootDir, hash.substring(0, 2)); - File file = new File(bucketDir, hash.substring(2) + ".debug"); - return file.isFile() ? FileSystemService.getInstance().getLocalFSRL(file) : null; - } - -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DWARFExternalDebugFilesPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DWARFExternalDebugFilesPlugin.java index d2f3289a62..ba63a1d0b4 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DWARFExternalDebugFilesPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DWARFExternalDebugFilesPlugin.java @@ -4,9 +4,9 @@ * 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. @@ -15,20 +15,14 @@ */ package ghidra.app.util.bin.format.dwarf.external; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - import docking.action.builder.ActionBuilder; import docking.tool.ToolConstants; -import docking.widgets.filechooser.GhidraFileChooser; -import docking.widgets.filechooser.GhidraFileChooserMode; import ghidra.app.CorePluginPackage; import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.util.bin.format.dwarf.external.gui.ExternalDebugFilesConfigDialog; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.framework.preferences.Preferences; +import ghidra.util.HelpLocation; //@formatter:off @PluginInfo( @@ -40,10 +34,8 @@ import ghidra.framework.preferences.Preferences; ) //@formatter:on public class DWARFExternalDebugFilesPlugin extends Plugin { + public static final String HELP_TOPIC = "DWARFExternalDebugFilesPlugin"; - private static final String EXT_DEBUG_FILES_OPTION = "ExternalDebugFiles"; - private static final String SEARCH_LOCATIONS_LIST_OPTION = - EXT_DEBUG_FILES_OPTION + ".searchLocations"; public DWARFExternalDebugFilesPlugin(PluginTool tool) { super(tool); @@ -55,78 +47,9 @@ public class DWARFExternalDebugFilesPlugin extends Plugin { new ActionBuilder("DWARF External Debug Config", this.getName()) .menuPath(ToolConstants.MENU_EDIT, "DWARF External Debug Config") .menuGroup(ToolConstants.TOOL_OPTIONS_MENU_GROUP) - .onAction(ac -> showConfigDialog()) + .onAction(ac -> ExternalDebugFilesConfigDialog.show()) + .helpLocation(new HelpLocation(HELP_TOPIC, "Configuration")) .buildAndInstall(tool); } - private void showConfigDialog() { - // Let the user pick a single directory, and configure a ".build-id/" search location - // and a recursive dir search location at that directory, as well as a - // same-dir search location to search the program's import directory. - GhidraFileChooser chooser = new GhidraFileChooser(tool.getActiveWindow()); - chooser.setMultiSelectionEnabled(false); - chooser.setApproveButtonText("Select"); - chooser.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); - chooser.setTitle("Select External Debug Files Directory"); - File selectedDir = chooser.getSelectedFile(); - chooser.dispose(); - if (selectedDir == null) { - return; - } - - BuildIdSearchLocation bisl = new BuildIdSearchLocation(new File(selectedDir, ".build-id")); - LocalDirectorySearchLocation ldsl = new LocalDirectorySearchLocation(selectedDir); - SameDirSearchLocation sdsl = new SameDirSearchLocation(new File("does not matter")); - - ExternalDebugFilesService edfs = new ExternalDebugFilesService(List.of(bisl, ldsl, sdsl)); - saveExternalDebugFilesService(edfs); - } - - /** - * Get a new instance of {@link ExternalDebugFilesService} using the previously saved - * information (via {@link #saveExternalDebugFilesService(ExternalDebugFilesService)}). - * - * @param context created via {@link SearchLocationRegistry#newContext(ghidra.program.model.listing.Program)} - * @return new {@link ExternalDebugFilesService} instance - */ - public static ExternalDebugFilesService getExternalDebugFilesService( - SearchLocationCreatorContext context) { - SearchLocationRegistry searchLocRegistry = SearchLocationRegistry.getInstance(); - String searchPathStr = Preferences.getProperty(SEARCH_LOCATIONS_LIST_OPTION, "", true); - String[] pathParts = searchPathStr.split(";"); - List
searchLocs = new ArrayList<>(); - for (String part : pathParts) { - if (!part.isBlank()) { - SearchLocation searchLoc = searchLocRegistry.createSearchLocation(part, context); - if (searchLoc != null) { - searchLocs.add(searchLoc); - } - } - } - if (searchLocs.isEmpty()) { - // default to search the same directory as the program - searchLocs.add(SameDirSearchLocation.create(null, context)); - } - return new ExternalDebugFilesService(searchLocs); - } - - /** - * Serializes an {@link ExternalDebugFilesService} to a string and writes to the Ghidra - * global preferences. - * - * @param service the {@link ExternalDebugFilesService} to commit to preferences - */ - public static void saveExternalDebugFilesService(ExternalDebugFilesService service) { - if (service != null) { - String path = service.getSearchLocations() - .stream() - .map(SearchLocation::getName) - .collect(Collectors.joining(";")); - Preferences.setProperty(SEARCH_LOCATIONS_LIST_OPTION, path); - } - else { - Preferences.setProperty(SEARCH_LOCATIONS_LIST_OPTION, null); - } - } - } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocation.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileProvider.java similarity index 54% rename from Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocation.java rename to Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileProvider.java index f4b6cf3da6..f060ac759a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocation.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileProvider.java @@ -4,9 +4,9 @@ * 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. @@ -15,40 +15,28 @@ */ package ghidra.app.util.bin.format.dwarf.external; +import java.io.File; import java.io.IOException; -import ghidra.formats.gfilesystem.FSRL; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; /** - * Represents a collection of dwarf external debug files that can be searched. + * A {@link DebugInfoProvider} that can directly provide {@link File files}. */ -public interface SearchLocation { +public interface DebugFileProvider extends DebugInfoProvider { /** - * Searchs for a debug file that fulfills the criteria specified in the {@link ExternalDebugInfo}. + * Searches for a debug file that fulfills the criteria specified in the + * {@link ExternalDebugInfo}. * * @param debugInfo search criteria * @param monitor {@link TaskMonitor} - * @return {@link FSRL} of the matching file, or {@code null} if not found + * @return File of the matching file, or {@code null} if not found * @throws IOException if error * @throws CancelledException if cancelled */ - FSRL findDebugFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) + File getFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) throws IOException, CancelledException; - /** - * Returns the name of this instance, which should be a serialized copy of this instance. - * - * @return String serialized data of this instance, typically in "something://serialized_data" - * form - */ - String getName(); - /** - * Returns a human formatted string describing this location, used in UI prompts or lists. - * - * @return formatted string - */ - String getDescriptiveName(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileStorage.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileStorage.java new file mode 100644 index 0000000000..3c7ac3c540 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileStorage.java @@ -0,0 +1,32 @@ +/* ### + * 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.dwarf.external; + +import java.io.File; +import java.io.IOException; + +import ghidra.app.util.bin.format.dwarf.external.DebugStreamProvider.StreamInfo; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A {@link DebugInfoProvider} that also allows storing files + */ +public interface DebugFileStorage extends DebugFileProvider { + File putStream(ExternalDebugInfo id, StreamInfo stream, TaskMonitor monitor) + throws IOException, CancelledException; + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProvider.java new file mode 100644 index 0000000000..0ea309fc4a --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProvider.java @@ -0,0 +1,41 @@ +/* ### + * 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.dwarf.external; + +import ghidra.util.task.TaskMonitor; + +/** + * Base interface for objects that can provide DWARF debug files. See {@link DebugFileProvider} or + * {@link DebugStreamProvider}. + */ +public interface DebugInfoProvider { + /** + * {@return the name of this instance, which should be a serialized copy of this instance, + * typically like "something://serialized_data"} + */ + String getName(); + + /** + * {@return a human formatted string describing this provider, used in UI prompts or lists} + */ + String getDescriptiveName(); + + /** + * {@return DebugInfoProviderStatus representing this provider's current status} + * @param monitor {@link TaskMonitor} + */ + DebugInfoProviderStatus getStatus(TaskMonitor monitor); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderCreatorContext.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderCreatorContext.java new file mode 100644 index 0000000000..c66938109e --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderCreatorContext.java @@ -0,0 +1,26 @@ +/* ### + * 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.dwarf.external; + +import ghidra.program.model.listing.Program; + +/** + * Information that might be needed to create a new {@link DebugInfoProvider} instance. + * + * @param registry {@link DebugInfoProviderRegistry} + * @param program {@link Program} + */ +public record DebugInfoProviderCreatorContext(DebugInfoProviderRegistry registry, Program program) {} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderRegistry.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderRegistry.java new file mode 100644 index 0000000000..2ab1033c8c --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderRegistry.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 ghidra.app.util.bin.format.dwarf.external; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import ghidra.program.model.listing.Program; + +/** + * List of {@link DebugInfoProvider} types that can be saved / restored from a configuration string. + */ +public class DebugInfoProviderRegistry { + public static DebugInfoProviderRegistry getInstance() { + return instance; + } + + private static final DebugInfoProviderRegistry instance = new DebugInfoProviderRegistry(); + + private List creators = new ArrayList<>(); + + /** + * Creates a new registry + */ + public DebugInfoProviderRegistry() { + register(DisabledDebugInfoProvider::matches, DisabledDebugInfoProvider::create); + register(LocalDirDebugLinkProvider::matches, LocalDirDebugLinkProvider::create); + register(SameDirDebugInfoProvider::matches, SameDirDebugInfoProvider::create); + register(BuildIdDebugFileProvider::matches, BuildIdDebugFileProvider::create); + register(LocalDirDebugInfoDProvider::matches, LocalDirDebugInfoDProvider::create); + register(HttpDebugInfoDProvider::matches, HttpDebugInfoDProvider::create); + } + + /** + * Adds a {@link DebugFileProvider} to this registry. + * + * @param testFunc a {@link Predicate} that tests a name string, returning true if the + * string specifies the provider in question + * @param createFunc a {@link DebugInfoProviderCreator} that will create a new + * {@link DebugFileProvider} instance given a name string and a + * {@link DebugInfoProviderCreatorContext context} + */ + public void register(Predicate testFunc, DebugInfoProviderCreator createFunc) { + creators.add(new DebugInfoProviderCreationInfo(testFunc, createFunc)); + } + + /** + * Creates a new {@link DebugInfoProviderCreatorContext context}. + * + * @param program {@link Program} + * @return new {@link DebugInfoProviderCreatorContext} + */ + public DebugInfoProviderCreatorContext newContext(Program program) { + return new DebugInfoProviderCreatorContext(this, program); + } + + /** + * Creates a {@link DebugFileProvider} using the specified name string. + * + * @param name string previously returned by {@link DebugFileProvider#getName()} + * @param context a {@link DebugInfoProviderCreatorContext context} + * @return new {@link DebugFileProvider} instance, or null if there are no registered matching + * providers + */ + public DebugInfoProvider create(String name, DebugInfoProviderCreatorContext context) { + for (DebugInfoProviderCreationInfo slci : creators) { + if (slci.testFunc.test(name)) { + return slci.createFunc.create(name, context); + } + } + return null; + } + + private interface DebugInfoProviderCreator { + /** + * Creates a new {@link DebugFileProvider} instance using the provided name string. + * + * @param name string, previously returned by {@link DebugFileProvider#getName()} + * @param context {@link DebugInfoProviderCreatorContext context} + * @return new {@link DebugFileProvider} + */ + DebugInfoProvider create(String name, DebugInfoProviderCreatorContext context); + } + + private static class DebugInfoProviderCreationInfo { + Predicate testFunc; + DebugInfoProviderCreator createFunc; + + DebugInfoProviderCreationInfo(Predicate testFunc, + DebugInfoProviderCreator createFunc) { + this.testFunc = testFunc; + this.createFunc = createFunc; + } + + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderStatus.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderStatus.java new file mode 100644 index 0000000000..2a2ab1a065 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderStatus.java @@ -0,0 +1,20 @@ +/* ### + * 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.dwarf.external; + +public enum DebugInfoProviderStatus { + UNKNOWN, VALID, INVALID +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugStreamProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugStreamProvider.java new file mode 100644 index 0000000000..bf7f1b4e1c --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugStreamProvider.java @@ -0,0 +1,37 @@ +/* ### + * 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.dwarf.external; + +import java.io.*; + +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A {@link DebugInfoProvider} that returns debug objects as a stream. + */ +public interface DebugStreamProvider extends DebugInfoProvider { + record StreamInfo(InputStream is, long contentLength) implements Closeable { + + @Override + public void close() throws IOException { + is.close(); + } + } + + StreamInfo getStream(ExternalDebugInfo id, TaskMonitor monitor) + throws IOException, CancelledException; +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DisabledDebugInfoProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DisabledDebugInfoProvider.java new file mode 100644 index 0000000000..8d22a4d0da --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DisabledDebugInfoProvider.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 ghidra.app.util.bin.format.dwarf.external; + +import ghidra.util.task.TaskMonitor; + +/** + * Wrapper around a DebugInfoProvider that prevents it from being queried, but retains it in the + * configuration list. + */ +public class DisabledDebugInfoProvider implements DebugInfoProvider { + private static String DISABLED_PREFIX = "disabled://"; + + /** + * Predicate that tests if the name string is an instance of a disabled name. + * + * @param name string + * @return boolean true if the string should be handled by the DisabledSymbolServer class + */ + public static boolean matches(String name) { + return name.startsWith(DISABLED_PREFIX); + } + + /** + * Factory method to create new instances from a name string. + * + * @param name string, earlier returned from {@link #getName()} + * @param context {@link DebugInfoProviderCreatorContext} to allow accessing information outside + * of the name string that might be needed to create a new instance + * @return new instance, or null if invalid name string + */ + public static DebugInfoProvider create(String name, DebugInfoProviderCreatorContext context) { + String delegateName = name.substring(DISABLED_PREFIX.length()); + DebugInfoProvider delegate = context.registry().create(delegateName, context); + return (delegate != null) ? new DisabledDebugInfoProvider(delegate) : null; + } + + private DebugInfoProvider delegate; + + public DisabledDebugInfoProvider(DebugInfoProvider delegate) { + this.delegate = delegate; + } + + @Override + public String getName() { + return DISABLED_PREFIX + delegate.getName(); + } + + @Override + public String getDescriptiveName() { + return "Disabled - " + delegate.getDescriptiveName(); + } + + public DebugInfoProvider getDelegate() { + return delegate; + } + + @Override + public DebugInfoProviderStatus getStatus(TaskMonitor monitor) { + return DebugInfoProviderStatus.UNKNOWN; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugFilesService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugFilesService.java index 656ca428c5..882a71874f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugFilesService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugFilesService.java @@ -4,9 +4,9 @@ * 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. @@ -15,55 +15,88 @@ */ package ghidra.app.util.bin.format.dwarf.external; +import java.io.File; import java.io.IOException; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; -import ghidra.formats.gfilesystem.FSRL; +import ghidra.app.util.bin.format.dwarf.external.DebugStreamProvider.StreamInfo; +import ghidra.framework.preferences.Preferences; +import ghidra.program.model.listing.Program; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; /** - * A collection of {@link SearchLocation search locations} that can be queried to find a - * DWARF external debug file, which is a second ELF binary that contains the debug information - * that was stripped from the original ELF binary. + * A collection of {@link DebugFileProvider providers} that can be queried to find a + * DWARF external debug file. Typically this will be an ELF binary that contains the debug + * information that was stripped from the original ELF binary, but can also include ability + * to fetch original binaries as well as source files. */ public class ExternalDebugFilesService { - private List searchLocations; + private static final String EXT_DEBUG_FILES_OPTION = "ExternalDebugFiles"; + private static final String STORAGE_OPTION = EXT_DEBUG_FILES_OPTION + ".storage"; + private static final String PROVIDERS_OPTION = EXT_DEBUG_FILES_OPTION + ".providers"; + + private final DebugFileStorage storage; + private List providers = new ArrayList<>(); /** - * Creates a new instance using the list of search locations. + * Creates a new instance using a {@link DebugFileStorage}, and a list of providers. * - * @param searchLocations list of {@link SearchLocation search locations} + * @param storage {@link DebugFileStorage} + * @param providers list of {@link DebugFileProvider providers} to search */ - public ExternalDebugFilesService(List searchLocations) { - this.searchLocations = searchLocations; + public ExternalDebugFilesService(DebugFileStorage storage, List providers) { + Objects.requireNonNull(storage); + this.storage = storage; + this.providers.add(storage); + this.providers.addAll(providers); + } + + public DebugFileStorage getStorage() { + return storage; } /** - * Returns the configured search locations. + * Returns the configured providers. * - * @return list of search locations + * @return list of providers */ - public List getSearchLocations() { - return searchLocations; + public List getProviders() { + return List.copyOf(providers.subList(1, providers.size())); + } + + /** + * Adds a {@link DebugInfoProvider} as a location to search. + * + * @param provider {@link DebugInfoProvider} to add + */ + public void addProvider(DebugInfoProvider provider) { + providers.add(provider); } /** * Searches for the specified external debug file. - * - * Returns the FSRL of a matching file, or null if not found. * * @param debugInfo information about the external debug file * @param monitor {@link TaskMonitor} - * @return {@link FSRL} of found file, or {@code null} if not found + * @return found file, or {@code null} if not found * @throws IOException if error */ - public FSRL findDebugFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) - throws IOException { + public File find(ExternalDebugInfo debugInfo, TaskMonitor monitor) throws IOException { try { - for (SearchLocation searchLoc : searchLocations) { + for (DebugInfoProvider provider : providers) { monitor.checkCancelled(); - FSRL result = searchLoc.findDebugFile(debugInfo, monitor); + File result = null; + if (provider instanceof DebugFileProvider fileProvider) { + result = fileProvider.getFile(debugInfo, monitor); + } + else if (provider instanceof DebugStreamProvider streamProvider) { + StreamInfo stream = streamProvider.getStream(debugInfo, monitor); + if (stream != null) { + result = storage.putStream(debugInfo, stream, monitor); + } + } if (result != null) { return result; } @@ -75,4 +108,94 @@ public class ExternalDebugFilesService { return null; } + //---------------------------------------- + /** + * {@return an ExternalDebugFilesService instance with no additional search locations} + */ + public static ExternalDebugFilesService getMinimal() { + return new ExternalDebugFilesService(LocalDirDebugInfoDProvider.getGhidraCacheInstance(), + List.of()); + } + + /** + * {@return an ExternalDebugFilesService instance with default search locations} + */ + public static ExternalDebugFilesService getDefault() { + return new ExternalDebugFilesService(LocalDirDebugInfoDProvider.getGhidraCacheInstance(), + List.of(new SameDirDebugInfoProvider(null), + LocalDirDebugInfoDProvider.getUserHomeCacheInstance())); + } + + /** + * Get a new instance of {@link ExternalDebugFilesService} using the previously saved + * information (via {@link #saveToPrefs(ExternalDebugFilesService)}), for the specified program. + * + * @param program {@link Program} + * @return new {@link ExternalDebugFilesService} instance + */ + public static ExternalDebugFilesService forProgram(Program program) { + return fromPrefs(DebugInfoProviderRegistry.getInstance().newContext(program)); + } + + /** + * Get a new instance of {@link ExternalDebugFilesService} using the previously saved + * information (via {@link #saveToPrefs(ExternalDebugFilesService)}). + * + * @param context created via {@link DebugInfoProviderRegistry#newContext(ghidra.program.model.listing.Program)} + * @return new {@link ExternalDebugFilesService} instance + */ + public static ExternalDebugFilesService fromPrefs(DebugInfoProviderCreatorContext context) { + DebugInfoProviderRegistry registry = DebugInfoProviderRegistry.getInstance(); + + String storageStr = Preferences.getProperty(STORAGE_OPTION, "", true); + DebugFileStorage storage = null; + if ( storageStr != null ) { + DebugInfoProvider storageProvider = registry.create(storageStr, context); + storage = (storageProvider instanceof DebugFileStorage dfs) ? dfs : null; + } + if ( storage == null ) { + storage = LocalDirDebugInfoDProvider.getGhidraCacheInstance(); + } + + String providersStr = Preferences.getProperty(PROVIDERS_OPTION, "", true); + String[] providerNames = providersStr.split(";"); + List
providers = new ArrayList<>(); + for (String providerName : providerNames) { + if (!providerName.isBlank()) { + DebugInfoProvider provider = registry.create(providerName, context); + if (provider != null) { + providers.add(provider); + } + } + } + if (providers.isEmpty()) { + // default to search the same directory as the program + providers.add(SameDirDebugInfoProvider.create(null, context)); + providers.add(LocalDirDebugInfoDProvider.getUserHomeCacheInstance()); + } + + return new ExternalDebugFilesService(storage, providers); + } + + /** + * Serializes an {@link ExternalDebugFilesService} to a string and writes to the Ghidra + * global preferences. + * + * @param service the {@link ExternalDebugFilesService} to commit to preferences + */ + public static void saveToPrefs(ExternalDebugFilesService service) { + if (service != null) { + String serializedProviders = service.getProviders() + .stream() + .map(DebugInfoProvider::getName) + .collect(Collectors.joining(";")); + Preferences.setProperty(STORAGE_OPTION, service.getStorage().getName()); + Preferences.setProperty(PROVIDERS_OPTION, serializedProviders); + } + else { + Preferences.setProperty(STORAGE_OPTION, null); + Preferences.setProperty(PROVIDERS_OPTION, null); + } + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugInfo.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugInfo.java index 5afa7e1ca2..47a2b92050 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugInfo.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugInfo.java @@ -4,9 +4,9 @@ * 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. @@ -45,26 +45,54 @@ public class ExternalDebugInfo { String filename = debugLink != null ? debugLink.getFilename() : null; int crc = debugLink != null ? debugLink.getCrc() : 0; - byte[] hash = buildId != null ? buildId.getDescription() : null; - return new ExternalDebugInfo(filename, crc, hash); + String hash = buildId != null + ? NumericUtilities.convertBytesToString(buildId.getDescription()) + : null; + + return new ExternalDebugInfo(filename, crc, hash, ObjectType.DEBUGINFO, null); } - private String filename; - private int crc; - private byte[] hash; + /** + * {@return a new ExternalDebugInfo instance created using the specified Build-Id value} + * @param buildId hex string + */ + public static ExternalDebugInfo forBuildId(String buildId) { + return new ExternalDebugInfo(null, 0, buildId, ObjectType.DEBUGINFO, null); + } + + /** + * {@return a new ExternalDebugInfo instance created using the specified debuglink values} + * @param debugLinkFilename filename from debuglink section + * @param crc crc32 from debuglink section + */ + public static ExternalDebugInfo forDebugLink(String debugLinkFilename, int crc) { + return new ExternalDebugInfo(debugLinkFilename, crc, null, ObjectType.DEBUGINFO, null); + } + + private final String filename; + private final int crc; + private final String buildId; + private final ObjectType objectType; + private final String extra; /** * Constructor to create an {@link ExternalDebugInfo} instance. * * @param filename filename of external debug file, or null * @param crc crc32 of external debug file, or 0 if no filename - * @param hash build-id hash digest found in ".note.gnu.build-id" section, or null if + * @param buildId build-id hash digest found in ".note.gnu.build-id" section, or null if * not present + * @param objectType {@link ObjectType} specifies what kind of debug file is specified by the + * other info + * @param extra additional information used by {@link ObjectType#SOURCE} */ - public ExternalDebugInfo(String filename, int crc, byte[] hash) { + public ExternalDebugInfo(String filename, int crc, String buildId, ObjectType objectType, + String extra) { this.filename = filename; this.crc = crc; - this.hash = hash; + this.buildId = buildId; + this.objectType = objectType; + this.extra = extra; } /** @@ -72,7 +100,7 @@ public class ExternalDebugInfo { * * @return boolean true if filename is available, false if not */ - public boolean hasFilename() { + public boolean hasDebugLink() { return filename != null && !filename.isBlank(); } @@ -95,19 +123,38 @@ public class ExternalDebugInfo { } /** - * Return the build-id hash digest. + * Return the build-id. * - * @return byte array containing the build-id hash (usually 20 bytes) + * @return build-id hash string */ - public byte[] getHash() { - return hash; + public String getBuildId() { + return buildId; + } + + /** + * {@return true if buildId is available, false if not} + */ + public boolean hasBuildId() { + return buildId != null && !buildId.isBlank(); + } + + public ObjectType getObjectType() { + return objectType; + } + + public String getExtra() { + return extra; + } + + public ExternalDebugInfo withType(ObjectType newObjectType, String newExtra) { + return new ExternalDebugInfo(extra, crc, buildId, newObjectType, newExtra); } @Override public String toString() { - return String.format("ExternalDebugInfo [filename=%s, crc=%s, hash=%s]", - filename, - Integer.toHexString(crc), - NumericUtilities.convertBytesToString(hash)); + return String.format( + "ExternalDebugInfo [filename=%s, crc=%s, hash=%s, objectType=%s, extra=%s]", filename, + crc, buildId, objectType, extra); } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProvider.java new file mode 100644 index 0000000000..f36d4ed5df --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProvider.java @@ -0,0 +1,246 @@ +/* ### + * 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.dwarf.external; + +import java.io.IOException; +import java.io.InputStream; +import java.net.*; +import java.net.http.*; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.channels.UnresolvedAddressException; +import java.util.Objects; +import java.util.concurrent.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import ghidra.net.HttpClients; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.CancelledListener; +import ghidra.util.task.TaskMonitor; + +/** + * Queries debuginfod REST servers for debug objects. + */ +public class HttpDebugInfoDProvider implements DebugStreamProvider { + private static final String GHIDRA_USER_AGENT = "Ghidra_HttpDebugInfoDProvider_client"; + private static final int HTTP_STATUS_OK = HttpURLConnection.HTTP_OK; + private static final int HTTP_STATUS_INTERNAL_ERROR = HttpURLConnection.HTTP_INTERNAL_ERROR; + private static final int HTTP_STATUS_NOT_FOUND = HttpURLConnection.HTTP_NOT_FOUND; + private static final int DEFAULT_HTTP_REQUEST_TIMEOUT_MS = 10 * 1000; // 10 seconds + private static final int DEFAULT_MAX_RETRY_COUNT = 5; + private static final Pattern HTTPPROVIDER_REGEX = Pattern.compile("(http(s)?://.*)"); + + public static boolean matches(String name) { + return HTTPPROVIDER_REGEX.matcher(name).matches(); + } + + public static HttpDebugInfoDProvider create(String name, + DebugInfoProviderCreatorContext context) { + Matcher m = HTTPPROVIDER_REGEX.matcher(name); + if (!m.matches()) { + return null; + } + String uriStr = m.group(1); + URI serverURI = URI.create(uriStr); + return new HttpDebugInfoDProvider(serverURI); + } + + private final URI serverURI; + private int retriedCount; + private int notFoundCount; + private int maxRetryCount = DEFAULT_MAX_RETRY_COUNT; + private int httpRequestTimeoutMs = DEFAULT_HTTP_REQUEST_TIMEOUT_MS; + + /** + * Creates a new instance of a HttpSymbolServer. + * + * @param serverURI URI / URL of the symbol server + */ + public HttpDebugInfoDProvider(URI serverURI) { + String path = serverURI.getPath(); + this.serverURI = + path.endsWith("/") ? serverURI : serverURI.resolve(serverURI.getPath() + "/"); + } + + @Override + public String getName() { + return serverURI.toString(); + } + + @Override + public String getDescriptiveName() { + return serverURI.toString(); + } + + @Override + public DebugInfoProviderStatus getStatus(TaskMonitor monitor) { + return DebugInfoProviderStatus.UNKNOWN; + } + + private HttpRequest.Builder request(ExternalDebugInfo id) throws IOException { + try { + String extra = ""; + if (id.getObjectType() == ObjectType.SOURCE) { + extra = "/" + Objects.requireNonNullElse(id.getExtra(), ""); + } + String requestPath = "buildid/%s/%s%s".formatted(id.getBuildId(), + id.getObjectType().getPathString(), extra); + return HttpRequest.newBuilder(serverURI.resolve(requestPath)) + .setHeader("User-Agent", GHIDRA_USER_AGENT); + } + catch (IllegalArgumentException e) { + throw new IOException(e); + } + } + + @Override + public StreamInfo getStream(ExternalDebugInfo id, TaskMonitor monitor) + throws IOException, CancelledException { + if (!id.hasBuildId()) { + return null; + } + + monitor.setIndeterminate(true); + monitor.setMessage("Connecting to " + serverURI); + + HttpRequest request = request(id).GET().build(); + + retryLoop: for (int retryNum = 0; retryNum < maxRetryCount; retryNum++) { + if (retryNum > 0) { + Msg.debug(this, logPrefix() + ": retry count: " + retryNum); + retriedCount++; + } + InputStream bodyIS = null; + try { + HttpResponse response = tryGet(request, monitor); + int statusCode = response.statusCode(); + bodyIS = response.body(); + HttpHeaders headers = response.headers(); + Msg.debug(this, logPrefix() + ": Http response: " + response.statusCode()); + switch (statusCode) { + case HTTP_STATUS_OK: { + // TODO: typical response headers from debuginfod that we may want to make + // use of in the future: + // x-debuginfod-size: 245872 + // x-debuginfod-archive: /path/to/somepackagefile.packagetype_ext + // x-debuginfod-file: 1e1abd8faf1cb290df755a558377c5d7def3b1.debug + long contentLen = headers.firstValueAsLong("Content-Length").orElse(-1); + long size = headers.firstValueAsLong("x-debuginfod-size").orElse(-1); + String archivePath = headers.firstValue("x-debuginfod-archive").orElse(""); + String debugFile = headers.firstValue("x-debuginfod-file").orElse(""); + Msg.debug(this, + logPrefix() + + ": Debug object info size: %d, archive path: %s, debug file: %s" + .formatted(size, archivePath, debugFile)); + Msg.info(this, + "Found DWARF external debug file: %s".formatted(request.uri())); + + InputStream successIS = bodyIS; + bodyIS = null; + return new StreamInfo(successIS, contentLen); + } + case HTTP_STATUS_INTERNAL_ERROR: + // retry connection + continue retryLoop; + case HTTP_STATUS_NOT_FOUND: + notFoundCount++; + return null; + default: + Msg.debug(this, logPrefix() + ": unexpected result status: " + statusCode); + return null; + } + } + catch (ConnectException e) { + if (e.getCause() instanceof UnresolvedAddressException) { + Msg.debug(this, logPrefix() + ": bad server name? " + serverURI); + return null; // fail + } + // fall thru, retry + } + catch (TimeoutException e) { + // fall thru, retry + } + finally { + uncheckedClose(bodyIS); + } + } + Msg.debug(this, logPrefix() + ": failed to query for: " + id); + return null; + } + + private HttpResponse tryGet(HttpRequest request, TaskMonitor monitor) + throws IOException, CancelledException, TimeoutException { + Msg.debug(this, logPrefix() + ": " + request.toString()); + CompletableFuture > futureResponse = + HttpClients.getHttpClient().sendAsync(request, BodyHandlers.ofInputStream()); + CancelledListener l = () -> futureResponse.cancel(true); + monitor.addCancelledListener(l); + + try { + HttpResponse response = + futureResponse.get(httpRequestTimeoutMs, TimeUnit.MILLISECONDS); + + return response; + } + catch (InterruptedException e) { + throw new CancelledException("Download canceled"); + } + catch (ExecutionException e) { + // if possible, unwrap the exception that happened inside the future + Throwable cause = e.getCause(); + if (cause instanceof IOException ioe) { + throw ioe; + } + Msg.error(this, "Error during HTTP get", cause); + throw new IOException("Error during HTTP get", cause); + } + finally { + monitor.removeCancelledListener(l); + } + } + + private String logPrefix() { + return getClass().getSimpleName() + "[" + serverURI + "]"; + } + + private static void uncheckedClose(InputStream is) { + try { + if (is != null) { + is.close(); + } + } + catch (IOException e) { + // ignore it + } + } + + public int getNotFoundCount() { + return notFoundCount; + } + + public int getRetriedCount() { + return retriedCount; + } + + public void setMaxRetryCount(int maxRetryCount) { + this.maxRetryCount = maxRetryCount; + } + + public void setHttpRequestTimeoutMs(int httpRequestTimeoutMs) { + this.httpRequestTimeoutMs = httpRequestTimeoutMs; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProvider.java new file mode 100644 index 0000000000..2dbec3aa06 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProvider.java @@ -0,0 +1,369 @@ +/* ### + * 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.dwarf.external; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.Duration; +import java.util.Date; +import java.util.Objects; + +import ghidra.app.util.bin.format.dwarf.external.DebugStreamProvider.StreamInfo; +import ghidra.formats.gfilesystem.FSUtilities; +import ghidra.framework.Application; +import ghidra.util.Msg; +import ghidra.util.NumericUtilities; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; +import utility.application.ApplicationUtilities; +import utility.application.XdgUtils; + +/** + * Provides debug files found in a debuginfod-client compatible directory structure. + * + * Provides ability to store files. + *
+ * Does not try to follow debuginfod's file age-off logic or config values. + */ +public class LocalDirDebugInfoDProvider implements DebugFileStorage { + // static cache maint timing values. + private static final long MAINT_INTERVAL_MS = Duration.ofDays(1).toMillis(); + public static final long MAX_FILE_AGE_MS = Duration.ofDays(7).toMillis(); + + private static final String DEBUGINFOD_NAME_PREFIX = "debuginfod-dir://"; + public static final String GHIDRACACHE_NAME = "$DEFAULT"; + public static final String USERHOMECACHE_NAME = "$DEBUGINFOD_CLIENT_CACHE"; + + /** + * Returns true if the specified name string specifies a LocalDirDebugInfoDProvider. + * + * @param name string to test + * @return boolean true if name specifies a LocalDirDebugInfoDProvider + */ + public static boolean matches(String name) { + return name.startsWith(DEBUGINFOD_NAME_PREFIX); + } + + /** + * Creates a new {@link BuildIdDebugFileProvider} instance using the specified name string. + * + * @param name string, earlier returned from {@link #getName()} + * @param context {@link DebugInfoProviderCreatorContext} to allow accessing information outside + * of the name string that might be needed to create a new instance + * @return new {@link BuildIdDebugFileProvider} instance + */ + public static LocalDirDebugInfoDProvider create(String name, + DebugInfoProviderCreatorContext context) { + name = name.substring(DEBUGINFOD_NAME_PREFIX.length()); + + if (USERHOMECACHE_NAME.equals(name)) { + return getUserHomeCacheInstance(); + } + if (GHIDRACACHE_NAME.equals(name)) { + return getGhidraCacheInstance(); + } + + return new LocalDirDebugInfoDProvider(new File(name)); + } + + /** + * {@return a new LocalDirDebugInfoDProvider that stores files in the same directory that the + * debuginfod-find CLI tool would (/home/user/.cache/debuginfod_client/)} + */ + public static LocalDirDebugInfoDProvider getUserHomeCacheInstance() { + File cacheDir = new File(getCacheHomeLocation(), "debuginfod_client"); + return new LocalDirDebugInfoDProvider(cacheDir, DEBUGINFOD_NAME_PREFIX + USERHOMECACHE_NAME, + "DebugInfoD Cache Dir <%s>".formatted(cacheDir)); + } + + /** + * {@return a new LocalDirDebugInfoDProvider that stores files in a Ghidra specific cache + * directory} + */ + public static LocalDirDebugInfoDProvider getGhidraCacheInstance() { + File cacheDir = new File(Application.getUserCacheDirectory(), "debuginfo-cache"); + FileUtilities.mkdirs(cacheDir); + LocalDirDebugInfoDProvider result = new LocalDirDebugInfoDProvider(cacheDir, + DEBUGINFOD_NAME_PREFIX + GHIDRACACHE_NAME, "Ghidra Cache Dir <%s>".formatted(cacheDir)); + result.setNeedsMaintCheck(true); + return result; + } + + private final File rootDir; + private final String name; + private final String descriptiveName; + private boolean needsInitMaintCheck; + + public LocalDirDebugInfoDProvider(File rootDir) { + this(rootDir, DEBUGINFOD_NAME_PREFIX + rootDir.getPath(), + rootDir.getPath() + " (debuginfod dir)"); + } + + public LocalDirDebugInfoDProvider(File rootDir, String name, String descriptiveName) { + this.rootDir = rootDir; + this.name = name; + this.descriptiveName = descriptiveName; + } + + public File getRootDir() { + return rootDir; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescriptiveName() { + return descriptiveName; + } + + @Override + public DebugInfoProviderStatus getStatus(TaskMonitor monitor) { + return isValid() ? DebugInfoProviderStatus.VALID : DebugInfoProviderStatus.INVALID; + } + + public File getDirectory() { + return rootDir; + } + + private boolean isValid() { + return rootDir.isDirectory(); + } + + public void setNeedsMaintCheck(boolean needsInitMaintCheck) { + this.needsInitMaintCheck = needsInitMaintCheck; + } + + @Override + public File getFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) + throws IOException, CancelledException { + if (!isValid() || !debugInfo.hasBuildId()) { + return null; + } + performInitMaintIfNeeded(); + + File f = getCachePath(debugInfo); + return f.isFile() ? f : null; + } + + private File getBuildidDir(String buildId) { + return new File(rootDir, buildId); + } + + private File getCachePath(ExternalDebugInfo id) { + String suffix = ""; + if (id.getObjectType() == ObjectType.SOURCE) { + suffix = "-" + escapePath(Objects.requireNonNullElse(id.getExtra(), "")); + } + + return new File(getBuildidDir(id.getBuildId()), + id.getObjectType().getPathString() + suffix); + } + + @Override + public File putStream(ExternalDebugInfo id, StreamInfo stream, TaskMonitor monitor) + throws IOException, CancelledException { + assertValid(); + if (!id.hasBuildId()) { + throw new IOException("Can't store debug file without BuildId value: " + id); + } + performInitMaintIfNeeded(); + + File f = getCachePath(id); + File tmpF = new File(f.getParent(), ".tmp_" + f.getName()); + FileUtilities.checkedMkdirs(f.getParentFile()); + try (stream; FileOutputStream fos = new FileOutputStream(tmpF)) { + FSUtilities.streamCopy(stream.is(), fos, monitor); + } + try { + if (f.isFile() && !f.delete()) { + throw new IOException("Could not delete %s".formatted(f)); + } + if (!tmpF.renameTo(f)) { + throw new IOException("Could not rename temp file %s to %s".formatted(tmpF, f)); + } + } + finally { + tmpF.delete(); // just blindly try to delete tmp file in case an exception was thrown + } + return f; + } + + private void assertValid() throws IOException { + if (!rootDir.isDirectory()) { + throw new IOException("Invalid debuginfo directory: " + rootDir); + } + } + + @Override + public String toString() { + return String.format("LocalDebugInfoProvider [rootDir=%s, name=%s]", rootDir, name); + } + + public void purgeAll() { + cacheMaint(-1); + File lastMaintFile = new File(rootDir, ".lastmaint"); + lastMaintFile.delete(); + } + + public void performInitMaintIfNeeded() { + if (needsInitMaintCheck) { + try { + performCacheMaintIfNeeded(); + } + finally { + needsInitMaintCheck = false; + } + } + } + + public void performCacheMaintIfNeeded() { + if (!rootDir.isDirectory()) { + return; + } + if (rootDir.getParentFile() == null) { + // if someone gave us "/" as our path, don't try to delete files + Msg.error(this, "Refusing to clean up files in " + rootDir); + return; + } + + long now = System.currentTimeMillis(); + File lastMaintFile = new File(rootDir, ".lastmaint"); + long lastMaintTS = lastMaintFile.isFile() ? lastMaintFile.lastModified() : 0; + if (lastMaintTS + MAINT_INTERVAL_MS > now) { + return; + } + + cacheMaint(MAX_FILE_AGE_MS); + + try { + Files.writeString(lastMaintFile.toPath(), "Last maint run at " + (new Date())); + } + catch (IOException e) { + Msg.error(this, "Unable to write file cache maintenance file: " + lastMaintFile, e); + } + } + + /** + * Ages off debug files found in a compatible directory struct. + * + * @param maxFileAgeMs max age of any debug file to allow, or -1 for all files + */ + private void cacheMaint(long maxFileAgeMs) { + long cutoffMS = + maxFileAgeMs >= 0 ? System.currentTimeMillis() - maxFileAgeMs : Long.MAX_VALUE; + int deletedCount = 0; + long deletedBytes = 0; + + for (File f : Objects.requireNonNullElse(rootDir.listFiles(), new File[0])) { + if (f.isDirectory() && isBuildIdSubdirName(f.getName())) { + int subDirFileCount = 0; + int deletedSubDirFileCount = 0; + for (File subF : Objects.requireNonNullElse(f.listFiles(), new File[0])) { + subDirFileCount++; + if (subF.isFile()) { + long modified = subF.lastModified(); + if (modified != 0 && modified < cutoffMS) { + long size = subF.length(); + if (subF.delete()) { + deletedCount++; + deletedBytes += size; + deletedSubDirFileCount++; + } + } + } + } + if (subDirFileCount == deletedSubDirFileCount) { + // build-id hash directory should be empty, remove it + if (!f.delete()) { + Msg.warn(this, "Failed to delete empty debuginfod hash directory: " + f); + } + } + } + } + Msg.debug(this, + "Finished cache cleanup of debug files in %s, deleted %d files, %d total bytes" + .formatted(rootDir, deletedCount, deletedBytes)); + } + //--------------------------------------------------------------------------------------------- + + /** + * Converts a path string into a string that can be used as a filename. + *
+ * For example: "/usr/include/stdio.h" becomes "AABBCCDD-#usr#include#stdio.h", where + * AABCCDD is the hex value of the 32 bit hash of the original path string. + * (See {@link #djbX33AHash(String)}). + * + * @param s path string + * @return escaped string + */ + private static String escapePath(String s) { + // TODO: needs testing on how strings just barely longer than maxPath match with + // the debuginfod-client.c logic + int maxPath = 255 /* NAME_MAX*/ / 2; // from debuginfod-client.c:path_escape() + int hash = (int) djbX33AHash(s); + if (s.length() > maxPath) { + int start = s.length() - maxPath; // keep trailing part of filepath + s = s.substring(start); + } + s = s.replaceAll("[^a-zA-Z0-9._-]", "#"); // NOTE: the dash '-' needs to be last in the "[]" regex class + return "%08x-%s".formatted(hash, s); + } + + private static long djbX33AHash(String s) { + // see debuginfod-client.c to ensure compatibility + long hash = 5381; + for (byte b : s.getBytes(StandardCharsets.UTF_8)) { + hash = ((hash << 5) + hash) + Byte.toUnsignedInt(b); + } + return hash; + } + + private static boolean isBuildIdSubdirName(String s) { + // subdirs under the debuginfod cache root should be simple 20 byte(ish) hash values. + byte[] bytes = NumericUtilities.convertStringToBytes(s); + return bytes != null && bytes.length >= 20 /* typical buildId hash size */; + } + + private static File getCacheHomeLocation() { + File cacheHomeDir = getEnvVarAsFile(XdgUtils.XDG_CACHE_HOME); + if (cacheHomeDir == null) { + try { + cacheHomeDir = ApplicationUtilities.getJavaUserHomeDir(); + } + catch (IOException e) { + throw new RuntimeException("Missing home directory", e); + } + cacheHomeDir = new File(cacheHomeDir, XdgUtils.XDG_CACHE_HOME_DEFAULT_SUBDIRNAME); + } + return cacheHomeDir; + } + + private static File getEnvVarAsFile(String name) { + String path = System.getenv(name); + if (path != null && !path.isBlank()) { + File result = new File(path.trim()); + if (result.isAbsolute()) { + return result; + } + } + return null; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirectorySearchLocation.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProvider.java similarity index 53% rename from Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirectorySearchLocation.java rename to Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProvider.java index cbae723c95..d30bbdedc8 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirectorySearchLocation.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProvider.java @@ -4,9 +4,9 @@ * 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. @@ -18,69 +18,77 @@ package ghidra.app.util.bin.format.dwarf.external; import java.io.*; import java.util.zip.CRC32; -import ghidra.formats.gfilesystem.FSRL; -import ghidra.formats.gfilesystem.FileSystemService; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; /** - * A {@link SearchLocation} that recursively searches for dwarf external debug files - * under a configured directory. + * Searches for DWARF external debug files specified via a debug-link filename / crc in a directory. */ -public class LocalDirectorySearchLocation implements SearchLocation { +public class LocalDirDebugLinkProvider implements DebugFileProvider { - private static final String LOCAL_DIR_PREFIX = "dir://"; + private static final String DEBUGLINK_NAME_PREFIX = "debuglink://"; /** - * Returns true if the specified location string specifies a LocalDirectorySearchLocation. + * Returns true if the specified name string specifies a LocalDirDebugLinkProvider. * - * @param locString string to test - * @return boolean true if locString specifies a local dir search location + * @param name string to test + * @return boolean true if name specifies a LocalDirDebugLinkProvider name */ - public static boolean isLocalDirSearchLoc(String locString) { - return locString.startsWith(LOCAL_DIR_PREFIX); + public static boolean matches(String name) { + return name.startsWith(DEBUGLINK_NAME_PREFIX); } /** - * Creates a new {@link LocalDirectorySearchLocation} instance using the specified location string. + * Creates a new {@link LocalDirDebugLinkProvider} instance using the specified name string. * - * @param locString string, earlier returned from {@link #getName()} - * @param context {@link SearchLocationCreatorContext} to allow accessing information outside - * of the location string that might be needed to create a new instance - * @return new {@link LocalDirectorySearchLocation} instance + * @param name string, earlier returned from {@link #getName()} + * @param context {@link DebugInfoProviderCreatorContext} to allow accessing information outside + * of the name string that might be needed to create a new instance + * @return new {@link LocalDirDebugLinkProvider} instance */ - public static LocalDirectorySearchLocation create(String locString, - SearchLocationCreatorContext context) { - locString = locString.substring(LOCAL_DIR_PREFIX.length()); - return new LocalDirectorySearchLocation(new File(locString)); + public static LocalDirDebugLinkProvider create(String name, + DebugInfoProviderCreatorContext context) { + String dir = name.substring(DEBUGLINK_NAME_PREFIX.length()); + return new LocalDirDebugLinkProvider(new File(dir)); } private final File searchDir; /** - * Creates a new {@link LocalDirectorySearchLocation} at the specified location. + * Creates a new {@link LocalDirDebugLinkProvider} at the specified dir. * * @param searchDir path to the root directory of where to search */ - public LocalDirectorySearchLocation(File searchDir) { + public LocalDirDebugLinkProvider(File searchDir) { this.searchDir = searchDir; } @Override public String getName() { - return LOCAL_DIR_PREFIX + searchDir.getPath(); + return DEBUGLINK_NAME_PREFIX + searchDir.getPath(); } @Override public String getDescriptiveName() { - return searchDir.getPath(); + return searchDir.getPath() + " (debug-link dir)"; } @Override - public FSRL findDebugFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) + public DebugInfoProviderStatus getStatus(TaskMonitor monitor) { + return isValid() + ? DebugInfoProviderStatus.VALID + : DebugInfoProviderStatus.INVALID; + } + + private boolean isValid() { + return searchDir.isDirectory(); + } + + @Override + public File getFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) throws CancelledException, IOException { - if (!debugInfo.hasFilename()) { + if (!debugInfo.hasDebugLink() || !isValid()) { return null; } ensureSafeFilename(debugInfo.getFilename()); @@ -94,24 +102,26 @@ public class LocalDirectorySearchLocation implements SearchLocation { } } - FSRL findFile(File dir, ExternalDebugInfo debugInfo, TaskMonitor monitor) + File findFile(File dir, ExternalDebugInfo debugInfo, TaskMonitor monitor) throws IOException, CancelledException { - if (!debugInfo.hasFilename()) { + if (!debugInfo.hasDebugLink()) { return null; } File file = new File(dir, debugInfo.getFilename()); if (file.isFile()) { int fileCRC = calcCRC(file); if (fileCRC == debugInfo.getCrc()) { - return FileSystemService.getInstance().getLocalFSRL(file); + return file; // success } - Msg.info(this, "DWARF external debug file found with mismatching crc, ignored: " + - file + ", (" + Integer.toHexString(fileCRC) + ")"); + Msg.info(this, + "DWARF external debug file found with mismatching crc, ignored: %s (%08x)" + .formatted(file, fileCRC)); } File[] subDirs; if ((subDirs = dir.listFiles(f -> f.isDirectory())) != null) { + // TODO: prevent recursing into symlinks? for (File subDir : subDirs) { - FSRL result = findFile(subDir, debugInfo, monitor); + File result = findFile(subDir, debugInfo, monitor); if (result != null) { return result; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ObjectType.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ObjectType.java new file mode 100644 index 0000000000..64aaf34c03 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ObjectType.java @@ -0,0 +1,24 @@ +/* ### + * 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.dwarf.external; + +public enum ObjectType { + DEBUGINFO, EXECUTABLE, SOURCE; + + public String getPathString() { + return name().toLowerCase(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirDebugInfoProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirDebugInfoProvider.java new file mode 100644 index 0000000000..d596487ab8 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirDebugInfoProvider.java @@ -0,0 +1,121 @@ +/* ### + * 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.dwarf.external; + +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; + +/** + * A {@link DebugFileProvider} that only looks in the program's original import directory for + * matching debug files. + */ +public class SameDirDebugInfoProvider implements DebugFileProvider { + + public static final String DESC = "Program's Import Location"; + + /** + * Returns true if the specified name string specifies a SameDirDebugInfoProvider. + * + * @param name string to test + * @return boolean true if locString specifies a SameDirDebugInfoProvider + */ + public static boolean matches(String name) { + return name.equals("."); + } + + /** + * Creates a new {@link SameDirDebugInfoProvider} instance using the current program's + * import location. + * + * @param name unused + * @param context {@link DebugInfoProviderCreatorContext} + * @return new {@link SameDirDebugInfoProvider} instance + */ + public static SameDirDebugInfoProvider create(String name, + DebugInfoProviderCreatorContext context) { + File exeLocation = context.program() != null + ? new File(FilenameUtils.getFullPath(context.program().getExecutablePath())) + : null; + return new SameDirDebugInfoProvider(exeLocation); + } + + private final File progDir; + + /** + * Creates a new {@link SameDirDebugInfoProvider} at the specified directory. + * + * @param progDir path to the program's import directory + */ + public SameDirDebugInfoProvider(File progDir) { + this.progDir = progDir; + } + + @Override + public String getName() { + return "."; + } + + @Override + public String getDescriptiveName() { + return DESC + (progDir != null ? " (" + progDir.getPath() + ")" : ""); + } + + @Override + public DebugInfoProviderStatus getStatus(TaskMonitor monitor) { + return progDir != null + ? progDir.isDirectory() + ? DebugInfoProviderStatus.VALID + : DebugInfoProviderStatus.INVALID + : DebugInfoProviderStatus.UNKNOWN; + } + + @Override + public File getFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) + throws IOException, CancelledException { + if (debugInfo.hasDebugLink()) { + // This differs from the LocalDirDebugLinkProvider in that it does NOT recursively search + // for the file + File debugFile = new File(progDir, debugInfo.getFilename()); + if (debugFile.isFile()) { + int fileCRC = LocalDirDebugLinkProvider.calcCRC(debugFile); + if (fileCRC == debugInfo.getCrc()) { + return debugFile; // success + } + Msg.info(this, + "DWARF external debug file found with mismatching crc, ignored: %s, (%08x)" + .formatted(debugFile, fileCRC)); + } + } + + if (debugInfo.hasBuildId()) { + // this probe is a w.a.g for what people might do when co-locating a build-id debug + // file with the original binary + File debugFile = new File(progDir, debugInfo.getBuildId() + ".debug"); + if (debugFile.isFile()) { + return debugFile; + } + } + + return null; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirSearchLocation.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirSearchLocation.java deleted file mode 100644 index 3ab0172962..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirSearchLocation.java +++ /dev/null @@ -1,99 +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.dwarf.external; - -import java.io.File; -import java.io.IOException; - -import org.apache.commons.io.FilenameUtils; - -import ghidra.formats.gfilesystem.FSRL; -import ghidra.formats.gfilesystem.FileSystemService; -import ghidra.util.Msg; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; - -/** - * A {@link SearchLocation} that only looks in the program's original import directory. - */ -public class SameDirSearchLocation implements SearchLocation { - - /** - * Returns true if the specified location string specifies a SameDirSearchLocation. - * - * @param locString string to test - * @return boolean true if locString specifies a BuildId location - */ - public static boolean isSameDirSearchLocation(String locString) { - return locString.equals("."); - } - - /** - * Creates a new {@link SameDirSearchLocation} instance using the current program's - * import location. - * - * @param locString unused - * @param context {@link SearchLocationCreatorContext} - * @return new {@link SameDirSearchLocation} instance - */ - public static SameDirSearchLocation create(String locString, - SearchLocationCreatorContext context) { - File exeLocation = - new File(FilenameUtils.getFullPath(context.getProgram().getExecutablePath())); - return new SameDirSearchLocation(exeLocation); - } - - private final File progDir; - - /** - * Creates a new {@link SameDirSearchLocation} at the specified location. - * - * @param progDir path to the program's import directory - */ - public SameDirSearchLocation(File progDir) { - this.progDir = progDir; - } - - @Override - public String getName() { - return "."; - } - - @Override - public String getDescriptiveName() { - return progDir.getPath() + " (Program's Import Location)"; - } - - @Override - public FSRL findDebugFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) - throws IOException, CancelledException { - if (!debugInfo.hasFilename()) { - return null; - } - File file = new File(progDir, debugInfo.getFilename()); - if (!file.isFile()) { - return null; - } - int fileCRC = LocalDirectorySearchLocation.calcCRC(file); - if (fileCRC != debugInfo.getCrc()) { - Msg.info(this, "DWARF external debug file found with mismatching crc, ignored: " + - file + ", (" + Integer.toHexString(fileCRC) + ")"); - return null; - } - return FileSystemService.getInstance().getLocalFSRL(file); - } - -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationCreatorContext.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationCreatorContext.java deleted file mode 100644 index 3ad2c3588d..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationCreatorContext.java +++ /dev/null @@ -1,52 +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.dwarf.external; - -import ghidra.program.model.listing.Program; - -/** - * Information outside of a location string that might be needed to create a new {@link SearchLocation} - * instance. - */ -public class SearchLocationCreatorContext { - private final SearchLocationRegistry registry; - private final Program program; - - /** - * Create a new context object with references to the registry and the current program. - * - * @param registry {@link SearchLocationRegistry} - * @param program the current {@link Program} - */ - public SearchLocationCreatorContext(SearchLocationRegistry registry, Program program) { - this.registry = registry; - this.program = program; - } - - /** - * @return the {@link SearchLocationRegistry} that is creating the {@link SearchLocation} - */ - public SearchLocationRegistry getRegistry() { - return registry; - } - - /** - * @return the current {@link Program} - */ - public Program getProgram() { - return program; - } -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationRegistry.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationRegistry.java deleted file mode 100644 index 1bd4695c4d..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationRegistry.java +++ /dev/null @@ -1,112 +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.dwarf.external; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Predicate; - -import ghidra.program.model.listing.Program; - -/** - * List of {@link SearchLocation} types that can be saved / restored from a configuration string. - */ -public class SearchLocationRegistry { - public static SearchLocationRegistry getInstance() { - return instance; - } - - private static final SearchLocationRegistry instance = new SearchLocationRegistry(true); - - private List
searchLocCreators = new ArrayList<>(); - - /** - * Creates a new registry, optionally registering the default SearchLocations. - * - * @param registerDefault boolean flag, if true register the built-in {@link SearchLocation}s - */ - public SearchLocationRegistry(boolean registerDefault) { - if (registerDefault) { - register(LocalDirectorySearchLocation::isLocalDirSearchLoc, - LocalDirectorySearchLocation::create); - register(BuildIdSearchLocation::isBuildIdSearchLocation, BuildIdSearchLocation::create); - register(SameDirSearchLocation::isSameDirSearchLocation, SameDirSearchLocation::create); - } - } - - /** - * Adds a {@link SearchLocation} to this registry. - * - * @param testFunc a {@link Predicate} that tests a location string, returning true if the - * string specifies the SearchLocation in question - * @param createFunc a {@link SearchLocationCreator} that will create a new {@link SearchLocation} - * instance given a location string and a {@link SearchLocationCreatorContext context} - */ - public void register(Predicate testFunc, SearchLocationCreator createFunc) { - searchLocCreators.add(new SearchLocationCreationInfo(testFunc, createFunc)); - } - - /** - * Creates a new {@link SearchLocationCreatorContext context}. - * - * @param program {@link Program} - * @return new {@link SearchLocationCreatorContext} - */ - public SearchLocationCreatorContext newContext(Program program) { - return new SearchLocationCreatorContext(this, program); - } - - /** - * Creates a {@link SearchLocation} using the provided location string. - * - * @param locString location string (previously returned by {@link SearchLocation#getName()} - * @param context a {@link SearchLocationCreatorContext context} - * @return new {@link SearchLocation} instance, or null if there are no registered matching - * SearchLocations - */ - public SearchLocation createSearchLocation(String locString, - SearchLocationCreatorContext context) { - for (SearchLocationCreationInfo slci : searchLocCreators) { - if (slci.testFunc.test(locString)) { - return slci.createFunc.create(locString, context); - } - } - return null; - } - - public interface SearchLocationCreator { - /** - * Creates a new {@link SearchLocation} instance using the provided location string. - * - * @param locString location string, previously returned by {@link SearchLocation#getName()} - * @param context {@link SearchLocationCreatorContext context} - * @return new {@link SearchLocation} - */ - SearchLocation create(String locString, SearchLocationCreatorContext context); - } - - private static class SearchLocationCreationInfo { - Predicate testFunc; - SearchLocationCreator createFunc; - - SearchLocationCreationInfo(Predicate testFunc, - SearchLocationCreator createFunc) { - this.testFunc = testFunc; - this.createFunc = createFunc; - } - - } -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/EnumIconColumnRenderer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/EnumIconColumnRenderer.java new file mode 100644 index 0000000000..8312786059 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/EnumIconColumnRenderer.java @@ -0,0 +1,69 @@ +/* ### + * 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.dwarf.external.gui; + +import java.awt.Component; + +import javax.swing.*; + +import docking.widgets.table.GTableCellRenderingData; +import ghidra.docking.settings.Settings; +import ghidra.util.table.column.AbstractGColumnRenderer; + +/** + * Table column renderer to render an enum value as a icon + * + * @param enum type + */ +public 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/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugFilesConfigDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugFilesConfigDialog.java new file mode 100644 index 0000000000..abba73dea4 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugFilesConfigDialog.java @@ -0,0 +1,601 @@ +/* ### + * 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.dwarf.external.gui; + +import java.awt.*; +import java.io.File; +import java.net.URI; +import java.util.*; +import java.util.List; + +import javax.swing.*; + +import docking.DialogComponentProvider; +import docking.DockingWindowManager; +import docking.widgets.OptionDialog; +import docking.widgets.button.BrowseButton; +import docking.widgets.button.GButton; +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.filechooser.GhidraFileChooserMode; +import docking.widgets.label.GLabel; +import docking.widgets.table.GTable; +import docking.widgets.textfield.HintTextField; +import generic.theme.GIcon; +import ghidra.app.util.bin.format.dwarf.external.*; +import ghidra.framework.preferences.Preferences; +import ghidra.util.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.layout.ThreeColumnLayout; +import ghidra.util.task.*; +import resources.Icons; + +public class ExternalDebugFilesConfigDialog extends DialogComponentProvider { + + public static boolean show() { + ExternalDebugFilesConfigDialog dlg = new ExternalDebugFilesConfigDialog(); + DockingWindowManager.showDialog(dlg); + return dlg.wasSuccess; + } + + private static final Dimension BUTTON_SIZE = new Dimension(32, 32); + + private List knownProviders = + WellKnownDebugProvider.loadAll(".debuginfod_urls"); + + private DebugInfoProviderCreatorContext creatorContext = + DebugInfoProviderRegistry.getInstance().newContext(null); + private DebugFileStorage storage; + + private ExternalDebugInfoProviderTableModel tableModel; + + private ExternalDebugFileProvidersPanel configPanel; + private boolean wasSuccess; + private boolean configChanged; + + public ExternalDebugFilesConfigDialog() { + super("DWARF External Debug Files Configuration", true, false, true, true); + + build(); + + tableModel.addTableModelListener(e -> updateButtonEnablement()); + setupInitialConfig(); + } + + private void setupInitialConfig() { + ExternalDebugFilesService tmpService = ExternalDebugFilesService.fromPrefs(creatorContext); + DebugFileStorage newStorage = tmpService.getStorage(); + setStorageLocation(newStorage); + tableModel.addItems(tmpService.getProviders()); + setConfigChanged(false); + } + + @Override + protected void cancelCallback() { + close(); + } + + @Override + protected void okCallback() { + if (isConfigChanged()) { + saveConfig(); + } + wasSuccess = true; + close(); + } + + @Override + protected void dialogShown() { + TableColumnInitializer.initializeTableColumns(configPanel.table, tableModel); + configPanel.refreshStatus(); + } + + private void build() { + tableModel = new ExternalDebugInfoProviderTableModel(); + + configPanel = new ExternalDebugFileProvidersPanel(); + + addButtons(); + addWorkPanel(configPanel); + + setHelpLocation( + new HelpLocation(DWARFExternalDebugFilesPlugin.HELP_TOPIC, "Configuration")); + + setRememberSize(false); + okButton.setEnabled(true); + } + + private void updateButtonEnablement() { + okButton.setEnabled(true); + configPanel.updatePanelButtonEnablement(); + } + + private void addButtons() { + addOKButton(); + addCancelButton(); + setDefaultButton(cancelButton); + } + + /** + * Screen shot usage only + */ + public void pushAddLocationButton() { + configPanel.addLocation(); + } + + /** + * Screen shot usage only + * + * @param list fake well known debug provider servers + */ + public void setWellknownProviders(List list) { + knownProviders = list; + } + + /** + * Screen shot only + */ + public void setService(ExternalDebugFilesService edfs) { + setProviders(edfs.getProviders()); + setStorageLocation(edfs.getStorage()); + } + + private void setStorageLocationPath(String path) { + configPanel.storageLocationTextField.setText(path); + } + + /** + * Returns a new {@link ExternalDebugFilesService} instance representing the currently + * displayed configuration, or null if the displayed configuration is not valid. + * + * @return new {@link ExternalDebugFilesService} or null + */ + ExternalDebugFilesService getService() { + return new ExternalDebugFilesService(storage, tableModel.getItems()); + } + + void setProviders(List providers) { + tableModel.setItems(providers); + } + + private void setStorageLocation(DebugFileStorage newStorage) { + storage = newStorage; + setStorageLocationPath(newStorage.getDescriptiveName()); + updateButtonEnablement(); + } + + 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); + } + + /** + * 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); + } + + /* package */ void saveConfig() { + ExternalDebugFilesService tmpService = getService(); + ExternalDebugFilesService.saveToPrefs(tmpService); + Preferences.store(); + setConfigChanged(false); + updateButtonEnablement(); + } + + private void registerHelp(Component comp, String anchorName) { + DockingWindowManager.getHelpService() + .registerHelp(comp, + new HelpLocation(DWARFExternalDebugFilesPlugin.HELP_TOPIC, anchorName)); + } + + //--------------------------------------------------------------------------------------------- + + class ExternalDebugFileProvidersPanel extends JPanel { + + private GTable table; + private JPanel additionalSearchLocationsPanel; + + private JButton refreshSearchLocationsStatusButton; + private JButton moveLocationUpButton; + private JButton moveLocationDownButton; + private JButton deleteLocationButton; + private JButton addLocationButton; + private JPanel storageLocationPanel; + private HintTextField storageLocationTextField; + private JButton saveSearchLocationsButton; + + ExternalDebugFileProvidersPanel() { + super(new BorderLayout()); + build(); + registerHelp(this, "Summary"); + } + + private void build() { + setBorder(BorderFactory.createTitledBorder("External Debug Files Config")); + + buildLocationPanel(); + JPanel tableButtonPanel = buildButtonPanel(); + JScrollPane tableScrollPane = buildTable(); + + additionalSearchLocationsPanel = new JPanel(); + additionalSearchLocationsPanel + .setLayout(new BoxLayout(additionalSearchLocationsPanel, BoxLayout.Y_AXIS)); + additionalSearchLocationsPanel.add(tableButtonPanel); + additionalSearchLocationsPanel.add(tableScrollPane); + + add(storageLocationPanel, BorderLayout.NORTH); + add(additionalSearchLocationsPanel, BorderLayout.CENTER); + } + + void refreshStatus() { + executeMonitoredRunnable("Refresh Provider Status", true, true, 0, monitor -> { + List rowsCopy = + new ArrayList<>(tableModel.getModelData()); + monitor.initialize(rowsCopy.size(), "Refreshing provider status"); + try { + for (ExternalDebugInfoProviderTableRow row : rowsCopy) { + if (monitor.isCancelled()) { + break; + } + monitor.setMessage("Checking " + row.getItem().getName()); + monitor.incrementProgress(); + + DebugInfoProvider provider = row.getItem(); + row.setStatus(provider.getStatus(monitor)); + } + } + finally { + Swing.runLater(() -> tableModel.fireTableDataChanged()); + } + }); + } + + private JScrollPane buildTable() { + table = new GTable(tableModel); + table.setVisibleRowCount(4); + table.setUserSortingEnabled(false); + table.getSelectionManager() + .addListSelectionListener(e -> updatePanelButtonEnablement()); + + table.setPreferredScrollableViewportSize(new Dimension(500, 100)); + + return new JScrollPane(table); + } + + private JPanel buildButtonPanel() { + refreshSearchLocationsStatusButton = + createImageButton(Icons.REFRESH_ICON, "Refresh Status", "ButtonActions"); + refreshSearchLocationsStatusButton.addActionListener(e -> refreshStatus()); + + moveLocationUpButton = createImageButton(Icons.UP_ICON, "Up", "ButtonActions"); + moveLocationUpButton.addActionListener(e -> moveLocation(-1)); + moveLocationUpButton.setToolTipText("Move location up"); + + moveLocationDownButton = createImageButton(Icons.DOWN_ICON, "Down", "ButtonActions"); + moveLocationDownButton.addActionListener(e -> moveLocation(1)); + moveLocationDownButton.setToolTipText("Move location down"); + + deleteLocationButton = createImageButton(Icons.DELETE_ICON, "Delete", "ButtonActions"); + deleteLocationButton.addActionListener(e -> deleteLocation()); + + addLocationButton = createImageButton(Icons.ADD_ICON, "Add", "ButtonActions"); + addLocationButton.addActionListener(e -> addLocation()); + + saveSearchLocationsButton = + createImageButton(Icons.SAVE_ICON, "Save Configuration", "ButtonActions"); + saveSearchLocationsButton.addActionListener(e -> saveConfig()); + + JPanel tableButtonPanel = new JPanel(); + tableButtonPanel.setLayout(new BoxLayout(tableButtonPanel, BoxLayout.X_AXIS)); + tableButtonPanel.add(new GLabel("Additional Locations:")); + tableButtonPanel.add(Box.createHorizontalGlue()); + tableButtonPanel.add(addLocationButton); + tableButtonPanel.add(deleteLocationButton); + tableButtonPanel.add(moveLocationUpButton); + tableButtonPanel.add(moveLocationDownButton); + tableButtonPanel.add(refreshSearchLocationsStatusButton); + tableButtonPanel.add(saveSearchLocationsButton); + + return tableButtonPanel; + } + + private JPanel buildLocationPanel() { + storageLocationTextField = new HintTextField(" Required "); + storageLocationTextField.setEditable(false); + storageLocationTextField.setFocusable(false); + storageLocationTextField.setToolTipText( + "User-specified directory where debug files are stored. Required."); + + JButton chooseStorageLocationButton = new BrowseButton(); + chooseStorageLocationButton.addActionListener(e -> chooseStorageLocation()); + registerHelp(chooseStorageLocationButton, "LocalStorage"); + + File ghidraCacheDir = + LocalDirDebugInfoDProvider.getGhidraCacheInstance().getDirectory(); + + JButton chooseGhidraCacheLocationButton = + createImageButton(new GIcon("icon.base.application.home"), + "Use private Ghidra cache location\n" + ghidraCacheDir, "LocalStorage"); + chooseGhidraCacheLocationButton.addActionListener(e -> chooseGhidraCacheLocation()); + + JPanel storageButtonPanel = new JPanel(); + storageButtonPanel.setLayout(new BoxLayout(storageButtonPanel, BoxLayout.X_AXIS)); + storageButtonPanel.add(chooseStorageLocationButton, BorderLayout.CENTER); + storageButtonPanel.add(chooseGhidraCacheLocationButton); + + GLabel storageLocLabel = new GLabel("Local Storage:", SwingConstants.RIGHT); + storageLocLabel.setToolTipText(storageLocationTextField.getToolTipText()); + + storageLocationPanel = new JPanel(new ThreeColumnLayout(5, 5, 5)); + storageLocationPanel.add(storageLocLabel); + storageLocationPanel.add(storageLocationTextField); + storageLocationPanel.add(storageButtonPanel); + return storageLocationPanel; + } + + private void updatePanelButtonEnablement() { + boolean singleRow = table.getSelectedRowCount() == 1; + boolean moreThanOneRow = table.getRowCount() > 1; + + refreshSearchLocationsStatusButton.setEnabled(!tableModel.isEmpty()); + moveLocationUpButton.setEnabled(singleRow && moreThanOneRow); + moveLocationDownButton.setEnabled(singleRow && moreThanOneRow); + addLocationButton.setEnabled(true); + deleteLocationButton.setEnabled(table.getSelectedRowCount() > 0); + saveSearchLocationsButton.setEnabled(isConfigChanged()); + } + + private void chooseStorageLocation() { + GhidraFileChooser chooser = getChooser("Choose Debug File Storage Directory"); + File f = chooser.getSelectedFile(); + chooser.dispose(); + + if (f != null) { + configChanged = true; + setStorageLocation(new LocalDirDebugInfoDProvider(f)); + updateButtonEnablement(); + } + } + + private void chooseGhidraCacheLocation() { + configChanged = true; + setStorageLocation(LocalDirDebugInfoDProvider.getGhidraCacheInstance()); + updateButtonEnablement(); + } + + private void importLocations() { + String envVar = (String) JOptionPane.showInputDialog(this, """ + Enter value:
+
+ Example: https://debuginfod.domain1.org https://debuginfod.domain2.org
+
""", "Enter DEBUGINFOD_URLS Value", JOptionPane.QUESTION_MESSAGE, null, + null, Objects.requireNonNullElse(System.getenv("DEBUGINFOD_URLS"), "")); + if (envVar == null) { + return; + } + + Listurls = getURLsFromEnvStr(envVar); + urls.forEach( + s -> tableModel.addItem(creatorContext.registry().create(s, creatorContext))); + updateButtonEnablement(); + } + + private void addLocation() { + JPopupMenu menu = createAddLocationPopupMenu(); + menu.show(addLocationButton, 0, 0); + } + + private JPopupMenu createAddLocationPopupMenu() { + JPopupMenu menu = new JPopupMenu(); + registerHelp(menu, "LocationTypes"); + + JMenuItem addProgLocMenuItem = new JMenuItem(SameDirDebugInfoProvider.DESC); + addProgLocMenuItem.addActionListener(e -> addSameDirLocation()); + addProgLocMenuItem + .setToolTipText("Directory that the program was originally imported from."); + menu.add(addProgLocMenuItem); + + JMenuItem addBuildIdDirMenuItem = new JMenuItem("Build-id Directory"); + addBuildIdDirMenuItem.addActionListener(e -> addBuildIdDirLocation()); + addBuildIdDirMenuItem.setToolTipText( + "Directory where debug files that are identified by a build-id hash are stored.\n" + + "Debug files are named AA/BBCCDD...ZZ.debug under the base directory\n" + + "This storage scheme for build-id debug files is distinct from debuginfod's scheme.\n\n" + + "e.g. /usr/lib/debug/.build-id"); + menu.add(addBuildIdDirMenuItem); + + JMenuItem addDebugLinkDirMenuItem = new JMenuItem("Debug Link Directory"); + addDebugLinkDirMenuItem.addActionListener(e -> addDebugLinkDirLocation()); + addDebugLinkDirMenuItem + .setToolTipText("Directory where debug files that are identified\n" + + "by a debug filename and crc hash\n" + + "(found in the binary's .gnu_debuglink section).\n\n" + + "NOTE: This directory is searched recursively for a matching file."); + menu.add(addDebugLinkDirMenuItem); + + JMenuItem addDebugInfoDDirMenuItem = new JMenuItem("Debuginfod Directory"); + addDebugInfoDDirMenuItem.addActionListener(e -> addDebugInfoDDirLocation()); + addDebugInfoDDirMenuItem.setToolTipText("Directory where debuginfod has stored files."); + menu.add(addDebugInfoDDirMenuItem); + + JMenuItem addURLMenuItem = new JMenuItem("Debuginfod URL"); + addURLMenuItem.addActionListener(e -> addUrlLocation()); + addURLMenuItem.setToolTipText("HTTP(s) URL that points to a debuginfod server."); + menu.add(addURLMenuItem); + + JMenuItem importEnvMenuItem = new JMenuItem("Import DEBUGINFOD_URLS Env Var"); + importEnvMenuItem.addActionListener(e -> importLocations()); + importEnvMenuItem.setToolTipText( + "Adds debuginfod URLs found in the system environment variable."); + menu.add(importEnvMenuItem); + + if (!knownProviders.isEmpty()) { + menu.add(new JSeparator()); + for (WellKnownDebugProvider provider : knownProviders) { + JMenuItem mi = new JMenuItem(provider.location()); + mi.addActionListener(e -> addKnownLocation(provider)); + mi.setToolTipText("Debuginfod URL [from " + provider.fileOrigin() + "]"); + menu.add(mi); + } + } + return menu; + } + + private void addSameDirLocation() { + SameDirDebugInfoProvider provider = new SameDirDebugInfoProvider(null); + tableModel.addItem(provider); + } + + private void addKnownLocation(WellKnownDebugProvider providerInfo) { + DebugInfoProvider newProvider = + creatorContext.registry().create(providerInfo.location(), creatorContext); + if (newProvider != null) { + tableModel.addItem(newProvider); + } + } + + private void addUrlLocation() { + String urlStr = OptionDialog.showInputSingleLineDialog(this, "Enter URL", + "Enter the URL of a Debuginfod Server: ", "https://"); + if (urlStr == null || urlStr.isBlank() || urlStr.equals("https://")) { + return; + } + try { + urlStr = urlStr.toLowerCase(); + if (urlStr.startsWith("http://") || urlStr.startsWith("https://")) { + HttpDebugInfoDProvider newProvider = + new HttpDebugInfoDProvider(URI.create(urlStr)); + tableModel.addItem(newProvider); + return; // success + } + } + catch (IllegalArgumentException e) { + // fall thru + } + Msg.showWarn(this, this, "Bad URL", "Invalid URL: " + urlStr); + } + + private void addBuildIdDirLocation() { + File dir = + FilePromptDialog.chooseDirectory("Enter Path", "Build-Id Root Directory: ", null); + if (dir == null) { + return; + } + if (!dir.exists() || !dir.isDirectory()) { + Msg.showError(this, this, "Bad path", "Invalid path: " + dir); + return; + } + BuildIdDebugFileProvider provider = new BuildIdDebugFileProvider(dir); + tableModel.addItem(provider); + } + + private void addDebugLinkDirLocation() { + File dir = + FilePromptDialog.chooseDirectory("Enter Path", "Debug-Link Root Directory: ", null); + if (dir == null) { + return; + } + if (!dir.exists() || !dir.isDirectory()) { + Msg.showError(this, this, "Bad path", "Invalid path: " + dir); + return; + } + LocalDirDebugLinkProvider provider = new LocalDirDebugLinkProvider(dir); + tableModel.addItem(provider); + } + + private void addDebugInfoDDirLocation() { + File dir = FilePromptDialog.chooseDirectory("Enter Path", + "Debuginfod Cache Directory: ", null); + if (dir == null) { + return; + } + if (!dir.exists() || !dir.isDirectory()) { + Msg.showError(this, this, "Bad path", "Invalid path: " + dir); + return; + } + LocalDirDebugInfoDProvider provider = new LocalDirDebugInfoDProvider(dir); + tableModel.addItem(provider); + } + + 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 GhidraFileChooser getChooser(String title) { + + GhidraFileChooser chooser = new GhidraFileChooser(this); + chooser.setMultiSelectionEnabled(false); + chooser.setApproveButtonText("Choose"); + chooser.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); + chooser.setTitle(title); + + return chooser; + } + + } + + //--------------------------------------------------------------------------------------------- + + private JButton createImageButton(Icon buttonIcon, String alternateText, String helpLoc) { + + JButton button = new GButton(buttonIcon); + button.setToolTipText(alternateText); + button.setPreferredSize(BUTTON_SIZE); + registerHelp(button, helpLoc); + + return button; + } + + private static List getURLsFromEnvStr(String envString) { + String[] envParts = envString.split("[ ;]"); + List results = new ArrayList<>(); + Set dedup = new HashSet<>(); + for (String envPart : envParts) { + String s = envPart.trim(); + if (!s.isBlank() && dedup.add(s)) { + results.add(s); + } + } + + return results; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableModel.java new file mode 100644 index 0000000000..1c14e03408 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableModel.java @@ -0,0 +1,255 @@ +/* ### + * 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.dwarf.external.gui; + +import java.awt.FontMetrics; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.Icon; +import javax.swing.table.TableColumn; + +import docking.widgets.table.*; +import generic.theme.GIcon; +import ghidra.app.util.bin.format.dwarf.external.DebugInfoProvider; +import ghidra.app.util.bin.format.dwarf.external.DebugInfoProviderStatus; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.framework.plugintool.ServiceProviderStub; +import ghidra.util.table.column.GColumnRenderer; +import resources.Icons; + +/** + * Table model for the {@link ExternalDebugFilesConfigDialog} table + */ +class ExternalDebugInfoProviderTableModel + extends GDynamicColumnTableModel > { + + private List rows = new ArrayList<>(); + private boolean dataChanged; + + ExternalDebugInfoProviderTableModel() { + super(new ServiceProviderStub()); + setDefaultTableSortState(null); + } + + boolean isEmpty() { + return rows.isEmpty(); + } + + void setItems(List newItems) { + rows.clear(); + for (DebugInfoProvider item : newItems) { + rows.add(new ExternalDebugInfoProviderTableRow(item)); + } + fireTableDataChanged(); + } + + List getItems() { + return rows.stream().map(ExternalDebugInfoProviderTableRow::getItem).toList(); + } + + void addItem(DebugInfoProvider newItem) { + ExternalDebugInfoProviderTableRow row = new ExternalDebugInfoProviderTableRow(newItem); + rows.add(row); + dataChanged = true; + fireTableDataChanged(); + } + + void addItems(List newItems) { + for (DebugInfoProvider item : newItems) { + rows.add(new ExternalDebugInfoProviderTableRow(item)); + } + dataChanged = true; + fireTableDataChanged(); + } + + void deleteRows(int[] rowIndexes) { + for (int i = rowIndexes.length - 1; i >= 0; i--) { + rows.remove(rowIndexes[i]); + } + dataChanged = true; + fireTableDataChanged(); + } + + void moveRow(int rowIndex, int deltaIndex) { + int destIndex = rowIndex + deltaIndex; + if (rowIndex < 0 || rowIndex >= rows.size() || destIndex < 0 || destIndex >= rows.size()) { + return; + } + + ExternalDebugInfoProviderTableRow row1 = rows.get(rowIndex); + ExternalDebugInfoProviderTableRow row2 = rows.get(destIndex); + rows.set(destIndex, row1); + rows.set(rowIndex, row2); + + dataChanged = true; + + fireTableDataChanged(); + } + + boolean isDataChanged() { + return dataChanged; + } + + void setDataChanged(boolean b) { + this.dataChanged = b; + } + + @Override + public String getName() { + return "External Debug Info Providers"; + } + + @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 && aValue instanceof Boolean boolVal) { + ExternalDebugInfoProviderTableRow row = getRowObject(rowIndex); + row.setEnabled(boolVal); + 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; + } + + //------------------------------------------------------------------------------------------- + static class EnabledColumn + extends AbstractDynamicTableColumnStub + implements TableColumnInitializer { + + @Override + public String getColumnDisplayName(Settings settings) { + return "Enabled"; + } + + @Override + public Boolean getValue(ExternalDebugInfoProviderTableRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.isEnabled(); + } + + @Override + public String getColumnName() { + return "Enabled"; + } + + @Override + public void initializeTableColumn(TableColumn col, FontMetrics fm, int padding) { + int colWidth = fm.stringWidth("Enabled") + padding; + col.setPreferredWidth(colWidth); + col.setMaxWidth(colWidth * 2); + col.setMinWidth(colWidth); + } + + } + + private static class StatusColumn extends + AbstractDynamicTableColumnStub + implements TableColumnInitializer { + + private static final Icon VALID_ICON = new GIcon("icon.checkmark.green"); + 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<>(DebugInfoProviderStatus.class, icons, toolTips); + + @Override + public DebugInfoProviderStatus getValue(ExternalDebugInfoProviderTableRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getStatus(); + } + + @Override + public String getColumnDisplayName(Settings settings) { + return "Status"; + } + + @Override + public String getColumnName() { + return "Status"; + } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + + @Override + public void initializeTableColumn(TableColumn col, FontMetrics fm, int padding) { + int colWidth = fm.stringWidth("Status") + padding; + col.setPreferredWidth(colWidth); + col.setMaxWidth(colWidth * 2); + col.setMinWidth(colWidth); + } + + } + + private class LocationColumn + extends AbstractDynamicTableColumnStub { + + @Override + public String getValue(ExternalDebugInfoProviderTableRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getItem().getDescriptiveName(); + } + + @Override + public String getColumnName() { + return "Location"; + } + + @Override + public int getColumnPreferredWidth() { + return 250; + } + + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableRow.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableRow.java new file mode 100644 index 0000000000..a89f68c06e --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableRow.java @@ -0,0 +1,73 @@ +/* ### + * 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.dwarf.external.gui; + + +import ghidra.app.util.bin.format.dwarf.external.*; + +/** + * Represents a row in a ExternalDebugInfoProviderTableModel + */ +class ExternalDebugInfoProviderTableRow { + + private DebugInfoProvider item; + private DebugInfoProviderStatus status = DebugInfoProviderStatus.UNKNOWN; + + ExternalDebugInfoProviderTableRow(DebugInfoProvider item) { + this.item = item; + } + + DebugInfoProvider getItem() { + return item; + } + + void setItem(DebugInfoProvider newItem) { + this.item = newItem; + } + + DebugInfoProviderStatus getStatus() { + return status; + } + + void setStatus(DebugInfoProviderStatus status) { + this.status = status; + } + + boolean isEnabled() { + return !(item instanceof DisabledDebugInfoProvider); + } + + void setEnabled(boolean enabled) { + if (isEnabled() == enabled) { + return; + } + status = DebugInfoProviderStatus.UNKNOWN; + if (enabled) { + DisabledDebugInfoProvider dss = (DisabledDebugInfoProvider) item; + item = dss.getDelegate(); + } + else { + item = new DisabledDebugInfoProvider(item); + } + } + + @Override + public String toString() { + return String.format("SearchLocationsTableRow: [ status: %s, item: %s]", status.toString(), + item.toString()); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/FilePromptDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/FilePromptDialog.java new file mode 100644 index 0000000000..748ba56519 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/FilePromptDialog.java @@ -0,0 +1,207 @@ +/* ### + * 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.dwarf.external.gui; + +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.widgets.OptionDialog; +import docking.widgets.button.BrowseButton; +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.filechooser.GhidraFileChooserMode; +import docking.widgets.label.GHtmlLabel; +import ghidra.util.filechooser.GhidraFileFilter; +import ghidra.util.layout.ThreeColumnLayout; + +/** + * 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); + File file = filePromptDialog.chosenValue; + filePromptDialog.dispose(); + return file; + } + + 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); + promptLabel.getAccessibleContext().setAccessibleName(prompt); + filePathTextField = new JTextField(file != null ? file.getPath() : null, 40); + filePathTextField.getAccessibleContext().setAccessibleName("File Path"); + 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 = new BrowseButton(); + browseButton.addActionListener(e -> browse()); + browseButton.getAccessibleContext().setAccessibleName("Browse"); + + JPanel mainPanel = new JPanel(new ThreeColumnLayout()); + mainPanel.add(promptLabel); + mainPanel.add(filePathTextField); + mainPanel.add(browseButton); + mainPanel.getAccessibleContext().setAccessibleName("File Prompt"); + + 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); + newMain.getAccessibleContext().setAccessibleName("File Prompt"); + addWorkPanel(newMain); + addOKButton(); + addCancelButton(); + } + + @Override + public void dispose() { + super.dispose(); + + if (chooser != null) { + chooser.dispose(); + } + } + + 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/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/TableColumnInitializer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/TableColumnInitializer.java new file mode 100644 index 0000000000..46267ff513 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/TableColumnInitializer.java @@ -0,0 +1,62 @@ +/* ### + * 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.dwarf.external.gui; + +import java.awt.FontMetrics; + +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +import docking.ComponentProvider; +import docking.DialogComponentProvider; +import docking.widgets.table.*; + +/** + * Add on interface for DynamicTableColumn classes inside a SearchLocationTableModel that let + * them control aspects of the matching TableColumn. + */ +public interface TableColumnInitializer { + /** + * Best called during {@link DialogComponentProvider#dialogShown} or + * {@link ComponentProvider#componentShown} + * + * @param table table component + * @param model table model + */ + static void initializeTableColumns(GTable table, GDynamicColumnTableModel, ?> model) { + TableColumnModel colModel = table.getColumnModel(); + + FontMetrics fm = table.getTableHeader().getFontMetrics(table.getTableHeader().getFont()); + int padding = fm.stringWidth("WW"); // w.a.g. for the left+right padding on the header column component + + for (int colIndex = 0; colIndex < model.getColumnCount(); colIndex++) { + DynamicTableColumn, ?, ?> dtableCol = model.getColumn(colIndex); + if (dtableCol instanceof TableColumnInitializer colInitializer) { + TableColumn tableCol = colModel.getColumn(colIndex); + colInitializer.initializeTableColumn(tableCol, fm, padding); + } + } + } + + /** + * Called to allow the initializer to modify the specified TableColumn + * + * @param col {@link TableColumn} + * @param fm {@link FontMetrics} used by the table header gui component + * @param padding padding to use in the column + */ + void initializeTableColumn(TableColumn col, FontMetrics fm, int padding); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/WellKnownDebugProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/WellKnownDebugProvider.java new file mode 100644 index 0000000000..b9eeac6d58 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/WellKnownDebugProvider.java @@ -0,0 +1,71 @@ +/* ### + * 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.dwarf.external.gui; + +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 debug file search location that has been pre-provided by a Ghidra config file. + * + * @param location url string + * @param locationCategory grouping criteria + * @param warning string + * @param fileOrigin file name that contained this info + */ +public record WellKnownDebugProvider(String location, String locationCategory, + String warning, String fileOrigin) { + + /** + * Loads information about wellknown debuginfod servers from any matching file found in the + * application and returns a list of entries. + * + * @param fileExt extension of the url files to find + * @return list of {@link WellKnownDebugProvider} elements + */ + public static List loadAll(String fileExt) { + List files = Application.findFilesByExtensionInApplication(fileExt); + Set seenProviders = new HashSet<>(); + List results = new ArrayList<>(); + for (ResourceFile file : files) { + 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) { + WellKnownDebugProvider provider = new WellKnownDebugProvider(fields[1], + fields[0], fields.length > 2 ? fields[2] : null, file.getName()); + if (seenProviders.add(provider)) { + results.add(provider); + } + } + } + } + catch (IOException e) { + Msg.warn(WellKnownDebugProvider.class, "Unable to read file: " + file); + } + } + return results; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/sectionprovider/ExternalDebugFileSectionProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/sectionprovider/ExternalDebugFileSectionProvider.java index 4a5d2776c4..ed57e9be4d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/sectionprovider/ExternalDebugFileSectionProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/sectionprovider/ExternalDebugFileSectionProvider.java @@ -15,17 +15,22 @@ */ package ghidra.app.util.bin.format.dwarf.sectionprovider; +import java.io.File; import java.io.IOException; import java.net.MalformedURLException; +import java.nio.file.AccessMode; import java.util.List; import ghidra.app.util.Option; import ghidra.app.util.bin.ByteProvider; -import ghidra.app.util.bin.format.dwarf.external.*; +import ghidra.app.util.bin.FileByteProvider; +import ghidra.app.util.bin.format.dwarf.external.ExternalDebugFilesService; +import ghidra.app.util.bin.format.dwarf.external.ExternalDebugInfo; import ghidra.app.util.importer.MessageLog; import ghidra.app.util.opinion.*; import ghidra.app.util.opinion.Loader.ImporterSettings; -import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.FSRL; +import ghidra.formats.gfilesystem.FileSystemService; import ghidra.framework.options.Options; import ghidra.plugin.importer.ImporterUtilities; import ghidra.program.database.ProgramDB; @@ -56,20 +61,16 @@ public class ExternalDebugFileSectionProvider extends BaseSectionProvider { } Msg.info(ExternalDebugFileSectionProvider.class, "DWARF external debug information found: " + extDebugInfo); - ExternalDebugFilesService edfs = - DWARFExternalDebugFilesPlugin.getExternalDebugFilesService( - SearchLocationRegistry.getInstance().newContext(program)); - FSRL extDebugFile = edfs.findDebugFile(extDebugInfo, monitor); + ExternalDebugFilesService edfs = ExternalDebugFilesService.forProgram(program); + File extDebugFile = edfs.find(extDebugInfo, monitor); if (extDebugFile == null) { return null; } Msg.info(ExternalDebugFileSectionProvider.class, "DWARF External Debug File: found: " + extDebugFile); - FileSystemService fsService = FileSystemService.getInstance(); - try ( - RefdFile refdDebugFile = fsService.getRefdFile(extDebugFile, monitor); - ByteProvider debugFileByteProvider = - fsService.getByteProvider(refdDebugFile.file.getFSRL(), false, monitor);) { + FSRL fsrl = FileSystemService.getInstance().getLocalFSRL(extDebugFile); + try (ByteProvider debugFileByteProvider = + new FileByteProvider(extDebugFile, fsrl, AccessMode.READ)) { Object consumer = new Object(); Language lang = program.getLanguage(); LoadSpec origLoadSpec = ImporterUtilities.getLoadSpec(program); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/test/MockHttpServerUtils.java b/Ghidra/Features/Base/src/main/java/ghidra/test/MockHttpServerUtils.java new file mode 100644 index 0000000000..2b8176b8b2 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/test/MockHttpServerUtils.java @@ -0,0 +1,203 @@ +/* ### + * 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.test; + +import static java.net.HttpURLConnection.*; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.*; +import java.util.Objects; + +import com.sun.net.httpserver.*; + +import ghidra.util.Msg; + +public class MockHttpServerUtils { + private static int LAST_SERVER_PORT_NUM = 8000 + 5000; + public static final String CONTENT_TYPE_HEADER = "Content-Type"; + + /** + * Convert a mock http server's address to a URL + * + * @param addr {@link InetSocketAddress} + * @return http connection URI, example "http://127.0.0.1:9999" + */ + public static URI getURI(InetSocketAddress addr) { + return URI.create("http://%s:%d".formatted(addr.getHostString(), addr.getPort())); + } + + /** + * {@return the next hopefully unused localhost socket addr} + */ + public static InetSocketAddress nextLoopbackServerAddr() { + InetSocketAddress serverAddr = + new InetSocketAddress(InetAddress.getLoopbackAddress(), LAST_SERVER_PORT_NUM); + LAST_SERVER_PORT_NUM++; // don't try to reuse the same server port num in the same session + return serverAddr; + } + + /** + * Creates an HttpServer, listening on localhost and a unique unused port number. + * + * Use {@link HttpServer#createContext(String, HttpHandler)} to add handlers for specific + * paths. + * + * @return new {@link HttpServer} + * @throws IOException if unused port is not found + */ + public static HttpServer createMockHttpServer() throws IOException { + IOException lastException = null; + for (int retryNum = 0; retryNum < 10; retryNum++) { + InetSocketAddress serverAddress = nextLoopbackServerAddr(); + + try { + HttpServer server = HttpServer.create(serverAddress, 0); + return server; + } + catch (IOException e) { + // ignore, just try again with next port num + lastException = e; + } + } + throw new IOException( + "Could not allocate port for mock http server, last attempted port: " + + LAST_SERVER_PORT_NUM, + lastException); + } + + /** + * Asserts that the specified {@link HttpExchange} has a specific content type header. + * + * @param expectedType example: "application/json" + * @param httpExchange {@link HttpExchange} + */ + public static void assertContentType(String expectedType, HttpExchange httpExchange) { + String contentType = httpExchange.getRequestHeaders().getFirst(CONTENT_TYPE_HEADER); + contentType = Objects.requireNonNullElse(contentType, "missing"); + if (!expectedType.equals(contentType)) { + fail("Content type incorrect: expected: %s, actual: %s".formatted(expectedType, + contentType)); + } + } + + /** + * Adds a delay to a handler. + * + * @param delegate {@link HttpHandler} to wrap + * @param delayMS milliseconds to delay before allowing the delegate to process the request + * @return new HttpHandler that wraps the specified delegate + */ + public static HttpHandler wrapHandlerWithDelay(HttpHandler delegate, int delayMS) { + return httpExchange -> { + try { + Thread.sleep(delayMS); + } + catch (InterruptedException e) { + // ignore + } + delegate.handle(httpExchange); + }; + } + + public static HttpHandler wrapHandlerWithRetryError(HttpHandler delegate, int errorCount, + int errorStatus) { + return new HttpHandler() { + int errorNum; + + @Override + public void handle(HttpExchange exchange) throws IOException { + if (errorNum++ < errorCount) { + exchange.sendResponseHeaders(errorStatus, 0); + exchange.close(); + return; + } + delegate.handle(exchange); + } + }; + } + + /** + * A handler that always returns a 404. Use this as the target of a lambda. This matches + * the {@link HttpHandler#handle(HttpExchange)} method signature. + * + * @param httpExchange {@link HttpExchange} + * @throws IOException if error + */ + public static void mock404Handler(HttpExchange httpExchange) throws IOException { + try { + httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_NOT_FOUND, 0); + } + finally { + httpExchange.close(); + } + } + + /** + * Creates a HttpHandler that returns a specified body + * + * @param contentType http content type header value (eg. "text/plain") + * @param resultBody bytes to send as body + * @return new HttpHandler + */ + public static HttpHandler createStaticResponseHandler(String contentType, byte[] resultBody) { + return createStaticResponseHandler(HTTP_OK, contentType, resultBody); + } + + /** + * Creates a HttpHandler that returns a specified body and result code. + * + * @param resultCode http result code to return (eg. HTTP_OK / 200 ) + * @param contentType http content type header value (eg. "text/plain") + * @param resultBody bytes to send as body + * @return new HttpHandler + */ + public static HttpHandler createStaticResponseHandler(int resultCode, String contentType, + byte[] resultBody) { + return httpExchange -> { + try { + byte[] actualResult = + httpExchange.getRequestMethod().equals("GET") ? resultBody : null; + httpExchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, contentType); + httpExchange.sendResponseHeaders(resultCode, + actualResult != null ? actualResult.length : -1); + if (actualResult != null) { + httpExchange.getResponseBody().write(resultBody); + } + } + catch (Throwable th) { + logMockHttp(httpExchange, + "Error during mockStaticResponseHandler: " + th.getMessage()); + throw th; + } + finally { + httpExchange.close(); + } + }; + } + + /** + * Logs (using Msg.info) a message using information from the http connection as a prefix + * + * @param httpExchange {@link HttpExchange} + * @param msg string message + */ + public static void logMockHttp(HttpExchange httpExchange, String msg) { + Msg.info(MockHttpServerUtils.class, "[%s %s] %s".formatted(httpExchange.getLocalAddress(), + httpExchange.getRequestURI(), msg)); + + } +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/string/translate/libretranslate/LibreTranslateStringTranslationServiceTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/string/translate/libretranslate/LibreTranslateStringTranslationServiceTest.java index 5589e732d0..edada63d17 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/string/translate/libretranslate/LibreTranslateStringTranslationServiceTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/string/translate/libretranslate/LibreTranslateStringTranslationServiceTest.java @@ -17,11 +17,12 @@ package ghidra.app.plugin.core.string.translate.libretranslate; import static ghidra.app.plugin.core.string.translate.libretranslate.LibreTranslatePlugin.SOURCE_LANGUAGE_OPTION.*; import static ghidra.app.plugin.core.string.translate.libretranslate.LibreTranslateStringTranslationService.*; +import static ghidra.test.MockHttpServerUtils.*; +import static ghidra.test.MockHttpServerUtils.CONTENT_TYPE_HEADER; import static java.net.HttpURLConnection.*; import static org.junit.Assert.*; import java.io.IOException; -import java.net.*; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; @@ -30,7 +31,8 @@ import org.junit.Before; import org.junit.Test; import com.google.gson.*; -import com.sun.net.httpserver.*; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; import docking.AbstractErrDialog; import docking.widgets.SelectFromListDialog; @@ -43,7 +45,6 @@ import ghidra.program.model.listing.Data; import ghidra.program.util.ProgramLocation; import ghidra.test.AbstractProgramBasedTest; import ghidra.test.ToyProgramBuilder; -import ghidra.util.Msg; import ghidra.util.Swing; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -54,7 +55,6 @@ import ghidra.util.task.TaskMonitor; */ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramBasedTest { - private static int LAST_SERVER_PORT_NUM = 8000 + 5000; private int supportedLanguageCount = 10; private AtomicInteger translateRequestCount = new AtomicInteger(); // number of times translate handler has been invoked private AtomicInteger translateStringCount = new AtomicInteger(); // number of strings that translate handler has processed @@ -91,7 +91,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB // test what happens when the server accepts requests on the REST api endpoint URL, but // returns unexpected json values - HttpServer server = createMockHttpServer(false); + HttpServer server = createMockHttpServer(); server.createContext("/", this::mockUnexpectedJsonResultHandler); try { @@ -120,7 +120,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB // test what happens when the server accepts requests on the REST api endpoint URL, but its // not json - HttpServer server = createMockHttpServer(false); + HttpServer server = createMockHttpServer(); server.createContext("/", this::mockUnexpectedTextResultHandler); try { @@ -149,7 +149,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB // test what happens when the URL doesn't point to active server LibreTranslateStringTranslationService sts = new LibreTranslateStringTranslationService( - getURI(nextUnusedAddr()), null, AUTO, "en", 100, 1000, 1000); + getURI(nextLoopbackServerAddr()), null, AUTO, "en", 100, 1000, 1000); setErrorsExpected(true); // don't kill the test because Msg.showError() was called somewhere Swing.runNow(() -> sts.translate(program, List.of(progLoc(0)), TranslateOptions.NONE)); @@ -346,62 +346,8 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB return builder.getProgram(); } - private URI getURI(InetSocketAddress addr) { - return URI.create("http://%s:%d".formatted(addr.getHostString(), addr.getPort())); - } - - private HttpServer createMockHttpServer() throws IOException { - return createMockHttpServer(true); - } - - private HttpServer createMockHttpServer(boolean addDefaultHandler) throws IOException { - IOException lastException = null; - for (int retryNum = 0; retryNum < 10; retryNum++) { - LAST_SERVER_PORT_NUM++; // don't try to reuse the same server port num in the same session - InetSocketAddress serverAddress = - new InetSocketAddress(InetAddress.getLoopbackAddress(), LAST_SERVER_PORT_NUM); - - try { - HttpServer server = HttpServer.create(serverAddress, 0); - if (addDefaultHandler) { - server.createContext("/", this::mock404Handler); - } - return server; - } - catch (IOException e) { - // ignore, just try again with next port num - lastException = e; - } - } - throw new IOException( - "Could not allocate port for mock http server, last attempted port: " + - LAST_SERVER_PORT_NUM, - lastException); - } - - private InetSocketAddress nextUnusedAddr() { - LAST_SERVER_PORT_NUM++; - return new InetSocketAddress(InetAddress.getLoopbackAddress(), LAST_SERVER_PORT_NUM); - } - - private void assertContentType(HttpExchange httpExchange, String expectedType) { - String contentType = httpExchange.getRequestHeaders() - .getFirst(LibreTranslateStringTranslationService.CONTENT_TYPE_HEADER); - contentType = Objects.requireNonNullElse(contentType, "missing"); - if (!expectedType.equals(contentType)) { - fail("Content type incorrect: expected: %s, actual: %s".formatted(expectedType, - contentType)); - } - } - - private void log(HttpExchange httpExchange, String msg) { - Msg.info(this, "[%s %s] %s".formatted(httpExchange.getLocalAddress(), - httpExchange.getRequestURI(), msg)); - - } - private void mockLangHandler(HttpExchange httpExchange) throws IOException { - assertContentType(httpExchange, CONTENT_TYPE_JSON); + assertContentType(CONTENT_TYPE_JSON, httpExchange); try { JsonArray langsResult = new JsonArray(); for (int i = 0; i < supportedLanguageCount; i++) { @@ -427,7 +373,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB try { translateRequestCount.incrementAndGet(); - assertContentType(httpExchange, CONTENT_TYPE_JSON); + assertContentType(CONTENT_TYPE_JSON, httpExchange); String requestBody = new String(httpExchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); @@ -438,7 +384,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB translateSourceLangs.add(sourceLang); - log(httpExchange, + logMockHttp(httpExchange, "request src=%s, strs=%s".formatted(sourceLang, queryStrs.toString())); JsonObject xlateResultObj = new JsonObject(); @@ -447,7 +393,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB for (int i = 0; i < queryStrs.size(); i++) { xlatedResults.add("result" + translateStringCount.getAndIncrement()); } - log(httpExchange, "response: " + xlateResultObj); + logMockHttp(httpExchange, "response: " + xlateResultObj); byte[] response = xlateResultObj.toString().getBytes(); httpExchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON); @@ -455,7 +401,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB httpExchange.getResponseBody().write(response); } catch (Throwable th) { - log(httpExchange, "Error during mockTranslateHandler: " + th.getMessage()); + logMockHttp(httpExchange, "Error during mockTranslateHandler: " + th.getMessage()); throw th; } finally { @@ -484,27 +430,6 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB httpExchange.close(); } - private HttpHandler wrapHandlerWithDelay(HttpHandler delegate, int delayMS) { - return httpExchange -> { - try { - Thread.sleep(delayMS); - } - catch (InterruptedException e) { - // ignore - } - delegate.handle(httpExchange); - }; - } - - private void mock404Handler(HttpExchange httpExchange) throws IOException { - try { - httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_NOT_FOUND, 0); - } - finally { - httpExchange.close(); - } - } - private ProgramLocation progLoc(int stringNum) { return new ProgramLocation(program, strings.get(stringNum).getAddress()); } diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProviderTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProviderTest.java new file mode 100644 index 0000000000..aa4811d619 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProviderTest.java @@ -0,0 +1,57 @@ +/* ### + * 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.dwarf.external; + +import static org.junit.Assert.*; + +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; + +public class BuildIdDebugFileProviderTest extends AbstractGenericTest { + private TaskMonitor monitor = TaskMonitor.DUMMY; + private File tmpDir; + + @Before + public void setUp() throws Exception { + tmpDir = createTempDirectory("buildid_provider_test"); + } + + @Test + public void testGet() throws IOException, CancelledException { + BuildIdDebugFileProvider provider = new BuildIdDebugFileProvider(tmpDir); + + String buildId = "0000000000000000000000000000000000000000"; + + File f = new File(tmpDir, + "%s/%s.debug".formatted(buildId.substring(0, 2), buildId.substring(2))); + FileUtilities.checkedMkdirs(f.getParentFile()); + FileUtilities.writeStringToFile(f, "test1"); + + File result = provider.getFile(ExternalDebugInfo.forBuildId(buildId), monitor); + + assertEquals("test1", Files.readString(result.toPath())); + assertEquals(5, result.length()); + } +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProviderTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProviderTest.java new file mode 100644 index 0000000000..72a7b99f2c --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProviderTest.java @@ -0,0 +1,201 @@ +/* ### + * 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.dwarf.external; + +import static ghidra.test.MockHttpServerUtils.*; +import static java.net.HttpURLConnection.*; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; + +import org.junit.Test; + +import com.sun.net.httpserver.HttpServer; + +import generic.test.AbstractGenericTest; +import ghidra.app.util.bin.format.dwarf.external.DebugStreamProvider.StreamInfo; +import ghidra.util.HashUtilities; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class HttpDebugInfoDProviderTest extends AbstractGenericTest { + private TaskMonitor monitor = TaskMonitor.DUMMY; + + @Test + public void testNoConnect() throws IOException, CancelledException { + InetSocketAddress unusedAddr = nextLoopbackServerAddr(); + HttpDebugInfoDProvider httpProvider = new HttpDebugInfoDProvider(getURI(unusedAddr)); + StreamInfo stream = httpProvider.getStream( + ExternalDebugInfo.forBuildId("0000000000000000000000000000000000000000"), monitor); + assertNull(stream); + } + + @Test + public void testGet() throws IOException, CancelledException { + String buildId = "0000000000000000000000000000000000000000"; + + HttpServer server = createMockHttpServer(); + server.createContext("/buildid/" + buildId + "/debuginfo", + createStaticResponseHandler("application/octet-stream", "result1".getBytes())); + server.createContext("/buildid/" + buildId + "/executable", + createStaticResponseHandler("application/octet-stream", "result2".getBytes())); + server.createContext("/buildid/" + buildId + "/source/usr/include/stdio.h", + createStaticResponseHandler("application/octet-stream", "result3".getBytes())); + + HttpDebugInfoDProvider httpProvider = + new HttpDebugInfoDProvider(getURI(server.getAddress())); + try { + server.start(); + + ExternalDebugInfo id = ExternalDebugInfo.forBuildId(buildId); + assertStreamResult("result1", httpProvider.getStream(id, monitor)); + assertStreamResult("result2", + httpProvider.getStream(id.withType(ObjectType.EXECUTABLE, null), monitor)); + assertStreamResult("result3", httpProvider + .getStream(id.withType(ObjectType.SOURCE, "/usr/include/stdio.h"), monitor)); + + assertEquals(0, httpProvider.getRetriedCount()); + assertEquals(0, httpProvider.getNotFoundCount()); + + } + finally { + server.stop(0); + } + } + + @Test + public void testGetWithRetry() throws IOException, CancelledException { + String buildId = "0000000000000000000000000000000000000000"; + + HttpServer server = createMockHttpServer(); + server.createContext("/buildid/" + buildId + "/debuginfo", + wrapHandlerWithRetryError( + createStaticResponseHandler("application/octet-stream", "result1".getBytes()), 3, + HTTP_INTERNAL_ERROR)); + + HttpDebugInfoDProvider httpProvider = + new HttpDebugInfoDProvider(getURI(server.getAddress())); + try { + server.start(); + + ExternalDebugInfo id = ExternalDebugInfo.forBuildId(buildId); + assertStreamResult("result1", httpProvider.getStream(id, monitor)); + assertEquals(3, httpProvider.getRetriedCount()); + } + finally { + server.stop(0); + } + } + + @Test + public void testTimeout() throws IOException, CancelledException { + String buildId = "0000000000000000000000000000000000000000"; + + HttpServer server = createMockHttpServer(); + server.createContext("/buildid/" + buildId + "/debuginfo", wrapHandlerWithDelay( + createStaticResponseHandler("application/octet-stream", "result1".getBytes()), 3000)); + + HttpDebugInfoDProvider httpProvider = + new HttpDebugInfoDProvider(getURI(server.getAddress())); + httpProvider.setMaxRetryCount(1); + httpProvider.setHttpRequestTimeoutMs(1000); + try { + server.start(); + + long startms = System.currentTimeMillis(); + ExternalDebugInfo id = ExternalDebugInfo.forBuildId(buildId); + long elapsed = System.currentTimeMillis() - startms; + assertNull(httpProvider.getStream(id, monitor)); + assertTrue("Request took too long", elapsed < (1000 * 2)); // make sure request time was approx same as timeout setting + } + finally { + server.stop(0); + } + } + + @Test + public void testGetNotFound() throws IOException, CancelledException { + HttpServer server = createMockHttpServer(); + + HttpDebugInfoDProvider httpProvider = + new HttpDebugInfoDProvider(getURI(server.getAddress())); + try { + server.start(); + + ExternalDebugInfo id = + ExternalDebugInfo.forBuildId("0000000000000000000000000000000000000000"); + assertNull(httpProvider.getStream(id, monitor)); + assertEquals(0, httpProvider.getRetriedCount()); + assertEquals(1, httpProvider.getNotFoundCount()); + } + finally { + server.stop(0); + } + } + + @Test + public void testServerError() throws IOException, CancelledException { + String buildId = "0000000000000000000000000000000000000000"; + HttpServer server = createMockHttpServer(); + server.createContext("/buildid/" + buildId + "/debuginfo", + createStaticResponseHandler(HTTP_INTERNAL_ERROR, "text/plain", "".getBytes())); + + HttpDebugInfoDProvider httpProvider = + new HttpDebugInfoDProvider(getURI(server.getAddress())); + try { + server.start(); + + ExternalDebugInfo id = + ExternalDebugInfo.forBuildId("0000000000000000000000000000000000000000"); + assertNull(httpProvider.getStream(id, monitor)); + assertEquals(4, httpProvider.getRetriedCount()); + assertEquals(0, httpProvider.getNotFoundCount()); + } + finally { + server.stop(0); + } + } + + //@Test + public void testElfUtilsOrg() throws IOException, CancelledException { + // test actual file from elfutils.org. + // Not enabled by default + // The specified buildId may stop being present at some point of time in the future + HttpDebugInfoDProvider httpProvider = + new HttpDebugInfoDProvider(URI.create("https://debuginfod.elfutils.org/")); + ExternalDebugInfo id = + ExternalDebugInfo.forBuildId("421e1abd8faf1cb290df755a558377c5d7def3b1"); + assertStreamHash("f5894783abae9084e531b8da76bbb2444a688d18", + httpProvider.getStream(id, monitor)); + } + + private void assertStreamResult(String expectedResult, StreamInfo stream) throws IOException { + try (stream) { + String result = new String(stream.is().readAllBytes()); + assertEquals(expectedResult, result); + } + } + + private void assertStreamHash(String expectedHash, StreamInfo stream) throws IOException { + try (stream) { + String hash = HashUtilities.getHash("SHA1", stream.is()); + assertEquals(expectedHash, hash); + } + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProviderTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProviderTest.java new file mode 100644 index 0000000000..380bac2246 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProviderTest.java @@ -0,0 +1,115 @@ +/* ### + * 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.dwarf.external; + +import static org.junit.Assert.*; + +import java.io.*; +import java.time.Duration; + +import org.junit.Before; +import org.junit.Test; + +import generic.test.AbstractGenericTest; +import ghidra.app.util.bin.format.dwarf.external.DebugStreamProvider.StreamInfo; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; + +public class LocalDirDebugInfoDProviderTest extends AbstractGenericTest { + private TaskMonitor monitor = TaskMonitor.DUMMY; + private File tmpDir; + + @Before + public void setUp() throws Exception { + tmpDir = createTempDirectory("debuginfod_provider_test"); + } + + @Test + public void testAgeOff() throws IOException { + LocalDirDebugInfoDProvider provider = new LocalDirDebugInfoDProvider(tmpDir); + provider.purgeAll(); + + String buildId = "0000000000000000000000000000000000000000"; + + File f = new File(tmpDir, buildId + "/debuginfo"); + + FileUtilities.checkedMkdirs(f.getParentFile()); + FileUtilities.writeStringToFile(f, "test1"); + f.setLastModified(System.currentTimeMillis() - Duration.ofDays(1).toMillis()); // make it look recent + + provider.performCacheMaintIfNeeded(); + assertTrue(f.isFile()); // should still be there + + provider.purgeAll(); + + FileUtilities.checkedMkdirs(f.getParentFile()); + FileUtilities.writeStringToFile(f, "test1"); + f.setLastModified( + System.currentTimeMillis() - LocalDirDebugInfoDProvider.MAX_FILE_AGE_MS - 1000); // make it look old + + provider.performCacheMaintIfNeeded(); + assertFalse(f.isFile()); // should be gone + } + + @Test + public void testGet() throws IOException, CancelledException { + LocalDirDebugInfoDProvider provider = new LocalDirDebugInfoDProvider(tmpDir); + provider.purgeAll(); + + String buildId = "0000000000000000000000000000000000000000"; + + File f = new File(tmpDir, buildId + "/debuginfo"); + FileUtilities.checkedMkdirs(f.getParentFile()); + FileUtilities.writeStringToFile(f, "test1"); + + File result = provider.getFile(ExternalDebugInfo.forBuildId(buildId), monitor); + + assertEquals("debuginfo", result.getName()); + assertEquals(5, result.length()); + } + + @Test + public void testPut() throws IOException, CancelledException { + LocalDirDebugInfoDProvider provider = new LocalDirDebugInfoDProvider(tmpDir); + provider.purgeAll(); + + String buildId = "0000000000000000000000000000000000000000"; + byte bytes[] = "test".getBytes(); + StreamInfo stream = new StreamInfo(new ByteArrayInputStream(bytes), bytes.length); + File f = provider.putStream(ExternalDebugInfo.forBuildId(buildId), stream, monitor); + + assertEquals("debuginfo", f.getName()); + assertEquals(bytes.length, f.length()); + } + + @Test + public void testPutNonBuildId() throws CancelledException { + LocalDirDebugInfoDProvider provider = new LocalDirDebugInfoDProvider(tmpDir); + provider.purgeAll(); + + byte bytes[] = "test".getBytes(); + StreamInfo stream = new StreamInfo(new ByteArrayInputStream(bytes), bytes.length); + try { + File f = provider.putStream(ExternalDebugInfo.forDebugLink("test.debug", 0x11223344), + stream, monitor); + fail("Shouldn't get here: " + f); + } + catch (IOException e) { + // successfully failed + } + } +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProviderTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProviderTest.java new file mode 100644 index 0000000000..6e568586d4 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProviderTest.java @@ -0,0 +1,55 @@ +/* ### + * 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.dwarf.external; + +import static org.junit.Assert.*; + +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; + +public class LocalDirDebugLinkProviderTest extends AbstractGenericTest { + private TaskMonitor monitor = TaskMonitor.DUMMY; + private File tmpDir; + + @Before + public void setUp() throws Exception { + tmpDir = createTempDirectory("debuglink_provider_test"); + } + + @Test + public void testGet() throws IOException, CancelledException { + File debugNestedDir = new File(tmpDir, "sub/sub2/sub3"); + File debugFile = new File(debugNestedDir, "debugfile.abc"); + FileUtilities.mkdirs(debugFile.getParentFile()); + Files.writeString(debugFile.toPath(), "test_debuglink"); + int crc = LocalDirDebugLinkProvider.calcCRC(debugFile); + + LocalDirDebugLinkProvider provider = new LocalDirDebugLinkProvider(tmpDir); + File result = + provider.getFile(ExternalDebugInfo.forDebugLink("debugfile.abc", crc), monitor); + + assertEquals("test_debuglink", Files.readString(result.toPath())); + } +} diff --git a/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/HttpSymbolServerTest.java b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/HttpSymbolServerTest.java index bcaccd4f8b..1e63a4a196 100644 --- a/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/HttpSymbolServerTest.java +++ b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/HttpSymbolServerTest.java @@ -4,9 +4,9 @@ * 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. @@ -15,17 +15,26 @@ */ package pdb.symbolserver; +import static ghidra.test.MockHttpServerUtils.*; import static org.junit.Assert.*; +import java.io.IOException; import java.net.URI; import java.util.List; +import org.junit.Test; + +import com.sun.net.httpserver.HttpServer; + +import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; public class HttpSymbolServerTest { + private TaskMonitor monitor = TaskMonitor.DUMMY; + //@Test - public void test() { + public void testMSFTSymbolServer() { // 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/")); @@ -37,4 +46,68 @@ public class HttpSymbolServerTest { assertEquals(1, results.size()); } + @Test + public void testLocalHttpserverLevel1() throws IOException, CancelledException { + HttpServer server = createMockHttpServer(); + + server.createContext("/kernelbase.pdb/C1C44EDD93E1B8BA671874B5C1490C2D1/kernelbase.pdb", + createStaticResponseHandler("application/octet", "result1".getBytes())); + + try { + server.start(); + + HttpSymbolServer httpSymbolServer = new HttpSymbolServer(getURI(server.getAddress())); + SymbolFileInfo pdbInfo = + SymbolFileInfo.fromValues("kernelbase.pdb", "C1C44EDD93E1B8BA671874B5C1490C2D", 1); + List
results = + httpSymbolServer.find(pdbInfo, FindOption.NO_OPTIONS, monitor); + assertEquals(1, results.size()); + SymbolFileLocation result = results.get(0); + + SymbolServerInputStream stream = + httpSymbolServer.getFileStream(result.getPath(), monitor); + assertStreamResult("result1", stream); + } + finally { + server.stop(0); + } + } + + @Test + public void testLocalHttpserverLevel2() throws IOException, CancelledException { + HttpServer server = createMockHttpServer(); + server.createContext("/index2.txt", + createStaticResponseHandler("text/plain", "".getBytes())); + + server.createContext("/ke/kernelbase.pdb/C1C44EDD93E1B8BA671874B5C1490C2D1/kernelbase.pdb", + createStaticResponseHandler("application/octet", "result1".getBytes())); + + try { + server.start(); + + HttpSymbolServer httpSymbolServer = new HttpSymbolServer(getURI(server.getAddress())); + SymbolFileInfo pdbInfo = + SymbolFileInfo.fromValues("kernelbase.pdb", "C1C44EDD93E1B8BA671874B5C1490C2D", 1); + List results = + httpSymbolServer.find(pdbInfo, FindOption.NO_OPTIONS, monitor); + assertEquals(1, results.size()); + SymbolFileLocation result = results.get(0); + + SymbolServerInputStream stream = + httpSymbolServer.getFileStream(result.getPath(), monitor); + assertStreamResult("result1", stream); + } + finally { + server.stop(0); + } + } + + private void assertStreamResult(String expectedResult, SymbolServerInputStream stream) + throws IOException { + try (stream) { + String result = new String(stream.getInputStream().readAllBytes()); + assertEquals(expectedResult, result); + } + } + } diff --git a/Ghidra/Framework/Gui/src/main/java/ghidra/util/layout/ThreeColumnLayout.java b/Ghidra/Framework/Gui/src/main/java/ghidra/util/layout/ThreeColumnLayout.java new file mode 100644 index 0000000000..d56ff10cf1 --- /dev/null +++ b/Ghidra/Framework/Gui/src/main/java/ghidra/util/layout/ThreeColumnLayout.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 ghidra.util.layout; + +import java.awt.*; + +/** + * LayoutManger for arranging components into exactly three columns. The first and last column + * are statically sized to be the max preferred width of those columns. The middle column's width + * will vary as the panel is resized. + * + * This layout works well for a panel that has rows of labels followed by a field and followed by + * a trailing component like a button group. + */ +public class ThreeColumnLayout implements LayoutManager { + private static final int DEFAULT_VGAP = 5; + private static final int DEFAULT_HGAP = 5; + private static final int MIN_MAIN_COMP_WIDTH = 80; + private int vgap; + private int hgaps[]; + private int minPreferredWidths[] = new int[3]; + + public ThreeColumnLayout() { + this(DEFAULT_VGAP, new int[] { DEFAULT_HGAP, DEFAULT_HGAP }, + new int[] { 0, MIN_MAIN_COMP_WIDTH, 0 }); + } + + public ThreeColumnLayout(int vgap, int hgap1, int hgap2) { + this(vgap, new int[] { hgap1, hgap2 }, new int[] { 0, MIN_MAIN_COMP_WIDTH, 0 }); + } + + public ThreeColumnLayout(int vgap, int hgaps[], int[] minPreferredWidths) { + this.vgap = vgap; + this.hgaps = hgaps; + this.minPreferredWidths = minPreferredWidths; + } + + @Override + public void addLayoutComponent(String name, Component comp) { + // empty + } + + @Override + public void removeLayoutComponent(Component comp) { + // empty + } + + @Override + public Dimension preferredLayoutSize(Container parent) { + Dimension d = new Dimension(0, 0); + Insets insets = parent.getInsets(); + int[] widths = getPreferredWidths(parent); + d.width = + widths[0] + hgaps[0] + widths[1] + hgaps[1] + widths[2] + insets.left + insets.right; + int n = parent.getComponentCount(); + for (int i = 0; i < n; i += 3) { + Component c = parent.getComponent(i); + int height = c.getPreferredSize().height; + if (i < n - 2) { + c = parent.getComponent(i + 1); + height = Math.max(c.getPreferredSize().height, height); + c = parent.getComponent(i + 2); + height = Math.max(c.getPreferredSize().height, height); + } + d.height += height; + d.height += vgap; + } + d.height -= vgap; + d.height += insets.top + insets.bottom; + return d; + } + + @Override + public Dimension minimumLayoutSize(Container parent) { + return preferredLayoutSize(parent); + } + + @Override + public void layoutContainer(Container parent) { + int[] widths = getPreferredWidths(parent); + Dimension d = parent.getSize(); + Insets insets = parent.getInsets(); + int width = d.width - (insets.left + insets.right); + int x = insets.left; + int y = insets.top; + int width1 = widths[0]; + int width3 = widths[2]; + int width2 = + Math.max(minPreferredWidths[1], width - (width1 + width3 + hgaps[0] + hgaps[1])); + + int compCount = parent.getComponentCount(); + for (int i = 0; i < compCount; i += 3) { + Component c = parent.getComponent(i); + int height = c.getPreferredSize().height; + if (i < compCount - 2) { + Component c2 = parent.getComponent(i + 1); + Component c3 = parent.getComponent(i + 2); + height = Math.max(height, c2.getPreferredSize().height); + height = Math.max(height, c3.getPreferredSize().height); + + c2.setBounds(x + width1 + hgaps[0], y, width2, height); + c3.setBounds(x + width1 + hgaps[0] + width2 + hgaps[1], y, width3, height); + } + c.setBounds(x, y, width1, height); + y += height + vgap; + } + } + + int[] getPreferredWidths(Container parent) { + int[] widths = new int[3]; + System.arraycopy(minPreferredWidths, 0, widths, 0, 3); + int n = parent.getComponentCount(); + for (int i = 0; i < n; i++) { + Component c = parent.getComponent(i); + Dimension d = c.getPreferredSize(); + int colIndex = i % 3; + widths[colIndex] = Math.max(widths[colIndex], d.width); + } + return widths; + } + +} diff --git a/Ghidra/Framework/Utility/src/main/java/utility/application/ApplicationUtilities.java b/Ghidra/Framework/Utility/src/main/java/utility/application/ApplicationUtilities.java index ec447a3734..c47527e807 100644 --- a/Ghidra/Framework/Utility/src/main/java/utility/application/ApplicationUtilities.java +++ b/Ghidra/Framework/Utility/src/main/java/utility/application/ApplicationUtilities.java @@ -341,7 +341,7 @@ public class ApplicationUtilities { * @throws FileNotFoundException if Java's user home directory is not defined or it is not an * absolute path */ - private static File getJavaUserHomeDir() throws FileNotFoundException { + public static File getJavaUserHomeDir() throws FileNotFoundException { return getSystemPropertyFile("user.home", true); } diff --git a/Ghidra/Framework/Utility/src/main/java/utility/application/XdgUtils.java b/Ghidra/Framework/Utility/src/main/java/utility/application/XdgUtils.java index bd77b1b84c..691bb26bab 100644 --- a/Ghidra/Framework/Utility/src/main/java/utility/application/XdgUtils.java +++ b/Ghidra/Framework/Utility/src/main/java/utility/application/XdgUtils.java @@ -4,9 +4,9 @@ * 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. @@ -66,6 +66,8 @@ public class XdgUtils { */ public static final String XDG_CACHE_HOME = "XDG_CACHE_HOME"; + public static final String XDG_CACHE_HOME_DEFAULT_SUBDIRNAME = ".cache"; + /** * $XDG_RUNTIME_DIR defines the base directory relative to which user-specific non-essential * runtime files and other file objects (such as sockets, named pipes, ...) should be stored. @@ -73,4 +75,5 @@ public class XdgUtils { * access to it. Its Unix access mode MUST be 0700. */ public static final String XDG_RUNTIME_DIR = "XDG_RUNTIME_DIR"; + } diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DWARFExternalDebugFilesPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DWARFExternalDebugFilesPluginScreenShots.java new file mode 100644 index 0000000000..db510b80f9 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DWARFExternalDebugFilesPluginScreenShots.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 help.screenshot; + +import java.io.File; +import java.net.URI; +import java.util.List; + +import org.junit.Test; + +import ghidra.app.util.bin.format.dwarf.external.*; +import ghidra.app.util.bin.format.dwarf.external.gui.ExternalDebugFilesConfigDialog; + +public class DWARFExternalDebugFilesPluginScreenShots extends GhidraScreenShotGenerator { + + @Test + public void testExternalDebugFilesConfigDialog() { + LocalDirDebugInfoDProvider store = LocalDirDebugInfoDProvider.getGhidraCacheInstance(); + store = new LocalDirDebugInfoDProvider(store.getRootDir(), store.getName(), + "Ghidra Cache Dir "); + + LocalDirDebugInfoDProvider homeCache = + LocalDirDebugInfoDProvider.getUserHomeCacheInstance(); + homeCache = new LocalDirDebugInfoDProvider(homeCache.getRootDir(), homeCache.getName(), + "DebugInfoD Cache Dir "); + + ExternalDebugFilesService edfs = new ExternalDebugFilesService(store, List.of()); + edfs.addProvider(new SameDirDebugInfoProvider(null)); + edfs.addProvider(homeCache); + edfs.addProvider(new BuildIdDebugFileProvider(new File("/usr/lib/debug/.build-id"))); + edfs.addProvider(new HttpDebugInfoDProvider(URI.create("http://debuginfod.elfutils.org"))); + + ExternalDebugFilesConfigDialog dlg = new ExternalDebugFilesConfigDialog(); + dlg.setService(edfs); + showDialogWithoutBlocking(tool, dlg); + waitForSwing(); + captureDialog(ExternalDebugFilesConfigDialog.class, 600, 300); + } + +}