Merge remote-tracking branch 'origin/GP-5924_dev747368_dwarf_debuginfod'

(Closes #8407)
This commit is contained in:
Ryan Kurtz
2025-10-07 05:00:03 -04:00
51 changed files with 3861 additions and 708 deletions

View File

@@ -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|

View File

@@ -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.

View File

@@ -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|

View File

@@ -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<DWARFCompilationUnit> compUnits = dprog.getCompilationUnits();
SourceFileManager sourceManager = currentProgram.getSourceFileManager();
@@ -103,6 +110,14 @@ public class DWARFLineInfoSourceMapScript extends GhidraScript {
SourceFile sFile = new SourceFile(path, type, sfi.md5());
sourceManager.addSourceFile(sFile);
sourceFileInfoToSourceFile.put(sfi, sFile);
if (hasBuildId) {
ExternalDebugInfo srcFileDebugInfo =
extDebugInfo.withType(ObjectType.SOURCE, path);
File srcFile = edfs.find(srcFileDebugInfo, monitor);
if (srcFile != null) {
println("Source file: " + srcFile);
}
}
}
catch (IllegalArgumentException e) {
if (numErrors++ < MAX_ERROR_MSGS_TO_DISPLAY) {

View File

@@ -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.
@@ -41,15 +41,16 @@ public class DWARFSetExternalDebugFilesLocationPrescript extends GhidraScript {
Msg.warn(this, "Invalid DWARF external debug files location specified: " + dir);
return;
}
List<SearchLocation> searchLocations = new ArrayList<>();
List<DebugInfoProvider> searchLocations = new ArrayList<>();
File buildIdDir = new File(dir, ".build-id");
if (buildIdDir.isDirectory()) {
searchLocations.add(new BuildIdSearchLocation(buildIdDir));
searchLocations.add(new BuildIdDebugFileProvider(buildIdDir));
}
searchLocations.add(new LocalDirectorySearchLocation(dir));
ExternalDebugFilesService edfs = new ExternalDebugFilesService(searchLocations);
DWARFExternalDebugFilesPlugin.saveExternalDebugFilesService(edfs);
searchLocations.add(new LocalDirDebugLinkProvider(dir));
ExternalDebugFilesService edfs = new ExternalDebugFilesService(
LocalDirDebugInfoDProvider.getGhidraCacheInstance(), searchLocations);
ExternalDebugFilesService.saveToPrefs(edfs);
}
}

View File

@@ -1,43 +1,75 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML>
<HEAD>
<TITLE>DWARF External Debug Files</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=utf-8">
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<HEAD>
<TITLE>DWARF External Debug Files</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=utf-8">
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<BODY lang="EN-US">
<H1>DWARF External Debug Files</H1>
<BODY lang="EN-US">
<H1><a name="Summary"></a>DWARF External Debug Files</H1>
<P>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).</P>
<P>Use the ExtractELFDebugFilesScript to pull external debug files from pre-packaged install
files, typically provided by Linux / BSD distributions, for later consumption by Ghidra.</P>
<H2>Menu Actions</H2>
<BLOCKQUOTE>
<H3 align="left"><A name="DWARF_External_Debug_Config"></A>DWARF External Debug Config</H3>
<P>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 <b>".gnu_debuglink"</b> section (a filename and
crc32) and/or <b>".note.gnu.build-id"</b> section (a hash value).</P>
<BLOCKQUOTE>
<P align="left">Allows the user to pick a directory where Ghidra will search for DWARF external debug files.</P>
<P align="left">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.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<P>Use the <code>ExtractELFDebugFilesScript</code> to pull external debug files from
pre-packaged install files, typically provided by Linux / BSD distributions, for later
consumption by Ghidra.</P>
<P>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
<b>.debug_info</b> sections.</P>
<h2><a name="Configuration"></a>Configuration</h2>
<P>See <b>Edit <IMG src="help/shared/arrow.gif" alt="-&gt;" border="0"> DWARF External Debug Config</b></P>
<P align="center"><IMG border="0" src="images/ExternalDebugFilesConfigDialog.png"></P>
<P class="providedbyplugin"><BR>
Provided by: <I>DWARF External Debug Files Plugin</I></P>
<UL>
<LI><a name="LocalStorage"></a><b>Local Storage</b> - the location where files downloaded
from remote debuginfod servers will be stored. This defaults to a Ghidra specific cache
directory, but can be changed to debuginfod's cache directory, or any other location.</LI>
<LI><b>Additional Locations</b> - a list of locations to search when trying to find
a debug file.
<h3><a name="ButtonActions"></a>Button actions:</h3>
<UL>
<LI><A name="Add"></A><img border="0" src="images/Plus2.png">&nbsp;(Add) Adds a location. See <a href="#LocationTypes">Debug location types</a></LI>
<LI><A name="Delete"></A><img border="0" src="images/error.png">&nbsp;(Delete) Deletes the highlighted row</LI>
<LI><A name="UpDown"></A><img border="0" src="images/up.png"><img border="0" src="images/down.png">&nbsp;(Up/Down) Moves the highlighted row up or down</LI>
<LI><A name="Refresh"></A><img border="0" src="images/reload3.png">&nbsp;(Refresh) Updates the status of all rows</LI>
<LI><A name="Save"></A><img border="0" src="images/disk.png">&nbsp;(Save) Saves the current information</LI>
</UL>
</LI>
</UL>
<P>&nbsp;</P>
<BR>
<BR>
<BR>
</BODY>
<h3><a name="LocationTypes"></a>Debug location types:</h3>
<UL>
<LI><b>Program's Import Location</b> - searchs the directory from which the program was
imported for any debug-link specified files, and for build-id specified files named
<code>aabbcc...zz.debug</code>, where <code>aa..zz</codE> is the build-id hash in hex.</LI>
<LI><b>Build-id Directory</b> - directory where debug files that are identified by a
build-id hash are stored.<br>
Debug files are named <code>aa/bbccdd...zz.debug</code> under the base directory<br>
This storage scheme for build-id debug files is distinct from debuginfod's scheme.<br><br>
Example: <code>/usr/lib/debug/.build-id</code></LI>
<LI><b>Debug Link Directory</b> - directory where debug files that are identified by a
debug filename and crc hash (found in the binary's .gnu_debuglink section).<br>
<b>NOTE</b>: This directory is searched recursively for a matching file.</LI>
<LI><b>Debuginfod Directory</b> - directory where debuginfod has stored files. This
typically will be something like <code>/home/user/.cache/debuginfod_client</code>.</LI>
<LI><b>Debuginfod URL</b> - HTTP(s) URL that points to a debuginfod server.</LI>
<LI><b>Import DEBUGINFOD_URLS Env Var</b> - Helper action that adds any HTTP(s) URLs found
in debuginfod's environment variable.</LI>
</UL>
<P class="providedbyplugin"><BR>
Provided by: <I>DWARF External Debug Files Plugin</I></P>
<P>&nbsp;</P>
<BR>
<BR>
<BR>
</BODY>
</HTML>

View File

@@ -11,7 +11,7 @@
</HEAD>
<BODY lang="EN-US">
<H1><a name="LibreTranslatePlugin"></a>LibreTranslate Plugin</H1>
<H1><a name="LibreTranslatePlugin"></a>LibreTranslate Plugin</H1>
<P>This plugin adds a string translation service that will appear in the <b>Translate</b>
menu of a string data instance. The <b>Translate</b> menu will appear in the right-click
@@ -19,21 +19,21 @@
<P>LibreTranslate (currently hosted at libretranslate.com) is an independant project that
provides an open source translation package that can be self-hosted.</P>
<P>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.</P>
server.</P>
<P>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
<b>http://localhost:5000/</b> (when configured with suggested defaults).</P>
<P>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.</P>
<P>When a string has been translated, the translated value will be shown in place of
the original value, bracketed with <b>&#x00BB;chevrons&#x00AB;</b></P>
<P>When a string has been translated, the translated value will be shown in place of
the original value, bracketed with <b>&#x00BB;chevrons&#x00AB;</b></P>
<h2><a name="Configuration"></a>Configuration</h2>
<P>See
<b>Edit <IMG src="help/shared/arrow.gif" alt="-&gt;" border="0">
@@ -41,7 +41,7 @@
Strings | LibreTranslate</b>
</P>
<blockquote>
<UL>
<UL>
<LI><b>URL</b> - required. Example: <b>http://localhost:5000/</b>
(if self hosted and following suggested values)</LI>
<LI><b>API Key</b> - a unique key that authorizes you to connect to the LibreTranslate

View File

@@ -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);

