From 4998e981b787903c9daf034a139d5bbd4e3508d2 Mon Sep 17 00:00:00 2001 From: ghidragon <106987263+ghidragon@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:22:23 -0500 Subject: [PATCH] ng utility class for merging structures, unions and enum. --- Ghidra/Features/Base/certification.manifest | 2 + .../data_type_manager_description.htm | 44 +- .../data_type_manager_window.html | 10 +- .../images/MergeConfirmationDialog.png | Bin 0 -> 19846 bytes .../images/MergeErrorDialog.png | Bin 0 -> 13096 bytes .../core/datamgr/DataTypesProvider.java | 1 + .../actions/AbstractDataTypeMergeDialog.java | 175 ++++++ .../DataTypeMergeConfirmationDialog.java | 68 +++ .../actions/DataTypeMergeErrorDialog.java | 42 ++ .../datamgr/actions/MergeDataTypeAction.java | 214 +++++++ .../actions/ReplaceDataTypeAction.java | 7 +- .../database/data/DataTypeUtilities.java | 44 ++ .../ghidra/program/database/data/EnumDB.java | 22 +- .../data/merge/DataTypeMergeException.java | 25 + .../database/data/merge/DataTypeMerger.java | 183 ++++++ .../database/data/merge/EnumMerger.java | 76 +++ .../database/data/merge/StructureMerger.java | 233 ++++++++ .../database/data/merge/UnionMerger.java | 130 +++++ .../java/ghidra/program/model/data/Enum.java | 3 + .../program/model/data/EnumDataType.java | 20 +- .../database/data/merge/EnumMergerTest.java | 214 +++++++ .../data/merge/StructureMergerTest.java | 520 ++++++++++++++++++ .../database/data/merge/UnionMergerTest.java | 243 ++++++++ .../DataTypeManagerPluginScreenShots.java | 58 +- 24 files changed, 2321 insertions(+), 13 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/images/MergeConfirmationDialog.png create mode 100644 Ghidra/Features/Base/src/main/help/help/topics/DataTypeManagerPlugin/images/MergeErrorDialog.png create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/AbstractDataTypeMergeDialog.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/DataTypeMergeConfirmationDialog.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/DataTypeMergeErrorDialog.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/actions/MergeDataTypeAction.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/DataTypeMergeException.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/DataTypeMerger.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/EnumMerger.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/StructureMerger.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/merge/UnionMerger.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/EnumMergerTest.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/StructureMergerTest.java create mode 100644 Ghidra/Framework/SoftwareModeling/src/test/java/ghidra/program/database/data/merge/UnionMergerTest.java 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 0000000000000000000000000000000000000000..18af9b130324a7c46887790382382dc092d74be9 GIT binary patch literal 19846 zcmdtK2UL^W+AfN{prS~TqNosR0O=hTC}0RpdXG{<6OfLf!~!TFAOS)zN+jwbG~!F?Ri@Q)KwL)UZ%fHMn-lO^7xSk z8QB?sGO|+-FP;Y8!DHUJkddjVKpx3zd6+CGNWEwHcJNI*RXxmP{Fl@Pl88Q|VV7wv}e8Cl2Pv5?mo%OSl>2K|iu?jUW?sY`Fr zGnbMccZu}o)2#PgV~BEU0}aRYs(iOG#_el_gBp>V?Oq+cLY@n=W=$OC$Rs?X{_Vq| z8mB6~ikr`+a+Zz-YS>9nlQpi$7#F6PLKY8*DoEernqK?ECk~SA+AHQkGL{X|1BCuE z;^_FXt>@yiTp!*aK6-e`d221wlhJ4RKAS2&gf7_kZc^;mxYw*Kx^0yytwkAag+5=F zP2V&u^$>v_)o2`*=v9R0kGAx$SQ<(MkB=l8lGs4@$CY00tljPbRjdndTx|@;7{vl% zy}!1oM0amtQEpAObgfo-I#hYjo^=OWuw_LKC9#uigFhT(Cv81vZytm^8_8BNJe|B& zZiF<>yG>mH;u*aDm$j}^);TU4y{JHY8Q1UJnf}8BQ&?z}atp zJt0Nl7#*$aOU#jOVvz=1!^z+fgOg3@OHib@Vj zRhVj1Qqy~RgcZc(i|Ki+oeqBQfKFF3omG_|1PLYydW$iOIzSdF=!-@X!y1NZC5Zkd zYdqW0zv=Sh)~`9y>owMWL@=`(N>h%|FZuJc~=<913oFIyd|Rm}dN zTW1#8*o15xj^1B1-FxP^7&pifpF%T9aim{{+gJ-eorbW60*{gW7jVb4mCb6y@_qLP-Il0Mfmt+uLES$0lb zQ7WHbd2lyci~R%U#WH+NLG;Fm%taNu51L>eGA)3lfIaB6B6xf9VjKA!Vj&coPB*Mwh(VI zJ4R@B`nzw$dmB1m%L8Qa`10#}m~HV7_(9oPH;47d_V>K9KYeCMW<_x)T_*d}Y|?Xu zZ?@e;t{4=FbtVwz#>X@WJ~(HQAC+O;6IZjp`|!YP}129imX1dsS_8uu`SOHKe|&CEyhT z#Z~awcySr7(RS(fnj8xeETkADg_w}=8N5?HydcvqM6iU0u5X1lE)PboVBo~3wFl`H zM>u*w_zl3wW^L&ihHsM;a{WoOt!4ZbMoqcr$^L9d(Fdf1`a_|CM<~sX@wn9scrBkc z)Pg0X{sNa&7pfAt+5oR<6P2mJ1FDp)dDh5ot&K2*f&gnquo=YFI{R#b=$5_)2F8CH zbuVW#nU@2;(Kl-6;z>rv*hkm088WO{8RMOKSQ$U$ZRZQRU7d3^De|3!Uw5w&5t6o5 zuH_l)P?JT#*%FR>hkU9_wj4^1v-I{ND2{$&lGnvEtJ@-porA}my7sgJ6Hg(h?!LjJ zH#gJPSC7`>=Es@JxUEIsip~ammR-NuAN^+ExMyU+9iqnIc3riq6<0Y^msNO^D6;K0 zja_cDS5}QzBPM20P$Q-DlN3 zl=|VL!T_H${~k*kYuTWWlIZK~!N$o}L6++OIl?NFG*)kNKp;~{Yb za(hd>T57ZC$x*)E-mT;7)*jQx&_X_TKlkMcYEF0l$ky)}u5q?}3g#d)Qu)k{@2)le+FE&Jjw!l%%xJw_uIC+xktFvW{w6mK zbzJm*cJL&9O)<-E)aNvr-W>p2>>nZ4;Jf}G%f)xByRT;K+wS(gJ}mJuW&%)wZumtg zBGV{quNQG)lBlGM)Z>WrGWFzYV6XIHvkq3&&*BL#Ma*#pK>bqsuto;z>%Y*rKApTReMpGShOI|-RrI_|Mi^yEc11Z*vz+mvBKgee`AhRTnc7r7Ok^?D|WS5MZWyzmSD z*q;A?>2;1@uSc16nw3QG9|n4PAgO&XDkJjqunXd8P8px0K*ps?cia_8w3ds52T_+J zMdLX;%Y3@E8BGmKs zPlKl*5^ulQ=!Jo(Dd^9>M)*A*wG@uiE?cKIy->wlZz)b8!>F^Om2RMH+izT;PMyn%wCn zB-;0bJvm_Qg<7dSjTbN?EBy!BD?Exiy~V{3 zJ@k̼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 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);