diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 16ac75be9d..15ff3d8966 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -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| diff --git a/Ghidra/Features/Base/ghidra_scripts/ShowSourceMapEntryStartsScript.java b/Ghidra/Features/Base/ghidra_scripts/ShowSourceMapEntryStartsScript.java new file mode 100644 index 0000000000..2f45404387 --- /dev/null +++ b/Ghidra/Features/Base/ghidra_scripts/ShowSourceMapEntryStartsScript.java @@ -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 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 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 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; + } + } + +} diff --git a/Ghidra/Features/Base/src/main/help/help/topics/SourceFilesTablePlugin/SourceFilesTable.html b/Ghidra/Features/Base/src/main/help/help/topics/SourceFilesTablePlugin/SourceFilesTable.html new file mode 100644 index 0000000000..375a464a2a --- /dev/null +++ b/Ghidra/Features/Base/src/main/help/help/topics/SourceFilesTablePlugin/SourceFilesTable.html @@ -0,0 +1,197 @@ + + + + + + + Source Files + + + + + +

Source File Information

+ +

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.

+ +

A major use case of this information is to synchronize Ghidra with an IDE, such as + Eclipse.

+ +

Source Files

+ +

A source file record in Ghidra consists of three pieces of information:

+ +
+
    +
  1. A path, which must be an absolute, normalized path using forward slashes (think file + URI).
  2. + +
  3. A SourceFileIdType, which can be NONE, UNKNOWN, TIMESTAMP_64, MD5, SHA1, SHA256, + or SHA512.
  4. + +
  5. An identifier, which is the value of the identifier as a byte array.
  6. +
+
+ +

Source Map Entries

+ +

A Source Map Entry associates a source file and a line number to an address or + address range in a program. It consists of:

+ +
+
    +
  1. A source file.
  2. + +
  3. A line number.
  4. + +
  5. A base address.
  6. + +
  7. A length. If the length is non-zero, the entry defines an address range, otherwise it + defines an address.
  8. +
+

+
+ + +

Source map entries are constrained as follows:

+ +
+
    +
  • An address in a program may not have duplicate (same source file, line number, base + address, and length) source file entries.
  • + +
  • 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.
  • +
+

+
+ + +

Source File Manager

+ +

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.

+ +
+

NoteNote 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.

+
+

+
+ + +

Source Path Transformations

+ +

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.

+ +

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.

+ +

There are two type of source path transforms:

+ +
+
    +
  1. File Transforms, which entirely replace a source file's path with another file + path.
  2. + +
  3. Directory Transforms, which replace a parent directory of a source file's path + with another directory.
  4. +
+

+
+ + +

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.

+ +

Source file path transformations are managed using a SourcePathTransformer. Path + transformations can be managed using the actions on the Source + Files Table. In a script, you can get the path transformer for a program via the static + method UserDataPathTransformer.getPathTransformer(Program). Note that modifications to + the path transformer are not affected by undo or redo actions in Ghidra.

+ +

Source Files Table Plugin

+ +

This plugin shows the source file information associated with the current program and allows + the user to manage source file path transforms.

+ +

Source Files Table

+ +
+

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 Transformed Path 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.

+ +

Reload Model

+ +

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.

+ +

Show Source Map Entries

+ +

This action brings up a table which displays all of the source map entries for the + selected source file.

+ +

Transform File

+ +

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.

+ +

Transform Directory

+ +

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.

+
+ +

Transforms Table

+ +
+

This table shows all of the source file transformations defined for a program.

+ +

Remove Transform

+ +

This action removes the selected transform from the list of transforms.

+ +

Edit Transform

+
+ +

This action allows you to change the destination of a transform (but not the source).

+ +

Related Topics:

