mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-05-31 16:08:27 +08:00
GP-5924 DWARF debuginfod support
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
+8
-7
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-33
@@ -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="->" 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"> (Add) Adds a location. See <a href="#LocationTypes">Debug location types</a></LI>
|
||||||
|
<LI><A name="Delete"></A><img border="0" src="images/error.png"> (Delete) Deletes the highlighted row</LI>
|
||||||
|
<LI><A name="UpDown"></A><img border="0" src="images/up.png"><img border="0" src="images/down.png"> (Up/Down) Moves the highlighted row up or down</LI>
|
||||||
|
<LI><A name="Refresh"></A><img border="0" src="images/reload3.png"> (Refresh) Updates the status of all rows</LI>
|
||||||
|
<LI><A name="Save"></A><img border="0" src="images/disk.png"> (Save) Saves the current information</LI>
|
||||||
|
</UL>
|
||||||
|
</LI>
|
||||||
|
</UL>
|
||||||
|
|
||||||
<P> </P>
|
<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> </P>
|
||||||
|
<BR>
|
||||||
|
<BR>
|
||||||
|
<BR>
|
||||||
|
</BODY>
|
||||||
</HTML>
|
</HTML>
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
+9
-9
@@ -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>»chevrons«</b></P>
|
the original value, bracketed with <b>»chevrons«</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="->" border="0">
|
<b>Edit <IMG src="help/shared/arrow.gif" alt="->" 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);
|
||||||
|
|||||||
+3
-2
@@ -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;
|
||||||
|
|||||||
+9
-12
@@ -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<>();
|
||||||
|
|
||||||
|
|||||||
+101
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
-97
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
+7
-84
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-21
@@ -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();
|
|
||||||
}
|
}
|
||||||
Vendored
+32
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
Vendored
+41
@@ -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);
|
||||||
|
}
|
||||||
+26
@@ -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) {}
|
||||||
+110
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -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
|
||||||
|
}
|
||||||
+37
@@ -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;
|
||||||
|
}
|
||||||
+76
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+146
-23
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+66
-19
@@ -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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+246
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+369
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
-34
@@ -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;
|
||||||
}
|
}
|
||||||
Vendored
+24
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+121
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
-99
@@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-52
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-112
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+69
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+601
File diff suppressed because it is too large
Load Diff
+255
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+207
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -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);
|
||||||
|
}
|
||||||
+71
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+12
-11
@@ -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));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-87
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
+57
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+201
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+115
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1
-1
@@ -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
Reference in New Issue
Block a user