diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 489a593cd8..2666f1b011 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -355,6 +355,8 @@ src/main/help/help/topics/DataTypeManagerPlugin/images/EditPaths.png||GHIDRA|||| src/main/help/help/topics/DataTypeManagerPlugin/images/FavoriteDts.png||GHIDRA||||END| src/main/help/help/topics/DataTypeManagerPlugin/images/FindDataTypes.png||GHIDRA||||END| src/main/help/help/topics/DataTypeManagerPlugin/images/FindDataTypesBySize.png||GHIDRA||||END| +src/main/help/help/topics/DataTypeManagerPlugin/images/MergeConfirmationDialog.png||GHIDRA||||END| +src/main/help/help/topics/DataTypeManagerPlugin/images/MergeErrorDialog.png||GHIDRA||||END| src/main/help/help/topics/DataTypeManagerPlugin/images/PreviewWindow.png||GHIDRA||||END| src/main/help/help/topics/DataTypeManagerPlugin/images/RevertDialog.png||GHIDRA||||END| src/main/help/help/topics/DataTypeManagerPlugin/images/SearchResults.png||GHIDRA||||END| diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/data_type_manager_description.htm b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/data_type_manager_description.htm index 0b88a58314..8872548c81 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/data_type_manager_description.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/data_type_manager_description.htm @@ -1086,12 +1086,54 @@

A data type can be replaced by another data type. This means that every occurrence of the original data type in a program is replaced by the new data type and the original data type is deleted. To replace a data type, right-click on the type to be replaced - and select the Replace Data Type... action. This will show a + and select the Replace... action. This will show a dialog that allows you to choose the replacement data type.

+

Merging Data Types

+ +
+

Some data types can be merged with others of the same type, provided there are no + conflicting entries in the two data types. Currently, + structures, unions and enums are supported for merging. If a conflict is detected, + an error dialog will be displayed showing the two data types and a message explaining + why the merge failed.

+

If the merge succeeds in producing a merged data type result, a confirmation dialog + will be displayed showing the result and the two datatypes being merged. If the user + confirms the merge, the original target data type will have its internals replaced + with the merged data type and the other data type will have all its references replaced + with the resulting datatype and then it will be deleted. +

+

To Merge a data type, right-click on the type to be merged into and select the + Merge... action. This will show a + dialog that allows + you to choose the data type to merge with. +

+

If the merge succeeds in producing a merged data type, + the following confirmation dialog will be displayed before the changes are actually + applied.

+

+ +

+ +

This dialog shows a side-by-side preview of the merged data type along with two + data types that were merged. It also will display any warning associated associated + with the merge. If the user presses the apply button, the first data type will be updated + to match the preview data type and the second data type will be deleted with all of its + uses replaced with the updated first data type.

+ + +

If the merge fails, the following error dialog is displayed showing a side-by-side + view of the two data types that couldn't be merged, along with a description of the error + that prevented the merge.

+ +

+ +

+


+

Setting Favorite Data Types

diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/data_type_manager_window.html b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/data_type_manager_window.html index 65835c2df9..74bc86cac8 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/data_type_manager_window.html +++ b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/data_type_manager_window.html @@ -582,7 +582,7 @@ -

Replace Data Type...

+

Replace...

The Replace... action is used to @@ -590,7 +590,13 @@ selected data type and all occurrences in the program.

- +

Merge...

+ +
+

The Merge... action is used to + merge a + data type with another data type.

+

Collapse All

diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/images/MergeConfirmationDialog.png b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/images/MergeConfirmationDialog.png new file mode 100644 index 0000000000..18af9b1303 Binary files /dev/null and b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/images/MergeConfirmationDialog.png differ diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/images/MergeErrorDialog.png b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/images/MergeErrorDialog.png new file mode 100644 index 0000000000..5813d6e57f Binary files /dev/null and b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/images/MergeErrorDialog.png differ diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesProvider.java index af8af9cb2a..b6196bdbd6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/DataTypesProvider.java @@ -162,6 +162,7 @@ public class DataTypesProvider extends ComponentProviderAdapter { addLocalAction(new CopyAction(plugin)); addLocalAction(new PasteAction(plugin)); addLocalAction(new ReplaceDataTypeAction(plugin)); + addLocalAction(new MergeDataTypeAction(plugin)); addLocalAction(new DeleteAction(plugin)); addLocalAction(new DeleteArchiveAction(plugin)); addLocalAction(new RenameAction(plugin)); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/AbstractDataTypeMergeDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/AbstractDataTypeMergeDialog.java new file mode 100644 index 0000000000..de452bc4ca --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/AbstractDataTypeMergeDialog.java @@ -0,0 +1,175 @@ +/* ### + * 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.datamgr.actions; + +import java.awt.*; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.border.TitledBorder; + +import org.apache.commons.lang3.StringUtils; + +import docking.DialogComponentProvider; +import docking.widgets.textpane.GHtmlTextPane; +import generic.theme.GThemeDefaults.Colors; +import ghidra.app.util.ToolTipUtils; +import ghidra.program.model.data.DataType; + +/** + * Base class for both the datatype merge confirmation dialog and the datatype merge error dialog + */ +public abstract class AbstractDataTypeMergeDialog extends DialogComponentProvider { + private static final int MAX_PREFERRED_HEIGHT = 800; + private DataType result; + private DataType mergeTo; + private DataType mergeFrom; + private String message; + + public AbstractDataTypeMergeDialog(String title, DataType result, DataType mergeTo, + DataType mergeFrom, String message) { + super(title, true, false, true, false); + this.result = result; + this.mergeTo = mergeTo; + this.mergeFrom = mergeFrom; + this.message = message; + this.setRememberSize(false); + + addWorkPanel(buildMainPanel()); + } + + protected abstract String getMessageAreaTitle(); + + private JComponent buildMainPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0)); + + if (!StringUtils.isBlank(message)) { + panel.add(buildWarningPanel(), BorderLayout.NORTH); + } + panel.add(buildScrollablePreviewPanel(), BorderLayout.CENTER); + + // Restrict the initial size so that larger sized datatypes doesn't make the dialog too big + Dimension preferredSize = panel.getPreferredSize(); + if (preferredSize.height > MAX_PREFERRED_HEIGHT) { + panel.setPreferredSize(new Dimension(preferredSize.width, 800)); + } + return panel; + } + + private JComponent buildScrollablePreviewPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + panel.add(buildTitlePanel(), BorderLayout.NORTH); + JScrollPane scroll = new JScrollPane(buildPreviewPanel()); + panel.add(scroll, BorderLayout.CENTER); + + return panel; + } + + private JComponent buildTitlePanel() { + JPanel panel = new JPanel(new GridLayout(1, 3, 10, 10)); + if (result != null) { + panel.add(new JLabel("DataType 1 (will be overwritten)")); + panel.add(new JLabel("Result Preview")); + panel.add(new JLabel("DataType 2 (will be replaced and deleted)")); + } + else { + panel.add(new JLabel("DataType 1")); + panel.add(new JLabel("DataType 2")); + } + return panel; + } + + private JComponent buildPreviewPanel() { + JPanel panel = new PreviewPanel(); + panel.add(buildPreview(mergeTo)); + if (result != null) { + panel.add(buildPreview(result)); + } + panel.add(buildPreview(mergeFrom)); + return panel; + } + + private Component buildPreview(DataType dataType) { + JPanel panel = new JPanel(new BorderLayout()); + JTextPane previewPane = new GHtmlTextPane(); + previewPane.setEditable(false); + previewPane.setBorder(BorderFactory.createLoweredBevelBorder()); + previewPane.setBackground(Colors.BACKGROUND); + + String previewText = ToolTipUtils.getFullToolTipText(dataType); + previewPane.setText(previewText); + previewPane.setCaretPosition(0); + panel.add(previewPane, BorderLayout.CENTER); + + return panel; + } + + private Component buildWarningPanel() { + JPanel panel = new JPanel(new BorderLayout()); + Border emptyBorder = BorderFactory.createEmptyBorder(10, 10, 10, 10); + TitledBorder title = BorderFactory.createTitledBorder(emptyBorder, getMessageAreaTitle()); + Border innerBorder = BorderFactory.createEmptyBorder(5, 10, 0, 0); + panel.setBorder(BorderFactory.createCompoundBorder(title, innerBorder)); + + JTextArea textArea = new JTextArea(); + textArea.setEditable(false); + textArea.insert(message, 0); + panel.add(textArea, BorderLayout.CENTER); + + return panel; + } + + private static class PreviewPanel extends JPanel implements Scrollable { + PreviewPanel() { + super(new GridLayout(1, 3, 10, 10)); + } + + @Override + public Dimension getPreferredScrollableViewportSize() { + return getPreferredSize(); + } + + @Override + public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, + int direction) { + return 10; + } + + @Override + public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, + int direction) { + return 50; + } + + @Override + public boolean getScrollableTracksViewportWidth() { + return true; + } + + @Override + public boolean getScrollableTracksViewportHeight() { + // we want to stretch if viewport is bigger, but use scrollbars if smaller + if (getParent() instanceof JViewport viewport) { + return getPreferredSize().height < viewport.getHeight(); + } + return false; + } + + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/DataTypeMergeConfirmationDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/DataTypeMergeConfirmationDialog.java new file mode 100644 index 0000000000..e0141ea86d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/DataTypeMergeConfirmationDialog.java @@ -0,0 +1,68 @@ +/* ### + * 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.datamgr.actions; + +import java.util.List; +import java.util.stream.Collectors; + +import ghidra.program.model.data.DataType; +import ghidra.util.HelpLocation; + +/** + * Confirmation dialog for merging two datatypes. The dialog displays the resulting datatype along + * with the two being merged in a side by side view. Also displays any warning messages associated + * with the merge. + */ +public class DataTypeMergeConfirmationDialog extends AbstractDataTypeMergeDialog { + + private boolean cancelled = false; + + public DataTypeMergeConfirmationDialog(DataType result, DataType mergeTo, DataType mergeFrom, + List warnings) { + super("Merge Data Types?", result, mergeTo, mergeFrom, join(warnings)); + setHelpLocation(new HelpLocation("DataTypeManagerPlugin", "Merge_Confirmation")); + addApplyButton(); + addCancelButton(); + } + + @Override + protected void applyCallback() { + close(); + } + + @Override + protected void cancelCallback() { + cancelled = true; + close(); + } + + public boolean wasCancelled() { + return cancelled; + } + + @Override + protected String getMessageAreaTitle() { + return "Warnings:"; + } + + private static String join(List lines) { + if (lines.isEmpty()) { + return null; + } + return lines.stream().collect(Collectors.joining("\n")); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/DataTypeMergeErrorDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/DataTypeMergeErrorDialog.java new file mode 100644 index 0000000000..6fce896666 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/DataTypeMergeErrorDialog.java @@ -0,0 +1,42 @@ +/* ### + * 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.datamgr.actions; + +import ghidra.program.model.data.DataType; +import ghidra.util.HelpLocation; + +/** + * Dialog for showing datatype merge errors. The dialog shows the error message and a display + * of the two datatypes that couldn't be merged. + */ +public class DataTypeMergeErrorDialog extends AbstractDataTypeMergeDialog { + + public DataTypeMergeErrorDialog(DataType mergeTo, DataType mergeFrom, String error) { + super("Merge Failed", null, mergeTo, mergeFrom, error); + setHelpLocation(new HelpLocation("DataTypeManagerPlugin", "Merge_Error")); + addOKButton(); + } + + @Override + protected void okCallback() { + close(); + } + + @Override + protected String getMessageAreaTitle() { + return "Merge Failed:"; + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/MergeDataTypeAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/MergeDataTypeAction.java new file mode 100644 index 0000000000..241a91d949 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/MergeDataTypeAction.java @@ -0,0 +1,214 @@ +/* ### + * 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.datamgr.actions; + +import javax.swing.*; +import javax.swing.tree.TreePath; + +import docking.ActionContext; +import docking.DockingWindowManager; +import docking.action.DockingAction; +import docking.action.MenuData; +import docking.widgets.label.GLabel; +import docking.widgets.tree.GTree; +import ghidra.app.plugin.core.datamgr.DataTypeManagerPlugin; +import ghidra.app.plugin.core.datamgr.DataTypesActionContext; +import ghidra.app.plugin.core.datamgr.tree.DataTypeNode; +import ghidra.app.plugin.core.datamgr.tree.DataTypeTreeNode; +import ghidra.app.util.datatype.DataTypeSelectionDialog; +import ghidra.app.util.datatype.DataTypeSelectionEditor; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.database.data.DataTypeUtilities; +import ghidra.program.database.data.ProgramDataTypeManager; +import ghidra.program.database.data.merge.DataTypeMergeException; +import ghidra.program.database.data.merge.DataTypeMerger; +import ghidra.program.model.data.*; +import ghidra.util.HelpLocation; +import ghidra.util.Msg; +import ghidra.util.data.DataTypeParser.AllowedDataTypes; +import ghidra.util.layout.VerticalLayout; + +/** + * Replace the selected data type with the chosen data type + */ +public class MergeDataTypeAction extends DockingAction { + + private DataTypeManagerPlugin plugin; + + public MergeDataTypeAction(DataTypeManagerPlugin plugin) { + super("Merge Data Types", plugin.getName()); + + this.plugin = plugin; + setPopupMenuData( + new MenuData(new String[] { "Merge..." }, "EditAdvanced")); + setHelpLocation(new HelpLocation("DataTypeManagerPlugin", "merge_datatypes")); + } + + @Override + public boolean isAddToPopup(ActionContext context) { + DataTypeTreeNode node = getSelectedDataTypeTreeNode(context); + return node instanceof DataTypeNode; + } + + @Override + public boolean isEnabledForContext(ActionContext context) { + DataTypeTreeNode node = getSelectedDataTypeTreeNode(context); + if (node == null) { + return false; + } + + if (!(node instanceof DataTypeNode dtNode)) { + return false; + } + + DataType dataType = dtNode.getDataType(); + DataTypeManager dataTypeManager = dataType.getDataTypeManager(); + + // for now, only allow merging on program datatypes. + if (!(dataTypeManager instanceof ProgramDataTypeManager)) { + return false; + } + + if (!dtNode.isModifiable()) { + return false; + } + + if (dataType instanceof BadDataType) { + // Although BAD datatype should not appear in tree, if it does replace is + // not supported. Delete should be used instead. + return false; + } + return true; + } + + private DataTypeTreeNode getSelectedDataTypeTreeNode(ActionContext context) { + if (!(context instanceof DataTypesActionContext)) { + return null; + } + + GTree gTree = (GTree) context.getContextObject(); + TreePath[] selectionPaths = gTree.getSelectionPaths(); + if (selectionPaths == null || selectionPaths.length == 0) { + return null; + } + + if (selectionPaths.length > 1) { + return null; + } + + DataTypeTreeNode node = (DataTypeTreeNode) selectionPaths[0].getLastPathComponent(); + return node; + } + + @Override + public void actionPerformed(ActionContext context) { + + DataTypeTreeNode node = getSelectedDataTypeTreeNode(context); + String name = node.getName(); + DataType mergeToDt = ((DataTypeNode) node).getDataType(); + DataTypeManager dtm = mergeToDt.getDataTypeManager(); + + PluginTool tool = plugin.getTool(); + DataTypeSelectionDialog selectionDialog = new DataTypeMergeSelectionDialog(name); + tool.showDialog(selectionDialog); + DataType selectedDt = selectionDialog.getUserChosenDataType(); + if (selectedDt == null) { + return; // cancelled + } + + DataTypeManager newDtm = selectedDt.getDataTypeManager(); + if (!(newDtm instanceof ProgramDataTypeManager)) { + Msg.showError(this, null, "Merge Failed", "Merge source must be a program datatype."); + return; + } + + int txId = dtm.startTransaction("Merge Data Type"); + try { + merge(mergeToDt, selectedDt); + } + finally { + dtm.endTransaction(txId, true); + } + } + + private void merge(DataType mergeToDt, DataType mergeFromDt) { + try { + DataTypeMerger merger = DataTypeUtilities.getMerger(mergeToDt, mergeFromDt); + DataType merged = merger.merge(); + + if (confirmMerger(merger, merged, mergeToDt, mergeFromDt)) { + DataTypeManager dtm = mergeToDt.getDataTypeManager(); + // first replace the guts of the original 'mergeTo' datatype with the results + mergeToDt.replaceWith(merged); + // now replace all uses of the mergeFromDt with the merged datatype and remove it + dtm.replaceDataType(mergeFromDt, mergeToDt, false); + } + } + catch (DataTypeMergeException e) { + DataTypeMergeErrorDialog dialog = + new DataTypeMergeErrorDialog(mergeToDt, mergeFromDt, e.getMessage()); + DockingWindowManager.showDialog(dialog); + } + catch (DataTypeDependencyException e) { + Msg.showError(this, null, "Merge Failed", + "Merge failed. Existing type '%s', replacement type '%s'.".formatted( + mergeFromDt.getName(), + mergeToDt.getName()), + e); + } + } + + private boolean confirmMerger(DataTypeMerger merger, DataType merged, DataType mergeTo, + DataType mergeFrom) { + DataTypeMergeConfirmationDialog dialog = + new DataTypeMergeConfirmationDialog(merged, mergeTo, mergeFrom, merger.getWarnings()); + + DockingWindowManager.showDialog(dialog); + return !dialog.wasCancelled(); + + } + + private class DataTypeMergeSelectionDialog extends DataTypeSelectionDialog { + + private String name; + + public DataTypeMergeSelectionDialog(String name) { + super(plugin.getTool(), plugin.getProgram().getDataTypeManager(), -1, + AllowedDataTypes.ALL); + this.name = name; + setHelpLocation(getHelpLocation()); + } + + @Override + protected JComponent createEditorPanel(DataTypeSelectionEditor dtEditor) { + + setTitle("Merge '" + name + "'"); + + JPanel updatedPanel = new JPanel(); + updatedPanel.setBorder(BorderFactory.createEmptyBorder(5, 10, 10, 0)); + updatedPanel.setLayout(new VerticalLayout(5)); + + GLabel label = new GLabel("Choose the data type to merge: "); + label.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0)); + updatedPanel.add(label); + + updatedPanel.add(dtEditor.getEditorComponent()); + + return updatedPanel; + } + + } +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/ReplaceDataTypeAction.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/ReplaceDataTypeAction.java index 018935d0cf..79f65c4454 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/ReplaceDataTypeAction.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/ReplaceDataTypeAction.java @@ -48,16 +48,13 @@ public class ReplaceDataTypeAction extends DockingAction { super("Replace", plugin.getName()); this.plugin = plugin; - setPopupMenuData(new MenuData(new String[] { "Replace..." }, "Edit")); + setPopupMenuData(new MenuData(new String[] { "Replace..." }, "EditAdvanced")); } @Override public boolean isAddToPopup(ActionContext context) { DataTypeTreeNode node = getSelectedDataTypeTreeNode(context); - if (node instanceof BuiltInArchiveNode) { - return false; - } - return (node != null); + return node instanceof DataTypeNode; } @Override diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/DataTypeUtilities.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/DataTypeUtilities.java index 5d76895dc6..8ec8391ec7 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/DataTypeUtilities.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/DataTypeUtilities.java @@ -21,6 +21,7 @@ import java.util.regex.*; import ghidra.app.util.NamespaceUtils; import ghidra.app.util.SymbolPathParser; import ghidra.docking.settings.Settings; +import ghidra.program.database.data.merge.*; import ghidra.program.model.data.*; import ghidra.program.model.data.Enum; import ghidra.program.model.listing.*; @@ -1003,4 +1004,47 @@ public class DataTypeUtilities { public static boolean equalsIgnoreConflict(String name1, String name2) { return getNameWithoutConflict(name1).equals(getNameWithoutConflict(name2)); } + + /** + * Convenience method for getting the appropriate datatype merger or throwing an exception + * if the two datatypes are not eligible to be merged. + * @param dt1 the first datatype to be merged + * @param dt2 the second datatype to be merged + * @return A merger to be used to merge the two data types. + * @throws DataTypeMergeException if the two datatypes are not the same type or their type + * is not supported for merging + */ + public static DataTypeMerger getMerger(DataType dt1, DataType dt2) + throws DataTypeMergeException { + + if (dt1 instanceof Structure struct1) { + if (dt2 instanceof Structure struct2) { + return new StructureMerger(struct1, struct2); + } + error("structure", dt1, dt2); + } + + if (dt1 instanceof Union union1) { + if (dt2 instanceof Union union2) { + return new UnionMerger(union1, union2); + } + error("union", dt1, dt2); + } + + if (dt1 instanceof Enum enum1) { + if (dt2 instanceof Enum enum2) { + return new EnumMerger(enum1, enum2); + } + error("enum", dt1, dt2); + } + + throw new DataTypeMergeException("Merge target must be one of structure, union, or enum."); + } + + private static void error(String typeName, DataType mergeToDt, DataType selectedDt) + throws DataTypeMergeException { + String msg = "Can't merge non-%s '%s' datatype into structure '%s' ".formatted(typeName, + selectedDt.getName(), mergeToDt.getName()); + throw new DataTypeMergeException(msg); + } } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/EnumDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/EnumDB.java index ab52133756..34833d665b 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/EnumDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/EnumDB.java @@ -29,8 +29,8 @@ import ghidra.docking.settings.Settings; import ghidra.docking.settings.SettingsDefinition; import ghidra.program.database.DBObjectCache; import ghidra.program.model.data.*; -import ghidra.program.model.data.Enum; import ghidra.program.model.data.DataTypeConflictHandler.ConflictResult; +import ghidra.program.model.data.Enum; import ghidra.program.model.mem.MemBuffer; import ghidra.program.model.mem.MemoryAccessException; import ghidra.program.model.scalar.Scalar; @@ -423,7 +423,7 @@ class EnumDB extends DataTypeDB implements Enum { } @Override - public DataType clone(DataTypeManager dtm) { + public Enum clone(DataTypeManager dtm) { if (dtm == getDataTypeManager()) { return this; } @@ -933,4 +933,22 @@ class EnumDB extends DataTypeDB implements Enum { lock.release(); } } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getPathName() + "\n"); + buf.append("\tDescription: " + getDescription()); + buf.append("\nValues: \n"); + for (String name : getNames()) { + buf.append("\t" + name + ": " + getValue(name)); + String comment = getComment(name); + if (comment != null) { + buf.append(" comment"); + } + buf.append("\n"); + } + return buf.toString(); + } + } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/DataTypeMergeException.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/DataTypeMergeException.java new file mode 100644 index 0000000000..66860b8cd4 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/DataTypeMergeException.java @@ -0,0 +1,25 @@ +/* ### + * 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.data.merge; + +/** + * Exception thrown when an error occurs when attempting to merge two datatypes. + */ +public class DataTypeMergeException extends Exception { + public DataTypeMergeException(String message) { + super(message); + } +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/DataTypeMerger.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/DataTypeMerger.java new file mode 100644 index 0000000000..ab8c8007ba --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/DataTypeMerger.java @@ -0,0 +1,183 @@ +/* ### + * 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.data.merge; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +import ghidra.program.model.data.*; + +/** + * Base class for datatype mergers. Note that these mergers are non interactive and can only + * merge datatypes that have no conflicting internal components or settings. + *

