mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-03 09:49:23 +02:00
GP-4190 sourcefiletable plugin and path transformer
This commit is contained in:
parent
8a170df554
commit
113943048e
17 changed files with 2324 additions and 32 deletions
|
@ -560,6 +560,7 @@ src/main/help/help/topics/ShowInstructionInfoPlugin/images/RawInstructionDisplay
|
||||||
src/main/help/help/topics/ShowInstructionInfoPlugin/images/ShowInstructionInfo.png||GHIDRA||||END|
|
src/main/help/help/topics/ShowInstructionInfoPlugin/images/ShowInstructionInfo.png||GHIDRA||||END|
|
||||||
src/main/help/help/topics/ShowInstructionInfoPlugin/images/UnableToLaunch.png||GHIDRA||||END|
|
src/main/help/help/topics/ShowInstructionInfoPlugin/images/UnableToLaunch.png||GHIDRA||||END|
|
||||||
src/main/help/help/topics/Snapshots/Snapshots.html||GHIDRA||||END|
|
src/main/help/help/topics/Snapshots/Snapshots.html||GHIDRA||||END|
|
||||||
|
src/main/help/help/topics/SourceFilesTablePlugin/SourceFilesTable.html||GHIDRA||||END|
|
||||||
src/main/help/help/topics/StackEditor/StackEditor.html||GHIDRA||||END|
|
src/main/help/help/topics/StackEditor/StackEditor.html||GHIDRA||||END|
|
||||||
src/main/help/help/topics/StackEditor/images/NumElementsPrompt.png||GHIDRA||||END|
|
src/main/help/help/topics/StackEditor/images/NumElementsPrompt.png||GHIDRA||||END|
|
||||||
src/main/help/help/topics/StackEditor/images/StackEditor.png||GHIDRA||||END|
|
src/main/help/help/topics/StackEditor/images/StackEditor.png||GHIDRA||||END|
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
/* ###
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This script displays a table showing the base address of each source map entry
|
||||||
|
// in the program along with a count of the number of entries starting at the address.
|
||||||
|
// @category SourceMapping
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.app.tablechooser.*;
|
||||||
|
import ghidra.program.model.address.Address;
|
||||||
|
import ghidra.program.model.sourcemap.SourceMapEntry;
|
||||||
|
import ghidra.program.model.sourcemap.SourceMapEntryIterator;
|
||||||
|
import ghidra.util.datastruct.Counter;
|
||||||
|
|
||||||
|
|
||||||
|
public class ShowSourceMapEntryStartsScript extends GhidraScript {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void run() throws Exception {
|
||||||
|
if (isRunningHeadless()) {
|
||||||
|
println("This script must be run through the Ghidra gui.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentProgram == null) {
|
||||||
|
println("This script requires an open program.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Address startAddress = currentProgram.getMinAddress();
|
||||||
|
SourceMapEntryIterator iter =
|
||||||
|
currentProgram.getSourceFileManager().getSourceMapEntryIterator(startAddress, true);
|
||||||
|
if (!iter.hasNext()) {
|
||||||
|
popup(currentProgram.getName() + " has no source map entries");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TableChooserDialog tableDialog =
|
||||||
|
createTableChooserDialog(currentProgram.getName() + " Source Map Entries", null);
|
||||||
|
configureTableColumns(tableDialog);
|
||||||
|
tableDialog.show();
|
||||||
|
|
||||||
|
Map<Address, Counter> entryCounts = new HashMap<>();
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
SourceMapEntry entry = iter.next();
|
||||||
|
Address addr = entry.getBaseAddress();
|
||||||
|
Counter count = entryCounts.getOrDefault(addr, new Counter());
|
||||||
|
count.increment();
|
||||||
|
entryCounts.put(addr, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalCount = 0;
|
||||||
|
for (Entry<Address, Counter> entryCount : entryCounts.entrySet()) {
|
||||||
|
int count = entryCount.getValue().intValue();
|
||||||
|
tableDialog.add(new SourceMapRowObject(entryCount.getKey(), count));
|
||||||
|
totalCount += count;
|
||||||
|
}
|
||||||
|
tableDialog.setTitle(
|
||||||
|
currentProgram.getName() + " Source Map Entries (" + totalCount + " total)");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureTableColumns(TableChooserDialog tableDialog) {
|
||||||
|
|
||||||
|
ColumnDisplay<Integer> numEntriesColumn = new AbstractComparableColumnDisplay<>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getColumnValue(AddressableRowObject rowObject) {
|
||||||
|
return ((SourceMapRowObject) rowObject).getNumEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Num Entries";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tableDialog.addCustomColumn(numEntriesColumn);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SourceMapRowObject implements AddressableRowObject {
|
||||||
|
|
||||||
|
private Address address;
|
||||||
|
private int numEntries;
|
||||||
|
|
||||||
|
SourceMapRowObject(Address address, int numEntries) {
|
||||||
|
this.address = address;
|
||||||
|
this.numEntries = numEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Address getAddress() {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNumEntries() {
|
||||||
|
return numEntries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
|
||||||
|
|
||||||
|
<HTML>
|
||||||
|
<HEAD>
|
||||||
|
<META name="generator" content=
|
||||||
|
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
|
||||||
|
|
||||||
|
<TITLE>Source Files</TITLE>
|
||||||
|
<META http-equiv="content-type" content="text/html; charset=windows-1252">
|
||||||
|
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
|
||||||
|
</HEAD>
|
||||||
|
|
||||||
|
<BODY>
|
||||||
|
<H1>Source File Information</H1>
|
||||||
|
|
||||||
|
<P>Ghidra can store information about the source files for a program, including their locations
|
||||||
|
in the build environment and the correspondence between lines of source code and addresses in a
|
||||||
|
program. A common source of this information is debug data, such as DWARF or PDB.</P>
|
||||||
|
|
||||||
|
<P>A major use case of this information is to synchronize Ghidra with an IDE, such as
|
||||||
|
Eclipse.</P>
|
||||||
|
|
||||||
|
<H2>Source Files</H2>
|
||||||
|
|
||||||
|
<P>A source file record in Ghidra consists of three pieces of information:</P>
|
||||||
|
|
||||||
|
<BLOCKQUOTE>
|
||||||
|
<OL>
|
||||||
|
<LI>A path, which must be an absolute, normalized path using forward slashes (think file
|
||||||
|
URI).</LI>
|
||||||
|
|
||||||
|
<LI>A <I>SourceFileIdType</I>, which can be NONE, UNKNOWN, TIMESTAMP_64, MD5, SHA1, SHA256,
|
||||||
|
or SHA512.</LI>
|
||||||
|
|
||||||
|
<LI>An identifier, which is the value of the identifier as a byte array.</LI>
|
||||||
|
</OL>
|
||||||
|
</BLOCKQUOTE>
|
||||||
|
|
||||||
|
<H2>Source Map Entries</H2>
|
||||||
|
|
||||||
|
<P>A <I>Source Map Entry</I> associates a source file and a line number to an address or
|
||||||
|
address range in a program. It consists of:</P>
|
||||||
|
|
||||||
|
<BLOCKQUOTE>
|
||||||
|
<OL>
|
||||||
|
<LI>A source file.</LI>
|
||||||
|
|
||||||
|
<LI>A line number.</LI>
|
||||||
|
|
||||||
|
<LI>A base address.</LI>
|
||||||
|
|
||||||
|
<LI>A length. If the length is non-zero, the entry defines an address range, otherwise it
|
||||||
|
defines an address.</LI>
|
||||||
|
</OL>
|
||||||
|
</BLOCKQUOTE><BR>
|
||||||
|
<BR>
|
||||||
|
|
||||||
|
|
||||||
|
<P>Source map entries are constrained as follows:</P>
|
||||||
|
|
||||||
|
<BLOCKQUOTE>
|
||||||
|
<UL>
|
||||||
|
<LI>An address in a program may not have duplicate (same source file, line number, base
|
||||||
|
address, and length) source file entries.</LI>
|
||||||
|
|
||||||
|
<LI>Given two source map entries with non-zero lengths, their associated address ranges
|
||||||
|
must be either identical or distinct (i.e., no partial overlaps). Multiple source maps
|
||||||
|
entries based at the same address are allowed as long as they obey this restriction. Length
|
||||||
|
zero entries may occur anywhere, including within ranges corresponding to entries of
|
||||||
|
non-zero lengths.</LI>
|
||||||
|
</UL>
|
||||||
|
</BLOCKQUOTE><BR>
|
||||||
|
<BR>
|
||||||
|
|
||||||
|
|
||||||
|
<H2>Source File Manager</H2>
|
||||||
|
|
||||||
|
<P>Source files and source map entries are managed by a program's source file manager (accessed
|
||||||
|
via Program.getSourceFileManager()). A source file must be added to a program before it can
|
||||||
|
used in a source map entry.</P>
|
||||||
|
|
||||||
|
<BLOCKQUOTE>
|
||||||
|
<P><IMG src="help/shared/note.png" alt="Note" border="0">Note that adding source files,
|
||||||
|
removing source files, or changing the source map requires exclusive access to a program.
|
||||||
|
Reading the source file list or source map does not require exclusive access.</P>
|
||||||
|
<BR>
|
||||||
|
</BLOCKQUOTE><BR>
|
||||||
|
<BR>
|
||||||
|
|
||||||
|
|
||||||
|
<H2>Source Path Transformations</H2>
|
||||||
|
|
||||||
|
<P>Source file path information can be sent to an external tool, such as an IDE. However, there
|
||||||
|
is no guarantee that a path recorded for a source file exists on the machine running Ghidra.
|
||||||
|
For instance, you could use Ghidra running under Linux to analyze a Windows program with source
|
||||||
|
file information. An additional complication is that the program may be in a shared Ghidra
|
||||||
|
repository where users have different operating systems or local file systems. We solve this
|
||||||
|
issue by allowing users to modify source file paths. The modifications are stored locally for
|
||||||
|
each user and are not checked in to a shared repository.</P>
|
||||||
|
|
||||||
|
<P>A note on terminology: to avoid overuse of the word "map", we use "map" when discussing the
|
||||||
|
association of a source file and a line number to an address and length in a program (the
|
||||||
|
"source file map"). We use the word "transform" when discussing user-determined modifications
|
||||||
|
of a source file's path.</P>
|
||||||
|
|
||||||
|
<P>There are two type of source path transforms:</P>
|
||||||
|
|
||||||
|
<BLOCKQUOTE>
|
||||||
|
<OL>
|
||||||
|
<LI><I>File Transforms</I>, which entirely replace a source file's path with another file
|
||||||
|
path.</LI>
|
||||||
|
|
||||||
|
<LI><I>Directory Transforms</I>, which replace a parent directory of a source file's path
|
||||||
|
with another directory.</LI>
|
||||||
|
</OL>
|
||||||
|
</BLOCKQUOTE><BR>
|
||||||
|
<BR>
|
||||||
|
|
||||||
|
|
||||||
|
<P>Given a source file, the transformed path is determined as follows. If there is a file
|
||||||
|
transform for that particular file, the file transform is applied. Otherwise, the most specific
|
||||||
|
directory transform (i.e., the one replacing the longest initial segment of the path) is
|
||||||
|
applied. If no transform is applied, the user may opt to use the untransformed path.</P>
|
||||||
|
|
||||||
|
<P>Source file path transformations are managed using a <I>SourcePathTransformer</I>. Path
|
||||||
|
transformations can be managed using the actions on the <A href="#Source_Files_Table">Source
|
||||||
|
Files Table</A>. In a script, you can get the path transformer for a program via the static
|
||||||
|
method <I>UserDataPathTransformer.getPathTransformer(Program)</I>. Note that modifications to
|
||||||
|
the path transformer are not affected by undo or redo actions in Ghidra.</P>
|
||||||
|
|
||||||
|
<H1><A name="Source_Files_Table_Plugin"></A>Source Files Table Plugin</H1>
|
||||||
|
|
||||||
|
<P>This plugin shows the source file information associated with the current program and allows
|
||||||
|
the user to manage source file path transforms.</P>
|
||||||
|
|
||||||
|
<H2><A name="Source_Files_Table"></A>Source Files Table</H2>
|
||||||
|
|
||||||
|
<BLOCKQUOTE>
|
||||||
|
<P>Each row in this table corresponds to a Source File added to the program's source file
|
||||||
|
manager. The columns show the source file, path, transformed path, and number of source map
|
||||||
|
entries for a source file. If the <I>Transformed Path</I> column is empty for a given source
|
||||||
|
file, then no transformation applies to that file. Note that there are optional columns,
|
||||||
|
hidden by default, to show the SourceFileIdType and identifier of each source file.</P>
|
||||||
|
|
||||||
|
<H3><A name="Reload_Source_Files_Model"></A>Reload Model</H3>
|
||||||
|
|
||||||
|
<P>This action reloads the Source File Table. Note that this can be an expensive operation
|
||||||
|
since the number of source map entries must be computed for each source file. For this
|
||||||
|
reason, the action is only enabled after program events which might change the data shown in
|
||||||
|
the table.</P>
|
||||||
|
|
||||||
|
<H3><A name="Show_Source_Map_Entries"></A>Show Source Map Entries</H3>
|
||||||
|
|
||||||
|
<P>This action brings up a table which displays all of the source map entries for the
|
||||||
|
selected source file.</P>
|
||||||
|
|
||||||
|
<H3><A name="Transform_File"></A>Transform File</H3>
|
||||||
|
|
||||||
|
<P>This action allows you to create a file transform for the selected source file. The input
|
||||||
|
must be an absolute, normalized file path using forward slashes.</P>
|
||||||
|
|
||||||
|
<H3><A name="Transform_Directory"></A>Transform Directory</H3>
|
||||||
|
|
||||||
|
<P>This action allows you to create a directory transform whose source is a user-selected
|
||||||
|
parent directory of the corresponding source file. The input must be an absolute, normalized
|
||||||
|
directory path using forward slashes.</P>
|
||||||
|
</BLOCKQUOTE>
|
||||||
|
|
||||||
|
<H2><A name="Transforms_Table"></A>Transforms Table</H2>
|
||||||
|
|
||||||
|
<BLOCKQUOTE>
|
||||||
|
<P>This table shows all of the source file transformations defined for a program.</P>
|
||||||
|
|
||||||
|
<H3><A name="Remove_Transform"></A>Remove Transform</H3>
|
||||||
|
|
||||||
|
<P>This action removes the selected transform from the list of transforms.</P>
|
||||||
|
|
||||||
|
<H3><A name="Edit_Transform"></A>Edit Transform</H3>
|
||||||
|
</BLOCKQUOTE>
|
||||||
|
|
||||||
|
<P>This action allows you to change the destination of a transform (but not the source).</P>
|
||||||
|
|
||||||
|
<P class="relatedtopic">Related Topics:</P>
|
||||||
|
|
||||||
|
<UL>
|
||||||
|
<LI><A href="help/topics/CodeBrowserPlugin/CodeBrowserOptions.htm#Source_Map_Field">Source
|
||||||
|
Map Field</A></LI>
|
||||||
|
</UL><BR>
|
||||||
|
<BR>
|
||||||
|
<BR>
|
||||||
|
<BR>
|
||||||
|
<BR>
|
||||||
|
<BR>
|
||||||
|
<BR>
|
||||||
|
<BR>
|
||||||
|
</BODY>
|
||||||
|
</HTML>
|
|
@ -0,0 +1,60 @@
|
||||||
|
/* ###
|
||||||
|
* 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.plugin.core.sourcefilestable;
|
||||||
|
|
||||||
|
import ghidra.program.database.sourcemap.SourceFile;
|
||||||
|
import ghidra.program.database.sourcemap.SourceFileIdType;
|
||||||
|
import ghidra.program.model.sourcemap.SourceFileManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The row object used by {@link SourceFilesTableModel}.
|
||||||
|
*/
|
||||||
|
public class SourceFileRowObject {
|
||||||
|
|
||||||
|
private SourceFile sourceFile;
|
||||||
|
private int numEntries; // cache this since it's expensive to compute
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param sourceFile source file
|
||||||
|
* @param sourceManager source file manager
|
||||||
|
*/
|
||||||
|
public SourceFileRowObject(SourceFile sourceFile, SourceFileManager sourceManager) {
|
||||||
|
this.sourceFile = sourceFile;
|
||||||
|
numEntries = sourceManager.getSourceMapEntries(sourceFile).size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileName() {
|
||||||
|
return sourceFile.getFilename();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
return sourceFile.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNumSourceMapEntries() {
|
||||||
|
return numEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SourceFile getSourceFile() {
|
||||||
|
return sourceFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SourceFileIdType getSourceFileIdType() {
|
||||||
|
return sourceFile.getIdType();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
/* ###
|
||||||
|
* 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.plugin.core.sourcefilestable;
|
||||||
|
|
||||||
|
import docking.widgets.table.AbstractDynamicTableColumn;
|
||||||
|
import docking.widgets.table.TableColumnDescriptor;
|
||||||
|
import docking.widgets.table.threaded.ThreadedTableModelStub;
|
||||||
|
import ghidra.docking.settings.Settings;
|
||||||
|
import ghidra.framework.plugintool.ServiceProvider;
|
||||||
|
import ghidra.program.database.sourcemap.*;
|
||||||
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.program.model.sourcemap.*;
|
||||||
|
import ghidra.util.datastruct.Accumulator;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A table model for displaying all of the {@link SourceFile}s which have been added
|
||||||
|
* to a program's {@link SourceFileManager}.
|
||||||
|
*/
|
||||||
|
public class SourceFilesTableModel extends ThreadedTableModelStub<SourceFileRowObject> {
|
||||||
|
|
||||||
|
private Program program;
|
||||||
|
private SourceFileManager sourceManager;
|
||||||
|
private SourcePathTransformer pathTransformer;
|
||||||
|
private boolean useExistingAsDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param sourceFilesTablePlugin plugin
|
||||||
|
*/
|
||||||
|
protected SourceFilesTableModel(SourceFilesTablePlugin sourceFilesTablePlugin) {
|
||||||
|
super("Source File Table Model", sourceFilesTablePlugin.getTool());
|
||||||
|
this.program = sourceFilesTablePlugin.getCurrentProgram();
|
||||||
|
if (program != null) {
|
||||||
|
sourceManager = program.getSourceFileManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TableColumnDescriptor<SourceFileRowObject> createTableColumnDescriptor() {
|
||||||
|
TableColumnDescriptor<SourceFileRowObject> descriptor = new TableColumnDescriptor<>();
|
||||||
|
descriptor.addVisibleColumn(new FileNameColumn());
|
||||||
|
descriptor.addVisibleColumn(new PathColumn());
|
||||||
|
descriptor.addHiddenColumn(new IdTypeColumn());
|
||||||
|
descriptor.addHiddenColumn(new IdentifierColumn());
|
||||||
|
descriptor.addVisibleColumn(new TransformedPathColumn());
|
||||||
|
descriptor.addVisibleColumn(new NumMappedEntriesColumn());
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoad(Accumulator<SourceFileRowObject> accumulator, TaskMonitor monitor)
|
||||||
|
throws CancelledException {
|
||||||
|
if (sourceManager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (SourceFile sourceFile : sourceManager.getAllSourceFiles()) {
|
||||||
|
accumulator.add(new SourceFileRowObject(sourceFile, sourceManager));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the table using data from {@code newProgram}
|
||||||
|
* @param newProgram program
|
||||||
|
*/
|
||||||
|
protected void reloadProgram(Program newProgram) {
|
||||||
|
program = newProgram;
|
||||||
|
sourceManager = program == null ? null : program.getSourceFileManager();
|
||||||
|
pathTransformer =
|
||||||
|
program == null ? null : UserDataPathTransformer.getPathTransformer(program);
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the program used to populate the table.
|
||||||
|
* @return program
|
||||||
|
*/
|
||||||
|
protected Program getProgram() {
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PathColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourceFileRowObject, String, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Path";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue(SourceFileRowObject rowObject, Settings settings, Object data,
|
||||||
|
ServiceProvider services) throws IllegalArgumentException {
|
||||||
|
return rowObject.getPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class IdTypeColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourceFileRowObject, SourceFileIdType, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "ID Type";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SourceFileIdType getValue(SourceFileRowObject rowObject, Settings settings,
|
||||||
|
Object data,
|
||||||
|
ServiceProvider services) throws IllegalArgumentException {
|
||||||
|
return rowObject.getSourceFileIdType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FileNameColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourceFileRowObject, String, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "File Name";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue(SourceFileRowObject rowObject, Settings settings, Object data,
|
||||||
|
ServiceProvider services) throws IllegalArgumentException {
|
||||||
|
return rowObject.getFileName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TransformedPathColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourceFileRowObject, String, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Transformed Path";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue(SourceFileRowObject rowObject, Settings settings, Object data,
|
||||||
|
ServiceProvider sProvider) throws IllegalArgumentException {
|
||||||
|
return pathTransformer.getTransformedPath(rowObject.getSourceFile(),
|
||||||
|
useExistingAsDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class IdentifierColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourceFileRowObject, String, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Identifier";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue(SourceFileRowObject rowObject, Settings settings, Object data,
|
||||||
|
ServiceProvider sProvider) throws IllegalArgumentException {
|
||||||
|
return rowObject.getSourceFile().getIdAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NumMappedEntriesColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourceFileRowObject, Integer, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Entry Count";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getValue(SourceFileRowObject rowObject, Settings settings, Object data,
|
||||||
|
ServiceProvider services) throws IllegalArgumentException {
|
||||||
|
return rowObject.getNumSourceMapEntries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
/* ###
|
||||||
|
* 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.plugin.core.sourcefilestable;
|
||||||
|
|
||||||
|
import static ghidra.program.util.ProgramEvent.*;
|
||||||
|
|
||||||
|
import ghidra.app.CorePluginPackage;
|
||||||
|
import ghidra.app.events.ProgramClosedPluginEvent;
|
||||||
|
import ghidra.app.plugin.PluginCategoryNames;
|
||||||
|
import ghidra.app.plugin.ProgramPlugin;
|
||||||
|
import ghidra.framework.model.*;
|
||||||
|
import ghidra.framework.plugintool.PluginInfo;
|
||||||
|
import ghidra.framework.plugintool.PluginTool;
|
||||||
|
import ghidra.framework.plugintool.util.PluginStatus;
|
||||||
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.program.util.ProgramChangeRecord;
|
||||||
|
|
||||||
|
//@formatter:off
|
||||||
|
@PluginInfo(
|
||||||
|
status = PluginStatus.RELEASED,
|
||||||
|
packageName = CorePluginPackage.NAME,
|
||||||
|
category = PluginCategoryNames.CODE_VIEWER,
|
||||||
|
shortDescription = "Source File Table",
|
||||||
|
description = "Plugin for viewing and managing source file information.",
|
||||||
|
eventsConsumed = { ProgramClosedPluginEvent.class }
|
||||||
|
)
|
||||||
|
//@formatter:on
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link ProgramPlugin} for displaying source file information about a program
|
||||||
|
* and for managing source file path transforms.
|
||||||
|
*/
|
||||||
|
public class SourceFilesTablePlugin extends ProgramPlugin {
|
||||||
|
|
||||||
|
private SourceFilesTableProvider provider;
|
||||||
|
private DomainObjectListener listener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param plugintool tool
|
||||||
|
*/
|
||||||
|
public SourceFilesTablePlugin(PluginTool plugintool) {
|
||||||
|
super(plugintool);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() {
|
||||||
|
super.init();
|
||||||
|
provider = new SourceFilesTableProvider(this);
|
||||||
|
listener = createDomainObjectListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void programActivated(Program program) {
|
||||||
|
program.addListener(listener);
|
||||||
|
provider.programActivated(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void programDeactivated(Program program) {
|
||||||
|
program.removeListener(listener);
|
||||||
|
provider.clearTableModels();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void dispose() {
|
||||||
|
if (currentProgram != null) {
|
||||||
|
currentProgram.removeListener(listener);
|
||||||
|
}
|
||||||
|
tool.removeComponentProvider(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DomainObjectListener createDomainObjectListener() {
|
||||||
|
// @formatter:off
|
||||||
|
return new DomainObjectListenerBuilder(this)
|
||||||
|
.ignoreWhen(() -> !provider.isVisible())
|
||||||
|
.any(DomainObjectEvent.RESTORED, MEMORY_BLOCK_MOVED, MEMORY_BLOCK_REMOVED)
|
||||||
|
.terminate(c -> provider.setIsStale(true))
|
||||||
|
.with(ProgramChangeRecord.class)
|
||||||
|
.each(SOURCE_FILE_ADDED,SOURCE_FILE_REMOVED,SOURCE_MAP_CHANGED)
|
||||||
|
.call(provider::handleProgramChange)
|
||||||
|
.build();
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,458 @@
|
||||||
|
/* ###
|
||||||
|
* 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.plugin.core.sourcefilestable;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.IntSupplier;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
|
||||||
|
import docking.*;
|
||||||
|
import docking.action.builder.ActionBuilder;
|
||||||
|
import docking.widgets.table.RowObjectTableModel;
|
||||||
|
import docking.widgets.values.GValuesMap;
|
||||||
|
import docking.widgets.values.ValuesMapDialog;
|
||||||
|
import generic.theme.GIcon;
|
||||||
|
import ghidra.app.plugin.core.table.TableComponentProvider;
|
||||||
|
import ghidra.app.util.SearchConstants;
|
||||||
|
import ghidra.app.util.query.TableService;
|
||||||
|
import ghidra.framework.plugintool.ComponentProviderAdapter;
|
||||||
|
import ghidra.program.database.sourcemap.SourceFile;
|
||||||
|
import ghidra.program.database.sourcemap.UserDataPathTransformer;
|
||||||
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.program.model.sourcemap.*;
|
||||||
|
import ghidra.program.util.ProgramChangeRecord;
|
||||||
|
import ghidra.program.util.ProgramEvent;
|
||||||
|
import ghidra.util.*;
|
||||||
|
import ghidra.util.table.GhidraFilterTable;
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
import resources.Icons;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link ComponentProviderAdapter} for displaying source file information about a program.
|
||||||
|
* This includes the {@link SourceFile}s added to the program's {@link SourceFileManager} as
|
||||||
|
* well as source file path transformations.
|
||||||
|
*/
|
||||||
|
public class SourceFilesTableProvider extends ComponentProviderAdapter {
|
||||||
|
|
||||||
|
private JSplitPane splitPane;
|
||||||
|
private SourceFilesTablePlugin sourceFilesTablePlugin;
|
||||||
|
private SourceFilesTableModel sourceFilesTableModel;
|
||||||
|
private GhidraFilterTable<SourceFileRowObject> sourceFilesTable;
|
||||||
|
private TransformerTableModel transformsModel;
|
||||||
|
private GhidraFilterTable<SourcePathTransformRecord> transformsTable;
|
||||||
|
private boolean isStale;
|
||||||
|
|
||||||
|
private static final String DESTINATION = "Dest";
|
||||||
|
private static final String SOURCE = "Src";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param sourceFilesPlugin plugin
|
||||||
|
*/
|
||||||
|
public SourceFilesTableProvider(SourceFilesTablePlugin sourceFilesPlugin) {
|
||||||
|
super(sourceFilesPlugin.getTool(), "Source Files and Transforms",
|
||||||
|
sourceFilesPlugin.getName());
|
||||||
|
this.sourceFilesTablePlugin = sourceFilesPlugin;
|
||||||
|
tool.addComponentProvider(this, false);
|
||||||
|
buildMainPanel();
|
||||||
|
createActions();
|
||||||
|
setHelpLocation(
|
||||||
|
new HelpLocation(sourceFilesTablePlugin.getName(), "Source_Files_Table_Plugin"));
|
||||||
|
setIsStale(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JComponent getComponent() {
|
||||||
|
return splitPane;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ActionContext getActionContext(MouseEvent event) {
|
||||||
|
if (event != null) {
|
||||||
|
return getActionContext(event.getSource());
|
||||||
|
}
|
||||||
|
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
|
||||||
|
return getActionContext(kfm.getFocusOwner());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void componentShown() {
|
||||||
|
reloadModels(sourceFilesTablePlugin.getCurrentProgram());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void componentHidden() {
|
||||||
|
reloadModels(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the model with {@code program} if the provider is showing.
|
||||||
|
* @param program activated program
|
||||||
|
*/
|
||||||
|
void programActivated(Program program) {
|
||||||
|
if (isVisible()) {
|
||||||
|
reloadModels(program);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the models.
|
||||||
|
*/
|
||||||
|
void clearTableModels() {
|
||||||
|
reloadModels(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value of isStale and invokes {@link ComponentProvider#contextChanged()}
|
||||||
|
* @param b value
|
||||||
|
*/
|
||||||
|
void setIsStale(boolean b) {
|
||||||
|
isStale = b;
|
||||||
|
contextChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets isStale to {@code true} when {@code rec} has an event type relevant to the source
|
||||||
|
* file table. If the event type is {@link ProgramEvent#SOURCE_FILE_REMOVED}, any associated
|
||||||
|
* file transform is also removed.
|
||||||
|
*
|
||||||
|
* @param rec program change record
|
||||||
|
*/
|
||||||
|
void handleProgramChange(ProgramChangeRecord rec) {
|
||||||
|
// if a source file is removed, remove the associated file transform
|
||||||
|
// note: if the removal of the file is undone, the file transform will not be restored
|
||||||
|
switch (rec.getEventType()) {
|
||||||
|
case ProgramEvent.SOURCE_FILE_REMOVED:
|
||||||
|
SourceFile removed = (SourceFile) rec.getOldValue();
|
||||||
|
SourcePathTransformer pathTransformer =
|
||||||
|
UserDataPathTransformer.getPathTransformer(sourceFilesTableModel.getProgram());
|
||||||
|
pathTransformer.removeFileTransform(removed);
|
||||||
|
transformsModel.reload();
|
||||||
|
// fall through intentional
|
||||||
|
case ProgramEvent.SOURCE_FILE_ADDED:
|
||||||
|
case ProgramEvent.SOURCE_MAP_CHANGED:
|
||||||
|
setIsStale(true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reloadModels(Program program) {
|
||||||
|
sourceFilesTableModel.reloadProgram(program);
|
||||||
|
transformsModel.reloadProgram(program);
|
||||||
|
setIsStale(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we want different actions depending on which table you right-click in
|
||||||
|
private ActionContext getActionContext(Object source) {
|
||||||
|
if (source == sourceFilesTable.getTable()) {
|
||||||
|
return new SourceFilesTableActionContext();
|
||||||
|
}
|
||||||
|
if (source == transformsTable.getTable()) {
|
||||||
|
return new TransformTableActionContext();
|
||||||
|
}
|
||||||
|
return new DefaultActionContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildMainPanel() {
|
||||||
|
sourceFilesTableModel = new SourceFilesTableModel(sourceFilesTablePlugin);
|
||||||
|
sourceFilesTable = new GhidraFilterTable<>(sourceFilesTableModel);
|
||||||
|
sourceFilesTable.setAccessibleNamePrefix("Source Files");
|
||||||
|
|
||||||
|
JPanel sourceFilesPanel = buildTitledTablePanel("Source Files", sourceFilesTable,
|
||||||
|
() -> sourceFilesTableModel.getUnfilteredRowCount());
|
||||||
|
|
||||||
|
transformsModel = new TransformerTableModel(sourceFilesTablePlugin);
|
||||||
|
transformsTable = new GhidraFilterTable<>(transformsModel);
|
||||||
|
transformsTable.setAccessibleNamePrefix("Transformations");
|
||||||
|
|
||||||
|
JPanel transformsPanel = buildTitledTablePanel("Transforms", transformsTable,
|
||||||
|
() -> transformsModel.getUnfilteredRowCount());
|
||||||
|
|
||||||
|
splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||||
|
splitPane.setResizeWeight(0.5);
|
||||||
|
splitPane.setDividerSize(10);
|
||||||
|
splitPane.setLeftComponent(sourceFilesPanel);
|
||||||
|
splitPane.setRightComponent(transformsPanel);
|
||||||
|
splitPane.setPreferredSize(new Dimension(1000, 800));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildTitledTablePanel(String title, GhidraFilterTable<?> table,
|
||||||
|
IntSupplier nonFilteredRowCount) {
|
||||||
|
JPanel panel = new JPanel(new BorderLayout());
|
||||||
|
panel.setBorder(BorderFactory.createEmptyBorder(10, 2, 10, 2));
|
||||||
|
JLabel titleLabel = new JLabel(title);
|
||||||
|
panel.add(titleLabel, BorderLayout.NORTH);
|
||||||
|
panel.add(table, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
RowObjectTableModel<?> model = table.getModel();
|
||||||
|
model.addTableModelListener((e) -> {
|
||||||
|
int rowCount = model.getRowCount();
|
||||||
|
String text = title + " - " + rowCount + " rows";
|
||||||
|
int nonFilteredSize = nonFilteredRowCount.getAsInt();
|
||||||
|
if (nonFilteredSize != rowCount) {
|
||||||
|
text += " (Filtered from " + nonFilteredSize + " rows)";
|
||||||
|
}
|
||||||
|
titleLabel.setText(text);
|
||||||
|
});
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createActions() {
|
||||||
|
|
||||||
|
new ActionBuilder("Show Source Map Entries", getName())
|
||||||
|
.popupMenuPath("Show Source Map Entries")
|
||||||
|
.description("Show a table of the source map entries associated with a SourceFile")
|
||||||
|
.helpLocation(
|
||||||
|
new HelpLocation(sourceFilesTablePlugin.getName(), "Show_Source_Map_Entries"))
|
||||||
|
.withContext(SourceFilesTableActionContext.class)
|
||||||
|
.enabledWhen(c -> c.getSelectedRowCount() == 1)
|
||||||
|
.onAction(this::showSourceMapEntries)
|
||||||
|
.buildAndInstallLocal(this);
|
||||||
|
|
||||||
|
new ActionBuilder("Transform File", getName()).popupMenuPath("Tranform File")
|
||||||
|
.description("Enter a file transform for a SourceFile")
|
||||||
|
.helpLocation(new HelpLocation(sourceFilesTablePlugin.getName(), "Transform_File"))
|
||||||
|
.withContext(SourceFilesTableActionContext.class)
|
||||||
|
.enabledWhen(c -> c.getSelectedRowCount() == 1)
|
||||||
|
.onAction(this::transformSourceFileAction)
|
||||||
|
.buildAndInstallLocal(this);
|
||||||
|
|
||||||
|
new ActionBuilder("Transform Directory", getName())
|
||||||
|
.popupMenuPath("Transform Directory")
|
||||||
|
.description("Add a directory transform based on this file's path")
|
||||||
|
.helpLocation(
|
||||||
|
new HelpLocation(sourceFilesTablePlugin.getName(), "Transform_Directory"))
|
||||||
|
.withContext(SourceFilesTableActionContext.class)
|
||||||
|
.enabledWhen(c -> c.getSelectedRowCount() == 1)
|
||||||
|
.onAction(this::transformPath)
|
||||||
|
.buildAndInstallLocal(this);
|
||||||
|
|
||||||
|
new ActionBuilder("Remove Transform", getName()).popupMenuPath("Remove Transform")
|
||||||
|
.description("Remove a transform")
|
||||||
|
.helpLocation(
|
||||||
|
new HelpLocation(sourceFilesTablePlugin.getName(), "Remove_Transform"))
|
||||||
|
.withContext(TransformTableActionContext.class)
|
||||||
|
.enabledWhen(c -> c.getSelectedRowCount() == 1)
|
||||||
|
.onAction(this::removeTransform)
|
||||||
|
.buildAndInstallLocal(this);
|
||||||
|
|
||||||
|
new ActionBuilder("Edit Transform", getName()).popupMenuPath("Edit Transform")
|
||||||
|
.description("Edit the transform")
|
||||||
|
.helpLocation(new HelpLocation(sourceFilesTablePlugin.getName(), "Edit_Transform"))
|
||||||
|
.withContext(TransformTableActionContext.class)
|
||||||
|
.onAction(c -> editTransform())
|
||||||
|
.buildAndInstallLocal(this);
|
||||||
|
|
||||||
|
new ActionBuilder("Reload Source File Table", getName()).toolBarIcon(Icons.REFRESH_ICON)
|
||||||
|
.description("Reloads the Source File Table")
|
||||||
|
.helpLocation(
|
||||||
|
new HelpLocation(sourceFilesTablePlugin.getName(), "Reload_Source_Files_Model"))
|
||||||
|
.enabledWhen(c -> isStale)
|
||||||
|
.onAction(c -> reloadModels(sourceFilesTablePlugin.getCurrentProgram()))
|
||||||
|
.buildAndInstallLocal(this);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeTransform(TransformTableActionContext actionContext) {
|
||||||
|
SourcePathTransformRecord rowObject = transformsTable.getSelectedRowObject();
|
||||||
|
SourcePathTransformer pathTransformer =
|
||||||
|
UserDataPathTransformer.getPathTransformer(transformsModel.getProgram());
|
||||||
|
String source = rowObject.source();
|
||||||
|
if (rowObject.isDirectoryTransform()) {
|
||||||
|
pathTransformer.removeDirectoryTransform(source);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pathTransformer.removeFileTransform(rowObject.sourceFile());
|
||||||
|
}
|
||||||
|
transformsModel.reload();
|
||||||
|
sourceFilesTableModel.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void editTransform() {
|
||||||
|
SourcePathTransformRecord transformRecord = transformsTable.getSelectedRowObject();
|
||||||
|
if (transformRecord.isDirectoryTransform()) {
|
||||||
|
editDirectoryTransform(transformRecord);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SourceFile sourceFile = transformRecord.sourceFile();
|
||||||
|
transformSourceFile(sourceFile);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void editDirectoryTransform(SourcePathTransformRecord transformRecord) {
|
||||||
|
SourcePathTransformer pathTransformer =
|
||||||
|
UserDataPathTransformer.getPathTransformer(sourceFilesTableModel.getProgram());
|
||||||
|
String source = transformRecord.source();
|
||||||
|
GValuesMap valueMap = new GValuesMap();
|
||||||
|
valueMap.defineString(DESTINATION, transformRecord.target());
|
||||||
|
valueMap.setValidator((map, status) -> {
|
||||||
|
String path = valueMap.getString(DESTINATION);
|
||||||
|
try {
|
||||||
|
UserDataPathTransformer.validateDirectoryPath(path);
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException e) {
|
||||||
|
status.setStatusText(e.getMessage(), MessageType.ERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
ValuesMapDialog mapDialog =
|
||||||
|
new ValuesMapDialog("Enter Directory Transform", "Transform for " + source, valueMap);
|
||||||
|
tool.showDialog(mapDialog, this);
|
||||||
|
GValuesMap results = mapDialog.getValues();
|
||||||
|
if (results == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String path = results.getString(DESTINATION);
|
||||||
|
pathTransformer.addDirectoryTransform(source, path);
|
||||||
|
|
||||||
|
sourceFilesTableModel.refresh();
|
||||||
|
transformsModel.reload();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void transformPath(SourceFilesTableActionContext actionContext) {
|
||||||
|
SourceFile sourceFile = sourceFilesTable.getSelectedRowObject().getSourceFile();
|
||||||
|
String path = sourceFile.getPath();
|
||||||
|
GValuesMap valueMap = new GValuesMap();
|
||||||
|
List<String> parentDirs = new ArrayList<>();
|
||||||
|
String[] directories = path.split("/");
|
||||||
|
parentDirs.add("/");
|
||||||
|
for (int i = 1; i < directories.length - 1; ++i) {
|
||||||
|
String latest = parentDirs.get(i - 1);
|
||||||
|
parentDirs.add(latest + directories[i] + "/");
|
||||||
|
}
|
||||||
|
valueMap.defineChoice(SOURCE, parentDirs.getLast(), parentDirs.toArray(new String[0]));
|
||||||
|
valueMap.defineString(DESTINATION);
|
||||||
|
valueMap.setValidator((map, status) -> {
|
||||||
|
String enteredPath = valueMap.getString(DESTINATION);
|
||||||
|
try {
|
||||||
|
UserDataPathTransformer.validateDirectoryPath(enteredPath);
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException e) {
|
||||||
|
status.setStatusText(e.getMessage(), MessageType.ERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
ValuesMapDialog mapDialog =
|
||||||
|
new ValuesMapDialog("Enter Directory Transform", null, valueMap);
|
||||||
|
tool.showDialog(mapDialog, this);
|
||||||
|
GValuesMap results = mapDialog.getValues();
|
||||||
|
if (results == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SourcePathTransformer pathTransformer =
|
||||||
|
UserDataPathTransformer.getPathTransformer(sourceFilesTableModel.getProgram());
|
||||||
|
String source = results.getChoice(SOURCE);
|
||||||
|
String destination = results.getString(DESTINATION);
|
||||||
|
pathTransformer.addDirectoryTransform(source, destination);
|
||||||
|
|
||||||
|
sourceFilesTableModel.refresh();
|
||||||
|
transformsModel.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void transformSourceFileAction(SourceFilesTableActionContext actionContext) {
|
||||||
|
SourceFile sourceFile = sourceFilesTable.getSelectedRowObject().getSourceFile();
|
||||||
|
transformSourceFile(sourceFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void transformSourceFile(SourceFile sourceFile) {
|
||||||
|
SourcePathTransformer pathTransformer =
|
||||||
|
UserDataPathTransformer.getPathTransformer(sourceFilesTableModel.getProgram());
|
||||||
|
String existing = pathTransformer.getTransformedPath(sourceFile, true);
|
||||||
|
GValuesMap valueMap = new GValuesMap();
|
||||||
|
valueMap.defineString(DESTINATION, existing);
|
||||||
|
valueMap.setValidator((map, status) -> {
|
||||||
|
String path = valueMap.getString(DESTINATION);
|
||||||
|
try {
|
||||||
|
String normalized = new SourceFile(path).getPath();
|
||||||
|
if (!normalized.equals(path)) {
|
||||||
|
status.setStatusText("Path not normalized", MessageType.ERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException e) {
|
||||||
|
status.setStatusText(e.getMessage(), MessageType.ERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
ValuesMapDialog mapDialog = new ValuesMapDialog("Enter File Tranform",
|
||||||
|
"Transform for " + sourceFile.toString(), valueMap);
|
||||||
|
tool.showDialog(mapDialog, this);
|
||||||
|
GValuesMap results = mapDialog.getValues();
|
||||||
|
if (results == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String path = results.getString(DESTINATION);
|
||||||
|
pathTransformer.addFileTransform(sourceFile, path);
|
||||||
|
sourceFilesTableModel.refresh();
|
||||||
|
transformsModel.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSourceMapEntries(SourceFilesTableActionContext actionContext) {
|
||||||
|
TableService tableService = sourceFilesTablePlugin.getTool().getService(TableService.class);
|
||||||
|
if (tableService == null) {
|
||||||
|
Msg.showWarn(this, null, "No Table Service", "Please add the TableServicePlugin.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SourceFileRowObject sourceFileRow = sourceFilesTable.getSelectedRowObject();
|
||||||
|
Icon markerIcon = new GIcon("icon.plugin.codebrowser.cursor.marker");
|
||||||
|
SourceFile sourceFile = sourceFileRow.getSourceFile();
|
||||||
|
String title = "Source Map Entries for " + sourceFile.getFilename();
|
||||||
|
SourceMapEntryTableModel tableModel =
|
||||||
|
new SourceMapEntryTableModel(sourceFilesTablePlugin.getTool(),
|
||||||
|
sourceFilesTablePlugin.getCurrentProgram(), TaskMonitor.DUMMY, sourceFile);
|
||||||
|
TableComponentProvider<SourceMapEntryRowObject> provider =
|
||||||
|
tableService.showTableWithMarkers(title, "SourceMapEntries", tableModel,
|
||||||
|
SearchConstants.SEARCH_HIGHLIGHT_COLOR, markerIcon, title, null);
|
||||||
|
provider.setTabText(sourceFile.getFilename());
|
||||||
|
provider.setHelpLocation(
|
||||||
|
new HelpLocation(sourceFilesTablePlugin.getName(), "Show_Source_Map_Entries"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SourceFilesTableActionContext extends DefaultActionContext {
|
||||||
|
|
||||||
|
SourceFilesTableActionContext() {
|
||||||
|
super(SourceFilesTableProvider.this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSelectedRowCount() {
|
||||||
|
return sourceFilesTable.getTable().getSelectedRowCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TransformTableActionContext extends DefaultActionContext {
|
||||||
|
|
||||||
|
TransformTableActionContext() {
|
||||||
|
super(SourceFilesTableProvider.this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSelectedRowCount() {
|
||||||
|
return transformsTable.getTable().getSelectedRowCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.plugin.core.sourcefilestable;
|
||||||
|
|
||||||
|
import ghidra.program.model.address.Address;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A row object class for {@link SourceMapEntryTableModel}.
|
||||||
|
*/
|
||||||
|
public class SourceMapEntryRowObject {
|
||||||
|
|
||||||
|
private Address baseAddress;
|
||||||
|
private int lineNumber;
|
||||||
|
private long length;
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param baseAddress base address
|
||||||
|
* @param lineNumber source line number
|
||||||
|
* @param length length of entry
|
||||||
|
* @param count number of mappings for source line
|
||||||
|
*/
|
||||||
|
public SourceMapEntryRowObject(Address baseAddress, int lineNumber, long length, int count) {
|
||||||
|
this.baseAddress = baseAddress;
|
||||||
|
this.lineNumber = lineNumber;
|
||||||
|
this.length = length;
|
||||||
|
this.count = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the base address
|
||||||
|
* @return base address
|
||||||
|
*/
|
||||||
|
public Address getBaseAddress() {
|
||||||
|
return baseAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the source file line number
|
||||||
|
* @return line number
|
||||||
|
*/
|
||||||
|
public int getLineNumber() {
|
||||||
|
return lineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the length of the associated source map entry
|
||||||
|
* @return entry length
|
||||||
|
*/
|
||||||
|
public long getLength() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of entries for this line number
|
||||||
|
* @return number of entries
|
||||||
|
*/
|
||||||
|
public int getCount() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.plugin.core.sourcefilestable;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import docking.widgets.table.AbstractDynamicTableColumn;
|
||||||
|
import docking.widgets.table.TableColumnDescriptor;
|
||||||
|
import ghidra.docking.settings.Settings;
|
||||||
|
import ghidra.framework.plugintool.ServiceProvider;
|
||||||
|
import ghidra.program.database.sourcemap.SourceFile;
|
||||||
|
import ghidra.program.model.address.Address;
|
||||||
|
import ghidra.program.model.address.AddressSet;
|
||||||
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.program.model.sourcemap.SourceFileManager;
|
||||||
|
import ghidra.program.model.sourcemap.SourceMapEntry;
|
||||||
|
import ghidra.program.util.ProgramLocation;
|
||||||
|
import ghidra.program.util.ProgramSelection;
|
||||||
|
import ghidra.util.datastruct.Accumulator;
|
||||||
|
import ghidra.util.datastruct.Counter;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
import ghidra.util.table.GhidraProgramTableModel;
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A table model for displaying all the {@link SourceMapEntry}s for a given {@link SourceFile}.
|
||||||
|
*/
|
||||||
|
public class SourceMapEntryTableModel extends GhidraProgramTableModel<SourceMapEntryRowObject> {
|
||||||
|
private SourceFile sourceFile;
|
||||||
|
private static final int END_ADDRESS_INDEX = 2;
|
||||||
|
private SourceFileManager sourceManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param serviceProvider service provider
|
||||||
|
* @param program program
|
||||||
|
* @param monitor task monitor
|
||||||
|
* @param sourceFile source file
|
||||||
|
*/
|
||||||
|
protected SourceMapEntryTableModel(ServiceProvider serviceProvider, Program program,
|
||||||
|
TaskMonitor monitor, SourceFile sourceFile) {
|
||||||
|
super("SourceMapEntryTableModel", serviceProvider, program, monitor);
|
||||||
|
this.sourceFile = sourceFile;
|
||||||
|
this.sourceManager = program.getSourceFileManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProgramLocation getProgramLocation(int modelRow, int modelColumn) {
|
||||||
|
SourceMapEntryRowObject rowObject = getRowObject(modelRow);
|
||||||
|
if (modelColumn == END_ADDRESS_INDEX) {
|
||||||
|
return new ProgramLocation(program, getEndAddress(rowObject));
|
||||||
|
}
|
||||||
|
return new ProgramLocation(program, rowObject.getBaseAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProgramSelection getProgramSelection(int[] modelRows) {
|
||||||
|
AddressSet selection = new AddressSet();
|
||||||
|
for (SourceMapEntryRowObject rowObject : getRowObjects(modelRows)) {
|
||||||
|
selection.addRange(rowObject.getBaseAddress(), getEndAddress(rowObject));
|
||||||
|
}
|
||||||
|
return new ProgramSelection(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refresh() {
|
||||||
|
// this class is used by the TableServicePlugin, which calls refresh on a ProgramChangeEvent
|
||||||
|
// we want to check for new SourceMapEntries on such events, so we reload first
|
||||||
|
reload();
|
||||||
|
super.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoad(Accumulator<SourceMapEntryRowObject> accumulator, TaskMonitor monitor)
|
||||||
|
throws CancelledException {
|
||||||
|
List<SourceMapEntry> mapEntries = sourceManager.getSourceMapEntries(sourceFile);
|
||||||
|
Map<Integer, Counter> lineCount = new HashMap<>();
|
||||||
|
for (SourceMapEntry entry : mapEntries) {
|
||||||
|
Integer lineNumber = entry.getLineNumber();
|
||||||
|
Counter count = lineCount.getOrDefault(lineNumber, new Counter());
|
||||||
|
count.increment();
|
||||||
|
lineCount.put(lineNumber, count);
|
||||||
|
}
|
||||||
|
for (SourceMapEntry entry : mapEntries) {
|
||||||
|
int lineNumber = entry.getLineNumber();
|
||||||
|
SourceMapEntryRowObject rowObject = new SourceMapEntryRowObject(entry.getBaseAddress(),
|
||||||
|
lineNumber, entry.getLength(), lineCount.get(lineNumber).intValue());
|
||||||
|
accumulator.add(rowObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TableColumnDescriptor<SourceMapEntryRowObject> createTableColumnDescriptor() {
|
||||||
|
TableColumnDescriptor<SourceMapEntryRowObject> descriptor = new TableColumnDescriptor<>();
|
||||||
|
descriptor.addVisibleColumn(new BaseAddressTableColumn());
|
||||||
|
descriptor.addVisibleColumn(new EndAddressTableColumn());
|
||||||
|
descriptor.addVisibleColumn(new LineNumberTableColumn());
|
||||||
|
descriptor.addVisibleColumn(new LengthTableColumn());
|
||||||
|
descriptor.addVisibleColumn(new CountTableColumn());
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Address getEndAddress(SourceMapEntryRowObject rowObject) {
|
||||||
|
long length = rowObject.getLength();
|
||||||
|
Address baseAddress = rowObject.getBaseAddress();
|
||||||
|
if (length == 0) {
|
||||||
|
return rowObject.getBaseAddress();
|
||||||
|
}
|
||||||
|
return baseAddress.add(length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BaseAddressTableColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourceMapEntryRowObject, Address, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Base Address";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Address getValue(SourceMapEntryRowObject rowObject, Settings settings, Object data,
|
||||||
|
ServiceProvider services) throws IllegalArgumentException {
|
||||||
|
return rowObject.getBaseAddress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class EndAddressTableColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourceMapEntryRowObject, Address, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "End Address";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Address getValue(SourceMapEntryRowObject rowObject, Settings settings, Object data,
|
||||||
|
ServiceProvider services) throws IllegalArgumentException {
|
||||||
|
return getEndAddress(rowObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LengthTableColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourceMapEntryRowObject, Long, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Length";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long getValue(SourceMapEntryRowObject rowObject, Settings settings, Object data,
|
||||||
|
ServiceProvider services) throws IllegalArgumentException {
|
||||||
|
return rowObject.getLength();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LineNumberTableColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourceMapEntryRowObject, Integer, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Line Number";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getValue(SourceMapEntryRowObject rowObject, Settings settings, Object data,
|
||||||
|
ServiceProvider services) throws IllegalArgumentException {
|
||||||
|
return rowObject.getLineNumber();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CountTableColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourceMapEntryRowObject, Integer, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Count";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getValue(SourceMapEntryRowObject rowObject, Settings settings, Object data,
|
||||||
|
ServiceProvider services) throws IllegalArgumentException {
|
||||||
|
return rowObject.getCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* ###
|
||||||
|
* 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.plugin.core.sourcefilestable;
|
||||||
|
|
||||||
|
import ghidra.framework.plugintool.ServiceProvider;
|
||||||
|
import ghidra.program.model.address.Address;
|
||||||
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.util.table.ProgramLocationTableRowMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A row mapper for {@link SourceMapEntryRowObject}s. Returns the base address.
|
||||||
|
*/
|
||||||
|
public class SourceMapEntryToAddressTableRowMapper
|
||||||
|
extends ProgramLocationTableRowMapper<SourceMapEntryRowObject, Address> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Address map(SourceMapEntryRowObject rowObject, Program program,
|
||||||
|
ServiceProvider serviceProvider) {
|
||||||
|
return rowObject.getBaseAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/* ###
|
||||||
|
* 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.plugin.core.sourcefilestable;
|
||||||
|
|
||||||
|
import ghidra.framework.plugintool.ServiceProvider;
|
||||||
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.program.util.ProgramLocation;
|
||||||
|
import ghidra.util.table.ProgramLocationTableRowMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A row mapper for {@link SourceMapEntryRowObject}s. Returns a {@link ProgramLocation}
|
||||||
|
* corresponding to the base address.
|
||||||
|
*/
|
||||||
|
public class SourceMapEntryToProgramLocationRowMapper
|
||||||
|
extends ProgramLocationTableRowMapper<SourceMapEntryRowObject, ProgramLocation> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProgramLocation map(SourceMapEntryRowObject rowObject, Program data,
|
||||||
|
ServiceProvider serviceProvider) {
|
||||||
|
return new ProgramLocation(data, rowObject.getBaseAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
/* ###
|
||||||
|
* 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.plugin.core.sourcefilestable;
|
||||||
|
|
||||||
|
import docking.widgets.table.AbstractDynamicTableColumn;
|
||||||
|
import docking.widgets.table.TableColumnDescriptor;
|
||||||
|
import docking.widgets.table.threaded.ThreadedTableModelStub;
|
||||||
|
import ghidra.docking.settings.Settings;
|
||||||
|
import ghidra.framework.plugintool.ServiceProvider;
|
||||||
|
import ghidra.program.database.sourcemap.UserDataPathTransformer;
|
||||||
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.program.model.sourcemap.SourcePathTransformRecord;
|
||||||
|
import ghidra.program.model.sourcemap.SourcePathTransformer;
|
||||||
|
import ghidra.util.datastruct.Accumulator;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A table model for source path transform information
|
||||||
|
*/
|
||||||
|
public class TransformerTableModel extends ThreadedTableModelStub<SourcePathTransformRecord> {
|
||||||
|
|
||||||
|
private SourceFilesTablePlugin plugin;
|
||||||
|
private Program program;
|
||||||
|
private SourcePathTransformer pathTransformer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param plugin plugin
|
||||||
|
*/
|
||||||
|
public TransformerTableModel(SourceFilesTablePlugin plugin) {
|
||||||
|
super("Transformer Table Model", plugin.getTool());
|
||||||
|
this.plugin = plugin;
|
||||||
|
program = plugin.getCurrentProgram();
|
||||||
|
if (program != null) {
|
||||||
|
pathTransformer = UserDataPathTransformer.getPathTransformer(program);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the program used to populate the table
|
||||||
|
* @return program
|
||||||
|
*/
|
||||||
|
protected Program getProgram() {
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoad(Accumulator<SourcePathTransformRecord> accumulator, TaskMonitor monitor)
|
||||||
|
throws CancelledException {
|
||||||
|
if (pathTransformer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (SourcePathTransformRecord transformRecord : pathTransformer.getTransformRecords()) {
|
||||||
|
accumulator.add(transformRecord);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TableColumnDescriptor<SourcePathTransformRecord> createTableColumnDescriptor() {
|
||||||
|
TableColumnDescriptor<SourcePathTransformRecord> descriptor = new TableColumnDescriptor<>();
|
||||||
|
descriptor.addVisibleColumn(new SourceColumn());
|
||||||
|
descriptor.addVisibleColumn(new TargetColumn());
|
||||||
|
descriptor.addVisibleColumn(new IsDirectoryTransformColumn());
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the table using the path transformer for {@code newProgram}.
|
||||||
|
* @param newProgram program
|
||||||
|
*/
|
||||||
|
protected void reloadProgram(Program newProgram) {
|
||||||
|
program = newProgram;
|
||||||
|
pathTransformer =
|
||||||
|
program == null ? null
|
||||||
|
: UserDataPathTransformer.getPathTransformer(plugin.getCurrentProgram());
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SourceColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourcePathTransformRecord, String, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Source";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue(SourcePathTransformRecord rowObject, Settings settings, Object data,
|
||||||
|
ServiceProvider services) throws IllegalArgumentException {
|
||||||
|
if (rowObject.isDirectoryTransform()) {
|
||||||
|
return rowObject.source();
|
||||||
|
}
|
||||||
|
return rowObject.sourceFile().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TargetColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourcePathTransformRecord, String, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Target";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValue(SourcePathTransformRecord rowObject, Settings settings, Object data,
|
||||||
|
ServiceProvider services) throws IllegalArgumentException {
|
||||||
|
return rowObject.target();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class IsDirectoryTransformColumn
|
||||||
|
extends AbstractDynamicTableColumn<SourcePathTransformRecord, Boolean, Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Directory Transform";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean getValue(SourcePathTransformRecord rowObject, Settings settings, Object data,
|
||||||
|
ServiceProvider services) throws IllegalArgumentException {
|
||||||
|
return rowObject.isDirectoryTransform();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,9 +4,9 @@
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@ -414,42 +414,36 @@ class ProgramUserDataDB extends DomainObjectAdapterDB implements ProgramUserData
|
||||||
* @param monitor task monitor
|
* @param monitor task monitor
|
||||||
*/
|
*/
|
||||||
private void setLanguage(LanguageTranslator translator, TaskMonitor monitor) {
|
private void setLanguage(LanguageTranslator translator, TaskMonitor monitor) {
|
||||||
lock.acquire();
|
//setEventsEnabled(false);
|
||||||
try {
|
try {
|
||||||
//setEventsEnabled(false);
|
|
||||||
try {
|
|
||||||
|
|
||||||
language = translator.getNewLanguage();
|
language = translator.getNewLanguage();
|
||||||
languageID = language.getLanguageID();
|
languageID = language.getLanguageID();
|
||||||
languageVersion = language.getVersion();
|
languageVersion = language.getVersion();
|
||||||
|
|
||||||
// AddressFactory need not change since we are using the instance from the
|
// AddressFactory need not change since we are using the instance from the
|
||||||
// Program which would have already been subject to an upgrade
|
// Program which would have already been subject to an upgrade
|
||||||
addressMap.setLanguage(language, addressFactory, translator);
|
addressMap.setLanguage(language, addressFactory, translator);
|
||||||
|
|
||||||
clearCache(true);
|
clearCache(true);
|
||||||
|
|
||||||
DBRecord record = SCHEMA.createRecord(new StringField(LANGUAGE_ID));
|
DBRecord record = SCHEMA.createRecord(new StringField(LANGUAGE_ID));
|
||||||
record.setString(VALUE_COL, languageID.getIdAsString());
|
record.setString(VALUE_COL, languageID.getIdAsString());
|
||||||
table.putRecord(record);
|
table.putRecord(record);
|
||||||
|
|
||||||
setChanged(true);
|
setChanged(true);
|
||||||
clearCache(true);
|
clearCache(true);
|
||||||
|
|
||||||
//invalidate();
|
//invalidate();
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Throwable t) {
|
catch (Throwable t) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"Set language aborted - program user data is now in an unusable state!", t);
|
"Set language aborted - program user data is now in an unusable state!", t);
|
||||||
}
|
}
|
||||||
// finally {
|
// finally {
|
||||||
// setEventsEnabled(true);
|
// setEventsEnabled(true);
|
||||||
// }
|
// }
|
||||||
}
|
|
||||||
finally {
|
|
||||||
lock.release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -676,24 +670,24 @@ class ProgramUserDataDB extends DomainObjectAdapterDB implements ProgramUserData
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setStringProperty(String propertyName, String value) {
|
public synchronized void setStringProperty(String propertyName, String value) {
|
||||||
metadata.put(propertyName, value);
|
metadata.put(propertyName, value);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getStringProperty(String propertyName, String defaultValue) {
|
public synchronized String getStringProperty(String propertyName, String defaultValue) {
|
||||||
String value = metadata.get(propertyName);
|
String value = metadata.get(propertyName);
|
||||||
return value == null ? defaultValue : value;
|
return value == null ? defaultValue : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<String> getStringPropertyNames() {
|
public synchronized Set<String> getStringPropertyNames() {
|
||||||
return metadata.keySet();
|
return new HashSet<String>(metadata.keySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String removeStringProperty(String propertyName) {
|
public synchronized String removeStringProperty(String propertyName) {
|
||||||
changed = true;
|
changed = true;
|
||||||
return metadata.remove(propertyName);
|
return metadata.remove(propertyName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,249 @@
|
||||||
|
/* ###
|
||||||
|
* 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.program.database.sourcemap;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import ghidra.framework.model.DomainObject;
|
||||||
|
import ghidra.framework.model.DomainObjectClosedListener;
|
||||||
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.program.model.listing.ProgramUserData;
|
||||||
|
import ghidra.program.model.sourcemap.SourcePathTransformRecord;
|
||||||
|
import ghidra.program.model.sourcemap.SourcePathTransformer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of {@link SourcePathTransformer} that stores transform information using
|
||||||
|
* {@link ProgramUserData}. This means that transform information will be stored locally
|
||||||
|
* but not checked in to a shared project.<br>
|
||||||
|
* <p>
|
||||||
|
* Use the static method {@link UserDataPathTransformer#getPathTransformer} to get the transformer
|
||||||
|
* for a program. <br>
|
||||||
|
* <p>
|
||||||
|
* Synchronization policy: {@code userData}, {@code pathMap}, {@code fileMap}, and
|
||||||
|
* {@code programsToTransformers} must be protected.
|
||||||
|
*/
|
||||||
|
public class UserDataPathTransformer implements SourcePathTransformer, DomainObjectClosedListener {
|
||||||
|
|
||||||
|
private Program program;
|
||||||
|
private static final String USER_FILE_TRANSFORM_PREFIX = "USER_FILE_TRANSFORM_";
|
||||||
|
private static final String USER_PATH_TRANSFORM_PREFIX = "USER_DIRECTORY_TRANSFORM_";
|
||||||
|
private TreeMap<String, String> pathMap;
|
||||||
|
private Map<String, String> fileMap;
|
||||||
|
private ProgramUserData userData;
|
||||||
|
private static final String FILE_SCHEME = "file";
|
||||||
|
private static HexFormat hexFormat = HexFormat.of();
|
||||||
|
private static HashMap<Program, UserDataPathTransformer> programsToTransformers =
|
||||||
|
new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path transformer for {@code program}
|
||||||
|
* @param program program
|
||||||
|
* @return path transformer
|
||||||
|
*/
|
||||||
|
public static synchronized SourcePathTransformer getPathTransformer(Program program) {
|
||||||
|
if (program == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return programsToTransformers.computeIfAbsent(program, p -> new UserDataPathTransformer(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws an {@link IllegalArgumentException} if {@code directory} is not
|
||||||
|
* a valid, normalized directory path (with forward slashes).
|
||||||
|
* @param directory path to validate
|
||||||
|
*/
|
||||||
|
public static void validateDirectoryPath(String directory) {
|
||||||
|
if (StringUtils.isBlank(directory)) {
|
||||||
|
throw new IllegalArgumentException("Blank directory path");
|
||||||
|
}
|
||||||
|
URI uri;
|
||||||
|
try {
|
||||||
|
uri = new URI(FILE_SCHEME, null, directory, null).normalize();
|
||||||
|
}
|
||||||
|
catch (URISyntaxException e) {
|
||||||
|
throw new IllegalArgumentException(e.getMessage());
|
||||||
|
}
|
||||||
|
String normalizedPath = uri.getPath();
|
||||||
|
if (!normalizedPath.endsWith("/")) {
|
||||||
|
throw new IllegalArgumentException(directory + " is not a directory path");
|
||||||
|
}
|
||||||
|
if (!directory.equals(normalizedPath)) {
|
||||||
|
throw new IllegalArgumentException(directory + " is not normalized");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserDataPathTransformer(Program program) {
|
||||||
|
this.program = program;
|
||||||
|
userData = program.getProgramUserData();
|
||||||
|
pathMap = new TreeMap<>(Collections.reverseOrder(UserDataPathTransformer::compareStrings));
|
||||||
|
fileMap = new HashMap<>();
|
||||||
|
reloadMaps();
|
||||||
|
program.addCloseListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void domainObjectClosed(DomainObject dobj) {
|
||||||
|
synchronized (UserDataPathTransformer.class) {
|
||||||
|
programsToTransformers.remove(program);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void addFileTransform(SourceFile sourceFile, String path) {
|
||||||
|
SourceFile validated = new SourceFile(path);
|
||||||
|
if (!validated.getPath().equals(path)) {
|
||||||
|
throw new IllegalArgumentException("path not normalized");
|
||||||
|
}
|
||||||
|
int txId = userData.startTransaction();
|
||||||
|
String sourceString = getString(sourceFile);
|
||||||
|
try {
|
||||||
|
userData.setStringProperty(USER_FILE_TRANSFORM_PREFIX + sourceString,
|
||||||
|
path);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
userData.endTransaction(txId);
|
||||||
|
}
|
||||||
|
fileMap.put(sourceString, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void removeFileTransform(SourceFile sourceFile) {
|
||||||
|
int txId = userData.startTransaction();
|
||||||
|
String sourceString = getString(sourceFile);
|
||||||
|
try {
|
||||||
|
userData.removeStringProperty(USER_FILE_TRANSFORM_PREFIX + getString(sourceFile));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
userData.endTransaction(txId);
|
||||||
|
}
|
||||||
|
fileMap.remove(sourceString);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void addDirectoryTransform(String sourceDir, String targetDir) {
|
||||||
|
validateDirectoryPath(sourceDir);
|
||||||
|
validateDirectoryPath(targetDir);
|
||||||
|
int txId = userData.startTransaction();
|
||||||
|
try {
|
||||||
|
userData.setStringProperty(USER_PATH_TRANSFORM_PREFIX + sourceDir, targetDir);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
userData.endTransaction(txId);
|
||||||
|
}
|
||||||
|
pathMap.put(sourceDir, targetDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void removeDirectoryTransform(String sourceDir) {
|
||||||
|
int txId = userData.startTransaction();
|
||||||
|
try {
|
||||||
|
userData.removeStringProperty(USER_PATH_TRANSFORM_PREFIX + sourceDir);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
userData.endTransaction(txId);
|
||||||
|
}
|
||||||
|
pathMap.remove(sourceDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized String getTransformedPath(SourceFile sourceFile,
|
||||||
|
boolean useExistingAsDefault) {
|
||||||
|
String sourceFileString = getString(sourceFile);
|
||||||
|
String mappedFile = fileMap.get(sourceFileString);
|
||||||
|
if (mappedFile != null) {
|
||||||
|
return mappedFile;
|
||||||
|
}
|
||||||
|
String path = sourceFile.getPath();
|
||||||
|
for (String src : pathMap.keySet()) {
|
||||||
|
if (path.startsWith(src)) {
|
||||||
|
return pathMap.get(src) + path.substring(src.length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return useExistingAsDefault ? path : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized List<SourcePathTransformRecord> getTransformRecords() {
|
||||||
|
List<SourcePathTransformRecord> transformRecords = new ArrayList<>();
|
||||||
|
for (Entry<String, String> entry : pathMap.entrySet()) {
|
||||||
|
transformRecords
|
||||||
|
.add(new SourcePathTransformRecord(entry.getKey(), null, entry.getValue()));
|
||||||
|
}
|
||||||
|
for (Entry<String, String> entry : fileMap.entrySet()) {
|
||||||
|
String sourceFileString = entry.getKey();
|
||||||
|
transformRecords.add(new SourcePathTransformRecord(sourceFileString,
|
||||||
|
getSourceFile(sourceFileString), entry.getValue()));
|
||||||
|
}
|
||||||
|
return transformRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int compareStrings(String left, String right) {
|
||||||
|
int leftLength = left.length();
|
||||||
|
int rightLength = right.length();
|
||||||
|
if (leftLength != rightLength) {
|
||||||
|
return Integer.compare(leftLength, rightLength);
|
||||||
|
}
|
||||||
|
return StringUtils.compare(left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reloadMaps() {
|
||||||
|
pathMap.clear();
|
||||||
|
fileMap.clear();
|
||||||
|
for (String key : userData.getStringPropertyNames()) {
|
||||||
|
if (key.startsWith(USER_PATH_TRANSFORM_PREFIX)) {
|
||||||
|
String value = userData.getStringProperty(key, null);
|
||||||
|
if (StringUtils.isBlank(value)) {
|
||||||
|
throw new AssertionError("blank value for path " + key);
|
||||||
|
}
|
||||||
|
pathMap.put(key.substring(USER_PATH_TRANSFORM_PREFIX.length()), value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key.startsWith(USER_FILE_TRANSFORM_PREFIX)) {
|
||||||
|
String value = userData.getStringProperty(key, null);
|
||||||
|
if (StringUtils.isBlank(value)) {
|
||||||
|
throw new AssertionError("blank value for file " + key);
|
||||||
|
}
|
||||||
|
fileMap.put(key.substring(USER_FILE_TRANSFORM_PREFIX.length()), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getString(SourceFile sourceFile) {
|
||||||
|
StringBuilder sb = new StringBuilder(sourceFile.getIdType().name());
|
||||||
|
sb.append("#");
|
||||||
|
sb.append(hexFormat.formatHex(sourceFile.getIdentifier()));
|
||||||
|
sb.append("#");
|
||||||
|
sb.append(sourceFile.getPath());
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SourceFile getSourceFile(String sourceFileString) {
|
||||||
|
int firstHash = sourceFileString.indexOf("#");
|
||||||
|
SourceFileIdType type = SourceFileIdType.valueOf(sourceFileString.substring(0, firstHash));
|
||||||
|
int secondHash = sourceFileString.indexOf("#", firstHash + 1);
|
||||||
|
byte[] identifier =
|
||||||
|
hexFormat.parseHex(sourceFileString.subSequence(firstHash + 1, secondHash));
|
||||||
|
String path = sourceFileString.substring(secondHash + 1);
|
||||||
|
return new SourceFile(path, type, identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
/* ###
|
||||||
|
* 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.program.model.sourcemap;
|
||||||
|
|
||||||
|
import ghidra.program.database.sourcemap.SourceFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container for a source path transformation. No validation is performed on the inputs.
|
||||||
|
* @param source A path (directory transform) or a String of the form SourceFileIdName + "#" + ID +
|
||||||
|
* "#" + SourceFile path (file transform)
|
||||||
|
* @param sourceFile SourceFile (null for directory tranforms)
|
||||||
|
* @param target transformed path
|
||||||
|
*/
|
||||||
|
public record SourcePathTransformRecord(String source, SourceFile sourceFile, String target) {
|
||||||
|
|
||||||
|
public boolean isDirectoryTransform() {
|
||||||
|
return source.endsWith("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/* ###
|
||||||
|
* 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.program.model.sourcemap;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import ghidra.program.database.sourcemap.SourceFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SourcePathTransformers are used to transform {@link SourceFile} paths. The intended use is
|
||||||
|
* to transform the path of a {@link SourceFile} in a programs's {@link SourceFileManager}
|
||||||
|
* before sending the path to an IDE.<br>
|
||||||
|
* <p>
|
||||||
|
* There are two types of transformations: file and directory. File transforms
|
||||||
|
* map a particular {@link SourceFile} to an absolute file path. Directory transforms
|
||||||
|
* transform an initial segment of a path. For example, the directory transforms
|
||||||
|
* "/c:/users/" -> "/src/test/" sends "/c:/users/dir/file1.c" to "/src/test/dir/file1.c"
|
||||||
|
*/
|
||||||
|
public interface SourcePathTransformer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new file transform. Any existing file transform for {@code sourceFile} is
|
||||||
|
* overwritten. {@code path} must be a valid, normalized file path (with forward slashes).
|
||||||
|
* @param sourceFile source file (can't be null).
|
||||||
|
* @param path new path
|
||||||
|
*/
|
||||||
|
public void addFileTransform(SourceFile sourceFile, String path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes any file transform for {@code sourceFile}.
|
||||||
|
* @param sourceFile source file
|
||||||
|
*/
|
||||||
|
public void removeFileTransform(SourceFile sourceFile);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new directory transform. Any existing directory transform for {@code sourceDir}
|
||||||
|
* is overwritten. {@code sourceDir} and {@code targetDir} must be valid, normalized
|
||||||
|
* directory paths (with forward slashes).
|
||||||
|
* @param sourceDir source directory
|
||||||
|
* @param targetDir target directory
|
||||||
|
*/
|
||||||
|
public void addDirectoryTransform(String sourceDir, String targetDir);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes any directory transform associated with {@code sourceDir}
|
||||||
|
* @param sourceDir source directory
|
||||||
|
*/
|
||||||
|
public void removeDirectoryTransform(String sourceDir);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the transformed path for {@code sourceFile}. The transformed path is determined as
|
||||||
|
* follows:<br>
|
||||||
|
* - If there is a file transform for {@code sourceFile}, the file transform is applied.<br>
|
||||||
|
* - Otherwise, the most specific directory transform (i.e., longest source directory string)
|
||||||
|
* is applied.<br>
|
||||||
|
* - If no directory transform applies, the value of {@code useExistingAsDefault} determines
|
||||||
|
* whether the path of {@code sourceFile} or {@code null} is returned.
|
||||||
|
*
|
||||||
|
* @param sourceFile source file to transform
|
||||||
|
* @param useExistingAsDefault whether to return sourceFile's path if no transform applies
|
||||||
|
* @return transformed path or null
|
||||||
|
*/
|
||||||
|
public String getTransformedPath(SourceFile sourceFile, boolean useExistingAsDefault);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all {@link SourcePathTransformRecord}s
|
||||||
|
* @return transform records
|
||||||
|
*/
|
||||||
|
public List<SourcePathTransformRecord> getTransformRecords();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,321 @@
|
||||||
|
/* ###
|
||||||
|
* 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.program.database.sourcemap;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.python.google.common.primitives.Longs;
|
||||||
|
|
||||||
|
import generic.test.AbstractGenericTest;
|
||||||
|
import ghidra.framework.store.LockException;
|
||||||
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.program.model.sourcemap.*;
|
||||||
|
import ghidra.test.ToyProgramBuilder;
|
||||||
|
|
||||||
|
public class UserDataPathTransformerTest extends AbstractGenericTest {
|
||||||
|
|
||||||
|
private Program program;
|
||||||
|
private ToyProgramBuilder builder;
|
||||||
|
private SourceFileManager sourceManager;
|
||||||
|
private SourceFile linuxRoot;
|
||||||
|
private SourceFile windowsRoot;
|
||||||
|
private SourceFile linux1;
|
||||||
|
private SourceFile linux2;
|
||||||
|
private SourceFile linux3;
|
||||||
|
private SourceFile windows1;
|
||||||
|
private SourceFile windows2;
|
||||||
|
private SourceFile windows3;
|
||||||
|
private SourcePathTransformer pathTransformer;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
builder = new ToyProgramBuilder("testprogram", true, false, this);
|
||||||
|
program = builder.getProgram();
|
||||||
|
sourceManager = program.getSourceFileManager();
|
||||||
|
int txID = program.startTransaction("create source path transformer test program");
|
||||||
|
try {
|
||||||
|
linuxRoot = new SourceFile("/file1.c");
|
||||||
|
sourceManager.addSourceFile(linuxRoot);
|
||||||
|
|
||||||
|
windowsRoot = new SourceFile("/c:/file2.c");
|
||||||
|
sourceManager.addSourceFile(windowsRoot);
|
||||||
|
|
||||||
|
linux1 = new SourceFile("/src/dir1/file3.c");
|
||||||
|
sourceManager.addSourceFile(linux1);
|
||||||
|
|
||||||
|
linux2 = new SourceFile("/src/dir1/dir2/file4.c");
|
||||||
|
sourceManager.addSourceFile(linux2);
|
||||||
|
|
||||||
|
linux3 = new SourceFile("/src/dir1/dir3/file5.c");
|
||||||
|
sourceManager.addSourceFile(linux3);
|
||||||
|
|
||||||
|
windows1 = new SourceFile("/c:/src/dir1/file6.c");
|
||||||
|
sourceManager.addSourceFile(windows1);
|
||||||
|
|
||||||
|
windows2 = new SourceFile("/c:/src/dir1/dir2/file7.c");
|
||||||
|
sourceManager.addSourceFile(windows2);
|
||||||
|
|
||||||
|
windows3 = new SourceFile("/c:/src/dir1/dir3/file8.c");
|
||||||
|
sourceManager.addSourceFile(windows3);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
program.endTransaction(txID, true);
|
||||||
|
}
|
||||||
|
pathTransformer = UserDataPathTransformer.getPathTransformer(program);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = NullPointerException.class)
|
||||||
|
public void testNullFileTransform() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addFileTransform(null, "/src/test.c");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testTransformFileToDirectory() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addFileTransform(linux1, "/src/dir/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testTransformFileToRelativePath() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addFileTransform(linux1, "src/dir/file.c");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testTransformFileToNull() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addFileTransform(linux1, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testTransformDirectoryNullSource() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform(null, "/src/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testTransformDirectoryNullDest() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform("/src/test", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testTransformDirectoryToFile() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform("/src/", linux1.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testApplyDirectoryTransformToFile() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform(linux1.getPath(), "/src/test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testTransformDirectoryInvalidSource1() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform("src/test/", "/source/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testTransformDirectoryInvalidSource2() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform("/src/test", "/source/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testTransformDirectoryInvalidDest1() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform("/source/", "/src/test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testTransformDirectoryInvalidDest2() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform("/source/", "src/test/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoDefault() {
|
||||||
|
assertNull(pathTransformer.getTransformedPath(linuxRoot, false));
|
||||||
|
assertNull(pathTransformer.getTransformedPath(linux1, false));
|
||||||
|
assertNull(pathTransformer.getTransformedPath(windowsRoot, false));
|
||||||
|
assertNull(pathTransformer.getTransformedPath(windows1, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformFile() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addFileTransform(linux1, "/src/test/newfile.c");
|
||||||
|
assertEquals("/src/test/newfile.c", pathTransformer.getTransformedPath(linux1, true));
|
||||||
|
assertEquals(linux2.getPath(), pathTransformer.getTransformedPath(linux2, true));
|
||||||
|
assertEquals(windowsRoot.getPath(), pathTransformer.getTransformedPath(windowsRoot, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformLinuxToWindows() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform("/src/dir1/", "/c:/source/");
|
||||||
|
assertEquals(linuxRoot.getPath(), pathTransformer.getTransformedPath(linuxRoot, true));
|
||||||
|
assertEquals("/c:/source/file3.c", pathTransformer.getTransformedPath(linux1, true));
|
||||||
|
assertEquals("/c:/source/dir2/file4.c", pathTransformer.getTransformedPath(linux2, true));
|
||||||
|
assertEquals("/c:/source/dir3/file5.c", pathTransformer.getTransformedPath(linux3, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformWindowsToLinux() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform("/c:/src/dir1/", "/source/");
|
||||||
|
assertEquals(windowsRoot.getPath(), pathTransformer.getTransformedPath(windowsRoot, true));
|
||||||
|
assertEquals("/source/file6.c", pathTransformer.getTransformedPath(windows1, true));
|
||||||
|
assertEquals("/source/dir2/file7.c", pathTransformer.getTransformedPath(windows2, true));
|
||||||
|
assertEquals("/source/dir3/file8.c", pathTransformer.getTransformedPath(windows3, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddingMoreSpecificDirectoryTransform() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform("/src/", "/c:/source/");
|
||||||
|
assertEquals("/c:/source/dir1/file3.c", pathTransformer.getTransformedPath(linux1, true));
|
||||||
|
assertEquals("/c:/source/dir1/dir2/file4.c",
|
||||||
|
pathTransformer.getTransformedPath(linux2, true));
|
||||||
|
pathTransformer.addDirectoryTransform("/src/dir1/dir2/", "/d:/test/");
|
||||||
|
assertEquals("/c:/source/dir1/file3.c", pathTransformer.getTransformedPath(linux1, true));
|
||||||
|
assertEquals("/d:/test/file4.c", pathTransformer.getTransformedPath(linux2, true));
|
||||||
|
pathTransformer.removeDirectoryTransform("/src/");
|
||||||
|
assertEquals(linux1.getPath(), pathTransformer.getTransformedPath(linux1, true));
|
||||||
|
assertEquals("/d:/test/file4.c", pathTransformer.getTransformedPath(linux2, true));
|
||||||
|
pathTransformer.removeDirectoryTransform("/src/dir1/dir2/");
|
||||||
|
assertEquals(linux1.getPath(), pathTransformer.getTransformedPath(linux1, true));
|
||||||
|
assertEquals(linux2.getPath(), pathTransformer.getTransformedPath(linux2, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddingDirectoryThenFileTransform() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform("/src/", "/c:/source/");
|
||||||
|
assertEquals("/c:/source/dir1/file3.c", pathTransformer.getTransformedPath(linux1, true));
|
||||||
|
pathTransformer.addFileTransform(linux1, "/e:/testDirectory/testFile.c");
|
||||||
|
assertEquals("/e:/testDirectory/testFile.c",
|
||||||
|
pathTransformer.getTransformedPath(linux1, true));
|
||||||
|
pathTransformer.removeFileTransform(linux1);
|
||||||
|
assertEquals("/c:/source/dir1/file3.c", pathTransformer.getTransformedPath(linux1, true));
|
||||||
|
pathTransformer.removeDirectoryTransform("/src/");
|
||||||
|
assertEquals(linux1.getPath(), pathTransformer.getTransformedPath(linux1, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUniqueness() {
|
||||||
|
SourcePathTransformer transformer2 = UserDataPathTransformer.getPathTransformer(program);
|
||||||
|
assertTrue(transformer2 == pathTransformer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testNormalizedFile() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addFileTransform(linux1, "/src/dir1/../dir2/file.c");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testNormalizedSourceDirectory() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform("/src/dir1/../dir2/", "/test/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testNormalizedDestDirectory() throws IllegalArgumentException {
|
||||||
|
pathTransformer.addDirectoryTransform("/test/", "src/dir1/../dir2/");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetTransformRecords() throws IllegalArgumentException {
|
||||||
|
assertEquals(0, pathTransformer.getTransformRecords().size());
|
||||||
|
pathTransformer.addFileTransform(linux1, "/test/file10.c");
|
||||||
|
List<SourcePathTransformRecord> transformRecords = pathTransformer.getTransformRecords();
|
||||||
|
assertEquals(1, transformRecords.size());
|
||||||
|
assertEquals(linux1, transformRecords.get(0).sourceFile());
|
||||||
|
assertEquals("/test/file10.c", transformRecords.get(0).target());
|
||||||
|
|
||||||
|
pathTransformer.addFileTransform(linux1, "/test/file20.c");
|
||||||
|
transformRecords = pathTransformer.getTransformRecords();
|
||||||
|
assertEquals(1, transformRecords.size());
|
||||||
|
assertEquals(linux1, transformRecords.get(0).sourceFile());
|
||||||
|
assertEquals("/test/file20.c", transformRecords.get(0).target());
|
||||||
|
|
||||||
|
pathTransformer.addFileTransform(linux2, "/test/file30.c");
|
||||||
|
transformRecords = pathTransformer.getTransformRecords();
|
||||||
|
assertEquals(2, transformRecords.size());
|
||||||
|
SourcePathTransformRecord rec1 =
|
||||||
|
new SourcePathTransformRecord("NONE##" + linux1.getPath(), linux1, "/test/file20.c");
|
||||||
|
SourcePathTransformRecord rec2 =
|
||||||
|
new SourcePathTransformRecord("NONE##" + linux2.getPath(), linux2, "/test/file30.c");
|
||||||
|
assertTrue(transformRecords.contains(rec1));
|
||||||
|
assertTrue(transformRecords.contains(rec2));
|
||||||
|
|
||||||
|
pathTransformer.addDirectoryTransform("/a/b/c/", "/d/e/f/");
|
||||||
|
transformRecords = pathTransformer.getTransformRecords();
|
||||||
|
assertEquals(3, transformRecords.size());
|
||||||
|
SourcePathTransformRecord rec3 = new SourcePathTransformRecord("/a/b/c/", null, "/d/e/f/");
|
||||||
|
assertTrue(transformRecords.contains(rec1));
|
||||||
|
assertTrue(transformRecords.contains(rec2));
|
||||||
|
assertTrue(transformRecords.contains(rec3));
|
||||||
|
|
||||||
|
pathTransformer.addDirectoryTransform("/a/b/c/", "/g/h/i/");
|
||||||
|
transformRecords = pathTransformer.getTransformRecords();
|
||||||
|
assertEquals(3, transformRecords.size());
|
||||||
|
SourcePathTransformRecord rec4 = new SourcePathTransformRecord("/a/b/c/", null, "/g/h/i/");
|
||||||
|
assertTrue(transformRecords.contains(rec1));
|
||||||
|
assertTrue(transformRecords.contains(rec2));
|
||||||
|
assertTrue(transformRecords.contains(rec4));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFileTransformsAndIdentifiers() throws LockException {
|
||||||
|
SourceFile source1 = new SourceFile("/src/file.c");
|
||||||
|
SourceFile source2 =
|
||||||
|
new SourceFile("/src/file.c", SourceFileIdType.TIMESTAMP_64, Longs.toByteArray(0));
|
||||||
|
SourceFile source3 = new SourceFile("/src/file.c", SourceFileIdType.MD5,
|
||||||
|
HexFormat.of().parseHex("0123456789abcdef0123456789abcdef"));
|
||||||
|
|
||||||
|
int txId = program.startTransaction("adding source files");
|
||||||
|
try {
|
||||||
|
sourceManager.addSourceFile(source1);
|
||||||
|
sourceManager.addSourceFile(source2);
|
||||||
|
sourceManager.addSourceFile(source3);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
program.endTransaction(txId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("/src/file.c", pathTransformer.getTransformedPath(source1, true));
|
||||||
|
assertEquals("/src/file.c", pathTransformer.getTransformedPath(source2, true));
|
||||||
|
assertEquals("/src/file.c", pathTransformer.getTransformedPath(source3, true));
|
||||||
|
|
||||||
|
pathTransformer.addFileTransform(source1, "/transformedFile/file.c");
|
||||||
|
assertEquals("/transformedFile/file.c", pathTransformer.getTransformedPath(source1, true));
|
||||||
|
assertEquals("/src/file.c", pathTransformer.getTransformedPath(source2, true));
|
||||||
|
assertEquals("/src/file.c", pathTransformer.getTransformedPath(source3, true));
|
||||||
|
|
||||||
|
pathTransformer.addFileTransform(source2, "/transformedFile/file2.c");
|
||||||
|
assertEquals("/transformedFile/file.c", pathTransformer.getTransformedPath(source1, true));
|
||||||
|
assertEquals("/transformedFile/file2.c", pathTransformer.getTransformedPath(source2, true));
|
||||||
|
assertEquals("/src/file.c", pathTransformer.getTransformedPath(source3, true));
|
||||||
|
|
||||||
|
pathTransformer.addDirectoryTransform("/src/", "/SOURCE/");
|
||||||
|
assertEquals("/transformedFile/file.c", pathTransformer.getTransformedPath(source1, true));
|
||||||
|
assertEquals("/transformedFile/file2.c", pathTransformer.getTransformedPath(source2, true));
|
||||||
|
assertEquals("/SOURCE/file.c", pathTransformer.getTransformedPath(source3, true));
|
||||||
|
|
||||||
|
pathTransformer.removeFileTransform(source2);
|
||||||
|
assertEquals("/transformedFile/file.c", pathTransformer.getTransformedPath(source1, true));
|
||||||
|
assertEquals("/SOURCE/file.c", pathTransformer.getTransformedPath(source2, true));
|
||||||
|
assertEquals("/SOURCE/file.c", pathTransformer.getTransformedPath(source3, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransformerForNullProgram() {
|
||||||
|
assertNull(UserDataPathTransformer.getPathTransformer(null));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue