From fe5ea1c91a9902abf68af61486591c82b5fe9124 Mon Sep 17 00:00:00 2001 From: dev747368 <48332326+dev747368@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:37:40 +0000 Subject: [PATCH] GP-5924 DWARF debuginfod support --- .../Public_Release/certification.manifest | 1 + .../Public_Release/data/DWARF.debuginfod_urls | 8 + Ghidra/Features/Base/certification.manifest | 1 + .../DWARFLineInfoSourceMapScript.java | 15 + ...etExternalDebugFilesLocationPrescript.java | 15 +- .../DWARFExternalDebugFilesPlugin.html | 98 ++- .../images/ExternalDebugFilesConfigDialog.png | Bin 0 -> 20768 bytes .../LibreTranslatePlugin.htm | 18 +- .../plugin/core/analysis/DWARFAnalyzer.java | 4 + .../bin/format/dwarf/DWARFImportOptions.java | 5 +- .../util/bin/format/dwarf/DWARFProgram.java | 21 +- .../external/BuildIdDebugFileProvider.java | 101 +++ .../dwarf/external/BuildIdSearchLocation.java | 97 --- .../DWARFExternalDebugFilesPlugin.java | 91 +-- ...chLocation.java => DebugFileProvider.java} | 30 +- .../dwarf/external/DebugFileStorage.java | 32 + .../dwarf/external/DebugInfoProvider.java | 41 ++ .../DebugInfoProviderCreatorContext.java | 26 + .../external/DebugInfoProviderRegistry.java | 110 ++++ .../external/DebugInfoProviderStatus.java | 20 + .../dwarf/external/DebugStreamProvider.java | 37 ++ .../external/DisabledDebugInfoProvider.java | 76 +++ .../external/ExternalDebugFilesService.java | 169 ++++- .../dwarf/external/ExternalDebugInfo.java | 85 ++- .../external/HttpDebugInfoDProvider.java | 246 +++++++ .../external/LocalDirDebugInfoDProvider.java | 369 +++++++++++ ...on.java => LocalDirDebugLinkProvider.java} | 78 ++- .../bin/format/dwarf/external/ObjectType.java | 24 + .../external/SameDirDebugInfoProvider.java | 121 ++++ .../dwarf/external/SameDirSearchLocation.java | 99 --- .../SearchLocationCreatorContext.java | 52 -- .../external/SearchLocationRegistry.java | 112 ---- .../external/gui/EnumIconColumnRenderer.java | 69 ++ .../gui/ExternalDebugFilesConfigDialog.java | 601 ++++++++++++++++++ .../ExternalDebugInfoProviderTableModel.java | 255 ++++++++ .../ExternalDebugInfoProviderTableRow.java | 73 +++ .../dwarf/external/gui/FilePromptDialog.java | 207 ++++++ .../external/gui/TableColumnInitializer.java | 62 ++ .../external/gui/WellKnownDebugProvider.java | 71 +++ .../ExternalDebugFileSectionProvider.java | 23 +- .../java/ghidra/test/MockHttpServerUtils.java | 203 ++++++ ...TranslateStringTranslationServiceTest.java | 99 +-- .../BuildIdDebugFileProviderTest.java | 57 ++ .../external/HttpDebugInfoDProviderTest.java | 201 ++++++ .../LocalDirDebugInfoDProviderTest.java | 115 ++++ .../LocalDirDebugLinkProviderTest.java | 55 ++ .../symbolserver/HttpSymbolServerTest.java | 79 ++- .../ghidra/util/layout/ThreeColumnLayout.java | 135 ++++ .../application/ApplicationUtilities.java | 2 +- .../java/utility/application/XdgUtils.java | 7 +- ...RFExternalDebugFilesPluginScreenShots.java | 53 ++ 51 files changed, 3861 insertions(+), 708 deletions(-) create mode 100644 Ghidra/Configurations/Public_Release/data/DWARF.debuginfod_urls create mode 100644 Ghidra/Features/Base/src/main/help/help/topics/DWARFExternalDebugFilesPlugin/images/ExternalDebugFilesConfigDialog.png create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProvider.java delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdSearchLocation.java rename Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/{SearchLocation.java => DebugFileProvider.java} (54%) create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileStorage.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderCreatorContext.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderRegistry.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderStatus.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugStreamProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DisabledDebugInfoProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProvider.java rename Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/{LocalDirectorySearchLocation.java => LocalDirDebugLinkProvider.java} (53%) create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ObjectType.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirDebugInfoProvider.java delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirSearchLocation.java delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationCreatorContext.java delete mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationRegistry.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/EnumIconColumnRenderer.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugFilesConfigDialog.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableModel.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableRow.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/FilePromptDialog.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/TableColumnInitializer.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/WellKnownDebugProvider.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/test/MockHttpServerUtils.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProviderTest.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProviderTest.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProviderTest.java create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProviderTest.java create mode 100644 Ghidra/Framework/Gui/src/main/java/ghidra/util/layout/ThreeColumnLayout.java create mode 100644 Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DWARFExternalDebugFilesPluginScreenShots.java diff --git a/Ghidra/Configurations/Public_Release/certification.manifest b/Ghidra/Configurations/Public_Release/certification.manifest index 2dca2b3c20..5deffac084 100644 --- a/Ghidra/Configurations/Public_Release/certification.manifest +++ b/Ghidra/Configurations/Public_Release/certification.manifest @@ -1,6 +1,7 @@ ##VERSION: 2.0 Module.manifest||GHIDRA||||END| README.md||GHIDRA||||END| +data/DWARF.debuginfod_urls||GHIDRA||||END| data/PDB_SYMBOL_SERVER_URLS.pdburl||GHIDRA||||END| src/global/docs/ChangeHistory.md||GHIDRA||||END| src/global/docs/WhatsNew.md||GHIDRA||||END| diff --git a/Ghidra/Configurations/Public_Release/data/DWARF.debuginfod_urls b/Ghidra/Configurations/Public_Release/data/DWARF.debuginfod_urls new file mode 100644 index 0000000000..0beade30b1 --- /dev/null +++ b/Ghidra/Configurations/Public_Release/data/DWARF.debuginfod_urls @@ -0,0 +1,8 @@ +Internet|https://debuginfod.elfutils.org/|WARNING: Check your organization's security policy before downloading files from the internet. +Internet|https://debuginfod.fedoraproject.org/|WARNING: Check your organization's security policy before downloading files from the internet. +Internet|https://debuginfod.ubuntu.com/|WARNING: Check your organization's security policy before downloading files from the internet. +Internet|https://debuginfod.debian.net/|WARNING: Check your organization's security policy before downloading files from the internet. +Internet|https://debuginfod.opensuse.org/|WARNING: Check your organization's security policy before downloading files from the internet. +Internet|https://debuginfod.archlinux.org/|WARNING: Check your organization's security policy before downloading files from the internet. +Internet|https://debuginfod.centos.org/|WARNING: Check your organization's security policy before downloading files from the internet. + diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 74fa75a598..9c4f4d006c 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -316,6 +316,7 @@ src/main/help/help/topics/ComputeChecksumsPlugin/images/Dialog_Blank.png||GHIDRA src/main/help/help/topics/ConsolePlugin/console.html||GHIDRA||||END| src/main/help/help/topics/ConsolePlugin/images/Console.png||GHIDRA||||END| src/main/help/help/topics/DWARFExternalDebugFilesPlugin/DWARFExternalDebugFilesPlugin.html||GHIDRA||||END| +src/main/help/help/topics/DWARFExternalDebugFilesPlugin/images/ExternalDebugFilesConfigDialog.png||GHIDRA||||END| src/main/help/help/topics/DataPlugin/Data.htm||GHIDRA||||END| src/main/help/help/topics/DataPlugin/images/CreateStructureDialog.png||GHIDRA||||END| src/main/help/help/topics/DataPlugin/images/CreateStructureDialogWithTableSelection.png||GHIDRA||||END| diff --git a/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoSourceMapScript.java b/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoSourceMapScript.java index 1bd7e86625..588d505d5b 100644 --- a/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoSourceMapScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/DWARFLineInfoSourceMapScript.java @@ -19,12 +19,14 @@ // Note that you can run this script on a program that has already been analyzed by the // DWARF analyzer. //@category DWARF +import java.io.File; import java.io.IOException; import java.util.*; import ghidra.app.script.GhidraScript; import ghidra.app.util.bin.BinaryReader; import ghidra.app.util.bin.format.dwarf.*; +import ghidra.app.util.bin.format.dwarf.external.*; import ghidra.app.util.bin.format.dwarf.line.DWARFLine; import ghidra.app.util.bin.format.dwarf.line.DWARFLine.SourceFileAddr; import ghidra.app.util.bin.format.dwarf.line.DWARFLine.SourceFileInfo; @@ -76,6 +78,11 @@ public class DWARFLineInfoSourceMapScript extends GhidraScript { popup("Unable to get reader for debug line info"); return; } + ExternalDebugInfo extDebugInfo = ExternalDebugInfo.fromProgram(dprog.getGhidraProgram()); + boolean hasBuildId = extDebugInfo != null && extDebugInfo.hasBuildId(); + ExternalDebugFilesService edfs = + ExternalDebugFilesService.forProgram(dprog.getGhidraProgram()); + int entryCount = 0; List compUnits = dprog.getCompilationUnits(); SourceFileManager sourceManager = currentProgram.getSourceFileManager(); @@ -103,6 +110,14 @@ public class DWARFLineInfoSourceMapScript extends GhidraScript { SourceFile sFile = new SourceFile(path, type, sfi.md5()); sourceManager.addSourceFile(sFile); sourceFileInfoToSourceFile.put(sfi, sFile); + if (hasBuildId) { + ExternalDebugInfo srcFileDebugInfo = + extDebugInfo.withType(ObjectType.SOURCE, path); + File srcFile = edfs.find(srcFileDebugInfo, monitor); + if (srcFile != null) { + println("Source file: " + srcFile); + } + } } catch (IllegalArgumentException e) { if (numErrors++ < MAX_ERROR_MSGS_TO_DISPLAY) { diff --git a/Ghidra/Features/Base/ghidra_scripts/DWARFSetExternalDebugFilesLocationPrescript.java b/Ghidra/Features/Base/ghidra_scripts/DWARFSetExternalDebugFilesLocationPrescript.java index dcde006cc2..177ae956fa 100644 --- a/Ghidra/Features/Base/ghidra_scripts/DWARFSetExternalDebugFilesLocationPrescript.java +++ b/Ghidra/Features/Base/ghidra_scripts/DWARFSetExternalDebugFilesLocationPrescript.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -41,15 +41,16 @@ public class DWARFSetExternalDebugFilesLocationPrescript extends GhidraScript { Msg.warn(this, "Invalid DWARF external debug files location specified: " + dir); return; } - List searchLocations = new ArrayList<>(); + List searchLocations = new ArrayList<>(); File buildIdDir = new File(dir, ".build-id"); if (buildIdDir.isDirectory()) { - searchLocations.add(new BuildIdSearchLocation(buildIdDir)); + searchLocations.add(new BuildIdDebugFileProvider(buildIdDir)); } - searchLocations.add(new LocalDirectorySearchLocation(dir)); - ExternalDebugFilesService edfs = new ExternalDebugFilesService(searchLocations); - DWARFExternalDebugFilesPlugin.saveExternalDebugFilesService(edfs); + searchLocations.add(new LocalDirDebugLinkProvider(dir)); + ExternalDebugFilesService edfs = new ExternalDebugFilesService( + LocalDirDebugInfoDProvider.getGhidraCacheInstance(), searchLocations); + ExternalDebugFilesService.saveToPrefs(edfs); } } diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DWARFExternalDebugFilesPlugin/DWARFExternalDebugFilesPlugin.html b/Ghidra/Features/Base/src/main/help/help/topics/DWARFExternalDebugFilesPlugin/DWARFExternalDebugFilesPlugin.html index 51d216ba01..46b41fb017 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/DWARFExternalDebugFilesPlugin/DWARFExternalDebugFilesPlugin.html +++ b/Ghidra/Features/Base/src/main/help/help/topics/DWARFExternalDebugFilesPlugin/DWARFExternalDebugFilesPlugin.html @@ -1,43 +1,75 @@ - - DWARF External Debug Files - - - + + DWARF External Debug Files + + + - -

DWARF External Debug Files

+ +

DWARF External Debug Files

-

These files contain DWARF debug information that has been stripped from the original binary and - placed into a separate file (typically to save space). These external files can be found using - information embedded in the original binary's ".gnu_debuglink" section (a filename and crc32) and/or - ".note.gnu.build-id" section (a hash value).

- -

Use the ExtractELFDebugFilesScript to pull external debug files from pre-packaged install - files, typically provided by Linux / BSD distributions, for later consumption by Ghidra.

- -

Menu Actions

- -
-

DWARF External Debug Config

+

These files contain DWARF debug information that has been stripped from the original binary and + placed into a separate file (typically to save space). These external files can be found using + information embedded in the original binary's ".gnu_debuglink" section (a filename and + crc32) and/or ".note.gnu.build-id" section (a hash value).

-
-

Allows the user to pick a directory where Ghidra will search for DWARF external debug files.

-

Ghidra will search for external debug files under the selected directory - as ".build-id/NN/hexhash.debug" if build-id information is available, falling back to trying - the debuglink filename in any subdirectory, and lastly in the original binary's import location.

-
-
+

Use the ExtractELFDebugFilesScript to pull external debug files from + pre-packaged install files, typically provided by Linux / BSD distributions, for later + consumption by Ghidra.

+ +

The DWARF analyzer will use the configured external debug file locations to search for + debug files when it encounters a binary that has external debug information and is missing its + .debug_info sections.

+

Configuration

+

See Edit -> DWARF External Debug Config

+

-


- Provided by: DWARF External Debug Files Plugin

+
    +
  • Local Storage - 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.
  • +
  • Additional Locations - a list of locations to search when trying to find + a debug file. +

    Button actions:

    +
      +
    •  (Add) Adds a location. See Debug location types
    • +
    •  (Delete) Deletes the highlighted row
    • +
    •  (Up/Down) Moves the highlighted row up or down
    • +
    •  (Refresh) Updates the status of all rows
    • +
    •  (Save) Saves the current information
    • +
    +
  • +
-

 

-
-
-
- +

Debug location types:

