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