View File

@@ -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;

View File

@@ -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<Long, DebugInfoEntry> diesByOffset = new WeakValueHashMap<>();
private WeakValueHashMap<Long, DIEAggregate> 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<Long, Long> typeReferers = new ArrayListValuedHashMap<>();

View File

@@ -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.
* <p>
* 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;
}
}

View File

@@ -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.
* <p>
* 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;
}
}

View File

@@ -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<SearchLocation> 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);
}
}
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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) {}

View File

@@ -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<DebugInfoProviderCreationInfo> 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<String> 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<String> testFunc;
DebugInfoProviderCreator createFunc;
DebugInfoProviderCreationInfo(Predicate<String> testFunc,
DebugInfoProviderCreator createFunc) {
this.testFunc = testFunc;
this.createFunc = createFunc;
}
}
}

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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<SearchLocation> 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<DebugInfoProvider> 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<SearchLocation> searchLocations) {
this.searchLocations = searchLocations;
public ExternalDebugFilesService(DebugFileStorage storage, List<DebugInfoProvider> 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<SearchLocation> getSearchLocations() {
return searchLocations;
public List<DebugInfoProvider> 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.
* <p>
* 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<DebugInfoProvider> 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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<InputStream> 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<InputStream> tryGet(HttpRequest request, TaskMonitor monitor)
throws IOException, CancelledException, TimeoutException {
Msg.debug(this, logPrefix() + ": " + request.toString());
CompletableFuture<HttpResponse<InputStream>> futureResponse =
HttpClients.getHttpClient().sendAsync(request, BodyHandlers.ofInputStream());
CancelledListener l = () -> futureResponse.cancel(true);
monitor.addCancelledListener(l);
try {
HttpResponse<InputStream> 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;
}
}

View File

@@ -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.
* <p>
* Provides ability to store files.
* <p>
* 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.
* <p>
* 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;
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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<SearchLocationCreationInfo> 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<String> 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<String> testFunc;
SearchLocationCreator createFunc;
SearchLocationCreationInfo(Predicate<String> testFunc,
SearchLocationCreator createFunc) {
this.testFunc = testFunc;
this.createFunc = createFunc;
}
}
}

View File

@@ -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 <E> enum type
*/
public class EnumIconColumnRenderer<E extends Enum<E>>
extends AbstractGColumnRenderer<E> {
private Icon[] icons;
private String[] toolTips;
EnumIconColumnRenderer(Class<E> 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();
}
}

View File

@@ -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<WellKnownDebugProvider> 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<WellKnownDebugProvider> 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<DebugInfoProvider> 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<ExternalDebugInfoProviderTableRow> 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, """
<html>Enter value:<br>
<br>
Example: https://debuginfod.domain1.org https://debuginfod.domain2.org<br>
<br>""", "Enter DEBUGINFOD_URLS Value", JOptionPane.QUESTION_MESSAGE, null,
null, Objects.requireNonNullElse(System.getenv("DEBUGINFOD_URLS"), ""));
if (envVar == null) {
return;
}
List<String> urls = 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<String> getURLsFromEnvStr(String envString) {
String[] envParts = envString.split("[ ;]");
List<String> results = new ArrayList<>();
Set<String> dedup = new HashSet<>();
for (String envPart : envParts) {
String s = envPart.trim();
if (!s.isBlank() && dedup.add(s)) {
results.add(s);
}
}
return results;
}
}