+
    +
  • Program's Import Location - searchs the directory from which the program was + imported for any debug-link specified files, and for build-id specified files named + aabbcc...zz.debug, where aa..zz is the build-id hash in hex.
  • +
  • Build-id Directory - directory where debug files that are identified by a + build-id hash are stored.
    + Debug files are named aa/bbccdd...zz.debug under the base directory
    + This storage scheme for build-id debug files is distinct from debuginfod's scheme.

    + Example: /usr/lib/debug/.build-id
  • +
  • Debug Link Directory - directory where debug files that are identified by a + debug filename and crc hash (found in the binary's .gnu_debuglink section).
    + NOTE: This directory is searched recursively for a matching file.
  • +
  • Debuginfod Directory - directory where debuginfod has stored files. This + typically will be something like /home/user/.cache/debuginfod_client.
  • +
  • Debuginfod URL - HTTP(s) URL that points to a debuginfod server.
  • +
  • Import DEBUGINFOD_URLS Env Var - Helper action that adds any HTTP(s) URLs found + in debuginfod's environment variable.
  • +
+ +


+ Provided by: DWARF External Debug Files Plugin

+ +

 

+
+
+
+ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DWARFExternalDebugFilesPlugin/images/ExternalDebugFilesConfigDialog.png b/Ghidra/Features/Base/src/main/help/help/topics/DWARFExternalDebugFilesPlugin/images/ExternalDebugFilesConfigDialog.png new file mode 100644 index 0000000000000000000000000000000000000000..bc2c0dd0d09066f1f67db16c232b5508df72bee8 GIT binary patch literal 20768 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 v6Uv?ZI#bha>*Ny}Szk -

LibreTranslate Plugin

+

LibreTranslate Plugin

This plugin adds a string translation service that will appear in the Translate menu of a string data instance. The Translate menu will appear in the right-click @@ -19,21 +19,21 @@

LibreTranslate (currently hosted at libretranslate.com) is an independant project that provides an open source translation package that can be self-hosted.

- +

This plugin queries a LibreTranslate server via HTTP to translate each specified string into a target language. The results of that translation will be determined by the LibreTranslate - server.

- + server.

+

A LibreTranslate server can be installed locally by following the instructions provided on LibreTranslate's website, and then this plugin can connect to it via a URL such as http://localhost:5000/ (when configured with suggested defaults).

- +

It is also possible to use someone else's LibreTranslate server, and typically they will issue an API key that will authorize the user to connect.

-

When a string has been translated, the translated value will be shown in place of - the original value, bracketed with »chevrons«

- +

When a string has been translated, the translated value will be shown in place of + the original value, bracketed with »chevrons«

+

Configuration

See Edit -> @@ -41,7 +41,7 @@ Strings | LibreTranslate

-
    +
    • URL - required. Example: http://localhost:5000/ (if self hosted and following suggested values)
    • API Key - a unique key that authorizes you to connect to the LibreTranslate diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/DWARFAnalyzer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/DWARFAnalyzer.java index 4445fe59da..2aea5dc07f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/DWARFAnalyzer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/DWARFAnalyzer.java @@ -121,6 +121,10 @@ public class DWARFAnalyzer extends AbstractAnalyzer { catch (CancelledException ce) { throw ce; } + catch (DWARFException e) { + log.appendMsg("Error during DWARFAnalyzer import: " + e.getMessage()); + Msg.error(this, "Error during DWARFAnalyzer import: " + e.getMessage()); + } catch (IOException e) { log.appendMsg("Error during DWARFAnalyzer import: " + e); Msg.error(this, "Error during DWARFAnalyzer import: ", e); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImportOptions.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImportOptions.java index fdf524ac27..f2a8b99be6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImportOptions.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFImportOptions.java @@ -88,7 +88,8 @@ public class DWARFImportOptions { "Copy External Debug File Symbols"; private static final String OPTION_COPY_EXTERNAL_DEBUG_FILE_SYMBOLS_DESC = "Copies symbols (which will typically be mangled) from a found external debug file into " + - "the main program"; + "the main program. See Edit | DWARF External Debug Config to control how those " + + "external debug files are found."; private static final String OPTION_CHARSET_NAME = "Debug Strings Charset"; private static final String OPTION_CHARSET_NAME_DESC = """ @@ -144,7 +145,7 @@ public class DWARFImportOptions { /** * Used to control which macro info entries are used to create enums. */ - public static enum MacroEnumSetting { + public enum MacroEnumSetting { NONE, IGNORE_COMMAND_LINE, ALL; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFProgram.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFProgram.java index dc980409b4..29ca7d2146 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFProgram.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/DWARFProgram.java @@ -83,16 +83,13 @@ public class DWARFProgram implements Closeable { public static boolean isDWARF(Program program) { String format = Objects.requireNonNullElse(program.getExecutableFormat(), ""); - switch (format) { - case ElfLoader.ELF_NAME: - case PeLoader.PE_NAME: - return hasExpectedDWARFSections(program) || - ExternalDebugInfo.fromProgram(program) != null; - case MachoLoader.MACH_O_NAME: - return hasExpectedDWARFSections(program) || - DSymSectionProvider.getDSYMForProgram(program) != null; - } - return false; + return switch (format) { + case ElfLoader.ELF_NAME, PeLoader.PE_NAME -> hasExpectedDWARFSections(program) || + ExternalDebugInfo.fromProgram(program) != null; + case MachoLoader.MACH_O_NAME -> hasExpectedDWARFSections(program) || + DSymSectionProvider.getDSYMForProgram(program) != null; + default -> false; + }; } private static boolean hasExpectedDWARFSections(Program program) { @@ -193,8 +190,8 @@ public class DWARFProgram implements Closeable { protected WeakValueHashMap diesByOffset = new WeakValueHashMap<>(); private WeakValueHashMap aggsByOffset = new WeakValueHashMap<>(); - // Map of DIE offsets of {@link DIEAggregate}s that are being pointed to by - // other {@link DIEAggregate}s with a DW_AT_type property. + // Map of DIE offsets of DIEAggregates that are being pointed to by + // other DIEAggregates with a DW_AT_type property. // In other words, a map of inbound links to a DIEA. private ListValuedMap typeReferers = new ArrayListValuedHashMap<>(); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProvider.java new file mode 100644 index 0000000000..fb7928cdf2 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProvider.java @@ -0,0 +1,101 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import java.io.File; +import java.io.IOException; + +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A {@link DebugFileProvider} that expects the external debug files to be named using the hexadecimal + * value of the hash of the file, and to be arranged in a bucketed directory hierarchy using the + * first 2 hexdigits of the hash. + *

      + * For example, the debug file with hash {@code 6addc39dc19c1b45f9ba70baf7fd81ea6508ea7f} would + * be stored as "6a/ddc39dc19c1b45f9ba70baf7fd81ea6508ea7f.debug" (under some root directory). + */ +public class BuildIdDebugFileProvider implements DebugFileProvider { + private static final String BUILDID_NAME_PREFIX = "build-id://"; + + /** + * Returns true if the specified name string specifies a BuildIdDebugFileProvider. + * + * @param name string to test + * @return boolean true if name specifies a BuildIdDebugFileProvider + */ + public static boolean matches(String name) { + return name.startsWith(BUILDID_NAME_PREFIX); + } + + /** + * Creates a new {@link BuildIdDebugFileProvider} instance using the specified name string. + * + * @param name string, earlier returned from {@link #getName()} + * @param context {@link DebugInfoProviderCreatorContext} to allow accessing information outside + * of the name string that might be needed to create a new instance + * @return new {@link BuildIdDebugFileProvider} instance + */ + public static BuildIdDebugFileProvider create(String name, + DebugInfoProviderCreatorContext context) { + name = name.substring(BUILDID_NAME_PREFIX.length()); + + return new BuildIdDebugFileProvider(new File(name)); + } + + private final File rootDir; + + /** + * Creates a new {@link BuildIdDebugFileProvider} at the specified directory. + * + * @param rootDir path to the root directory of the build-id directory (typically ends with + * "./build-id") + */ + public BuildIdDebugFileProvider(File rootDir) { + this.rootDir = rootDir; + } + + @Override + public String getName() { + return BUILDID_NAME_PREFIX + rootDir.getPath(); + } + + @Override + public String getDescriptiveName() { + return rootDir.getPath() + " (.build-id dir)"; + } + + @Override + public DebugInfoProviderStatus getStatus(TaskMonitor monitor) { + return rootDir.isDirectory() + ? DebugInfoProviderStatus.VALID + : DebugInfoProviderStatus.INVALID; + } + + @Override + public File getFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) + throws IOException, CancelledException { + String buildId = debugInfo.getBuildId(); + if (buildId == null || buildId.length() < 4 /* 2 bytes = 4 hex digits */ ) { + return null; + } + File bucketDir = new File(rootDir, buildId.substring(0, 2)); + File file = new File(bucketDir, buildId.substring(2) + ".debug"); + return file.isFile() ? file : null; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdSearchLocation.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdSearchLocation.java deleted file mode 100644 index c3e4adc0a6..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/BuildIdSearchLocation.java +++ /dev/null @@ -1,97 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.util.bin.format.dwarf.external; - -import java.io.File; -import java.io.IOException; - -import ghidra.formats.gfilesystem.FSRL; -import ghidra.formats.gfilesystem.FileSystemService; -import ghidra.util.NumericUtilities; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; - -/** - * A {@link SearchLocation} that expects the external debug files to be named using the hexadecimal - * value of the hash of the file, and to be arranged in a bucketed directory hierarchy using the - * first 2 hexdigits of the hash. - *

      - * For example, the debug file with hash {@code 6addc39dc19c1b45f9ba70baf7fd81ea6508ea7f} would - * be stored as "6a/ddc39dc19c1b45f9ba70baf7fd81ea6508ea7f.debug" (under some root directory). - */ -public class BuildIdSearchLocation implements SearchLocation { - - /** - * Returns true if the specified location string specifies a BuildIdSearchLocation. - * - * @param locString string to test - * @return boolean true if locString specifies a BuildId location - */ - public static boolean isBuildIdSearchLocation(String locString) { - return locString.startsWith(BUILD_ID_PREFIX); - } - - /** - * Creates a new {@link BuildIdSearchLocation} instance using the specified location string. - * - * @param locString string, earlier returned from {@link #getName()} - * @param context {@link SearchLocationCreatorContext} to allow accessing information outside - * of the location string that might be needed to create a new instance - * @return new {@link BuildIdSearchLocation} instance - */ - public static BuildIdSearchLocation create(String locString, - SearchLocationCreatorContext context) { - locString = locString.substring(BUILD_ID_PREFIX.length()); - - return new BuildIdSearchLocation(new File(locString)); - } - - private static final String BUILD_ID_PREFIX = "build-id://"; - private final File rootDir; - - /** - * Creates a new {@link BuildIdSearchLocation} at the specified location. - * - * @param rootDir path to the root directory of the build-id directory (typically ends with - * "./build-id") - */ - public BuildIdSearchLocation(File rootDir) { - this.rootDir = rootDir; - } - - @Override - public String getName() { - return BUILD_ID_PREFIX + rootDir.getPath(); - } - - @Override - public String getDescriptiveName() { - return rootDir.getPath() + " (build-id)"; - } - - @Override - public FSRL findDebugFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) - throws IOException, CancelledException { - String hash = NumericUtilities.convertBytesToString(debugInfo.getHash()); - if (hash == null || hash.length() < 4 /* 2 bytes = 4 hex digits */ ) { - return null; - } - File bucketDir = new File(rootDir, hash.substring(0, 2)); - File file = new File(bucketDir, hash.substring(2) + ".debug"); - return file.isFile() ? FileSystemService.getInstance().getLocalFSRL(file) : null; - } - -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DWARFExternalDebugFilesPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DWARFExternalDebugFilesPlugin.java index d2f3289a62..ba63a1d0b4 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DWARFExternalDebugFilesPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DWARFExternalDebugFilesPlugin.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,20 +15,14 @@ */ package ghidra.app.util.bin.format.dwarf.external; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - import docking.action.builder.ActionBuilder; import docking.tool.ToolConstants; -import docking.widgets.filechooser.GhidraFileChooser; -import docking.widgets.filechooser.GhidraFileChooserMode; import ghidra.app.CorePluginPackage; import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.util.bin.format.dwarf.external.gui.ExternalDebugFilesConfigDialog; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.framework.preferences.Preferences; +import ghidra.util.HelpLocation; //@formatter:off @PluginInfo( @@ -40,10 +34,8 @@ import ghidra.framework.preferences.Preferences; ) //@formatter:on public class DWARFExternalDebugFilesPlugin extends Plugin { + public static final String HELP_TOPIC = "DWARFExternalDebugFilesPlugin"; - private static final String EXT_DEBUG_FILES_OPTION = "ExternalDebugFiles"; - private static final String SEARCH_LOCATIONS_LIST_OPTION = - EXT_DEBUG_FILES_OPTION + ".searchLocations"; public DWARFExternalDebugFilesPlugin(PluginTool tool) { super(tool); @@ -55,78 +47,9 @@ public class DWARFExternalDebugFilesPlugin extends Plugin { new ActionBuilder("DWARF External Debug Config", this.getName()) .menuPath(ToolConstants.MENU_EDIT, "DWARF External Debug Config") .menuGroup(ToolConstants.TOOL_OPTIONS_MENU_GROUP) - .onAction(ac -> showConfigDialog()) + .onAction(ac -> ExternalDebugFilesConfigDialog.show()) + .helpLocation(new HelpLocation(HELP_TOPIC, "Configuration")) .buildAndInstall(tool); } - private void showConfigDialog() { - // Let the user pick a single directory, and configure a ".build-id/" search location - // and a recursive dir search location at that directory, as well as a - // same-dir search location to search the program's import directory. - GhidraFileChooser chooser = new GhidraFileChooser(tool.getActiveWindow()); - chooser.setMultiSelectionEnabled(false); - chooser.setApproveButtonText("Select"); - chooser.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); - chooser.setTitle("Select External Debug Files Directory"); - File selectedDir = chooser.getSelectedFile(); - chooser.dispose(); - if (selectedDir == null) { - return; - } - - BuildIdSearchLocation bisl = new BuildIdSearchLocation(new File(selectedDir, ".build-id")); - LocalDirectorySearchLocation ldsl = new LocalDirectorySearchLocation(selectedDir); - SameDirSearchLocation sdsl = new SameDirSearchLocation(new File("does not matter")); - - ExternalDebugFilesService edfs = new ExternalDebugFilesService(List.of(bisl, ldsl, sdsl)); - saveExternalDebugFilesService(edfs); - } - - /** - * Get a new instance of {@link ExternalDebugFilesService} using the previously saved - * information (via {@link #saveExternalDebugFilesService(ExternalDebugFilesService)}). - * - * @param context created via {@link SearchLocationRegistry#newContext(ghidra.program.model.listing.Program)} - * @return new {@link ExternalDebugFilesService} instance - */ - public static ExternalDebugFilesService getExternalDebugFilesService( - SearchLocationCreatorContext context) { - SearchLocationRegistry searchLocRegistry = SearchLocationRegistry.getInstance(); - String searchPathStr = Preferences.getProperty(SEARCH_LOCATIONS_LIST_OPTION, "", true); - String[] pathParts = searchPathStr.split(";"); - List searchLocs = new ArrayList<>(); - for (String part : pathParts) { - if (!part.isBlank()) { - SearchLocation searchLoc = searchLocRegistry.createSearchLocation(part, context); - if (searchLoc != null) { - searchLocs.add(searchLoc); - } - } - } - if (searchLocs.isEmpty()) { - // default to search the same directory as the program - searchLocs.add(SameDirSearchLocation.create(null, context)); - } - return new ExternalDebugFilesService(searchLocs); - } - - /** - * Serializes an {@link ExternalDebugFilesService} to a string and writes to the Ghidra - * global preferences. - * - * @param service the {@link ExternalDebugFilesService} to commit to preferences - */ - public static void saveExternalDebugFilesService(ExternalDebugFilesService service) { - if (service != null) { - String path = service.getSearchLocations() - .stream() - .map(SearchLocation::getName) - .collect(Collectors.joining(";")); - Preferences.setProperty(SEARCH_LOCATIONS_LIST_OPTION, path); - } - else { - Preferences.setProperty(SEARCH_LOCATIONS_LIST_OPTION, null); - } - } - } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocation.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileProvider.java similarity index 54% rename from Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocation.java rename to Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileProvider.java index f4b6cf3da6..f060ac759a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocation.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileProvider.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,40 +15,28 @@ */ package ghidra.app.util.bin.format.dwarf.external; +import java.io.File; import java.io.IOException; -import ghidra.formats.gfilesystem.FSRL; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; /** - * Represents a collection of dwarf external debug files that can be searched. + * A {@link DebugInfoProvider} that can directly provide {@link File files}. */ -public interface SearchLocation { +public interface DebugFileProvider extends DebugInfoProvider { /** - * Searchs for a debug file that fulfills the criteria specified in the {@link ExternalDebugInfo}. + * Searches for a debug file that fulfills the criteria specified in the + * {@link ExternalDebugInfo}. * * @param debugInfo search criteria * @param monitor {@link TaskMonitor} - * @return {@link FSRL} of the matching file, or {@code null} if not found + * @return File of the matching file, or {@code null} if not found * @throws IOException if error * @throws CancelledException if cancelled */ - FSRL findDebugFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) + File getFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) throws IOException, CancelledException; - /** - * Returns the name of this instance, which should be a serialized copy of this instance. - * - * @return String serialized data of this instance, typically in "something://serialized_data" - * form - */ - String getName(); - /** - * Returns a human formatted string describing this location, used in UI prompts or lists. - * - * @return formatted string - */ - String getDescriptiveName(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileStorage.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileStorage.java new file mode 100644 index 0000000000..3c7ac3c540 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugFileStorage.java @@ -0,0 +1,32 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import java.io.File; +import java.io.IOException; + +import ghidra.app.util.bin.format.dwarf.external.DebugStreamProvider.StreamInfo; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A {@link DebugInfoProvider} that also allows storing files + */ +public interface DebugFileStorage extends DebugFileProvider { + File putStream(ExternalDebugInfo id, StreamInfo stream, TaskMonitor monitor) + throws IOException, CancelledException; + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProvider.java new file mode 100644 index 0000000000..0ea309fc4a --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProvider.java @@ -0,0 +1,41 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import ghidra.util.task.TaskMonitor; + +/** + * Base interface for objects that can provide DWARF debug files. See {@link DebugFileProvider} or + * {@link DebugStreamProvider}. + */ +public interface DebugInfoProvider { + /** + * {@return the name of this instance, which should be a serialized copy of this instance, + * typically like "something://serialized_data"} + */ + String getName(); + + /** + * {@return a human formatted string describing this provider, used in UI prompts or lists} + */ + String getDescriptiveName(); + + /** + * {@return DebugInfoProviderStatus representing this provider's current status} + * @param monitor {@link TaskMonitor} + */ + DebugInfoProviderStatus getStatus(TaskMonitor monitor); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderCreatorContext.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderCreatorContext.java new file mode 100644 index 0000000000..c66938109e --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderCreatorContext.java @@ -0,0 +1,26 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import ghidra.program.model.listing.Program; + +/** + * Information that might be needed to create a new {@link DebugInfoProvider} instance. + * + * @param registry {@link DebugInfoProviderRegistry} + * @param program {@link Program} + */ +public record DebugInfoProviderCreatorContext(DebugInfoProviderRegistry registry, Program program) {} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderRegistry.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderRegistry.java new file mode 100644 index 0000000000..2ab1033c8c --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderRegistry.java @@ -0,0 +1,110 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import ghidra.program.model.listing.Program; + +/** + * List of {@link DebugInfoProvider} types that can be saved / restored from a configuration string. + */ +public class DebugInfoProviderRegistry { + public static DebugInfoProviderRegistry getInstance() { + return instance; + } + + private static final DebugInfoProviderRegistry instance = new DebugInfoProviderRegistry(); + + private List creators = new ArrayList<>(); + + /** + * Creates a new registry + */ + public DebugInfoProviderRegistry() { + register(DisabledDebugInfoProvider::matches, DisabledDebugInfoProvider::create); + register(LocalDirDebugLinkProvider::matches, LocalDirDebugLinkProvider::create); + register(SameDirDebugInfoProvider::matches, SameDirDebugInfoProvider::create); + register(BuildIdDebugFileProvider::matches, BuildIdDebugFileProvider::create); + register(LocalDirDebugInfoDProvider::matches, LocalDirDebugInfoDProvider::create); + register(HttpDebugInfoDProvider::matches, HttpDebugInfoDProvider::create); + } + + /** + * Adds a {@link DebugFileProvider} to this registry. + * + * @param testFunc a {@link Predicate} that tests a name string, returning true if the + * string specifies the provider in question + * @param createFunc a {@link DebugInfoProviderCreator} that will create a new + * {@link DebugFileProvider} instance given a name string and a + * {@link DebugInfoProviderCreatorContext context} + */ + public void register(Predicate testFunc, DebugInfoProviderCreator createFunc) { + creators.add(new DebugInfoProviderCreationInfo(testFunc, createFunc)); + } + + /** + * Creates a new {@link DebugInfoProviderCreatorContext context}. + * + * @param program {@link Program} + * @return new {@link DebugInfoProviderCreatorContext} + */ + public DebugInfoProviderCreatorContext newContext(Program program) { + return new DebugInfoProviderCreatorContext(this, program); + } + + /** + * Creates a {@link DebugFileProvider} using the specified name string. + * + * @param name string previously returned by {@link DebugFileProvider#getName()} + * @param context a {@link DebugInfoProviderCreatorContext context} + * @return new {@link DebugFileProvider} instance, or null if there are no registered matching + * providers + */ + public DebugInfoProvider create(String name, DebugInfoProviderCreatorContext context) { + for (DebugInfoProviderCreationInfo slci : creators) { + if (slci.testFunc.test(name)) { + return slci.createFunc.create(name, context); + } + } + return null; + } + + private interface DebugInfoProviderCreator { + /** + * Creates a new {@link DebugFileProvider} instance using the provided name string. + * + * @param name string, previously returned by {@link DebugFileProvider#getName()} + * @param context {@link DebugInfoProviderCreatorContext context} + * @return new {@link DebugFileProvider} + */ + DebugInfoProvider create(String name, DebugInfoProviderCreatorContext context); + } + + private static class DebugInfoProviderCreationInfo { + Predicate testFunc; + DebugInfoProviderCreator createFunc; + + DebugInfoProviderCreationInfo(Predicate testFunc, + DebugInfoProviderCreator createFunc) { + this.testFunc = testFunc; + this.createFunc = createFunc; + } + + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderStatus.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderStatus.java new file mode 100644 index 0000000000..2a2ab1a065 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugInfoProviderStatus.java @@ -0,0 +1,20 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +public enum DebugInfoProviderStatus { + UNKNOWN, VALID, INVALID +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugStreamProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugStreamProvider.java new file mode 100644 index 0000000000..bf7f1b4e1c --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DebugStreamProvider.java @@ -0,0 +1,37 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import java.io.*; + +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A {@link DebugInfoProvider} that returns debug objects as a stream. + */ +public interface DebugStreamProvider extends DebugInfoProvider { + record StreamInfo(InputStream is, long contentLength) implements Closeable { + + @Override + public void close() throws IOException { + is.close(); + } + } + + StreamInfo getStream(ExternalDebugInfo id, TaskMonitor monitor) + throws IOException, CancelledException; +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DisabledDebugInfoProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DisabledDebugInfoProvider.java new file mode 100644 index 0000000000..8d22a4d0da --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/DisabledDebugInfoProvider.java @@ -0,0 +1,76 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import ghidra.util.task.TaskMonitor; + +/** + * Wrapper around a DebugInfoProvider that prevents it from being queried, but retains it in the + * configuration list. + */ +public class DisabledDebugInfoProvider implements DebugInfoProvider { + private static String DISABLED_PREFIX = "disabled://"; + + /** + * Predicate that tests if the name string is an instance of a disabled name. + * + * @param name string + * @return boolean true if the string should be handled by the DisabledSymbolServer class + */ + public static boolean matches(String name) { + return name.startsWith(DISABLED_PREFIX); + } + + /** + * Factory method to create new instances from a name string. + * + * @param name string, earlier returned from {@link #getName()} + * @param context {@link DebugInfoProviderCreatorContext} to allow accessing information outside + * of the name string that might be needed to create a new instance + * @return new instance, or null if invalid name string + */ + public static DebugInfoProvider create(String name, DebugInfoProviderCreatorContext context) { + String delegateName = name.substring(DISABLED_PREFIX.length()); + DebugInfoProvider delegate = context.registry().create(delegateName, context); + return (delegate != null) ? new DisabledDebugInfoProvider(delegate) : null; + } + + private DebugInfoProvider delegate; + + public DisabledDebugInfoProvider(DebugInfoProvider delegate) { + this.delegate = delegate; + } + + @Override + public String getName() { + return DISABLED_PREFIX + delegate.getName(); + } + + @Override + public String getDescriptiveName() { + return "Disabled - " + delegate.getDescriptiveName(); + } + + public DebugInfoProvider getDelegate() { + return delegate; + } + + @Override + public DebugInfoProviderStatus getStatus(TaskMonitor monitor) { + return DebugInfoProviderStatus.UNKNOWN; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugFilesService.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugFilesService.java index 656ca428c5..882a71874f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugFilesService.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugFilesService.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,55 +15,88 @@ */ package ghidra.app.util.bin.format.dwarf.external; +import java.io.File; import java.io.IOException; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; -import ghidra.formats.gfilesystem.FSRL; +import ghidra.app.util.bin.format.dwarf.external.DebugStreamProvider.StreamInfo; +import ghidra.framework.preferences.Preferences; +import ghidra.program.model.listing.Program; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; /** - * A collection of {@link SearchLocation search locations} that can be queried to find a - * DWARF external debug file, which is a second ELF binary that contains the debug information - * that was stripped from the original ELF binary. + * A collection of {@link DebugFileProvider providers} that can be queried to find a + * DWARF external debug file. Typically this will be an ELF binary that contains the debug + * information that was stripped from the original ELF binary, but can also include ability + * to fetch original binaries as well as source files. */ public class ExternalDebugFilesService { - private List searchLocations; + private static final String EXT_DEBUG_FILES_OPTION = "ExternalDebugFiles"; + private static final String STORAGE_OPTION = EXT_DEBUG_FILES_OPTION + ".storage"; + private static final String PROVIDERS_OPTION = EXT_DEBUG_FILES_OPTION + ".providers"; + + private final DebugFileStorage storage; + private List providers = new ArrayList<>(); /** - * Creates a new instance using the list of search locations. + * Creates a new instance using a {@link DebugFileStorage}, and a list of providers. * - * @param searchLocations list of {@link SearchLocation search locations} + * @param storage {@link DebugFileStorage} + * @param providers list of {@link DebugFileProvider providers} to search */ - public ExternalDebugFilesService(List searchLocations) { - this.searchLocations = searchLocations; + public ExternalDebugFilesService(DebugFileStorage storage, List providers) { + Objects.requireNonNull(storage); + this.storage = storage; + this.providers.add(storage); + this.providers.addAll(providers); + } + + public DebugFileStorage getStorage() { + return storage; } /** - * Returns the configured search locations. + * Returns the configured providers. * - * @return list of search locations + * @return list of providers */ - public List getSearchLocations() { - return searchLocations; + public List getProviders() { + return List.copyOf(providers.subList(1, providers.size())); + } + + /** + * Adds a {@link DebugInfoProvider} as a location to search. + * + * @param provider {@link DebugInfoProvider} to add + */ + public void addProvider(DebugInfoProvider provider) { + providers.add(provider); } /** * Searches for the specified external debug file. - *

      - * Returns the FSRL of a matching file, or null if not found. * * @param debugInfo information about the external debug file * @param monitor {@link TaskMonitor} - * @return {@link FSRL} of found file, or {@code null} if not found + * @return found file, or {@code null} if not found * @throws IOException if error */ - public FSRL findDebugFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) - throws IOException { + public File find(ExternalDebugInfo debugInfo, TaskMonitor monitor) throws IOException { try { - for (SearchLocation searchLoc : searchLocations) { + for (DebugInfoProvider provider : providers) { monitor.checkCancelled(); - FSRL result = searchLoc.findDebugFile(debugInfo, monitor); + File result = null; + if (provider instanceof DebugFileProvider fileProvider) { + result = fileProvider.getFile(debugInfo, monitor); + } + else if (provider instanceof DebugStreamProvider streamProvider) { + StreamInfo stream = streamProvider.getStream(debugInfo, monitor); + if (stream != null) { + result = storage.putStream(debugInfo, stream, monitor); + } + } if (result != null) { return result; } @@ -75,4 +108,94 @@ public class ExternalDebugFilesService { return null; } + //---------------------------------------- + /** + * {@return an ExternalDebugFilesService instance with no additional search locations} + */ + public static ExternalDebugFilesService getMinimal() { + return new ExternalDebugFilesService(LocalDirDebugInfoDProvider.getGhidraCacheInstance(), + List.of()); + } + + /** + * {@return an ExternalDebugFilesService instance with default search locations} + */ + public static ExternalDebugFilesService getDefault() { + return new ExternalDebugFilesService(LocalDirDebugInfoDProvider.getGhidraCacheInstance(), + List.of(new SameDirDebugInfoProvider(null), + LocalDirDebugInfoDProvider.getUserHomeCacheInstance())); + } + + /** + * Get a new instance of {@link ExternalDebugFilesService} using the previously saved + * information (via {@link #saveToPrefs(ExternalDebugFilesService)}), for the specified program. + * + * @param program {@link Program} + * @return new {@link ExternalDebugFilesService} instance + */ + public static ExternalDebugFilesService forProgram(Program program) { + return fromPrefs(DebugInfoProviderRegistry.getInstance().newContext(program)); + } + + /** + * Get a new instance of {@link ExternalDebugFilesService} using the previously saved + * information (via {@link #saveToPrefs(ExternalDebugFilesService)}). + * + * @param context created via {@link DebugInfoProviderRegistry#newContext(ghidra.program.model.listing.Program)} + * @return new {@link ExternalDebugFilesService} instance + */ + public static ExternalDebugFilesService fromPrefs(DebugInfoProviderCreatorContext context) { + DebugInfoProviderRegistry registry = DebugInfoProviderRegistry.getInstance(); + + String storageStr = Preferences.getProperty(STORAGE_OPTION, "", true); + DebugFileStorage storage = null; + if ( storageStr != null ) { + DebugInfoProvider storageProvider = registry.create(storageStr, context); + storage = (storageProvider instanceof DebugFileStorage dfs) ? dfs : null; + } + if ( storage == null ) { + storage = LocalDirDebugInfoDProvider.getGhidraCacheInstance(); + } + + String providersStr = Preferences.getProperty(PROVIDERS_OPTION, "", true); + String[] providerNames = providersStr.split(";"); + List providers = new ArrayList<>(); + for (String providerName : providerNames) { + if (!providerName.isBlank()) { + DebugInfoProvider provider = registry.create(providerName, context); + if (provider != null) { + providers.add(provider); + } + } + } + if (providers.isEmpty()) { + // default to search the same directory as the program + providers.add(SameDirDebugInfoProvider.create(null, context)); + providers.add(LocalDirDebugInfoDProvider.getUserHomeCacheInstance()); + } + + return new ExternalDebugFilesService(storage, providers); + } + + /** + * Serializes an {@link ExternalDebugFilesService} to a string and writes to the Ghidra + * global preferences. + * + * @param service the {@link ExternalDebugFilesService} to commit to preferences + */ + public static void saveToPrefs(ExternalDebugFilesService service) { + if (service != null) { + String serializedProviders = service.getProviders() + .stream() + .map(DebugInfoProvider::getName) + .collect(Collectors.joining(";")); + Preferences.setProperty(STORAGE_OPTION, service.getStorage().getName()); + Preferences.setProperty(PROVIDERS_OPTION, serializedProviders); + } + else { + Preferences.setProperty(STORAGE_OPTION, null); + Preferences.setProperty(PROVIDERS_OPTION, null); + } + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugInfo.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugInfo.java index 5afa7e1ca2..47a2b92050 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugInfo.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ExternalDebugInfo.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -45,26 +45,54 @@ public class ExternalDebugInfo { String filename = debugLink != null ? debugLink.getFilename() : null; int crc = debugLink != null ? debugLink.getCrc() : 0; - byte[] hash = buildId != null ? buildId.getDescription() : null; - return new ExternalDebugInfo(filename, crc, hash); + String hash = buildId != null + ? NumericUtilities.convertBytesToString(buildId.getDescription()) + : null; + + return new ExternalDebugInfo(filename, crc, hash, ObjectType.DEBUGINFO, null); } - private String filename; - private int crc; - private byte[] hash; + /** + * {@return a new ExternalDebugInfo instance created using the specified Build-Id value} + * @param buildId hex string + */ + public static ExternalDebugInfo forBuildId(String buildId) { + return new ExternalDebugInfo(null, 0, buildId, ObjectType.DEBUGINFO, null); + } + + /** + * {@return a new ExternalDebugInfo instance created using the specified debuglink values} + * @param debugLinkFilename filename from debuglink section + * @param crc crc32 from debuglink section + */ + public static ExternalDebugInfo forDebugLink(String debugLinkFilename, int crc) { + return new ExternalDebugInfo(debugLinkFilename, crc, null, ObjectType.DEBUGINFO, null); + } + + private final String filename; + private final int crc; + private final String buildId; + private final ObjectType objectType; + private final String extra; /** * Constructor to create an {@link ExternalDebugInfo} instance. * * @param filename filename of external debug file, or null * @param crc crc32 of external debug file, or 0 if no filename - * @param hash build-id hash digest found in ".note.gnu.build-id" section, or null if + * @param buildId build-id hash digest found in ".note.gnu.build-id" section, or null if * not present + * @param objectType {@link ObjectType} specifies what kind of debug file is specified by the + * other info + * @param extra additional information used by {@link ObjectType#SOURCE} */ - public ExternalDebugInfo(String filename, int crc, byte[] hash) { + public ExternalDebugInfo(String filename, int crc, String buildId, ObjectType objectType, + String extra) { this.filename = filename; this.crc = crc; - this.hash = hash; + this.buildId = buildId; + this.objectType = objectType; + this.extra = extra; } /** @@ -72,7 +100,7 @@ public class ExternalDebugInfo { * * @return boolean true if filename is available, false if not */ - public boolean hasFilename() { + public boolean hasDebugLink() { return filename != null && !filename.isBlank(); } @@ -95,19 +123,38 @@ public class ExternalDebugInfo { } /** - * Return the build-id hash digest. + * Return the build-id. * - * @return byte array containing the build-id hash (usually 20 bytes) + * @return build-id hash string */ - public byte[] getHash() { - return hash; + public String getBuildId() { + return buildId; + } + + /** + * {@return true if buildId is available, false if not} + */ + public boolean hasBuildId() { + return buildId != null && !buildId.isBlank(); + } + + public ObjectType getObjectType() { + return objectType; + } + + public String getExtra() { + return extra; + } + + public ExternalDebugInfo withType(ObjectType newObjectType, String newExtra) { + return new ExternalDebugInfo(extra, crc, buildId, newObjectType, newExtra); } @Override public String toString() { - return String.format("ExternalDebugInfo [filename=%s, crc=%s, hash=%s]", - filename, - Integer.toHexString(crc), - NumericUtilities.convertBytesToString(hash)); + return String.format( + "ExternalDebugInfo [filename=%s, crc=%s, hash=%s, objectType=%s, extra=%s]", filename, + crc, buildId, objectType, extra); } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProvider.java new file mode 100644 index 0000000000..f36d4ed5df --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProvider.java @@ -0,0 +1,246 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import java.io.IOException; +import java.io.InputStream; +import java.net.*; +import java.net.http.*; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.channels.UnresolvedAddressException; +import java.util.Objects; +import java.util.concurrent.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import ghidra.net.HttpClients; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.CancelledListener; +import ghidra.util.task.TaskMonitor; + +/** + * Queries debuginfod REST servers for debug objects. + */ +public class HttpDebugInfoDProvider implements DebugStreamProvider { + private static final String GHIDRA_USER_AGENT = "Ghidra_HttpDebugInfoDProvider_client"; + private static final int HTTP_STATUS_OK = HttpURLConnection.HTTP_OK; + private static final int HTTP_STATUS_INTERNAL_ERROR = HttpURLConnection.HTTP_INTERNAL_ERROR; + private static final int HTTP_STATUS_NOT_FOUND = HttpURLConnection.HTTP_NOT_FOUND; + private static final int DEFAULT_HTTP_REQUEST_TIMEOUT_MS = 10 * 1000; // 10 seconds + private static final int DEFAULT_MAX_RETRY_COUNT = 5; + private static final Pattern HTTPPROVIDER_REGEX = Pattern.compile("(http(s)?://.*)"); + + public static boolean matches(String name) { + return HTTPPROVIDER_REGEX.matcher(name).matches(); + } + + public static HttpDebugInfoDProvider create(String name, + DebugInfoProviderCreatorContext context) { + Matcher m = HTTPPROVIDER_REGEX.matcher(name); + if (!m.matches()) { + return null; + } + String uriStr = m.group(1); + URI serverURI = URI.create(uriStr); + return new HttpDebugInfoDProvider(serverURI); + } + + private final URI serverURI; + private int retriedCount; + private int notFoundCount; + private int maxRetryCount = DEFAULT_MAX_RETRY_COUNT; + private int httpRequestTimeoutMs = DEFAULT_HTTP_REQUEST_TIMEOUT_MS; + + /** + * Creates a new instance of a HttpSymbolServer. + * + * @param serverURI URI / URL of the symbol server + */ + public HttpDebugInfoDProvider(URI serverURI) { + String path = serverURI.getPath(); + this.serverURI = + path.endsWith("/") ? serverURI : serverURI.resolve(serverURI.getPath() + "/"); + } + + @Override + public String getName() { + return serverURI.toString(); + } + + @Override + public String getDescriptiveName() { + return serverURI.toString(); + } + + @Override + public DebugInfoProviderStatus getStatus(TaskMonitor monitor) { + return DebugInfoProviderStatus.UNKNOWN; + } + + private HttpRequest.Builder request(ExternalDebugInfo id) throws IOException { + try { + String extra = ""; + if (id.getObjectType() == ObjectType.SOURCE) { + extra = "/" + Objects.requireNonNullElse(id.getExtra(), ""); + } + String requestPath = "buildid/%s/%s%s".formatted(id.getBuildId(), + id.getObjectType().getPathString(), extra); + return HttpRequest.newBuilder(serverURI.resolve(requestPath)) + .setHeader("User-Agent", GHIDRA_USER_AGENT); + } + catch (IllegalArgumentException e) { + throw new IOException(e); + } + } + + @Override + public StreamInfo getStream(ExternalDebugInfo id, TaskMonitor monitor) + throws IOException, CancelledException { + if (!id.hasBuildId()) { + return null; + } + + monitor.setIndeterminate(true); + monitor.setMessage("Connecting to " + serverURI); + + HttpRequest request = request(id).GET().build(); + + retryLoop: for (int retryNum = 0; retryNum < maxRetryCount; retryNum++) { + if (retryNum > 0) { + Msg.debug(this, logPrefix() + ": retry count: " + retryNum); + retriedCount++; + } + InputStream bodyIS = null; + try { + HttpResponse response = tryGet(request, monitor); + int statusCode = response.statusCode(); + bodyIS = response.body(); + HttpHeaders headers = response.headers(); + Msg.debug(this, logPrefix() + ": Http response: " + response.statusCode()); + switch (statusCode) { + case HTTP_STATUS_OK: { + // TODO: typical response headers from debuginfod that we may want to make + // use of in the future: + // x-debuginfod-size: 245872 + // x-debuginfod-archive: /path/to/somepackagefile.packagetype_ext + // x-debuginfod-file: 1e1abd8faf1cb290df755a558377c5d7def3b1.debug + long contentLen = headers.firstValueAsLong("Content-Length").orElse(-1); + long size = headers.firstValueAsLong("x-debuginfod-size").orElse(-1); + String archivePath = headers.firstValue("x-debuginfod-archive").orElse(""); + String debugFile = headers.firstValue("x-debuginfod-file").orElse(""); + Msg.debug(this, + logPrefix() + + ": Debug object info size: %d, archive path: %s, debug file: %s" + .formatted(size, archivePath, debugFile)); + Msg.info(this, + "Found DWARF external debug file: %s".formatted(request.uri())); + + InputStream successIS = bodyIS; + bodyIS = null; + return new StreamInfo(successIS, contentLen); + } + case HTTP_STATUS_INTERNAL_ERROR: + // retry connection + continue retryLoop; + case HTTP_STATUS_NOT_FOUND: + notFoundCount++; + return null; + default: + Msg.debug(this, logPrefix() + ": unexpected result status: " + statusCode); + return null; + } + } + catch (ConnectException e) { + if (e.getCause() instanceof UnresolvedAddressException) { + Msg.debug(this, logPrefix() + ": bad server name? " + serverURI); + return null; // fail + } + // fall thru, retry + } + catch (TimeoutException e) { + // fall thru, retry + } + finally { + uncheckedClose(bodyIS); + } + } + Msg.debug(this, logPrefix() + ": failed to query for: " + id); + return null; + } + + private HttpResponse tryGet(HttpRequest request, TaskMonitor monitor) + throws IOException, CancelledException, TimeoutException { + Msg.debug(this, logPrefix() + ": " + request.toString()); + CompletableFuture> futureResponse = + HttpClients.getHttpClient().sendAsync(request, BodyHandlers.ofInputStream()); + CancelledListener l = () -> futureResponse.cancel(true); + monitor.addCancelledListener(l); + + try { + HttpResponse response = + futureResponse.get(httpRequestTimeoutMs, TimeUnit.MILLISECONDS); + + return response; + } + catch (InterruptedException e) { + throw new CancelledException("Download canceled"); + } + catch (ExecutionException e) { + // if possible, unwrap the exception that happened inside the future + Throwable cause = e.getCause(); + if (cause instanceof IOException ioe) { + throw ioe; + } + Msg.error(this, "Error during HTTP get", cause); + throw new IOException("Error during HTTP get", cause); + } + finally { + monitor.removeCancelledListener(l); + } + } + + private String logPrefix() { + return getClass().getSimpleName() + "[" + serverURI + "]"; + } + + private static void uncheckedClose(InputStream is) { + try { + if (is != null) { + is.close(); + } + } + catch (IOException e) { + // ignore it + } + } + + public int getNotFoundCount() { + return notFoundCount; + } + + public int getRetriedCount() { + return retriedCount; + } + + public void setMaxRetryCount(int maxRetryCount) { + this.maxRetryCount = maxRetryCount; + } + + public void setHttpRequestTimeoutMs(int httpRequestTimeoutMs) { + this.httpRequestTimeoutMs = httpRequestTimeoutMs; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProvider.java new file mode 100644 index 0000000000..2dbec3aa06 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProvider.java @@ -0,0 +1,369 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.Duration; +import java.util.Date; +import java.util.Objects; + +import ghidra.app.util.bin.format.dwarf.external.DebugStreamProvider.StreamInfo; +import ghidra.formats.gfilesystem.FSUtilities; +import ghidra.framework.Application; +import ghidra.util.Msg; +import ghidra.util.NumericUtilities; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; +import utility.application.ApplicationUtilities; +import utility.application.XdgUtils; + +/** + * Provides debug files found in a debuginfod-client compatible directory structure. + *

      + * Provides ability to store files. + *

      + * Does not try to follow debuginfod's file age-off logic or config values. + */ +public class LocalDirDebugInfoDProvider implements DebugFileStorage { + // static cache maint timing values. + private static final long MAINT_INTERVAL_MS = Duration.ofDays(1).toMillis(); + public static final long MAX_FILE_AGE_MS = Duration.ofDays(7).toMillis(); + + private static final String DEBUGINFOD_NAME_PREFIX = "debuginfod-dir://"; + public static final String GHIDRACACHE_NAME = "$DEFAULT"; + public static final String USERHOMECACHE_NAME = "$DEBUGINFOD_CLIENT_CACHE"; + + /** + * Returns true if the specified name string specifies a LocalDirDebugInfoDProvider. + * + * @param name string to test + * @return boolean true if name specifies a LocalDirDebugInfoDProvider + */ + public static boolean matches(String name) { + return name.startsWith(DEBUGINFOD_NAME_PREFIX); + } + + /** + * Creates a new {@link BuildIdDebugFileProvider} instance using the specified name string. + * + * @param name string, earlier returned from {@link #getName()} + * @param context {@link DebugInfoProviderCreatorContext} to allow accessing information outside + * of the name string that might be needed to create a new instance + * @return new {@link BuildIdDebugFileProvider} instance + */ + public static LocalDirDebugInfoDProvider create(String name, + DebugInfoProviderCreatorContext context) { + name = name.substring(DEBUGINFOD_NAME_PREFIX.length()); + + if (USERHOMECACHE_NAME.equals(name)) { + return getUserHomeCacheInstance(); + } + if (GHIDRACACHE_NAME.equals(name)) { + return getGhidraCacheInstance(); + } + + return new LocalDirDebugInfoDProvider(new File(name)); + } + + /** + * {@return a new LocalDirDebugInfoDProvider that stores files in the same directory that the + * debuginfod-find CLI tool would (/home/user/.cache/debuginfod_client/)} + */ + public static LocalDirDebugInfoDProvider getUserHomeCacheInstance() { + File cacheDir = new File(getCacheHomeLocation(), "debuginfod_client"); + return new LocalDirDebugInfoDProvider(cacheDir, DEBUGINFOD_NAME_PREFIX + USERHOMECACHE_NAME, + "DebugInfoD Cache Dir <%s>".formatted(cacheDir)); + } + + /** + * {@return a new LocalDirDebugInfoDProvider that stores files in a Ghidra specific cache + * directory} + */ + public static LocalDirDebugInfoDProvider getGhidraCacheInstance() { + File cacheDir = new File(Application.getUserCacheDirectory(), "debuginfo-cache"); + FileUtilities.mkdirs(cacheDir); + LocalDirDebugInfoDProvider result = new LocalDirDebugInfoDProvider(cacheDir, + DEBUGINFOD_NAME_PREFIX + GHIDRACACHE_NAME, "Ghidra Cache Dir <%s>".formatted(cacheDir)); + result.setNeedsMaintCheck(true); + return result; + } + + private final File rootDir; + private final String name; + private final String descriptiveName; + private boolean needsInitMaintCheck; + + public LocalDirDebugInfoDProvider(File rootDir) { + this(rootDir, DEBUGINFOD_NAME_PREFIX + rootDir.getPath(), + rootDir.getPath() + " (debuginfod dir)"); + } + + public LocalDirDebugInfoDProvider(File rootDir, String name, String descriptiveName) { + this.rootDir = rootDir; + this.name = name; + this.descriptiveName = descriptiveName; + } + + public File getRootDir() { + return rootDir; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescriptiveName() { + return descriptiveName; + } + + @Override + public DebugInfoProviderStatus getStatus(TaskMonitor monitor) { + return isValid() ? DebugInfoProviderStatus.VALID : DebugInfoProviderStatus.INVALID; + } + + public File getDirectory() { + return rootDir; + } + + private boolean isValid() { + return rootDir.isDirectory(); + } + + public void setNeedsMaintCheck(boolean needsInitMaintCheck) { + this.needsInitMaintCheck = needsInitMaintCheck; + } + + @Override + public File getFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) + throws IOException, CancelledException { + if (!isValid() || !debugInfo.hasBuildId()) { + return null; + } + performInitMaintIfNeeded(); + + File f = getCachePath(debugInfo); + return f.isFile() ? f : null; + } + + private File getBuildidDir(String buildId) { + return new File(rootDir, buildId); + } + + private File getCachePath(ExternalDebugInfo id) { + String suffix = ""; + if (id.getObjectType() == ObjectType.SOURCE) { + suffix = "-" + escapePath(Objects.requireNonNullElse(id.getExtra(), "")); + } + + return new File(getBuildidDir(id.getBuildId()), + id.getObjectType().getPathString() + suffix); + } + + @Override + public File putStream(ExternalDebugInfo id, StreamInfo stream, TaskMonitor monitor) + throws IOException, CancelledException { + assertValid(); + if (!id.hasBuildId()) { + throw new IOException("Can't store debug file without BuildId value: " + id); + } + performInitMaintIfNeeded(); + + File f = getCachePath(id); + File tmpF = new File(f.getParent(), ".tmp_" + f.getName()); + FileUtilities.checkedMkdirs(f.getParentFile()); + try (stream; FileOutputStream fos = new FileOutputStream(tmpF)) { + FSUtilities.streamCopy(stream.is(), fos, monitor); + } + try { + if (f.isFile() && !f.delete()) { + throw new IOException("Could not delete %s".formatted(f)); + } + if (!tmpF.renameTo(f)) { + throw new IOException("Could not rename temp file %s to %s".formatted(tmpF, f)); + } + } + finally { + tmpF.delete(); // just blindly try to delete tmp file in case an exception was thrown + } + return f; + } + + private void assertValid() throws IOException { + if (!rootDir.isDirectory()) { + throw new IOException("Invalid debuginfo directory: " + rootDir); + } + } + + @Override + public String toString() { + return String.format("LocalDebugInfoProvider [rootDir=%s, name=%s]", rootDir, name); + } + + public void purgeAll() { + cacheMaint(-1); + File lastMaintFile = new File(rootDir, ".lastmaint"); + lastMaintFile.delete(); + } + + public void performInitMaintIfNeeded() { + if (needsInitMaintCheck) { + try { + performCacheMaintIfNeeded(); + } + finally { + needsInitMaintCheck = false; + } + } + } + + public void performCacheMaintIfNeeded() { + if (!rootDir.isDirectory()) { + return; + } + if (rootDir.getParentFile() == null) { + // if someone gave us "/" as our path, don't try to delete files + Msg.error(this, "Refusing to clean up files in " + rootDir); + return; + } + + long now = System.currentTimeMillis(); + File lastMaintFile = new File(rootDir, ".lastmaint"); + long lastMaintTS = lastMaintFile.isFile() ? lastMaintFile.lastModified() : 0; + if (lastMaintTS + MAINT_INTERVAL_MS > now) { + return; + } + + cacheMaint(MAX_FILE_AGE_MS); + + try { + Files.writeString(lastMaintFile.toPath(), "Last maint run at " + (new Date())); + } + catch (IOException e) { + Msg.error(this, "Unable to write file cache maintenance file: " + lastMaintFile, e); + } + } + + /** + * Ages off debug files found in a compatible directory struct. + * + * @param maxFileAgeMs max age of any debug file to allow, or -1 for all files + */ + private void cacheMaint(long maxFileAgeMs) { + long cutoffMS = + maxFileAgeMs >= 0 ? System.currentTimeMillis() - maxFileAgeMs : Long.MAX_VALUE; + int deletedCount = 0; + long deletedBytes = 0; + + for (File f : Objects.requireNonNullElse(rootDir.listFiles(), new File[0])) { + if (f.isDirectory() && isBuildIdSubdirName(f.getName())) { + int subDirFileCount = 0; + int deletedSubDirFileCount = 0; + for (File subF : Objects.requireNonNullElse(f.listFiles(), new File[0])) { + subDirFileCount++; + if (subF.isFile()) { + long modified = subF.lastModified(); + if (modified != 0 && modified < cutoffMS) { + long size = subF.length(); + if (subF.delete()) { + deletedCount++; + deletedBytes += size; + deletedSubDirFileCount++; + } + } + } + } + if (subDirFileCount == deletedSubDirFileCount) { + // build-id hash directory should be empty, remove it + if (!f.delete()) { + Msg.warn(this, "Failed to delete empty debuginfod hash directory: " + f); + } + } + } + } + Msg.debug(this, + "Finished cache cleanup of debug files in %s, deleted %d files, %d total bytes" + .formatted(rootDir, deletedCount, deletedBytes)); + } + //--------------------------------------------------------------------------------------------- + + /** + * Converts a path string into a string that can be used as a filename. + *

      + * For example: "/usr/include/stdio.h" becomes "AABBCCDD-#usr#include#stdio.h", where + * AABCCDD is the hex value of the 32 bit hash of the original path string. + * (See {@link #djbX33AHash(String)}). + * + * @param s path string + * @return escaped string + */ + private static String escapePath(String s) { + // TODO: needs testing on how strings just barely longer than maxPath match with + // the debuginfod-client.c logic + int maxPath = 255 /* NAME_MAX*/ / 2; // from debuginfod-client.c:path_escape() + int hash = (int) djbX33AHash(s); + if (s.length() > maxPath) { + int start = s.length() - maxPath; // keep trailing part of filepath + s = s.substring(start); + } + s = s.replaceAll("[^a-zA-Z0-9._-]", "#"); // NOTE: the dash '-' needs to be last in the "[]" regex class + return "%08x-%s".formatted(hash, s); + } + + private static long djbX33AHash(String s) { + // see debuginfod-client.c to ensure compatibility + long hash = 5381; + for (byte b : s.getBytes(StandardCharsets.UTF_8)) { + hash = ((hash << 5) + hash) + Byte.toUnsignedInt(b); + } + return hash; + } + + private static boolean isBuildIdSubdirName(String s) { + // subdirs under the debuginfod cache root should be simple 20 byte(ish) hash values. + byte[] bytes = NumericUtilities.convertStringToBytes(s); + return bytes != null && bytes.length >= 20 /* typical buildId hash size */; + } + + private static File getCacheHomeLocation() { + File cacheHomeDir = getEnvVarAsFile(XdgUtils.XDG_CACHE_HOME); + if (cacheHomeDir == null) { + try { + cacheHomeDir = ApplicationUtilities.getJavaUserHomeDir(); + } + catch (IOException e) { + throw new RuntimeException("Missing home directory", e); + } + cacheHomeDir = new File(cacheHomeDir, XdgUtils.XDG_CACHE_HOME_DEFAULT_SUBDIRNAME); + } + return cacheHomeDir; + } + + private static File getEnvVarAsFile(String name) { + String path = System.getenv(name); + if (path != null && !path.isBlank()) { + File result = new File(path.trim()); + if (result.isAbsolute()) { + return result; + } + } + return null; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirectorySearchLocation.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProvider.java similarity index 53% rename from Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirectorySearchLocation.java rename to Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProvider.java index cbae723c95..d30bbdedc8 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirectorySearchLocation.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProvider.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -18,69 +18,77 @@ package ghidra.app.util.bin.format.dwarf.external; import java.io.*; import java.util.zip.CRC32; -import ghidra.formats.gfilesystem.FSRL; -import ghidra.formats.gfilesystem.FileSystemService; import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; /** - * A {@link SearchLocation} that recursively searches for dwarf external debug files - * under a configured directory. + * Searches for DWARF external debug files specified via a debug-link filename / crc in a directory. */ -public class LocalDirectorySearchLocation implements SearchLocation { +public class LocalDirDebugLinkProvider implements DebugFileProvider { - private static final String LOCAL_DIR_PREFIX = "dir://"; + private static final String DEBUGLINK_NAME_PREFIX = "debuglink://"; /** - * Returns true if the specified location string specifies a LocalDirectorySearchLocation. + * Returns true if the specified name string specifies a LocalDirDebugLinkProvider. * - * @param locString string to test - * @return boolean true if locString specifies a local dir search location + * @param name string to test + * @return boolean true if name specifies a LocalDirDebugLinkProvider name */ - public static boolean isLocalDirSearchLoc(String locString) { - return locString.startsWith(LOCAL_DIR_PREFIX); + public static boolean matches(String name) { + return name.startsWith(DEBUGLINK_NAME_PREFIX); } /** - * Creates a new {@link LocalDirectorySearchLocation} instance using the specified location string. + * Creates a new {@link LocalDirDebugLinkProvider} instance using the specified name string. * - * @param locString string, earlier returned from {@link #getName()} - * @param context {@link SearchLocationCreatorContext} to allow accessing information outside - * of the location string that might be needed to create a new instance - * @return new {@link LocalDirectorySearchLocation} instance + * @param name string, earlier returned from {@link #getName()} + * @param context {@link DebugInfoProviderCreatorContext} to allow accessing information outside + * of the name string that might be needed to create a new instance + * @return new {@link LocalDirDebugLinkProvider} instance */ - public static LocalDirectorySearchLocation create(String locString, - SearchLocationCreatorContext context) { - locString = locString.substring(LOCAL_DIR_PREFIX.length()); - return new LocalDirectorySearchLocation(new File(locString)); + public static LocalDirDebugLinkProvider create(String name, + DebugInfoProviderCreatorContext context) { + String dir = name.substring(DEBUGLINK_NAME_PREFIX.length()); + return new LocalDirDebugLinkProvider(new File(dir)); } private final File searchDir; /** - * Creates a new {@link LocalDirectorySearchLocation} at the specified location. + * Creates a new {@link LocalDirDebugLinkProvider} at the specified dir. * * @param searchDir path to the root directory of where to search */ - public LocalDirectorySearchLocation(File searchDir) { + public LocalDirDebugLinkProvider(File searchDir) { this.searchDir = searchDir; } @Override public String getName() { - return LOCAL_DIR_PREFIX + searchDir.getPath(); + return DEBUGLINK_NAME_PREFIX + searchDir.getPath(); } @Override public String getDescriptiveName() { - return searchDir.getPath(); + return searchDir.getPath() + " (debug-link dir)"; } @Override - public FSRL findDebugFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) + public DebugInfoProviderStatus getStatus(TaskMonitor monitor) { + return isValid() + ? DebugInfoProviderStatus.VALID + : DebugInfoProviderStatus.INVALID; + } + + private boolean isValid() { + return searchDir.isDirectory(); + } + + @Override + public File getFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) throws CancelledException, IOException { - if (!debugInfo.hasFilename()) { + if (!debugInfo.hasDebugLink() || !isValid()) { return null; } ensureSafeFilename(debugInfo.getFilename()); @@ -94,24 +102,26 @@ public class LocalDirectorySearchLocation implements SearchLocation { } } - FSRL findFile(File dir, ExternalDebugInfo debugInfo, TaskMonitor monitor) + File findFile(File dir, ExternalDebugInfo debugInfo, TaskMonitor monitor) throws IOException, CancelledException { - if (!debugInfo.hasFilename()) { + if (!debugInfo.hasDebugLink()) { return null; } File file = new File(dir, debugInfo.getFilename()); if (file.isFile()) { int fileCRC = calcCRC(file); if (fileCRC == debugInfo.getCrc()) { - return FileSystemService.getInstance().getLocalFSRL(file); + return file; // success } - Msg.info(this, "DWARF external debug file found with mismatching crc, ignored: " + - file + ", (" + Integer.toHexString(fileCRC) + ")"); + Msg.info(this, + "DWARF external debug file found with mismatching crc, ignored: %s (%08x)" + .formatted(file, fileCRC)); } File[] subDirs; if ((subDirs = dir.listFiles(f -> f.isDirectory())) != null) { + // TODO: prevent recursing into symlinks? for (File subDir : subDirs) { - FSRL result = findFile(subDir, debugInfo, monitor); + File result = findFile(subDir, debugInfo, monitor); if (result != null) { return result; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ObjectType.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ObjectType.java new file mode 100644 index 0000000000..64aaf34c03 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/ObjectType.java @@ -0,0 +1,24 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +public enum ObjectType { + DEBUGINFO, EXECUTABLE, SOURCE; + + public String getPathString() { + return name().toLowerCase(); + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirDebugInfoProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirDebugInfoProvider.java new file mode 100644 index 0000000000..d596487ab8 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirDebugInfoProvider.java @@ -0,0 +1,121 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FilenameUtils; + +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +/** + * A {@link DebugFileProvider} that only looks in the program's original import directory for + * matching debug files. + */ +public class SameDirDebugInfoProvider implements DebugFileProvider { + + public static final String DESC = "Program's Import Location"; + + /** + * Returns true if the specified name string specifies a SameDirDebugInfoProvider. + * + * @param name string to test + * @return boolean true if locString specifies a SameDirDebugInfoProvider + */ + public static boolean matches(String name) { + return name.equals("."); + } + + /** + * Creates a new {@link SameDirDebugInfoProvider} instance using the current program's + * import location. + * + * @param name unused + * @param context {@link DebugInfoProviderCreatorContext} + * @return new {@link SameDirDebugInfoProvider} instance + */ + public static SameDirDebugInfoProvider create(String name, + DebugInfoProviderCreatorContext context) { + File exeLocation = context.program() != null + ? new File(FilenameUtils.getFullPath(context.program().getExecutablePath())) + : null; + return new SameDirDebugInfoProvider(exeLocation); + } + + private final File progDir; + + /** + * Creates a new {@link SameDirDebugInfoProvider} at the specified directory. + * + * @param progDir path to the program's import directory + */ + public SameDirDebugInfoProvider(File progDir) { + this.progDir = progDir; + } + + @Override + public String getName() { + return "."; + } + + @Override + public String getDescriptiveName() { + return DESC + (progDir != null ? " (" + progDir.getPath() + ")" : ""); + } + + @Override + public DebugInfoProviderStatus getStatus(TaskMonitor monitor) { + return progDir != null + ? progDir.isDirectory() + ? DebugInfoProviderStatus.VALID + : DebugInfoProviderStatus.INVALID + : DebugInfoProviderStatus.UNKNOWN; + } + + @Override + public File getFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) + throws IOException, CancelledException { + if (debugInfo.hasDebugLink()) { + // This differs from the LocalDirDebugLinkProvider in that it does NOT recursively search + // for the file + File debugFile = new File(progDir, debugInfo.getFilename()); + if (debugFile.isFile()) { + int fileCRC = LocalDirDebugLinkProvider.calcCRC(debugFile); + if (fileCRC == debugInfo.getCrc()) { + return debugFile; // success + } + Msg.info(this, + "DWARF external debug file found with mismatching crc, ignored: %s, (%08x)" + .formatted(debugFile, fileCRC)); + } + } + + if (debugInfo.hasBuildId()) { + // this probe is a w.a.g for what people might do when co-locating a build-id debug + // file with the original binary + File debugFile = new File(progDir, debugInfo.getBuildId() + ".debug"); + if (debugFile.isFile()) { + return debugFile; + } + } + + return null; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirSearchLocation.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirSearchLocation.java deleted file mode 100644 index 3ab0172962..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SameDirSearchLocation.java +++ /dev/null @@ -1,99 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.util.bin.format.dwarf.external; - -import java.io.File; -import java.io.IOException; - -import org.apache.commons.io.FilenameUtils; - -import ghidra.formats.gfilesystem.FSRL; -import ghidra.formats.gfilesystem.FileSystemService; -import ghidra.util.Msg; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; - -/** - * A {@link SearchLocation} that only looks in the program's original import directory. - */ -public class SameDirSearchLocation implements SearchLocation { - - /** - * Returns true if the specified location string specifies a SameDirSearchLocation. - * - * @param locString string to test - * @return boolean true if locString specifies a BuildId location - */ - public static boolean isSameDirSearchLocation(String locString) { - return locString.equals("."); - } - - /** - * Creates a new {@link SameDirSearchLocation} instance using the current program's - * import location. - * - * @param locString unused - * @param context {@link SearchLocationCreatorContext} - * @return new {@link SameDirSearchLocation} instance - */ - public static SameDirSearchLocation create(String locString, - SearchLocationCreatorContext context) { - File exeLocation = - new File(FilenameUtils.getFullPath(context.getProgram().getExecutablePath())); - return new SameDirSearchLocation(exeLocation); - } - - private final File progDir; - - /** - * Creates a new {@link SameDirSearchLocation} at the specified location. - * - * @param progDir path to the program's import directory - */ - public SameDirSearchLocation(File progDir) { - this.progDir = progDir; - } - - @Override - public String getName() { - return "."; - } - - @Override - public String getDescriptiveName() { - return progDir.getPath() + " (Program's Import Location)"; - } - - @Override - public FSRL findDebugFile(ExternalDebugInfo debugInfo, TaskMonitor monitor) - throws IOException, CancelledException { - if (!debugInfo.hasFilename()) { - return null; - } - File file = new File(progDir, debugInfo.getFilename()); - if (!file.isFile()) { - return null; - } - int fileCRC = LocalDirectorySearchLocation.calcCRC(file); - if (fileCRC != debugInfo.getCrc()) { - Msg.info(this, "DWARF external debug file found with mismatching crc, ignored: " + - file + ", (" + Integer.toHexString(fileCRC) + ")"); - return null; - } - return FileSystemService.getInstance().getLocalFSRL(file); - } - -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationCreatorContext.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationCreatorContext.java deleted file mode 100644 index 3ad2c3588d..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationCreatorContext.java +++ /dev/null @@ -1,52 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.util.bin.format.dwarf.external; - -import ghidra.program.model.listing.Program; - -/** - * Information outside of a location string that might be needed to create a new {@link SearchLocation} - * instance. - */ -public class SearchLocationCreatorContext { - private final SearchLocationRegistry registry; - private final Program program; - - /** - * Create a new context object with references to the registry and the current program. - * - * @param registry {@link SearchLocationRegistry} - * @param program the current {@link Program} - */ - public SearchLocationCreatorContext(SearchLocationRegistry registry, Program program) { - this.registry = registry; - this.program = program; - } - - /** - * @return the {@link SearchLocationRegistry} that is creating the {@link SearchLocation} - */ - public SearchLocationRegistry getRegistry() { - return registry; - } - - /** - * @return the current {@link Program} - */ - public Program getProgram() { - return program; - } -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationRegistry.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationRegistry.java deleted file mode 100644 index 1bd4695c4d..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/SearchLocationRegistry.java +++ /dev/null @@ -1,112 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.app.util.bin.format.dwarf.external; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Predicate; - -import ghidra.program.model.listing.Program; - -/** - * List of {@link SearchLocation} types that can be saved / restored from a configuration string. - */ -public class SearchLocationRegistry { - public static SearchLocationRegistry getInstance() { - return instance; - } - - private static final SearchLocationRegistry instance = new SearchLocationRegistry(true); - - private List searchLocCreators = new ArrayList<>(); - - /** - * Creates a new registry, optionally registering the default SearchLocations. - * - * @param registerDefault boolean flag, if true register the built-in {@link SearchLocation}s - */ - public SearchLocationRegistry(boolean registerDefault) { - if (registerDefault) { - register(LocalDirectorySearchLocation::isLocalDirSearchLoc, - LocalDirectorySearchLocation::create); - register(BuildIdSearchLocation::isBuildIdSearchLocation, BuildIdSearchLocation::create); - register(SameDirSearchLocation::isSameDirSearchLocation, SameDirSearchLocation::create); - } - } - - /** - * Adds a {@link SearchLocation} to this registry. - * - * @param testFunc a {@link Predicate} that tests a location string, returning true if the - * string specifies the SearchLocation in question - * @param createFunc a {@link SearchLocationCreator} that will create a new {@link SearchLocation} - * instance given a location string and a {@link SearchLocationCreatorContext context} - */ - public void register(Predicate testFunc, SearchLocationCreator createFunc) { - searchLocCreators.add(new SearchLocationCreationInfo(testFunc, createFunc)); - } - - /** - * Creates a new {@link SearchLocationCreatorContext context}. - * - * @param program {@link Program} - * @return new {@link SearchLocationCreatorContext} - */ - public SearchLocationCreatorContext newContext(Program program) { - return new SearchLocationCreatorContext(this, program); - } - - /** - * Creates a {@link SearchLocation} using the provided location string. - * - * @param locString location string (previously returned by {@link SearchLocation#getName()} - * @param context a {@link SearchLocationCreatorContext context} - * @return new {@link SearchLocation} instance, or null if there are no registered matching - * SearchLocations - */ - public SearchLocation createSearchLocation(String locString, - SearchLocationCreatorContext context) { - for (SearchLocationCreationInfo slci : searchLocCreators) { - if (slci.testFunc.test(locString)) { - return slci.createFunc.create(locString, context); - } - } - return null; - } - - public interface SearchLocationCreator { - /** - * Creates a new {@link SearchLocation} instance using the provided location string. - * - * @param locString location string, previously returned by {@link SearchLocation#getName()} - * @param context {@link SearchLocationCreatorContext context} - * @return new {@link SearchLocation} - */ - SearchLocation create(String locString, SearchLocationCreatorContext context); - } - - private static class SearchLocationCreationInfo { - Predicate testFunc; - SearchLocationCreator createFunc; - - SearchLocationCreationInfo(Predicate testFunc, - SearchLocationCreator createFunc) { - this.testFunc = testFunc; - this.createFunc = createFunc; - } - - } -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/EnumIconColumnRenderer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/EnumIconColumnRenderer.java new file mode 100644 index 0000000000..8312786059 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/EnumIconColumnRenderer.java @@ -0,0 +1,69 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external.gui; + +import java.awt.Component; + +import javax.swing.*; + +import docking.widgets.table.GTableCellRenderingData; +import ghidra.docking.settings.Settings; +import ghidra.util.table.column.AbstractGColumnRenderer; + +/** + * Table column renderer to render an enum value as a icon + * + * @param enum type + */ +public class EnumIconColumnRenderer> + extends AbstractGColumnRenderer { + + private Icon[] icons; + private String[] toolTips; + + EnumIconColumnRenderer(Class enumClass, Icon[] icons, String[] toolTips) { + if (enumClass.getEnumConstants().length != icons.length || + icons.length != toolTips.length) { + throw new IllegalArgumentException(); + } + this.icons = icons; + this.toolTips = toolTips; + } + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + + JLabel renderer = (JLabel) super.getTableCellRendererComponent(data); + + E e = (E) data.getValue(); + renderer.setHorizontalAlignment(SwingConstants.CENTER); + renderer.setText(""); + renderer.setIcon(e != null ? icons[e.ordinal()] : null); + renderer.setToolTipText(e != null ? toolTips[e.ordinal()] : null); + return renderer; + } + + @Override + protected String getText(Object value) { + return ""; + } + + @Override + public String getFilterString(E t, Settings settings) { + return t == null ? "" : t.toString(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugFilesConfigDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugFilesConfigDialog.java new file mode 100644 index 0000000000..abba73dea4 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugFilesConfigDialog.java @@ -0,0 +1,601 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external.gui; + +import java.awt.*; +import java.io.File; +import java.net.URI; +import java.util.*; +import java.util.List; + +import javax.swing.*; + +import docking.DialogComponentProvider; +import docking.DockingWindowManager; +import docking.widgets.OptionDialog; +import docking.widgets.button.BrowseButton; +import docking.widgets.button.GButton; +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.filechooser.GhidraFileChooserMode; +import docking.widgets.label.GLabel; +import docking.widgets.table.GTable; +import docking.widgets.textfield.HintTextField; +import generic.theme.GIcon; +import ghidra.app.util.bin.format.dwarf.external.*; +import ghidra.framework.preferences.Preferences; +import ghidra.util.*; +import ghidra.util.exception.CancelledException; +import ghidra.util.layout.ThreeColumnLayout; +import ghidra.util.task.*; +import resources.Icons; + +public class ExternalDebugFilesConfigDialog extends DialogComponentProvider { + + public static boolean show() { + ExternalDebugFilesConfigDialog dlg = new ExternalDebugFilesConfigDialog(); + DockingWindowManager.showDialog(dlg); + return dlg.wasSuccess; + } + + private static final Dimension BUTTON_SIZE = new Dimension(32, 32); + + private List knownProviders = + WellKnownDebugProvider.loadAll(".debuginfod_urls"); + + private DebugInfoProviderCreatorContext creatorContext = + DebugInfoProviderRegistry.getInstance().newContext(null); + private DebugFileStorage storage; + + private ExternalDebugInfoProviderTableModel tableModel; + + private ExternalDebugFileProvidersPanel configPanel; + private boolean wasSuccess; + private boolean configChanged; + + public ExternalDebugFilesConfigDialog() { + super("DWARF External Debug Files Configuration", true, false, true, true); + + build(); + + tableModel.addTableModelListener(e -> updateButtonEnablement()); + setupInitialConfig(); + } + + private void setupInitialConfig() { + ExternalDebugFilesService tmpService = ExternalDebugFilesService.fromPrefs(creatorContext); + DebugFileStorage newStorage = tmpService.getStorage(); + setStorageLocation(newStorage); + tableModel.addItems(tmpService.getProviders()); + setConfigChanged(false); + } + + @Override + protected void cancelCallback() { + close(); + } + + @Override + protected void okCallback() { + if (isConfigChanged()) { + saveConfig(); + } + wasSuccess = true; + close(); + } + + @Override + protected void dialogShown() { + TableColumnInitializer.initializeTableColumns(configPanel.table, tableModel); + configPanel.refreshStatus(); + } + + private void build() { + tableModel = new ExternalDebugInfoProviderTableModel(); + + configPanel = new ExternalDebugFileProvidersPanel(); + + addButtons(); + addWorkPanel(configPanel); + + setHelpLocation( + new HelpLocation(DWARFExternalDebugFilesPlugin.HELP_TOPIC, "Configuration")); + + setRememberSize(false); + okButton.setEnabled(true); + } + + private void updateButtonEnablement() { + okButton.setEnabled(true); + configPanel.updatePanelButtonEnablement(); + } + + private void addButtons() { + addOKButton(); + addCancelButton(); + setDefaultButton(cancelButton); + } + + /** + * Screen shot usage only + */ + public void pushAddLocationButton() { + configPanel.addLocation(); + } + + /** + * Screen shot usage only + * + * @param list fake well known debug provider servers + */ + public void setWellknownProviders(List list) { + knownProviders = list; + } + + /** + * Screen shot only + */ + public void setService(ExternalDebugFilesService edfs) { + setProviders(edfs.getProviders()); + setStorageLocation(edfs.getStorage()); + } + + private void setStorageLocationPath(String path) { + configPanel.storageLocationTextField.setText(path); + } + + /** + * Returns a new {@link ExternalDebugFilesService} instance representing the currently + * displayed configuration, or null if the displayed configuration is not valid. + * + * @return new {@link ExternalDebugFilesService} or null + */ + ExternalDebugFilesService getService() { + return new ExternalDebugFilesService(storage, tableModel.getItems()); + } + + void setProviders(List providers) { + tableModel.setItems(providers); + } + + private void setStorageLocation(DebugFileStorage newStorage) { + storage = newStorage; + setStorageLocationPath(newStorage.getDescriptiveName()); + updateButtonEnablement(); + } + + void executeMonitoredRunnable(String taskTitle, boolean canCancel, boolean hasProgress, + int delay, MonitoredRunnable runnable) { + Task task = new Task(taskTitle, canCancel, hasProgress, false) { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + runnable.monitoredRun(monitor); + } + }; + executeProgressTask(task, delay); + } + + /** + * The union of the changed status of the local storage path and the additional + * search paths table model changed status. + * + * @return boolean true if the config has changed + */ + boolean isConfigChanged() { + return configChanged || tableModel.isDataChanged(); + } + + void setConfigChanged(boolean configChanged) { + this.configChanged = configChanged; + tableModel.setDataChanged(configChanged); + } + + /* package */ void saveConfig() { + ExternalDebugFilesService tmpService = getService(); + ExternalDebugFilesService.saveToPrefs(tmpService); + Preferences.store(); + setConfigChanged(false); + updateButtonEnablement(); + } + + private void registerHelp(Component comp, String anchorName) { + DockingWindowManager.getHelpService() + .registerHelp(comp, + new HelpLocation(DWARFExternalDebugFilesPlugin.HELP_TOPIC, anchorName)); + } + + //--------------------------------------------------------------------------------------------- + + class ExternalDebugFileProvidersPanel extends JPanel { + + private GTable table; + private JPanel additionalSearchLocationsPanel; + + private JButton refreshSearchLocationsStatusButton; + private JButton moveLocationUpButton; + private JButton moveLocationDownButton; + private JButton deleteLocationButton; + private JButton addLocationButton; + private JPanel storageLocationPanel; + private HintTextField storageLocationTextField; + private JButton saveSearchLocationsButton; + + ExternalDebugFileProvidersPanel() { + super(new BorderLayout()); + build(); + registerHelp(this, "Summary"); + } + + private void build() { + setBorder(BorderFactory.createTitledBorder("External Debug Files Config")); + + buildLocationPanel(); + JPanel tableButtonPanel = buildButtonPanel(); + JScrollPane tableScrollPane = buildTable(); + + additionalSearchLocationsPanel = new JPanel(); + additionalSearchLocationsPanel + .setLayout(new BoxLayout(additionalSearchLocationsPanel, BoxLayout.Y_AXIS)); + additionalSearchLocationsPanel.add(tableButtonPanel); + additionalSearchLocationsPanel.add(tableScrollPane); + + add(storageLocationPanel, BorderLayout.NORTH); + add(additionalSearchLocationsPanel, BorderLayout.CENTER); + } + + void refreshStatus() { + executeMonitoredRunnable("Refresh Provider Status", true, true, 0, monitor -> { + List rowsCopy = + new ArrayList<>(tableModel.getModelData()); + monitor.initialize(rowsCopy.size(), "Refreshing provider status"); + try { + for (ExternalDebugInfoProviderTableRow row : rowsCopy) { + if (monitor.isCancelled()) { + break; + } + monitor.setMessage("Checking " + row.getItem().getName()); + monitor.incrementProgress(); + + DebugInfoProvider provider = row.getItem(); + row.setStatus(provider.getStatus(monitor)); + } + } + finally { + Swing.runLater(() -> tableModel.fireTableDataChanged()); + } + }); + } + + private JScrollPane buildTable() { + table = new GTable(tableModel); + table.setVisibleRowCount(4); + table.setUserSortingEnabled(false); + table.getSelectionManager() + .addListSelectionListener(e -> updatePanelButtonEnablement()); + + table.setPreferredScrollableViewportSize(new Dimension(500, 100)); + + return new JScrollPane(table); + } + + private JPanel buildButtonPanel() { + refreshSearchLocationsStatusButton = + createImageButton(Icons.REFRESH_ICON, "Refresh Status", "ButtonActions"); + refreshSearchLocationsStatusButton.addActionListener(e -> refreshStatus()); + + moveLocationUpButton = createImageButton(Icons.UP_ICON, "Up", "ButtonActions"); + moveLocationUpButton.addActionListener(e -> moveLocation(-1)); + moveLocationUpButton.setToolTipText("Move location up"); + + moveLocationDownButton = createImageButton(Icons.DOWN_ICON, "Down", "ButtonActions"); + moveLocationDownButton.addActionListener(e -> moveLocation(1)); + moveLocationDownButton.setToolTipText("Move location down"); + + deleteLocationButton = createImageButton(Icons.DELETE_ICON, "Delete", "ButtonActions"); + deleteLocationButton.addActionListener(e -> deleteLocation()); + + addLocationButton = createImageButton(Icons.ADD_ICON, "Add", "ButtonActions"); + addLocationButton.addActionListener(e -> addLocation()); + + saveSearchLocationsButton = + createImageButton(Icons.SAVE_ICON, "Save Configuration", "ButtonActions"); + saveSearchLocationsButton.addActionListener(e -> saveConfig()); + + JPanel tableButtonPanel = new JPanel(); + tableButtonPanel.setLayout(new BoxLayout(tableButtonPanel, BoxLayout.X_AXIS)); + tableButtonPanel.add(new GLabel("Additional Locations:")); + tableButtonPanel.add(Box.createHorizontalGlue()); + tableButtonPanel.add(addLocationButton); + tableButtonPanel.add(deleteLocationButton); + tableButtonPanel.add(moveLocationUpButton); + tableButtonPanel.add(moveLocationDownButton); + tableButtonPanel.add(refreshSearchLocationsStatusButton); + tableButtonPanel.add(saveSearchLocationsButton); + + return tableButtonPanel; + } + + private JPanel buildLocationPanel() { + storageLocationTextField = new HintTextField(" Required "); + storageLocationTextField.setEditable(false); + storageLocationTextField.setFocusable(false); + storageLocationTextField.setToolTipText( + "User-specified directory where debug files are stored. Required."); + + JButton chooseStorageLocationButton = new BrowseButton(); + chooseStorageLocationButton.addActionListener(e -> chooseStorageLocation()); + registerHelp(chooseStorageLocationButton, "LocalStorage"); + + File ghidraCacheDir = + LocalDirDebugInfoDProvider.getGhidraCacheInstance().getDirectory(); + + JButton chooseGhidraCacheLocationButton = + createImageButton(new GIcon("icon.base.application.home"), + "Use private Ghidra cache location\n" + ghidraCacheDir, "LocalStorage"); + chooseGhidraCacheLocationButton.addActionListener(e -> chooseGhidraCacheLocation()); + + JPanel storageButtonPanel = new JPanel(); + storageButtonPanel.setLayout(new BoxLayout(storageButtonPanel, BoxLayout.X_AXIS)); + storageButtonPanel.add(chooseStorageLocationButton, BorderLayout.CENTER); + storageButtonPanel.add(chooseGhidraCacheLocationButton); + + GLabel storageLocLabel = new GLabel("Local Storage:", SwingConstants.RIGHT); + storageLocLabel.setToolTipText(storageLocationTextField.getToolTipText()); + + storageLocationPanel = new JPanel(new ThreeColumnLayout(5, 5, 5)); + storageLocationPanel.add(storageLocLabel); + storageLocationPanel.add(storageLocationTextField); + storageLocationPanel.add(storageButtonPanel); + return storageLocationPanel; + } + + private void updatePanelButtonEnablement() { + boolean singleRow = table.getSelectedRowCount() == 1; + boolean moreThanOneRow = table.getRowCount() > 1; + + refreshSearchLocationsStatusButton.setEnabled(!tableModel.isEmpty()); + moveLocationUpButton.setEnabled(singleRow && moreThanOneRow); + moveLocationDownButton.setEnabled(singleRow && moreThanOneRow); + addLocationButton.setEnabled(true); + deleteLocationButton.setEnabled(table.getSelectedRowCount() > 0); + saveSearchLocationsButton.setEnabled(isConfigChanged()); + } + + private void chooseStorageLocation() { + GhidraFileChooser chooser = getChooser("Choose Debug File Storage Directory"); + File f = chooser.getSelectedFile(); + chooser.dispose(); + + if (f != null) { + configChanged = true; + setStorageLocation(new LocalDirDebugInfoDProvider(f)); + updateButtonEnablement(); + } + } + + private void chooseGhidraCacheLocation() { + configChanged = true; + setStorageLocation(LocalDirDebugInfoDProvider.getGhidraCacheInstance()); + updateButtonEnablement(); + } + + private void importLocations() { + String envVar = (String) JOptionPane.showInputDialog(this, """ + Enter value:
      +
      + Example: https://debuginfod.domain1.org https://debuginfod.domain2.org
      +
      """, "Enter DEBUGINFOD_URLS Value", JOptionPane.QUESTION_MESSAGE, null, + null, Objects.requireNonNullElse(System.getenv("DEBUGINFOD_URLS"), "")); + if (envVar == null) { + return; + } + + List urls = getURLsFromEnvStr(envVar); + urls.forEach( + s -> tableModel.addItem(creatorContext.registry().create(s, creatorContext))); + updateButtonEnablement(); + } + + private void addLocation() { + JPopupMenu menu = createAddLocationPopupMenu(); + menu.show(addLocationButton, 0, 0); + } + + private JPopupMenu createAddLocationPopupMenu() { + JPopupMenu menu = new JPopupMenu(); + registerHelp(menu, "LocationTypes"); + + JMenuItem addProgLocMenuItem = new JMenuItem(SameDirDebugInfoProvider.DESC); + addProgLocMenuItem.addActionListener(e -> addSameDirLocation()); + addProgLocMenuItem + .setToolTipText("Directory that the program was originally imported from."); + menu.add(addProgLocMenuItem); + + JMenuItem addBuildIdDirMenuItem = new JMenuItem("Build-id Directory"); + addBuildIdDirMenuItem.addActionListener(e -> addBuildIdDirLocation()); + addBuildIdDirMenuItem.setToolTipText( + "Directory where debug files that are identified by a build-id hash are stored.\n" + + "Debug files are named AA/BBCCDD...ZZ.debug under the base directory\n" + + "This storage scheme for build-id debug files is distinct from debuginfod's scheme.\n\n" + + "e.g. /usr/lib/debug/.build-id"); + menu.add(addBuildIdDirMenuItem); + + JMenuItem addDebugLinkDirMenuItem = new JMenuItem("Debug Link Directory"); + addDebugLinkDirMenuItem.addActionListener(e -> addDebugLinkDirLocation()); + addDebugLinkDirMenuItem + .setToolTipText("Directory where debug files that are identified\n" + + "by a debug filename and crc hash\n" + + "(found in the binary's .gnu_debuglink section).\n\n" + + "NOTE: This directory is searched recursively for a matching file."); + menu.add(addDebugLinkDirMenuItem); + + JMenuItem addDebugInfoDDirMenuItem = new JMenuItem("Debuginfod Directory"); + addDebugInfoDDirMenuItem.addActionListener(e -> addDebugInfoDDirLocation()); + addDebugInfoDDirMenuItem.setToolTipText("Directory where debuginfod has stored files."); + menu.add(addDebugInfoDDirMenuItem); + + JMenuItem addURLMenuItem = new JMenuItem("Debuginfod URL"); + addURLMenuItem.addActionListener(e -> addUrlLocation()); + addURLMenuItem.setToolTipText("HTTP(s) URL that points to a debuginfod server."); + menu.add(addURLMenuItem); + + JMenuItem importEnvMenuItem = new JMenuItem("Import DEBUGINFOD_URLS Env Var"); + importEnvMenuItem.addActionListener(e -> importLocations()); + importEnvMenuItem.setToolTipText( + "Adds debuginfod URLs found in the system environment variable."); + menu.add(importEnvMenuItem); + + if (!knownProviders.isEmpty()) { + menu.add(new JSeparator()); + for (WellKnownDebugProvider provider : knownProviders) { + JMenuItem mi = new JMenuItem(provider.location()); + mi.addActionListener(e -> addKnownLocation(provider)); + mi.setToolTipText("Debuginfod URL [from " + provider.fileOrigin() + "]"); + menu.add(mi); + } + } + return menu; + } + + private void addSameDirLocation() { + SameDirDebugInfoProvider provider = new SameDirDebugInfoProvider(null); + tableModel.addItem(provider); + } + + private void addKnownLocation(WellKnownDebugProvider providerInfo) { + DebugInfoProvider newProvider = + creatorContext.registry().create(providerInfo.location(), creatorContext); + if (newProvider != null) { + tableModel.addItem(newProvider); + } + } + + private void addUrlLocation() { + String urlStr = OptionDialog.showInputSingleLineDialog(this, "Enter URL", + "Enter the URL of a Debuginfod Server: ", "https://"); + if (urlStr == null || urlStr.isBlank() || urlStr.equals("https://")) { + return; + } + try { + urlStr = urlStr.toLowerCase(); + if (urlStr.startsWith("http://") || urlStr.startsWith("https://")) { + HttpDebugInfoDProvider newProvider = + new HttpDebugInfoDProvider(URI.create(urlStr)); + tableModel.addItem(newProvider); + return; // success + } + } + catch (IllegalArgumentException e) { + // fall thru + } + Msg.showWarn(this, this, "Bad URL", "Invalid URL: " + urlStr); + } + + private void addBuildIdDirLocation() { + File dir = + FilePromptDialog.chooseDirectory("Enter Path", "Build-Id Root Directory: ", null); + if (dir == null) { + return; + } + if (!dir.exists() || !dir.isDirectory()) { + Msg.showError(this, this, "Bad path", "Invalid path: " + dir); + return; + } + BuildIdDebugFileProvider provider = new BuildIdDebugFileProvider(dir); + tableModel.addItem(provider); + } + + private void addDebugLinkDirLocation() { + File dir = + FilePromptDialog.chooseDirectory("Enter Path", "Debug-Link Root Directory: ", null); + if (dir == null) { + return; + } + if (!dir.exists() || !dir.isDirectory()) { + Msg.showError(this, this, "Bad path", "Invalid path: " + dir); + return; + } + LocalDirDebugLinkProvider provider = new LocalDirDebugLinkProvider(dir); + tableModel.addItem(provider); + } + + private void addDebugInfoDDirLocation() { + File dir = FilePromptDialog.chooseDirectory("Enter Path", + "Debuginfod Cache Directory: ", null); + if (dir == null) { + return; + } + if (!dir.exists() || !dir.isDirectory()) { + Msg.showError(this, this, "Bad path", "Invalid path: " + dir); + return; + } + LocalDirDebugInfoDProvider provider = new LocalDirDebugInfoDProvider(dir); + tableModel.addItem(provider); + } + + private void deleteLocation() { + int selectedRow = table.getSelectedRow(); + tableModel.deleteRows(table.getSelectedRows()); + if (selectedRow >= 0 && selectedRow < table.getRowCount()) { + table.selectRow(selectedRow); + } + } + + private void moveLocation(int delta) { + if (table.getSelectedRowCount() == 1) { + tableModel.moveRow(table.getSelectedRow(), delta); + } + } + + private GhidraFileChooser getChooser(String title) { + + GhidraFileChooser chooser = new GhidraFileChooser(this); + chooser.setMultiSelectionEnabled(false); + chooser.setApproveButtonText("Choose"); + chooser.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY); + chooser.setTitle(title); + + return chooser; + } + + } + + //--------------------------------------------------------------------------------------------- + + private JButton createImageButton(Icon buttonIcon, String alternateText, String helpLoc) { + + JButton button = new GButton(buttonIcon); + button.setToolTipText(alternateText); + button.setPreferredSize(BUTTON_SIZE); + registerHelp(button, helpLoc); + + return button; + } + + private static List getURLsFromEnvStr(String envString) { + String[] envParts = envString.split("[ ;]"); + List results = new ArrayList<>(); + Set dedup = new HashSet<>(); + for (String envPart : envParts) { + String s = envPart.trim(); + if (!s.isBlank() && dedup.add(s)) { + results.add(s); + } + } + + return results; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableModel.java new file mode 100644 index 0000000000..1c14e03408 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableModel.java @@ -0,0 +1,255 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external.gui; + +import java.awt.FontMetrics; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.Icon; +import javax.swing.table.TableColumn; + +import docking.widgets.table.*; +import generic.theme.GIcon; +import ghidra.app.util.bin.format.dwarf.external.DebugInfoProvider; +import ghidra.app.util.bin.format.dwarf.external.DebugInfoProviderStatus; +import ghidra.docking.settings.Settings; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.framework.plugintool.ServiceProviderStub; +import ghidra.util.table.column.GColumnRenderer; +import resources.Icons; + +/** + * Table model for the {@link ExternalDebugFilesConfigDialog} table + */ +class ExternalDebugInfoProviderTableModel + extends GDynamicColumnTableModel> { + + private List rows = new ArrayList<>(); + private boolean dataChanged; + + ExternalDebugInfoProviderTableModel() { + super(new ServiceProviderStub()); + setDefaultTableSortState(null); + } + + boolean isEmpty() { + return rows.isEmpty(); + } + + void setItems(List newItems) { + rows.clear(); + for (DebugInfoProvider item : newItems) { + rows.add(new ExternalDebugInfoProviderTableRow(item)); + } + fireTableDataChanged(); + } + + List getItems() { + return rows.stream().map(ExternalDebugInfoProviderTableRow::getItem).toList(); + } + + void addItem(DebugInfoProvider newItem) { + ExternalDebugInfoProviderTableRow row = new ExternalDebugInfoProviderTableRow(newItem); + rows.add(row); + dataChanged = true; + fireTableDataChanged(); + } + + void addItems(List newItems) { + for (DebugInfoProvider item : newItems) { + rows.add(new ExternalDebugInfoProviderTableRow(item)); + } + dataChanged = true; + fireTableDataChanged(); + } + + void deleteRows(int[] rowIndexes) { + for (int i = rowIndexes.length - 1; i >= 0; i--) { + rows.remove(rowIndexes[i]); + } + dataChanged = true; + fireTableDataChanged(); + } + + void moveRow(int rowIndex, int deltaIndex) { + int destIndex = rowIndex + deltaIndex; + if (rowIndex < 0 || rowIndex >= rows.size() || destIndex < 0 || destIndex >= rows.size()) { + return; + } + + ExternalDebugInfoProviderTableRow row1 = rows.get(rowIndex); + ExternalDebugInfoProviderTableRow row2 = rows.get(destIndex); + rows.set(destIndex, row1); + rows.set(rowIndex, row2); + + dataChanged = true; + + fireTableDataChanged(); + } + + boolean isDataChanged() { + return dataChanged; + } + + void setDataChanged(boolean b) { + this.dataChanged = b; + } + + @Override + public String getName() { + return "External Debug Info Providers"; + } + + @Override + public List getModelData() { + return rows; + } + + @Override + public List getDataSource() { + return rows; + } + + @Override + public boolean isSortable(int columnIndex) { + return false; + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + DynamicTableColumn column = getColumn(columnIndex); + if (column instanceof EnabledColumn && aValue instanceof Boolean boolVal) { + ExternalDebugInfoProviderTableRow row = getRowObject(rowIndex); + row.setEnabled(boolVal); + dataChanged = true; + fireTableDataChanged(); + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + DynamicTableColumn column = getColumn(columnIndex); + return column instanceof EnabledColumn; + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + + descriptor.addVisibleColumn(new EnabledColumn()); + descriptor.addVisibleColumn(new StatusColumn()); + descriptor.addVisibleColumn(new LocationColumn()); + + return descriptor; + } + + //------------------------------------------------------------------------------------------- + static class EnabledColumn + extends AbstractDynamicTableColumnStub + implements TableColumnInitializer { + + @Override + public String getColumnDisplayName(Settings settings) { + return "Enabled"; + } + + @Override + public Boolean getValue(ExternalDebugInfoProviderTableRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.isEnabled(); + } + + @Override + public String getColumnName() { + return "Enabled"; + } + + @Override + public void initializeTableColumn(TableColumn col, FontMetrics fm, int padding) { + int colWidth = fm.stringWidth("Enabled") + padding; + col.setPreferredWidth(colWidth); + col.setMaxWidth(colWidth * 2); + col.setMinWidth(colWidth); + } + + } + + private static class StatusColumn extends + AbstractDynamicTableColumnStub + implements TableColumnInitializer { + + private static final Icon VALID_ICON = new GIcon("icon.checkmark.green"); + private static final Icon INVALID_ICON = Icons.ERROR_ICON; + + private static Icon[] icons = new Icon[] { null, VALID_ICON, INVALID_ICON }; + private static String[] toolTips = new String[] { null, "Status: Ok", "Status: Failed" }; + + EnumIconColumnRenderer renderer = + new EnumIconColumnRenderer<>(DebugInfoProviderStatus.class, icons, toolTips); + + @Override + public DebugInfoProviderStatus getValue(ExternalDebugInfoProviderTableRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getStatus(); + } + + @Override + public String getColumnDisplayName(Settings settings) { + return "Status"; + } + + @Override + public String getColumnName() { + return "Status"; + } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + + @Override + public void initializeTableColumn(TableColumn col, FontMetrics fm, int padding) { + int colWidth = fm.stringWidth("Status") + padding; + col.setPreferredWidth(colWidth); + col.setMaxWidth(colWidth * 2); + col.setMinWidth(colWidth); + } + + } + + private class LocationColumn + extends AbstractDynamicTableColumnStub { + + @Override + public String getValue(ExternalDebugInfoProviderTableRow rowObject, Settings settings, + ServiceProvider serviceProvider) throws IllegalArgumentException { + return rowObject.getItem().getDescriptiveName(); + } + + @Override + public String getColumnName() { + return "Location"; + } + + @Override + public int getColumnPreferredWidth() { + return 250; + } + + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableRow.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableRow.java new file mode 100644 index 0000000000..a89f68c06e --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/ExternalDebugInfoProviderTableRow.java @@ -0,0 +1,73 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external.gui; + + +import ghidra.app.util.bin.format.dwarf.external.*; + +/** + * Represents a row in a ExternalDebugInfoProviderTableModel + */ +class ExternalDebugInfoProviderTableRow { + + private DebugInfoProvider item; + private DebugInfoProviderStatus status = DebugInfoProviderStatus.UNKNOWN; + + ExternalDebugInfoProviderTableRow(DebugInfoProvider item) { + this.item = item; + } + + DebugInfoProvider getItem() { + return item; + } + + void setItem(DebugInfoProvider newItem) { + this.item = newItem; + } + + DebugInfoProviderStatus getStatus() { + return status; + } + + void setStatus(DebugInfoProviderStatus status) { + this.status = status; + } + + boolean isEnabled() { + return !(item instanceof DisabledDebugInfoProvider); + } + + void setEnabled(boolean enabled) { + if (isEnabled() == enabled) { + return; + } + status = DebugInfoProviderStatus.UNKNOWN; + if (enabled) { + DisabledDebugInfoProvider dss = (DisabledDebugInfoProvider) item; + item = dss.getDelegate(); + } + else { + item = new DisabledDebugInfoProvider(item); + } + } + + @Override + public String toString() { + return String.format("SearchLocationsTableRow: [ status: %s, item: %s]", status.toString(), + item.toString()); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/FilePromptDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/FilePromptDialog.java new file mode 100644 index 0000000000..748ba56519 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/FilePromptDialog.java @@ -0,0 +1,207 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external.gui; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.io.File; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import docking.DialogComponentProvider; +import docking.DockingWindowManager; +import docking.widgets.OptionDialog; +import docking.widgets.button.BrowseButton; +import docking.widgets.filechooser.GhidraFileChooser; +import docking.widgets.filechooser.GhidraFileChooserMode; +import docking.widgets.label.GHtmlLabel; +import ghidra.util.filechooser.GhidraFileFilter; +import ghidra.util.layout.ThreeColumnLayout; + +/** + * Non-public, package-only dialog that prompts the user to enter a path + * in a text field (similar to an {@link OptionDialog}) and allows them to click + * a "..." browse button to pick the file and/or directory via a + * {@link GhidraFileChooser} dialog. + */ +class FilePromptDialog extends DialogComponentProvider { + + /** + * Prompts the user to enter the path to a directory, + * or to pick it using a browser dialog. + * + * @param title the dialog title + * @param prompt HTML enabled prompt + * @param initialValue initial value to pre-populate the input field with + * @return the {@link File} the user entered / picked, or null if canceled + */ + public static File chooseDirectory(String title, String prompt, File initialValue) { + return chooseFile(title, prompt, "Choose", null, initialValue, + GhidraFileChooserMode.DIRECTORIES_ONLY); + } + + /** + * Prompts the user to entry the path to a file and/or directory, + * or to pick it using a browser dialog. + * + * @param title the dialog title + * @param prompt HTML enabled prompt + * @param chooseButtonText text of the choose button in the browser dialog + * @param directory the initial directory of the browser dialog + * @param initialFileValue the initial value to pre-populate the input field with + * @param chooserMode {@link GhidraFileChooserMode} of the browser dialog + * @param fileFilters optional {@link GhidraFileFilter filters} + * @return the {@link File} the user entered / picked, or null if canceled + */ + public static File chooseFile(String title, String prompt, String chooseButtonText, + File directory, File initialFileValue, GhidraFileChooserMode chooserMode, + GhidraFileFilter... fileFilters) { + FilePromptDialog filePromptDialog = new FilePromptDialog(title, prompt, chooseButtonText, + directory, initialFileValue, chooserMode, fileFilters); + DockingWindowManager.showDialog(filePromptDialog); + File file = filePromptDialog.chosenValue; + filePromptDialog.dispose(); + return file; + } + + private GhidraFileChooser chooser; + private GhidraFileFilter[] fileFilters; + private File directory; + private File file; + private String approveButtonText; + private JTextField filePathTextField; + private GhidraFileChooserMode chooserMode; + private File chosenValue; + + protected FilePromptDialog(String title, String prompt, String approveButtonText, + File directory, File file, GhidraFileChooserMode chooserMode, + GhidraFileFilter... fileFilters) { + super(title, true, false, true, false); + + this.approveButtonText = approveButtonText; + this.directory = directory; + this.file = file; + this.chooserMode = chooserMode; + this.fileFilters = fileFilters; + setRememberSize(false); + + build(prompt); + updateButtonEnablement(); + } + + private void build(String prompt) { + + GHtmlLabel promptLabel = new GHtmlLabel(prompt); + promptLabel.getAccessibleContext().setAccessibleName(prompt); + filePathTextField = new JTextField(file != null ? file.getPath() : null, 40); + filePathTextField.getAccessibleContext().setAccessibleName("File Path"); + filePathTextField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void removeUpdate(DocumentEvent e) { + updateButtonEnablement(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + updateButtonEnablement(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + updateButtonEnablement(); + } + }); + JButton browseButton = new BrowseButton(); + browseButton.addActionListener(e -> browse()); + browseButton.getAccessibleContext().setAccessibleName("Browse"); + + JPanel mainPanel = new JPanel(new ThreeColumnLayout()); + mainPanel.add(promptLabel); + mainPanel.add(filePathTextField); + mainPanel.add(browseButton); + mainPanel.getAccessibleContext().setAccessibleName("File Prompt"); + + Dimension size = mainPanel.getPreferredSize(); + size.width = Math.max(size.width, 500); + mainPanel.setPreferredSize(size); + mainPanel.setMinimumSize(size); + JPanel newMain = new JPanel(new BorderLayout()); + newMain.add(mainPanel, BorderLayout.CENTER); + newMain.getAccessibleContext().setAccessibleName("File Prompt"); + addWorkPanel(newMain); + addOKButton(); + addCancelButton(); + } + + @Override + public void dispose() { + super.dispose(); + + if (chooser != null) { + chooser.dispose(); + } + } + + private void updateButtonEnablement() { + okButton.setEnabled(!filePathTextField.getText().isBlank()); + } + + @Override + protected void okCallback() { + chosenValue = new File(filePathTextField.getText()); + close(); + } + + @Override + protected void cancelCallback() { + chosenValue = null; + close(); + } + + private void browse() { + initChooser(); + String filePathText = filePathTextField.getText(); + filePathText = filePathText.isBlank() && file != null ? file.getPath() : ""; + if (!filePathText.isBlank()) { + chooser.setSelectedFile(new File(filePathText)); + } + File selectedFile = chooser.getSelectedFile(); + if (selectedFile != null) { + filePathTextField.setText(selectedFile.getPath()); + } + filePathTextField.requestFocusInWindow(); + } + + private void initChooser() { + + if (chooser == null) { + chooser = new GhidraFileChooser(rootPanel); + for (GhidraFileFilter gff : fileFilters) { + chooser.addFileFilter(gff); + } + chooser.setMultiSelectionEnabled(false); + chooser.setApproveButtonText(approveButtonText); + chooser.setFileSelectionMode(chooserMode); + chooser.setTitle(getTitle()); + + if (directory != null) { + chooser.setCurrentDirectory(directory); + } + } + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/TableColumnInitializer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/TableColumnInitializer.java new file mode 100644 index 0000000000..46267ff513 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/TableColumnInitializer.java @@ -0,0 +1,62 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external.gui; + +import java.awt.FontMetrics; + +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +import docking.ComponentProvider; +import docking.DialogComponentProvider; +import docking.widgets.table.*; + +/** + * Add on interface for DynamicTableColumn classes inside a SearchLocationTableModel that let + * them control aspects of the matching TableColumn. + */ +public interface TableColumnInitializer { + /** + * Best called during {@link DialogComponentProvider#dialogShown} or + * {@link ComponentProvider#componentShown} + * + * @param table table component + * @param model table model + */ + static void initializeTableColumns(GTable table, GDynamicColumnTableModel model) { + TableColumnModel colModel = table.getColumnModel(); + + FontMetrics fm = table.getTableHeader().getFontMetrics(table.getTableHeader().getFont()); + int padding = fm.stringWidth("WW"); // w.a.g. for the left+right padding on the header column component + + for (int colIndex = 0; colIndex < model.getColumnCount(); colIndex++) { + DynamicTableColumn dtableCol = model.getColumn(colIndex); + if (dtableCol instanceof TableColumnInitializer colInitializer) { + TableColumn tableCol = colModel.getColumn(colIndex); + colInitializer.initializeTableColumn(tableCol, fm, padding); + } + } + } + + /** + * Called to allow the initializer to modify the specified TableColumn + * + * @param col {@link TableColumn} + * @param fm {@link FontMetrics} used by the table header gui component + * @param padding padding to use in the column + */ + void initializeTableColumn(TableColumn col, FontMetrics fm, int padding); +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/WellKnownDebugProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/WellKnownDebugProvider.java new file mode 100644 index 0000000000..b9eeac6d58 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/external/gui/WellKnownDebugProvider.java @@ -0,0 +1,71 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external.gui; + +import java.io.IOException; +import java.util.*; + +import generic.jar.ResourceFile; +import ghidra.framework.Application; +import ghidra.util.Msg; +import utilities.util.FileUtilities; + +/** + * Represents a debug file search location that has been pre-provided by a Ghidra config file. + * + * @param location url string + * @param locationCategory grouping criteria + * @param warning string + * @param fileOrigin file name that contained this info + */ +public record WellKnownDebugProvider(String location, String locationCategory, + String warning, String fileOrigin) { + + /** + * Loads information about wellknown debuginfod servers from any matching file found in the + * application and returns a list of entries. + * + * @param fileExt extension of the url files to find + * @return list of {@link WellKnownDebugProvider} elements + */ + public static List loadAll(String fileExt) { + List files = Application.findFilesByExtensionInApplication(fileExt); + Set seenProviders = new HashSet<>(); + List results = new ArrayList<>(); + for (ResourceFile file : files) { + try { + List lines = FileUtilities.getLines(file); + for (String line : lines) { + // format: location_category|location_string|warning_string + // example: "Internet|https://msdl.microsoft.com/download/symbols|Warning: be careful!" + String[] fields = line.split("\\|"); + if (fields.length > 1) { + WellKnownDebugProvider provider = new WellKnownDebugProvider(fields[1], + fields[0], fields.length > 2 ? fields[2] : null, file.getName()); + if (seenProviders.add(provider)) { + results.add(provider); + } + } + } + } + catch (IOException e) { + Msg.warn(WellKnownDebugProvider.class, "Unable to read file: " + file); + } + } + return results; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/sectionprovider/ExternalDebugFileSectionProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/sectionprovider/ExternalDebugFileSectionProvider.java index 4a5d2776c4..ed57e9be4d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/sectionprovider/ExternalDebugFileSectionProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/bin/format/dwarf/sectionprovider/ExternalDebugFileSectionProvider.java @@ -15,17 +15,22 @@ */ package ghidra.app.util.bin.format.dwarf.sectionprovider; +import java.io.File; import java.io.IOException; import java.net.MalformedURLException; +import java.nio.file.AccessMode; import java.util.List; import ghidra.app.util.Option; import ghidra.app.util.bin.ByteProvider; -import ghidra.app.util.bin.format.dwarf.external.*; +import ghidra.app.util.bin.FileByteProvider; +import ghidra.app.util.bin.format.dwarf.external.ExternalDebugFilesService; +import ghidra.app.util.bin.format.dwarf.external.ExternalDebugInfo; import ghidra.app.util.importer.MessageLog; import ghidra.app.util.opinion.*; import ghidra.app.util.opinion.Loader.ImporterSettings; -import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.FSRL; +import ghidra.formats.gfilesystem.FileSystemService; import ghidra.framework.options.Options; import ghidra.plugin.importer.ImporterUtilities; import ghidra.program.database.ProgramDB; @@ -56,20 +61,16 @@ public class ExternalDebugFileSectionProvider extends BaseSectionProvider { } Msg.info(ExternalDebugFileSectionProvider.class, "DWARF external debug information found: " + extDebugInfo); - ExternalDebugFilesService edfs = - DWARFExternalDebugFilesPlugin.getExternalDebugFilesService( - SearchLocationRegistry.getInstance().newContext(program)); - FSRL extDebugFile = edfs.findDebugFile(extDebugInfo, monitor); + ExternalDebugFilesService edfs = ExternalDebugFilesService.forProgram(program); + File extDebugFile = edfs.find(extDebugInfo, monitor); if (extDebugFile == null) { return null; } Msg.info(ExternalDebugFileSectionProvider.class, "DWARF External Debug File: found: " + extDebugFile); - FileSystemService fsService = FileSystemService.getInstance(); - try ( - RefdFile refdDebugFile = fsService.getRefdFile(extDebugFile, monitor); - ByteProvider debugFileByteProvider = - fsService.getByteProvider(refdDebugFile.file.getFSRL(), false, monitor);) { + FSRL fsrl = FileSystemService.getInstance().getLocalFSRL(extDebugFile); + try (ByteProvider debugFileByteProvider = + new FileByteProvider(extDebugFile, fsrl, AccessMode.READ)) { Object consumer = new Object(); Language lang = program.getLanguage(); LoadSpec origLoadSpec = ImporterUtilities.getLoadSpec(program); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/test/MockHttpServerUtils.java b/Ghidra/Features/Base/src/main/java/ghidra/test/MockHttpServerUtils.java new file mode 100644 index 0000000000..2b8176b8b2 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/test/MockHttpServerUtils.java @@ -0,0 +1,203 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.test; + +import static java.net.HttpURLConnection.*; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.*; +import java.util.Objects; + +import com.sun.net.httpserver.*; + +import ghidra.util.Msg; + +public class MockHttpServerUtils { + private static int LAST_SERVER_PORT_NUM = 8000 + 5000; + public static final String CONTENT_TYPE_HEADER = "Content-Type"; + + /** + * Convert a mock http server's address to a URL + * + * @param addr {@link InetSocketAddress} + * @return http connection URI, example "http://127.0.0.1:9999" + */ + public static URI getURI(InetSocketAddress addr) { + return URI.create("http://%s:%d".formatted(addr.getHostString(), addr.getPort())); + } + + /** + * {@return the next hopefully unused localhost socket addr} + */ + public static InetSocketAddress nextLoopbackServerAddr() { + InetSocketAddress serverAddr = + new InetSocketAddress(InetAddress.getLoopbackAddress(), LAST_SERVER_PORT_NUM); + LAST_SERVER_PORT_NUM++; // don't try to reuse the same server port num in the same session + return serverAddr; + } + + /** + * Creates an HttpServer, listening on localhost and a unique unused port number. + *

      + * Use {@link HttpServer#createContext(String, HttpHandler)} to add handlers for specific + * paths. + * + * @return new {@link HttpServer} + * @throws IOException if unused port is not found + */ + public static HttpServer createMockHttpServer() throws IOException { + IOException lastException = null; + for (int retryNum = 0; retryNum < 10; retryNum++) { + InetSocketAddress serverAddress = nextLoopbackServerAddr(); + + try { + HttpServer server = HttpServer.create(serverAddress, 0); + return server; + } + catch (IOException e) { + // ignore, just try again with next port num + lastException = e; + } + } + throw new IOException( + "Could not allocate port for mock http server, last attempted port: " + + LAST_SERVER_PORT_NUM, + lastException); + } + + /** + * Asserts that the specified {@link HttpExchange} has a specific content type header. + * + * @param expectedType example: "application/json" + * @param httpExchange {@link HttpExchange} + */ + public static void assertContentType(String expectedType, HttpExchange httpExchange) { + String contentType = httpExchange.getRequestHeaders().getFirst(CONTENT_TYPE_HEADER); + contentType = Objects.requireNonNullElse(contentType, "missing"); + if (!expectedType.equals(contentType)) { + fail("Content type incorrect: expected: %s, actual: %s".formatted(expectedType, + contentType)); + } + } + + /** + * Adds a delay to a handler. + * + * @param delegate {@link HttpHandler} to wrap + * @param delayMS milliseconds to delay before allowing the delegate to process the request + * @return new HttpHandler that wraps the specified delegate + */ + public static HttpHandler wrapHandlerWithDelay(HttpHandler delegate, int delayMS) { + return httpExchange -> { + try { + Thread.sleep(delayMS); + } + catch (InterruptedException e) { + // ignore + } + delegate.handle(httpExchange); + }; + } + + public static HttpHandler wrapHandlerWithRetryError(HttpHandler delegate, int errorCount, + int errorStatus) { + return new HttpHandler() { + int errorNum; + + @Override + public void handle(HttpExchange exchange) throws IOException { + if (errorNum++ < errorCount) { + exchange.sendResponseHeaders(errorStatus, 0); + exchange.close(); + return; + } + delegate.handle(exchange); + } + }; + } + + /** + * A handler that always returns a 404. Use this as the target of a lambda. This matches + * the {@link HttpHandler#handle(HttpExchange)} method signature. + * + * @param httpExchange {@link HttpExchange} + * @throws IOException if error + */ + public static void mock404Handler(HttpExchange httpExchange) throws IOException { + try { + httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_NOT_FOUND, 0); + } + finally { + httpExchange.close(); + } + } + + /** + * Creates a HttpHandler that returns a specified body + * + * @param contentType http content type header value (eg. "text/plain") + * @param resultBody bytes to send as body + * @return new HttpHandler + */ + public static HttpHandler createStaticResponseHandler(String contentType, byte[] resultBody) { + return createStaticResponseHandler(HTTP_OK, contentType, resultBody); + } + + /** + * Creates a HttpHandler that returns a specified body and result code. + * + * @param resultCode http result code to return (eg. HTTP_OK / 200 ) + * @param contentType http content type header value (eg. "text/plain") + * @param resultBody bytes to send as body + * @return new HttpHandler + */ + public static HttpHandler createStaticResponseHandler(int resultCode, String contentType, + byte[] resultBody) { + return httpExchange -> { + try { + byte[] actualResult = + httpExchange.getRequestMethod().equals("GET") ? resultBody : null; + httpExchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, contentType); + httpExchange.sendResponseHeaders(resultCode, + actualResult != null ? actualResult.length : -1); + if (actualResult != null) { + httpExchange.getResponseBody().write(resultBody); + } + } + catch (Throwable th) { + logMockHttp(httpExchange, + "Error during mockStaticResponseHandler: " + th.getMessage()); + throw th; + } + finally { + httpExchange.close(); + } + }; + } + + /** + * Logs (using Msg.info) a message using information from the http connection as a prefix + * + * @param httpExchange {@link HttpExchange} + * @param msg string message + */ + public static void logMockHttp(HttpExchange httpExchange, String msg) { + Msg.info(MockHttpServerUtils.class, "[%s %s] %s".formatted(httpExchange.getLocalAddress(), + httpExchange.getRequestURI(), msg)); + + } +} diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/string/translate/libretranslate/LibreTranslateStringTranslationServiceTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/string/translate/libretranslate/LibreTranslateStringTranslationServiceTest.java index 5589e732d0..edada63d17 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/string/translate/libretranslate/LibreTranslateStringTranslationServiceTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/string/translate/libretranslate/LibreTranslateStringTranslationServiceTest.java @@ -17,11 +17,12 @@ package ghidra.app.plugin.core.string.translate.libretranslate; import static ghidra.app.plugin.core.string.translate.libretranslate.LibreTranslatePlugin.SOURCE_LANGUAGE_OPTION.*; import static ghidra.app.plugin.core.string.translate.libretranslate.LibreTranslateStringTranslationService.*; +import static ghidra.test.MockHttpServerUtils.*; +import static ghidra.test.MockHttpServerUtils.CONTENT_TYPE_HEADER; import static java.net.HttpURLConnection.*; import static org.junit.Assert.*; import java.io.IOException; -import java.net.*; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; @@ -30,7 +31,8 @@ import org.junit.Before; import org.junit.Test; import com.google.gson.*; -import com.sun.net.httpserver.*; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; import docking.AbstractErrDialog; import docking.widgets.SelectFromListDialog; @@ -43,7 +45,6 @@ import ghidra.program.model.listing.Data; import ghidra.program.util.ProgramLocation; import ghidra.test.AbstractProgramBasedTest; import ghidra.test.ToyProgramBuilder; -import ghidra.util.Msg; import ghidra.util.Swing; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -54,7 +55,6 @@ import ghidra.util.task.TaskMonitor; */ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramBasedTest { - private static int LAST_SERVER_PORT_NUM = 8000 + 5000; private int supportedLanguageCount = 10; private AtomicInteger translateRequestCount = new AtomicInteger(); // number of times translate handler has been invoked private AtomicInteger translateStringCount = new AtomicInteger(); // number of strings that translate handler has processed @@ -91,7 +91,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB // test what happens when the server accepts requests on the REST api endpoint URL, but // returns unexpected json values - HttpServer server = createMockHttpServer(false); + HttpServer server = createMockHttpServer(); server.createContext("/", this::mockUnexpectedJsonResultHandler); try { @@ -120,7 +120,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB // test what happens when the server accepts requests on the REST api endpoint URL, but its // not json - HttpServer server = createMockHttpServer(false); + HttpServer server = createMockHttpServer(); server.createContext("/", this::mockUnexpectedTextResultHandler); try { @@ -149,7 +149,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB // test what happens when the URL doesn't point to active server LibreTranslateStringTranslationService sts = new LibreTranslateStringTranslationService( - getURI(nextUnusedAddr()), null, AUTO, "en", 100, 1000, 1000); + getURI(nextLoopbackServerAddr()), null, AUTO, "en", 100, 1000, 1000); setErrorsExpected(true); // don't kill the test because Msg.showError() was called somewhere Swing.runNow(() -> sts.translate(program, List.of(progLoc(0)), TranslateOptions.NONE)); @@ -346,62 +346,8 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB return builder.getProgram(); } - private URI getURI(InetSocketAddress addr) { - return URI.create("http://%s:%d".formatted(addr.getHostString(), addr.getPort())); - } - - private HttpServer createMockHttpServer() throws IOException { - return createMockHttpServer(true); - } - - private HttpServer createMockHttpServer(boolean addDefaultHandler) throws IOException { - IOException lastException = null; - for (int retryNum = 0; retryNum < 10; retryNum++) { - LAST_SERVER_PORT_NUM++; // don't try to reuse the same server port num in the same session - InetSocketAddress serverAddress = - new InetSocketAddress(InetAddress.getLoopbackAddress(), LAST_SERVER_PORT_NUM); - - try { - HttpServer server = HttpServer.create(serverAddress, 0); - if (addDefaultHandler) { - server.createContext("/", this::mock404Handler); - } - return server; - } - catch (IOException e) { - // ignore, just try again with next port num - lastException = e; - } - } - throw new IOException( - "Could not allocate port for mock http server, last attempted port: " + - LAST_SERVER_PORT_NUM, - lastException); - } - - private InetSocketAddress nextUnusedAddr() { - LAST_SERVER_PORT_NUM++; - return new InetSocketAddress(InetAddress.getLoopbackAddress(), LAST_SERVER_PORT_NUM); - } - - private void assertContentType(HttpExchange httpExchange, String expectedType) { - String contentType = httpExchange.getRequestHeaders() - .getFirst(LibreTranslateStringTranslationService.CONTENT_TYPE_HEADER); - contentType = Objects.requireNonNullElse(contentType, "missing"); - if (!expectedType.equals(contentType)) { - fail("Content type incorrect: expected: %s, actual: %s".formatted(expectedType, - contentType)); - } - } - - private void log(HttpExchange httpExchange, String msg) { - Msg.info(this, "[%s %s] %s".formatted(httpExchange.getLocalAddress(), - httpExchange.getRequestURI(), msg)); - - } - private void mockLangHandler(HttpExchange httpExchange) throws IOException { - assertContentType(httpExchange, CONTENT_TYPE_JSON); + assertContentType(CONTENT_TYPE_JSON, httpExchange); try { JsonArray langsResult = new JsonArray(); for (int i = 0; i < supportedLanguageCount; i++) { @@ -427,7 +373,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB try { translateRequestCount.incrementAndGet(); - assertContentType(httpExchange, CONTENT_TYPE_JSON); + assertContentType(CONTENT_TYPE_JSON, httpExchange); String requestBody = new String(httpExchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); @@ -438,7 +384,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB translateSourceLangs.add(sourceLang); - log(httpExchange, + logMockHttp(httpExchange, "request src=%s, strs=%s".formatted(sourceLang, queryStrs.toString())); JsonObject xlateResultObj = new JsonObject(); @@ -447,7 +393,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB for (int i = 0; i < queryStrs.size(); i++) { xlatedResults.add("result" + translateStringCount.getAndIncrement()); } - log(httpExchange, "response: " + xlateResultObj); + logMockHttp(httpExchange, "response: " + xlateResultObj); byte[] response = xlateResultObj.toString().getBytes(); httpExchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON); @@ -455,7 +401,7 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB httpExchange.getResponseBody().write(response); } catch (Throwable th) { - log(httpExchange, "Error during mockTranslateHandler: " + th.getMessage()); + logMockHttp(httpExchange, "Error during mockTranslateHandler: " + th.getMessage()); throw th; } finally { @@ -484,27 +430,6 @@ public class LibreTranslateStringTranslationServiceTest extends AbstractProgramB httpExchange.close(); } - private HttpHandler wrapHandlerWithDelay(HttpHandler delegate, int delayMS) { - return httpExchange -> { - try { - Thread.sleep(delayMS); - } - catch (InterruptedException e) { - // ignore - } - delegate.handle(httpExchange); - }; - } - - private void mock404Handler(HttpExchange httpExchange) throws IOException { - try { - httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_NOT_FOUND, 0); - } - finally { - httpExchange.close(); - } - } - private ProgramLocation progLoc(int stringNum) { return new ProgramLocation(program, strings.get(stringNum).getAddress()); } diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProviderTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProviderTest.java new file mode 100644 index 0000000000..aa4811d619 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/BuildIdDebugFileProviderTest.java @@ -0,0 +1,57 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.junit.Before; +import org.junit.Test; + +import generic.test.AbstractGenericTest; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; + +public class BuildIdDebugFileProviderTest extends AbstractGenericTest { + private TaskMonitor monitor = TaskMonitor.DUMMY; + private File tmpDir; + + @Before + public void setUp() throws Exception { + tmpDir = createTempDirectory("buildid_provider_test"); + } + + @Test + public void testGet() throws IOException, CancelledException { + BuildIdDebugFileProvider provider = new BuildIdDebugFileProvider(tmpDir); + + String buildId = "0000000000000000000000000000000000000000"; + + File f = new File(tmpDir, + "%s/%s.debug".formatted(buildId.substring(0, 2), buildId.substring(2))); + FileUtilities.checkedMkdirs(f.getParentFile()); + FileUtilities.writeStringToFile(f, "test1"); + + File result = provider.getFile(ExternalDebugInfo.forBuildId(buildId), monitor); + + assertEquals("test1", Files.readString(result.toPath())); + assertEquals(5, result.length()); + } +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProviderTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProviderTest.java new file mode 100644 index 0000000000..72a7b99f2c --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/HttpDebugInfoDProviderTest.java @@ -0,0 +1,201 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import static ghidra.test.MockHttpServerUtils.*; +import static java.net.HttpURLConnection.*; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; + +import org.junit.Test; + +import com.sun.net.httpserver.HttpServer; + +import generic.test.AbstractGenericTest; +import ghidra.app.util.bin.format.dwarf.external.DebugStreamProvider.StreamInfo; +import ghidra.util.HashUtilities; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class HttpDebugInfoDProviderTest extends AbstractGenericTest { + private TaskMonitor monitor = TaskMonitor.DUMMY; + + @Test + public void testNoConnect() throws IOException, CancelledException { + InetSocketAddress unusedAddr = nextLoopbackServerAddr(); + HttpDebugInfoDProvider httpProvider = new HttpDebugInfoDProvider(getURI(unusedAddr)); + StreamInfo stream = httpProvider.getStream( + ExternalDebugInfo.forBuildId("0000000000000000000000000000000000000000"), monitor); + assertNull(stream); + } + + @Test + public void testGet() throws IOException, CancelledException { + String buildId = "0000000000000000000000000000000000000000"; + + HttpServer server = createMockHttpServer(); + server.createContext("/buildid/" + buildId + "/debuginfo", + createStaticResponseHandler("application/octet-stream", "result1".getBytes())); + server.createContext("/buildid/" + buildId + "/executable", + createStaticResponseHandler("application/octet-stream", "result2".getBytes())); + server.createContext("/buildid/" + buildId + "/source/usr/include/stdio.h", + createStaticResponseHandler("application/octet-stream", "result3".getBytes())); + + HttpDebugInfoDProvider httpProvider = + new HttpDebugInfoDProvider(getURI(server.getAddress())); + try { + server.start(); + + ExternalDebugInfo id = ExternalDebugInfo.forBuildId(buildId); + assertStreamResult("result1", httpProvider.getStream(id, monitor)); + assertStreamResult("result2", + httpProvider.getStream(id.withType(ObjectType.EXECUTABLE, null), monitor)); + assertStreamResult("result3", httpProvider + .getStream(id.withType(ObjectType.SOURCE, "/usr/include/stdio.h"), monitor)); + + assertEquals(0, httpProvider.getRetriedCount()); + assertEquals(0, httpProvider.getNotFoundCount()); + + } + finally { + server.stop(0); + } + } + + @Test + public void testGetWithRetry() throws IOException, CancelledException { + String buildId = "0000000000000000000000000000000000000000"; + + HttpServer server = createMockHttpServer(); + server.createContext("/buildid/" + buildId + "/debuginfo", + wrapHandlerWithRetryError( + createStaticResponseHandler("application/octet-stream", "result1".getBytes()), 3, + HTTP_INTERNAL_ERROR)); + + HttpDebugInfoDProvider httpProvider = + new HttpDebugInfoDProvider(getURI(server.getAddress())); + try { + server.start(); + + ExternalDebugInfo id = ExternalDebugInfo.forBuildId(buildId); + assertStreamResult("result1", httpProvider.getStream(id, monitor)); + assertEquals(3, httpProvider.getRetriedCount()); + } + finally { + server.stop(0); + } + } + + @Test + public void testTimeout() throws IOException, CancelledException { + String buildId = "0000000000000000000000000000000000000000"; + + HttpServer server = createMockHttpServer(); + server.createContext("/buildid/" + buildId + "/debuginfo", wrapHandlerWithDelay( + createStaticResponseHandler("application/octet-stream", "result1".getBytes()), 3000)); + + HttpDebugInfoDProvider httpProvider = + new HttpDebugInfoDProvider(getURI(server.getAddress())); + httpProvider.setMaxRetryCount(1); + httpProvider.setHttpRequestTimeoutMs(1000); + try { + server.start(); + + long startms = System.currentTimeMillis(); + ExternalDebugInfo id = ExternalDebugInfo.forBuildId(buildId); + long elapsed = System.currentTimeMillis() - startms; + assertNull(httpProvider.getStream(id, monitor)); + assertTrue("Request took too long", elapsed < (1000 * 2)); // make sure request time was approx same as timeout setting + } + finally { + server.stop(0); + } + } + + @Test + public void testGetNotFound() throws IOException, CancelledException { + HttpServer server = createMockHttpServer(); + + HttpDebugInfoDProvider httpProvider = + new HttpDebugInfoDProvider(getURI(server.getAddress())); + try { + server.start(); + + ExternalDebugInfo id = + ExternalDebugInfo.forBuildId("0000000000000000000000000000000000000000"); + assertNull(httpProvider.getStream(id, monitor)); + assertEquals(0, httpProvider.getRetriedCount()); + assertEquals(1, httpProvider.getNotFoundCount()); + } + finally { + server.stop(0); + } + } + + @Test + public void testServerError() throws IOException, CancelledException { + String buildId = "0000000000000000000000000000000000000000"; + HttpServer server = createMockHttpServer(); + server.createContext("/buildid/" + buildId + "/debuginfo", + createStaticResponseHandler(HTTP_INTERNAL_ERROR, "text/plain", "".getBytes())); + + HttpDebugInfoDProvider httpProvider = + new HttpDebugInfoDProvider(getURI(server.getAddress())); + try { + server.start(); + + ExternalDebugInfo id = + ExternalDebugInfo.forBuildId("0000000000000000000000000000000000000000"); + assertNull(httpProvider.getStream(id, monitor)); + assertEquals(4, httpProvider.getRetriedCount()); + assertEquals(0, httpProvider.getNotFoundCount()); + } + finally { + server.stop(0); + } + } + + //@Test + public void testElfUtilsOrg() throws IOException, CancelledException { + // test actual file from elfutils.org. + // Not enabled by default + // The specified buildId may stop being present at some point of time in the future + HttpDebugInfoDProvider httpProvider = + new HttpDebugInfoDProvider(URI.create("https://debuginfod.elfutils.org/")); + ExternalDebugInfo id = + ExternalDebugInfo.forBuildId("421e1abd8faf1cb290df755a558377c5d7def3b1"); + assertStreamHash("f5894783abae9084e531b8da76bbb2444a688d18", + httpProvider.getStream(id, monitor)); + } + + private void assertStreamResult(String expectedResult, StreamInfo stream) throws IOException { + try (stream) { + String result = new String(stream.is().readAllBytes()); + assertEquals(expectedResult, result); + } + } + + private void assertStreamHash(String expectedHash, StreamInfo stream) throws IOException { + try (stream) { + String hash = HashUtilities.getHash("SHA1", stream.is()); + assertEquals(expectedHash, hash); + } + } + +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProviderTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProviderTest.java new file mode 100644 index 0000000000..380bac2246 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugInfoDProviderTest.java @@ -0,0 +1,115 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import static org.junit.Assert.*; + +import java.io.*; +import java.time.Duration; + +import org.junit.Before; +import org.junit.Test; + +import generic.test.AbstractGenericTest; +import ghidra.app.util.bin.format.dwarf.external.DebugStreamProvider.StreamInfo; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; + +public class LocalDirDebugInfoDProviderTest extends AbstractGenericTest { + private TaskMonitor monitor = TaskMonitor.DUMMY; + private File tmpDir; + + @Before + public void setUp() throws Exception { + tmpDir = createTempDirectory("debuginfod_provider_test"); + } + + @Test + public void testAgeOff() throws IOException { + LocalDirDebugInfoDProvider provider = new LocalDirDebugInfoDProvider(tmpDir); + provider.purgeAll(); + + String buildId = "0000000000000000000000000000000000000000"; + + File f = new File(tmpDir, buildId + "/debuginfo"); + + FileUtilities.checkedMkdirs(f.getParentFile()); + FileUtilities.writeStringToFile(f, "test1"); + f.setLastModified(System.currentTimeMillis() - Duration.ofDays(1).toMillis()); // make it look recent + + provider.performCacheMaintIfNeeded(); + assertTrue(f.isFile()); // should still be there + + provider.purgeAll(); + + FileUtilities.checkedMkdirs(f.getParentFile()); + FileUtilities.writeStringToFile(f, "test1"); + f.setLastModified( + System.currentTimeMillis() - LocalDirDebugInfoDProvider.MAX_FILE_AGE_MS - 1000); // make it look old + + provider.performCacheMaintIfNeeded(); + assertFalse(f.isFile()); // should be gone + } + + @Test + public void testGet() throws IOException, CancelledException { + LocalDirDebugInfoDProvider provider = new LocalDirDebugInfoDProvider(tmpDir); + provider.purgeAll(); + + String buildId = "0000000000000000000000000000000000000000"; + + File f = new File(tmpDir, buildId + "/debuginfo"); + FileUtilities.checkedMkdirs(f.getParentFile()); + FileUtilities.writeStringToFile(f, "test1"); + + File result = provider.getFile(ExternalDebugInfo.forBuildId(buildId), monitor); + + assertEquals("debuginfo", result.getName()); + assertEquals(5, result.length()); + } + + @Test + public void testPut() throws IOException, CancelledException { + LocalDirDebugInfoDProvider provider = new LocalDirDebugInfoDProvider(tmpDir); + provider.purgeAll(); + + String buildId = "0000000000000000000000000000000000000000"; + byte bytes[] = "test".getBytes(); + StreamInfo stream = new StreamInfo(new ByteArrayInputStream(bytes), bytes.length); + File f = provider.putStream(ExternalDebugInfo.forBuildId(buildId), stream, monitor); + + assertEquals("debuginfo", f.getName()); + assertEquals(bytes.length, f.length()); + } + + @Test + public void testPutNonBuildId() throws CancelledException { + LocalDirDebugInfoDProvider provider = new LocalDirDebugInfoDProvider(tmpDir); + provider.purgeAll(); + + byte bytes[] = "test".getBytes(); + StreamInfo stream = new StreamInfo(new ByteArrayInputStream(bytes), bytes.length); + try { + File f = provider.putStream(ExternalDebugInfo.forDebugLink("test.debug", 0x11223344), + stream, monitor); + fail("Shouldn't get here: " + f); + } + catch (IOException e) { + // successfully failed + } + } +} diff --git a/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProviderTest.java b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProviderTest.java new file mode 100644 index 0000000000..6e568586d4 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/app/util/bin/format/dwarf/external/LocalDirDebugLinkProviderTest.java @@ -0,0 +1,55 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.util.bin.format.dwarf.external; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.junit.Before; +import org.junit.Test; + +import generic.test.AbstractGenericTest; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; +import utilities.util.FileUtilities; + +public class LocalDirDebugLinkProviderTest extends AbstractGenericTest { + private TaskMonitor monitor = TaskMonitor.DUMMY; + private File tmpDir; + + @Before + public void setUp() throws Exception { + tmpDir = createTempDirectory("debuglink_provider_test"); + } + + @Test + public void testGet() throws IOException, CancelledException { + File debugNestedDir = new File(tmpDir, "sub/sub2/sub3"); + File debugFile = new File(debugNestedDir, "debugfile.abc"); + FileUtilities.mkdirs(debugFile.getParentFile()); + Files.writeString(debugFile.toPath(), "test_debuglink"); + int crc = LocalDirDebugLinkProvider.calcCRC(debugFile); + + LocalDirDebugLinkProvider provider = new LocalDirDebugLinkProvider(tmpDir); + File result = + provider.getFile(ExternalDebugInfo.forDebugLink("debugfile.abc", crc), monitor); + + assertEquals("test_debuglink", Files.readString(result.toPath())); + } +} diff --git a/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/HttpSymbolServerTest.java b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/HttpSymbolServerTest.java index bcaccd4f8b..1e63a4a196 100644 --- a/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/HttpSymbolServerTest.java +++ b/Ghidra/Features/PDB/src/test/java/pdb/symbolserver/HttpSymbolServerTest.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,17 +15,26 @@ */ package pdb.symbolserver; +import static ghidra.test.MockHttpServerUtils.*; import static org.junit.Assert.*; +import java.io.IOException; import java.net.URI; import java.util.List; +import org.junit.Test; + +import com.sun.net.httpserver.HttpServer; + +import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; public class HttpSymbolServerTest { + private TaskMonitor monitor = TaskMonitor.DUMMY; + //@Test - public void test() { + public void testMSFTSymbolServer() { // This test is not enabled by default as it depends on an third-party resource HttpSymbolServer httpSymbolServer = new HttpSymbolServer(URI.create("http://msdl.microsoft.com/download/symbols/")); @@ -37,4 +46,68 @@ public class HttpSymbolServerTest { assertEquals(1, results.size()); } + @Test + public void testLocalHttpserverLevel1() throws IOException, CancelledException { + HttpServer server = createMockHttpServer(); + + server.createContext("/kernelbase.pdb/C1C44EDD93E1B8BA671874B5C1490C2D1/kernelbase.pdb", + createStaticResponseHandler("application/octet", "result1".getBytes())); + + try { + server.start(); + + HttpSymbolServer httpSymbolServer = new HttpSymbolServer(getURI(server.getAddress())); + SymbolFileInfo pdbInfo = + SymbolFileInfo.fromValues("kernelbase.pdb", "C1C44EDD93E1B8BA671874B5C1490C2D", 1); + List results = + httpSymbolServer.find(pdbInfo, FindOption.NO_OPTIONS, monitor); + assertEquals(1, results.size()); + SymbolFileLocation result = results.get(0); + + SymbolServerInputStream stream = + httpSymbolServer.getFileStream(result.getPath(), monitor); + assertStreamResult("result1", stream); + } + finally { + server.stop(0); + } + } + + @Test + public void testLocalHttpserverLevel2() throws IOException, CancelledException { + HttpServer server = createMockHttpServer(); + server.createContext("/index2.txt", + createStaticResponseHandler("text/plain", "".getBytes())); + + server.createContext("/ke/kernelbase.pdb/C1C44EDD93E1B8BA671874B5C1490C2D1/kernelbase.pdb", + createStaticResponseHandler("application/octet", "result1".getBytes())); + + try { + server.start(); + + HttpSymbolServer httpSymbolServer = new HttpSymbolServer(getURI(server.getAddress())); + SymbolFileInfo pdbInfo = + SymbolFileInfo.fromValues("kernelbase.pdb", "C1C44EDD93E1B8BA671874B5C1490C2D", 1); + List results = + httpSymbolServer.find(pdbInfo, FindOption.NO_OPTIONS, monitor); + assertEquals(1, results.size()); + SymbolFileLocation result = results.get(0); + + SymbolServerInputStream stream = + httpSymbolServer.getFileStream(result.getPath(), monitor); + assertStreamResult("result1", stream); + } + finally { + server.stop(0); + } + } + + private void assertStreamResult(String expectedResult, SymbolServerInputStream stream) + throws IOException { + try (stream) { + String result = new String(stream.getInputStream().readAllBytes()); + assertEquals(expectedResult, result); + } + } + } diff --git a/Ghidra/Framework/Gui/src/main/java/ghidra/util/layout/ThreeColumnLayout.java b/Ghidra/Framework/Gui/src/main/java/ghidra/util/layout/ThreeColumnLayout.java new file mode 100644 index 0000000000..d56ff10cf1 --- /dev/null +++ b/Ghidra/Framework/Gui/src/main/java/ghidra/util/layout/ThreeColumnLayout.java @@ -0,0 +1,135 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.util.layout; + +import java.awt.*; + +/** + * LayoutManger for arranging components into exactly three columns. The first and last column + * are statically sized to be the max preferred width of those columns. The middle column's width + * will vary as the panel is resized. + *

      + * This layout works well for a panel that has rows of labels followed by a field and followed by + * a trailing component like a button group. + */ +public class ThreeColumnLayout implements LayoutManager { + private static final int DEFAULT_VGAP = 5; + private static final int DEFAULT_HGAP = 5; + private static final int MIN_MAIN_COMP_WIDTH = 80; + private int vgap; + private int hgaps[]; + private int minPreferredWidths[] = new int[3]; + + public ThreeColumnLayout() { + this(DEFAULT_VGAP, new int[] { DEFAULT_HGAP, DEFAULT_HGAP }, + new int[] { 0, MIN_MAIN_COMP_WIDTH, 0 }); + } + + public ThreeColumnLayout(int vgap, int hgap1, int hgap2) { + this(vgap, new int[] { hgap1, hgap2 }, new int[] { 0, MIN_MAIN_COMP_WIDTH, 0 }); + } + + public ThreeColumnLayout(int vgap, int hgaps[], int[] minPreferredWidths) { + this.vgap = vgap; + this.hgaps = hgaps; + this.minPreferredWidths = minPreferredWidths; + } + + @Override + public void addLayoutComponent(String name, Component comp) { + // empty + } + + @Override + public void removeLayoutComponent(Component comp) { + // empty + } + + @Override + public Dimension preferredLayoutSize(Container parent) { + Dimension d = new Dimension(0, 0); + Insets insets = parent.getInsets(); + int[] widths = getPreferredWidths(parent); + d.width = + widths[0] + hgaps[0] + widths[1] + hgaps[1] + widths[2] + insets.left + insets.right; + int n = parent.getComponentCount(); + for (int i = 0; i < n; i += 3) { + Component c = parent.getComponent(i); + int height = c.getPreferredSize().height; + if (i < n - 2) { + c = parent.getComponent(i + 1); + height = Math.max(c.getPreferredSize().height, height); + c = parent.getComponent(i + 2); + height = Math.max(c.getPreferredSize().height, height); + } + d.height += height; + d.height += vgap; + } + d.height -= vgap; + d.height += insets.top + insets.bottom; + return d; + } + + @Override + public Dimension minimumLayoutSize(Container parent) { + return preferredLayoutSize(parent); + } + + @Override + public void layoutContainer(Container parent) { + int[] widths = getPreferredWidths(parent); + Dimension d = parent.getSize(); + Insets insets = parent.getInsets(); + int width = d.width - (insets.left + insets.right); + int x = insets.left; + int y = insets.top; + int width1 = widths[0]; + int width3 = widths[2]; + int width2 = + Math.max(minPreferredWidths[1], width - (width1 + width3 + hgaps[0] + hgaps[1])); + + int compCount = parent.getComponentCount(); + for (int i = 0; i < compCount; i += 3) { + Component c = parent.getComponent(i); + int height = c.getPreferredSize().height; + if (i < compCount - 2) { + Component c2 = parent.getComponent(i + 1); + Component c3 = parent.getComponent(i + 2); + height = Math.max(height, c2.getPreferredSize().height); + height = Math.max(height, c3.getPreferredSize().height); + + c2.setBounds(x + width1 + hgaps[0], y, width2, height); + c3.setBounds(x + width1 + hgaps[0] + width2 + hgaps[1], y, width3, height); + } + c.setBounds(x, y, width1, height); + y += height + vgap; + } + } + + int[] getPreferredWidths(Container parent) { + int[] widths = new int[3]; + System.arraycopy(minPreferredWidths, 0, widths, 0, 3); + int n = parent.getComponentCount(); + for (int i = 0; i < n; i++) { + Component c = parent.getComponent(i); + Dimension d = c.getPreferredSize(); + int colIndex = i % 3; + widths[colIndex] = Math.max(widths[colIndex], d.width); + } + return widths; + } + +} diff --git a/Ghidra/Framework/Utility/src/main/java/utility/application/ApplicationUtilities.java b/Ghidra/Framework/Utility/src/main/java/utility/application/ApplicationUtilities.java index ec447a3734..c47527e807 100644 --- a/Ghidra/Framework/Utility/src/main/java/utility/application/ApplicationUtilities.java +++ b/Ghidra/Framework/Utility/src/main/java/utility/application/ApplicationUtilities.java @@ -341,7 +341,7 @@ public class ApplicationUtilities { * @throws FileNotFoundException if Java's user home directory is not defined or it is not an * absolute path */ - private static File getJavaUserHomeDir() throws FileNotFoundException { + public static File getJavaUserHomeDir() throws FileNotFoundException { return getSystemPropertyFile("user.home", true); } diff --git a/Ghidra/Framework/Utility/src/main/java/utility/application/XdgUtils.java b/Ghidra/Framework/Utility/src/main/java/utility/application/XdgUtils.java index bd77b1b84c..691bb26bab 100644 --- a/Ghidra/Framework/Utility/src/main/java/utility/application/XdgUtils.java +++ b/Ghidra/Framework/Utility/src/main/java/utility/application/XdgUtils.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -66,6 +66,8 @@ public class XdgUtils { */ public static final String XDG_CACHE_HOME = "XDG_CACHE_HOME"; + public static final String XDG_CACHE_HOME_DEFAULT_SUBDIRNAME = ".cache"; + /** * $XDG_RUNTIME_DIR defines the base directory relative to which user-specific non-essential * runtime files and other file objects (such as sockets, named pipes, ...) should be stored. @@ -73,4 +75,5 @@ public class XdgUtils { * access to it. Its Unix access mode MUST be 0700. */ public static final String XDG_RUNTIME_DIR = "XDG_RUNTIME_DIR"; + } diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DWARFExternalDebugFilesPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DWARFExternalDebugFilesPluginScreenShots.java new file mode 100644 index 0000000000..db510b80f9 --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DWARFExternalDebugFilesPluginScreenShots.java @@ -0,0 +1,53 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package help.screenshot; + +import java.io.File; +import java.net.URI; +import java.util.List; + +import org.junit.Test; + +import ghidra.app.util.bin.format.dwarf.external.*; +import ghidra.app.util.bin.format.dwarf.external.gui.ExternalDebugFilesConfigDialog; + +public class DWARFExternalDebugFilesPluginScreenShots extends GhidraScreenShotGenerator { + + @Test + public void testExternalDebugFilesConfigDialog() { + LocalDirDebugInfoDProvider store = LocalDirDebugInfoDProvider.getGhidraCacheInstance(); + store = new LocalDirDebugInfoDProvider(store.getRootDir(), store.getName(), + "Ghidra Cache Dir "); + + LocalDirDebugInfoDProvider homeCache = + LocalDirDebugInfoDProvider.getUserHomeCacheInstance(); + homeCache = new LocalDirDebugInfoDProvider(homeCache.getRootDir(), homeCache.getName(), + "DebugInfoD Cache Dir "); + + ExternalDebugFilesService edfs = new ExternalDebugFilesService(store, List.of()); + edfs.addProvider(new SameDirDebugInfoProvider(null)); + edfs.addProvider(homeCache); + edfs.addProvider(new BuildIdDebugFileProvider(new File("/usr/lib/debug/.build-id"))); + edfs.addProvider(new HttpDebugInfoDProvider(URI.create("http://debuginfod.elfutils.org"))); + + ExternalDebugFilesConfigDialog dlg = new ExternalDebugFilesConfigDialog(); + dlg.setService(edfs); + showDialogWithoutBlocking(tool, dlg); + waitForSwing(); + captureDialog(ExternalDebugFilesConfigDialog.class, 600, 300); + } + +}