GP-5924 DWARF debuginfod support

This commit is contained in:
dev747368
2025-09-17 16:37:40 +00:00
parent e4e2df4a09
commit fe5ea1c91a
51 changed files with 3861 additions and 708 deletions
@@ -1,6 +1,7 @@
##VERSION: 2.0 ##VERSION: 2.0
Module.manifest||GHIDRA||||END| Module.manifest||GHIDRA||||END|
README.md||GHIDRA||||END| README.md||GHIDRA||||END|
data/DWARF.debuginfod_urls||GHIDRA||||END|
data/PDB_SYMBOL_SERVER_URLS.pdburl||GHIDRA||||END| data/PDB_SYMBOL_SERVER_URLS.pdburl||GHIDRA||||END|
src/global/docs/ChangeHistory.md||GHIDRA||||END| src/global/docs/ChangeHistory.md||GHIDRA||||END|
src/global/docs/WhatsNew.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/console.html||GHIDRA||||END|
src/main/help/help/topics/ConsolePlugin/images/Console.png||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/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/Data.htm||GHIDRA||||END|
src/main/help/help/topics/DataPlugin/images/CreateStructureDialog.png||GHIDRA||||END| src/main/help/help/topics/DataPlugin/images/CreateStructureDialog.png||GHIDRA||||END|
src/main/help/help/topics/DataPlugin/images/CreateStructureDialogWithTableSelection.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 // Note that you can run this script on a program that has already been analyzed by the
// DWARF analyzer. // DWARF analyzer.
//@category DWARF //@category DWARF
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import ghidra.app.script.GhidraScript; import ghidra.app.script.GhidraScript;
import ghidra.app.util.bin.BinaryReader; import ghidra.app.util.bin.BinaryReader;
import ghidra.app.util.bin.format.dwarf.*; 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;
import ghidra.app.util.bin.format.dwarf.line.DWARFLine.SourceFileAddr; import ghidra.app.util.bin.format.dwarf.line.DWARFLine.SourceFileAddr;
import ghidra.app.util.bin.format.dwarf.line.DWARFLine.SourceFileInfo; 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"); popup("Unable to get reader for debug line info");
return; return;
} }
ExternalDebugInfo extDebugInfo = ExternalDebugInfo.fromProgram(dprog.getGhidraProgram());
boolean hasBuildId = extDebugInfo != null && extDebugInfo.hasBuildId();
ExternalDebugFilesService edfs =
ExternalDebugFilesService.forProgram(dprog.getGhidraProgram());
int entryCount = 0; int entryCount = 0;
List<DWARFCompilationUnit> compUnits = dprog.getCompilationUnits(); List<DWARFCompilationUnit> compUnits = dprog.getCompilationUnits();
SourceFileManager sourceManager = currentProgram.getSourceFileManager(); SourceFileManager sourceManager = currentProgram.getSourceFileManager();
@@ -103,6 +110,14 @@ public class DWARFLineInfoSourceMapScript extends GhidraScript {
SourceFile sFile = new SourceFile(path, type, sfi.md5()); SourceFile sFile = new SourceFile(path, type, sfi.md5());
sourceManager.addSourceFile(sFile); sourceManager.addSourceFile(sFile);
sourceFileInfoToSourceFile.put(sfi, 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) { catch (IllegalArgumentException e) {
if (numErrors++ < MAX_ERROR_MSGS_TO_DISPLAY) { if (numErrors++ < MAX_ERROR_MSGS_TO_DISPLAY) {
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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); Msg.warn(this, "Invalid DWARF external debug files location specified: " + dir);
return; return;
} }
List<SearchLocation> searchLocations = new ArrayList<>(); List<DebugInfoProvider> searchLocations = new ArrayList<>();
File buildIdDir = new File(dir, ".build-id"); File buildIdDir = new File(dir, ".build-id");
if (buildIdDir.isDirectory()) { if (buildIdDir.isDirectory()) {
searchLocations.add(new BuildIdSearchLocation(buildIdDir)); searchLocations.add(new BuildIdDebugFileProvider(buildIdDir));
} }
searchLocations.add(new LocalDirectorySearchLocation(dir)); searchLocations.add(new LocalDirDebugLinkProvider(dir));
ExternalDebugFilesService edfs = new ExternalDebugFilesService(searchLocations); ExternalDebugFilesService edfs = new ExternalDebugFilesService(
DWARFExternalDebugFilesPlugin.saveExternalDebugFilesService(edfs); LocalDirDebugInfoDProvider.getGhidraCacheInstance(), searchLocations);
ExternalDebugFilesService.saveToPrefs(edfs);
} }
} }
@@ -1,43 +1,75 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN"> <!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML> <HTML>
<HEAD> <HEAD>
<TITLE>DWARF External Debug Files</TITLE> <TITLE>DWARF External Debug Files</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=utf-8"> <META http-equiv="Content-Type" content="text/html; charset=utf-8">
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css"> <LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD> </HEAD>
<BODY lang="EN-US"> <BODY lang="EN-US">
<H1>DWARF External Debug Files</H1> <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 <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 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 information embedded in the original binary's <b>".gnu_debuglink"</b> section (a filename and
".note.gnu.build-id" section (a hash value).</P> crc32) and/or <b>".note.gnu.build-id"</b> 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>
<BLOCKQUOTE> <P>Use the <code>ExtractELFDebugFilesScript</code> to pull external debug files from
<P align="left">Allows the user to pick a directory where Ghidra will search for DWARF external debug files.</P> pre-packaged install files, typically provided by Linux / BSD distributions, for later
<P align="left">Ghidra will search for external debug files under the selected directory consumption by Ghidra.</P>
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> <P>The DWARF analyzer will use the configured external debug file locations to search for
</BLOCKQUOTE> debug files when it encounters a binary that has external debug information and is missing its
</BLOCKQUOTE> <b>.debug_info</b> sections.</P>
<h2><a name="Configuration"></a>Configuration</h2>
<P>See <b>Edit <IMG src="help/shared/arrow.gif" alt="-&gt;" border="0"> DWARF External Debug Config</b></P>
<P align="center"><IMG border="0" src="images/ExternalDebugFilesConfigDialog.png"></P>
<P class="providedbyplugin"><BR> <UL>
Provided by: <I>DWARF External Debug Files Plugin</I></P> <LI><a name="LocalStorage"></a><b>Local Storage</b> - the location where files downloaded
from remote debuginfod servers will be stored. This defaults to a Ghidra specific cache
directory, but can be changed to debuginfod's cache directory, or any other location.</LI>
<LI><b>Additional Locations</b> - a list of locations to search when trying to find
a debug file.
<h3><a name="ButtonActions"></a>Button actions:</h3>
<UL>
<LI><A name="Add"></A><img border="0" src="images/Plus2.png">&nbsp;(Add) Adds a location. See <a href="#LocationTypes">Debug location types</a></LI>
<LI><A name="Delete"></A><img border="0" src="images/error.png">&nbsp;(Delete) Deletes the highlighted row</LI>
<LI><A name="UpDown"></A><img border="0" src="images/up.png"><img border="0" src="images/down.png">&nbsp;(Up/Down) Moves the highlighted row up or down</LI>
<LI><A name="Refresh"></A><img border="0" src="images/reload3.png">&nbsp;(Refresh) Updates the status of all rows</LI>
<LI><A name="Save"></A><img border="0" src="images/disk.png">&nbsp;(Save) Saves the current information</LI>
</UL>
</LI>
</UL>
<P>&nbsp;</P> <h3><a name="LocationTypes"></a>Debug location types:</h3>
<BR> <UL>
<BR> <LI><b>Program's Import Location</b> - searchs the directory from which the program was
<BR> imported for any debug-link specified files, and for build-id specified files named
</BODY> <code>aabbcc...zz.debug</code>, where <code>aa..zz</codE> is the build-id hash in hex.</LI>
<LI><b>Build-id Directory</b> - directory where debug files that are identified by a
build-id hash are stored.<br>
Debug files are named <code>aa/bbccdd...zz.debug</code> under the base directory<br>
This storage scheme for build-id debug files is distinct from debuginfod's scheme.<br><br>
Example: <code>/usr/lib/debug/.build-id</code></LI>
<LI><b>Debug Link Directory</b> - directory where debug files that are identified by a
debug filename and crc hash (found in the binary's .gnu_debuglink section).<br>
<b>NOTE</b>: This directory is searched recursively for a matching file.</LI>
<LI><b>Debuginfod Directory</b> - directory where debuginfod has stored files. This
typically will be something like <code>/home/user/.cache/debuginfod_client</code>.</LI>
<LI><b>Debuginfod URL</b> - HTTP(s) URL that points to a debuginfod server.</LI>
<LI><b>Import DEBUGINFOD_URLS Env Var</b> - Helper action that adds any HTTP(s) URLs found
in debuginfod's environment variable.</LI>
</UL>
<P class="providedbyplugin"><BR>
Provided by: <I>DWARF External Debug Files Plugin</I></P>
<P>&nbsp;</P>
<BR>
<BR>
<BR>
</BODY>
</HTML> </HTML>
@@ -11,7 +11,7 @@
</HEAD> </HEAD>
<BODY lang="EN-US"> <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> <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 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 <P>LibreTranslate (currently hosted at libretranslate.com) is an independant project that
provides an open source translation package that can be self-hosted.</P> 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 <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 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 <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 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> <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 <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> 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 <P>When a string has been translated, the translated value will be shown in place of
the original value, bracketed with <b>&#x00BB;chevrons&#x00AB;</b></P> the original value, bracketed with <b>&#x00BB;chevrons&#x00AB;</b></P>
<h2><a name="Configuration"></a>Configuration</h2> <h2><a name="Configuration"></a>Configuration</h2>
<P>See <P>See
<b>Edit <IMG src="help/shared/arrow.gif" alt="-&gt;" border="0"> <b>Edit <IMG src="help/shared/arrow.gif" alt="-&gt;" border="0">
@@ -41,7 +41,7 @@
Strings | LibreTranslate</b> Strings | LibreTranslate</b>
</P> </P>
<blockquote> <blockquote>
<UL> <UL>
<LI><b>URL</b> - required. Example: <b>http://localhost:5000/</b> <LI><b>URL</b> - required. Example: <b>http://localhost:5000/</b>
(if self hosted and following suggested values)</LI> (if self hosted and following suggested values)</LI>
<LI><b>API Key</b> - a unique key that authorizes you to connect to the LibreTranslate <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) { catch (CancelledException ce) {
throw 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) { catch (IOException e) {
log.appendMsg("Error during DWARFAnalyzer import: " + e); log.appendMsg("Error during DWARFAnalyzer import: " + e);
Msg.error(this, "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"; "Copy External Debug File Symbols";
private static final String OPTION_COPY_EXTERNAL_DEBUG_FILE_SYMBOLS_DESC = 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 " + "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 = "Debug Strings Charset";
private static final String OPTION_CHARSET_NAME_DESC = """ 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. * Used to control which macro info entries are used to create enums.
*/ */
public static enum MacroEnumSetting { public enum MacroEnumSetting {
NONE, NONE,
IGNORE_COMMAND_LINE, IGNORE_COMMAND_LINE,
ALL; ALL;
@@ -83,16 +83,13 @@ public class DWARFProgram implements Closeable {
public static boolean isDWARF(Program program) { public static boolean isDWARF(Program program) {
String format = Objects.requireNonNullElse(program.getExecutableFormat(), ""); String format = Objects.requireNonNullElse(program.getExecutableFormat(), "");
switch (format) { return switch (format) {
case ElfLoader.ELF_NAME: case ElfLoader.ELF_NAME, PeLoader.PE_NAME -> hasExpectedDWARFSections(program) ||
case PeLoader.PE_NAME: ExternalDebugInfo.fromProgram(program) != null;
return hasExpectedDWARFSections(program) || case MachoLoader.MACH_O_NAME -> hasExpectedDWARFSections(program) ||
ExternalDebugInfo.fromProgram(program) != null; DSymSectionProvider.getDSYMForProgram(program) != null;
case MachoLoader.MACH_O_NAME: default -> false;
return hasExpectedDWARFSections(program) || };
DSymSectionProvider.getDSYMForProgram(program) != null;
}
return false;
} }
private static boolean hasExpectedDWARFSections(Program program) { private static boolean hasExpectedDWARFSections(Program program) {
@@ -193,8 +190,8 @@ public class DWARFProgram implements Closeable {
protected WeakValueHashMap<Long, DebugInfoEntry> diesByOffset = new WeakValueHashMap<>(); protected WeakValueHashMap<Long, DebugInfoEntry> diesByOffset = new WeakValueHashMap<>();
private WeakValueHashMap<Long, DIEAggregate> aggsByOffset = new WeakValueHashMap<>(); private WeakValueHashMap<Long, DIEAggregate> aggsByOffset = new WeakValueHashMap<>();
// Map of DIE offsets of {@link DIEAggregate}s that are being pointed to by // Map of DIE offsets of DIEAggregates that are being pointed to by
// other {@link DIEAggregate}s with a DW_AT_type property. // other DIEAggregates with a DW_AT_type property.
// In other words, a map of inbound links to a DIEA. // In other words, a map of inbound links to a DIEA.
private ListValuedMap<Long, Long> typeReferers = new ArrayListValuedHashMap<>(); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,20 +15,14 @@
*/ */
package ghidra.app.util.bin.format.dwarf.external; 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.action.builder.ActionBuilder;
import docking.tool.ToolConstants; import docking.tool.ToolConstants;
import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
import ghidra.app.CorePluginPackage; import ghidra.app.CorePluginPackage;
import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.util.bin.format.dwarf.external.gui.ExternalDebugFilesConfigDialog;
import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus; import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.framework.preferences.Preferences; import ghidra.util.HelpLocation;
//@formatter:off //@formatter:off
@PluginInfo( @PluginInfo(
@@ -40,10 +34,8 @@ import ghidra.framework.preferences.Preferences;
) )
//@formatter:on //@formatter:on
public class DWARFExternalDebugFilesPlugin extends Plugin { 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) { public DWARFExternalDebugFilesPlugin(PluginTool tool) {
super(tool); super(tool);
@@ -55,78 +47,9 @@ public class DWARFExternalDebugFilesPlugin extends Plugin {
new ActionBuilder("DWARF External Debug Config", this.getName()) new ActionBuilder("DWARF External Debug Config", this.getName())
.menuPath(ToolConstants.MENU_EDIT, "DWARF External Debug Config") .menuPath(ToolConstants.MENU_EDIT, "DWARF External Debug Config")
.menuGroup(ToolConstants.TOOL_OPTIONS_MENU_GROUP) .menuGroup(ToolConstants.TOOL_OPTIONS_MENU_GROUP)
.onAction(ac -> showConfigDialog()) .onAction(ac -> ExternalDebugFilesConfigDialog.show())
.helpLocation(new HelpLocation(HELP_TOPIC, "Configuration"))
.buildAndInstall(tool); .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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,40 +15,28 @@
*/ */
package ghidra.app.util.bin.format.dwarf.external; package ghidra.app.util.bin.format.dwarf.external;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import ghidra.formats.gfilesystem.FSRL;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; 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 debugInfo search criteria
* @param monitor {@link TaskMonitor} * @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 IOException if error
* @throws CancelledException if cancelled * @throws CancelledException if cancelled
*/ */
FSRL findDebugFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) File getFile(ExternalDebugInfo debugInfo, TaskMonitor monitor)
throws IOException, CancelledException; 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,55 +15,88 @@
*/ */
package ghidra.app.util.bin.format.dwarf.external; package ghidra.app.util.bin.format.dwarf.external;
import java.io.File;
import java.io.IOException; 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.exception.CancelledException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
/** /**
* A collection of {@link SearchLocation search locations} that can be queried to find a * A collection of {@link DebugFileProvider providers} that can be queried to find a
* DWARF external debug file, which is a second ELF binary that contains the debug information * DWARF external debug file. Typically this will be an ELF binary that contains the debug
* that was stripped from the original ELF binary. * 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 { 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) { public ExternalDebugFilesService(DebugFileStorage storage, List<DebugInfoProvider> providers) {
this.searchLocations = searchLocations; 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() { public List<DebugInfoProvider> getProviders() {
return searchLocations; 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. * 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 debugInfo information about the external debug file
* @param monitor {@link TaskMonitor} * @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 * @throws IOException if error
*/ */
public FSRL findDebugFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) public File find(ExternalDebugInfo debugInfo, TaskMonitor monitor) throws IOException {
throws IOException {
try { try {
for (SearchLocation searchLoc : searchLocations) { for (DebugInfoProvider provider : providers) {
monitor.checkCancelled(); 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) { if (result != null) {
return result; return result;
} }
@@ -75,4 +108,94 @@ public class ExternalDebugFilesService {
return null; 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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; String filename = debugLink != null ? debugLink.getFilename() : null;
int crc = debugLink != null ? debugLink.getCrc() : 0; int crc = debugLink != null ? debugLink.getCrc() : 0;
byte[] hash = buildId != null ? buildId.getDescription() : null; String hash = buildId != null
return new ExternalDebugInfo(filename, crc, hash); ? NumericUtilities.convertBytesToString(buildId.getDescription())
: null;
return new ExternalDebugInfo(filename, crc, hash, ObjectType.DEBUGINFO, null);
} }
private String filename; /**
private int crc; * {@return a new ExternalDebugInfo instance created using the specified Build-Id value}
private byte[] hash; * @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. * Constructor to create an {@link ExternalDebugInfo} instance.
* *
* @param filename filename of external debug file, or null * @param filename filename of external debug file, or null
* @param crc crc32 of external debug file, or 0 if no filename * @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 * 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.filename = filename;
this.crc = crc; 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 * @return boolean true if filename is available, false if not
*/ */
public boolean hasFilename() { public boolean hasDebugLink() {
return filename != null && !filename.isBlank(); 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() { public String getBuildId() {
return hash; 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 @Override
public String toString() { public String toString() {
return String.format("ExternalDebugInfo [filename=%s, crc=%s, hash=%s]", return String.format(
filename, "ExternalDebugInfo [filename=%s, crc=%s, hash=%s, objectType=%s, extra=%s]", filename,
Integer.toHexString(crc), crc, buildId, objectType, extra);
NumericUtilities.convertBytesToString(hash));
} }
} }
@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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.io.*;
import java.util.zip.CRC32; import java.util.zip.CRC32;
import ghidra.formats.gfilesystem.FSRL;
import ghidra.formats.gfilesystem.FileSystemService;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
/** /**
* A {@link SearchLocation} that recursively searches for dwarf external debug files * Searches for DWARF external debug files specified via a debug-link filename / crc in a directory.
* under a configured 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 * @param name string to test
* @return boolean true if locString specifies a local dir search location * @return boolean true if name specifies a LocalDirDebugLinkProvider name
*/ */
public static boolean isLocalDirSearchLoc(String locString) { public static boolean matches(String name) {
return locString.startsWith(LOCAL_DIR_PREFIX); 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 name string, earlier returned from {@link #getName()}
* @param context {@link SearchLocationCreatorContext} to allow accessing information outside * @param context {@link DebugInfoProviderCreatorContext} to allow accessing information outside
* of the location string that might be needed to create a new instance * of the name string that might be needed to create a new instance
* @return new {@link LocalDirectorySearchLocation} instance * @return new {@link LocalDirDebugLinkProvider} instance
*/ */
public static LocalDirectorySearchLocation create(String locString, public static LocalDirDebugLinkProvider create(String name,
SearchLocationCreatorContext context) { DebugInfoProviderCreatorContext context) {
locString = locString.substring(LOCAL_DIR_PREFIX.length()); String dir = name.substring(DEBUGLINK_NAME_PREFIX.length());
return new LocalDirectorySearchLocation(new File(locString)); return new LocalDirDebugLinkProvider(new File(dir));
} }
private final File searchDir; 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 * @param searchDir path to the root directory of where to search
*/ */
public LocalDirectorySearchLocation(File searchDir) { public LocalDirDebugLinkProvider(File searchDir) {
this.searchDir = searchDir; this.searchDir = searchDir;
} }
@Override @Override
public String getName() { public String getName() {
return LOCAL_DIR_PREFIX + searchDir.getPath(); return DEBUGLINK_NAME_PREFIX + searchDir.getPath();
} }
@Override @Override
public String getDescriptiveName() { public String getDescriptiveName() {
return searchDir.getPath(); return searchDir.getPath() + " (debug-link dir)";
} }
@Override @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 { throws CancelledException, IOException {
if (!debugInfo.hasFilename()) { if (!debugInfo.hasDebugLink() || !isValid()) {
return null; return null;
} }
ensureSafeFilename(debugInfo.getFilename()); 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 { throws IOException, CancelledException {
if (!debugInfo.hasFilename()) { if (!debugInfo.hasDebugLink()) {
return null; return null;
} }
File file = new File(dir, debugInfo.getFilename()); File file = new File(dir, debugInfo.getFilename());
if (file.isFile()) { if (file.isFile()) {
int fileCRC = calcCRC(file); int fileCRC = calcCRC(file);
if (fileCRC == debugInfo.getCrc()) { if (fileCRC == debugInfo.getCrc()) {
return FileSystemService.getInstance().getLocalFSRL(file); return file; // success
} }
Msg.info(this, "DWARF external debug file found with mismatching crc, ignored: " + Msg.info(this,
file + ", (" + Integer.toHexString(fileCRC) + ")"); "DWARF external debug file found with mismatching crc, ignored: %s (%08x)"
.formatted(file, fileCRC));
} }
File[] subDirs; File[] subDirs;
if ((subDirs = dir.listFiles(f -> f.isDirectory())) != null) { if ((subDirs = dir.listFiles(f -> f.isDirectory())) != null) {
// TODO: prevent recursing into symlinks?
for (File subDir : subDirs) { for (File subDir : subDirs) {
FSRL result = findFile(subDir, debugInfo, monitor); File result = findFile(subDir, debugInfo, monitor);
if (result != null) { if (result != null) {
return result; return result;
} }
@@ -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,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; package ghidra.app.util.bin.format.dwarf.sectionprovider;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.nio.file.AccessMode;
import java.util.List; import java.util.List;
import ghidra.app.util.Option; import ghidra.app.util.Option;
import ghidra.app.util.bin.ByteProvider; 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.importer.MessageLog;
import ghidra.app.util.opinion.*; import ghidra.app.util.opinion.*;
import ghidra.app.util.opinion.Loader.ImporterSettings; 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.framework.options.Options;
import ghidra.plugin.importer.ImporterUtilities; import ghidra.plugin.importer.ImporterUtilities;
import ghidra.program.database.ProgramDB; import ghidra.program.database.ProgramDB;
@@ -56,20 +61,16 @@ public class ExternalDebugFileSectionProvider extends BaseSectionProvider {
} }
Msg.info(ExternalDebugFileSectionProvider.class, Msg.info(ExternalDebugFileSectionProvider.class,
"DWARF external debug information found: " + extDebugInfo); "DWARF external debug information found: " + extDebugInfo);
ExternalDebugFilesService edfs = ExternalDebugFilesService edfs = ExternalDebugFilesService.forProgram(program);
DWARFExternalDebugFilesPlugin.getExternalDebugFilesService( File extDebugFile = edfs.find(extDebugInfo, monitor);
SearchLocationRegistry.getInstance().newContext(program));
FSRL extDebugFile = edfs.findDebugFile(extDebugInfo, monitor);
if (extDebugFile == null) { if (extDebugFile == null) {
return null; return null;
} }
Msg.info(ExternalDebugFileSectionProvider.class, Msg.info(ExternalDebugFileSectionProvider.class,
"DWARF External Debug File: found: " + extDebugFile); "DWARF External Debug File: found: " + extDebugFile);
FileSystemService fsService = FileSystemService.getInstance(); FSRL fsrl = FileSystemService.getInstance().getLocalFSRL(extDebugFile);
try ( try (ByteProvider debugFileByteProvider =
RefdFile refdDebugFile = fsService.getRefdFile(extDebugFile, monitor); new FileByteProvider(extDebugFile, fsrl, AccessMode.READ)) {
ByteProvider debugFileByteProvider =
fsService.getByteProvider(refdDebugFile.file.getFSRL(), false, monitor);) {
Object consumer = new Object(); Object consumer = new Object();
Language lang = program.getLanguage(); Language lang = program.getLanguage();
LoadSpec origLoadSpec = ImporterUtilities.getLoadSpec(program); 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.LibreTranslatePlugin.SOURCE_LANGUAGE_OPTION.*;
import static ghidra.app.plugin.core.string.translate.libretranslate.LibreTranslateStringTranslationService.*; 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 java.net.HttpURLConnection.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.io.IOException; import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@@ -30,7 +31,8 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import com.google.gson.*; 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.AbstractErrDialog;
import docking.widgets.SelectFromListDialog; import docking.widgets.SelectFromListDialog;
@@ -43,7 +45,6 @@ import ghidra.program.model.listing.Data;
import ghidra.program.util.ProgramLocation; import ghidra.program.util.ProgramLocation;
import ghidra.test.AbstractProgramBasedTest; import ghidra.test.AbstractProgramBasedTest;
import ghidra.test.ToyProgramBuilder; import ghidra.test.ToyProgramBuilder;
import ghidra.util.Msg;
import ghidra.util.Swing; import ghidra.util.Swing;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
@@ -54,7 +55,6 @@ import ghidra.util.task.TaskMonitor;
*/ */
public class LibreTranslateStringTranslationServiceTest extends AbstractProgramBasedTest { public class LibreTranslateStringTranslationServiceTest extends AbstractProgramBasedTest {
private static int LAST_SERVER_PORT_NUM = 8000 + 5000;
private int supportedLanguageCount = 10; private int supportedLanguageCount = 10;
private AtomicInteger translateRequestCount = new AtomicInteger(); // number of times translate handler has been invoked 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 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 // test what happens when the server accepts requests on the REST api endpoint URL, but
// returns unexpected json values // returns unexpected json values
HttpServer server = createMockHttpServer(false); HttpServer server = createMockHttpServer();
server.createContext("/", this::mockUnexpectedJsonResultHandler); server.createContext("/", this::mockUnexpectedJsonResultHandler);
try { 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 // test what happens when the server accepts requests on the REST api endpoint URL, but its
// not json // not json
HttpServer server = createMockHttpServer(false); HttpServer server = createMockHttpServer();
server.createContext("/", this::mockUnexpectedTextResultHandler); server.createContext("/", this::mockUnexpectedTextResultHandler);
try { try {
@@ -149,7 +149,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB
// test what happens when the URL doesn't point to active server // test what happens when the URL doesn't point to active server
LibreTranslateStringTranslationService sts = new LibreTranslateStringTranslationService( 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 setErrorsExpected(true); // don't kill the test because Msg.showError() was called somewhere
Swing.runNow(() -> sts.translate(program, List.of(progLoc(0)), TranslateOptions.NONE)); Swing.runNow(() -> sts.translate(program, List.of(progLoc(0)), TranslateOptions.NONE));
@@ -346,62 +346,8 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB
return builder.getProgram(); 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 { private void mockLangHandler(HttpExchange httpExchange) throws IOException {
assertContentType(httpExchange, CONTENT_TYPE_JSON); assertContentType(CONTENT_TYPE_JSON, httpExchange);
try { try {
JsonArray langsResult = new JsonArray(); JsonArray langsResult = new JsonArray();
for (int i = 0; i < supportedLanguageCount; i++) { for (int i = 0; i < supportedLanguageCount; i++) {
@@ -427,7 +373,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB
try { try {
translateRequestCount.incrementAndGet(); translateRequestCount.incrementAndGet();
assertContentType(httpExchange, CONTENT_TYPE_JSON); assertContentType(CONTENT_TYPE_JSON, httpExchange);
String requestBody = String requestBody =
new String(httpExchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); new String(httpExchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
@@ -438,7 +384,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB
translateSourceLangs.add(sourceLang); translateSourceLangs.add(sourceLang);
log(httpExchange, logMockHttp(httpExchange,
"request src=%s, strs=%s".formatted(sourceLang, queryStrs.toString())); "request src=%s, strs=%s".formatted(sourceLang, queryStrs.toString()));
JsonObject xlateResultObj = new JsonObject(); JsonObject xlateResultObj = new JsonObject();
@@ -447,7 +393,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB
for (int i = 0; i < queryStrs.size(); i++) { for (int i = 0; i < queryStrs.size(); i++) {
xlatedResults.add("result" + translateStringCount.getAndIncrement()); xlatedResults.add("result" + translateStringCount.getAndIncrement());
} }
log(httpExchange, "response: " + xlateResultObj); logMockHttp(httpExchange, "response: " + xlateResultObj);
byte[] response = xlateResultObj.toString().getBytes(); byte[] response = xlateResultObj.toString().getBytes();
httpExchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON); httpExchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON);
@@ -455,7 +401,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB
httpExchange.getResponseBody().write(response); httpExchange.getResponseBody().write(response);
} }
catch (Throwable th) { catch (Throwable th) {
log(httpExchange, "Error during mockTranslateHandler: " + th.getMessage()); logMockHttp(httpExchange, "Error during mockTranslateHandler: " + th.getMessage());
throw th; throw th;
} }
finally { finally {
@@ -484,27 +430,6 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB
httpExchange.close(); 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) { private ProgramLocation progLoc(int stringNum) {
return new ProgramLocation(program, strings.get(stringNum).getAddress()); 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,17 +15,26 @@
*/ */
package pdb.symbolserver; package pdb.symbolserver;
import static ghidra.test.MockHttpServerUtils.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.List; import java.util.List;
import org.junit.Test;
import com.sun.net.httpserver.HttpServer;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
public class HttpSymbolServerTest { public class HttpSymbolServerTest {
private TaskMonitor monitor = TaskMonitor.DUMMY;
//@Test //@Test
public void test() { public void testMSFTSymbolServer() {
// This test is not enabled by default as it depends on an third-party resource // This test is not enabled by default as it depends on an third-party resource
HttpSymbolServer httpSymbolServer = HttpSymbolServer httpSymbolServer =
new HttpSymbolServer(URI.create("http://msdl.microsoft.com/download/symbols/")); new HttpSymbolServer(URI.create("http://msdl.microsoft.com/download/symbols/"));
@@ -37,4 +46,68 @@ public class HttpSymbolServerTest {
assertEquals(1, results.size()); 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 * @throws FileNotFoundException if Java's user home directory is not defined or it is not an
* absolute path * absolute path
*/ */
private static File getJavaUserHomeDir() throws FileNotFoundException { public static File getJavaUserHomeDir() throws FileNotFoundException {
return getSystemPropertyFile("user.home", true); return getSystemPropertyFile("user.home", true);
} }
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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 = "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 * $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. * 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. * access to it. Its Unix access mode MUST be 0700.
*/ */
public static final String XDG_RUNTIME_DIR = "XDG_RUNTIME_DIR"; public static final String XDG_RUNTIME_DIR = "XDG_RUNTIME_DIR";
} }

Some files were not shown because too many files have changed in this diff Show More