View File

@@ -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<ExternalDebugInfoProviderTableRow, List<ExternalDebugInfoProviderTableRow>> {
private List<ExternalDebugInfoProviderTableRow> rows = new ArrayList<>();
private boolean dataChanged;
ExternalDebugInfoProviderTableModel() {
super(new ServiceProviderStub());
setDefaultTableSortState(null);
}
boolean isEmpty() {
return rows.isEmpty();
}
void setItems(List<DebugInfoProvider> newItems) {
rows.clear();
for (DebugInfoProvider item : newItems) {
rows.add(new ExternalDebugInfoProviderTableRow(item));
}
fireTableDataChanged();
}
List<DebugInfoProvider> 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<DebugInfoProvider> 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<ExternalDebugInfoProviderTableRow> getModelData() {
return rows;
}
@Override
public List<ExternalDebugInfoProviderTableRow> getDataSource() {
return rows;
}
@Override
public boolean isSortable(int columnIndex) {
return false;
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
DynamicTableColumn<ExternalDebugInfoProviderTableRow, ?, ?> 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<ExternalDebugInfoProviderTableRow, ?, ?> column = getColumn(columnIndex);
return column instanceof EnabledColumn;
}
@Override
protected TableColumnDescriptor<ExternalDebugInfoProviderTableRow> createTableColumnDescriptor() {
TableColumnDescriptor<ExternalDebugInfoProviderTableRow> descriptor = new TableColumnDescriptor<>();
descriptor.addVisibleColumn(new EnabledColumn());
descriptor.addVisibleColumn(new StatusColumn());
descriptor.addVisibleColumn(new LocationColumn());
return descriptor;
}
//-------------------------------------------------------------------------------------------
static class EnabledColumn
extends AbstractDynamicTableColumnStub<ExternalDebugInfoProviderTableRow, Boolean>
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<ExternalDebugInfoProviderTableRow, DebugInfoProviderStatus>
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<DebugInfoProviderStatus> 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<DebugInfoProviderStatus> 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<ExternalDebugInfoProviderTableRow, String> {
@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;
}
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}

View File

@@ -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<WellKnownDebugProvider> loadAll(String fileExt) {
List<ResourceFile> files = Application.findFilesByExtensionInApplication(fileExt);
Set<WellKnownDebugProvider> seenProviders = new HashSet<>();
List<WellKnownDebugProvider> results = new ArrayList<>();
for (ResourceFile file : files) {
try {
List<String> 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;
}
}

View File

@@ -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);

View File

@@ -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.
* <p>
* 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));
}
}

View File

@@ -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());
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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()));
}
}

View File

@@ -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<SymbolFileLocation> 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<SymbolFileLocation> 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);
}
}
}

View File

@@ -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.
* <p>
* 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;
}
}

View File

@@ -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);
}

View File

@@ -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";
}

View File

@@ -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 </var/tmp/user1-ghidra/debuginfo-cache>");
LocalDirDebugInfoDProvider homeCache =
LocalDirDebugInfoDProvider.getUserHomeCacheInstance();
homeCache = new LocalDirDebugInfoDProvider(homeCache.getRootDir(), homeCache.getName(),
"DebugInfoD Cache Dir </home/user1/.cache/debuginfod_client>");
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);
}
}