mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-01-08 21:47:59 -05:00
Merge remote-tracking branch 'origin/GP-5924_dev747368_dwarf_debuginfod'
(Closes #8407)
This commit is contained in:
@@ -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|
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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="->" 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"> (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"> (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"> (Up/Down) Moves the highlighted row up or down</LI>
|
||||
<LI><A name="Refresh"></A><img border="0" src="images/reload3.png"> (Refresh) Updates the status of all rows</LI>
|
||||
<LI><A name="Save"></A><img border="0" src="images/disk.png"> (Save) Saves the current information</LI>
|
||||
</UL>
|
||||
</LI>
|
||||
</UL>
|
||||
|
||||
<P> </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> </P>
|
||||
<BR>
|
||||
<BR>
|
||||
<BR>
|
||||
</BODY>
|
||||
</HTML>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -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>»chevrons«</b></P>
|
||||
|
||||
<P>When a string has been translated, the translated value will be shown in place of
|
||||
the original value, bracketed with <b>»chevrons«</b></P>
|
||||
|
||||
<h2><a name="Configuration"></a>Configuration</h2>
|
||||
<P>See
|
||||
<b>Edit <IMG src="help/shared/arrow.gif" alt="->" 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
24
Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ObjectType.java
vendored
Normal file
24
Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ObjectType.java
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user