GP-4190 sourcefiletable plugin and path transformer

This commit is contained in:
James 2024-12-20 11:34:40 -05:00
parent 8a170df554
commit 113943048e
17 changed files with 2324 additions and 32 deletions

View file

@ -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/UnableToLaunch.png||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/images/NumElementsPrompt.png||GHIDRA||||END|
src/main/help/help/topics/StackEditor/images/StackEditor.png||GHIDRA||||END|

View file

@ -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;
}
}
}

View file

@ -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>

View file

@ -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();
}
}

View file

@ -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();
}
}
}

View file

@ -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
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}

View file

@ -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();
}
}

View file

@ -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());
}
}

View file

@ -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();
}
}
}

View file

@ -414,8 +414,6 @@ class ProgramUserDataDB extends DomainObjectAdapterDB implements ProgramUserData
* @param monitor task monitor
*/
private void setLanguage(LanguageTranslator translator, TaskMonitor monitor) {
lock.acquire();
try {
//setEventsEnabled(false);
try {
@ -447,10 +445,6 @@ class ProgramUserDataDB extends DomainObjectAdapterDB implements ProgramUserData
// setEventsEnabled(true);
// }
}
finally {
lock.release();
}
}
@Override
public synchronized boolean canSave() {
@ -676,24 +670,24 @@ class ProgramUserDataDB extends DomainObjectAdapterDB implements ProgramUserData
}
@Override
public void setStringProperty(String propertyName, String value) {
public synchronized void setStringProperty(String propertyName, String value) {
metadata.put(propertyName, value);
changed = true;
}
@Override
public String getStringProperty(String propertyName, String defaultValue) {
public synchronized String getStringProperty(String propertyName, String defaultValue) {
String value = metadata.get(propertyName);
return value == null ? defaultValue : value;
}
@Override
public Set<String> getStringPropertyNames() {
return metadata.keySet();
public synchronized Set<String> getStringPropertyNames() {
return new HashSet<String>(metadata.keySet());
}
@Override
public String removeStringProperty(String propertyName) {
public synchronized String removeStringProperty(String propertyName) {
changed = true;
return metadata.remove(propertyName);
}

View file

@ -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);
}
}

View file

@ -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("/");
}
}

View file

@ -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();
}

View file

@ -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));
}
}