+ +
+
+
+
+
+
+
+
+ + diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFileRowObject.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFileRowObject.java new file mode 100644 index 0000000000..837439f114 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFileRowObject.java @@ -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(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFilesTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFilesTableModel.java new file mode 100644 index 0000000000..7c83bb7c20 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFilesTableModel.java @@ -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 { + + 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 createTableColumnDescriptor() { + TableColumnDescriptor 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 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 { + + @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 { + + @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 { + + @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 { + + @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 { + + @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 { + + @Override + public String getColumnName() { + return "Entry Count"; + } + + @Override + public Integer getValue(SourceFileRowObject rowObject, Settings settings, Object data, + ServiceProvider services) throws IllegalArgumentException { + return rowObject.getNumSourceMapEntries(); + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFilesTablePlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFilesTablePlugin.java new file mode 100644 index 0000000000..8f044b9228 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFilesTablePlugin.java @@ -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 + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFilesTableProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFilesTableProvider.java new file mode 100644 index 0000000000..f3cbb85f46 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceFilesTableProvider.java @@ -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 sourceFilesTable; + private TransformerTableModel transformsModel; + private GhidraFilterTable 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 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 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(); + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceMapEntryRowObject.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceMapEntryRowObject.java new file mode 100644 index 0000000000..1ad48486ed --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceMapEntryRowObject.java @@ -0,0 +1,76 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.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; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceMapEntryTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceMapEntryTableModel.java new file mode 100644 index 0000000000..2cd350d982 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceMapEntryTableModel.java @@ -0,0 +1,201 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.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 { + 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 accumulator, TaskMonitor monitor) + throws CancelledException { + List mapEntries = sourceManager.getSourceMapEntries(sourceFile); + Map 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 createTableColumnDescriptor() { + TableColumnDescriptor 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 { + + @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 { + + @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 { + + @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 { + + @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 { + + @Override + public String getColumnName() { + return "Count"; + } + + @Override + public Integer getValue(SourceMapEntryRowObject rowObject, Settings settings, Object data, + ServiceProvider services) throws IllegalArgumentException { + return rowObject.getCount(); + } + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceMapEntryToAddressTableRowMapper.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceMapEntryToAddressTableRowMapper.java new file mode 100644 index 0000000000..a3e5e5fb94 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceMapEntryToAddressTableRowMapper.java @@ -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 { + + @Override + public Address map(SourceMapEntryRowObject rowObject, Program program, + ServiceProvider serviceProvider) { + return rowObject.getBaseAddress(); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceMapEntryToProgramLocationRowMapper.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceMapEntryToProgramLocationRowMapper.java new file mode 100644 index 0000000000..5c26c41453 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/SourceMapEntryToProgramLocationRowMapper.java @@ -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 { + + @Override + public ProgramLocation map(SourceMapEntryRowObject rowObject, Program data, + ServiceProvider serviceProvider) { + return new ProgramLocation(data, rowObject.getBaseAddress()); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/TransformerTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/TransformerTableModel.java new file mode 100644 index 0000000000..ad46a31ced --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/sourcefilestable/TransformerTableModel.java @@ -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 { + + 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 accumulator, TaskMonitor monitor) + throws CancelledException { + if (pathTransformer == null) { + return; + } + for (SourcePathTransformRecord transformRecord : pathTransformer.getTransformRecords()) { + accumulator.add(transformRecord); + } + return; + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor 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 { + + @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 { + + @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 { + + @Override + public String getColumnName() { + return "Directory Transform"; + } + + @Override + public Boolean getValue(SourcePathTransformRecord rowObject, Settings settings, Object data, + ServiceProvider services) throws IllegalArgumentException { + return rowObject.isDirectoryTransform(); + } + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramUserDataDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramUserDataDB.java index 781d9ca1fa..f115f0fcc0 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramUserDataDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramUserDataDB.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -414,42 +414,36 @@ class ProgramUserDataDB extends DomainObjectAdapterDB implements ProgramUserData * @param monitor task monitor */ private void setLanguage(LanguageTranslator translator, TaskMonitor monitor) { - lock.acquire(); + //setEventsEnabled(false); try { - //setEventsEnabled(false); - try { - language = translator.getNewLanguage(); - languageID = language.getLanguageID(); - languageVersion = language.getVersion(); + language = translator.getNewLanguage(); + languageID = language.getLanguageID(); + languageVersion = language.getVersion(); - // AddressFactory need not change since we are using the instance from the - // Program which would have already been subject to an upgrade - addressMap.setLanguage(language, addressFactory, translator); + // AddressFactory need not change since we are using the instance from the + // Program which would have already been subject to an upgrade + addressMap.setLanguage(language, addressFactory, translator); - clearCache(true); + clearCache(true); - DBRecord record = SCHEMA.createRecord(new StringField(LANGUAGE_ID)); - record.setString(VALUE_COL, languageID.getIdAsString()); - table.putRecord(record); + DBRecord record = SCHEMA.createRecord(new StringField(LANGUAGE_ID)); + record.setString(VALUE_COL, languageID.getIdAsString()); + table.putRecord(record); - setChanged(true); - clearCache(true); + setChanged(true); + clearCache(true); - //invalidate(); + //invalidate(); - } - catch (Throwable t) { - throw new IllegalStateException( - "Set language aborted - program user data is now in an unusable state!", t); - } + } + catch (Throwable t) { + throw new IllegalStateException( + "Set language aborted - program user data is now in an unusable state!", t); + } // finally { // setEventsEnabled(true); // } - } - finally { - lock.release(); - } } @Override @@ -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 getStringPropertyNames() { - return metadata.keySet(); + public synchronized Set getStringPropertyNames() { + return new HashSet(metadata.keySet()); } @Override - public String removeStringProperty(String propertyName) { + public synchronized String removeStringProperty(String propertyName) { changed = true; return metadata.remove(propertyName); } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/UserDataPathTransformer.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/UserDataPathTransformer.java new file mode 100644 index 0000000000..e3fab3204b --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/sourcemap/UserDataPathTransformer.java @@ -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.
+ *

+ * Use the static method {@link UserDataPathTransformer#getPathTransformer} to get the transformer + * for a program.
+ *

+ * 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 pathMap; + private Map fileMap; + private ProgramUserData userData; + private static final String FILE_SCHEME = "file"; + private static HexFormat hexFormat = HexFormat.of(); + private static HashMap 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 getTransformRecords() { + List transformRecords = new ArrayList<>(); + for (Entry entry : pathMap.entrySet()) { + transformRecords + .add(new SourcePathTransformRecord(entry.getKey(), null, entry.getValue())); + } + for (Entry 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); + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourcePathTransformRecord.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourcePathTransformRecord.java new file mode 100644 index 0000000000..543821e71b --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourcePathTransformRecord.java @@ -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("/"); + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourcePathTransformer.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourcePathTransformer.java new file mode 100644 index 0000000000..a62bd06b37 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/sourcemap/SourcePathTransformer.java @@ -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.
+ *

+ * 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:
+ * - If there is a file transform for {@code sourceFile}, the file transform is applied.
+ * - Otherwise, the most specific directory transform (i.e., longest source directory string) + * is applied.
+ * - 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 getTransformRecords(); + +} diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/UserDataPathTransformerTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/UserDataPathTransformerTest.java new file mode 100644 index 0000000000..ff805d568e --- /dev/null +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/program/database/sourcemap/UserDataPathTransformerTest.java @@ -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 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)); + } +}