+ * The basic pattern is to create a merger with the two datatypes to be merged and then call + * the {@link #merge()} method to produce the new datatype that is the merger of the two original + * datatypes. The new datatype will have the same name and category path of the first original + * datatype. + *

+ * Even if the merge is able to successfully complete and return a new datatype, the merge may + * have associated warnings that can be retrieved by calling {@link #getWarnings()}. The intent + * is that the warning messages can be presented to the user before applying the new datatype. + *

+ * Typically, after merging the two datatypes, the client will replace the first datatype with + * the merged datatype (it already has the same name and category). Then all uses of the second + * datatype will be replaced with updated first datatype. + * + * @param The specific type of datatype being merged. + */ +public abstract class DataTypeMerger { + + private T dt1; + private T dt2; + + protected T working; + protected T other; + private List warnings = new ArrayList<>(); + + protected DataTypeMerger(T dt1, T dt2) { + this.dt1 = dt1; + this.dt2 = dt2; + } + + /** + * Merge the two datatypes that this object was created with into a new datatype. + * @return the the new merged datatype. + * @throws DataTypeMergeException thrown if the merge was unable to complete due to some + * conflict. + */ + public final T merge() throws DataTypeMergeException { + warnings.clear(); + intializedDataTypes(); + doMerge(); + return working; + } + + @SuppressWarnings("unchecked") + private void intializedDataTypes() { + DataTypeManager dtm = dt1.getDataTypeManager(); + // copy the first datatype to be the working datatype, this forces a complete new object + // versus a clone which may return itself. + this.working = (T) dt1.copy(dtm); + + // we clone the second datatype to make sure it has the same data type organization + this.other = (T) dt2.clone(dtm); + } + + protected abstract void doMerge() throws DataTypeMergeException; + + /** + * {@return any warnings that were generated by a call to the merge() method} + */ + public List getWarnings() { + return warnings; + } + + /** + * {@return true if there are any warnings} + */ + public boolean hasWarnings() { + return !warnings.isEmpty(); + } + + /** + * Adds a warning to the list of warnings. + * @param message the warning message to add + */ + protected void warning(String message) { + warnings.add(message); + } + + /** + * Generates a DataTypeMergeException with the given error message. + * @param message the error message + * @throws DataTypeMergeException with the given message + */ + protected void error(String message) throws DataTypeMergeException { + throw new DataTypeMergeException(message); + } + + /** + * Joins two comment strings unless they are identical or one is null or blank. + * @param comment1 the first comment + * @param comment2 the second comment + * @return a new string with the two non-blank, non-identical commments joined by a space. + */ + protected String join(String comment1, String comment2) { + if (StringUtils.isBlank(comment1)) { + return comment2; + } + else if (StringUtils.isBlank(comment2)) { + return comment1; + } + if (comment1.equals(comment2)) { + return comment1; + } + return comment1 + " " + comment2; + } + + protected DataType pickBestTypeForMerge(DataType type1, DataType type2) { + if (type1.getLength() != type2.getLength()) { + return null; + } + if (canUpgrade(type1, type2)) { + return type2; + } + if (canUpgrade(type2, type1)) { + return type1; + } + return null; + } + + /** + * Checks if one datatype can can be upgraded to a another during a merge operation. In this + * context, upgrade means that one datatype is often a stand-in for another because there + * wasn't initially enough information to get the correct type. For example, any undefined + * type of some size is a stand-in for any other datatype of that size, so undefined types + * can always be upgraded to another type of the same size. Another case is integer types are + * often mistaken for pointers, integer types can be upgraded to pointers. + * @param from the datatype to test if it can be updated + * @param to the dataType to that is the possible upgrade + * @return true if typically the from dataType can be reasonably upgraded to the to datatype + */ + protected boolean canUpgrade(DataType from, DataType to) { + if (from.getLength() != to.getLength()) { + return false; + } + + if (Undefined.isUndefined(from)) { + return true; + } + if (from instanceof AbstractIntegerDataType && to instanceof Pointer) { + return true; + } + if (from instanceof Pointer fromPointer && to instanceof Pointer toPointer) { + DataType pdt1 = fromPointer.getDataType(); + DataType pdt2 = toPointer.getDataType(); + return canUpgrade(pdt1, pdt2); + } + return false; + } + + protected void mergeDescription() { + String description1 = dt1.getDescription(); + String description2 = dt2.getDescription(); + String merged = join(description1, description2); + working.setDescription(merged); + } +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/EnumMerger.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/EnumMerger.java new file mode 100644 index 0000000000..4a8f1d4957 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/EnumMerger.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.program.database.data.merge; + +import ghidra.program.model.data.Enum; +import ghidra.program.model.data.EnumDataType; + +/** + * Datatype merger for Enums. + */ +public class EnumMerger extends DataTypeMerger { + + public EnumMerger(Enum enum1, Enum enum2) { + super(enum1, enum2); + } + + @Override + public void doMerge() throws DataTypeMergeException { + mergeSize(); + mergeDescription(); + + String[] names = other.getNames(); + for (String name : names) { + long value = other.getValue(name); + + if (!working.contains(name)) { + addValue(name, value, other.getComment(name)); + + } + else { + // current value must match + long currentResultValue = working.getValue(name); + if (value != currentResultValue) { + String msg = "Enums have different values for name \"%s\". %d and %d" + .formatted(name, currentResultValue, value); + error(msg); + } + + // otherwise join comments + String comment1 = working.getComment(name); + String comment2 = other.getComment(name); + working.remove(name); + working.add(name, value, join(comment1, comment2)); + } + + } + } + + private void addValue(String name, long value, String comment) throws DataTypeMergeException { + try { + working.add(name, value, other.getComment(name)); + } + catch (IllegalArgumentException e) { + error("Enum conflict: one enum has negative values: one has large unsigned values"); + } + } + + private void mergeSize() { + if (working.getLength() < other.getLength()) { + ((EnumDataType) working).setLength(other.getLength()); + } + } +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/StructureMerger.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/StructureMerger.java new file mode 100644 index 0000000000..8afc2aba8a --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/StructureMerger.java @@ -0,0 +1,233 @@ +/* ### + * 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.data.merge; + +import java.util.List; + +import ghidra.program.model.data.*; +import ghidra.util.exception.DuplicateNameException; + +/** + * DataType merger for structures. + */ +public class StructureMerger extends DataTypeMerger { + + public StructureMerger(Structure struct1, Structure struct2) { + super(struct1, struct2); + } + + @Override + public void doMerge() throws DataTypeMergeException { + checkSizes(); + mergeDescription(); + + if (working.isPackingEnabled()) { + mergePacked(); + } + else { + mergeUnpacked(); + } + } + + private void checkSizes() { + int resultLength = working.getLength(); + int otherLength = other.getLength(); + if (resultLength != otherLength) { + warning("Structures are not the same size."); + } + if (resultLength < otherLength) { + working.growStructure(otherLength - resultLength); + } + } + + private void mergeUnpacked() throws DataTypeMergeException { + if (other.isPackingEnabled()) { + warning("Merging packed structure into an unpacked structure."); + } + DataTypeComponent[] otherComponents = other.getDefinedComponents(); + + for (DataTypeComponent comp : otherComponents) { + DataTypeComponent workingComp = findCorrespondingResultComponent(comp); + if (workingComp != null) { + processFieldNames(workingComp, comp); + processComments(workingComp, comp); + } + else { + copyCompToWorking(comp); + } + } + } + + private void copyCompToWorking(DataTypeComponent comp) throws DataTypeMergeException { + int offset = comp.getOffset(); + int length = comp.getLength(); + + DataTypeComponent workingComp = working.getComponentAt(offset); + if (workingComp != null && workingComp.getDataType() != DataType.DEFAULT) { + // datatypes are different or else we would have handled in calling method + // so we can either merge them or we will throw an error + tryMergingDataTypes(workingComp, comp); + } + else if (hasUndefinedSpace(offset, length)) { + DataType dt = comp.getDataType(); + String name = comp.getFieldName(); + String comment = comp.getComment(); + working.replaceAtOffset(offset, dt, dt.getLength(), name, comment); + } + else if (working.getComponentAt(offset) == null) { + error("Conflict at offset " + offset + ". Existing component extends to this offset."); + } + else { + error("Conflict at offset " + offset + ". Not enough undefined bytes to insert here."); + } + } + + private void tryMergingDataTypes(DataTypeComponent workingComp, DataTypeComponent comp) + throws DataTypeMergeException { + DataType workingDt = workingComp.getDataType(); + DataType otherDt = comp.getDataType(); + + DataType mergedDt = pickBestTypeForMerge(workingDt, otherDt); + if (mergedDt == null) { + error("Conflict at offset " + comp.getOffset() + + ". Incompatible datatype already defined here."); + } + int offset = workingComp.getOffset(); + warning("Merging '%s' and '%s' at offset %d to '%s'.".formatted(workingDt.getName(), + otherDt.getName(), offset, mergedDt.getName())); + + processFieldNames(workingComp, comp); // checks for conflicts and handles null field names + String name = workingComp.getFieldName(); + String comment = join(workingComp.getComment(), comp.getComment()); + int length = workingComp.getLength(); + working.replaceAtOffset(offset, mergedDt, length, name, comment); + } + + private boolean hasUndefinedSpace(int offset, int length) { + for (int i = 0; i < length; i++) { + DataTypeComponent componentAt = working.getComponentAt(offset + i); + if (componentAt == null || componentAt.getDataType() != DataType.DEFAULT) { + return false; + } + } + return true; + } + + private DataTypeComponent findCorrespondingResultComponent(DataTypeComponent comp) { + int offset = comp.getOffset(); + List otherComps = other.getComponentsContaining(offset); + List workingComps = working.getComponentsContaining(offset); + + if (workingComps.isEmpty()) { + return null; + } + + if (workingComps.get(0).getDataType() == DataType.DEFAULT) { + return null; + } + + if (otherComps.size() == workingComps.size()) { + int index = otherComps.indexOf(comp); + DataTypeComponent workingComp = workingComps.get(index); + if (isSameComponent(comp, workingComp)) { + return workingComp; + } + } + return null; + } + + private boolean isSameComponent(DataTypeComponent comp, DataTypeComponent workingComp) { + if (comp.getOffset() != workingComp.getOffset()) { + return false; + } + if (!comp.getDataType().equals(workingComp.getDataType())) { + return false; + } + return true; + } + + private void mergePacked() throws DataTypeMergeException { + // merging packed is much more restricted. The only thing we are merging are defined + // field names against undefined field names and field comments. All component + // datatypes must match exactly. + + if (!working.isPackingEnabled()) { + error("Can't merge an unpacked structure into a packed structure"); + } + + DataTypeComponent[] otherComps = other.getComponents(); + DataTypeComponent[] workingComps = working.getComponents(); + if (otherComps.length != workingComps.length) { + error("Packed structures must have same size."); + } + for (int i = 0; i < otherComps.length; i++) { + DataTypeComponent fromComp = otherComps[i]; + DataTypeComponent workingComp = workingComps[i]; + checkDataType(workingComp, fromComp); + checkOffsets(workingComp, fromComp); + processFieldNames(workingComp, fromComp); + processComments(workingComp, fromComp); + } + } + + private void checkDataType(DataTypeComponent workingComp, DataTypeComponent comp) + throws DataTypeMergeException { + + DataType resultDt = workingComp.getDataType(); + DataType dt = comp.getDataType(); + if (!resultDt.equals(dt)) { + error("Packed components have conflicting datatypes at ordinal" + + workingComp.getOrdinal() + ", offset " + comp.getOffset()); + } + } + + private void checkOffsets(DataTypeComponent comp1, DataTypeComponent comp2) + throws DataTypeMergeException { + int offset1 = comp1.getOffset(); + int offset2 = comp2.getOffset(); + if (offset1 != offset2) { + error("Packed components have different offsets at ordinal " + comp1.getOrdinal() + + "struct1 = " + offset1 + ", struct2 = " + offset2); + } + } + + private void processFieldNames(DataTypeComponent workingComp, DataTypeComponent otherComp) + throws DataTypeMergeException { + String workingName = workingComp.getFieldName(); + String otherName = otherComp.getFieldName(); + if (workingName != null & otherName != null && !workingName.equals(otherName)) { + error( + "Components have conflicting field names at ordinal %d, offset %d. Names: %s vs %s" + .formatted(workingComp.getOrdinal(), workingComp.getOffset(), workingName, + otherName)); + } + if (workingName == null && otherName != null) { + try { + workingComp.setFieldName(otherName); + } + catch (DuplicateNameException e) { + // This exception is going away soon, so ignore it for now + } + } + } + + private void processComments(DataTypeComponent workingComp, DataTypeComponent otherComp) { + String resultComment = workingComp.getComment(); + String otherComment = otherComp.getComment(); + workingComp.setComment(join(resultComment, otherComment)); + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/UnionMerger.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/UnionMerger.java new file mode 100644 index 0000000000..a2883983d1 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/UnionMerger.java @@ -0,0 +1,130 @@ +/* ### + * 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.data.merge; + +import ghidra.program.model.data.*; +import ghidra.util.exception.DuplicateNameException; + +/** + * Datatype merger for Unions + */ +public class UnionMerger extends DataTypeMerger { + + public UnionMerger(Union union1, Union union2) { + super(union1, union2); + } + + @Override + public void doMerge() throws DataTypeMergeException { + mergeDescription(); + + DataTypeComponent[] components = other.getComponents(); + for (DataTypeComponent component : components) { + if (component.getFieldName() != null) { + processNamedComponent(component); + } + else { + processUnnamedComponent(component); + } + } + } + + private void processUnnamedComponent(DataTypeComponent component) { + DataTypeComponent[] existingComps = working.getComponents(); + DataType dt = component.getDataType(); + if (!hasComponentWithDatatype(existingComps, dt)) { + working.add(dt, component.getLength()); + } + } + + private boolean hasComponentWithDatatype(DataTypeComponent[] existingComps, DataType dt) { + for (DataTypeComponent component : existingComps) { + if (component.getDataType().equals(dt)) { + return true; + } + } + return false; + } + + private void processNamedComponent(DataTypeComponent component) throws DataTypeMergeException { + DataTypeComponent[] resultComps = working.getComponents(); + + DataTypeComponent resultComp = findByName(resultComps, component.getFieldName()); + if (resultComp != null) { + applySameNamedComponent(resultComp, component); + return; + } + + resultComp = findUnnamedByType(resultComps, component.getDataType()); + if (resultComp != null) { + appySameTypeComponent(resultComp, component); + } + working.add(component.getDataType(), component.getLength(), component.getFieldName(), + component.getComment()); + } + + private void appySameTypeComponent(DataTypeComponent resultComp, DataTypeComponent component) { + try { + resultComp.setFieldName(component.getFieldName()); + resultComp.setComment(join(resultComp.getComment(), component.getComment())); + } + catch (DuplicateNameException e) { + // can't happen, we already looked for a component with the same name + } + } + + private void applySameNamedComponent(DataTypeComponent comp1, DataTypeComponent comp2) + throws DataTypeMergeException { + DataType dt1 = comp1.getDataType(); + DataType dt2 = comp2.getDataType(); + if (dt1.equals(dt2)) { + comp1.setComment(join(comp1.getComment(), comp2.getComment())); + return; + } + + DataType mergedDt = pickBestTypeForMerge(dt1, dt2); + + if (mergedDt != null && comp2.getLength() == comp1.getLength()) { + int ordinal = comp1.getOrdinal(); + working.delete(ordinal); + String comment = join(comp1.getComment(), comp2.getComment()); + working.insert(ordinal, mergedDt, comp2.getLength(), comp2.getFieldName(), comment); + warning("Merging '%s' and '%s' to '%s' for member '%s'.".formatted(dt1.getName(), + dt2.getName(), mergedDt.getName(), comp1.getFieldName())); + return; + } + error("Unions have conflicting components named " + comp1.getFieldName()); + } + + private DataTypeComponent findByName(DataTypeComponent[] components, String name) { + for (DataTypeComponent component : components) { + if (name.equals(component.getFieldName())) { + return component; + } + } + return null; + } + + private DataTypeComponent findUnnamedByType(DataTypeComponent[] components, + DataType dataType) { + for (DataTypeComponent component : components) { + if (component.getFieldName() == null && component.getDataType().equals(dataType)) { + return component; + } + } + return null; + } +} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Enum.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Enum.java index 93044b293c..02d5622e9b 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Enum.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/Enum.java @@ -160,4 +160,7 @@ public interface Enum extends DataType { */ public int getMinimumPossibleLength(); + @Override + public Enum clone(DataTypeManager dtm); + } diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/EnumDataType.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/EnumDataType.java index 9aac8912b8..12c98d9bb5 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/EnumDataType.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/data/EnumDataType.java @@ -225,7 +225,7 @@ public class EnumDataType extends GenericDataType implements Enum { } @Override - public DataType clone(DataTypeManager dtm) { + public Enum clone(DataTypeManager dtm) { if (getDataTypeManager() == dtm) { return this; } @@ -545,4 +545,22 @@ public class EnumDataType extends GenericDataType implements Enum { public void pack() { setLength(getMinimumPossibleLength()); } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getPathName() + "\n"); + buf.append("\tDescription: " + getDescription()); + buf.append("\nValues: \n"); + for (String name : getNames()) { + buf.append("\t" + name + ": " + getValue(name)); + String comment = getComment(name); + if (comment != null) { + buf.append(" " + comment); + } + buf.append("\n"); + } + return buf.toString(); + } + } diff --git a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/EnumMergerTest.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/EnumMergerTest.java new file mode 100644 index 0000000000..717df34aca --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/EnumMergerTest.java @@ -0,0 +1,214 @@ +/* ### + * 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.data.merge; + +import static org.junit.Assert.*; + +import org.junit.*; + +import generic.test.AbstractGenericTest; +import ghidra.program.model.data.*; +import ghidra.program.model.data.Enum; +import ghidra.util.UniversalIdGenerator; + +public class EnumMergerTest extends AbstractGenericTest { + private StandAloneDataTypeManager dataTypeManager; + private int txId; + + @Before + public void setUp() throws Exception { + UniversalIdGenerator.initialize(); + dataTypeManager = new StandAloneDataTypeManager("Test"); + txId = dataTypeManager.startTransaction("Test"); + } + + @After + public void tearDown() { + dataTypeManager.endTransaction(txId, false); + } + + @Test + public void testSimpleMerge() throws Exception { + Enum enum1 = new EnumBuilder("A", 8) + .entry("joe", 5) + .buildDb(); + + Enum enum2 = new EnumBuilder("B", 8) + .entry("bob", 10) + .buildDb(); + + Enum expected = new EnumBuilder("A", 8) + .entry("bob", 10) + .entry("joe", 5) + .build(); + + EnumMerger merger = new EnumMerger(enum1, enum2); + Enum result = merger.merge(); + + assertEnumEquals(expected, result); + } + + @Test + public void testSameValueForDifferentNames() throws Exception { + Enum enum1 = new EnumBuilder("A", 8) + .entry("joe", 10) + .buildDb(); + + Enum enum2 = new EnumBuilder("B", 8) + .entry("bob", 10) + .buildDb(); + + Enum expected = new EnumBuilder("A", 8) + .entry("bob", 10) + .entry("joe", 10) + .build(); + + EnumMerger merger = new EnumMerger(enum1, enum2); + Enum result = merger.merge(); + + assertEnumEquals(expected, result); + } + + @Test + public void testValueCollision() throws Exception { + Enum enum1 = new EnumBuilder("A", 8) + .entry("joe", 5) + .buildDb(); + + Enum enum2 = new EnumBuilder("B", 8) + .entry("joe", 10) + .buildDb(); + + EnumMerger merger = new EnumMerger(enum1, enum2); + + try { + merger.merge(); + fail("Expected exception due to value conflict"); + } + catch (DataTypeMergeException e) { + assertEquals("Enums have different values for name \"joe\". 5 and 10", e.getMessage()); + } + } + + @Test + public void testSizeDifference() throws Exception { + Enum enum1 = new EnumBuilder("A", 4) + .entry("joe", 5) + .buildDb(); + + Enum enum2 = new EnumBuilder("B", 8) + .entry("bob", 10) + .buildDb(); + + Enum expected = new EnumBuilder("A", 8) + .entry("joe", 5) + .entry("bob", 10) + .build(); + + EnumMerger merger = new EnumMerger(enum1, enum2); + Enum result = merger.merge(); + + assertEnumEquals(expected, result); + } + + @Test + public void testSignedDifference() throws Exception { + Enum enum1 = new EnumBuilder("A", 1) + .entry("joe", -5) + .buildDb(); + + Enum enum2 = new EnumBuilder("B", 1) + .entry("bob", 255) + .buildDb(); + + EnumMerger merger = new EnumMerger(enum1, enum2); + try { + merger.merge(); + fail("Expected exception due to signedness conflict"); + } + catch (DataTypeMergeException e) { + assertEquals( + "Enum conflict due to one enum having negative values and the other large unsigned values", + e.getMessage()); + } + } + + @Test + public void testCommentMerge() throws Exception { + Enum enum1 = new EnumBuilder("A", 8) + .entry("aaa", 1, "hey") + .entry("bbb", 2) + .entry("ccc", 3, "hey") + .entry("ddd", 4, "hey") + .buildDb(); + + Enum enum2 = new EnumBuilder("B", 8) + .entry("aaa", 1, "hey") + .entry("bbb", 2, "hey") + .entry("ccc", 3) + .entry("ddd", 4, "there") + .buildDb(); + + Enum expected = new EnumBuilder("A", 8) + .entry("aaa", 1, "hey") + .entry("bbb", 2, "hey") + .entry("ccc", 3, "hey") + .entry("ddd", 4, "hey there") + .build(); + + EnumMerger merger = new EnumMerger(enum1, enum2); + Enum result = merger.merge(); + + assertEnumEquals(expected, result); + } + + private void assertEnumEquals(Enum expected, Enum actual) { + if (expected.equals(actual)) { + return; + } + String es = expected.toString(); + String as = actual.toString(); + String msg = "\nExpected: \n%s\nActual: \n%s".formatted(es, as); + fail(msg); + } + + private class EnumBuilder { + private Enum result; + + public EnumBuilder(String name, int size) { + result = new EnumDataType(CategoryPath.ROOT, name, size, dataTypeManager); + } + + public EnumBuilder entry(String name, long value) { + result.add(name, value); + return this; + } + + public EnumBuilder entry(String name, long value, String comment) { + result.add(name, value, comment); + return this; + } + + public Enum build() { + return result; + } + + public Enum buildDb() { + return (Enum) dataTypeManager.resolve(result, null); + } + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/StructureMergerTest.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/StructureMergerTest.java new file mode 100644 index 0000000000..ef74796220 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/StructureMergerTest.java @@ -0,0 +1,520 @@ +/* ### + * 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.data.merge; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.*; + +import generic.test.AbstractGenericTest; +import ghidra.program.model.data.*; +import ghidra.util.UniversalIdGenerator; + +public class StructureMergerTest extends AbstractGenericTest { + private DataType wordDt; + private DataType dwordDt; + private IntegerDataType intDt; + private StandAloneDataTypeManager dataTypeManager; + private int txId; + + @Before + public void setUp() throws Exception { + UniversalIdGenerator.initialize(); + wordDt = new WordDataType(); + dwordDt = new DWordDataType(); + intDt = new IntegerDataType(); + dataTypeManager = new StandAloneDataTypeManager("Test"); + txId = dataTypeManager.startTransaction("Test"); + } + + @After + public void tearDown() { + dataTypeManager.endTransaction(txId, false); + } + + @Test + public void testSimpleMerge() throws Exception { + Structure struct1 = new StructBuilder("A", 8) + .entry(0, wordDt, "joe") + .buildDb(); + + Structure struct2 = new StructBuilder("B", 8) + .entry(4, wordDt, "bob") + .buildDb(); + + Structure expected = new StructBuilder("A", 8) + .entry(0, wordDt, "joe") + .entry(4, wordDt, "bob") + .build(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + Structure result = merger.merge(); + + assertStructEquals(expected, result); + } + + @Test + public void testSimpleMerge_NoDb() throws Exception { + Structure struct1 = new StructBuilder("A", 8) + .entry(0, wordDt, "joe") + .build(); + + Structure struct2 = new StructBuilder("B", 8) + .entry(4, wordDt, "bob") + .build(); + + Structure expected = new StructBuilder("A", 8) + .entry(0, wordDt, "joe") + .entry(4, wordDt, "bob") + .build(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + Structure result = merger.merge(); + + assertStructEquals(expected, result); + } + + @Test + public void testNameCollision() { + Structure struct1 = new StructBuilder("A", 8) + .entry(4, wordDt, "joe") + .buildDb(); + + Structure struct2 = new StructBuilder("B", 8) + .entry(4, wordDt, "bob") + .buildDb(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + try { + merger.merge(); + fail("Expected error for name collision"); + } + catch (DataTypeMergeException e) { + assertEquals( + "Components have conflicting field names at ordinal 4, offset 4. Names: joe vs bob", + e.getMessage()); + } + } + + @Test + public void testMerge_DifferentSizes() throws Exception { + Structure struct1 = new StructBuilder("A", 4) + .entry(0, wordDt, "joe") + .build(); + + Structure struct2 = new StructBuilder("B", 12) + .entry(4, wordDt, "bob") + .build(); + + Structure expected = new StructBuilder("A", 12) + .entry(0, wordDt, "joe") + .entry(4, wordDt, "bob") + .build(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + + Structure result = merger.merge(); + assertStructEquals(expected, result); + + // expected a warning + List warnings = merger.getWarnings(); + assertEquals(1, warnings.size()); + assertEquals("Structures are not the same size.", warnings.get(0)); + } + + @Test + public void testOverlappingFields_otherInsertsIntoMiddleOfExisting() { + Structure struct1 = new StructBuilder("A", 8) + .entry(0, dwordDt, "joe") + .buildDb(); + + Structure struct2 = new StructBuilder("B", 8) + .entry(3, wordDt, "bob") + .buildDb(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + try { + merger.merge(); + fail("Expected error for offcut collision"); + } + catch (DataTypeMergeException e) { + assertEquals("Conflict at offset 3. Existing component extends to this offset.", + e.getMessage()); + } + } + + @Test + public void testOverlappingFields_NotEnoughRoom() { + Structure struct1 = new StructBuilder("A", 8) + .entry(3, dwordDt, "joe") + .buildDb(); + + Structure struct2 = new StructBuilder("B", 8) + .entry(0, dwordDt, "bob") + .buildDb(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + try { + merger.merge(); + fail("Expected error for offcut collision"); + } + catch (DataTypeMergeException e) { + assertEquals("Conflict at offset 0. Not enough undefined bytes to insert here.", + e.getMessage()); + } + } + + @Test + public void testDefinedFieldNameOverridesDefaultFieldName() throws DataTypeMergeException { + Structure struct1 = new StructBuilder("A", 8) + .entry(0, dwordDt, null) + .buildDb(); + + Structure struct2 = new StructBuilder("B", 8) + .entry(0, dwordDt, "bob") + .buildDb(); + + Structure expected = new StructBuilder("A", 8) + .entry(0, dwordDt, "bob") + .build(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + Structure result = merger.merge(); + assertStructEquals(expected, result); + } + + @Test + public void testCommentsAreCombined() throws DataTypeMergeException { + Structure struct1 = new StructBuilder("A", 8) + .entry(0, wordDt, "aaa") + .entry(2, wordDt, "bbb", "hey") + .entry(4, wordDt, "ccc", "hey") + .entry(6, wordDt, "ddd", "hey") + .buildDb(); + + Structure struct2 = new StructBuilder("B", 8) + .entry(0, wordDt, "aaa", "hey") + .entry(2, wordDt, "bbb") + .entry(4, wordDt, "ccc", "hey") + .entry(6, wordDt, "ddd", "there") + .buildDb(); + + Structure expected = new StructBuilder("A", 8) + .entry(0, wordDt, "aaa", "hey") + .entry(2, wordDt, "bbb", "hey") + .entry(4, wordDt, "ccc", "hey") + .entry(6, wordDt, "ddd", "hey there") + .build(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + Structure result = merger.merge(); + assertStructEquals(expected, result); + } + + @Test + public void testUpgradeFromUndefined4ToDWord() throws DataTypeMergeException { + Structure struct1 = new StructBuilder("A", 8) + .entry(0, new Undefined4DataType(), "bob", "aaa") + .buildDb(); + + Structure struct2 = new StructBuilder("B", 8) + .entry(0, dwordDt, "bob", "aaa") + .buildDb(); + + Structure expected = new StructBuilder("A", 8) + .entry(0, dwordDt, "bob", "aaa") + .build(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + Structure result = merger.merge(); + assertStructEquals(expected, result); + + // expected a warning + List warnings = merger.getWarnings(); + assertEquals(1, warnings.size()); + assertEquals("Merging 'undefined4' and 'dword' at offset 0 to 'dword'.", + warnings.get(0)); + } + + @Test + public void testUpgradeFromUndefined4ToDWord_otherDirection() throws DataTypeMergeException { + Structure struct1 = new StructBuilder("A", 8) + .entry(0, dwordDt, "bob", "aaa") + .buildDb(); + + Structure struct2 = new StructBuilder("B", 8) + .entry(0, new Undefined4DataType(), "bob", "aaa") + .buildDb(); + + Structure expected = new StructBuilder("A", 8) + .entry(0, dwordDt, "bob", "aaa") + .build(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + Structure result = merger.merge(); + assertStructEquals(expected, result); + + // expected a warning + List warnings = merger.getWarnings(); + assertEquals(1, warnings.size()); + assertEquals("Merging 'dword' and 'undefined4' at offset 0 to 'dword'.", + warnings.get(0)); + } + + @Test + public void testUpgradeFromDWordToPointer() throws DataTypeMergeException { + Structure struct1 = new StructBuilder("A", 8) + .entry(0, dwordDt, "bob", "aaa") + .buildDb(); + + PointerDataType pointer = new PointerDataType(intDt); + Structure struct2 = new StructBuilder("B", 8) + .entry(0, pointer, "bob", "aaa") + .buildDb(); + + Structure expected = new StructBuilder("A", 8) + .entry(0, pointer, "bob", "aaa") + .build(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + Structure result = merger.merge(); + assertStructEquals(expected, result); + // expected a warning + List warnings = merger.getWarnings(); + assertEquals(1, warnings.size()); + assertEquals("Merging 'dword' and 'int *' at offset 0 to 'int *'.", warnings.get(0)); + } + + @Test + public void testUpgradePointers() throws DataTypeMergeException { + Structure struct1 = new StructBuilder("A", 8) + .entry(0, new PointerDataType(new Undefined4DataType()), "bob", "aaa") + .buildDb(); + + PointerDataType pointer = new PointerDataType(intDt); + Structure struct2 = new StructBuilder("B", 8) + .entry(0, new PointerDataType(new IntegerDataType()), "bob", "aaa") + .buildDb(); + + Structure expected = new StructBuilder("A", 8) + .entry(0, new PointerDataType(new IntegerDataType()), "bob", "aaa") + .build(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + Structure result = merger.merge(); + assertStructEquals(expected, result); + // expected a warning + List warnings = merger.getWarnings(); + assertEquals(1, warnings.size()); + assertEquals("Merging 'undefined4 *' and 'int *' at offset 0 to 'int *'.", warnings.get(0)); + } + + @Test + public void testPackedStructureSameExceptFieldName() throws DataTypeMergeException { + Structure struct1 = new StructBuilder("A", 8) + .entry(0, dwordDt, null) + .pack() + .buildDb(); + + Structure struct2 = new StructBuilder("B", 8) + .entry(0, dwordDt, "bob") + .pack() + .buildDb(); + + Structure expected = new StructBuilder("A", 8) + .entry(0, dwordDt, "bob") + .pack() + .build(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + Structure result = merger.merge(); + assertStructEquals(expected, result); + } + + @Test + public void testPackedStructureDifferentSize() { + Structure struct1 = new StructBuilder("A", 8) + .entry(0, dwordDt, null) + .pack() + .buildDb(); + + Structure struct2 = new StructBuilder("B", 8) + .entry(0, dwordDt, "bob") + .entry(4, dwordDt, "joe") + .pack() + .buildDb(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + try { + merger.merge(); + fail("Expected failure due to different sized packed structures"); + } + catch (DataTypeMergeException e) { + assertEquals( + "Packed structures must have same size.", + e.getMessage()); + } + } + + @Test + public void testMergingPackedIntoUnpacked() throws DataTypeMergeException { + Structure struct1 = new StructBuilder("A", 8) + .entry(4, dwordDt, "joe") + .buildDb(); + + Structure struct2 = new StructBuilder("B", 8) + .entry(0, dwordDt, "bob") + .pack() + .buildDb(); + + Structure expected = new StructBuilder("A", 8) + .entry(0, dwordDt, "bob") + .entry(4, dwordDt, "joe") + .build(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + Structure result = merger.merge(); + assertStructEquals(expected, result); + } + + @Test + public void testMergeCyclic() throws Exception { + Structure struct1 = new StructBuilder("A", 16) + .entry(0, wordDt, "joe") + .buildDb(); + + Structure struct2 = new StructBuilder("B", 16) + .entry(4, wordDt, "bob") + .entry(10, new PointerDataType(struct1), "ptr2") + .buildDb(); + struct1.replaceAtOffset(6, new PointerDataType(struct2), 6, "ptr1", null); + + Structure expected = new StructBuilder("A", 16) + .entry(0, wordDt, "joe") + .entry(4, wordDt, "bob") + .build(); + expected.replaceAtOffset(6, new PointerDataType(expected), 4, "ptr1", null); + expected.replaceAtOffset(10, new PointerDataType(expected), 4, "ptr2", null); + + StructureMerger merger = new StructureMerger(struct1, struct2); + Structure result = merger.merge(); + + // we need to do the replace with to complete the cycle + struct1.replaceWith(result); + dataTypeManager.replaceDataType(struct2, struct1, false); + assertStructEquals(expected, struct1); + } + + @Test + public void testMergeSameDescription() throws DataTypeMergeException { + Structure struct1 = new StructBuilder("A", 16) + .entry(0, wordDt, "joe") + .description("Hi") + .buildDb(); + + Structure struct2 = new StructBuilder("B", 16) + .entry(4, wordDt, "bob") + .description("Hi") + .buildDb(); + + Structure expected = new StructBuilder("A", 16) + .entry(0, wordDt, "joe") + .entry(4, wordDt, "bob") + .description("Hi") + .build(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + Structure result = merger.merge(); + + assertStructEquals(expected, result); + assertEquals("Hi", expected.getDescription()); + } + + @Test + public void testMergeDifferentDescription() throws DataTypeMergeException { + Structure struct1 = new StructBuilder("A", 16) + .entry(0, wordDt, "joe") + .description("Hi") + .buildDb(); + + Structure struct2 = new StructBuilder("B", 16) + .entry(4, wordDt, "bob") + .description("There") + .buildDb(); + + Structure expected = new StructBuilder("A", 16) + .entry(0, wordDt, "joe") + .entry(4, wordDt, "bob") + .description("Hi There") + .build(); + + StructureMerger merger = new StructureMerger(struct1, struct2); + Structure result = merger.merge(); + + assertStructEquals(expected, result); + assertEquals("Hi There", expected.getDescription()); + } + + private void assertStructEquals(Structure expected, Structure actual) { + if (expected.equals(actual)) { + return; + } + + String es = expected.toString(); + String as = actual.toString(); + String msg = "\nExpected: \n%s\nActual: \n%s".formatted(es, as); + fail(msg); + } + + private class StructBuilder { + Structure result; + + public StructBuilder(String name, int size) { + result = new StructureDataType(name, size, dataTypeManager); + } + + public StructBuilder entry(int offset, DataType dt, String name) { + result.replaceAtOffset(offset, dt, -1, name, null); + return this; + } + + public StructBuilder entry(int offset, DataType dt, String name, String comment) { + result.replaceAtOffset(offset, dt, -1, name, comment); + return this; + } + + public StructBuilder description(String description) { + result.setDescription(description); + return this; + } + + public Structure build() { + return result; + } + + public StructBuilder pack() { + result.setPackingEnabled(true); + return this; + } + + public Structure buildDb() { + return (Structure) dataTypeManager.resolve(result, null); + } + } + +} diff --git a/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/UnionMergerTest.java b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/UnionMergerTest.java new file mode 100644 index 0000000000..4ea6d510c6 --- /dev/null +++ b/Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/UnionMergerTest.java @@ -0,0 +1,243 @@ +/* ### + * 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.data.merge; + +import static org.junit.Assert.*; + +import org.junit.*; + +import generic.test.AbstractGenericTest; +import ghidra.program.model.data.*; +import ghidra.util.UniversalIdGenerator; + +public class UnionMergerTest extends AbstractGenericTest { + private DataType wordDt; + private DataType dwordDt; + private IntegerDataType intDt; + private StandAloneDataTypeManager dataTypeManager; + private int txId; + + @Before + public void setUp() throws Exception { + UniversalIdGenerator.initialize(); + wordDt = new WordDataType(); + dwordDt = new DWordDataType(); + intDt = new IntegerDataType(); + dataTypeManager = new StandAloneDataTypeManager("Test"); + txId = dataTypeManager.startTransaction("Test"); + } + + @After + public void tearDown() { + dataTypeManager.endTransaction(txId, false); + } + + @Test + public void testSimpleMerge() throws Exception { + Union union1 = new UnionBuilder("A", 8) + .entry(wordDt, "joe") + .buildDb(); + + Union union2 = new UnionBuilder("B", 8) + .entry(wordDt, "bob") + .buildDb(); + + Union expected = new UnionBuilder("A", 8) + .entry(wordDt, "joe") + .entry(wordDt, "bob") + .build(); + + UnionMerger merger = new UnionMerger(union1, union2); + Union result = merger.merge(); + + assertUnionEquals(expected, result); + } + + @Test + public void testMergeWithAdditionalEntry() throws Exception { + Union union1 = new UnionBuilder("A", 8) + .entry(wordDt, "joe") + .buildDb(); + + Union union2 = new UnionBuilder("B", 8) + .entry(wordDt, "bob") + .entry(wordDt, "joe", "hey") + .buildDb(); + + Union expected = new UnionBuilder("A", 8) + .entry(wordDt, "joe", "hey") + .entry(wordDt, "bob") + .build(); + + UnionMerger merger = new UnionMerger(union1, union2); + Union result = merger.merge(); + + assertUnionEquals(expected, result); + } + + @Test + public void testConflictEntry() throws Exception { + Union union1 = new UnionBuilder("A", 8) + .entry(wordDt, "joe") + .buildDb(); + + Union union2 = new UnionBuilder("B", 8) + .entry(dwordDt, "joe") + .buildDb(); + + UnionMerger merger = new UnionMerger(union1, union2); + + try { + merger.merge(); + fail("Expected exception"); + } + catch (DataTypeMergeException e) { + assertEquals("Unions have conflicting components named joe", e.getMessage()); + } + } + + @Test + public void testMergeComments() throws Exception { + Union union1 = new UnionBuilder("A", 8) + .entry(wordDt, "aaa") + .entry(wordDt, "bbb", "hey") + .entry(wordDt, "ccc", "hey") + .entry(wordDt, "ddd", "hey") + .buildDb(); + + Union union2 = new UnionBuilder("B", 8) + .entry(wordDt, "aaa", "hey") + .entry(wordDt, "bbb") + .entry(wordDt, "ccc", "hey") + .entry(wordDt, "ddd", "there") + .buildDb(); + + Union expected = new UnionBuilder("A", 8) + .entry(wordDt, "aaa", "hey") + .entry(wordDt, "bbb", "hey") + .entry(wordDt, "ccc", "hey") + .entry(wordDt, "ddd", "hey there") + .build(); + + UnionMerger merger = new UnionMerger(union1, union2); + Union result = merger.merge(); + + assertUnionEquals(expected, result); + } + + @Test + public void testUpgradeFromUndefined4ToDWord() throws DataTypeMergeException { + Union union1 = new UnionBuilder("A", 8) + .entry(new Undefined4DataType(), "joe") + .buildDb(); + + Union union2 = new UnionBuilder("B", 8) + .entry(intDt, "joe") + .buildDb(); + + Union expected = new UnionBuilder("A", 8) + .entry(intDt, "joe") + .build(); + + UnionMerger merger = new UnionMerger(union1, union2); + + Union result = merger.merge(); + assertUnionEquals(expected, result); + assertEquals("Merging \"undefined4\" and \"int to \"int\" for member \"joe\".", + merger.getWarnings().get(0)); + } + + @Test + public void testUpgradeFromUndefined4ToDWord_otherDirection() throws DataTypeMergeException { + Union union1 = new UnionBuilder("A", 8) + .entry(intDt, "joe") + .buildDb(); + + Union union2 = new UnionBuilder("B", 8) + .entry(new Undefined4DataType(), "joe") + .buildDb(); + + Union expected = new UnionBuilder("A", 8) + .entry(intDt, "joe") + .build(); + + UnionMerger merger = new UnionMerger(union1, union2); + + Union result = merger.merge(); + assertUnionEquals(expected, result); + assertEquals("Merging \"int\" and \"undefined4 to \"int\" for member \"joe\".", + merger.getWarnings().get(0)); + } + + @Test + public void testUpgradeFromintToPointer() throws DataTypeMergeException { + Union union1 = new UnionBuilder("A", 8) + .entry(intDt, "joe") + .buildDb(); + + Union union2 = new UnionBuilder("B", 8) + .entry(new PointerDataType(wordDt), "joe") + .buildDb(); + + Union expected = new UnionBuilder("A", 8) + .entry(new PointerDataType(wordDt), "joe") + .build(); + + UnionMerger merger = new UnionMerger(union1, union2); + + Union result = merger.merge(); + assertUnionEquals(expected, result); + assertEquals("Merging \"int\" and \"word * to \"word *\" for member \"joe\".", + merger.getWarnings().get(0)); + } + + private void assertUnionEquals(Union expected, Union actual) { + if (expected.equals(actual)) { + return; + } + String es = expected.toString(); + String as = actual.toString(); + String msg = "\nExpected: \n%s\nActual: \n%s".formatted(es, as); + fail(msg); + } + + private class UnionBuilder { + private Union result; + + public UnionBuilder(String name, int size) { + result = new UnionDataType(null, name, dataTypeManager); + } + + public UnionBuilder entry(DataType dt, String name) { + result.add(dt, -1, name, null); + return this; + } + + public UnionBuilder entry(DataType dt, String name, String comment) { + result.add(dt, -1, name, comment); + return this; + } + + public Union build() { + return result; + } + + public Union buildDb() { + return (Union) dataTypeManager.resolve(result, null); + } + } + +} diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeManagerPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeManagerPluginScreenShots.java index 586b38c9be..58b9e4ecd5 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeManagerPluginScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeManagerPluginScreenShots.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. @@ -26,6 +26,7 @@ import org.junit.Test; import docking.*; import docking.action.DockingActionIf; +import docking.widgets.DropDownSelectionTextField; import docking.widgets.table.GTable; import docking.widgets.table.GTableCellRenderer; import docking.widgets.tree.GTree; @@ -140,6 +141,59 @@ public class DataTypeManagerPluginScreenShots extends GhidraScreenShotGenerator captureIsolatedProvider(DataTypesProvider.class, 500, 400); } + @Test + public void testMergeConfirmationDialog() { + createStructure("foo", 0, new IntegerDataType(), "aaa", 12); + createStructure("bar", 4, new FloatDataType(), "bbb", 16); + + DataTypesProvider provider = getProvider(DataTypesProvider.class); + GTree tree = (GTree) getInstanceField("archiveGTree", provider); + GTreeNode rootNode = tree.getViewRoot(); + GTreeNode child = rootNode.getChild("WinHelloCPP.exe"); + tree.expandPath(child); + GTreeNode dtNode = child.getChild("foo"); + tree.addSelectionPath(dtNode.getTreePath()); + performAction("Merge Data Types", "DataTypeManagerPlugin", provider, false); + DialogComponentProvider dialog = getDialog(); + DropDownSelectionTextField textField = + findComponent(dialog, DropDownSelectionTextField.class); + runSwing(() -> textField.setText("bar")); + pressOkOnDialog(); + captureDialog(); + pressButtonOnDialog("Cancel"); + } + + @Test + public void testMergeErrorDialog() { + createStructure("foo", 0, new IntegerDataType(), "aaa", 12); + createStructure("bar", 0, new FloatDataType(), "bbb", 16); + + DataTypesProvider provider = getProvider(DataTypesProvider.class); + GTree tree = (GTree) getInstanceField("archiveGTree", provider); + GTreeNode rootNode = tree.getViewRoot(); + GTreeNode child = rootNode.getChild("WinHelloCPP.exe"); + tree.expandPath(child); + GTreeNode dtNode = child.getChild("foo"); + tree.addSelectionPath(dtNode.getTreePath()); + performAction("Merge Data Types", "DataTypeManagerPlugin", provider, false); + DialogComponentProvider dialog = getDialog(); + DropDownSelectionTextField textField = + findComponent(dialog, DropDownSelectionTextField.class); + runSwing(() -> textField.setText("bar")); + pressOkOnDialog(); + captureDialog(); + pressButtonOnDialog("OK"); + } + + private void createStructure(String name, int offset, DataType dt, String fieldName, int size) { + ProgramBasedDataTypeManager dtm = program.getDataTypeManager(); + Structure struct = new StructureDataType(name, size); + struct.replaceAtOffset(offset, dt, dt.getLength(), fieldName, null); + program.withTransaction("test", () -> { + dtm.addDataType(struct, null); + }); + } + @Test public void testFindDataTypes() { performAction("Find Data Types", "DataTypeManagerPlugin", false);